后端架构演进:从单体到云原生,一个成都程序员的血泪实战手记

线上问题观察员
2025-12-14 13:54
阅读 392

作者注:我是 Cursor 的重度用户,几乎到了「没 AI 写不了代码」的地步。每天早上泡杯茶(成都人嘛,生活节奏舒服点),打开 VS Code + Cursor,跟 Claude 聊两句:“帮我重构一下这个服务”,或者“这段 ORM 查询能不能更优雅?”。坐标成都,团队 10 人左右,技术栈以 Python 为主。本文不是教科书,是我被线上事故、产品经理改需求、运维兄弟半夜 call 醒之后,一路踩坑踩出来的实战总结。


去年双 11 前两周,我们那个跑了三年的 Django 单体应用,在凌晨 2 点突然崩了。不是那种优雅的 500 错误,是直接 OOM,连日志都写不进去了。我顶着黑眼圈翻监控,发现是某个新上的“智能推荐”功能——产品经理拍脑袋加的,测试只测了 10 条数据,上线后百万级请求一压,内存直接爆成烟花。

那一刻我真的想砸电脑。

但冷静下来一想:这锅不能全甩给产品。我们的架构早就该升级了。那个单体应用,models.py 快 2000 行,urls.py 像蜘蛛网,部署一次要等 15 分钟,CI/CD 流水线跑得比我家楼下茶馆的盖碗茶还慢。

于是,领导一句话:“搞云原生吧,明年 Q1 要上 K8s。”
我说好,心里却在骂:你倒是说得轻巧,老子连 Helm 都没摸过!

但没办法,为了跳槽简历好看,也为了晚上能睡个安稳觉,干就完了。

从“能跑就行”到“不敢动”的单体时代

先说说我们的老系统:一个典型的 Django + PostgreSQL + Celery 架构。所有业务逻辑——用户管理、订单、支付、推荐、后台运营——全塞在一个 repo 里。本地开发用 docker-compose up,部署靠 Jenkins 打包镜像推到阿里云 ECS。

听起来是不是很熟悉?没错,这就是无数中小厂的“标准配置”。

但问题在哪?

  • 耦合度高到离谱:改个用户头像上传逻辑,可能影响支付回调。因为两者共用同一个数据库连接池、同一个中间件链。
  • 扩缩容靠玄学:流量来了?加机器!但因为是单体,你只能整体扩容,哪怕只有推荐模块吃资源。钱烧得飞快。
  • 部署即冒险:每次发版,测试兄弟都要念三遍佛经。有一次我改了个字段名,忘了同步到 Celery 任务里,结果异步任务全挂,积压了 10 万条消息。

最致命的是:可维护性为负。新人来了看三天代码,第四天提离职。我自己都不忍心看 models.py 里的那堆 @propertysave() 重写。

拆!微服务化第一步:按领域切分

云原生的第一步,不是上 K8s,而是解耦

我们花了两个月,把单体拆成四个核心服务:

服务名 职责 技术栈
user-service 用户注册、登录、资料管理 FastAPI + SQLAlchemy
order-service 订单创建、状态流转 FastAPI + asyncpg
payment-service 支付对接、对账 FastAPI + Celery (for webhook)
recommend-service 推荐算法、内容分发 Flask + Redis + PyTorch

为什么选 FastAPI?简单:自动 OpenAPI 文档 + 异步支持 + 类型提示友好。对我们这种注重代码可读性的团队太香了。而且,Cursor 跟 FastAPI 配合简直绝配——它能根据 Pydantic 模型自动生成 CRUD 代码,省下我大把时间喝茶。

拆分过程中最大的坑?数据库隔离

一开始我们天真地以为“服务拆了,DB 还共用一个表”,结果 order-service 直接查 user 表,导致跨服务事务混乱。后来痛定思痛,每个服务必须有自己的数据库(甚至 schema 都隔离),通过 API 或事件驱动通信。

比如:用户下单 → order-service 创建订单 → 发送 Kafka 消息 → payment-service 监听并发起支付。

# order-service 中发送事件
from confluent_kafka import Producer

producer = Producer({'bootstrap.servers': 'kafka:9092'})

def create_order(user_id: int, items: List[Item]):
    order = Order.create(...)
    producer.produce(
        topic="order.created",
        key=str(order.id),
        value=json.dumps({
            "order_id": order.id,
            "user_id": user_id,
            "amount": order.total
        })
    )
    producer.flush()

这里有个血泪教训:别用 RabbitMQ 做事件总线!我们试过,消息堆积时性能暴跌,Kafka 才是扛大流量的王者(虽然运维复杂点)。

容器化:Docker 不是终点,只是起点

拆完服务,下一步自然是容器化。每个服务一个 Dockerfile,用 Gunicorn + Uvicorn 跑 FastAPI。

# user-service/Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# 使用 Uvicorn + Gunicorn 提升并发
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "main:app", "--bind", "0.0.0.0:8000"]

但很快发现:本地跑得好好的,一上测试环境就超时。排查半天,原来是 DNS 解析慢——容器内默认用 /etc/resolv.conf,而公司内网 DNS 响应巨慢。

