从单体到云原生:一个深夜码农的架构觉醒
去年冬天,凌晨两点,我正对着满屏的日志发呆。系统又崩了——双11大促刚过,订单服务直接打挂了整个单体应用。产品经理在群里@我:“能不能快点?用户都投诉到CEO邮箱了。”运维兄弟在旁边小声嘀咕:“这破单体,改一行代码全量发布,半夜上线跟开盲盒一样。”
那一刻,我真的想砸电脑。
我是那种典型的“老派”后端工程师:写了三年多 Java 单体,代码必须格式整齐、注释完整,函数不能超过 50 行。我一直觉得 AI 写代码是邪道——变量名乱起、逻辑绕得像迷宫,根本没法维护。直到某次被逼着用 Copilot 生成一段数据库迁移脚本,结果它比我手写还规范……真香定律虽迟但到。
最近在考虑跳槽,面试官总爱问:“你们系统怎么拆微服务的?”、“云原生落地遇到哪些挑战?”——说实话,以前我答得支支吾吾。但现在不一样了,因为我自己刚带着团队走完从单体到云原生的血泪之路。今天就来聊聊这段经历,顺便给正在准备面试题挑战的兄弟们一点实战参考。
起点:那个又大又臭的“全能”应用
我们最初的产品是一个典型的电商后台,所有功能——用户、商品、订单、支付、库存——全塞在一个 Spring Boot 应用里。本地跑起来要 2 分钟,CI/CD 一次 15 分钟。改个商品详情页的颜色,居然要全量回归测试!测试妹子每次看我都眼神幽怨。
最要命的是扩展性。促销一来,订单模块扛不住,但商品和用户服务其实很闲。可因为是单体,你只能把整个应用水平扩容,资源浪费严重不说,还经常因为某个小 Bug 拖垮全局。
“能不能只扩订单服务?”我问架构师。
他苦笑:“兄弟,现在所有模块共享同一个数据库连接池、同一个线程池,拆?等于重写。”
第一步:垂直拆分,先活下来
我们没直接上 Kubernetes,而是先做垂直拆分——把订单、用户、商品三个核心域独立成服务,每个服务有自己的数据库。技术栈也趁机切换:新服务用 Go 重写。
为什么选 Go?简单、并发强、启动快。而且我们团队里几个年轻小伙早就摩拳擦掌想用 Go 了(Java 写久了真的会秃)。我一开始还反对:“Go 的错误处理太原始了!”结果真上手发现,配合 errors.Is 和自定义 error 类型,反而让异常流更清晰。
比如订单创建的核心逻辑:
func (s *OrderService) Create(ctx context.Context, req *CreateOrderReq) (*Order, error) {
// 1. 校验库存
if err := s.inventoryClient.CheckStock(ctx, req.SKUs); err != nil {
return nil, fmt.Errorf("stock check failed: %w", err)
}
// 2. 创建订单(事务)
order, err := s.repo.CreateWithTx(ctx, req)
if err != nil {
return nil, fmt.Errorf("db create failed: %w", err)
}
// 3. 异步扣减库存(通过消息队列)
if err := s.msgQueue.Publish("inventory_deduct", order.ID); err != nil {
log.Warn("Failed to publish inventory deduct msg", "order_id", order.ID)
// 注意:这里不阻断主流程,靠补偿机制兜底
}
return order, nil
}
你看,每一步错误都带上下文,链路清晰。而且 Go 的 goroutine 让高并发下单变得轻松——再也不用担心 Tomcat 线程池被打满。
进阶:拥抱云原生,但别被 buzzword 带偏
拆完微服务,问题才刚开始。
- 服务间调用超时怎么办?
- 配置怎么动态更新?
- 日志散落在各处,排查问题像拼图?
这时候,云原生不是选择,是刚需。但我们没盲目上 Istio 或复杂 Service Mesh,而是从最痛的点入手:
- 配置中心:用 Consul + envconfig,启动时自动拉取配置,支持热更新。
- 服务发现:Go-kit + Consul,注册/发现全自动。
- 可观测性:Prometheus + Grafana 监控指标,Loki 收集日志(比 ELK 轻量太多)。
- API 网关:用 Kong 做统一入口,限流、鉴权、路由一气呵成。
最让我惊喜的是 Kubernetes 的就绪探针(Readiness Probe)。以前服务刚启动就接流量,数据库连接还没建好,直接 500。现在只要 HTTP 探针返回 200 才加入负载均衡,稳如老狗。
部署 YAML 也做了标准化:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
template:
spec:
containers:
- name: order
image: registry/order:v1.2.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
资源限制写清楚,避免某个服务吃光节点资源。运维大哥终于不用半夜打电话骂我们了。
面试题挑战?这些坑我都踩过
最近面了几家公司,发现高频面试题挑战基本围绕这几个点:
| 问题类型 | 我的回答思路 |
|---|---|
| “如何保证微服务数据一致性?” | 最终一致性 + 消息队列 + 幂等设计。比如订单创建后发消息扣库存,消费者必须幂等,避免重复扣减。 |
| “Go 服务如何做优雅停机?” | 监听 SIGTERM,关闭 HTTP server 后等待 in-flight 请求完成(用 sync.WaitGroup),再退出。 |
| “云原生下如何做配置管理?” | 不要把配置写死在镜像里!用 ConfigMap + 环境变量或 volume mount,配合应用框架动态 reload。 |
有一次面试官问我:“你们为什么不用 Dapr 或其他云原生框架?”
我说:“Dapr 很酷,但对我们当前规模有点重。我们更倾向组合成熟组件(Consul + Prometheus + Kong),保持架构简单可理解。毕竟,可维护性比时髦更重要——这是我三年单体血泪换来的教训。”
效果与反思
上线半年后,系统稳定性大幅提升:
- 订单服务独立扩缩容,大促期间 CPU 利用率稳定在 60%
- 故障隔离:商品服务宕机,不影响用户下单
- 发布效率:从 15 分钟 → 2 分钟(只发变更服务)
当然也有代价:运维复杂度上升,团队需要学习新工具链。但长远看,这是值得的。
最重要的是,我现在写代码的心态变了。不再执着于“完美单体”,而是思考:“这个模块未来会不会独立?它的边界在哪?”
写在最后
从抵触 AI 到拥抱 Go,从坚守单体到拥抱云原生,这一路走得跌跌撞撞。但程序员的成长,不就是不断推翻自己、重建认知的过程吗?
如果你也在准备跳槽,或者被产品经理催着重构老系统——别慌。架构演进不是一蹴而就的革命,而是一步步的进化。先解决最痛的点,再逐步引入云原生能力。
对了,上周五晚上我又在公司写代码到凌晨。但这次不是救火,而是在优化一个 Go 服务的内存占用。窗外安静,键盘轻响,终端里 kubectl get pods 显示一切正常。
这种感觉,真好。

评论 0