面灵AI→

美团 C++ 一面高强度拷打:自研通信层、服务治理与 STL 选型

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

《面试题目》

  1. 你这个项目为什么自己做通信层,而不是直接上现成 RPC?
  2. 自定义协议里,长度域、魔数、版本号你怎么设计?
  3. 序列化如果既要快又要可扩展,你会怎么取舍?
  4. 服务挂了你怎么发现?靠心跳还是靠调用失败?
  5. 注册中心脑裂或主从切换时,调用方怎么保证不错路由?
  6. 高可用你怎么定义?只说“多副本”不够,还看什么?
  7. 两副本数据不一致,读请求你怎么选?
  8. 选主过程中写请求来了怎么办?
  9. RPC 迁到公司内部框架,你觉得最先要改哪几块?
  10. 限流组件和注册中心职责怎么划?能互相替代吗?
  11. 压测时 QPS 上去了但延迟先爆,你怎么拆?
  12. std::map 和 unordered_map 你怎么选?
  13. 哈希冲突你更倾向链址还是开放寻址?为什么?

《参考解析》

  1. 自研通信层这类「为什么不用现成的」问题,答边界而不是情绪:说清当时的约束(链路短、字段固定、可控性优先、依赖与部署成本)和自研主动放弃的东西(不做服务治理全家桶、只做请求应答),再给出演进路径——要上平台时把编解码与传输层拆开,方便替换成公司 RPC。面试官反感的是「现成的太重」,欣赏的是「知道自己在哪一层欠了债、将来怎么还」。

  2. 自定义协议四件套要能默写:魔数、版本号、长度域、校验:魔数用于快速滤掉脏数据和误连;版本号用于灰度升级与新旧字段分流;长度域必须设上限,防止异常长度把内存打爆,同时它是解决 TCP 粘包/半包的关键——头定长、先收头再按长度收体,头和体分开校验。加分项是讲清「长度域到底指包长还是包体长」「要不要带序列化类型与压缩标志位」这些真实踩过的歧义。

  3. 注册中心脑裂与选主是同一类问题:接受不可用,拒绝错写:客户端本地保留最近可用的副本列表,注册中心抖动时先用缓存降级;路由变更带版本号,防止旧通知覆盖新结果;选主窗口内禁止写或把写悬停入队,客户端收到「领导未知」应退避重试而不是换节点乱写,等新主确认日志追平后再放写。写操作额外做幂等,避免切换窗口重复提交;大面积失败时切静态配置并保证可回滚。

  4. 高可用不是一个词,是一组可量化的指标:故障切换时间(RTO)、数据丢不丢(RPO)、是否出现双写冲突,再加依赖单点排查(配置、命名、限流中心都可能拖垮整条链路)和降级能力(弱依赖挂掉时核心链路能否继续跑)。最后补验证手段:压测与演练要覆盖杀主、杀从、断网,只测快乐路径的高可用是纸面上的。

  5. 压测里「QPS 上去、延迟先爆」要按排队论拆:先区分是 CPU、锁竞争、IO 还是内存分配抖动;再拆平均延迟和长尾,看是不是少数慢请求占住线程池;重点看队列堆积——线程池满之后等待时间会被算进延迟,表现为「吞吐不再涨、延迟线性涨」。定位靠火焰图或采样看热点函数,再决定是扩容、改批处理、调连接池还是减少锁粒度。

  6. STL 容器选型答三件事:语义、复杂度、常数:需要有序遍历或范围查询用 std::map(红黑树,O(log n) 稳定);只要平均 O(1) 查找且不要求顺序用 std::unordered_map;哈希对键质量与哈希函数敏感,最坏会退化,关键路径要评估。小数据量时常数比复杂度更重要,哈希表不一定更快;哈希冲突实现上,链址删除简单、负载高时性能下降平滑,开放寻址缓存友好但删除与聚集处理麻烦——选择取决于负载因子、内存局部性与删除频率。