后端架构演进:从单体到云原生——一个百度搜索算法工程师的“代码人生”

Nginx门卫
2025-12-17 11:38
阅读 1443

大家好,我是小陈,在百度干了两年算法,主要搞搜索相关。没错,就是那个你每天搜“今天杭州天气怎么样”背后默默搬砖的人(虽然其实不是我直接写前端展示逻辑 😅)。坐标杭州,常年在阿里网易的地盘上晃悠,偶尔投个简历感受下外面世界的温度。

上周五晚上10点,我正用VSCode写着Python脚本处理离线特征,突然手机一震——产品PM发来消息:“陈哥,咱这个新需求能不能下周上线?老板很看好!” 我内心OS:又来了……但嘴上只能回“好的,我们评估下”。

结果一评估,发现当前系统是典型的Java单体应用,跑在几台物理机上,数据库还是MySQL 5.6,部署靠手动 scp + restart。别说快速迭代了,改个配置都得提心吊胆,生怕把线上服务搞崩。去年双11期间就因为一个NPE(NullPointerException)导致搜索建议接口503,被值班同学疯狂@,那会儿真的想砸电脑。

于是,我和后端兄弟们一合计:是时候重构了!


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

刚入职那会儿,我们的后端系统其实挺简单的:

  • 一个Spring Boot项目
  • 所有业务逻辑塞在一个 repo 里(包括用户管理、搜索日志、推荐策略、甚至定时任务)
  • 数据库就一张大表 search_records,字段多到连Navicat都要卡
  • 部署方式:开发打包成 jar,运维手动拷贝到服务器,nohup java -jar app.jar &

听起来是不是有点熟悉?很多创业公司或早期项目都是这样起步的——快、糙、猛。产品经理要个新功能?改两行代码,测一下,上线!那时候我们甚至觉得“微服务”是大厂才玩得起的奢侈品。

但问题很快来了:

  1. 代码耦合严重:加个用户画像功能,不小心改到了日志模块,测试没覆盖到,线上炸了。
  2. 部署风险高:哪怕只改了个文案,也得全量发布,万一回滚慢,影响面巨大。
  3. 资源浪费:搜索高峰期CPU打满,但用户管理模块几乎空闲,没法弹性伸缩。
  4. 技术栈僵化:想用Python写个轻量级特征服务?不行,整个项目都是Java,团队也不敢轻易引入新语言。

最惨的是有一次,为了支持实时热搜词,我在单体里加了个WebSocket连接池,结果内存泄漏,GC频繁,整个服务响应时间从200ms飙到2s+。产品问我:“为啥热搜加载这么慢?” 我只能苦笑:“因为咱们的‘巨石’太重了。”


拆!拆!拆!微服务初体验

被逼无奈,我们决定走微服务化路线。目标很明确:按业务域拆分,让每个服务独立开发、部署、扩展。

我们先画了个粗糙的领域模型:

原单体模块 拆分后服务 技术栈
用户管理 user-service Java + Spring Cloud
搜索日志 log-ingestor Java + Kafka Producer
搜索核心 search-core Java (保留)
热搜计算 trend-calculator Python + Flask + Redis
特征工程 feature-engine Python + gRPC

对,你没看错,我们引入了Python!毕竟搞算法的,用Python写数据处理脚本效率高太多了。而且像trend-calculator这种计算密集型服务,用Python + NumPy/Redis比纯Java更轻便。

