【SRE 实习面试】TrafficManager-面试要点

本文档结合 traffic-manager 项目实战经验,覆盖 SRE 实习面试可能涉及的全部知识点,包括项目深度挖掘、SRE 核心概念、Linux/Kubernetes 系统知识、故障排查、可观测性等方向。


1. 项目深度挖掘

Q1: 请详细描述 traffic-manager 的核心工作原理

答: Traffic Manager 是一个基于 eBPF 的 Kubernetes Service L4 透明负载均衡器,核心工作流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
1. 用户态启动:
   - 加载 eBPF 程序(bpf2go 生成)→ JIT 编译 → 验证器检查
   - 挂载到 cgroup/connect4、sendmsg4、recvmsg4 钩子
   - 启动 Kubernetes 控制器(Informer + Workqueue)

2. 数据面流程(以 TCP 为例):
   应用 connect(Service_VIP:Port)
   → 内核 socket 层触发 cgroup/connect4 钩子
   → eBPF 程序 sock4_connect() 执行
   → 查找 service_meta_map(O(1) Hash)
   → 根据策略选择后端槽位(随机 O(1) / 加权 O(log N)   → 查找 service_slot_map → backend_map 获取 Pod IP:Port
   → 修改 ctx->user_ip4 和 ctx->user_port
   → 内核继续完成 TCP 握手(目标是 Pod IP)

3. 控制面流程:
   - Service/EndpointSlice Informer 监听 K8s 资源变化
   - 事件入队到 Workqueue(去重、限流)
   - Worker 消费:构建 serviceState → 对比旧状态 → 更新 BPF Maps
   - 每 10 秒全量对账(syncLoop)兜底

关键数字:

  • 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):

1
2
3
4
5
6
7
8
SEC("cgroup/connect4")
int sock4_connect(struct bpf_sock_addr* ctx) {
    if (ctx->protocol != IPPROTO_TCP) {
        return SYS_PROCEED;  // 非 TCP 直接放行
    }
    sock4_forward_entry(ctx, IPPROTO_TCP);
    return SYS_PROCEED;  // 始终返回 1,内核继续连接
}
  • TCP 是面向连接的,connect() 建立后方向固定
  • 只需在连接建立时重写一次目标地址
  • 后续数据传输不再需要 eBPF 干预

UDP 处理(sendmsg4 + recvmsg4):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SEC("cgroup/sendmsg4")
int sock4_sendmsg(struct bpf_sock_addr* ctx) {
    sock4_udp_sendmsg_entry(ctx);  // 每次发送都选后端 + 记录亲和性
    return SYS_PROCEED;
}

SEC("cgroup/recvmsg4")
int sock4_recvmsg(struct bpf_sock_addr* ctx) {
    sock4_udp_recvmsg_entry(ctx);  // 恢复 VIP 源地址
    return SYS_PROCEED;
}
  • UDP 无连接,每次 sendmsg()/recvmsg() 都需要处理
  • 需要亲和性表(udp_affinity_map)保证同一客户端路由到同一后端
  • 需要反向 NAT 表(nat_sk_map)恢复源地址(后端回复时源是 Pod IP,需改为 VIP)

Q3: 加权负载均衡的具体实现细节是什么?

答: 加权负载均衡分为用户态权重计算和内核态权重选择两部分:

用户态权重计算(metrics + controller):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// metrics/metrics.go
func (nm *NodeExporterNodeMetric) AvailableRate() float64 {
    if nm.load1 > 10 {
        return 0  // 负载 >= 10 认为节点不可用
    }
    return (10 - nm.load1) / 10  // load1=2 → 0.8, load1=5 → 0.5
}

// controller 中归一化权重
func (c *Controller) nodeMetricWeight(nodeName string) float64 {
    if weight, ok := c.queryNodeMetricWeight(nodeName); ok {
        return weight
    }
    return 1.0  // Prometheus 不可达时降级为等权重
}

内核态二分搜索(bpf/cgroup_sock.c):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static __always_inline int sock_fast_select_weighted_slot(__u16 sbc, __u16 total_weight, struct svc_slot_key key) {
    int random_point = bpf_get_prandom_u32() % total_weight;
    int l = 1, r = sbc;
    for (int i = 0; i < 10; i++) {  // log2(1024) ≈ 10
        int mid = (l + r) >> 1;
        key.backend_slot = mid;
        backend_slot = lookup_backend_slot(&key);
        if (!backend_slot) return -ENOENT;
        if (backend_slot->weight_range_upper <= random_point)
            l = mid + 1;
        else
            r = mid;
    }
    return l;
}

权重前缀和示例:

1
2
3
4
5
后端 A: weight=0.5 → weight_range_upper = 512 (0.5 * 1024)
后端 B: weight=0.3 → weight_range_upper = 512 + 307 = 819
后端 C: weight=0.2 → weight_range_upper = 819 + 205 = 1024

随机点 600 → 落在 (512, 819] → 选中后端 B

Q4: 为什么用 LRU_HASH 而不是普通 HASH?

答: UDP 亲和性表和 NAT 表需要自动淘汰机制:

Map 类型淘汰策略适用场景
BPF_MAP_TYPE_HASH无,满了返回错误静态数据(Service、Backend)
BPF_MAP_TYPE_LRU_HASH满时淘汰最久未使用动态会话数据(UDP 亲和、NAT)

