Hi,欢迎来到 卡瓦邦噶!我是 laixintao,现在生活在新加坡。我的工作是 SRE,喜欢在终端完成大部分工作,对各种技术都感兴趣。我从 2013 年开始写这个博客,写的内容很广泛,运维的方法论,编程的思考,工作的感悟,除了技术内容之外,还会分享一些读书感想,旅行游记,电影和音乐等。欢迎留下你的评论。

声明:本博客内容仅代表本人观点,和我的雇主无关。本博客承诺不含有 AI 生成的内容,所有内容未加说明均为博主原创,一经发布自动进入公有领域,本人放弃所有权利。转载无需本人同意。但是依然建议在转载的时候留下本博客的链接,因为这里的很多内容在发布之后会还会不断地继续更新和追加内容。 Not By AI

网络问题排查的思路和技巧

这一篇计划很久了还没有写,今天终于开始下笔了。

许久无从下笔的原因是,网络问题排查——哦不,应该说不光网络,基本所有的问题排查,都是这样——思路相比于技巧要重要很多。思路,简单来说就是几句话的事情:

  • 依据事实猜测而不是全凭经验猜测;
  • 验证测试,比如使用工具复现,或者实现一个最小化的 case;
  • 提出 fix;
  • 实施 fix;

核心就是一句话,「基于事实猜测」,如果实践起来,却不是那么容易。顺藤摸瓜的正确排查方法是需要大量的基础以及经验支撑的,并不是什么独门秘籍,可以让人瞬间开悟的东西。

但还是有一些规律可以遵循。

一般的问题定位,第一步是明确问题。很多人表达问题不清楚的,与这种人沟通很容易就陷入了 X Y 问题1。所以无比问清楚:现在问题是什么,网络从哪里到哪里不通,预期的结果是什么。在非常复杂的网络环境下,需要收集必要的信息,比如有没有 overlay,有没有一些特殊的配置。可能问题的报告者也说不清,大部分情况需要自己收集信息。

第二步就是复现问题。如果问题没有办法复现,就很难解决。如果可以复现,基本就解决了 90% 了。我自己最怕的问题也是无法复现的问题,比如高流量的偶尔延迟高,复现的现象定义不明确,复现条件也不明确。有一些工具可以使用,比如 iperf,scapy,或者自己写一些程序2来复现。有人遇到问题直接去查有没有变更,去看监控,这就像无头苍蝇。从根本解决问题,第一步一定是复现,最后一步对应的,是确定之前复现的场景下问题已经解决了。当然了,如果是线上的故障,立即查找变更和监控是正确的,应该需要在不明确根因的情况下能够恢复故障的手段。

第三部就很简单了,根据现象,做合理的假设,测试,逐步排除错误的假设,剩下的就是正确的答案。

时间长了,每个人都会积累一些技巧。比如,在最近的上一篇博客中使用到的,找一个正常的机器,进行比较一下。积累也是一个比较重要的过程,我喜欢把有意思的 case 都记录下载,对我很有帮助。

我发现厉害的人(偶尔,我自己也是),在看相同的信息的时候可以得到更多的结论。比如看到同样一个网络问题,使用同样的工具查看,别人说是网络不通,高手通过 curl -v 看发现是网络可以连接,而服务器没有返回内容。就把问题进一步定位到了 7 层问题,可能是 LB,WAF,application 的问题,但不会是路由或者 TCP 的问题。再比如,看到同样一个 MTR,高手可以分析出来默认网关有没有问题,出机房有没有问题,是单设备的问题还是某一跳的问题。

这是一种很酷的能力:能够在有限的环境里面定位到根本问题。

又想起来一个例子,我们经常需要网工给我们配置路由,但是配置好了没有?我们无从得知,因为我们没有交换机的登录权限。遇到问题的时候,低手会直接去麻烦人家,问问这个路由配置好了没,人家说好了,也没有办法验证是不是真的好了。高手在麻烦人家之前必然会提供相应的证据,比如,使用 traceroute,目标为这个网段的任意一个 IP,能不能 ping 通并不重要,重要的是看网工配置的交换机有没有出现在 traceroute 里面。这样报告问题,无法甩锅,解决问题的速度也快。

就写到这里,有很多大道理讲,但是讲起来没有什么说服力。好在日常已经积累了很多问题排查的博客,写具体的例子还是比较有意思。读者可以关注一下我的这类文章。

