返回文章索引

BIGO 二面:Linux 性能采集、RPC 异常与 AI 工具面经复盘

复盘 BIGO C++ 后端二面,覆盖 Linux CPU 指标采集、多核归一化、Protobuf、熔断与数据积压、MCP、SSE、协程 M:N 调度和合并有序数组。

GitHub 原文

BIGO 二面:Linux 性能采集、RPC 异常与 AI 工具面经复盘

一、面试概况

本次面试约 36 分钟,问题基本围绕简历上的实习项目、分布式系统、协程 RPC 和 AI 工具项目展开,流程大致是:

  1. 自我介绍和项目经历;
  2. Linux CPU、负载、内存和软中断指标采集;
  3. 多核 CPU 使用率如何归一成一个监控指标;
  4. Protobuf、gRPC 和 bRPC;
  5. Manager 断连后的熔断、重试和数据积压;
  6. MCP、Skill 与 RAG 工具路由;
  7. SSE 与 WebSocket 的选择;
  8. 手写合并两个有序数组;
  9. 协程、线程和 M:N 调度;
  10. 技术广度、AI 模型、学习方式和反问。

这场面试最明显的特点是追问非常贴近工程边界。只回答“我使用了熔断”“我读取了 /proc”“协程可以提高并发量”还不够,面试官会继续追问计算公式、队列上限、数据是否丢失、线程数量和协议选择。

二、自我介绍与项目经历

自我介绍中主要提到了三个方向。

实习项目

实习参与的是一个类似分布式加密网盘的项目,负责采集子服务的性能指标,并根据指标计算 RPC 请求的节点权重,再通过负载均衡连接目标子服务。

相关指标包括:

  • CPU 使用率;
  • 系统负载;
  • 内存占用;
  • 软中断次数;
  • 网络相关指标。

RAG MCP 项目

当 Agent 可以调用的工具数量达到上百个甚至更多时,如果把全部工具定义放进 Prompt,会带来较大的 Token 消耗,也会增加模型选择错误工具的概率。

项目的思路是:

用户请求
    ↓
主 Agent 识别意图
    ↓
对工具描述进行向量检索
    ↓
计算相似度并选择 Top-K
    ↓
重排候选工具
    ↓
只把最相关的工具交给模型

Raft 与协程 RPC

学习 MIT 6.824 时实现了一个 Raft 一致性服务,并结合学校网站的需求搭建了协程化 RPC 框架。

自我介绍的问题在于项目名称很多,但缺少可以立即证明结果的数据,例如:

  • 节点数量和采样频率;
  • 动态权重更新周期;
  • 故障恢复时间;
  • RAG 工具检索的召回率;
  • Prompt 或 Token 实际减少比例;
  • RPC 的 QPS 和 P99 延迟。

以后介绍项目时,最好使用固定结构:

项目背景 → 我的职责 → 技术方案 → 最难的问题 → 解决过程 → 量化结果

三、Linux 性能指标如何采集

面试问题

面试官询问 CPU 使用率怎么采集,以及 CPU 指标包含哪些维度。

面试中的回答提到了 uptimetop/proc,也提到了通过 mmap 读取软中断统计、使用 eBPF 获取网络数据。方向没有完全错误,但缺少最关键的采样和计算过程。

CPU 使用率的来源

Linux 中可以读取 /proc/stat 的第一行:

cpu  user nice system idle iowait irq softirq steal guest guest_nice

这些数据是累计时间,不能读取一次就直接得到某个时间区间内的 CPU 使用率。通常需要间隔一段时间采样两次:

total = user + nice + system + idle + iowait + irq + softirq + steal
idle_all = idle + iowait

CPU 使用率 = 1 - Δidle_all / Δtotal

一个简单的 C++ 计算函数可以写成:

struct CpuTimes
{
    std::uint64_t user{};
    std::uint64_t nice{};
    std::uint64_t system{};
    std::uint64_t idle{};
    std::uint64_t iowait{};
    std::uint64_t irq{};
    std::uint64_t softirq{};
    std::uint64_t steal{};
};