为什么需要 LRU:

  1. 避免内存泄漏: UDP 客户端可能崩溃或断开,无法主动清理映射表
  2. 自动过期: 60 秒超时(LB_UDP_STATE_TIMEOUT_NS)通过 bpf_ktime_get_ns() 检查,但 LRU 提供了额外的兜底
  3. 条目限制: udp_affinity_map 最大 262,144 条目,满时自动淘汰最老的会话

代码中的超时检查 + LRU 双重保障:

1
2
3
4
5
6
__u64 now = bpf_ktime_get_ns();
if (now - existing_affinity->updated_at_ns > LB_UDP_STATE_TIMEOUT_NS) {
    bpf_map_delete_elem(&udp_affinity_map, &affinity_key);  // 主动删除过期
    goto select_backend;
}
// LRU 会在 map 满时自动淘汰最久未使用的条目

Q5: 控制器如何保证与 K8s 状态的最终一致性?

答: 三层保障机制:

1. 事件驱动(实时同步):

1
2
3
4
5
// 任何 Service/EndpointSlice 变化都触发入队
func (c *Controller) handleServiceAdd(obj interface{}) {
    key := ... // 生成 namespace/name
    c.queue.Add(key)
}

2. Workqueue 特性(去重 + 限流):

  • 去重: 同一 key 在队列中存在时不会重复添加
  • 限流: 失败时 AddRateLimited 指数退避重试
  • 有序: FIFO 保证公平性

3. 定期全量对账(兜底):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (c *Controller) syncLoop() {
    ticker := time.NewTicker(10 * time.Second)
    for range ticker.C {
        c.mutex.Lock()
        for key := range c.svcStates {
            c.queue.Add(key)  // 所有已知 Service 重新入队
        }
        c.mutex.Unlock()
    }
}

4. 状态变更检测(避免无效更新):

1
2
3
if serviceStateEqual(oldState, newState) {
    return nil  // 状态未变,不写 BPF Map
}

这就是 Kubernetes 控制器模式的核心:声明式 + 最终一致性。


2. 项目架构与设计决策

Q6: 为什么选择二级映射(Service → Slot → Backend)而不是一级映射?

答: 二级映射是经典的间接引用设计,带来以下好处:

一级映射(反例):

1
2
// 如果直接 service_ip → backend_ip,权重如何实现?
// 需要为每个后端复制多个条目,浪费内存且更新复杂

二级映射(实际实现):

1
2
3
4
5
6
ServiceMeta (VIP:Port → Meta)
    ↓ 业务维度:count, action, total_weight
ServiceSlot (VIP:Port:SlotIndex → BackendID)
    ↓ 调度维度:weight_range_upper(前缀和)
Backend (BackendID → IP:Port:Proto)
    ↓ 物理维度

优势:

  1. 权重灵活表示: 一个后端可以有多个槽位(权重 0.5 = 512 个槽位指向同一 BackendID)
  2. 减少内存: Backend 的完整信息(IP、Port、Flags)只存一份,Slot 只存 BackendID(u32)
  3. 原子更新: Pod 重建后 IP 变化,只需更新 backend_map,无需改动 service_slot_map
  4. 二分搜索支持: weight_range_upper 前缀和使得 O(log N) 查找成为可能

Q7: 为什么选择 cgroup/connect4 而不是 XDP 或 TC?

答: 对比分析:

钩子挂载位置网络栈路径本项目为何选择/不选择
XDP网卡驱动层(最早)包未进入内核协议栈需要改网卡驱动,且处理的是路由后的包,已过 netfilter
TC协议栈入口/出口经过 netfilter同样在 iptables 规则链路径上
cgroup/connect4socket 层 connect()最早的用户态触发点完全绕过 netfilter,直接改写目标地址

核心原因:

  1. 时机最早:connect() 系统调用时就完成重写,内核后续完全感知不到 VIP
  2. 无需理解包结构: 直接使用 bpf_sock_addr 上下文修改 user_ip4/user_port
  3. 天然隔离: cgroup v2 可以按进程组控制哪些流量被重定向
  4. 绕过 netfilter: XDP/TC 处理的包仍会经过 iptables 链

Q8: bpf2go 工具链的作用是什么?

答: bpf2go 是 Cilium 提供的工具,用于简化 eBPF 开发与 Go 的集成:

工作流程:

1
2
3
4
5
6
7
8
# 1. 编写 eBPF C 代码(bpf/cgroup_sock.c)
# 2. 运行 go generate(触发 bpf2go)
cd pkg/bpf && go generate
# 生成文件:
#   - connect_bpfel.go(Go 绑定,包含 connectObjects 结构体)
#   - connect_bpfel.o(eBPF 字节码)
# 3. 编译主程序
go build -o bin/traffic-manager main.go

bpf2go 做了什么:

  1. clang 将 C 代码编译为 eBPF 字节码(.o 文件)
  2. 将字节码嵌入 Go 源码(作为 []byte 常量)
  3. 生成类型安全的 Go 绑定(Maps、Programs 的访问封装)
  4. 支持 CO-RE(Compile Once, Run Everywhere)特性

生成代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// connect_bpfel.go(自动生成)
type connectObjects struct {
    connectPrograms
    connectMaps
}

type connectMaps struct {
    ServiceMetaMap  *ebpf.Map `ebpf:"service_meta_map"`
    ServiceSlotMap  *ebpf.Map `ebpf:"service_slot_map"`
    // ...
}

3. eBPF 技术深度

Q9: eBPF Verifier 如何保证内核安全?