对了,在 AI 时代,如今大部分问题 AI 如果有权限都可以排查出来了,我们人类排查问题的技能还有用吗?

当然是有的,排查问题,解决问题,再想怎么一劳永逸地解决问题,对最终的质量负责,还是需要人的。如果你把一个系统完全交给 AI,AI 来应对用户的问题和需求,这里面没有了人做 trade off,感觉过不久就会变成非常大的一坨屎吧。

不过这也是现在,说不定多段时间 AI 真的可以完全自主运行系统了。在那之前,我们还是好好享受技术带来的快乐和成就感吧。

  1. https://zh.wikipedia.org/zh-sg/XY%E9%97%AE%E9%A2%98 ↩︎
  2. 现在有了 AI,这样做已经非常方便了 ↩︎
 

一次内存引起的网络丢包问题排查

记录一下最近排查的一个问题:某台机器一上线就有丢包,同 Rack 同规格的其他机器都没有问题,由于负载都是一样的,所以问题集中到这台机器本身的问题。已经可以 100% 复现,只要一接流量,网卡就开始时不时的丢包。

网卡使用率对应丢包,左边蓝色的线是有问题的机器,0% 是下线状态,可见,只要一上线就会丢包

初步检查:网卡的 CRC 正常,光模块正常。(这些一般是丢包的常见问题)

仔细检查网卡的计数器,发现丢包发生在 rx_prio0_buf_discard: 397172。

中间经历了很多错误的排查思路,比如这个 metric 是每一个 NIC 都有一个,但是 driver 的实现应该是用了同一个 metric,因为 2 个 NIC 的数字总是相同的。

以为是 DCB 的 buffer 不够,尝试了调大 DCB 的 buffer。

发现 buffer 大了丢包反而更严重了一些,buffer 调小也是有丢包。应该是和 buffer 无关。

由于是 rx 的丢包,应该是 ingress 的处理能力不足,接下来的排查包括了:

  • 查看 PCIe 的带宽,状态;
  • 调整 Interrupt coalescing,让它处理更快;

……都没有效果。

中间经历了一次大促,容量足够,就暂停排查了,这周又开始继续。

好像和网卡有关的东西检查都是正常的,同事建议换一张网卡试试。换之前我使出了最后的一招:server diff.

server diff 是我排查问题作为没有思路之后的一个手段,可以简化成一段 skill:

这是一个正常的机器,这是一个异常的机器。你对比一下两个机器之间的 sysctl 参数,kernel 参数,硬件规格,看下两台机器有什么不一样的部分。

AI 发现的问题如下:

能找到的差异点(按怀疑程度排)

  1. 硬件/内存健康 IPMI SEL:08/31 16:46 和 16:49 两次 Uncorrectable ECC DIMMB5;mcelog 有 08/31 15:39 一条 Memory read corrected error;随后 08/31、09/02、09/03 连续多次重启。DIMM 全是 Micron 36ASF4G72PZ-3G2J3(.144 是 Samsung M393A4K40DB3-CWE),B5 序列号 2E313345,看不出被换过。 本次启动(09-03)以来 EDAC ce/ue 都是 0,但 Dell 在 UE/CE 后可能进入 ADDDC/lockstep 降级模式, 会拉低内存带宽/延迟。 保留意见:网卡在 NUMA node0(CPU1/A 组),B5 属于 CPU2(B 组),RX ring 页也在 node0, 理论上不直接受影响;但这台机器的内存子系统整体存疑。
  2. …

这是非常可疑的一个点,Linux 的收包链路上,NIC DMA 到内存1,路线是:

PCIe NIC → PCIe Root Port/Root Complex → IOMMU → CPU Mesh/Interconnect → LLC(L3) / Memory Controller → DRAM

内存子系统异常会显著降低 host RX path 的处理和 buffer 回收能力,可能进一步造成 NIC ingress buffer 无法及时 drain,最终表现为 rx_prio0_buf_discard。

于是我下线机器之后测试内存的带宽,我们使用的服务器有 2 个 CPU,2 个 NUMA node,于是分别测试。

测试使用 stream.c 2结果如下:

(CPU 2 和 3 分别在 NUMA node 0 和 1)

