后端架构演进:从单体到云原生,一个成都程序员的血泪实战手记
作者注:我是 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 里的那堆 @property 和 save() 重写。
拆!微服务化第一步:按领域切分
云原生的第一步,不是上 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