答: Verifier 是 eBPF 的安全守门员,在程序加载到内核前执行静态分析:

主要检查项:

  1. DAG 检测(无死循环):

    • 传统 BPF 完全不允许循环
    • 内核 5.3+ 支持有界循环(循环次数在编译期可确定)
    • 我的代码中二分搜索:for (int i = 0; i < 10; i++) 上限固定为 10
  2. 内存安全:

    • 所有指针访问前必须检查非空
    • 不允许越界访问(通过指针类型追踪)
    1
    2
    
    backend_slot = lookup_backend_slot(&key);
    if (!backend_slot) return -ENOENT;  // 必须检查 NULL
    
  3. 寄存器状态追踪:

    • 每个寄存器有类型(PTR_TO_MAP_KEY、PTR_TO_STACK 等)
    • 禁止未初始化变量的使用
  4. 指令数限制:

    • 内核 5.2+:100 万条指令
    • 之前:4096 条

我的代码如何通过验证:

  • 使用 static __always_inline 内联函数,减少栈深度
  • 二分搜索循环上限硬编码(verifier 可完全展开)
  • 所有 bpf_map_lookup_elem 返回值都做 NULL 检查
  • 使用 volatile 防止编译器优化掉边界检查

Q10: eBPF Map 的 Pin 机制有什么用?

答: Pin(固化)将 BPF Map 实例化为文件系统对象(通常在 /sys/fs/bpf/),使其生命周期独立于创建进程:

为什么需要:

  1. 进程重启恢复: traffic-manager crash 后重启,通过 ebpf.LoadPinnedMap() 重新打开已有 Map,恢复之前的状态
  2. 多进程共享: 调试工具 bpftool map dump 可以读取 pinned Map
  3. 优雅升级: 新版本程序可以直接接管旧的 Map

代码演示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 加载时设置 Pin 路径
options.Maps.PinPath = "/sys/fs/bpf/sock_ops_map"
loadConnectObjects(&program.connectObj, &options)

// 检查命令通过 LoadPinnedMap 读取
func LookupPinnedService(ip, port) (exists bool, ...) {
    mapObj, err := ebpf.LoadPinnedMap("/sys/fs/bpf/sock_ops_map/service_meta_map", nil)
    // ...
}

// 关闭时 Unpin(从文件系统移除引用)
program.connectObj.connectMaps.ServiceMetaMap.Unpin()

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 自定义控制器的标准模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
           ┌─────────────┐
           │  API Server  │
           └──────┬───────┘
                  │ List & Watch (HTTP/2 长连接)
           ┌──────▼───────┐
           │   Informer   │
(Reflector) │ ← 全量 List + 增量 Watch
           │   DeltaFIFO  │ ← 存储未处理的事件
           └──────┬───────┘
                  │ Add/Update/Delete Event
           ┌──────▼───────┐
           │  WorkQueue   │ ← 去重、限流、顺序保证
(RateLimiting)           └──────┬───────┘
                  │ 消费者取 key
           ┌──────▼───────┐
           │   Worker     │ ← 获取最新状态,协调到 eBPF Maps
           └──────────────┘

Informer 的关键特性:

  1. 本地缓存(Store): svcLister.Services(namespace).Get(name) 是纯内存操作,不访问 API Server
  2. List + Watch: 先全量 List,然后增量 Watch 变更事件
  3. 事件去重: DeltaFIFO 保证同一对象不会被重复处理

Workqueue 的关键特性:

  1. 去重: 同一 key 在队列中存在时不会重复添加
  2. 限流: AddRateLimited 指数退避(5s → 10s → 20s…)
  3. 失败重试: 处理失败时重新入队,成功后 Forget 清空限流计数

Q13: EndpointSlice 相比 Endpoints 有什么优势?

答: EndpointSlice 是 Kubernetes 1.21+ 引入的 Endpoints 替代方案:

维度EndpointsEndpointSlice
对象结构一个 Service 的所有后端在一个对象中自动分片,每片最多 100 个端点
更新机制全量更新(即使只变一个后端)增量更新(只传输变化的切片)
大规模场景单个对象可能数 MB分片后每片小,传输快
拓扑感知不支持支持 Topology 字段(zone、node 等)

我的项目使用 EndpointSlice:

1
2
3
4
5
6
epInformer := factory.Discovery().V1().EndpointSlices()
// 通过 LabelServiceName 标签关联到 Service
selector := labels.SelectorFromSet(labels.Set{
    discoveryv1.LabelServiceName: name,
})
endpointSlices, _ := c.epLister.EndpointSlices(namespace).List(selector)

优势:

  • 单个 Service 有数百个后端 Pod 时,EndpointSlice 避免每次传输数 MB 的完整对象
  • 增量更新减少 API Server 和网络的负载

Q14: 控制器如何处理并发?

答: 并发控制通过 sync.Mutex 保护共享状态:

1
2
3
4
5
type Controller struct {
    svcStates map[string]*serviceState  // 被保护的共享状态
    mutex     sync.Mutex
    queue     workqueue.TypedRateLimitingInterface[string]
}

临界区分析:

操作位置是否需要锁
读 svcStatesrefreshState() → 获取旧状态
写 svcStatescommitServiceState() → 更新状态
遍历 svcStatessyncLoop() → 全量入队
删除 svcStatesService 被删除时