可以看到,node 1 的内存,无论是同 NUMA node local 读写还是跨 NUMA remote 读写,速度只有区区 700MB,性能只有 node 0 的 1/17,确定 node 1 的内存有问题了。

进一步验证,由于我们使用的 NIC 是双网口 bond,port 1 的中断绑定到 node 0 的 CPU,port 2 的中断都绑定到 node 1 的 CPU,(这样提高 CPU 的利用率,又提供稳定的转发延迟)。所以可以通过关闭 port 2 来关闭 node 1 的使用,验证只有 node 1 的内存有问题。

关闭一个网卡之后上线,再也没有发现有丢包。

由于是单卡模式上线,机器分到的流量不变,所以这台机器的网卡使用率是其他机器的2倍,但是丢包是0.
  1. 见服务器高性能网络调优 ↩︎
  2. source code: https://www.cs.virginia.edu/stream/FTP/Code/stream.c ↩︎
 

BPG 连接建立失败问题的答案和解析

本文是 BPG 连接建立失败问题 一文的答案和解析。

抓包文件中有两处可疑的地方。

第一处是 BGP OPEN 消息建立的过程,我可以直接 follow 这个 tcp stream:

Open 消息

可以看出:

  • Server 是可以正常和对端设备建立 TCP 连接的,所以 4 层以下的网络没有问题;
  • 在 TCP 连接建立之后,一旦发送 OPEN 消息,直接收到了 TCP RST,意味着对面的设备拒绝和我们建立 BGP 连接;
  • 我们收到的 RST 包,IP 层的 TTL 是 1,这个也很可疑。

另一处可疑的地方是,对面的设备一直再对我们发送 keepalive 的消息:

从对面发给我们的 keepalive 消息

综上可以推断:我们的 Server 和对面的设备已经存在一个 BGP 会话了,而且 Server 上建立 BGP 会话使用的 port 是 22709,根据这个 port,我们可以找出来是哪一个进程。

后来我们发现,机器上有一个 bird 进程,使用 EBGP (这也就是为什么发过来的 keepalive 包的 TTL 都是 1 的原因)与对面的设备建立连接。由于连接已经建立好了,所以我们的进程如果使用 IBGP 再尝试建立连接的话就会被直接拒绝。

为什么一次升级会触发这个问题呢?

因为之前我们的 IGBP 会话是正常的,bird 尝试的 EBGP 反而是一直失败的,在我们重启过一次之后,EBGP 会话成功建立了,我们的 IGBP 就一直失败。

(还好只是一个无关紧要的环境,上面部署的东西太乱了。)

 

BGP 连接建立失败问题

好久没有更新「抓包破案」系列了,最近遇到的一个 case 适合拿出来考一考读者,需要一点点的 BGP 前置知识,嘿嘿。

事情是这样:在一次日常升级中,服务器原本可以与一个网络设备建立 BGP 连接,但是升级之后无法建立了。

服务器升级操作执行的内容有:

  • 更新程序;
  • 重启;

理论上应该可以重新连接对端 IP 的 179 端口,然后恢复服务。但是升级之后一直没有流量进来,排查发现是 BGP 异常。为什么重启之后,原本好好的 BGP 就异常了呢?

我们进行了抓包,文件如下:

看了半天也没有看出所以然来,于是在终端启动了一个 AI Agent,让它分析这个文件。

第一轮,agent 也没有分析出来问题。因为调用的工具失败了。

我告诉它不应该用 wireshark 这种 GUI,对你来说最好的工具应该是 tshark。

Agent 开始使用 tshark 分析,不到一分钟,就准确地告诉了我答案,令人震惊!

回到这个问题,我发现即使不使用 AI,也是完全可以破案的,这里就把问题交给读者,看看能否发现异常的本质。

 

Ingress 流量整形

问题是这样,我们有 2 组服务器,A 组和 B 组,一部分用户从 A 组下载文件,一部分从 B 组下载文件。分成 2 组的初衷是,B 组用户下载量通常比较大,并且是离线服务,意味着可以慢一些,但不能影响 A 组用户。现在的问题是,A 组 和 B 组最终下载的来源都是 S3,依然存在共享资源,会出现问题:在下载量比较大的时候,占用全部的 S3 上传带宽,导致 A 组用户收到影响。

我们希望限制 B 到 S3 的带宽,比如 10Gbps.