解决方案?在 docker-compose.yml 里硬编码 DNS:

services:
  user-service:
    dns:
      - 8.8.8.8
      - 114.114.114.114

上线后,运维兄弟吐槽:“你们开发又在乱改网络配置!” 我回他:“要不你来调?反正半夜报警是你接。”

上云原生:K8s + Helm + ArgoCD

真正让我头秃的,是上 K8s。

第一次写 Deployment YAML,写了三小时,Pod 还是 CrashLoopBackOff。错误日志就一句:CrashLoopBackOff: back-off 5m0s restarting failed container=user-service pod=user-service-7d5b8c9f4-xk2lq。我当时真想哭。

后来才发现:没配 readinessProbe!容器启动了,但 DB 连接还没初始化完,K8s 就开始转发流量,结果 500 一片。

加上探针才稳住:

# k8s/user-service.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: user-service
        image: registry/user-service:v1.2
        ports:
        - containerPort: 8000
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8000
          initialDelaySeconds: 30
          periodSeconds: 10

对应的 /healthz 接口也要实现:

# main.py in user-service
@app.get("/healthz")
async def health_check():
    # 检查 DB 连通性
    try:
        await db.execute("SELECT 1")
        return {"status": "ok"}
    except Exception as e:
        raise HTTPException(status_code=503, detail=str(e))

Helm 也是个神器。以前每次改配置都要手动 sed 替换,现在用 Helm values.yaml 管理:

# helm/user-service/values.yaml
replicaCount: 3
image:
  repository: registry/user-service
  tag: v1.2
env:
  DATABASE_URL: "postgresql://user:pass@prod-db:5432/userdb"
resources:
  limits:
    memory: "512Mi"
    cpu: "500m"

配合 ArgoCD 做 GitOps,配置即代码,发布靠 merge PR。运维终于不用半夜被叫起来敲 kubectl 了——虽然他们现在抱怨“你们开发又把 prod 配置 merge 错了”。

性能与可观测性:没有监控的云原生都是耍流氓

架构拆了、上了 K8s,不代表万事大吉。线上问题只会更隐蔽

有一次,recommend-service 的 P99 延迟从 200ms 飙到 2s,但 CPU、内存都正常。查了一天,发现是 Redis 连接池耗尽——因为用了同步 requests 调外部 API,阻塞了 asyncio loop。

解决方案?全面拥抱 async:

# 用 httpx 替代 requests
import httpx

async def fetch_user_profile(user_id: int):
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"https://user-service/api/v1/users/{user_id}")
        return resp.json()

同时,可观测性必须拉满

  • 日志:用 Loki + Promtail 收集,Grafana 查
  • 指标:Prometheus 抓取 /metrics(FastAPI 有现成 middleware)
  • 链路追踪:Jaeger + OpenTelemetry

比如在 FastAPI 里集成 OTel:

from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

tracer = trace.get_tracer(__name__)

app = FastAPI()
FastAPIInstrumentor.instrument_app(app)

现在,任何一个请求慢了,我都能在 Jaeger 里看到完整调用链:从 Nginx Ingress → user-service → order-service → Kafka → payment-service,哪一环卡住一目了然。

效果如何?值不值得折腾?

上线三个月后,数据说话:

指标 单体时代 云原生后
平均部署时间 15 分钟 3 分钟(ArgoCD 自动 sync)
故障恢复时间 30+ 分钟 < 5 分钟(Pod 自愈 + HPA 自动扩缩)
资源成本 4 台 8C16G ECS K8s 集群 + HPA,峰值时多,平时少 40%
开发幸福感 ❌(改一行怕崩全局) ✅(独立开发、测试、部署)

最关键的是:双 11 那天,系统稳如老狗。推荐服务 QPS 到 5000,HPA 自动从 3 个 Pod 扩到 20 个,流量退了又缩回去。我躺在沙发上刷抖音,运维兄弟发微信:“今年没报警,你是不是偷偷拜了佛?”

其实哪有什么佛,不过是把技术债一点点还了而已。

最后几句真心话

云原生不是银弹。如果你团队就 3 个人,业务也没多少流量,别盲目跟风。但如果你像我一样,被单体折磨到失眠,那拆微服务 + 上 K8s 绝对值得。

不过记住:工具是为人服务的。我用 Cursor 写 80% 的样板代码,但核心逻辑、数据一致性、错误处理,还得自己抠。AI 是副驾驶,不是机长。

另外,别信“一次重构到位”的鬼话。我们是边跑边换轮子——先拆服务,再容器化,再上 K8s,每一步都有回滚方案。稳字当头,才能活得久。

现在,我终于敢在 models.py 里写干净的代码了。
产品经理又来提需求?不怕,改就完了——反正只影响一个服务。

(完)


P.S. 如果你在成都,想找人一起喝茶聊架构,欢迎私信。
P.P.S. 别问为啥不用 Go,我们是 Python 死忠粉,人生苦短,我用 Python。

评论 0

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