关键改造点

  1. API网关统一入口
    用Spring Cloud Gateway做路由,所有请求先经过它,再分发到各服务。配置如下:
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
        - Here comes the trend-calculator!
        - id: trend-service
          uri: http://trend-calculator:8000
          predicates:
            - Path=/api/trend/**
  1. 服务通信:REST + gRPC 双轨制
    内部高频调用(比如search-core调feature-engine)用gRPC,低频或外部对接用REST。gRPC的IDL定义清晰,性能也更好。

  2. 数据库拆分
    每个服务有自己的DB实例(或schema),彻底解耦。比如trend-calculator只读Redis和ClickHouse,不碰主MySQL。

  3. 配置中心 & 注册中心
    用了Nacos,服务启动自动注册,配置热更新。再也不用手动改properties文件了!

但微服务不是银弹。拆完第一天,我们就遇到了分布式事务问题:用户注册成功,但画像服务没收到消息。后来上了RocketMQ事务消息才搞定。还有一次,因为Kafka topic没建,log-ingestor直接OOM,差点背锅。


容器化:Docker 让部署不再玄学

微服务拆完,部署成了新痛点。每个服务依赖不同(Java 11 vs Python 3.9),环境变量一堆,运维小哥天天抱怨:“你们开发能不能统一一下版本?”

这时候,Docker救场了。

我们给每个服务写了Dockerfile:

# trend-calculator/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 . .

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

然后用docker-compose.yml本地调试:

version: '3'
services:
  trend-calculator:
    build: ./trend-calculator
    ports:
      - "8000:8000"
    environment:
      - REDIS_URL=redis://redis:6379
  redis:
    image: redis:6-alpine

终于实现了“在我机器上是好的” → “在任何地方都一样”。运维也松了口气:“现在你们只要给个镜像,我扔K8s里就行。”


上云原生:拥抱 Kubernetes + Service Mesh

今年初,公司决定全面上云,技术栈向云原生靠拢。领导说:“我们要具备分钟级弹性扩缩容能力,应对流量洪峰。”

于是,我们把Docker镜像扔进了Kubernetes集群。

K8s 配置片段(简化版)

# trend-calculator-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: trend-calculator
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: app
        image: registry/trend-calculator:v1.2
        ports:
        - containerPort: 8000
        resources:
          requests:
            memory: "256Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: trend-calculator-svc
spec:
  selector:
    app: trend-calculator
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8000

配合HPA(Horizontal Pod Autoscaler),CPU超过70%就自动扩容。双11预演时,流量突增3倍,Pod从3个秒扩到20个,稳如老狗。

但服务间调用链路变复杂了。怎么监控?怎么限流?怎么灰度?

我们引入了Istio作为Service Mesh。Sidecar代理自动注入,无需改业务代码就能实现:

  • 超时重试
  • 熔断降级
  • 流量镜像(用于灰度测试)
  • 分布式追踪(集成Jaeger)

比如,给search-core加个500ms超时:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
spec:
  hosts:
  - search-core
  http:
  - route:
    - destination:
        host: search-core
    timeout: 0.5s

运维同学看到Istio的Kiali面板后直呼“这比Zabbix酷多了”,而我终于不用在代码里手写Hystrix了(谁用谁知道,注解堆成山)。


架构对比:数字不会骗人

经过半年折腾,我们做了次性能压测(JMeter模拟1000并发):

指标 单体架构 云原生架构
平均响应时间 850ms 210ms
P99延迟 2.1s 450ms
部署耗时 15分钟 2分钟
故障恢复时间 10分钟+ <1分钟(自动重启Pod)
资源利用率 CPU平均30% CPU平均65%(弹性调度)

最爽的是,现在产品经理再来提“下周上线”,我们可以淡定回复:“没问题,feature分支测完,CI/CD自动发布到预发环境,你验收就行。”


给新手的几点血泪建议

  1. 别为了微服务而微服务
    如果你的系统QPS不到100,用户就几百个,单体完全够用。微服务带来的运维复杂度可能远超收益。

  2. 接口设计要“契约先行”
    用OpenAPI/Swagger定义好接口,前后端、跨团队协作效率翻倍。别等联调才发现字段名对不上。

  3. 日志、监控、告警必须跟上
    云原生环境下,服务动态启停,传统日志文件根本没法查。上ELK或Loki+Promtail,配合Prometheus+AlertManager。

  4. 数据库拆分要谨慎
    别一上来就分库分表。先垂直拆分(按业务),再考虑水平拆分。否则join查询会让你哭。

  5. 学会说“不”
    产品经理想要“实时个性化热搜+用户行为分析+AI预测”,但资源有限?优先保核心链路。非核心功能可以用异步队列削峰。


结语:代码人生,不止于写码

从单体到云原生,表面看是技术升级,本质是工程思维的进化——从“让代码跑起来”到“让系统活得好”。

现在的我,依然用VSCode写Python处理特征,但背后的服务已经跑在K8s集群上,自动扩缩容,故障自愈。上周产品又来提需求,我说:“这次我们用Serverless函数试试?” 他一脸懵,我笑而不语。

技术人的“代码人生”,不就是一边吐槽产品,一边用更好的架构让他们“为所欲为”吗?

对了,如果你也在杭州,搞搜索/推荐/大数据,欢迎交流!说不定哪天就在网易食堂偶遇了(他们家的糖醋排骨真香)。


P.S. 文中所有配置和代码均来自真实项目脱敏,如有雷同,纯属同行。
P.P.S. 别问为啥不用Go——因为团队Java/Python栈熟啊!技术选型,适合最重要。

评论 0

最热最新
暂无评论
Nginx门卫Lv.1
0
影响力
0
文章
0
粉丝