BIGO 二面:Linux 性能采集、RPC 异常与 AI 工具面经复盘
一、面试概况
本次面试约 36 分钟,问题基本围绕简历上的实习项目、分布式系统、协程 RPC 和 AI 工具项目展开,流程大致是:
- 自我介绍和项目经历;
- Linux CPU、负载、内存和软中断指标采集;
- 多核 CPU 使用率如何归一成一个监控指标;
- Protobuf、gRPC 和 bRPC;
- Manager 断连后的熔断、重试和数据积压;
- MCP、Skill 与 RAG 工具路由;
- SSE 与 WebSocket 的选择;
- 手写合并两个有序数组;
- 协程、线程和 M:N 调度;
- 技术广度、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 指标包含哪些维度。
面试中的回答提到了 uptime、top 和 /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 工具路由可以采用:
- 将工具名称、描述、参数和使用场景向量化;
- 根据用户请求生成查询向量;
- 计算余弦相似度,召回候选工具;
- 对候选结果进行重排;
- 只把 Top-K 工具提供给模型;
- 模型选择工具并生成调用参数。
项目不能只用“降低 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 使用建议,反问意识是好的。但下一次还可以加入更贴近岗位的问题:
- 这个岗位当前主要负责哪些服务?
- 服务的请求规模、延迟目标和稳定性要求是什么?
- 团队最希望应届生前三个月完成什么?
- 团队如何进行代码评审、压测和线上故障复盘?
- 公司允许在什么边界内使用 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 异常处理和协程调度三个方向真正吃透。每个项目都准备好数据流、核心代码、故障场景、测试方法和量化结果,回答时再使用“结论—原理—项目—边界”的结构,下一轮表现会稳定很多。