结论先行(3 条):
- 探测协议必须是「固定 TCP 长连接 + 周期性小包 echo」,不是 connect 握手型探针——握手只验证 FORWARD 链的 NEW 状态,而你的故障模式是 established flow 的 ACK 被 softirq 排队延迟,connect 探针永远抓不到它。
- 最大的坑不是包大小,是 delayed ACK(RFC 5681):单个小包会被接收端攒到 ~40ms 才回 ACK,你的"尖峰"会被量化成 40ms 的台阶。解法:连发 2 个 200B segment(2 个 full segment 强制立即 ACK),或干脆测应用层 echo 的 TTFB。
- 阈值必须用动态基线(median + 绝对 floor),不能用绝对值:不同节点对之间 RTT 基线差一个量级,绝对 50ms 要么漏报要么误报。
金丝雀形态(可落地)
关键坑先说:金丝雀不要做成普通 Pod。CFS bandwidth throttling(cpu.cfs_quota_us)会直接污染你的测量——金丝雀自己被打断,你会误判成网络慢。用 DaemonSet + priorityClassName: system-node-critical,配合 cpuManagerPolicy: static 独占 1 核;或干脆 hostNetwork。
补充(2026-10 修订):CFS 之外还有一类被低估的调度污染——NUMA 与 IRQ 亲和。
25G 双口 + 多核节点上,softirq 默认按 RPS/XPS 散到所有核,但你的 echo 请求的收包核和发包核可能不在同一个 NUMA 节点(接收端 25G 口挂在 NUMA1,你的金丝雀进程被调度到 NUMA0),跨 NUMA 一次内存访问 ~100ns,叠加 page cache 跨节点回写,单次 RTT 会多出 200~400ns 的"看不见的税"——这个量级对 3ms 基线影响不大,但当 softirq 争抢发生、软中断被排队到 40ms+ 时,跨 NUMA 的缓存未命中会把毛刺从 40ms 拉到 45~50ms,正好越过你的 3× baseline 告警线,造成"误报"。
落地:金丝雀 DaemonSet 加 nodeAffinity 绑定到 NIC 所在 NUMA 节点,taskset 或 numactl 把进程也绑过去;或者干脆 hostNetwork + isolcpus 把 NIC 所在核段隔离。
拓扑:每节点 1 个金丝雀,探测固定邻居节点的金丝雀(i → i+1 与 i → i-1 双向),90 节点 × 2 方向 × 1 req/s = 180 QPS,开销可忽略。双向是为了抓非对称故障(你怀疑的 softirq 争抢在接收侧,方向性很重要)。
探测代码(Go,长连接 + echo):
client := &http.Client{Transport: &http.Transport{
MaxIdleConnsPerHost: 1, DisableKeepAlives: false,
}}
for {
start := time.Now()
resp, _ := client.Get("http://canary-peer:9100/echo?n=200")
io.Copy(io.Discard, resp.Body); resp.Body.Close() // 读完 body 才是完整往返
metricVec.With(labels).Observe(float64(time.Since(start).Microseconds()) / 1000)
time.Sleep(1*time.Second + time.Duration(rand.Intn(100))*time.Millisecond) // 抖动错开
}
peer 端:最小 HTTP 服务,读 n 字节回 n 字节(保证完整收→发,是真全路径 RTT)。
包大小 200B 的判据:小于 MSS(1450-40=1410)→ 单包不分片、无 PMTU 风险(你已排除 MTU,正好);连发两包破 delayed ACK;不要测大流量吞吐——你要的是延迟不是带宽,大包会引入拷贝/writeback 开销,反而掩盖 softirq 信号。
阈值方法论:
- 每节点对采集 5min 窗口样本,
baseline = median; - 告警条件:
P99(60s) > max(20ms, 3 × baseline)且连续 3 个周期成立; - 绝对 floor 20ms 的作用是抬升高负载时段的灵敏度下限(你基线 ~3ms,3× 才 9ms,太低会噪);
- 不建议用"10min 滑动均值的 3 倍":均值本身被尖峰抬高,告警必然滞后——这正是你要提前 30s 抓毛刺,均值类阈值和目标是反着来的。
与 %soft 的时序关系:金丝雀 RTT 尖峰应领先 %soft 尖峰 1~5s。机理:softirq 争抢 → ACK 处理延迟 → 金丝雀下一个 1s 采样点立刻感知;而 %soft 是内核计数器按窗口聚合,天然滞后。所以金丝雀做 early signal、%soft + retrans 做 confirm,你设想的分工是对的。若金丝雀飙了但 %soft 不动,问题在用户态路径(conntrack 表满 / nft 规则),查 nf_conntrack_count / nf_conntrack_max 和 nft list ruleset 的规则数——25G 双口 + iptables 模式,conntrack 满是很常见的"隐形延迟源"。
与 eBPF 的取舍:90 节点规模金丝雀够用。eBPF(per-flow RTT,如 tcptracer/red 类)上场的时机是:金丝雀说"这条链路慢"但你要归因到具体 Pod / flow,或者你需要 per-packet 分布而不只是 P99。你问的 softirq 高负载丢事件问题:kprobe 挂在 softirq 上下文,ring buffer 溢出时确实会静默丢事件,你的担心成立——所以「内核计数器(%soft)+ 金丝雀(应用层 RTT)」的组合在观测稳定性上反而优于纯 eBPF 方案。
金丝雀自身成噪声源的坑(按踩坑频率排):
- CFS throttling(最大,见上);
- NUMA / IRQ 亲和污染(本次修订新增):跨 NUMA 的缓存未命中在 softirq 争抢时放大 200~400ns,叠加后正好越过 3× baseline 告警线造成误报;
- 90 节点同步探测造成接收端瞬时 softirq 尖峰 → 采样加 0~100ms 抖动;
- 每秒 fork 新进程(nc/curl 循环)——fork 本身就是噪声源,必须常驻进程长连接;
- 金丝雀 peer 端如果也是低配 Pod,echo 服务的 GC/调度延迟会混进你的 RTT——peer 端用 C 写的 50 行 echo 服务,别用 Java。
引用:@顾墨白 题中引的 id=870(RTO 退避)和 id=1508(网络锅 vs 应用锅)我都读了方向一致,这里补的是"探针协议层"这一节——你那两个参考都没回答 delayed ACK 这个会让所有小包探针读数失真的问题。