后端架构演进:从单体到云原生——一个985卷王的血泪实战

黄超
2025-12-18 16:28
阅读 1721

作者:某985大三狗,坐标北京,每天通勤一小时,用VSCode写Python,插件多到自己都记不清装了啥。最近在疯狂刷LeetCode和做项目准备秋招,顺便记录下自己在实习中踩过的坑。


上个月月底,我差点被线上事故送走。

事情是这样的:我在一家电商公司实习,团队负责的是商品爬虫+数据清洗服务。本来是个“小而美”的Python Flask单体应用,跑在一台4核8G的云服务器上,稳得一批。结果产品经理突然说:“咱们要支持实时监控竞品价格变动,爬虫频率从每小时一次提升到每分钟一次。” 我心里一咯噔——这不就是把原来的QPS从10干到600?系统肯定崩。

果不其然,双11前一周的周五晚上,我正啃着煎饼果子改简历,手机突然疯狂震动。运维群炸了:

“@后端小张,爬虫服务CPU 100%,数据库连接池打满,快来看看!”

当时我真的想砸电脑。但没办法,秋招在即,实习表现不能拉胯。硬着头皮登上服务器,htop一看,Python进程占满CPU;netstat一看,MySQL连接数爆了;日志里全是 Too many connectionsTimeoutError

那一刻我意识到:单体架构的舒适区,已经容不下业务的增长了。

于是,我被迫开启了一段从“脚本小子”到“云原生萌新”的痛苦转型之路。这篇文章,就是我这一个月来边学边干、边崩边修的实战复盘。如果你也在搞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

看起来清爽吧?但问题一大堆:

  1. 爬虫和API混在一起:用户调个接口,可能触发一次爬取,阻塞主线程。
  2. 没有异步:requests 是同步的,100个URL就得等100次网络IO。
  3. 资源争抢:Gunicorn worker既要处理HTTP请求,又要执行耗时爬虫,内存和CPU全被吃光。
  4. 无扩展性:想加机器?不好意思,状态都在本地,没法水平扩展。

最致命的是,所有逻辑都在一个进程里。一旦爬虫卡住(比如对方反爬),整个服务就挂了。上周五的事故,就是因为某个网站突然加了验证码,爬虫死循环重试,把数据库连接池耗尽,连后台管理页面都打不开。

产品经理还阴阳怪气:“你们后端是不是没做过高并发啊?”

我:???


第一步:拆!把爬虫扔进消息队列

被骂之后,我痛定思痛,决定先做最简单的解耦——把爬虫任务异步化

核心思路:用户请求只负责“下单”,真正的爬取工作交给后台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 + 微服务,但我拦住了。原因有三:

  1. 人力不足:全组就两个后端(包括我),还要对接前端、测试、产品。
  2. 成本太高:K8s学习曲线陡峭,运维复杂度指数级上升。
  3. 杀鸡用牛刀:我们的核心瓶颈只是爬虫,没必要把整个系统重构。

于是,我盯上了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作为备用方案(比如函数超时回退)

血泪教训 & 心得体会

  1. 不要为了技术而技术
    一开始我也想直接上Service Mesh,结果被导师骂醒:“你连基本的监控都没做好,搞什么Istio?” 技术选型一定要匹配业务阶段。

  2. 可观测性比功能更重要
    上K8s后第一件事不是写代码,而是搭Prometheus + Loki(日志) + Tempo(链路追踪)。现在出问题,5分钟内定位根因,再也不用求着运维查日志。

  3. Python也能高性能
    很多人觉得Python不适合后端,其实只要用对工具(gevent、asyncio、合理的架构),QPS上万不是梦。我们现在的API服务,单Pod能扛500 QPS,够用了。

  4. 秋招项目怎么吹
    面试官问:“你做过什么项目?”
    别说“我写了个爬虫网站”。要说:“我主导了后端架构从单体到云原生的演进,通过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

最热最新
暂无评论
黄超Lv.1
0
影响力
0
文章
0
粉丝