double cpu_usage(const CpuTimes& before, const CpuTimes& after)
{
    const auto total = [](const CpuTimes& value) {
        return value.user + value.nice + value.system + value.idle +
               value.iowait + value.irq + value.softirq + value.steal;
    };
    const auto idle = [](const CpuTimes& value) {
        return value.idle + value.iowait;
    };

    const auto total_delta = total(after) - total(before);
    const auto idle_delta = idle(after) - idle(before);
    if (total_delta == 0)
    {
        return 0.0;
    }
    return 1.0 - static_cast<double>(idle_delta) / total_delta;
}

其他指标来源

指标 常见来源
CPU 累计时间 /proc/stat
Load Average /proc/loadavg
内存使用情况 /proc/meminfo
进程级状态 /proc/<pid>/stat/proc/<pid>/status
软中断 /proc/softirqs
中断 /proc/interrupts
网卡收发统计 /proc/net/dev/sys/class/net

eBPF 更适合采集传统统计文件无法直接提供的细粒度信息,例如函数耗时、调用路径、调度延迟、网络丢包位置和请求生命周期。

四、多核 CPU 使用率如何归一化

面试官给出了一个非常具体的场景:四个 CPU 核中,一个使用率为 80%,一个为 70%,另外两个基本空闲,如何最终得到一个监控值?

如果四个核的处理能力相同,并且每个核的采样区间一致,简单平均为:

(80% + 70% + 0% + 0%) / 4 = 37.5%

但工程实现不应该先计算每个核的瞬时百分比再随意加权。更准确的做法是直接读取 /proc/stat 中总 CPU 的累计时间,对所有 CPU 核在采样区间内的 ΔtotalΔidle 统一计算。

如果用于服务节点负载均衡,CPU 使用率也不能成为唯一指标。通常还需要结合:

  • Load Average;
  • iowait
  • 可用内存;
  • 请求延迟;
  • 错误率;
  • 活跃连接数;
  • 队列长度。

可以将不同指标归一化成 0—1 的分数,再组合成节点健康度:

score = a × cpu_idle
      + b × memory_available
      - c × io_wait
      - d × request_latency
      - e × error_rate

指标最好通过滑动窗口或 EWMA 进行平滑,避免瞬时抖动导致 RPC 流量在节点之间反复迁移。故障节点应通过健康检查摘除,恢复节点应慢启动,而不是立即恢复到最高权重。

五、为什么 RPC 使用 Protobuf

面试中的回答主要是“Protobuf 使用二进制格式,传输速度比较快”。这个结论不算错误,但不够完整。

Protobuf 的定位是接口定义语言和二进制序列化协议,而不是网络传输协议。它的主要价值包括:

  • 使用 .proto 文件统一定义消息和服务;
  • 自动生成多种语言的类型安全代码;
  • 编码体积通常小于 JSON;
  • 解析效率较高;
  • 使用字段编号支持向前和向后兼容;
  • 适合跨语言 RPC 服务。

gRPC 通常使用 HTTP/2 作为传输层,使用 Protobuf 作为默认消息格式。bRPC 也可以支持 Protobuf 定义的消息,但二者的运行时和协议实现并不因此完全相同。

常见的其他数据格式还有:

格式 特点
JSON 可读性好、调试方便,但体积和解析成本较高
MessagePack 二进制格式,使用方式接近 JSON
Thrift 同时提供 IDL、序列化和 RPC 能力
FlatBuffers 适合减少反序列化和内存复制的场景

回答这类问题时,不能只说“二进制比文本快”,还需要说明类型约束、代码生成、兼容性和生态支持。

六、Manager 断连后,数据到底怎么办

这是本场面试追问最深入的一部分。

面试官的追问链路

Manager 不响应
    ↓
是否一直发送
    ↓
