后端架构演进:从单体到云原生,一个摸鱼程序员的观察笔记

刘磊★
2026-04-28 10:37
阅读 1490

入职新公司两个月,我的键盘上咖啡渍还没干,就眼睁睁看着我们那个“祖传”Python单体应用差点在上周五晚上的流量高峰里原地升天。

当时我正用Vim敲着Rust的Result<T, E>,突然钉钉疯狂震动——运维老哥在群里发了一张CPU飙到98%的监控图,配文:“兄弟们,救救孩子。”产品经理在旁边悠悠补了一句:“这个需求不是很简单吗?加个按钮而已。”

我默默合上终端,心里想:这哪是加按钮,这是给泰坦尼克号甲板上装个新躺椅啊。


从“能跑就行”到“别崩就行”

我们最初的系统,说白了就是个典型的Django单体应用。数据库用PostgreSQL,前端直接套个Jinja2模板,部署靠手动scp + supervisor。代码库里既有用户管理、订单逻辑,又有报表生成、短信发送,甚至还有十年前留下的Excel导出脚本——注释写着“别动,动了会死人”。

这种架构在业务量小的时候确实香:改一行代码,git push,重启服务,搞定。但一旦用户数突破百万,QPS超过500,问题就接踵而至:

  • 数据库连接池打满
  • 某个慢查询拖垮整个API响应
  • 部署一次要停机10分钟(测试同学每次都在群里哀嚎)
  • 新来的实习生不小心删了生产库的一张表(还好有备份)

最要命的是,团队协作效率极低。前端想上线个新页面,得等后端改完三个模块;后端想重构支付模块,又怕影响导出功能。大家就像在同一个游泳池里同时练自由泳、蛙泳和仰泳,互相泼水还撞头。

于是,领导拍板:“搞微服务吧,学学人家大厂。”


微服务?先学会拆“锅”再说

说干就干。我们先把最独立的用户中心抽出来,用FastAPI重写,单独部署。然后是订单服务、商品服务、通知服务……每个服务都有自己的数据库(当然,初期为了省事,还是共用一个PostgreSQL实例,只是分了schema)。

这时候,Python的优势就体现出来了:开发快、生态好、异步支持也不错。比如一个简单的用户查询接口:

# user_service/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import asyncpg

app = FastAPI()

class User(BaseModel):
    id: int
    name: str
    email: str

@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
    conn = await asyncpg.connect("postgresql://user:pass@db/user_db")
    try:
        row = await conn.fetchrow("SELECT id, name, email FROM users WHERE id = $1", user_id)
        if not row:
            raise HTTPException(status_code=404, detail="User not found")
        return User(**row)
    finally:
        await conn.close()

看起来挺清爽,对吧?但实际上,这只是万里长征第一步。

很快我们就遇到了分布式系统的经典难题:

  • 服务发现:订单服务怎么知道用户服务现在在哪台机器上?
  • 链路追踪:一个下单请求横跨5个服务,哪里慢了根本找不到
  • 数据一致性:用户扣款成功,但订单状态没更新,钱没了货也没了

更别提网络抖动、超时重试、熔断降级这些“高级操作”。有次因为Redis缓存穿透,导致用户服务雪崩,连带着整个下单流程瘫痪了半小时。运维老哥半夜打电话骂人,我躲在被窝里一边回消息一边心想:早知道就不摸鱼看Rust文档了,该去学学SRE手册。


容器化:把“在我机器上能跑”变成“在哪都能跑”

为了解决环境不一致的问题,我们引入了Docker。每个服务打包成镜像,配上docker-compose.yml,本地开发终于不用再装一堆Python依赖了。

# docker-compose.yml
version: '3.8'
services:
  user-service:
    build: ./user_service
    ports:
      - "8001:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres/user_db
    depends_on:
      - postgres

  order-service:
    build: ./order_service
    ports:
      - "8002:8000"
    environment:
      - USER_SERVICE_URL=http://user-service:8001
      - DATABASE_URL=postgresql://user:pass@postgres/order_db

  postgres:
    image: postgres:14
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass

本地跑起来确实舒服多了。但一到生产环境,问题又来了:手动docker run几十个容器?谁来管扩缩容?谁来监控日志?

于是,Kubernetes登场。


上K8s:从“运维求我”到“我求运维”

说实话,第一次看到K8s的YAML配置文件,我差点以为自己在写科幻小说。Deployment、Service、Ingress、ConfigMap、Secret……每个概念都像天书。

但架不住真香。一旦跑通,你会发现:

  • 自动扩缩容:流量来了自动加Pod,走了自动缩回去
  • 自愈能力:某个Pod挂了,秒级重建
  • 蓝绿发布:再也不用半夜停机升级了

