从单体到云原生:一个普通CS毕业生的两年血泪史
大家好,我是小李,普通一本CS专业大四狗一枚,已经拿了某二线大厂的offer,正等着7月入职。不过在这之前,我其实已经在现在的实习组干了快两年了——没错,就是那种“学生还没毕业,代码已经上线”的社畜预备役。
我们组是个典型的“老系统维护+新业务拓展”混合团队。去年双11前,老板突然拍板:“老系统扛不住了,得往云原生走!”——当时我正在用Vim改一个Go写的订单服务,听到这话差点把键盘砸了。毕竟,我平时连IDE都懒得开,更别说搞什么K8s、Service Mesh这些听起来就头大的东西了。
但没办法,需求压下来,deadline就在眼前,只能硬着头皮上。这篇文章,就是我在过去一年里,从单体架构一路折腾到云原生的实战踩坑记录。不吹牛,全是血泪经验,希望能帮到还在挣扎的你。
起点:那个又大又臭的单体应用
两年前刚进组时,我们的核心系统是一个用Go写的单体应用,名字叫order-center(对,就是字面意思)。整个系统打包成一个二进制,部署在3台物理机上,数据库是MySQL主从,缓存靠Redis。听起来是不是很经典?确实经典,但也经典地“屎山”。
- 所有业务逻辑塞在一个repo里:用户、订单、库存、支付、通知……全在
internal/下面堆着。 - 启动时间30秒+,改一行日志要重新编译5分钟(别问,问就是没做增量构建)。
- 想加个新功能?先祈祷别影响其他模块,不然测试同学能追着你跑三栋楼。
最离谱的是,有一次产品说要“快速上线一个预售功能”,结果因为和库存模块耦合太紧,改完之后下单流程崩了。那天晚上我们三人通宵,最后发现是个锁竞争问题——而这段代码居然是三年前某位已离职大哥写的,注释只有两个字:“勿动”。
那一刻我悟了:单体不是原罪,耦合才是。
第一步拆分:微服务初体验,结果翻车了
老板发话后,我们决定先做最简单的拆分:把订单和库存拆出去。技术栈还是Go,框架用Gin + GORM,部署方式从物理机迁到Docker容器(终于告别手动scp部署了!)。
踩坑1:服务间调用变慢了十倍
原本在单体里,调用库存就是一次函数调用,现在变成了HTTP请求。本地测试没问题,一上预发环境,P99延迟直接飙到800ms。查了半天,发现是每次请求都新建HTTP客户端,没有连接复用。
修复方案:用全局http.Client + 连接池。
// 别再每次NewRequest都new client了!
var httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
},
Timeout: 5 * time.Second,
}
小贴士:Go的
http.Client默认是长连接的,但如果你每次都&http.Client{},那就等于每次新建TCP连接,性能直接爆炸。
踩坑2:分布式事务怎么办?
订单创建成功,但库存扣减失败,钱收了货没了——这种事故谁担得起?我们一开始想用Saga模式,但实现起来太复杂。最后妥协用了“最终一致性 + 补偿任务”:
- 创建订单时,发一条MQ消息到
inventory-decrease队列 - 库存服务消费消息,尝试扣减
- 如果失败,写入重试表,由定时任务兜底
- 用户端展示“处理中”,等最终状态
虽然不够完美,但在业务容忍范围内。产品经理也接受了,毕竟他说:“只要别让用户投诉就行。”
进阶:拥抱Kubernetes,但差点被YAML逼疯
拆完微服务后,部署成了新痛点。每个服务都要写Dockerfile、起容器、配Nginx反向代理……运维大哥天天抱怨:“你们开发能不能统一一下端口?”
于是我们上了K8s。说实话,第一次看Deployment YAML的时候,我真想回到单体时代。光是一个Pod的spec就能写50行,还不包括Service、Ingress、ConfigMap……
实战配置:一个Go服务的最小K8s部署
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order
image: registry.example.com/order-service:v1.2.0
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: order-config
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
注意:
/healthz和/ready这两个endpoint必须在Go代码里实现!否则Pod会一直CrashLoopBackOff。
我们在Go里加了:
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
// 简单检查DB连接
if db.Ping() == nil {
w.WriteHeader(200)
w.Write([]byte("OK"))
} else {
w.WriteHeader(500)
}
})
http.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
// 检查依赖服务是否就绪(比如Redis、MQ)
w.WriteHeader(200)
w.Write([]byte("Ready"))
})
踩坑3:日志去哪了?
在物理机时代,日志直接打到/var/log/,grep一把梭。上了K8s后,Pod一重启,日志就没了。我们紧急接入了ELK(Elasticsearch + Logstash + Kibana),要求所有Go服务用JSON格式输出日志:
// 使用 zap 日志库
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("order created",
zap.String("order_id", "12345"),
zap.Int64("user_id", 67890),
)
然后通过Fluentd采集到ES。虽然配置过程痛苦,但一旦搞定,查日志效率飞升——再也不用ssh进机器了!
云原生的甜头:弹性伸缩与可观测性
真正让我觉得“值了”的,是去年双11。
往年这时候,运维要提前一周扩容机器,盯着监控屏不敢睡觉。今年,我们配了HPA(Horizontal Pod Autoscaler),根据CPU和QPS自动扩缩容:
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "100"
双11当晚,订单服务从3个Pod自动扩到18个,流量高峰一过,又慢慢缩回去。运维大哥喝了杯咖啡,说了句:“这届云原生,真香。”
同时,我们接入了Prometheus + Grafana,监控所有服务的:
- 请求量(QPS)
- 错误率(5xx)
- 延迟(P50/P95/P99)
- Go runtime指标(goroutine数、GC pause)
有一次,P99突然飙升,Grafana图表一拉,发现是某个新接口没加缓存,直接查DB。十分钟定位,二十分钟上线热修复——这种效率,在单体时代根本不敢想。
架构演进对比:数据说话
为了说服老板继续投入云原生,我整理了一份对比表格:
| 维度 | 单体架构(2022) | 云原生微服务(2024) |
|---|---|---|
| 部署频率 | 1次/周 | 20+次/天 |
| 故障隔离 | 全站挂 | 单服务降级 |
| 扩容速度 | 手动,30分钟+ | 自动,2分钟内 |
| 新人上手成本 | 高(需理解全系统) | 低(只关注自己服务) |
| 线上问题定位时间 | 平均4小时 | 平均30分钟 |
| 资源利用率 | 30%(常驻高配机器) | 70%+(按需分配) |
最让我骄傲的是,线上事故次数下降了80%。以前每月总有那么一两次“半夜被PagerDuty叫醒”,现在基本可以安心睡觉了(除非产品经理半夜改需求)。
写在最后:稳定比炫技更重要
作为一个喜欢折腾新技术的Vim党,我一度想在项目里上gRPC、etcd、Istio……但带我的导师一句话点醒了我:“线上系统,稳定压倒一切。”
所以我们的云原生架构其实很“保守”:
- 通信还是用HTTP/JSON(而不是gRPC),因为调试方便
- 没上Service Mesh,怕运维复杂度太高
- 数据库还是MySQL,没敢碰TiDB或CockroachDB
但正是这种“稳中求进”的策略,让我们在保障业务的同时,完成了架构升级。上周五晚上,我用Vim提交了最后一个PR,看着CI/CD流水线自动部署到K8s集群,心里莫名踏实。
如果你也在经历类似的转型,别怕慢,别怕土。架构演进不是百米冲刺,而是马拉松。每一步踩稳了,才能跑得更远。
对了,7月我就要去新公司正式搬砖了。希望到时候,能把我这两年的经验,带到更大的战场。
共勉。
附:给后来者的建议
- 先拆业务,再拆技术:不要为了微服务而微服务,先看业务边界。
- 可观测性是生命线:没监控的日志等于没有日志。
- Go的并发模型是利器:善用goroutine + channel,但别滥用。
- 自动化一切:从CI/CD到告警,能自动的绝不手动。
- 别信“银弹”:云原生不是万能药,它只是工具。
好了,文章写完,我去改简历了——毕竟offer虽好,也不能忘了提升自己嘛 😉

评论 0