58 同城 C++ 二面:广告检索漏斗、CTR 服务化与两道手撕
- 轮次
- 二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 广告检索这条漏斗,每一层的耗时预算是多少?
- 倒排索引和正排索引分别存什么,为什么不能只留一种?
- 多路召回的结果怎么融合,重复的广告怎么去重?
- CTR 模型怎么服务化,特征拼接放在哪一侧?
- 用户刚搜过的词,多久能进模型?
- 手撕:多路有序结果归并去重,取 top-k。
- 手撕:布隆过滤器,误判率怎么算?
- 布隆过滤器不支持删除,你们这个场景怎么绕?
《参考解析》
漏斗耗时预算要按「端到端 P99」倒推。 以一个 80ms 的检索请求为例:网关与序列化 10ms,召回 15ms,粗排 8ms,精排 25ms,重排 5ms,留 10ms 余量;每层都要有独立 deadline,超时就降级——精排超时退回粗排分数,重排超时按默认规则出。预算不是拍脑袋,要按量级和候选数估:召回 3000 条、粗排留 300、精排留 30、最终出 8 条,每层的单条耗时乘以条数就是计算预算,再决定并行、批处理还是 SIMD。压测要看 P99 而不是均值,尾部延迟才是用户感知到的卡顿。
倒排存「词 → 文档」,正排存「文档 → 属性」,职责不同不能省。 倒排用于召回:term 到 docId 列表,带词频、位置、权重,按 term 分片;正排用于过滤和排序:docId 到出价、类目、质量分、创意信息,随机读要快。只留倒排没法按出价/质量分排序,只留正排要全表扫。工程上用 docId 对齐,召回拿到一批 id 后批量 mget 正排,减少随机 IO;索引更新走增量 + 定期全量,删除用墓碑标记,避免读写冲突。
多路融合先解决「分数不可比」,再去重。 各路召回的分数量纲不同(BM25、余弦、规则分),先做归一化(min-max、分位数、z-score)或直接用排名融合(RRF),再加权;权重用离线评测扫,按 query 类型分桶。去重按广告 ID 保留最高分,同时按计划/广告主做频控,避免同一计划霸屏;跨路重复要在融合前做,否则重复项会挤占 top-k 名额。融合本身有成本,3000 条上做 SIMD 批处理能把归一化压到 1ms 内。
CTR 服务化:模型在服务端,特征分侧拼。 模型导出 ONNX/TensorRT,服务用 onnxruntime/Triton 推理;ID 类特征查 embedding 表(Redis/本地内存),数值特征在服务侧本地拼,避免把几百维特征整体 RPC 过去。模型文件大时用 mmap 加载,热更新用双 buffer 切换,切换瞬间会有少量抖动,要提前预热。要监控特征缺失率、模型版本、P99 延迟,特征和模型版本必须能对账,否则离线离线不一致很难查。
实时特征要分层看时效。 曝光点击走 Kafka,Flink 做秒级/分钟级窗口聚合写 Redis;用户画像 T+1;商品统计分钟级。刚搜过的词属于行为特征,通常秒级到分钟级可进模型,但要注意乱序、重复消费和窗口重启丢数据——开 checkpoint、给特征加事件时间、下游做幂等。对时效要求不高的场景,没必要追求实时,批流结合更稳。
多路归并取 top-k:小顶堆 + 哈希去重。 k 路各一个游标,堆里放每路当前元素,每次取堆顶输出,再从该路补一个;用哈希集合记录已输出 id,命中就跳过并继续推进,注意循环取到新值为止。复杂度 O(N log k),N 是总条数;某路耗尽要及时移出堆,否则会死循环;k 大于总条数时直接全量排序更简单。手写时注意比较器、重复元素、空路和边界。
布隆过滤器:m 位、k 个哈希、n 个元素,误判率约 (1 - e^(-kn/m))^k。 k 取 (m/n)·ln2 时最优;比如 1 亿条、1% 误判,算出来约 9.6 亿位(114MB)、k=7。它只保证「不存在一定不存在,存在可能误判」,不支持删除,也不支持枚举。实现上用 double hashing 从一个 64 位哈希派生多个位置,比调多次哈希函数快。
要删除就用计数布隆或换结构。 计数布隆把每位换成计数器,插入 +1、删除 -1,代价是内存涨几倍,且有计数溢出和并发问题;更稳的是「布隆 + 定期重建」:按时间分桶,查询时查多个桶,删除靠整体淘汰旧桶;或者直接用 Cuckoo Filter(支持删除、空间效率更好),以及 Roaring Bitmap(id 空间可控时)。选型看场景:广告去重这种「只增不改、可容忍极小误判」的,普通布隆最划算。