Workqueue 的并发保证:

  • client-go 的 Workqueue 保证:同一时间只有一个 worker 处理同一个 key
  • 即使多个 worker 并行,相同的 namespace/name 不会同时被处理
  • 因此 refreshState 中对同一 key 的操作是串行的

Worker 数量: = NumCPU,通过 runtime.NumCPU() 获取


5. 可观测性实践

Q15: 项目中有哪些可观测性手段?

答: 三层可观测性:

1. 数据面统计(eBPF 内嵌):

1
2
3
4
5
6
7
// stats_map 是 BPF_MAP_TYPE_ARRAY,6 个计数器
incr_stat(STAT_CONNECT_ATTEMPTS);   // 总连接尝试
incr_stat(STAT_SERVICE_MISS);       // 服务未找到
incr_stat(STAT_BACKEND_SLOT_MISS);  // 槽位未找到
incr_stat(STAT_BACKEND_MISS);       // 后端未找到
incr_stat(STAT_REWRITE_SUCCESS);    // 重写成功
incr_stat(STAT_UNSUPPORTED_ACTION); // 不支持的动作

通过 --dump-stats 读取:

1
2
3
./traffic-manager --dump-stats
# 输出:
# INFO BPF metrics dump stats.connect_attempts=12345 stats.service_misses=12 ...

2. 控制面指标(Prometheus + node-exporter):

1
2
3
// 查询 node_load1 指标
query := `avg_over_time(node_load1{job="node-exporter"}[1m])`
promAPI.QueryRange(ctx, query, v1.Range{...})

3. 应用日志(slog 结构化日志):

1
2
slog.Warn("Failed to update metrics", "service", key, "error", err)
// 可输出为 JSON 格式,便于 Loki/ELK 收集

Q16: node_load1 作为权重指标有什么问题?

答: node_load1 的局限性:

  1. load1 是 CPU 队列长度: 不能区分 CPU 密集型和 IO 密集型
  2. 1 分钟平均有滞后: 流量尖峰不能立即反映
  3. 硬编码阈值 10: 2 核机器 load1=2 已满载,64 核机器 load1=2 还很空闲
  4. 不考虑内存/网络压力: 节点可能因内存不足或网络拥塞而不可用

更好的方案:

