Java 这套 Bidder 我跟了五年,日请求从几千万涨到现在这个量级,一直是它在扛。栈我熟,量也是在它上面一点点做稳的。只是这两年越来越明显:改一个小功能,得先花大半天确认自己会不会碰到别的东西。
代码经手过三四任开发,各人写法不一样,需求一层层加上去,几乎没人回头清。当初为了赶某个 ADX 对接、临时挡一次线上异常写死的状态值,还留在主路径上;广告早就下线了,ID 还在名单和判断里;类和方法名后面挂着版本号后缀,光看名字分不出哪一套是现网在跑的。模型那块更死一点,加载、常驻、热更新、打分全在竞价进程内部。
再加上几件顺水的事。ADX 送过来的请求,看 Content-Type 就知道对端是 Go 写的;我们自己也想找个正经服务把 Go 用起来;公司又引进了 Cursor。这些凑到一块,我才一直惦记着动 Bidder。
真推起来是八月。当年一起搭 DSP 的老大来找我聊,我第三天写了第一版启动计划:Go 重写 Bidder、把推理拆成独立服务、再单独建一套模型管理后台。后来和产品评估,两个月装不下后两件,这一期就先收成重写竞价引擎。
这套 Bidder 现在难在哪
Bidder 接 ADX 的竞价请求,一次进来要定:这个流量出不出、出哪条、出多少。ADX 通常给到 100ms 左右,扣掉两趟网络往返,真正留给我们算的只有二三十毫秒,定向、频控、召回、打分、出价全挤在这一段里。它是 DSP 里真正花钱的地方,也是整套系统资源吃得最多的服务。
麻烦的不是逻辑有多绕,是分不清哪些还活着。改一处过滤,得先翻:这个状态值是谁加的,当时挡的哪个 ADX,现在还有没有流量走到这里。翻不出结论,就拿现网日志比对,或者干脆在旁边再加一个判断——上一任大概也是这么想的,所以才有今天这一堆。
模型内置把问题钉死了。模型常驻在竞价进程里,扩容得按最重的那个模型拉机器,高峰加的内存有一大半是给模型的;上下线靠进程内热更新,或者重启一批实例;算法跑个模型 AB,常常要我们改工程代码、跟着发一版 Bidder。竞价要稳、要按时返回,模型要快速换版、单独扩缩,两种节奏塞在一个进程里,谁都不舒服。线上推理是自建的 Java ORT,加载和打分仍跟竞价绑在一起。
为什么非得换 Go,启动计划里我自己先泼过冷水:历史冗余、命名混乱,说到底是清理,用 Java 一样能清;只有模型和业务耦在一起,非改架构不行。Go 比「Java 清干净再把推理挪出去」多的,是模型移出之后更轻的内存、更稳的 P99,以及重写时把工程链路和打分边界直接写进结构里。这两条还没有我们自己的压测数,得等 PoC。另一半没那么硬核:对端已经在用 Go,我们也想在延迟紧、逻辑杂的服务上真用一次,而不是停在写几个接口。
Cursor 上了以后,样板和测试可以先让它出一版,人盯设计和并发。风险是它很会把旧 Java 一行行翻译过来,不该留的东西跟着进来。所以规矩先立好:不照搬现有实现,按标准流程重新搭——解析、定向、拦截(过滤和频控)、召回、调模型、出价、拼响应,细节顺着这条流程往里补。
这次是怎么推进的
上半年没立上项。三月提过一次,算法同事觉得风险太高,我那会儿只有口头方向,范围和回滚一条都没写出来。五月我重构数据表、追踪服务和结算服务,出了一次事故:一个广告主的曝光漏了去重,持续半个月。事情本身不算严重,但人一多就开始来回拉扯,服务器成本、指标异常、模型效果不好,都往这次事故上挂。那之后我对 Bidder 重构也就不太积极了。
八月 CTO 来找我,就是当年一起搭 DSP 的老大。聊了很久,我挺兴奋,第三天把启动计划写出来,日期是 8 月 11 日。那一版是三套系统:Go Bidder 只管工程链路,无状态;推理独立,Bidder 用 gRPC 要分数;再单独建模型管理后台,管上下线、离线对拍、特征配置、模型 AB 和监控,算法自己做实验,不用再找工程当传话筒。
那一版心思大半在后两件。推理不打算从零写引擎,是业务薄壳加成熟后端:契约、特征拼装、模型分流、超时降级自己写,加载、热切换、动态 batching 交给后端。后端列了三条路:自建 ORT 最轻,现网已经验证过;Triton 能力全但镜像重;TF Serving 跟我们 PMML 加 ONNX 不太搭。没锁定。后台是为了把模型运营从工程发版里摘出来。次序也想过:先在 Java Bidder 上改成调远程推理,把延迟和稳定性验掉,Go 这边同步写,验过再接同一套。
再拿去聊,后两件在期限内控不住。选型、C++ 薄壳、管理后台、PMML 转 ONNX 还得分数对得上,每一项都定不死时间。两个月三套一起上,一处卡住就滑出期限。所以这一期先保 Bidder 按时上线,打分继续走进程内,外置和后台往后放。
然后给部门回了两份起草。一份技术方案,写清骨架怎么搭、为什么不照搬 Java、Go 为什么用、推理和外置后台为什么这期先收掉;另一份给产品和排期看。会上估了大约两个月。上半年推不动,就是没法评估;这次把做什么、多久、先保什么摆出来。范围也划了:结算、追踪、业务后台这一期不碰,OpenRTB 语义不动。五月漏去重写成了上线前必验的一条。
接下来两个月想做成什么样
这是第一次把 Go 放到生产上,而且直接是高流量的 Bidder。五六年前用 gin 写过接口,语法和工程设计心里都没那么有底。信心不在语言,在 Cursor、对竞价流程的熟悉、这几年攒下来的经验。流程我清楚,语言可以边写边补,拿不准的先让模型跑一版再对回去。所以不太担心,边做边学。
上线不会一刀切。先拿现网请求做回放对拍,再影子流量(完整跑但不返给 ADX),然后按比例灰度,和 Java 对账竞价量、win 率、花费。跟量、跟率相关的逻辑,上线前都得先验一遍。
新仓库不搬 Java 实现,骨架按标准流程搭,细节按现在还在投的业务往里补。下线广告 ID、临时状态值、带版本号后缀的命名都不跟着进来。Nacos、Redis、Kafka 继续复用。推理这期还在进程里,打分调用先按将来要 RPC 出去的样子留口,下一刀接 ORT 或 Triton 时不用再挖一遍。
第一版想一次做成三套系统,这一期收成一件事:把跑了五年的竞价引擎,在 Go 里重新做成后面还能接着改的东西。