从单体到云原生:一个普通CS毕业生的两年血泪史

开发者后花园
2025-12-25 10:39
阅读 235

大家好,我是小李,普通一本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模式,但实现起来太复杂。最后妥协用了“最终一致性 + 补偿任务”:

  1. 创建订单时,发一条MQ消息到inventory-decrease队列
  2. 库存服务消费消息,尝试扣减
  3. 如果失败,写入重试表,由定时任务兜底
  4. 用户端展示“处理中”,等最终状态

虽然不够完美,但在业务容忍范围内。产品经理也接受了,毕竟他说:“只要别让用户投诉就行。”


进阶:拥抱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月我就要去新公司正式搬砖了。希望到时候,能把我这两年的经验,带到更大的战场。

共勉。


附:给后来者的建议

  1. 先拆业务,再拆技术:不要为了微服务而微服务,先看业务边界。
  2. 可观测性是生命线:没监控的日志等于没有日志。
  3. Go的并发模型是利器:善用goroutine + channel,但别滥用。
  4. 自动化一切:从CI/CD到告警,能自动的绝不手动。
  5. 别信“银弹”:云原生不是万能药,它只是工具。

好了,文章写完,我去改简历了——毕竟offer虽好,也不能忘了提升自己嘛 😉

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