后端架构演进:从单体到云原生——一个985卷王的血泪实战
作者:某985大三狗,坐标北京,每天通勤一小时,用VSCode写Python,插件多到自己都记不清装了啥。最近在疯狂刷LeetCode和做项目准备秋招,顺便记录下自己在实习中踩过的坑。
上个月月底,我差点被线上事故送走。
事情是这样的:我在一家电商公司实习,团队负责的是商品爬虫+数据清洗服务。本来是个“小而美”的Python Flask单体应用,跑在一台4核8G的云服务器上,稳得一批。结果产品经理突然说:“咱们要支持实时监控竞品价格变动,爬虫频率从每小时一次提升到每分钟一次。” 我心里一咯噔——这不就是把原来的QPS从10干到600?系统肯定崩。
果不其然,双11前一周的周五晚上,我正啃着煎饼果子改简历,手机突然疯狂震动。运维群炸了:
“@后端小张,爬虫服务CPU 100%,数据库连接池打满,快来看看!”
当时我真的想砸电脑。但没办法,秋招在即,实习表现不能拉胯。硬着头皮登上服务器,htop一看,Python进程占满CPU;netstat一看,MySQL连接数爆了;日志里全是 Too many connections 和 TimeoutError。
那一刻我意识到:单体架构的舒适区,已经容不下业务的增长了。
于是,我被迫开启了一段从“脚本小子”到“云原生萌新”的痛苦转型之路。这篇文章,就是我这一个月来边学边干、边崩边修的实战复盘。如果你也在搞Python后端,或者正准备秋招想吹点高大上的项目经历,希望这篇能帮你少踩几个坑。
起点:那个“能跑就行”的单体应用
先说说我们原来的架构,典型的学生党/初创公司风格:
- 语言:Python 3.9
- 框架:Flask + Requests + BeautifulSoup(对,就是那种上课写爬虫用的组合)
- 部署:Gunicorn + Nginx,跑在单台ECS上
- 数据库:MySQL 5.7,主从都没配
- 任务调度:APScheduler,直接嵌在Flask app里
代码结构长这样:
project/
├── app.py # Flask入口
├── crawler.py # 爬虫逻辑
├── processor.py # 数据清洗
├── models.py # SQLAlchemy模型
└── config.py
看起来清爽吧?但问题一大堆:
- 爬虫和API混在一起:用户调个接口,可能触发一次爬取,阻塞主线程。
- 没有异步:requests 是同步的,100个URL就得等100次网络IO。
- 资源争抢:Gunicorn worker既要处理HTTP请求,又要执行耗时爬虫,内存和CPU全被吃光。
- 无扩展性:想加机器?不好意思,状态都在本地,没法水平扩展。
最致命的是,所有逻辑都在一个进程里。一旦爬虫卡住(比如对方反爬),整个服务就挂了。上周五的事故,就是因为某个网站突然加了验证码,爬虫死循环重试,把数据库连接池耗尽,连后台管理页面都打不开。
产品经理还阴阳怪气:“你们后端是不是没做过高并发啊?”
我:???
第一步:拆!把爬虫扔进消息队列
被骂之后,我痛定思痛,决定先做最简单的解耦——把爬虫任务异步化。
核心思路:用户请求只负责“下单”,真正的爬取工作交给后台Worker处理。技术选型上,我用了Celery + Redis,原因很简单:Python生态里最成熟、文档最多、面试还能吹。
改造后的流程:
用户请求 → Flask API → 发布任务到Redis → Celery Worker消费 → 存入DB
关键代码改动:
# tasks.py
from celery import Celery
import requests
from bs4 import BeautifulSoup
app = Celery('crawler', broker='redis://localhost:6379/0')
@app.task(bind=True, max_retries=3)
def fetch_product_price(self, url):
try:
resp = requests.get(url, timeout=10)
soup = BeautifulSoup(resp.text, 'html.parser')
price = extract_price(soup) # 自定义解析函数
save_to_db(url, price)
except Exception as exc:
# 重试机制,避免临时网络抖动导致失败
raise self.retry(exc=exc, countdown=60)
# api.py
from flask import request, jsonify
from tasks import fetch_product_price
@app.route('/trigger_crawl', methods=['POST'])
def trigger_crawl():
url = request.json.get('url')
task = fetch_product_price.delay(url) # 异步提交
return jsonify({'task_id': task.id, 'status': 'queued'})
部署上,我把Celery Worker单独起在另一台机器(其实是同一台机器的不同进程,穷学生买不起多台服务器),用supervisor管理。
效果立竿见影:
- API响应时间从平均2s降到200ms
- 即使爬虫挂了,用户也能正常提交任务
- 可以通过增加Worker数量横向扩展爬虫能力
但新问题来了:Redis成了单点故障,而且Celery的监控太简陋,想知道某个任务为什么失败,得翻日志翻到眼瞎。
这时候,我导师(一位阿里P7)淡淡地说了一句:“你这还是单体思维,只是把锅甩给了消息队列。”
我:……扎心了。
第二步:拥抱微服务?先别急,试试Serverless
其实团队里有人提议直接上K8s + 微服务,但我拦住了。原因有三:
- 人力不足:全组就两个后端(包括我),还要对接前端、测试、产品。
- 成本太高:K8s学习曲线陡峭,运维复杂度指数级上升。
- 杀鸡用牛刀:我们的核心瓶颈只是爬虫,没必要把整个系统重构。
于是,我盯上了Serverless。具体来说,用阿里云的函数计算(FC) 来跑爬虫任务。
为啥选Serverless?
- 按需计费:没任务时不花钱,对我们这种低频但突发流量的场景太友好了。
- 自动扩缩容:1个请求 or 1000个请求,平台自动分配实例。
- 免运维:不用管OS、网络、部署,专注写爬虫逻辑就行。
我把fetch_product_price函数改造成FC兼容格式:
# main.py (for Aliyun FC)
import json
import requests
from bs4 import BeautifulSoup
def handler(event, context):
evt = json.loads(event)
url = evt['url']
try:
resp = requests.get(url, timeout=10)
price = parse_price(resp.text)
# 写入表格存储(TableStore)替代MySQL
save_to_tablestore(url, price)
return {'status': 'success'}
except Exception as e:
print(f"Error: {str(e)}")
return {'status': 'failed', 'error': str(e)}
配合API网关,用户请求直接触发函数,完全绕过了Flask和Celery。
上线后第一周,成本从每月300块降到30块(主要是API网关费用)。运维同学看我的眼神都变了:“小伙子,有点东西啊。”
但Serverless也有坑:
- 冷启动延迟:首次调用要1~2秒,不适合实时性要求极高的场景。
- 超时限制:FC默认超时10分钟,有些深度爬虫会超时。
- 调试困难:本地模拟环境和线上行为不一致,一度让我怀疑人生。
不过对于爬虫这种短生命周期、无状态、可重试的任务,Serverless简直是天作之合。
第三步:向云原生靠拢——容器化 + K8s 初体验
虽然Serverless解决了爬虫问题,但我们的核心API服务(用户管理、商品展示等)还是单体Flask,随着功能增加,部署越来越痛苦:
- 每次改一行代码,都要重新打包整个镜像
- 测试环境和生产环境依赖不一致,经常“在我机器上能跑”
- 想灰度发布?不存在的,要么全上,要么全下
于是,在秋招压力(想写点K8s经验)和业务需求双重驱动下,我开始尝试容器化 + K8s。
第一步:Docker化
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
COPY . .
# 使用gunicorn启动,而不是flask run
CMD ["gunicorn", "-k", "gevent", "--workers", "4", "--bind", "0.0.0.0:8000", "app:app"]
注意这里用了gevent worker,让Gunicorn支持协程,能更好处理IO密集型请求(比如调外部API)。
第二步:写K8s YAML(哭)
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-api
spec:
replicas: 3
selector:
matchLabels:
app: backend-api
template:
metadata:
labels:
app: backend-api
spec:
containers:
- name: api
image: registry.cn-beijing.aliyuncs.com/my-group/backend-api:v1.2
ports:
- containerPort: 8000
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: backend-api-svc
spec:
selector:
app: backend-api
ports:
- protocol: TCP
port: 80
targetPort: 8000
说实话,第一次写这些YAML时,我感觉自己像个在拼乐高却丢了说明书的小孩。光是resources的单位(m vs Mi)就搞错好几次,导致Pod被OOMKilled。
但一旦跑起来,好处太多了:
- 环境一致性:开发、测试、生产用同一个镜像
- 弹性伸缩:配合HPA,CPU超过50%自动加Pod
- 滚动更新:
kubectl set image一条命令平滑升级,再也不用半夜发版
最爽的是,结合Prometheus + Grafana,终于能看到每个接口的QPS、延迟、错误率了。以前只能靠日志grep,现在直接看图,老板都说“可视化做得不错”。
架构对比:从单体到云原生的收益
为了秋招面试能吹清楚,我整理了个对比表:
| 维度 | 单体架构 | 异步解耦 | Serverless | 云原生(K8s) |
|---|---|---|---|---|
| 开发效率 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 部署复杂度 | ⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐⭐ |
| 扩展性 | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 成本 | 中 | 中 | 极低(按需) | 高(需集群) |
| 运维负担 | 低 | 中 | 极低 | 高 |
| 适合场景 | MVP、小项目 | IO密集型任务 | 事件驱动、短任务 | 复杂业务、长期演进 |
对我们团队来说,混合架构是最优解:
- 核心API:K8s容器化,保证稳定和可观测性
- 爬虫任务:Serverless函数,极致弹性+低成本
- 消息队列:保留Redis/Celery作为备用方案(比如函数超时回退)
血泪教训 & 心得体会
不要为了技术而技术
一开始我也想直接上Service Mesh,结果被导师骂醒:“你连基本的监控都没做好,搞什么Istio?” 技术选型一定要匹配业务阶段。可观测性比功能更重要
上K8s后第一件事不是写代码,而是搭Prometheus + Loki(日志) + Tempo(链路追踪)。现在出问题,5分钟内定位根因,再也不用求着运维查日志。Python也能高性能
很多人觉得Python不适合后端,其实只要用对工具(gevent、asyncio、合理的架构),QPS上万不是梦。我们现在的API服务,单Pod能扛500 QPS,够用了。秋招项目怎么吹
面试官问:“你做过什么项目?”
别说“我写了个爬虫网站”。要说:“我主导了后端架构从单体到云原生的演进,通过Serverless降低60%成本,K8s实现99.95%可用性,支撑日均10万+爬取任务。” —— 这才叫项目!
写在最后
从那个被线上事故吓到手抖的实习生,到现在能独立设计混合云架构的“准后端工程师”,我最大的感悟是:架构不是设计出来的,是被业务逼出来的。
每次崩溃、每次加班、每次被产品经理diss,都在推着你往前走。而作为985大三狗,我深知秋招战场有多卷。但正是这些实战经历,让我在简历上能写下“主导系统云原生改造”而不是“熟悉Flask”。
如果你也在准备秋招,别光刷题。找个开源项目,或者自己造个轮子,亲手把一个单体应用拆成微服务,再容器化上云。这个过程踩的每一个坑,都会变成面试时最硬的筹码。
毕竟,代码人生,不就是一边debug一边成长吗?
P.S. 本文所有配置和代码均已脱敏,实际项目更复杂。文中提到的技术栈均为真实使用,如有雷同,纯属同行。
P.P.S. VSCode插件推荐:Remote - SSH, Docker, Kubernetes, Python, REST Client。装完基本不用开终端(除了kubectl)。

评论 0