是否需要熔断和重试
    ↓
已经采集的数据是否丢弃
    ↓
任务队列持续增长怎么办
    ↓
本地保存多少数据
    ↓
队列满时采用什么淘汰策略

面试中的回答提到了熔断、半开状态、定时重连、任务队列、LRU 和双端队列,但没有一开始就给出完整的数据生命周期设计,因此被连续追问。

更完整的处理方式

首先需要区分数据类型。

可以丢失的监控快照

监控数据通常更关心近期趋势,不一定需要永久保留每一个采样点。可以使用:

  • 有界环形队列;
  • 只保留最近 N 分钟数据;
  • 队列满时淘汰最旧数据;
  • 将高频快照聚合成分钟级数据;
  • 为数据设置 TTL。

不允许丢失的业务数据

关键业务数据不应该只存在内存队列中,可以使用:

  • Write-Ahead Log;
  • 本地数据库;
  • 磁盘队列;
  • Kafka、RabbitMQ 等可靠消息队列。

熔断与重试

熔断器通常包含三个状态:

Closed:正常发送
Open:停止发送,快速失败
Half-Open:放少量请求探测服务是否恢复

重试还需要:

  • 指数退避;
  • 随机抖动;
  • 最大重试次数;
  • 请求幂等;
  • 超时控制;
  • 恢复后的限速回放。

如果连接恢复后立即把所有积压数据同时发送,可能再次压垮服务端。因此,恢复阶段同样需要背压和发送速率限制。

一个更完整的面试回答可以是:

网络断开后进入熔断状态,停止立即发送,并通过指数退避进行半开探测。监控快照进入有界队列,只保留最近一段时间的数据,队列满时淘汰最旧数据或进行聚合;关键数据则落盘。连接恢复后按限制速率回放,同时监控队列长度、最老数据年龄和丢弃数量。

七、MCP、Skill 和 RAG 工具路由

面试官使用天气工具举例:既可以通过 MCP 直接调用,也可以在 Skill 中写说明并调用脚本,两者有什么区别?

MCP

MCP 更关注模型或 Agent 如何通过标准化方式访问外部工具和资源,例如:

  • 查询数据库;
  • 读取文件;
  • 调用天气服务;
  • 执行内部业务接口。

Skill

Skill 更偏向可复用的任务说明和工作流,通常包含:

  • 完成任务的步骤;
  • 输入输出约束;
  • 工具使用规则;
  • 模板、脚本和参考资料;
  • 验证和失败处理方式。

Skill 可以调用 MCP 工具,两者并不是互相替代的关系。

RAG 工具路由

工具数量较少时,可以直接把工具描述交给模型。工具数量达到几百或上千后,全量工具描述会增加 Token、延迟和误选概率。

RAG 工具路由可以采用:

  1. 将工具名称、描述、参数和使用场景向量化;
  2. 根据用户请求生成查询向量;
  3. 计算余弦相似度,召回候选工具;
  4. 对候选结果进行重排;
  5. 只把 Top-K 工具提供给模型;
  6. 模型选择工具并生成调用参数。

项目不能只用“降低 Token”证明有效,还应该评估:

  • 正确工具召回率;
  • Top-K 命中率;
  • 错误工具调用率;
  • 检索和重排延迟;
  • 最终任务成功率;
  • Prompt Token 减少比例。

八、为什么使用 SSE 而不是 WebSocket

这部分回答相对清楚:当前场景主要是服务端向客户端持续输出模型生成结果,不需要高频双向通信,因此 SSE 更简单。

对比项 SSE WebSocket
通信方向 主要是服务端到客户端 全双工
基础协议 HTTP 建立连接后升级为 WebSocket
数据类型 主要是 UTF-8 文本 文本和二进制
自动重连 浏览器原生支持 通常需要自行实现
适用场景 大模型流式输出、通知、日志 聊天、游戏、实时协作

推荐短答:

