本文档结合 traffic-manager 项目实战经验,覆盖 SRE 实习面试可能涉及的全部知识点,包括项目深度挖掘、SRE 核心概念、Linux/Kubernetes 系统知识、故障排查、可观测性等方向。
1. 项目深度挖掘
Q1: 请详细描述 traffic-manager 的核心工作原理
答: Traffic Manager 是一个基于 eBPF 的 Kubernetes Service L4 透明负载均衡器,核心工作流程:
| |
关键数字:
- eBPF 查找复杂度:O(1) Hash + O(log N) 二分搜索
- 对比 iptables:O(n) 规则遍历
- 性能提升:正常场景 +19.47%,500 个干扰 Service +27.74%
Q2: 代码中如何处理 TCP 和 UDP 的差异?
答: TCP 和 UDP 在 socket 层的行为差异决定了 eBPF 钩子的使用:
TCP 处理(cgroup/connect4):
| |
- TCP 是面向连接的,
connect()建立后方向固定 - 只需在连接建立时重写一次目标地址
- 后续数据传输不再需要 eBPF 干预
UDP 处理(sendmsg4 + recvmsg4):
| |
- UDP 无连接,每次
sendmsg()/recvmsg()都需要处理 - 需要亲和性表(udp_affinity_map)保证同一客户端路由到同一后端
- 需要反向 NAT 表(nat_sk_map)恢复源地址(后端回复时源是 Pod IP,需改为 VIP)
Q3: 加权负载均衡的具体实现细节是什么?
答: 加权负载均衡分为用户态权重计算和内核态权重选择两部分:
用户态权重计算(metrics + controller):
| |
内核态二分搜索(bpf/cgroup_sock.c):
| |
权重前缀和示例:
| |
Q4: 为什么用 LRU_HASH 而不是普通 HASH?
答: UDP 亲和性表和 NAT 表需要自动淘汰机制:
| Map 类型 | 淘汰策略 | 适用场景 |
|---|---|---|
BPF_MAP_TYPE_HASH | 无,满了返回错误 | 静态数据(Service、Backend) |
BPF_MAP_TYPE_LRU_HASH | 满时淘汰最久未使用 | 动态会话数据(UDP 亲和、NAT) |
为什么需要 LRU:
- 避免内存泄漏: UDP 客户端可能崩溃或断开,无法主动清理映射表
- 自动过期: 60 秒超时(
LB_UDP_STATE_TIMEOUT_NS)通过bpf_ktime_get_ns()检查,但 LRU 提供了额外的兜底 - 条目限制:
udp_affinity_map最大 262,144 条目,满时自动淘汰最老的会话
代码中的超时检查 + LRU 双重保障:
| |
Q5: 控制器如何保证与 K8s 状态的最终一致性?
答: 三层保障机制:
1. 事件驱动(实时同步):
| |
2. Workqueue 特性(去重 + 限流):
- 去重: 同一 key 在队列中存在时不会重复添加
- 限流: 失败时
AddRateLimited指数退避重试 - 有序: FIFO 保证公平性
3. 定期全量对账(兜底):
| |
4. 状态变更检测(避免无效更新):
| |
这就是 Kubernetes 控制器模式的核心:声明式 + 最终一致性。
2. 项目架构与设计决策
Q6: 为什么选择二级映射(Service → Slot → Backend)而不是一级映射?
答: 二级映射是经典的间接引用设计,带来以下好处:
一级映射(反例):
| |
二级映射(实际实现):
| |
优势:
- 权重灵活表示: 一个后端可以有多个槽位(权重 0.5 = 512 个槽位指向同一 BackendID)
- 减少内存: Backend 的完整信息(IP、Port、Flags)只存一份,Slot 只存 BackendID(u32)
- 原子更新: Pod 重建后 IP 变化,只需更新
backend_map,无需改动service_slot_map - 二分搜索支持:
weight_range_upper前缀和使得 O(log N) 查找成为可能
Q7: 为什么选择 cgroup/connect4 而不是 XDP 或 TC?
答: 对比分析:
| 钩子 | 挂载位置 | 网络栈路径 | 本项目为何选择/不选择 |
|---|---|---|---|
| XDP | 网卡驱动层(最早) | 包未进入内核协议栈 | 需要改网卡驱动,且处理的是路由后的包,已过 netfilter |
| TC | 协议栈入口/出口 | 经过 netfilter | 同样在 iptables 规则链路径上 |
| cgroup/connect4 | socket 层 connect() | 最早的用户态触发点 | 完全绕过 netfilter,直接改写目标地址 |
核心原因:
- 时机最早: 在
connect()系统调用时就完成重写,内核后续完全感知不到 VIP - 无需理解包结构: 直接使用
bpf_sock_addr上下文修改user_ip4/user_port - 天然隔离: cgroup v2 可以按进程组控制哪些流量被重定向
- 绕过 netfilter: XDP/TC 处理的包仍会经过 iptables 链
Q8: bpf2go 工具链的作用是什么?
答: bpf2go 是 Cilium 提供的工具,用于简化 eBPF 开发与 Go 的集成:
工作流程:
| |
bpf2go 做了什么:
- 用
clang将 C 代码编译为 eBPF 字节码(.o文件) - 将字节码嵌入 Go 源码(作为
[]byte常量) - 生成类型安全的 Go 绑定(Maps、Programs 的访问封装)
- 支持 CO-RE(Compile Once, Run Everywhere)特性
生成代码示例:
| |
3. eBPF 技术深度
Q9: eBPF Verifier 如何保证内核安全?
答: Verifier 是 eBPF 的安全守门员,在程序加载到内核前执行静态分析:
主要检查项:
DAG 检测(无死循环):
- 传统 BPF 完全不允许循环
- 内核 5.3+ 支持有界循环(循环次数在编译期可确定)
- 我的代码中二分搜索:
for (int i = 0; i < 10; i++)上限固定为 10
内存安全:
- 所有指针访问前必须检查非空
- 不允许越界访问(通过指针类型追踪)
1 2backend_slot = lookup_backend_slot(&key); if (!backend_slot) return -ENOENT; // 必须检查 NULL寄存器状态追踪:
- 每个寄存器有类型(PTR_TO_MAP_KEY、PTR_TO_STACK 等)
- 禁止未初始化变量的使用
指令数限制:
- 内核 5.2+:100 万条指令
- 之前:4096 条
我的代码如何通过验证:
- 使用
static __always_inline内联函数,减少栈深度 - 二分搜索循环上限硬编码(verifier 可完全展开)
- 所有
bpf_map_lookup_elem返回值都做 NULL 检查 - 使用
volatile防止编译器优化掉边界检查
Q10: eBPF Map 的 Pin 机制有什么用?
答: Pin(固化)将 BPF Map 实例化为文件系统对象(通常在 /sys/fs/bpf/),使其生命周期独立于创建进程:
为什么需要:
- 进程重启恢复:
traffic-managercrash 后重启,通过ebpf.LoadPinnedMap()重新打开已有 Map,恢复之前的状态 - 多进程共享: 调试工具
bpftool map dump可以读取 pinned Map - 优雅升级: 新版本程序可以直接接管旧的 Map
代码演示:
| |
Q11: 为什么需要 rlimit.RemoveMemlock()?
答: eBPF Maps 和 Programs 的内存被锁定在 RAM 中(不能被 swap),因为内核处理网络包时不能承受 page fault。
历史限制:
- Linux 默认限制每个进程的可锁定内存量(通常 64KB)
- eBPF Map 通常需要 MB 级内存(如
udp_affinity_map有 262,144 条目) rlimit.RemoveMemlock()移除这个限制
现代内核的解决方案:
- 内核 5.11+ 引入了
CAP_BPF能力 - 有此能力的进程不再需要
RLIMIT_MEMLOCK设置 - 但在旧内核上仍需要此调用
4. Kubernetes 控制器模式
Q12: Informer + Workqueue 的工作原理是什么?
答: 这是 Kubernetes 自定义控制器的标准模式:
| |
Informer 的关键特性:
- 本地缓存(Store):
svcLister.Services(namespace).Get(name)是纯内存操作,不访问 API Server - List + Watch: 先全量 List,然后增量 Watch 变更事件
- 事件去重: DeltaFIFO 保证同一对象不会被重复处理
Workqueue 的关键特性:
- 去重: 同一 key 在队列中存在时不会重复添加
- 限流:
AddRateLimited指数退避(5s → 10s → 20s…) - 失败重试: 处理失败时重新入队,成功后
Forget清空限流计数
Q13: EndpointSlice 相比 Endpoints 有什么优势?
答: EndpointSlice 是 Kubernetes 1.21+ 引入的 Endpoints 替代方案:
| 维度 | Endpoints | EndpointSlice |
|---|---|---|
| 对象结构 | 一个 Service 的所有后端在一个对象中 | 自动分片,每片最多 100 个端点 |
| 更新机制 | 全量更新(即使只变一个后端) | 增量更新(只传输变化的切片) |
| 大规模场景 | 单个对象可能数 MB | 分片后每片小,传输快 |
| 拓扑感知 | 不支持 | 支持 Topology 字段(zone、node 等) |
我的项目使用 EndpointSlice:
| |
优势:
- 单个 Service 有数百个后端 Pod 时,EndpointSlice 避免每次传输数 MB 的完整对象
- 增量更新减少 API Server 和网络的负载
Q14: 控制器如何处理并发?
答: 并发控制通过 sync.Mutex 保护共享状态:
| |
临界区分析:
| 操作 | 位置 | 是否需要锁 |
|---|---|---|
| 读 svcStates | refreshState() → 获取旧状态 | ✓ |
| 写 svcStates | commitServiceState() → 更新状态 | ✓ |
| 遍历 svcStates | syncLoop() → 全量入队 | ✓ |
| 删除 svcStates | Service 被删除时 | ✓ |
Workqueue 的并发保证:
- client-go 的 Workqueue 保证:同一时间只有一个 worker 处理同一个 key
- 即使多个 worker 并行,相同的
namespace/name不会同时被处理 - 因此
refreshState中对同一 key 的操作是串行的
Worker 数量: = NumCPU,通过 runtime.NumCPU() 获取
5. 可观测性实践
Q15: 项目中有哪些可观测性手段?
答: 三层可观测性:
1. 数据面统计(eBPF 内嵌):
| |
通过 --dump-stats 读取:
| |
2. 控制面指标(Prometheus + node-exporter):
| |
3. 应用日志(slog 结构化日志):
| |
Q16: node_load1 作为权重指标有什么问题?
答: node_load1 的局限性:
- load1 是 CPU 队列长度: 不能区分 CPU 密集型和 IO 密集型
- 1 分钟平均有滞后: 流量尖峰不能立即反映
- 硬编码阈值 10: 2 核机器 load1=2 已满载,64 核机器 load1=2 还很空闲
- 不考虑内存/网络压力: 节点可能因内存不足或网络拥塞而不可用
更好的方案:
| 指标 | 公式 | 优势 | 劣势 |
|---|---|---|---|
| 相对负载 | (num_cpu - load1) / num_cpu | 适配不同规格节点 | 仍是 CPU 维度 |
| 内存可用率 | node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes | 考虑内存压力 | 不反映 CPU |
| 综合打分 | 0.4*CPU + 0.3*Mem + 0.2*Net + 0.1*Latency | 多维度 | 需要调参 |
| 应用层延迟 | P99/P50 响应时间 | 最直接的 QoE | 需要应用暴露 |
Q17: 如果 Prometheus 不可达,系统会怎样?
答: 优雅降级机制:
| |
降级链路:
- Prometheus 查询成功 → 使用真实权重
- Prometheus 不可达 → 尝试用 Node IP 查询
- 全部失败 → 返回 1.0(退化为随机负载均衡)
设计评价: 优雅降级做得不错,但没有缓存上一次成功的权重值。如果 Prometheus 短暂不可达,会立即丢失所有节点的权重差异。
6. 性能优化与基准测试
Q18: 基准测试的设计有什么优缺点?
答: 测试设计(来自 scripts/benchmark-minikube.py):
测试流程:
| |
优点:
| 方面 | 说明 |
|---|---|
| 预热机制 | 避免冷启动偏差 |
| 干扰 Service | 验证了 O(1) vs O(n) 的差异 |
| HTTP 短连接 | 覆盖了新建连接场景(connect() 频繁) |
不足:
| 方面 | 改进建议 |
|---|---|
| 单节点环境 | 应补充多节点测试(跨节点网络开销) |
| 只测 QPS | 应补充 P99 延迟、CPU 使用率 |
| 短连接为主 | 应测试长连接(gRPC、WebSocket) |
| 25 个后端 | 应测试数百个后端(验证二分搜索优势) |
Q19: 为什么 eBPF 在大规模 Service 场景优势更明显?
答: 核心原因:iptables 是 O(n),eBPF 是 O(1)。
iptables 处理流程:
| |
eBPF 处理流程:
| |
数据验证:
- 正常场景(少量 Service):eBPF 快 19.47%
- 500 个干扰 Service:eBPF 快 27.74%
- 差距扩大:iptables 规则遍历成本随 Service 数线性增长,eBPF Hash 查找时间恒定
7. SRE 核心概念
Q20: 什么是 SRE?SRE 和 DevOps 的关系是什么?
答: SRE(Site Reliability Engineering)是 Google 提出的一种工程实践,通过软件工程的方法解决运维问题。
核心理念:
- 拥抱风险: 100% 可用性是错误目标,追求合理的可用性(如 99.9%)
- SLO/SLI/SLA: 用数据驱动可靠性决策
- 错误预算: 可用性的 " 货币 “,用于平衡创新和稳定性
- 自动化: 消除重复手工劳动(“SRE 不手动操作 “)
- 监控: 必须有数据支撑决策
SRE vs DevOps:
| 维度 | DevOps | SRE |
|---|---|---|
| 定位 | 文化运动、方法论 | 具体工程实践 |
| 关注点 | 开发运维协作 | 系统可靠性工程 |
| 实践 | CI/CD、自动化 | SLO、错误预算、容量规划 |
| 关系 | SRE 是 DevOps 的具体实现之一 | SRE 是 DevOps 文化的工程化落地 |
Q21: 解释 SLO、SLI、SLA 的区别
答:
SLI(Service Level Indicator,服务水平指标):
- 测量的具体指标,通常是量化的值
- 例如:请求延迟、错误率、吞吐量
- 公式:
SLI = (好事件数 / 总事件数) * 100%
SLO(Service Level Objective,服务水平目标):
- 对 SLI 的目标值,内部承诺
- 例如:“99% 的请求延迟 < 100ms”
- 不是对外承诺,是团队内部的质量目标
SLA(Service Level Agreement,服务水平协议):
- 对外承诺,通常有经济惩罚条款
- 例如:” 可用性 99.9%,否则赔偿 10% 服务费 "
- SLA ≤ SLO(对外承诺要低于内部目标)
示例:
| |
Q22: 什么是错误预算(Error Budget)?如何使用?
答: 错误预算是 SLO 的 " 货币 “,表示允许的不可用时间。
计算:
| |
使用场景:
- 发布决策: 如果错误预算剩余 < 10%,暂停非关键发布
- 权衡创新与稳定性: 预算充足时快速迭代,预算不足时保守运营
- 事故复盘: 事故消耗了多少预算?如何改进?
实践建议:
- 错误预算不是 " 目标 “,而是 " 最大可接受不可用时间 "
- 不要故意消耗预算(” 反正有 43 分钟 “)
- 用错误预算推动文化:团队共同负责可靠性
8. Linux 系统知识
Q23: 解释 cgroup v2 和 v1 的区别
答:
| 维度 | cgroup v1 | cgroup v2 |
|---|---|---|
| 层级结构 | 每个控制器独立层级(cpu、memory、blkio 各自一棵树) | 统一层级(所有控制器在一棵树上) |
| 进程归属 | 一个进程可以在不同层级的多个 cgroup 中 | 一个进程只能在一个 cgroup 中 |
| 线程支持 | 不支持线程级控制 | 支持线程级控制 |
| eBPF 支持 | 部分钩子支持 | 所有 cgroup BPF 钩子只支持 v2 |
为什么项目需要 cgroup v2:
cgroup/connect4、cgroup/sendmsg4、cgroup/recvmsg4等 BPF 程序类型只支持 cgroup v2- cgroup v2 支持无特权 BPF 程序挂载(通过
CAP_BPF)
检测 cgroup v2 挂载点:
| |
Q24: 解释 Linux Socket 层的 connect()、sendmsg()、recvmsg()
答:
| 系统调用 | 用途 | TCP | UDP |
|---|---|---|---|
connect() | 建立连接/设置默认目标 | 三次握手,后续数据面向此连接 | 只设置默认目标地址,不影响数据面 |
sendmsg() / sendto() | 发送数据 | 使用 connect() 建立的目标 | 可以每次指定不同目标 |
recvmsg() / recvfrom() | 接收数据 | 返回发送方地址 | 返回数据来源地址 |
eBPF 钩子对应关系:
cgroup/connect4→connect()系统调用cgroup/sendmsg4→sendmsg()/sendto()系统调用cgroup/recvmsg4→recvmsg()/recvfrom()系统调用
UDP 的特殊性:
- UDP 无连接,每次
sendmsg()都可能选择不同后端 - 需要亲和性表(udp_affinity_map)保证同一客户端路由到同一后端
- 需要反向 NAT 表(nat_sk_map)恢复源地址
Q25: 什么是 CO-RE(Compile Once, Run Everywhere)?
答: CO-RE 是 BPF 的一项特性,允许 eBPF 程序编译一次,在不同内核版本上运行。
问题背景:
- 传统 BPF:需要读取内核结构体(如
struct task_struct),但不同内核版本结构体布局不同 - 解决方案 1:在每个节点上安装内核头文件,现场编译
- 解决方案 2(CO-RE):在 eBPF 程序中嵌入 BTF(BPF Type Format)信息,加载时根据当前内核的 BTF 重定位
我的项目是否使用 CO-RE?
| |
优势:
- 无需在目标节点安装内核头文件
- 一个二进制可以在不同内核版本上运行
- 简化部署(只需拷贝二进制)
9. Kubernetes 运维实践
Q26: 如何排查 Kubernetes Service 无法访问的问题?
答: 分层排查:
1. 应用层:
| |
2. Service 层:
| |
3. 网络层(以本项目为例):
| |
4. 内核层:
| |
Q27: Kubernetes 控制器开发的最佳实践有哪些?
答:
1. 使用 client-go 的 Informer + Workqueue:
- 不要自己轮询 API Server
- Informer 提供本地缓存,减少 API Server 压力
2. 幂等性(Idempotency):
| |
3. 最终一致性:
- 不要期望立即一致,允许短暂不一致
- 定期全量对账(syncLoop)兜底
4. 优雅退出:
| |
Q28: 如何升级 Kubernetes 集群?
答: 滚动升级策略:
1. 升级控制平面(Control Plane):
| |
2. 升级工作节点(Worker Node):
| |
3. 注意事项:
- 逐个节点升级,避免同时升级多个节点
- 确保 PodDisruptionBudget(PDB)配置正确
- 备份 etcd 数据(升级前)
10. 故障排查与应急响应
Q29: 如果发现生产环境某个 Service 流量异常,如何排查?
答: 按层次从上到下:
1. 快速定位问题范围:
| |
2. 检查 Kubernetes 资源:
| |
3. 检查本项目(traffic-manager)状态:
| |
4. 检查数据面(eBPF):
| |
5. 检查控制面(Prometheus/Controller):
| |
Q30: 如何设计故障演练(GameDay)?
答: 故障演练是 SRE 的重要实践,验证系统在故障时的表现。
常见故障场景:
| 故障类型 | 注入方法 | 预期行为 |
|---|---|---|
| Pod 崩溃 | kubectl delete pod | 流量应路由到其他后端 |
| 节点故障 | 关闭节点 / 断开网络 | 流量应路由到其他节点 |
| API Server 不可达 | 断开 API Server 网络 | Controller 停止更新,但现有规则继续工作 |
| Prometheus 不可达 | 停止 Prometheus | 降级为等权重负载均衡 |
| eBPF 程序卸载 | pkill traffic-manager | cgroup 钩子解除,流量走 kube-proxy |
演练流程:
- 计划: 明确演练目标、范围、时间窗口
- 通知: 通知相关团队,避免误报告警
- 注入故障: 逐步注入,观察系统行为
- 记录: 记录发现的问题、恢复时间
- 复盘: 总结改进措施
示例(本项目):
| |
11. 监控告警与 SLO/SLI/SLA
Q31: 如何为本项目设计 SLO?
答: 基于项目特性,可以设计以下 SLO:
1. 可用性 SLO:
| |
2. 延迟 SLO:
| |
3. 数据一致性 SLO:
| |
错误预算计算:
| |
Q32: 如何设计告警规则?
答: 基于项目的可观测性数据:
1. 数据面告警:
| |
2. 控制面告警:
| |
3. 系统级告警:
| |
12. 容量规划与扩展性
Q33: 如何做容量规划?
答: 容量规划需要关注各层的瓶颈:
1. BPF Map 容量:
| Map | 类型 | 最大条目 | 建议规划 |
|---|---|---|---|
| service*meta_map | Hash | 65536 | 按 Service 数量 * 1.5 预留 |
| backend*map | Hash | 65536 | 按总 Pod 数量 * 2 预留 |
| service*slot_map | Hash | 65536 | 按总槽位数(后端数 * 权重因子) |
| udp*affinity_map | LRU Hash | 262144 | 按并发 UDP 会话数 * 2 |
2. Controller 性能:
- Worker 数量 = NumCPU
- 每个 Service 更新需要 2-3 次 Map 操作(O(1))
- 1000 个 Service 全量更新:~1000 * 3 = 3000 次 Map 操作,毫秒级完成
3. API Server 压力:
| |
- HTTP/2 多路复用,实际压力可控
- 大规模集群(>1000 节点)建议评估 API Server 规格
4. 建议的容量规划公式:
| |
Q34: 如果要在 1000 节点集群部署,需要做什么调整?
答: 需要评估和调整:
1. API Server 压力:
| |
- 评估 API Server 的 CPU/内存规格
- 考虑使用
EndpointSliceHint减少推送频率
2. BPF Map 大小:
- 如果 Service + Pod 总数超过 65536,需要增大 Map 大小
- 或者分片部署(不同节点组使用不同的 BPF Maps)
3. 控制器架构:
- 当前每节点一个控制器实例,Watch 全部 Service
- 优化方案:使用 Leader Election,只在一个节点运行控制器,其他节点只运行 eBPF 程序
4. 监控和告警:
- 增加 API Server 连接数监控
- 增加 BPF Map 使用率监控
13. 安全与权限管理
Q35: 运行本项目需要哪些 Linux Capabilities?
答:
| Capability | 用途 | 是否必需 |
|---|---|---|
CAP_BPF | 加载 eBPF 程序、操作 BPF Maps | 必需(内核 5.8+) |
CAP_NET_ADMIN | 挂载 eBPF 程序到 cgroup 网络钩子 | 必需 |
CAP_SYS_ADMIN | 旧内核中的超级权限(代替 CAP_BPF) | 旧内核必需 |
CAP_SYS_RESOURCE | 调整 RLIMIT_MEMLOCK | 旧内核必需(< 5.11) |
最佳实践:
| |
Q36: eBPF 程序可能引入哪些安全风险?
答:
信息泄露:
- eBPF 程序可以读取所有经过的连接的 VIP→Pod IP 映射
- 如果 Map 被非特权进程读取,可能泄露集群拓扑
流量劫持:
- 恶意 eBPF 程序可以把流量导向任意地址
- 这也是为什么 eBPF 加载需要
CAP_BPF
资源耗尽:
- 过多的 eBPF Maps 可能耗尽内核内存
- 复杂的 eBPF 程序可能消耗大量 CPU(verifier 限制指令数)
侧信道攻击:
- 理论上,eBPF 程序的执行时间差异可能被利用
项目的防护措施:
- eBPF Verifier 保证程序不会越界访问或死循环
- Map Pin 在
/sys/fs/bpf/,默认只有 root 可访问 bpf_printk仅输出 debug 信息,不泄露敏感数据
14. 分布式系统理论
Q37: 解释 CAP 定理
答: CAP 定理指出,分布式系统无法同时满足以下三个特性:
C(Consistency,一致性):
- 所有节点在同一时间看到相同的数据
- 强一致性:每次读取都得到最新写入的值
A(Availability,可用性):
- 每个请求都能收到响应(不保证最新数据)
- 即使部分节点故障,系统仍然可响应
P(Partition tolerance,分区容错性):
- 网络分区(节点间网络断开)时系统仍能继续工作
- 在分布式系统中,P 通常是必须选择的
CAP 组合:
- CA: 单节点系统(无分区风险),如传统关系型数据库
- CP: 优先保证一致性,牺牲可用性,如 ZooKeeper、etcd
- AP: 优先保证可用性,牺牲强一致性,如 Cassandra、DynamoDB
Kubernetes 的选择:
- etcd(CP 系统):优先保证一致性和分区容错
- kube-apiserver 无状态,可以水平扩展提高可用性
Q38: 最终一致性是什么?为什么 Kubernetes 使用它?
答: 最终一致性是指:如果没有新的更新,经过一段时间后,所有节点会看到相同的数据。
Kubernetes 的控制器模式就是最终一致性:
| |
为什么选择最终一致性:
- 容忍短暂不一致: 网络延迟、事件丢失不会导致系统错误
- 简化设计: 不需要复杂的分布式事务
- 高可用: 即使 API Server 短暂不可达,系统仍能工作(使用本地缓存)
代价:
- 需要定期全量对账(syncLoop)兜底
- 用户可能会观察到短暂的不一致
15. 行为面试题与场景题
Q39: 描述一次你解决技术难题的经历
答: (基于项目经历)
背景:
在开发 traffic-manager 时,遇到 eBPF 程序加载失败的问题。Verifier 报错:“invalid indirect read from stack off -64+0 size 4”。
排查过程:
- 理解错误: Verifier 检测到可能的栈越界访问
- 检查代码: 发现二分搜索函数中
key.backend_slot = mid这行,Verifier 无法确定mid的范围 - 分析原因: 虽然
l和r都是有界的,但 Verifier 无法追踪跨循环迭代的变量关系 - 解决方案: 将二分搜索的循环上限硬编码为 10(log2(1024)),让 Verifier 能够完全展开循环
结果:
- eBPF 程序成功加载
- 学到了 Verifier 的限制和绕过方法
- 在 INTERVIEW_PREP.md 中记录了这个经验
总结:
遇到技术难题时,先理解错误信息,再逐步缩小问题范围,最后用系统化的方法解决。
Q40: 如果你发现生产环境流量分配不均,如何排查?
答: 分层排查:
1. 确认问题范围:
| |
2. 检查 Kubernetes 层:
| |
3. 检查用户态(Controller):
| |
4. 检查权重计算:
| |
5. 检查内核态(eBPF Maps):
| |
6. 检查 eBPF 日志:
| |
16. 面试官追问与陷阱题
Q41: " 你的项目对 ExternalTrafficPolicy=Local 的 Service 怎么处理?”
答: 代码中定义了 scope 字段(LB_LOOKUP_SCOPE_EXT=0 / LB_LOOKUP_SCOPE_INT=1),但当前 Controller 始终传入 scope=0(Cluster 范围)。
对于 ExternalTrafficPolicy=Local 的 Service,只应将流量路由到本节点的后端。当前实现会把流量路由到任意节点的后端,这是一个已知的功能缺口。
改进方案:
| |
Q42: " 为什么不用 Service Mesh(如 Istio)?”
答: 对比分析:
| 维度 | Traffic Manager | Istio |
|---|---|---|
| 工作层 | L4(TCP/UDP) | L7(HTTP/gRPC) |
| 性能开销 | 极低(eBPF 在内核态) | 较高(Sidecar 代理) |
| 复杂度 | 约 2000 行代码 | 数十万行代码 |
| 功能 | 仅负载均衡 | 流量管理、安全、可观测性等 |
| 部署 | 独立二进制 | 需要替换 CNI + Sidecar 注入 |
选择依据:
- 如果只需要 L4 负载均衡,Traffic Manager 更轻量
- 如果需要 L7 路由、熔断、灰度发布等,选择 Istio
- 两者不冲突,可以共存(Traffic Manager 处理 L4,Istio 处理 L7)
Q43: " 如果让你设计一个生产级的 eBPF 负载均衡器,你会怎么做?”
答: 基于项目经验,生产级需要考虑:
1. 可靠性:
- 健康检查:主动探测后端健康状态,而不依赖 kubelet 的就绪探针
- 优雅退出:收到 SIGTERM 后,先停止接受新连接,等待现有连接完成
- 崩溃恢复:通过 Pinned Maps 恢复状态
2. 可扩展性:
- 支持数千个 Service 和数万个后端
- 考虑使用 LPM Trie Map(最长前缀匹配)支持 CIDR 路由
- 控制器 Leader Election,避免每节点 Watch 所有资源
3. 可观测性:
- 暴露 Prometheus metrics(不只是 eBPF 计数器,还有延迟分布)
- 集成 OpenTelemetry,提供分布式追踪
- 日志结构化(已使用 slog)
4. 安全:
- 最小权限(CAP_BPF + CAP_NET_ADMIN)
- 支持 mTLS(如果需要 L7)
- 审计日志
5. 灰度发布:
- 支持按百分比逐步迁移流量
- 快速回滚机制
17. 项目改进与未来方向
Q44: 你的项目有哪些已知的限制?
答:
- 只支持 IPv4: 当前代码只处理
connect4、sendmsg4、recvmsg4,不支持 IPv6 - ExternalTrafficPolicy=Local 未实现: 如前所述,scope 字段未使用
- 权重指标单一: 只使用
node_load1,不考虑内存、网络等其他指标 - 没有健康检查: 完全依赖 Kubernetes 的 EndpointSlice,不主动探测后端
- 没有连接跟踪表: TCP 长连接虽然不需要,但 UDP 的亲和性表可能会在节点重启后丢失
- Leader Election 缺失: 每节点运行一个控制器,大规模集群下 API Server 压力大
Q45: 如果给你更多时间,你会如何改进这个项目?
答:
短期改进(1-2 周):
- 实现
ExternalTrafficPolicy=Local支持 - 添加 IPv6 支持(
connect6、sendmsg6、recvmsg6) - 支持更多权重指标(内存、网络 IO)
- 添加 Prometheus metrics 暴露(当前只有
--dump-stats)
中期改进(1-2 月):
- 实现 Leader Election,优化大规模集群部署
- 添加主动健康检查(TCP connect 探测或 HTTP GET)
- 支持 L7 路由(基于 HTTP Host/Path)
- 集成 OpenTelemetry,提供追踪能力
长期愿景(3-6 月):
- 支持 Cilium 式的全功能 CNI 插件
- 支持 Service Mesh 模式(Sidecar 或 Ambient)
- 提供 Web UI,可视化流量拓扑
- 支持多集群服务发现
附录:面试速查卡片
核心数字:
- eBPF vs iptables 性能提升:+19% ~ +28%
- BPF Map 最大条目:65536(Service/Slot/Backend)
- UDP 亲和超时:60 秒
- 定期对账周期:10 秒
- Worker 数量:NumCPU
核心设计模式:
- 控制器模式:Informer + Workqueue + Reconciliation Loop
- 二级间接引用:ServiceMeta → ServiceSlot → Backend
- 优雅降级:Prometheus 不可用 → 等权重退化为随机
核心系统调用链:
| |
必需的 Linux Capabilities:
CAP_BPF:加载 eBPF 程序CAP_NET_ADMIN:挂载到 cgroup 钩子
常见问题快速回答:
- 为什么用 cgroup/connect4?→ 最早拦截点,完全绕过 netfilter
- 为什么用 LRU_HASH?→ UDP 亲和表需要自动淘汰过期条目
- 如何保证最终一致性?→ Informer + Workqueue + 定期全量对账
文档版本: v2.0 (hy3)
生成时间: 2026-04-26
基于项目: traffic-manager (github.com/kerolt/traffic-manager)