有几个限制:

  1. 我们无法在 Client – Server 这里限流,因为 Server 侧有 Cache,我们希望 Client – Server 这里依然可以使用非常大的带宽来下载;
  2. S3 的服务器没有限流的功能,这些服务器我们也无法控制,我们只能配置 Server A 和 Server B;

这个问题的难点在于:流量整形一般都是做在 egress 的,因为 egress 天然有一个 queue,发送端可以决定在什么时候发出去 packet,从而把发送速率可以稳定控制在一个想要的比例。

但是我们这里的场景是 ingress,对于 ingress 是没有 queue 的,流量整形一般也无法作用在 ingress ——因为包已经被物理网卡收到,DMA 完成,作为 ingress 侧是无法决定这个包什么时候到达的,只能决定怎么处理这个包。所以 ingress 侧一般是 classification, action,即 drop。

基于 Drop 的 policing(真形象的名字,超速开罚单的警察) 就有一个问题:由于 TCP 的拥塞控制机制,发送的带宽会逐渐上涨,最终会超过 10Gb。限流开始生效时,sender 瞬时发送速率超过 10 Gbit/s,并持续消耗 policer 的 token;当 token 不足时,后续 packet 被判定为 exceed 并丢弃。

大量包被丢弃导致 TCP 的发送端认为发生了拥塞,会大幅下降 cwnd。实际测试,如果用 policing,实际能跑的速度大概在 5-7Gb,而且不稳定。

Policing 和 Shaping 的区别和效果,Cisco 博客的这张图非常直观:

image source

以下是一个模拟环境,使用 policing 限速在 1Gbps (为了模拟环境里面用 wireshark 分析速度快一些,这里用 1Gbps 的限速为目标),其 throughput 如下图所示:

可以看到限速在 1Gbps 的时候,实际速度在 850Mbps 左右震荡
Window Scaling 图

再看 window scaling,可以看到 outstanding bytes 的上包络线呈现出明显的 AIMD/CUBIC loss-response 特征;结合 sender 侧的 ss -ti 可以确认 cwnd 在 loss 后也发生了类似下降——在上升到一定的大小之后,就会因为超过 policing 的限速而被 drop 一些,TCP 在发现有丢包之后,就降低 cwnd,从而降低发送速度,符合 AIMD。(这个环境用的拥塞算法是 cubic,默认的参数 cat /sys/module/tcp_cubic/parameters/beta 是 717,下降的比例是 717/1024 ≈ 0.7,和图中相符,图中是从 500kb 下降到 350kb)。

这样用户就不高兴了:为什么实际只能用到 5Gb 左右,不是说好了 10Gb 的吗?

如果要做到规整的 10Gb,这里就需要用到流量整形了,traffic shaping。

Linux 有一个特殊的驱动:ifb,全称是 Intermediate Functional Block,是一个虚拟网络设备,主要用来把原本的 ingress 流量重定向到一个“可做 egress qdisc”的设备上,从而实现 ingress shaping。

本质上,就是在收包链路上加一个网卡绕一下,这个网卡会调用 egress 的代码,这样我们就可以使用 tc 原来的逻辑做 egress 的 traffic shaping 了。

加上 ifb 虚拟网卡之后,包的路线如下:

IFB 加入之后的包路线

在 ifb_xmit() 的时候,看起来是一个负责发送包的代码,实际上它的逻辑是:修改 skb 的 dev 为原来真实的进来的那个 dev1,(而不是 ifb 这个虚拟的),然后进入到 netif_receive_skb() kernel 网络栈里面。

这样,我们就可以用这个 egress 的 qdisc 做 shaping 了。

实现的代码如下:

第一步,创建 ifb 虚拟接口,并且把流量从物理网卡 redirect 到 ifb:

第二步,创建一个 HTB,有 2 个 class,一个有限速 10Gb,另一个没有,可以跑到物理线速。

第三步,把要限速的 IP 放到 1:10 class:

然后测试吞吐,证明实际的 throughput 稳定在 956 Mbits/sec。

在 ifb 参与的 traffic shaping 下,限速可以稳定在 950 Mbits
cwnd 也在稳定地上涨

这样就达到了我们的目的:这条链路的速度稳定地使用限速 1Gbps 左右。

有几个小问题值得讨论。

为什么是 950 Mbits/sec 而不是 1Gbits/sec?

