👋 动态记录 & 转发分享 https://tg.okhk.net/ ✌️
2024 06 18 HackerNews
终于懂了懂了,这个 bug 查了整整两天。

症状是这样的,考虑 K8s 上在不同节点的两个 pods 通信,本来好好的,但是一旦启用了七层透明代理(envoy),流量的路径会变成 src pod -> envoy(src host) -> envoy(dst host) -> dst pod,然后 curl 就不通了。最奇怪的是必须两个 envoy 同时代理才会出问题,单 envoy 没事。

先确认丢包发生在哪一步,这一步在普通场景很简单,但是在透明代理的场景下会看到相同 tuple 的 skb 飞来飞去但又并不是同一 tcp 会话,需要格外小心仔细。此外我追了几小时自己 ctrl-c 打断 curl 导致的 SKB_DROP_REASON_NOT_SPECIFIED,很绝望。

确认 MTU 丢包之后开始仔细摸查内核处理 MTU 的代码。一定要放弃胡乱猜测,老老实实对照着汇编和源码人肉逆向。这一步异常辛苦,虽然还挺好玩的,但是如果有自动化的工具我会更加感动。

一个简单的例子,我想看那个被丢弃的 skb 执行到下面代码时的 mtu 变量是多少(我知道可以直接抓 icmp):

  IPCB(skb)->flags |= IPSKB_FORWARDED;
  mtu = ip_dst_mtu_maybe_forward(&rt->dst, true);
  if (ip_exceeds_mtu(skb, mtu)) {
    IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
    icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
        htonl(mtu));
    SKB_DR_SET(reason, PKT_TOO_BIG);
    goto drop;
  }


我的笨办法是 gdb -ex disas 检查 ip_forward 的汇编,先过一遍 call 指令看看都有哪些函数调用,立刻能发现一个指令是 call __icmp_send ,对应上面的 icmp_send() 函数,立刻能知道 mtu 作为第四参数在 rcx 寄存器,所以往前看 rcx 变量怎么来的,一点点就能分析出每行代码对应哪些汇编,每个变量在哪个寄存器、内存,然后可以用 bpftrace 仔细检查可疑的变量,如检查 ip_exceeds_mtu 函数调用时的各变量:

// if (ip_exceeds_mtu(skb, mtu)) {
k:ip_forward+492
{
    $skb = (struct sk_buff*)@tid2skb[tid];
    $ip = (struct iphdr*)($skb->network_header+$skb->head);
    if ($skb != 0) {
        $len = *(uint32*)(reg("bx") + 0x70);
        $mtu = reg("r13");
        $df = $ip->frag_off ;
        $fms = ((struct inet_skb_parm*)(((uint8*)$skb)+40))->frag_max_size;
        printf("mtu=%ld len=%lld df=%d fms=%ld\n", $mtu, $len, $df, $fms);
    }
}


就这样反复排查所有可疑的地方,最终我找到了两个根因:

1. xfrm 会导致 mtu 隐式下降,没有任何命令行工具能够检查出来 mtu 居然从 1500 降到了 1446;
2. envoy 透明代理导致 TCP 握手时交换的 MSS 变大了,pod->pod 的时候 MSS 是 1383,envoy->pod/envoy 的 MSS 是 1460。内核会把 TCP MSS 设为 skb gso_size,这里变大的 gso_size 导致 MTU 检查过不去导致内核丢包。MTU 检查很多,并不是包长大于 MTU 就直接扔,这里也花了很多精力。

快解是把相关 netdev mtu 减小到 1446,但是会影响 node2node 流量,下周再想办法吧。。
2024 06 14 HackerNews
Back to Top
OKHK