后端架构演进:从单体到云原生,一个搜索工程师的血泪史

朱超_后端
2026-01-03 01:58
阅读 255

大家好,我是百度搜索中台组的一名算法工程师,入组快两年了。平时除了调模型、搞召回排序,也经常被拉去“救火”——比如系统扛不住流量、接口慢得像树懒、资源打满告警刷屏……上周五晚上11点,我正戴着耳机听《Radiohead》写Go服务的重构代码,运维兄弟突然在群里@我:“线上CPU飙到95%,再不处理明天早会你就站C位了。”那一刻,我真的想拔网线跑路。

但跑是跑不了的。毕竟简历上写着“高并发系统优化经验”,跳槽时HR问起架构演进,总不能说“我们还在用2016年的单体Spring Boot”。所以今天这篇,不灌鸡汤,不画大饼,就聊聊我们组这两年如何把一个臃肿不堪的单体后端,一步步拆成云原生微服务——顺便说说为什么现在求职市场对Go和云原生技能这么看重。


起点:那个又大又慢的“巨石”

两年前我刚来的时候,核心搜索后端还是个典型的Java单体应用。所有模块——查询解析、索引读取、相关性计算、日志埋点——全塞在一个Tomcat里。本地跑一次启动要4分半,改一行配置就得等一杯咖啡凉透。更离谱的是,每次上线都得全员戒严:测试堵门、产品蹲群、运维守着Kibana,生怕半夜被叫起来回滚。

资源利用率?别提了。为了扛住双11峰值,我们常年按最高负载预留机器,结果平时CPU平均不到15%。老板看监控图直摇头:“这钱烧得比我耳机还贵。”

最致命的是扩展性差。有一次产品说要加个“实时热词推荐”,技术方案一评审,发现得改七八个耦合模块。最后硬塞进去,结果上线三天后,因为某个小循环没加缓存,直接拖垮整个服务。那晚我和运维大哥在机房啃泡面到凌晨三点,他边敲命令边叹气:“你们算法是不是觉得服务器是无限的?”


第一步:微服务拆分,用Go重写关键路径

痛定思痛,领导拍板:拆!但怎么拆?全盘重写风险太大,于是我们采用“绞杀者模式”——新功能用新架构,老功能逐步迁移。

而语言选型成了第一个战场。团队原本清一色Java,但新服务需要极致性能和低内存开销(毕竟搜索对延迟敏感)。有人提议用Rust,结果被运维否了:“你们谁敢上生产出core dump?” 最后我们选了Go:编译快、并发模型简单、二进制部署无依赖,最关键的是——招人容易。

求职市场上,会Go的后端工程师薪资普遍高出15%-20%,尤其在搜索、广告这类高并发场景。

我们第一个用Go重写的模块是“查询预处理器”。它负责解析用户Query、做拼写纠错、实体识别,属于高频低延迟路径。对比下来,效果惊人:

指标 Java单体版 Go微服务版
P99延迟 128ms 37ms
内存占用 2.1GB 320MB
启动时间 250s 1.2s
线程模型 Tomcat线程池 Goroutine(轻量级)

代码也清爽多了。比如并发请求下游服务,Java得搞CompletableFuture链式调用,Go直接:

func ProcessQuery(ctx context.Context, query string) (*Result, error) {
    // 并发执行多个子任务
    var wg sync.WaitGroup
    var spellResult, entityResult *Result
    var spellErr, entityErr error

    wg.Add(2)
    go func() {
        defer wg.Done()
        spellResult, spellErr = spellCorrect(ctx, query)
    }()
    go func() {
        defer wg.Done()
        entityResult, entityErr = extractEntity(ctx, query)
    }()
    wg.Wait()

    if spellErr != nil || entityErr != nil {
        return nil, errors.Join(spellErr, entityErr)
    }
    // 合并结果...
}

上线后,不仅延迟降了70%,连带减少了30%的机器资源。老板终于笑了:“省下的钱够给你们买新耳机了。”


云原生:不是换个K8s就完事了

但微服务只是开始。随着服务数破百,新问题来了:部署混乱、配置散落、链路追踪靠grep日志……这时候,“云原生”三个字开始频繁出现在周报里。

我们没直接上Service Mesh(太重),而是先做三件事:

  1. 容器化:所有服务Docker化,用多阶段构建瘦身镜像。Go天然适合这点——静态编译后一个二进制文件搞定。
  2. 声明式部署:YAML定义Deployment + HPA(水平扩缩容),根据CPU或自定义指标自动伸缩。双11前再也不用手动扩容了。
  3. 可观测性:集成Prometheus + Grafana + Jaeger,每个服务暴露/metrics,关键接口打Span。

举个例子,HPA配置让资源利用率从15%提到60%以上:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: query-processor-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: query-processor
  minReplicas: 5
  maxReplicas: 50
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"

当然,踩坑少不了。有次HPA配错了指标单位,Pod疯狂扩缩,账单暴涨。运维差点把我工牌挂闲鱼上。


资源与成本:架构师的终极KPI

在百度这种体量公司,架构演进最终要回答一个问题:省了多少钱?

云原生带来的不仅是技术先进性,更是资源效率的飞跃。我们做了个粗略统计:

  • 单体时代:100台8核32G机器,常年空转
  • 微服务+K8s后:通过混部+弹性伸缩,同等流量只需65台,且可动态调整

更别说故障隔离带来的稳定性提升——以前一个模块OOM,全站挂;现在最多影响一个服务,熔断几秒就恢复。

这也直接影响了团队的OKR。今年Q2,我们组的KR之一就是“降低单位查询资源消耗15%”。听起来枯燥,但真能落地,年终奖就厚了。


给想跳槽的同学:Go + 云原生 = 求职加速器

写到这里,不得不提一句:如果你在准备后端岗位求职,Go和云原生经验几乎是标配了

我帮内推过几个候选人,凡是简历上有“基于K8s的微服务治理”、“Go高并发服务优化”的,基本都能进二面。反观只会CRUD Spring Boot的,连笔试都过不了。

不是说Java不行,而是行业在变。搜索、电商、金融——所有高并发场景都在往云原生迁移。而Go凭借其简洁、高效、部署友好的特性,成了新服务的首选语言。

我自己也是被“逼”学的。去年想跳槽去字节,一看JD:“熟练掌握Go,有云原生落地经验优先”。连夜啃完《Cloud Native Go》,现在反而觉得比Java香。


写在最后

从单体到云原生,不是一场技术革命,而是一步步被业务逼出来的自救。过程中有凌晨三点的报警,有PR被拒十几次的崩溃,也有看到P99曲线陡降时的爽感。

但回头看看,那些熬过的夜、掉过的头发,都换成了简历上亮眼的项目经验和面试时的谈资。更重要的是,系统稳了,老板笑了,我们也能准时下班听歌写代码了——虽然耳机还是那副百元杂牌。

架构没有银弹,只有合适。但在今天这个时代,不懂云原生的后端,就像不会用IDE的程序员——不是不能干活,只是效率太低。

共勉。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