这里客户端提交请求后,主要由服务端持续推送模型输出,不需要高频双向消息,因此 SSE 已经足够。SSE 基于 HTTP,浏览器支持自动重连,实现和代理部署都更简单。如果需要实时双向交互或二进制传输,再考虑 WebSocket。

九、算法题:合并两个有序数组

面试中的方案是把 B 数组追加到 A 数组,再整体排序:

时间复杂度:O((m+n) log(m+n))

这个方案可以得到正确结果,但没有利用两个数组已经有序的条件。

如果 A 数组尾部已经预留足够空间,可以从后向前使用双指针:

#include <vector>

void merge(std::vector<int>& first,
           int first_size,
           const std::vector<int>& second,
           int second_size)
{
    int left = first_size - 1;
    int right = second_size - 1;
    int write = first_size + second_size - 1;

    while (right >= 0)
    {
        if (left >= 0 && first[left] > second[right])
        {
            first[write--] = first[left--];
        }
        else
        {
            first[write--] = second[right--];
        }
    }
}

该方案的时间复杂度为 O(m+n),额外空间复杂度为 O(1)

拿到算法题后,应该先确认:

  • 两个数组是否已经有序;
  • A 数组是否预留空间;
  • 是否允许额外数组;
  • 是否需要原地修改;
  • 空数组和重复元素如何处理。

十、协程、线程与 M:N 调度

面试中的回答把协程描述为轻量级用户态线程,提到了上下文、寄存器、线程池、M:N 和 epoll。大方向正确,但几个概念混在了一起。

线程与协程

  • 线程通常由内核调度;
  • 协程通常由用户态运行时调度;
  • 多个协程可以复用少量内核线程;
  • 协程切换通常不需要每次进入内核调度器;
  • 协程遇到普通阻塞调用时,仍可能阻塞整个工作线程。

M:N 是什么

M:N 表示 M 个协程映射到 N 个内核线程,并不是一个固定的线程数量公式。

CPU 密集型工作线程数量通常接近可用 CPU 核数。N+1 只是经验值,I/O 密集型使用 2N 也不是固定结论,最终需要根据阻塞比例、内存、上下文切换和压测结果调整。

协程 I/O 流程

协程发起非阻塞 I/O
    ↓
暂时不可读或不可写
    ↓
co_await 挂起协程
    ↓
事件循环通过 epoll 等待
    ↓
文件描述符就绪
    ↓
调度器恢复对应协程
    ↓
继续执行后续逻辑

回答协程问题时,应该明确区分:工作线程数量、协程数量、任务队列、事件循环和调度策略。

十一、技术广度与学习深度

面试官指出简历涉及的方向跨度较大,并追问每个库到底学习到了什么程度。

候选人提到:

  • C++ 后端;
  • Windows 与 Qt 客户端;
  • 常用中间件;
  • Boost、Asio 等开源库;
  • Drogon C++ Web 框架;
  • AI 编程工具。

用项目驱动学习是优点,但技术名称过多也容易让面试官怀疑深度。更稳妥的表达是主动划分掌握程度:

我目前最熟悉的是 Linux 性能采集、RPC 和 C++ 网络服务,可以深入解释实现和异常处理。Drogon、Qt 和部分中间件有实际使用经验;其他技术仍然处于源码阅读或 Demo 验证阶段。

“了解”“使用过”“熟悉”“精通”不能混用。简历上每写一个“熟悉”,都应该准备原理、实现、故障和性能四类追问。

十二、AI 模型与 Skill 使用

面试中提到了 DeepSeek、ChatGPT、Claude Code、Cursor、Codex 和 Trae 等工具。

“有预算就使用最新模型,预算不足就使用便宜模型”虽然是真实体验,但正式面试中可以换成更工程化的表述:

我会根据任务类型选择模型。复杂代码分析更关注推理和长上下文能力;批量生成和格式转换更关注速度与成本;涉及公司代码时还要考虑隐私和部署方式。我会用固定任务比较正确率、修改次数、延迟和 Token 成本,而不是只依赖主观体验。