指标公式优势劣势
相对负载(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 不可达,系统会怎样?

答: 优雅降级机制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// controller.go
if c.metrics != nil {
    c.metrics.Update()  // 更新失败只打 WARN,不崩溃
}

// 权重查询降级链路
func (c *Controller) nodeMetricWeight(nodeName string) float64 {
    // 1. 尝试用 NodeName 查询
    if weight, ok := c.queryNodeMetricWeight(nodeName); ok {
        return weight
    }
    // 2. 通过 Node InternalIP 再试一次
    if node, ok := c.nodeLister.Get(nodeName); ok {
        // 获取 Node InternalIP 再查询...
    }
    // 3. 最终降级:返回 1.0(等权重)
    return 1.0
}

降级链路:

  1. Prometheus 查询成功 → 使用真实权重
  2. Prometheus 不可达 → 尝试用 Node IP 查询
  3. 全部失败 → 返回 1.0(退化为随机负载均衡)

设计评价: 优雅降级做得不错,但没有缓存上一次成功的权重值。如果 Prometheus 短暂不可达,会立即丢失所有节点的权重差异。


6. 性能优化与基准测试

Q18: 基准测试的设计有什么优缺点?

答: 测试设计(来自 scripts/benchmark-minikube.py):

测试流程:

1
2
3
4
5
6
7
1. 部署 25 个后端 Pod (sisyphe Deployment)
2. 使用 siege 压测工具发起 HTTP 请求
3. 对比场景:
   - Baseline: kube-proxy (iptables)
   - Experiment: Traffic Manager (eBPF)
4. 额外场景:创建 500/1000 个 dummy Service
5. 每个场景:50 并发 5s 预热 → 200 并发 15s 正式测试

优点:

方面说明
预热机制避免冷启动偏差
干扰 Service验证了 O(1) vs O(n) 的差异
HTTP 短连接覆盖了新建连接场景(connect() 频繁)

不足:

方面改进建议
单节点环境应补充多节点测试(跨节点网络开销)
只测 QPS应补充 P99 延迟、CPU 使用率
短连接为主应测试长连接(gRPC、WebSocket)
25 个后端应测试数百个后端(验证二分搜索优势)

Q19: 为什么 eBPF 在大规模 Service 场景优势更明显?

答: 核心原因:iptables 是 O(n),eBPF 是 O(1)。

iptables 处理流程:

1
2
3
4
5
6
connect() → OUTPUT 链 → KUBE-SERVICES 链
→ 遍历规则 1 → 不匹配
→ 遍历规则 2 → 不匹配
→ ...
→ 遍历规则 500 → 匹配!(第 500 个 Service)
→ DNAT → 完成

eBPF 处理流程:

1
2
3
4
connect() → cgroup/connect4 钩子
→ Hash 查找 service_meta_map(O(1),无论多少 Service)
→ 二分搜索后端槽位(O(log N),N 为后端数)
→ 完成

数据验证:

  • 正常场景(少量 Service):eBPF 快 19.47%
  • 500 个干扰 Service:eBPF 快 27.74%
  • 差距扩大:iptables 规则遍历成本随 Service 数线性增长,eBPF Hash 查找时间恒定

7. SRE 核心概念

Q20: 什么是 SRE?SRE 和 DevOps 的关系是什么?

答: SRE(Site Reliability Engineering)是 Google 提出的一种工程实践,通过软件工程的方法解决运维问题。

核心理念:

  1. 拥抱风险: 100% 可用性是错误目标,追求合理的可用性(如 99.9%)
  2. SLO/SLI/SLA: 用数据驱动可靠性决策
  3. 错误预算: 可用性的 " 货币 “,用于平衡创新和稳定性
  4. 自动化: 消除重复手工劳动(“SRE 不手动操作 “)
  5. 监控: 必须有数据支撑决策

SRE vs DevOps:

维度DevOpsSRE
定位文化运动、方法论具体工程实践
关注点开发运维协作系统可靠性工程
实践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(对外承诺要低于内部目标)

示例:

1
2
3
4
SLI: HTTP 请求成功率
SLO: 99.9% 的请求成功(月度)
SLA: 99.5% 的可用性承诺(对客户)
错误预算: 0.1% * 月度总请求数 = 允许的失败次数

Q22: 什么是错误预算(Error Budget)?如何使用?

答: 错误预算是 SLO 的 " 货币 “,表示允许的不可用时间。

计算:

1
2
SLO: 99.9% 可用
错误预算: 0.1% * 月度总分钟数 = 0.1% * 43200 = 43.2 分钟

使用场景:

  1. 发布决策: 如果错误预算剩余 < 10%,暂停非关键发布
  2. 权衡创新与稳定性: 预算充足时快速迭代,预算不足时保守运营
  3. 事故复盘: 事故消耗了多少预算?如何改进?

实践建议:

  • 错误预算不是 " 目标 “,而是 " 最大可接受不可用时间 "
  • 不要故意消耗预算(” 反正有 43 分钟 “)
  • 用错误预算推动文化:团队共同负责可靠性

8. Linux 系统知识

Q23: 解释 cgroup v2 和 v1 的区别

答:

维度cgroup v1cgroup v2
层级结构每个控制器独立层级(cpu、memory、blkio 各自一棵树)统一层级(所有控制器在一棵树上)
进程归属一个进程可以在不同层级的多个 cgroup 中一个进程只能在一个 cgroup 中
线程支持不支持线程级控制支持线程级控制
eBPF 支持部分钩子支持所有 cgroup BPF 钩子只支持 v2

为什么项目需要 cgroup v2:

  • cgroup/connect4cgroup/sendmsg4cgroup/recvmsg4 等 BPF 程序类型只支持 cgroup v2
  • cgroup v2 支持无特权 BPF 程序挂载(通过 CAP_BPF

检测 cgroup v2 挂载点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func DetectCgroupPath() (string, error) {
    f, _ := os.Open("/proc/mounts")
    for scanner.Scan() {
        fields := strings.Split(scanner.Text(), " ")
        if len(fields) >= 3 && fields[2] == "cgroup2" {
            return fields[1], nil  // 返回挂载点,如 /sys/fs/cgroup
        }
    }
    return "", fmt.Errorf("cgroup2 not mounted")
}

Q24: 解释 Linux Socket 层的 connect()、sendmsg()、recvmsg()

答:

系统调用用途TCPUDP
connect()建立连接/设置默认目标三次握手,后续数据面向此连接只设置默认目标地址,不影响数据面
sendmsg() / sendto()发送数据使用 connect() 建立的目标可以每次指定不同目标
recvmsg() / recvfrom()接收数据返回发送方地址返回数据来源地址

eBPF 钩子对应关系:

  • cgroup/connect4connect() 系统调用
  • cgroup/sendmsg4sendmsg() / sendto() 系统调用
  • cgroup/recvmsg4recvmsg() / 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?

1
2
3
# bpf2go 默认生成 CO-RE 兼容的代码
go tool bpf2go -tags linux -target bpfel connect ../../bpf/cgroup_sock.c
# 生成的 connect_bpfel.go 包含 BTF 重定位信息

优势:

  • 无需在目标节点安装内核头文件
  • 一个二进制可以在不同内核版本上运行
  • 简化部署(只需拷贝二进制)

9. Kubernetes 运维实践

Q26: 如何排查 Kubernetes Service 无法访问的问题?

答: 分层排查:

1. 应用层:

1
2
3
4
5
6
# 检查 Pod 是否正常运行
kubectl get pods -n <namespace>
kubectl logs <pod-name> -n <namespace>

# 检查 Pod 内服务是否监听正确端口
kubectl exec <pod-name> -- netstat -tlnp

2. Service 层:

1
2
3
4
5
6
7
8
9
# 检查 Service 是否存在
kubectl get svc <service-name> -n <namespace>

# 检查 Endpoints/EndpointSlice 是否有后端
kubectl get endpoints <service-name> -n <namespace>
kubectl get endpointslice -l kubernetes.io/service-name=<service-name> -n <namespace>

# 描述 Service,查看 selector 和 endpoints
kubectl describe svc <service-name> -n <namespace>

3. 网络层(以本项目为例):

1
2
3
4
5
6
7
8
# 检查 eBPF 程序是否挂载
ls /sys/fs/bpf/sock_ops_map/

# 检查 BPF Maps 是否正确
bpftool map dump pinned /sys/fs/bpf/sock_ops_map/service_meta_map

# 检查统计信息
./traffic-manager --dump-stats

4. 内核层:

1
2
3
4
5
# 查看 eBPF 程序日志
cat /sys/kernel/debug/tracing/trace_pipe | grep traffic

# 检查 cgroup v2 挂载
mount | grep cgroup2

Q27: Kubernetes 控制器开发的最佳实践有哪些?

答:

1. 使用 client-go 的 Informer + Workqueue:

  • 不要自己轮询 API Server
  • Informer 提供本地缓存,减少 API Server 压力

2. 幂等性(Idempotency):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (c *Controller) processNextItem() bool {
    key, _ := c.queue.Get()
    defer c.queue.Done(key)

    // 无论执行多少次,结果应该相同
    err := c.syncHandler(key.(string))
    if err != nil {
        c.queue.AddRateLimited(key)  // 失败重试
        return true
    }
    c.queue.Forget(key)  // 成功,清空限流计数
    return true
}

3. 最终一致性:

  • 不要期望立即一致,允许短暂不一致
  • 定期全量对账(syncLoop)兜底

4. 优雅退出:

1
2
3
4
func (c *Controller) Run(stopCh <-chan struct{}) {
    defer c.queue.ShutDown()  // 关闭队列,不再接受新任务
    // ...
}

Q28: 如何升级 Kubernetes 集群?

答: 滚动升级策略:

1. 升级控制平面(Control Plane):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1. 升级 kubeadm
apt-get update && apt-get install kubeadm=1.29.x

# 2. 检查升级计划
kubeadm upgrade plan

# 3. 执行升级
kubeadm upgrade apply v1.29.x

# 4. 升级 kubelet 和 kubectl
apt-get install kubelet=1.29.x kubectl=1.29.x
systemctl restart kubelet

2. 升级工作节点(Worker Node):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1. 标记节点不可调度
kubectl cordon <node-name>

# 2. 驱逐 Pod
kubectl drain <node-name> --ignore-daemonsets

# 3. 升级 kubelet
apt-get install kubelet=1.29.x
systemctl restart kubelet

# 4. 恢复调度
kubectl uncordon <node-name>

3. 注意事项:

  • 逐个节点升级,避免同时升级多个节点
  • 确保 PodDisruptionBudget(PDB)配置正确
  • 备份 etcd 数据(升级前)

10. 故障排查与应急响应

Q29: 如果发现生产环境某个 Service 流量异常,如何排查?

答: 按层次从上到下:

1. 快速定位问题范围:

1
2
3
# 是所有用户受影响,还是部分用户?
# 是全部 Service,还是单个 Service?
# 是某个节点,还是全部节点?

2. 检查 Kubernetes 资源:

1
2
3
4
5
6
7
8
9
# Service 是否存在
kubectl get svc <service-name>

# 后端 Pod 是否 Ready
kubectl get pods -l app=<app-label>
kubectl describe pod <pod-name>

# EndpointSlice 是否正确
kubectl get endpointslice -l kubernetes.io/service-name=<service-name>

3. 检查本项目(traffic-manager)状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 检查进程是否运行
ps aux | grep traffic-manager

# 检查 eBPF 程序是否挂载
bpftool cgroup show /sys/fs/cgroup

# 检查 BPF Maps
./traffic-manager --check-service-ip=<VIP> --check-service-port=<port>

# 查看统计信息
./traffic-manager --dump-stats

4. 检查数据面(eBPF):

1
2
3
4
5
# 查看 eBPF 日志
cat /sys/kernel/debug/tracing/trace_pipe

# dump BPF Maps 查看配置
bpftool map dump pinned /sys/fs/bpf/sock_ops_map/service_meta_map

5. 检查控制面(Prometheus/Controller):

1
2
3
4
5
# Prometheus 是否可达
curl http://prometheus:9090/-/healthy

# 查看 Controller 日志
journalctl -u traffic-manager -f

Q30: 如何设计故障演练(GameDay)?

答: 故障演练是 SRE 的重要实践,验证系统在故障时的表现。

常见故障场景:

故障类型注入方法预期行为
Pod 崩溃kubectl delete pod流量应路由到其他后端
节点故障关闭节点 / 断开网络流量应路由到其他节点
API Server 不可达断开 API Server 网络Controller 停止更新,但现有规则继续工作
Prometheus 不可达停止 Prometheus降级为等权重负载均衡
eBPF 程序卸载pkill traffic-managercgroup 钩子解除,流量走 kube-proxy

演练流程:

  1. 计划: 明确演练目标、范围、时间窗口
  2. 通知: 通知相关团队,避免误报告警
  3. 注入故障: 逐步注入,观察系统行为
  4. 记录: 记录发现的问题、恢复时间
  5. 复盘: 总结改进措施

示例(本项目):

1
2
3
4
5
6
7
8
# 故障注入:停止 traffic-manager
pkill traffic-manager

# 观察:新连接是否还能正常工作?
# 预期:cgroup 钩子解除,流量回退到 kube-proxy

# 恢复:重启 traffic-manager
sudo ./traffic-manager &

11. 监控告警与 SLO/SLI/SLA

Q31: 如何为本项目设计 SLO?

答: 基于项目特性,可以设计以下 SLO:

1. 可用性 SLO:

1
2
3
SLI: 成功转发的连接数 / 总连接尝试数
SLO: 99.9% 的连接成功转发
测量方法: stats_map 中的 rewrite_success / connect_attempts

2. 延迟 SLO:

1
2
3
SLI: eBPF 程序执行时间(可以通过 bpf_ktime_get_ns() 测量)
SLO: 99% 的 connect() 重定向 < 100μs
注意: eBPF 程序执行时间需要额外的观测手段(perf event)

3. 数据一致性 SLO:

1
2
3
SLI: K8s Service 变化到 eBPF Map 更新的延迟
SLO: 99% 的变更在 10 秒内生效
测量方法: 记录 Informer 事件时间戳 vs Map 更新时间戳

错误预算计算:

1
2
月度可用性 SLO: 99.9%
错误预算: 0.1% * 43200 分钟 = 43.2 分钟不可用时间

Q32: 如何设计告警规则?

答: 基于项目的可观测性数据:

1. 数据面告警:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Prometheus 告警规则
- alert: HighServiceMissRate
  expr: rate(stats_service_misses[5m]) / rate(stats_connect_attempts[5m]) > 0.05
  annotations:
    description: "Service 未命中率超过 5%"

- alert: HighBackendMissRate
  expr: rate(stats_backend_misses[5m]) > 10
  annotations:
    description: "后端未找到速率超过 10/s"

2. 控制面告警:

1
2
3
4
5
6
7
8
9
- alert: ControllerSyncFailure
  expr: increase(controller_sync_errors[5m]) > 0
  annotations:
    description: "控制器同步失败"

- alert: PrometheusUnavailable
  expr: up{job="prometheus"} == 0
  annotations:
    description: "Prometheus 不可达,权重降级为等权重"

3. 系统级告警:

1
2
3
4
5
6
7
8
9
- alert: TrafficManagerDown
  expr: up{job="traffic-manager"} == 0
  annotations:
    description: "traffic-manager 进程崩溃"

- alert: BPFMapNearFull
  expr: bpf_map_size / bpf_map_max > 0.8
  annotations:
    description: "BPF Map 使用率超过 80%"

12. 容量规划与扩展性

Q33: 如何做容量规划?

答: 容量规划需要关注各层的瓶颈:

1. BPF Map 容量:

Map类型最大条目建议规划
service*meta_mapHash65536按 Service 数量 * 1.5 预留
backend*mapHash65536按总 Pod 数量 * 2 预留
service*slot_mapHash65536按总槽位数(后端数 * 权重因子)
udp*affinity_mapLRU Hash262144按并发 UDP 会话数 * 2

2. Controller 性能:

  • Worker 数量 = NumCPU
  • 每个 Service 更新需要 2-3 次 Map 操作(O(1))
  • 1000 个 Service 全量更新:~1000 * 3 = 3000 次 Map 操作,毫秒级完成

3. API Server 压力:

1
2
单节点: 2 个 Watch 连接(Service + EndpointSlice)
1000 节点集群: 2000 个 Watch 连接
  • HTTP/2 多路复用,实际压力可控
  • 大规模集群(>1000 节点)建议评估 API Server 规格

4. 建议的容量规划公式:

1
2
3
最大 Service  = 65536 / 1.5 ≈ 43690
最大后端 Pod  = 65536 / 232768
最大并发 UDP 会话 = 262144 / 2131072

Q34: 如果要在 1000 节点集群部署,需要做什么调整?

答: 需要评估和调整:

1. API Server 压力:

1
2
当前: 每节点 Watch 2 种资源(Service + EndpointSlice)
1000 节点: 2000 个长连接
  • 评估 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)

最佳实践:

1
2
3
4
5
6
# 不要用 root 运行,而是授予最小权限
sudo setcap cap_bpf,cap_net_admin+ep ./bin/traffic-manager

# 验证
getcap ./bin/traffic-manager
# 输出: ./bin/traffic-manager = cap_bpf,cap_net_admin+ep

Q36: eBPF 程序可能引入哪些安全风险?

答:

  1. 信息泄露:

    • eBPF 程序可以读取所有经过的连接的 VIP→Pod IP 映射
    • 如果 Map 被非特权进程读取,可能泄露集群拓扑
  2. 流量劫持:

    • 恶意 eBPF 程序可以把流量导向任意地址
    • 这也是为什么 eBPF 加载需要 CAP_BPF
  3. 资源耗尽:

    • 过多的 eBPF Maps 可能耗尽内核内存
    • 复杂的 eBPF 程序可能消耗大量 CPU(verifier 限制指令数)
  4. 侧信道攻击:

    • 理论上,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 的控制器模式就是最终一致性:

1
2
3
4
5
6
7
8
// 控制器不断协调,直到实际状态 = 期望状态
for {
    desired := getDesiredStateFromAPI()  // 从 Informer 缓存获取
    current := getCurrentState()          // 从 eBPF Maps 获取
    if desired != current {
        updateBPFMaps(desired)           // 更新到期望状态
    }
}

为什么选择最终一致性:

  1. 容忍短暂不一致: 网络延迟、事件丢失不会导致系统错误
  2. 简化设计: 不需要复杂的分布式事务
  3. 高可用: 即使 API Server 短暂不可达,系统仍能工作(使用本地缓存)

代价:

  • 需要定期全量对账(syncLoop)兜底
  • 用户可能会观察到短暂的不一致

15. 行为面试题与场景题

Q39: 描述一次你解决技术难题的经历

答: (基于项目经历)

背景:

在开发 traffic-manager 时,遇到 eBPF 程序加载失败的问题。Verifier 报错:“invalid indirect read from stack off -64+0 size 4”。

排查过程:

  1. 理解错误: Verifier 检测到可能的栈越界访问
  2. 检查代码: 发现二分搜索函数中 key.backend_slot = mid 这行,Verifier 无法确定 mid 的范围
  3. 分析原因: 虽然 lr 都是有界的,但 Verifier 无法追踪跨循环迭代的变量关系
  4. 解决方案: 将二分搜索的循环上限硬编码为 10(log2(1024)),让 Verifier 能够完全展开循环

结果:

  • eBPF 程序成功加载
  • 学到了 Verifier 的限制和绕过方法
  • 在 INTERVIEW_PREP.md 中记录了这个经验

总结:

遇到技术难题时,先理解错误信息,再逐步缩小问题范围,最后用系统化的方法解决。

Q40: 如果你发现生产环境流量分配不均,如何排查?

答: 分层排查:

1. 确认问题范围:

1
2
# 是所有 Service 都有问题,还是单个 Service?
# 是加权策略有问题,还是随机策略也有问题?

2. 检查 Kubernetes 层:

1
2
3
4
5
# EndpointSlice 的后端列表是否正确
kubectl get endpointslice -l kubernetes.io/service-name=<service> -o yaml

# 后端 Pod 是否都 Ready
kubectl get pods -l app=<app>

3. 检查用户态(Controller):

1
2
3
4
5
# 查看 Service 在 BPF Maps 中的状态
./traffic-manager --check-service-ip=<VIP> --check-service-port=<port>

# 查看 Controller 日志
journalctl -u traffic-manager -f

4. 检查权重计算:

1
2
3
4
# Prometheus 中的 node_load1 是否正常
curl 'http://prometheus:9090/api/v1/query?query=node_load1'

# 是否有节点 load1 >= 10(被认为不可用)

5. 检查内核态(eBPF Maps):

1
2
3
4
5
6
# dump service_slot_map 查看权重分布
bpftool map dump pinned /sys/fs/bpf/sock_ops_map/service_slot_map

# 查看统计信息
./traffic-manager --dump-stats
# 关注 backend_slot_misses 是否异常高

6. 检查 eBPF 日志:

1
cat /sys/kernel/debug/tracing/trace_pipe | grep traffic

16. 面试官追问与陷阱题

Q41: " 你的项目对 ExternalTrafficPolicy=Local 的 Service 怎么处理?”

答: 代码中定义了 scope 字段(LB_LOOKUP_SCOPE_EXT=0 / LB_LOOKUP_SCOPE_INT=1),但当前 Controller 始终传入 scope=0(Cluster 范围)。

对于 ExternalTrafficPolicy=Local 的 Service,只应将流量路由到本节点的后端。当前实现会把流量路由到任意节点的后端,这是一个已知的功能缺口

改进方案:

1
2
3
4
5
// 在 Controller 中识别 Service 的 ExternalTrafficPolicy
if service.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyLocal {
    scope = LB_LOOKUP_SCOPE_EXT  // 只路由到本节点后端
    // 需要额外过滤 EndpointSlice,只保留本节点的后端
}

Q42: " 为什么不用 Service Mesh(如 Istio)?”

答: 对比分析:

维度Traffic ManagerIstio
工作层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: 你的项目有哪些已知的限制?

答:

  1. 只支持 IPv4: 当前代码只处理 connect4sendmsg4recvmsg4,不支持 IPv6
  2. ExternalTrafficPolicy=Local 未实现: 如前所述,scope 字段未使用
  3. 权重指标单一: 只使用 node_load1,不考虑内存、网络等其他指标
  4. 没有健康检查: 完全依赖 Kubernetes 的 EndpointSlice,不主动探测后端
  5. 没有连接跟踪表: TCP 长连接虽然不需要,但 UDP 的亲和性表可能会在节点重启后丢失
  6. Leader Election 缺失: 每节点运行一个控制器,大规模集群下 API Server 压力大

Q45: 如果给你更多时间,你会如何改进这个项目?

答:

短期改进(1-2 周):

  1. 实现 ExternalTrafficPolicy=Local 支持
  2. 添加 IPv6 支持(connect6sendmsg6recvmsg6
  3. 支持更多权重指标(内存、网络 IO)
  4. 添加 Prometheus metrics 暴露(当前只有 --dump-stats

中期改进(1-2 月):

  1. 实现 Leader Election,优化大规模集群部署
  2. 添加主动健康检查(TCP connect 探测或 HTTP GET)
  3. 支持 L7 路由(基于 HTTP Host/Path)
  4. 集成 OpenTelemetry,提供追踪能力

长期愿景(3-6 月):

  1. 支持 Cilium 式的全功能 CNI 插件
  2. 支持 Service Mesh 模式(Sidecar 或 Ambient)
  3. 提供 Web UI,可视化流量拓扑
  4. 支持多集群服务发现

附录:面试速查卡片

核心数字:

  • eBPF vs iptables 性能提升:+19% ~ +28%
  • BPF Map 最大条目:65536(Service/Slot/Backend)
  • UDP 亲和超时:60 秒
  • 定期对账周期:10 秒
  • Worker 数量:NumCPU

核心设计模式:

  • 控制器模式:Informer + Workqueue + Reconciliation Loop
  • 二级间接引用:ServiceMeta → ServiceSlot → Backend
  • 优雅降级:Prometheus 不可用 → 等权重退化为随机

核心系统调用链:

1
connect() → cgroup/connect4 → service_meta_map lookup → slot selection → backend_map lookup → IP rewrite

必需的 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)

Licensed under CC BY-NC-SA 4.0
最后更新于 2026年4月26日星期日
使用 Hugo 构建
主题 StackJimmy 设计