从单体到云原生:一个文科转码仔的后端架构踩坑实录
大家好,我是小林,非科班出身的前端开发,大学读的是中文系——没错,就是那个写论文能写八千字、写代码只能写 console.log('hello world') 的专业。两年前,我硬是靠啃文档、刷 LeetCode 和凌晨三点的咖啡,混进了现在的这家 SaaS 初创公司,干起了“伪全栈”(其实是被产品经理和后端同事轮流压榨的前端)。
虽然主职是前端,但因为团队小、人手紧,加上我好奇心重,这两年也深度参与了不少后端架构的演进工作,尤其是从去年开始,我们整个产品线从 Python 单体应用逐步迁移到云原生架构。今天就想聊聊这段“痛并快乐着”的经历,顺便吐吐槽,给同样在转型路上挣扎的朋友一点参考。
起因:产品疯了,系统崩了
事情得从去年双11前说起。我们的核心产品是一个面向中小企业的营销自动化平台,早期用 Django + PostgreSQL 搞了个单体应用,跑在一台 8C32G 的云服务器上。那时候产品需求简单,用户也不多,一切岁月静好。
但随着市场部一顿猛如虎的操作,客户量三个月翻了五倍。结果双1一大促当天,系统直接雪崩:API 响应时间飙到 30s+,数据库 CPU 打满,连后台管理页面都打不开。运维小哥半夜打电话给我:“小林,你们前端是不是又发了啥奇怪请求?” 我一脸懵:“我连后端代码都没碰过啊!”
后来复盘发现,根本问题不在前端——是单体架构扛不住高并发了。所有功能模块(用户管理、邮件发送、数据分析、Webhook 回调)全塞在一个进程里,一个慢查询就能拖垮整个服务。更糟的是,部署一次要停机十分钟,产品经理看我的眼神都快冒火了:“你这前端能不能让系统别老挂?”
那一刻我悟了:再不搞微服务,我就要被产品祭天了。
第一步:拆!但别乱拆
我们决定先做服务拆分。作为文科生,我对“领域驱动设计”这种词天然恐惧,于是拉着后端大佬老张一起画模块边界。原则很简单:高频调用、资源密集、独立业务逻辑的模块优先拆。
第一个被拎出来的是“邮件发送服务”。它依赖外部 SMTP,经常超时,还吃大量内存。我们用 FastAPI 重写,独立部署,通过 REST API 供主站调用。
# email-service/main.py
from fastapi import FastAPI, BackgroundTasks
import smtplib
app = FastAPI()
def send_email_async(to: str, subject: str, body: str):
# 模拟耗时操作
with smtplib.SMTP("smtp.example.com") as server:
server.sendmail("noreply@ourproduct.com", to, f"Subject: {subject}\n\n{body}")
@app.post("/send")
async def send_email(to: str, subject: str, body: str, background_tasks: BackgroundTasks):
background_tasks.add_task(send_email_async, to, subject, body)
return {"status": "queued"}
踩坑点1:一开始没加异步队列,直接同步发邮件,结果新服务自己也被拖垮了。后来引入 Celery + Redis,才算稳住。
上云原生:K8s 不是万能药
拆完几个核心服务后,我们决定上 Kubernetes。理由很现实:老板说“别家都在用云原生,显得我们很土”。
作为前端,我原本以为 K8s 就是个高级版 Docker Compose,结果第一天就栽了。写了个 Deployment,Pod 死活起不来,日志就一行:
CrashLoopBackOff: Liveness probe failed: HTTP probe failed with statuscode: 500
查了半天,原来是健康检查路径 /healthz 没实现。文科生的直觉告诉我:“这不就是个 ping 吗?随便返回 200 不就行了?” 于是:
@app.get("/healthz")
def health_check():
return {"status": "ok"} # 实际应该检查 DB、Redis 等依赖!
上线后三天,数据库挂了,但健康检查还是绿的,K8s 以为服务正常,继续往里导流,事故扩大。教训:健康检查不是摆设,得真查资源状态。
后来我们补上了真正的探针逻辑:
@app.get("/healthz")
def health_check():
try:
db.execute("SELECT 1")
redis.ping()
return {"status": "healthy"}
except Exception:
raise HTTPException(status_code=500, detail="Dependency down")
资源调度:别让 Python 成为瓶颈
我们主力后端语言还是 Python(团队历史包袱),但 GIL 和内存占用一直让人头疼。有次压测,一个数据分析服务的 Pod 内存飙到 2GB,被 K8s OOMKilled 了十几次。
踩坑点2:一开始只设了 requests,没设 limits,结果某个服务吃光节点内存,把其他服务全挤下去了。
后来我们做了三件事:
- 用
gunicorn+uvicorn部署 ASGI 应用,合理设置 worker 数(CPU 核数 * 2 + 1) - 严格限制每个 Pod 的内存上限(比如 512Mi)
- 对内存敏感的服务改用 Go 重写(别骂,真香)
| 服务类型 | 语言 | CPU Request | Memory Limit | P99 延迟 |
|---|---|---|---|---|
| 用户中心 | Python | 500m | 512Mi | 180ms |
| 邮件发送 | Python | 300m | 768Mi | 420ms |
| 实时分析 | Go | 1000m | 1Gi | 65ms |
数据不会骗人:Go 在 CPU 密集型场景确实碾压 Python。但考虑到团队熟悉度,我们只对关键路径做语言替换,其他仍用 Python —— 技术选型不是炫技,是平衡。
开发心得:文科生也能懂架构
作为一个曾经连“什么是负载均衡”都要百度的人,这两年最大的感悟是:架构演进不是一蹴而就的炫酷技术堆砌,而是对“资源”和“产品需求”的持续权衡。
- 产品要快速试错?那就别一上来就搞 Service Mesh,先保证交付速度。
- 资源有限?那就优先拆解故障域,而不是追求“彻底微服务化”。
- 团队能力不足?那就用托管服务(比如 AWS RDS、阿里云 ACK)降低运维复杂度。
上周五晚上,我又在加班调 HPA(Horizontal Pod Autoscaler)策略。产品经理路过,笑问:“小林,你一个前端怎么天天跟 K8s 打交道?”
我苦笑:“因为我不想再背锅了啊。”
最后几句真心话
从单体到云原生,我们走了弯路,也踩了坑,但每一步都值得。现在的系统,即使流量突增 3 倍,也能自动扩容、平稳运行。最爽的是——再也没在半夜被叫起来救火。
如果你也是非科班出身,别怕接触后端或架构。我一个写散文的都能搞懂 K8s,你肯定行。记住:技术没有高低贵贱,解决问题才是王道。
对了,最近我在学 eBPF 做网络观测……别问,问就是“产品经理说竞品用了,我们也得有”。
(完)

评论 0