我们的一个典型Deployment配置:

# k8s/user-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user
        image: registry.example.com/user-service:v1.2.3
        ports:
        - containerPort: 8000
        envFrom:
        - configMapRef:
            name: user-config
        - secretRef:
            name: user-secrets
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"

配合Helm Chart管理版本,CI/CD流水线自动构建镜像并部署,整个流程丝滑了不少。

不过,学习曲线是真的陡。有次我把replicas写成replica,整个服务没起来,测试同学在群里@我:“你是不是又在摸鱼?” 我只能苦笑:“我在摸K8s的鱼。”


云原生:不止是技术,更是心态

折腾了几个月,我们终于从“单体巨兽”进化到了“云原生小舰队”。但真正的转变,其实发生在团队协作方式上。

以前:

  • 所有人都往一个Git仓库提交
  • 发布靠“勇气”
  • 出问题第一反应是“谁动了代码?”

现在:

  • 每个服务独立仓库,Owner负责制
  • 发布靠自动化流水线
  • 出问题先看链路追踪(Jaeger)和指标监控(Prometheus + Grafana)

最关键的是,我们开始用声明式API不可变基础设施的思维来思考问题。不再是“怎么修”,而是“怎么设计才能不出错”。

顺便提一句,最近我用Claude Code(没错,就是那个AI编程助手)帮我们生成了一些K8s的Ingress配置和Prometheus告警规则,效率提升不少。虽然它偶尔也会把http写成htp,但至少比我手写YAML少犯错。摸鱼间隙让它干活,何乐不为?


性能与成本:永远的权衡

架构演进不是免费的。微服务带来了灵活性,但也增加了复杂度和资源开销。

我们做了一次性能对比(模拟500并发用户):

架构类型 平均响应时间 (ms) P99延迟 (ms) CPU使用率 内存占用 部署复杂度
单体Django 120 450 40% 800MB ★☆☆☆☆
微服务(裸机) 180 800 65% 2.1GB ★★★★☆
云原生(K8s) 140 520 50% 1.8GB ★★★★★

可以看到,纯微服务在裸机上跑,延迟反而更高——因为服务间调用多了网络开销。但上了K8s后,通过合理的资源限制、HPA(Horizontal Pod Autoscaler)和Service Mesh(我们用了Linkerd),延迟控制住了,还获得了弹性伸缩能力。

不过,老板看到账单的时候还是皱了眉头:“这云服务费用比去年翻倍了啊。” 我们只能默默优化Pod资源请求,关掉非核心服务的副本数,甚至把一些低频任务迁回单体——架构没有银弹,只有合适与否


给后来者的几点建议(血泪总结)

作为一个刚入行不久、还在边摸鱼边学Rust的佛系程序员,我想分享几点踩坑心得:

  1. 别为了微服务而微服务
    如果你的业务还没到需要拆的程度,硬拆只会增加维护成本。先做好模块化,等瓶颈真正出现再动手。

  2. 可观测性要前置
    从第一个微服务开始,就把日志、指标、追踪埋进去。别等线上炸了才想起来要监控。

  3. 自动化是生命线
    手动部署微服务等于自杀。CI/CD、镜像仓库、Helm Chart,这些基础设施越早搭越好。

  4. 数据库拆分最难
    服务拆了容易,数据拆了难。考虑用CDC(Change Data Capture)或事件驱动架构来解耦。

  5. 别忽视本地开发体验
    K8s虽好,但本地调试太痛苦。可以用telepresenceskaffold让开发像单体一样简单。

  6. 拥抱工具,但别迷信
    Claude Code这样的AI助手能帮你写样板代码,但核心逻辑还得自己把控。它生成的SQL可能没加索引,YAML可能语法错误——别全信。


最后:摸鱼也要摸出生产力

写这篇文章的时候,我刚用Rust写完一个简单的sidecar代理,打算用来替代部分Nginx配置。虽然可能最后不会上线,但至少让我对网络层理解更深了。

技术演进从来不是一蹴而就的。从单体到云原生,我们走了弯路,也收获了经验。重要的是,在每天应付PRD和deadline的同时,还能保持一点好奇心,摸鱼的时候也能学到东西。

毕竟,程序员的终极目标不是写出完美架构,而是在系统不崩的前提下,多摸一会儿鱼。

(完)

附:为什么我还在用Vim?
因为在K8s里kubectl exec -it pod-name -- sh进去排查问题时,vi是唯一能用的编辑器。早点习惯,少点崩溃。

评论 0

最热最新
暂无评论
刘磊★Lv.1
0
影响力
0
文章
0
粉丝