技术探索这事儿,真不能光靠教程活着

春花秋月
2026-01-03 13:17
阅读 2204

上周五晚上十点半,我盯着屏幕上一个诡异的线上报警,内心一万只羊驼奔腾而过。刚入职新公司两个月,就碰上核心搜索链路的 QPS 突然掉了 30%,运维那边催得跟催命符似的。更离谱的是,这 bug 在本地和测试环境死活复现不出来——典型的“线上玄学”。

我是谁?百度干了两年搜索算法的老油条,现在跳槽到一家中型互联网公司做后端(对,你没看错,算法转后端,别问,问就是想多拿点年终奖)。虽然简历上写得天花乱坠,但实话实说,这两年重度依赖 ChatGPT 和 Claude 写代码、查问题、甚至写周报。不是我不想自己写,是 deadline 不等人啊!尤其这次求职时被 HR 反复问:“你有高并发经验吗?” 我嘴上说“有”,心里慌得一批。

于是,这场“技术探索与实践”的血泪史,就这么开始了。


教程救不了你,但能给你个起点

事情起因其实挺简单:团队要重构旧版搜索服务,从单体架构拆成微服务。我负责的模块需要对接新的召回引擎,用 gRPC 调用,QPS 预估 5w+。听起来不难?可现实是,老系统用了三年没人敢动,文档全靠口口相传,连部署脚本都是 Shell + Python 混搭,注释比代码还长的那种。

一开始我天真地以为,照着网上那篇《gRPC 高性能实战教程》抄一遍就行。结果呢?教程里跑 1000 QPS 很稳,我一上生产,2000 就开始 OOM。当时真的想砸电脑——教程里压根没提连接池配置、流控策略、还有 TLS 握手开销这些“脏活”。

教训一:教程是理想世界的投影,现实世界全是坑。

后来我冷静下来,把 ChatGPT 当“高级搜索引擎”用:不是让它直接写代码,而是问“gRPC 在高并发下内存暴涨可能原因有哪些?”、“如何监控 Go 的 goroutine 泄露?”。它给的线索让我意识到问题出在 client 连接没复用——每次请求都新建连接,goroutine 疯涨,GC 压力爆表。

于是赶紧改用 grpc.ClientConn 全局复用,配合 sync.Pool 管理请求上下文。关键代码如下:

// 初始化全局连接池(实际项目中会加健康检查)
var (
    grpcConn *grpc.ClientConn
    once     sync.Once
)

func GetGRPCClient() (pb.SearchServiceClient, error) {
    var err error
    once.Do(func() {
        grpcConn, err = grpc.Dial(
            "search-engine:8080",
            grpc.WithInsecure(), // 生产记得换 TLS
            grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(100*1024*1024)),
            grpc.WithUnaryInterceptor(loggingInterceptor),
        )
    })
    if err != nil {
        return nil, err
    }
    return pb.NewSearchServiceClient(grpcConn), nil
}

加上这段之后,QPS 稳了,内存也降下来了。那一刻,我仿佛看到了年终奖在向我招手。


技术选型不是炫技,是权衡的艺术

解决了连接问题,又来了新麻烦:召回结果延迟太高。产品经理拿着用户反馈来找我:“为啥搜‘iPhone’要等两秒?” 我看了一眼日志,好家伙,平均 P99 延迟 1.8s,其中 1.2s 花在等待下游服务。

这时候,团队里有人提议:“上 Redis 缓存吧!教程里都说缓存万能!” 但我犹豫了。因为我们的查询词长尾分布极严重,80% 的 query 都是唯一或低频的,缓存命中率预估不到 15%。而且缓存一致性也是个雷——召回结果每小时更新一次,但用户可能搜到过期内容。

于是我做了个小实验:对比三种方案的预期收益。

方案 预估缓存命中率 开发成本 运维复杂度 用户体验提升
全量 Redis 缓存 <15%
LRU 本地缓存(Go) ~30%
优化下游调用(批处理+异步) N/A

最终我们选了第三条路:把原本串行调用多个召回源的方式,改成批量并行 + 异步合并。虽然代码复杂度上去了,但 P99 直接干到 400ms 以内,而且不引入新组件,运维同学直呼“懂事”。

这里有个小技巧:用 errgroup 控制并发超时,避免某个慢服务拖垮整个请求。

func batchRecall(ctx context.Context, queries []string) ([]*Result, error) {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10) // 控制最大并发数

    results := make([]*Result, len(queries))
    for i, q := range queries {
        i, q := i, q // 闭包陷阱!
        g.Go(func() error {
            res, err := recallEngine.Call(ctx, q)
            if err != nil {
                return err
            }
            results[i] = res
            return nil
        })
    }
    return results, g.Wait()
}

你看,技术探索不是“哪个新就用哪个”,而是在业务约束、团队能力、系统稳定性之间找平衡点。这也是我在百度那两年学到的最宝贵的东西——别为了 KPI 堆砌技术,要为用户价值服务。


从“能跑就行”到“可维护才是王道”

最后说点题外话。以前在百度,代码 review 特别严格,注释不全、变量命名随意、魔法数字一堆,直接打回。刚开始觉得烦,现在真香了。

这次重构,我坚持三点:

  1. 所有对外接口写清楚 OpenAPI 文档(用 Swagger)
  2. 关键路径加埋点指标(比如 recall_latency_seconds
  3. 配置项全部抽到 YAML,支持热加载

结果上周线上一个小波动,我直接通过 Grafana 看到是某个召回源响应变慢,5 分钟定位问题,10 分钟回滚配置。要是还像老系统那样“硬编码+无监控”,估计又得熬通宵。


写在最后:技术探索的本质是解决问题

回头看这段经历,其实没啥高深理论,就是遇到问题 → 查资料 → 小步验证 → 快速迭代。教程、AI 工具、开源项目,都是你的外挂,但方向盘得自己握紧。

如果你也在求职、转岗、或者被老板逼着搞新技术,我的建议是:

  • 别迷信教程,动手试才是真理
  • 后端开发的核心不是框架,是数据流 + 错误处理 + 可观测性
  • 代码可读性 > 一时爽的黑科技

毕竟,我们不是在写艺术,是在修高速公路——得让后来的人也能安全、快速地开过去。

对了,今天下班前运维又来找我:“那个报警是不是你修好的?”
我微微一笑:“嗯,顺便加了监控。”
他愣了一下:“牛啊!”

那一刻,我觉得,值了。

评论 0

最热最新
暂无评论
春花秋月Lv.1
0
影响力
0
文章
0
粉丝