tc 做限制的时候,计算的是 skb 的 size,1500 Bytes,而这张图(以及使用的 iperf3 的显示)是计算的 TCP 的 body,即 MSS,1448 Bytes。1000 × 1448 / 1500 ≈ 965 Mbit/s。

在 cwnd 的图里面,刚开始的时候有一些丢包,为什么?

TCP 启动时通常先经过 slow start,cwnd 快速增长;进入 congestion avoidance 后,CUBIC 按三次函数继续探测可用带宽。发生 loss 后,它记录此前的窗口位置并降低 cwnd,随后重新逼近旧的 Wmax。

在这个图里面,只有一开始的时候有 loss,后续几乎没有 loss,贴着 throughput 发,说明 traffic shaping 做的不错。

按照 TCP 的拥塞控制算法,AIMD,即使是 traffic shaping 不也应该在最大窗口附近震荡吗?为什么这个图看起来这么平滑?

一个原因是,在逐渐逼近 1Gbps 的时候,limit 已经不是 cwnd 了,这种情况下 cubic 不会再激进地增加 cwnd2。通过 ss -ti 可以看到,大部分的时间卡在 sndbuf_limited:4688ms(29.6%)。

如果调整一下参数,扩大 wmem,cwnd 实际也会震荡的。

最后一列是 cwnd,也在不断震荡

最后一个问题也是最关键的问题,为什么在 traffic shaping 之后,cwnd 的变化速度变得这么平滑了呢?都是一样的 cubic 算法,为什么不像 policing 那样剧烈下降?以及为什么这里 send buffer 出现了瓶颈?

使用 policing 时,包进入 ingress 只有 2 中 action:

  • 接受,然后 kernel 的网络栈会 ack,处理包;
  • 直接 drop,没有 ack,导致 cwnd 下降。

使用 ifb 的 traffic shaping 之后,ingress 的包进入物理网卡,被 mirror 到 ifb,然后在这里经过 queue 排队。经过流量整形之后,包到达 kernel 的网络栈,然后发出 ack。这里的关键是,tc 添加了一个漏斗,大量的包(当然,buffer 需要内存的,内存是有限的,如果大到一定的程度就只能 drop 了)进来之后都存起来,然后用固定的流速放出去。只有被放出去的包才进入网络栈,只有进入了网络栈,kernel 才会给 sender 发回去 ack,此时,sender 的 cwnd 才有了新的空闲,新的包得以发送。

再换一个角度解释一下,从 sender 的角度,假设 cwnd 是 4,sender 发送了 4 个包,在收到 ACK 之前,无法再发送新的包了。这时候,receiver 的 shaper 每秒通过一个包,即每秒会发回来一个 ack,这样,后续的每一秒,比如5,6,7,8,sender 都可以再发送一个包到网络上。

send buffer 是另一层限制。TCP 已经发送但尚未被 ACK 的数据仍然需要保留在 send buffer 中,以便发生丢包时重传。因此 IFB shaping 不仅延迟 ACK、限制 cwnd 中空间的释放,也会延迟 TCP send buffer 中已发送数据的释放。当 send buffer 不够大时,sender 甚至可能在 cwnd 尚未用满之前就受到 send buffer 限制。

所以 IFB 以固定速率释放数据后,ACK 也以相应的节奏返回。ACK 一方面释放 cwnd 中的 outstanding 空间,另一方面使已经确认的数据可以从 send buffer 中清除。两者共同使 sender 的发送节奏逐渐跟随 shaper 的速率。

即,网络上最多可以飞多少,是 cwnd/rwnd 等窗口决定;什么时候可以继续注入新 packet,则由 ACK clock 驱动。(而 ACK 也会让总的 cwnd 提高)。

以上就叫做 TCP 的 self-clocking3,ACK 本身就像“时钟脉冲”,会决定发送速度。

这个机制有一个关键就是包没有被丢弃,只是 ack 的时间晚了一些,如果包被丢了(比如 policing),那么 cwnd 会下降,无法匀速发送。

  1. https://github.com/torvalds/linux/blob/master/drivers/net/ifb.c#L115 ↩︎
  2. if (!tcp_is_cwnd_limited(sk)) return; https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_cubic.c?utm_source=chatgpt.com#L326 ↩︎
  3. 在 Computer Networking: A Top-Down Approach Chapter 3.7 也有介绍 ↩︎