微服务架构设计实战:从单体到分布式
上周五晚上11点,公司办公室只剩我一个人。窗外下着小雨,咖啡已经凉透,屏幕上的 docker-compose up 正在疯狂输出日志。这场景简直是我入职这两个月的缩影——白天跟产品经理扯需求边界,晚上偷偷重构老系统。说真的,刚来这家公司时我以为只是个 CRUD 项目维护工,结果没想到两个月就卷进了“微服务改造”这个深水炸弹。
被逼上梁山:为什么我们要拆单体?
我们原来的系统是个典型的 JavaScript 单体应用(Node.js + Express),前后端没分离,数据库是 MySQL,部署方式是直接扔到 ECS 上跑。听起来是不是有点像十年前的架构?但说实话,在业务量不大的时候它跑得还挺稳。
直到去年双11前夕,运营同学突然搞了个“裂变拉新”活动,用户量一夜之间涨了5倍。我们的 API 延迟直接飙到3秒+,数据库连接池爆满,最离谱的是有一次订单服务把整个用户模块拖垮了——因为它们共用同一个进程和数据库连接。
运维大哥当时在群里吼:“你们能不能别在一个进程里干这么多事?!”
测试妹子补刀:“连个独立的测试环境都没有,每次上线我都怕。”
而产品经理只问了一句:“啥时候能好?”
那一刻我意识到:再不拆,迟早被线上事故搞失业。
技术选型:JS 还是 Go?成年人不做选择
作为一个深夜写代码效率更高的“夜猫子程序员”,我对性能特别敏感。老系统用的是 JavaScript(准确说是 TypeScript),开发体验确实爽,类型安全、生态丰富、调试方便。但一到高并发场景,V8 的 GC 停顿和单线程模型就成了瓶颈。
于是我和后端组的老王(一个写了十年 Go 的老炮)开了个小会。他甩给我一句灵魂拷问:“你是要开发快,还是要跑得快?”
最终我们定了个混合方案:
- 核心交易链路(订单、支付、库存)用 Go 重写,追求极致性能和稳定性
- 非核心业务(用户资料、通知、活动配置)保留 JavaScript/TypeScript,快速迭代
有人可能会说“技术栈不统一很麻烦”。但现实是:没有银弹。我们不是 Google,没必要为了“统一”而牺牲效率。况且现在 DevOps 工具链这么成熟,多语言部署早就不是问题。
拆分策略:别一上来就“微”到骨子里
很多新人一听说微服务,立马想拆成20个服务,每个服务只干一件事。结果呢?服务间调用复杂度爆炸,链路追踪变成噩梦,本地开发环境搭三天都跑不起来。
我们采取的是渐进式拆分:
- 先垂直拆分:按业务域切,比如用户中心、商品中心、订单中心
- 再水平优化:对高频接口做缓存、异步化、限流
- 最后治理:加监控、日志聚合、熔断降级
举个具体例子:原来的 /api/order/create 接口,内部要查用户、查库存、扣库存、生成订单、发消息……全在一个函数里。现在拆成:
user-service:提供用户信息(TS)inventory-service:处理库存扣减(Go)order-service:创建订单(Go)notification-service:发短信/邮件(TS)
服务间通信用 gRPC(Go 服务之间)和 REST(TS 服务 or 跨语言调用),消息队列用 RabbitMQ 解耦异步任务。
关键代码片段:服务通信与错误处理
Go 服务间 gRPC 调用(带超时和重试)
// inventory_client.go
func (c *InventoryClient) DeductStock(ctx context.Context, skuID string, qty int) error {
// 设置超时,防止雪崩
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
req := &pb.DeductRequest{
SkuId: skuID,
Qty: int32(qty),
}
// 加上简单重试逻辑(生产环境建议用 retryablehttp 或 circuit breaker)
for i := 0; i < 2; i++ {
_, err := c.client.DeductStock(ctx, req)
if err == nil {
return nil
}
log.Printf("DeductStock failed on attempt %d: %v", i+1, err)
time.Sleep(50 * time.Millisecond)
}
return fmt.Errorf("deduct stock failed after retries")
}
JavaScript 服务调用 Go 服务(REST + Circuit Breaker)
// order-service/src/utils/inventory.ts
import axios from 'axios';
import { CircuitBreaker } from 'opossum';
const inventoryAxios = axios.create({
baseURL: process.env.INVENTORY_SERVICE_URL,
timeout: 1000, // 1秒超时
});
// 熔断器:失败5次就断开,30秒后尝试恢复
const breaker = new CircuitBreaker(inventoryAxios.post, {
timeout: 1200,
errorThresholdPercentage: 50,
resetTimeout: 30000,
});
export const deductStock = async (skuId: string, qty: number) => {
try {
await breaker.fire('/stock/deduct', { skuId, qty });
} catch (err) {
if (breaker.halfOpen) {
console.warn('Circuit breaker is half-open, proceeding with caution');
}
throw new Error(`Inventory service unavailable: ${err.message}`);
}
};
数据库设计:别让“分布式事务”变成你的噩梦
微服务最头疼的不是代码,是数据一致性。我们坚决不用两阶段提交(2PC)——太重,性能差,还容易卡死。
解决方案:Saga 模式 + 补偿事务
比如创建订单流程:
- order-service 创建“待支付”订单(状态=PENDING)
- 调用 inventory-service 扣库存
- 如果扣库存失败 → order-service 主动把订单状态改为 CANCELLED
- 如果后续支付失败 → 发送“回滚库存”消息,inventory-service 收到后加回库存
关键点:所有操作必须幂等!
我们在数据库表里加了 request_id 字段,配合唯一索引,确保同一个请求不会重复执行:
-- inventory_stock_changes 表
CREATE TABLE inventory_stock_changes (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
request_id VARCHAR(64) NOT NULL UNIQUE, -- 幂等键
sku_id VARCHAR(32) NOT NULL,
delta INT NOT NULL, -- 正数为加,负数为减
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
性能对比:拆完真香?
上线两周后,我们做了压测对比(模拟双11流量):
| 指标 | 单体架构 | 微服务架构 |
|---|---|---|
| P99 延迟 | 2850ms | 320ms |
| 错误率 | 8.7% | 0.3% |
| CPU 使用率(峰值) | 95% | 62% |
| 部署粒度 | 整个应用 | 单个服务 |
| 故障隔离 | 无(全挂) | 仅影响单服务 |
最让我开心的是:现在改用户头像功能,再也不用担心把支付系统搞崩了。运维也终于能睡个好觉——因为我们可以单独扩缩容订单服务,而不必把整个应用实例翻倍。
血泪教训:那些没人告诉你的坑
本地开发环境地狱:微服务多了,本地启动一堆依赖太痛苦。解决方案:用 Docker Compose + Makefile 封装启动命令,或者直接连测试环境(打上 dev 标签)
日志分散:以前查 bug 只要看一个日志文件,现在要 grep 十几个服务。赶紧上了 ELK(Elasticsearch + Logstash + Kibana),加 trace_id 串联请求链路
配置管理混乱:每个服务一堆 env 文件?我们迁移到了 Consul + 配置中心,支持动态刷新
网络抖动放大故障:服务 A 调 B,B 调 C,C 超时 2 秒,A 的超时设 1 秒 → 直接级联失败。务必遵守超时传递原则:下游超时 < 上游超时
TypeScript 和 Go 的 JSON 兼容性:Go 的
omitempty和 TS 的 optional 字段经常对不上。最后统一约定:所有字段显式定义,null 就是 null,不要省略
最后一点心里话
微服务不是银弹,它解决的是规模化协作和高可用性问题,而不是“我的代码不够酷”。如果你的团队只有3个人,业务日活不到1万,老老实实用单体吧——别为了简历好看硬上微服务。
但如果你像我一样,被线上事故逼到凌晨三点还在修 bug,那拆分可能是你唯一的出路。
现在每次看到监控面板上平稳的曲线,我就觉得这两个月熬的夜值了。虽然产品经理还是天天催新需求,但至少我不用再担心改一行代码就让整个公司停摆。
对了,刚收到消息:下周要开始拆支付模块了。Go 写支付,想想就刺激。咖啡续上,今晚继续肝。
P.S. 有朋友问工具链,简单列一下我们用的:
- 服务注册发现:Consul
- API 网关:Kong
- 链路追踪:Jaeger
- 消息队列:RabbitMQ(后面可能换 Kafka)
- 容器编排:K8s(测试环境用 Docker Compose)
- 监控:Prometheus + Grafana
如果你也在经历类似改造,欢迎留言交流——尤其是那些踩过的坑,咱们一起避雷。

评论 0