对于 Skill,也不应该只说从 GitHub 下载或让 AI 自动生成。一个可复用 Skill 至少需要明确:

  • 输入和输出;
  • 执行步骤;
  • 可调用工具;
  • 权限边界;
  • 失败处理;
  • 测试样例;
  • 版本管理。

十三、面试官反馈与反问

面试官给出的直接反馈可以归纳为:

  • 实践和接触面比较广;
  • 部分内容真正亲手深入完成得可能不够;
  • 对应届生来说,网络等基础知识非常重要;
  • 学校环境中实际工程项目有限,更需要把基础打牢;
  • 公司使用 AI 的方式和主流工具没有本质区别,关键是结合业务、规避陷阱并拆分任务。

候选人询问了面试评价、结果安排和公司 AI 使用建议,反问意识是好的。但下一次还可以加入更贴近岗位的问题:

  1. 这个岗位当前主要负责哪些服务?
  2. 服务的请求规模、延迟目标和稳定性要求是什么?
  3. 团队最希望应届生前三个月完成什么?
  4. 团队如何进行代码评审、压测和线上故障复盘?
  5. 公司允许在什么边界内使用 AI 辅助开发?

十四、本场面试的主要优点

  • 有实际项目可以讨论,而不是只背面试题;
  • 对 Linux、RPC、分布式和 AI 工具都有主动探索;
  • 能够通过 Drogon 博客和小型项目验证学习结果;
  • RAG MCP 工具路由项目具有一定辨识度;
  • SSE 与 WebSocket 的场景选择回答比较清楚;
  • 面试结束时能够主动索要反馈和建议。

十五、最需要改进的地方

1. 先回答问题,再展开背景

多核 CPU 的问题应该先直接回答“等价核心可平均,更准确的是计算聚合累计时间差”,而不是继续讨论权重。

2. 基础概念必须准确

Protobuf 不是传输协议,M:N 不是线程数量公式,熔断也不能自动解决队列积压。

3. 异常场景要覆盖完整

网络断开问题至少要同时考虑:超时、重试、熔断、背压、有界队列、持久化、淘汰策略、幂等和恢复限速。

4. 算法题先利用已有条件

看到“两个有序数组”应该优先想到归并和双指针,而不是重新排序。

5. 项目必须准备量化结果

没有 QPS、延迟、资源占用、错误率、Token 降幅或准确率,项目价值很难被验证。

6. 减少口头语

回答时可以使用统一结构:

一句结论
→ 两到三个原理
→ 项目中的实现
→ 异常与取舍
→ 数据或测试结果

十六、下一轮重点复习清单

  • /proc/stat 和多核 CPU 使用率计算;
  • Load Average、iowait、软中断和上下文切换;
  • 动态负载均衡、EWMA、健康检查和慢启动;
  • Protobuf 编码、字段兼容和 gRPC HTTP/2;
  • TCP 超时、重试、熔断、限流和背压;
  • 有界队列、WAL、幂等、数据淘汰和恢复回放;
  • SSE、WebSocket 和 HTTP 长连接;
  • 线程、协程、M:N、epoll 和非阻塞 I/O;
  • Raft 选举、日志复制、快照和网络分区;
  • 双指针、滑动窗口、链表、哈希表和 LRU;
  • RAG 工具路由的召回、重排和效果评估。

十七、总结

这场 BIGO 二面展示了较强的学习主动性和技术广度,也暴露了应届生面试中很常见的问题:知道许多技术名词,但对计算公式、运行机制、异常边界和工程取舍准备不足。

后续最有效的提升方式不是继续增加新的框架,而是把 Linux 性能采集、RPC 异常处理和协程调度三个方向真正吃透。每个项目都准备好数据流、核心代码、故障场景、测试方法和量化结果,回答时再使用“结论—原理—项目—边界”的结构,下一轮表现会稳定很多。