面灵AI→

美团核心本地商业后端一面:从 COW 追问到 Go 调度

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

《面试题目》

  1. 请做一下自我介绍。
  2. 耗时监控是怎么定位到线程阻塞问题的?
  3. 引入线程池后,如何处理线程间同步和任务优先级问题?IO 任务过载导致线程池排队的情况有没有遇到过?
  4. Copy-on-Write 优化,什么情况下会导致性能下降?
  5. 针对 COW 性能下降的情况,有后续的解决方式吗?
  6. Google Benchmark,有哪些保障性工作来保证测试结果的准确性或确定性?
  7. 插桩系统在高频递归调用或小函数时,插桩本身的开销会影响火焰图吗?
  8. 处理问题过程中,有没有遇到时间紧急的情况?
  9. 有没有主动尝试解决已发现的问题或使用新技术?
  10. 零件校验过程中如何确保精度问题?
  11. 批量检查的量级有多大?如果要从 10 提升到 100、1000,有什么改进方向?
  12. 异步写入日志过程中,意外退出或崩溃如何保障完整性?
  13. 结构化日志树是如何设计的?
  14. 如何处理 AI 这种长耗时任务?请求比较多的情况怎么办?
  15. 线程和协程有什么区别?
  16. Session 重用与内存复用策略是什么?频繁创建 session 的影响?有做预分配吗?
  17. MCP 协议主要解决什么问题?
  18. ReAct 流程中容易出现死循环或逻辑死锁,有什么优化手段?
  19. 设计一个能自主修复 C++ 编译错误的 AI Agent,结合 MCP 和 ReAct 说一下?
  20. 解释 epoll 的边缘触发和水平触发的区别?I/O 多路复用通常选择哪个模式?
  21. TCP 拥塞控制了解吗?
  22. MSL 大概是多长时间?
  23. TCP/IP 有几层模型?分别是什么?
  24. Go 的 GMP 调度模型?
  25. Go 的 defer 关键字有什么特征?三个 defer 的执行顺序?在函数中什么时候执行?
  26. Channel 有缓冲和无缓冲的区别?
  27. MySQL 用了哪些?JOIN 的用法?
  28. Raft 的一致性如何保障?
  29. 算法题:零钱兑换(求凑成总金额的硬币组合数)。
  30. 算法题:删除链表的倒数第 N 个节点(LeetCode 19)。
  31. 非科班怎么跑到计算机来的?
  32. 你在腾讯什么时候结束?

《参考解析》

**Copy-on-Write 的性能下降场景:**COW 只在「读多写少、对象大、复制次数有限」时划算,反过来就会变慢:写比例一高,每次写都要复制整块数据,写放大远超直接拷贝;对象很小时复制与引用计数的固定开销盖过收益;多线程并发读写同一个对象时,引用计数的原子操作会变成 cache line 争用热点,甚至产生伪共享;复制发生在写入路径上,会造成内存瞬时翻倍,在内存紧张的进程里直接触发更多回收。后续优化方向按成本排序:先减少无谓复制(用不可变快照 + 批量合并写,把多次小写攒成一次复制);再降低计数开销(线程本地计数、按需再加锁的共享计数、或者改用 RCU/epoch 延迟回收而不是精确计数);最后处理内存峰值(对象池/arena 复用,避免每次复制都走分配器)。

**线程池排队与耗时定位:**IO 任务过载导致排队,根因通常是 CPU 密集与 IO 密集任务共用一个池、队列无界、下游变慢后没有背压。做法是把两类任务拆到不同线程池,用有界队列加明确的拒绝策略,配合超时与熔断把压力还给上游;任务优先级不要指望在单池里用优先级队列解决——低优先级任务会被一直插队而饥饿,正确做法是多级队列 + 按比例调度。定位线程阻塞则是另一回事:先在请求链路上分层打点(入口、各阶段、下游调用分别计时),把 P99 而不是均值作为信号;再抓线程栈采样(jstack、pprof、perf)看大家卡在同一个位置,区分是锁竞争、连接池耗尽、下游超时还是 GC 停顿;最后把线程状态计数与耗时曲线对齐,才能从「慢」说到「被谁挡住」。

**异步日志与崩溃完整性:**异步入队天然存在一个丢失窗口——数据还在内存缓冲里进程就没了。能拿出来的保障手段有几层:关键日志先落本地文件或 WAL 再返回,或者批量写 + 定期 fsync 把窗口压缩到可接受范围;消息通道用持久化 + 消费确认,落库端做幂等;注册退出钩子做 flush,处理信号(TERM/INT)而不是直接被杀;日志文件分段并带 CRC 或长度前缀,启动时丢弃或修补尾部半截记录。回答这类题一定要主动给出「最多丢多少、能不能接受、怎么验证」的取舍,不然听起来只是罗列手段。结构化日志树的设计同理:先定字段规范(时间、级别、trace/span id、模块、事件、上下文),再定父子关系的表达方式(trace id + parent span id),最后考虑采样与分级输出,让日志能按一次请求串起来查。

