面灵AI→

字节推荐架构一面:召回并行化与两级缓存

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 请做自我介绍。上一段实习是否已经结束、已经离职?
  2. 介绍第二个经历,重点讲与推荐架构更匹配的工作。
  3. 怎样发现召回链路问题,并给出什么解决方案?
  4. 原先的调用之间是否有依赖,为什么可以并行?
  5. 实际有多少数据源、分区和 RPC,原先哪些调用串行?
  6. 底层调用的是什么服务或存储?存储中的 Key 怎样区分场景,不同用户是否读取不同数据?
  7. 默认词是什么业务,怎样决定给用户展示什么?
  8. 为什么每个数据源使用独立线程池,能否共享一个召回线程池?
  9. ThreadPoolExecutor 怎样接收和调度任务,队列顺序是什么?
  10. I/O 型与 CPU 型任务哪个需要更多线程,线程池参数怎样确定?
  11. 并发后是否仍要等全部结果,最终耗时由什么决定?
  12. 主链路耗时较长,为什么并行改造后只优化了一小段?
  13. 这次改造是否降低 CPU、提高吞吐,压测结果是多少?
  14. 串行改并行后是否应调整各 RPC 和整条分支的超时?
  15. 介绍 Guava + Tair 两级缓存架构及命中率。
  16. 本地缓存怎样处理一致性?集群部署后,本地缓存是否最终会覆盖大部分数据?
  17. 使用本地缓存对发布、冷启动和日常运维有什么影响?
  18. 本地缓存对 GC 和单实例内存的影响是多少?
  19. 简单介绍第一个 Agent 实习。AI 工具具体在哪项开发中带来改进?请举一个例子。
  20. AI 能否自动部署、发 Debug 请求、修改并验证,当前缺口是什么?
  21. 怎样求二叉树两节点最近公共祖先,递归解法复杂度是多少?
  22. 每个节点有父指针时,怎样把 LCA 优化到 O(1) 额外空间?

《参考解析》

召回串行改并行:先确认”可并行”的依据——多个数据源的召回彼此不依赖、结果在最后融合,这才是并行的前提;如果后一步要用前一步的输出,并行就是错的。改造的完整链路是:定位(监控显示某几个 RPC 串行叠加占了主链路大头)→ 分段(把串行的 N 次调用改成并发提交)→ 汇总(CompletableFuture.allOf 或 invokeAll 等待全部完成,失败的按降级策略给空结果)→ 复测(P99 与吞吐对比)。

为什么每个数据源独立线程池:隔离故障与保护下游。共享一个池的话,一个慢数据源会把池里所有线程占住(线程饥饿),导致其他数据源也一起超时——本来是局部慢,变成整体雪崩。独立池的代价是线程总数变多、内存与上下文切换开销上升,所以要给每个池设上限并在监控里看队列长度与拒绝次数。

线程池参数怎么定:I/O 密集型任务线程数可以远大于 CPU 核数(常见经验是 核数 × (1 + 等待时间/计算时间)),因为线程大部分时间在等网络;CPU 密集型则接近核数(核数 + 1)。但经验公式只是起点,真正的参数要靠压测调——先设保守值,观察 CPU 利用率、队列积压、P99 和拒绝率,再逐步调整。队列必须有界,拒绝策略要能触发降级(返回空结果或走兜底数据源),而不是无限排队把延迟拖成雪崩。

为什么只优化了一小段:并发改造只压缩了”可并行部分”,按 Amdahl 定律,整体加速比受限于串行部分。如果主链路里还有一段强串行(如入口鉴权、特征获取、最终排序)占比很大,那么召回段即使从 200ms 压到 30ms,总耗时的改善也有限。要提升就得继续找下一个串行大头,或者把部分串行逻辑也改成可重叠执行。

超时要不要跟着改:必须改,而且要成体系。串行时整条链路的超时是各段之和,并行后如果保持原来的单段超时,最坏情况的分支会把总等待时间拖回串行水平;但也不能把超时砍得太短,那会让正常慢请求被误杀、降级率上升。合理做法是自上而下分配预算:先定整条链路的 SLA,再按段分配并留出余量,同时每个分支设独立超时并配降级结果。

Guava + Tair 两级缓存:L1 是进程内的 Guava Cache(纳秒级读取、网络零开销),L2 是 Tair/Redis(跨实例共享)。读路径是先查 L1,未命中查 L2 并回填 L1,L2 也没有才回源。收益是把热点数据挡在进程内,网络调用量和 L2 压力大幅下降。代价是一致性变难——本地缓存各自一份,更新时其他实例看不到。

本地缓存一致性:常见做法有三档:① 设一个很短的过期时间(如 1-5 秒),接受最终一致,适合对一致性要求不高的数据;② 通过消息广播失效通知(发布订阅),各实例收到后删除本地 key,注意消息丢失要有兜底(过期时间 + 定期全量刷新);③ 版本号/时间戳校验,读的时候比对版本,旧的就丢弃。要注意”广播更新本地缓存”本身不可靠(消息可能丢、实例可能离线),所以必须有 TTL 兜底。

本地缓存对发布、冷启动与运维的影响:① 发布时实例重启,本地缓存全空,会有一段时间大量请求穿透到 L2/DB,需要有预热机制或发布时的流量保护;② 集群规模大时,本地缓存让”每个实例各存一份”,内存总量被放大,需要评估单实例内存上限;③ GC 上要注意缓存的量级和对象大小,用软引用/权重上限避免把堆撑爆(Guava 的 maximumSize + weigher);④ 运维上要能观测命中率——如果本地命中率很高,说明大部分请求根本没到 L2,L2 的容量可以重新规划。

LCA:普通二叉树用递归——lowestCommonAncestor(root, p, q) 返回自身(若等于 p 或 q)或左右子树的非空结果;左右都非空说明当前节点就是 LCA,只有一边非空就上抛。时间 O(n)、空间 O(h)(递归栈)。如果有父指针,可以先各自上溯求两个链表的交点(类似”两个链表相交”):先求两节点深度差,深的先走差值的步数,再一起走直到相遇,空间 O(1)、时间 O(h)。