从单体到微服务:一个双非学生的实战血泪史
大家好,我是小林,某双非院校大二在读(别问,问就是自学编程两年半的老油条),目前在一家百来人的电商创业公司打杂。白天写业务代码,晚上刷 LeetCode 准备跳槽——没错,就是那种“边搬砖边自救”的典型打工人状态。
最近我们组接了个硬核需求:把老掉牙的单体系统拆成微服务,老板说“双11前必须上线”,产品还画了个“全球秒杀架构图”给我看,我当时差点一口老血喷在 MacBook Pro 的键盘上。更离谱的是,运维大哥还在群里@我说:“小林啊,Go 服务部署记得打静态编译包,不然我半夜不想爬起来救火。”
行吧,既然躲不过,那就干!这篇技术分享,就聊聊我这一个月怎么从“单体狗”变成“微服务民工”的实战经历,顺便整理点面试题和踩坑记录,给同样想跳槽、搞分布式系统的兄弟们避避雷。
起因:那个要命的“单体巨兽”
我们的老系统是用 Python + Django 写的,三年没大重构,代码库已经膨胀到 50w+ 行。用户下单、库存扣减、优惠券核销、物流推送……全塞在一个项目里。每次改个小功能,CI/CD 跑半小时,测试同学一看到我的 PR 就叹气:“又来了?”
最要命的是去年双11,订单服务一崩,整个站都挂了。运维查日志发现是库存模块死锁,但因为所有逻辑耦合在一起,根本没法隔离故障。老板震怒:“必须拆!谁不拆谁走人!”——于是这个“光荣任务”落到了我头上(可能因为我简历写了“熟悉分布式系统”?其实是 B 站看多了)。
技术选型:Go 上场,Python 退居二线
虽然我主力语言是 Python,但考虑到高并发和部署轻量化,新微服务果断选了 Go。理由很现实:
- 编译成二进制,扔服务器上直接跑,不用装 Python 环境(运维狂喜)
- Goroutine 天然适合高并发场景(比如秒杀)
- 生态成熟:gRPC、etcd、Go-kit 都很稳
不过老系统还是得兼容,所以定了个混合架构:
- 新业务(如订单创建、库存服务)用 Go
- 旧接口保留 Python,逐步迁移
- 通信协议统一用 gRPC(HTTP/JSON 留给前端)
自嘲一下:当初学 Go 就是为了跳槽,结果被逼着实战,果然“学以致用”才是最快的提升方式。
拆分策略:不是随便切一刀就行
很多人以为微服务就是“把代码按功能拆成几个 repo”,Too young!真正难的是边界划分和数据一致性。
我们参考了《领域驱动设计》(DDD),先画了上下文映射图,最后拆出这几个核心服务:
| 服务名 | 职责 | 语言 | 数据库 |
|---|---|---|---|
| user-service | 用户信息、鉴权 | Go | MySQL |
| order-service | 订单创建、状态管理 | Go | MySQL |
| stock-service | 库存扣减、回滚 | Go | Redis + MySQL |
| coupon-service | 优惠券发放、核销 | Python | MongoDB |
注意:coupon-service 还用 Python,因为历史逻辑太复杂,团队没人敢动。但对外暴露 gRPC 接口,其他服务调它时完全无感。
关键问题 1:跨服务事务怎么办?
下单流程涉及三个服务:扣库存 → 创建订单 → 扣优惠券。任何一个失败,都得全部回滚。但在分布式系统里,没有 ACID,只有 BASE。
我们试过两种方案:
方案 A:两阶段提交(2PC)
理论完美,实测拉胯。网络抖一下,参与者就卡住,还得人工干预。直接 PASS。
方案 B:Saga 模式 + 补偿事务
最终选定这个。核心思想:每个步骤都有对应的“撤销操作”。
比如:
stock-service扣库存 → 成功order-service创建订单 → 失败!- 调用
stock-service/CompensateStock回滚库存
代码示意(Go):
// order-service 中的下单逻辑
func CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
// 1. 扣库存
_, err := stockClient.DeductStock(ctx, &pb.DeductStockRequest{SkuId: req.SkuId, Qty: req.Qty})
if err != nil {
return nil, status.Errorf(codes.Internal, "deduct stock failed: %v", err)
}
// 2. 创建订单(假设这里 panic 了)
if err := db.CreateOrder(req); err != nil {
// 触发补偿:回滚库存
stockClient.CompensateStock(ctx, &pb.CompensateStockRequest{
SkuId: req.SkuId,
Qty: req.Qty,
TraceId: getTraceId(ctx), // 用于幂等
})
return nil, err
}
return &pb.CreateOrderResponse{OrderId: "xxx"}, nil
}
面试题预警:面试官常问“如何保证分布式事务一致性?”
别只会说“用 RocketMQ 事务消息”,得讲清楚业务场景、补偿机制、幂等设计!
关键问题 2:服务发现与调用链追踪
早期我们用 Nginx 做负载均衡,结果每次加机器都得改配置,运维骂娘。后来上了 etcd + gRPC 负载均衡,服务启动自动注册,客户端轮询调用。
但更头疼的是排查问题。以前单体时代,一个请求日志从头打到尾;现在一次下单涉及 4 个服务,日志散落在不同机器。
解决方案:OpenTelemetry + Jaeger
- 每个服务注入 trace ID
- gRPC 拦截器自动透传上下文
- 日志格式统一带上
trace_id
效果拔群!上周五晚上线上偶发超时,我 5 分钟就定位到是 coupon-service 的 MongoDB 查询慢了——换作以前,估计得通宵。
数据库设计:别让 SQL 成为瓶颈
微服务不代表每个服务都能乱建表。我们定了几条军规:
禁止跨服务直接查 DB
想拿用户信息?调user-service的 gRPC 接口,别偷偷连 user_db!读写分离 + 分库分表预埋
order-service的订单表,主键用 Snowflake ID(避免自增瓶颈),未来可按 user_id 分库。缓存策略统一
Redis 用service:key:id格式命名,比如stock:sku:1001,避免 key 冲突。
吐槽一句:产品经理曾要求“实时统计全站订单量”,我差点答应写个 SELECT COUNT(*) FROM orders……还好被 DBA 拦住了,改成用 Flink 实时聚合到 Redis。
性能压测:别信本地跑得快
本地 wrk -t12 -c400 -d30s http://localhost/order 跑出 5000 QPS,上线后直接 502。为啥?
- 网络延迟:服务间调用多了 RTT
- GC 压力:Go 服务对象太多,STW 时间长
- DB 连接池:默认 10 连接,高并发下排队
优化措施:
- gRPC 启用连接复用(
grpc.WithInsecure()+ 连接池) - Go 服务调整 GOMAXPROCS=CPU核数
- 数据库连接池调到 100+
最终压测结果(4 核 8G 云主机):
| 场景 | QPS | P99 延迟 |
|---|---|---|
| 单体 Django | 320 | 420ms |
| 微服务 Go | 2100 | 85ms |
虽然 QPS 提升明显,但复杂度也爆炸了。运维说:“现在一个 bug 要查四个日志,你们开发是不是该请我喝奶茶?”
给想跳槽的同学:微服务面试题清单
最近面了几家公司,发现微服务是必考项。整理几个高频题:
- 如何设计一个高可用的微服务注册中心?(答 etcd/ZooKeeper 原理,脑裂处理)
- gRPC 和 RESTful 如何选型?(性能 vs 兼容性)
- 分布式 ID 生成方案有哪些?Snowflake 有什么坑?(时钟回拨!)
- 服务雪崩怎么防?(熔断 Hystrix / Sentinel,限流令牌桶)
- Python 能做微服务吗?(能,但高并发场景不如 Go/Java)
记住:别背八股文,结合项目讲。比如我说“我们在 stock-service 用 Redis Lua 脚本保证扣库存原子性”,面试官眼睛都亮了。
最后:微服务不是银弹
折腾一个月,系统终于跑起来了。双11 当天峰值 1800 QPS,零重大故障——老板请全组吃了顿火锅(人均 80,抠门但够意思)。
但我也看清了:微服务不是万能药。如果你的业务简单、团队小、迭代快,单体反而更香。我们现在的运维成本、监控复杂度、调试难度,比之前高了至少 3 倍。
不过嘛,作为想跳槽的双非学生,这段经历值了。简历上能写“主导微服务拆分,支撑日订单 10w+”,HR 筛简历时大概率不会把我扔进垃圾桶。
共勉吧,打工人!下次见面,希望是在大厂茶水间里吹空调,而不是在创业公司通宵修 Bug。
P.S. Windows 我只用来测 IE 兼容性(虽然现在没人用 IE 了),MacBook 键盘都被我敲出包浆了……

评论 0