**epoll 的 ET 与 LT:**水平触发(LT)只要缓冲区里还有未读完的数据就会持续通知,编程模型接近阻塞 IO,漏读一次下轮还会被叫醒;边缘触发(ET)只在状态发生变化(可读/可写事件到来)时通知一次,必须配合非阻塞 fd 一次性读到返回 EAGAIN 为止,否则剩下的数据可能永远不再触发事件。因此 ET 效率更高(减少了重复唤醒和系统调用)、但对代码正确性要求更严,是主流 Reactor 框架的选择;而且 ET 必须注册 EPOLLOUT 只在需要写时才开,否则会 busy loop。追问一般会落到「ET 下为什么要读到 EAGAIN」以及「ET + 非阻塞 + 一次读干净」这个组合上。

**TCP 拥塞控制与 MSL:**拥塞控制四个阶段要能串起来讲:慢启动时 cwnd 指数增长,到慢启动阈值后转拥塞避免线性增长;出现丢包(三个重复 ACK)走快重传 + 快恢复,把 cwnd 减半继续;超时重传则把 cwnd 降到最小重来。现代实现多是 CUBIC 或 BBR 这类基于带宽与延迟估计的算法。MSL 是报文在网络中的最大生存时间,主动关闭方在 TIME_WAIT 状态等 2MSL,一是为了让最后那个 ACK 丢失时对方重发的 FIN 还能被处理,二是让本次连接里的迷途报文彻底消散,避免串到新连接上;Linux 上 TIME_WAIT 的固定时长是 60 秒,高并发短连接场景要留意端口耗尽与 tcp_tw_reuse 这类参数的影响。

**Go 的 GMP、defer 与 channel:**GMP 是 goroutine(G)、内核线程(M)、处理器上下文(P)三层,P 的数量由 GOMAXPROCS 决定、代表真正并行的上限,G 挂在 P 的本地队列上运行;发生阻塞式系统调用时 M 会和 P 解绑,P 被交给其他 M 继续跑,本地队列空了会从全局队列或其他 P 那里偷任务。defer 的关键特征有三个:注册顺序执行、执行顺序为 LIFO(后注册的先执行),在函数返回之前执行(包括 panic 展开栈的时候),并且参数在 defer 语句执行时就已经求值——所以三个 defer 的输出是倒序的,而且循环里 defer 引用循环变量要小心捕获时机。channel 无缓冲是同步交接,发送和接收必须同时就位,天然做「交接完成」的信号;有缓冲是异步队列,缓冲满才阻塞,能起削峰作用。附带要知道:向已关闭的 channel 发送会 panic、关闭两次也会 panic、nil channel 永久阻塞,这些是常考边界。

**Raft 的一致性保障:**拆成三块:① 选主——每个任期最多一个 Leader,候选人要拿到多数派投票,随机化的选举超时避免瓜分选票;② 日志复制——Leader 收到写请求先追加本地日志,再并行发给 Follower,多数派确认后提交并应用到状态机,随后通知 Follower 提交;③ 安全性约束——「选举限制」保证新 Leader 一定包含所有已提交的日志条目,提交索引只能单调前进,从而保证已提交的日志不会被覆盖。工程上还要补:成员变更用联合共识或单步变更避免出现两个多数派;日志无限增长要靠快照 + 截断;只读请求可以走 ReadIndex 或 Leader Lease,不必写一条日志。收敛到一句:Raft 用多数派 + 任期 + 日志匹配三条规则,把一致性归约成「多数派同意」。

**长耗时任务与 ReAct 死循环治理:**AI 任务的长耗时来自串行的多步推理和外部调用,处理思路和异步任务系统一样:提交后立刻返回任务 ID,用轮询或推送回传进度;按用户与模型配额限流排队,避免请求多时互相拖垮;每一步的中间结果落库,支持断点续跑;超时与取消要能真正中断底层调用。连接与内存上,session 复用(连接池、模型客户端单例)能省掉反复建连的开销,但要评估并发下的资源占用,必要时做预分配和上限控制。ReAct 死循环的典型成因是工具返回没有信息增量、模型在原地重复同一个动作、或者缺少终止判据;手段是设最大步数与总预算、对 (thought, action) 做去重检测、每步强制回答「现在是否已经具备答案」、工具报错走指数退避并给出降级路径。被要求现场设计「能自主修复 C++ 编译错误的 Agent」时,按这个骨架回答:编译 → 解析报错定位文件与行号 → 检索上下文与历史修复案例 → 生成最小补丁 → 重新编译验证 → 失败则带着新信息重试,同时限定最多改哪些文件、不许改测试、每步留痕。

**线程与协程的区别:**线程由内核调度,栈通常是 MB 级,创建与切换要陷入内核,数量上限受内存与调度开销限制,共享内存需要锁来保护;协程在用户态调度,栈是 KB 级且可增长,切换只保存少量寄存器,所以能开到几十万个,概念上更适合「大量并发但每个任务大部分时间在等 IO」的场景。代价是协程不能真正并行(并行仍靠多线程/多核),一旦调用阻塞式系统调用就会占住底层线程,所以运行时要么把阻塞调用切到专门的线程上、要么要求全链路异步。两者不是替代关系,而是同一套并发模型的两层:用少量线程承载大量协程。