阅读视图

发现新文章,点击刷新页面。
🔲 ☆

MTU Probe 引起的初始延迟

答案是 MTU 设置错误1,链路可以发送的最大 MTU 小于 1500 bytes,但是客户端侧配置的 MTU 却是 1500 bytes。所以 TCP 连接建立之后的包,无法到达另一端。在等待 3.5s 左右,发送端觉得链路存在问题,尝试降低发出的包的大小,这时候就可以发过去了,链路上才开始数据传输。

要定位这个问题,我们现在抓包文件中,找到传输数据最多的那个 TCP flow,就是传输数据最多的那个。这个连接是我们重点研究的对象。右键选择 Follow > TCP Stream,来只展示这一个会话。(如何追踪某一个 TCP 流,在前面 重新认识 TCP 的握手和挥手2 中也介绍过)。

TCP connection 建立之后,客户端一下发了 10 个包,大小都为 1500 bytes。但是服务端一个都没有 ACK。

前10个包都没有得到服务端的 ACK,前两个包也可以看到 TCP 两端协商的 MSS 是 1460 bytes

然后客户端开始重传。

前 3 次重传分别发生在 0.3s, 0.8s, 1.7s,也都没有收到 ACK。

第 4 次重传发生在 3.5s 这个包不再以 1500 bytes 发送了,而是 1076 bytes (TCP 层是 1024 bytes) 发送。这个包得到了服务端的 ACK。

第 4 次重传降低了 MTU,并且收到了 ACK

于是客户端知道,1076 bytes 的包可以被服务端收到,所以后续都用 1076 bytes 作为 MTU 发送。过一段时间之后尝试更大的包,1288 bytes,1394 bytes,发现也没有被 drop,就继续 probe 并且增大 MTU,最后用 1399 bytes,接近链路的真实 MTU (实际链路 MTU 是 1400 bytes)。

这就是每一个连接都会卡 3.5s 的原因。

为什么要重传 3 次才开始降低 MTU 呢?

这是通过 tcp_retries1 设置的。man 是这么说的:

The number of times TCP will attempt to retransmit a packet on an established connection normally, without the extra effort of getting the network layers involved. Once we exceed this number of retransmits, we first have the network layer update the route if possible before each new retransmit. The default is the RFC specified minimum of 3.

即 TCP 会尝试 tcp_retries1 次数的重传,而不涉及 IP (network) layer.

如果超过了 tcp_retries1 还没有重传成功的话,(默认第 4 次)就会认为链路存在异常了,需要 IP 层接入,比如开始 mtu probe 尝试解决问题。

如果设置为 1,那么在卡的时间就会降低到 1s 左右。(当然,最好的解决方式还是设置正确的 MTU)。

# sysctl -w  net.ipv4.tcp_retries1=1                                                                                                                                                                                             
net.ipv4.tcp_retries1 = 1

# iperf3 -c 10.0.2.2 -b 10M
Connecting to host 10.0.2.2, port 5201
[  5] local 10.0.1.2 port 54580 connected to 10.0.2.2 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec  0.00 Bytes  0.00 bits/sec    2   1.41 KBytes
[  5]   1.00-2.00   sec   128 KBytes  1.05 Mbits/sec   22   15.7 KBytes
[  5]   2.00-3.00   sec  3.50 MBytes  29.4 Mbits/sec    2    150 KBytes
[  5]   3.00-4.00   sec  1.25 MBytes  10.5 Mbits/sec    0    200 KBytes
[  5]   4.00-5.00   sec  1.12 MBytes  9.44 Mbits/sec    0    200 KBytes
[  5]   5.00-6.00   sec  1.25 MBytes  10.5 Mbits/sec    0    200 KBytes
[  5]   6.00-7.00   sec  1.12 MBytes  9.44 Mbits/sec    0    200 KBytes
[  5]   7.00-8.00   sec  1.25 MBytes  10.5 Mbits/sec    0    200 KBytes
[  5]   8.00-9.00   sec  1.12 MBytes  9.44 Mbits/sec    0    200 KBytes
[  5]   9.00-10.00  sec  1.25 MBytes  10.5 Mbits/sec    0    200 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  12.0 MBytes  10.1 Mbits/sec   26             sender
[  5]   0.00-10.02  sec  12.0 MBytes  10.0 Mbits/sec                  receiver

iperf Done.

而根据 TCP 的重传时间,大概就是在 3.5s 左右了。

前面说,第 4 次重传的包是从 1076 bytes 开始发送,为什么是这个数字呢?

这个是 sysctl 参数 net.ipv4.tcp_base_mss = 1024 控制的。TCP payload 是 1024 bytes,加上 TCP 的 header,就是 1076 bytes 了。

如果读者尝试复现这个例子,会发现在 MTU 设置错误的情况下,会一直卡住。这是因为 MTU 出现问题的时候,默认的工作机制是靠 ICMP 消息 “Fragmentation Needed” (Type 3, Code 4 in IPv4) 来提示需要降低 MTU 的。但是现实情况中,这个 ICMP 一般是收不到的(因为各种设备为了所谓的「安全问题」丢弃了 ICMP),这样就会出现 blackhole,一直重试缺卡在这里。默认情况下,MTU 错误的设置地大了,是会一直卡住的。

但是可以通过 sysctl 参数设置 net.ipv4.tcp_mtu_probing,默认是 0,即不用 probe。

如果是 1 的话,在出现 blackhole 的时候就会尝试降低 segment 的大小看能不能通。即本文中的情况。

如果是 2,是一直启用,即 TCP 不管对方说的 MSS,发送的时候总是从一个小的 MSS 开始发,如果没有问题,再逐渐加大。

MTU 常见的原因,以及一些其他的相关讨论,可以参考下面列出来的 有关 MTU 和 MSS 的一切 一文。

  1. 有关 MTU 的详细分析,可以参考之前的文章:有关 MTU 和 MSS 的一切 ↩
  2. 重新认识 TCP 的握手和挥手:答案和解析 ↩


==抓包破案录==

这篇文章是抓包破案录系列文章(之前叫做《计算机网络实用技术》,后来改名了)中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网络抓包与分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

如果您正在阅读的是题目类的文章,这个目录内容正好用来隔离其他读者的评论。读完题目可以稍作暂停,进行思考,继续向下滑动,可能会被其他的读者剧透答案。

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析阅读加餐!
  8. 重新认识 TCP 的握手和挥手答案和解析
  9. 3.5 秒初始延迟问题答案和解析
  10. 网络断断续续……答案和解析
  11. 延迟增加了多少?答案和解析
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. TCP 下载速度为什么这么慢?答案和解析
  14. 请求为什么超时了?答案和解析
  15. 0.01% 的概率超时问题答案和解析
  16. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
🔲 ☆

3.5 秒的固定延迟问题

一天工作日,你拿着刚买的咖啡来到了办公室,准备开始做计划好的工作,度过本该是平平无奇的一天,直到——一位用户发过来消息说他们有新的机器上线之后,所有的 TCP 连接都自带3.5s左右的延迟!他们的服务在使用新的服务器之后,延迟都上升了 3.5s!

经过他们自己的 debug,他们发现,延迟增加之后,在 TCP 连接建立之后,有3.5s 的时间没有发送数据,之后,网络就正常了!然后我们知道,不光服务器是新的,机架,网络设备,都是新的。这批服务器本不该你来负责,但是这个现象也太怪了!所有人都知道你是公司里的网络专家,如果有有解决不了的网络问题,就会来找你。

你让用户用 iperf 测试一下带宽1,用户测试了一下,结果如下:

$ iperf3 -c 10.0.2.2 -b 10M
Connecting to host 10.0.2.2, port 5201
[  5] local 10.0.1.2 port 45026 connected to 10.0.2.2 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec  0.00 Bytes  0.00 bits/sec    2   1.41 KBytes
[  5]   1.00-2.00   sec  0.00 Bytes  0.00 bits/sec    1   1.41 KBytes
[  5]   2.00-3.00   sec  0.00 Bytes  0.00 bits/sec    0   1.41 KBytes
[  5]   3.00-4.00   sec   384 KBytes  3.15 Mbits/sec   24   22.4 KBytes
[  5]   4.00-5.00   sec  5.12 MBytes  43.0 Mbits/sec    0    221 KBytes
[  5]   5.00-6.00   sec  1.75 MBytes  14.7 Mbits/sec    0    328 KBytes
[  5]   6.00-7.00   sec  1.12 MBytes  9.44 Mbits/sec    0    328 KBytes
[  5]   7.00-8.00   sec  1.25 MBytes  10.5 Mbits/sec    0    328 KBytes
[  5]   8.00-9.00   sec  1.12 MBytes  9.44 Mbits/sec    0    328 KBytes
[  5]   9.00-10.00  sec  1.25 MBytes  10.5 Mbits/sec    0    328 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  12.0 MBytes  10.1 Mbits/sec   27             sender
[  5]   0.00-10.02  sec  12.0 MBytes  10.0 Mbits/sec                  receiver

iperf Done.

还真是和用户说的一样!

这必须要抓包一下才知道原因了!用户又做了一次 iperf,并且同时执行 tcpdump 进行抓包,过一会儿,就发来了抓包文件。

你看了一会,然后马上就发现了不对劲的地方……

请分析这个抓包文件,找出固定3.5s延迟的问题在哪里。

  1. https://iperf.fr/ ↩

==抓包破案录==

这篇文章是抓包破案录系列文章(之前叫做《计算机网络实用技术》,后来改名了)中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网络抓包与分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

如果您正在阅读的是题目类的文章,这个目录内容正好用来隔离其他读者的评论。读完题目可以稍作暂停,进行思考,继续向下滑动,可能会被其他的读者剧透答案。

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析阅读加餐!
  8. 重新认识 TCP 的握手和挥手答案和解析
  9. 3.5 秒初始延迟问题答案和解析
  10. 网络断断续续……答案和解析
  11. 延迟增加了多少?答案和解析
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. TCP 下载速度为什么这么慢?答案和解析
  14. 请求为什么超时了?答案和解析
  15. 0.01% 的概率超时问题答案和解析
  16. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
🔲 ☆

学习网络的一点经验

在网络抓包系列的最后,再来说一下网络学习的一些经验。毕竟,能通过抓包来解决问题的前提是具备网络协议的知识,不然的话 Wireshark 和 tcpdump 用得再熟练也看不懂抓到的包。

以下是我觉得一些比较好的学习方法,我从中获益良多。

和朋友讨论学到的东西

学到了一个新的知识点或者新的协议,可以尝试解释给朋友或者同事听。比如在吃饭的时候,「嘿,你知道吗?TCP 三次握手的第三个包实际上是可以携带数据的!」然后他们会想这个问题,问你「要是有些 Server 不能处理带数据的第三个包怎么办?」这样,你就要重新思考你学的知识,看它是否考虑周全。教是最好的学习方法。如果没有朋友的话,可以考虑写一个博客(像我一样?呵呵)。

认识实际的网络,也可以从公司的网络结构开始。平时总结一下自己的问题,多阅读一些内部的文档。在内部 traceroute 跑一下,看看每一个 hop 都是什么。找时间请网络团队的同事吃饭,然后问他们你的问题。

举一反三

要学会提问。网络协议的目的一部分是传输数据,更重要的,它需要处理各种异常情况,保证即使出现各种意外,也能尽最大努力工作。所以,我们在学习的时候可以经常问题自己:如果这时候出现 xx 会怎么样呢?

问自己问题,然后思考,尝试给自己解答。在思考的过程中,会产生很多解决方案,大部分可能都是有问题的,然后找到其中的问题,推翻自己的方案。比如,网络为什么要分开三层和二层?这个世界上只有 IP 层,没有链路层行不行?或者只有链路层,不要 IP 层了行不行,整个世界都是一个「大二层」,会有什么问题?

网络相关的知识常常是零碎散乱。我的经验是,不要一开始就跳到兔子洞里面去。面对一个新的概念的时候,可以这样来入门。举个例子说,比如 SOCKS5 协议:

  • 第一步是问:「这个东西是来解决什么问题的?」这个问题必须始终记在心里的,如果搞不清楚,后面会有更多的困惑。
  • 第二步,可以自己想出来一种方案来解决这个问题。方案一开始不必完美,只要解决问题即可。很多方案就是这样设计出来的,现有一个简单的方法,再想想有没有什么缺陷,然后试图优化方案来解决缺陷。
  • 如果觉得自己的方案应该没有问题了,就可以进行第三步了,去深入理解 SOCKS5 协议。可以直接去读 RFC1,不要害怕,RFC 读起来没有那么难!阅读 RFC,可以和自己的方案做对比,看看有哪些问题是还没有想到的。

其实我觉得,网络协议的设计在最开始的时候考虑并没有很周全,比如 TCP 的一些问题,HOL Blocking;比如 OSPF 协议的奇怪名字 NSSA 之类的。感觉是设计出来之后,然后发现在现实中会有问题,就会在给原来的东西打上补丁。

研究网络协议

很多网络协议设计的是很美的,我喜欢那些简单又能解决问题的协议。比如 VRRP,只有一种数据格式包。像 TCP,DHCP 这种就复杂许多,它们的魅力在于解决了复杂的问题,适应各种复杂的环境。

了解网络协议除了按照上面的方法自己来挑战设计,还可以格物致知,看协议的每一个字段,搞清楚这些字段都在解决什么问题。一般每一个字段都是为了解决某种问题或需求而存在的。

面对复杂的协议的时候,把它拆成一个一个小的部分,单独攻破。比如 BGP,状态,选路之类的太复杂了。方法就是 break down,一个一个来。

光学习 Wireshark 这种工具是不行的,工具是围绕网络协议设计出来的,网络协议设计出来是为了解决真实的问题的。所以学习网络协议,它们解决问题的方法,才是主要的。另外,只有懂网络协议才能用好工具。比如,精通 ICMP 和 IP 的 TTL 设计,才能精通 traceroute 的使用。

多读书

像 TCP 的书从协议设计到实现,有数不清的书来介绍。读一本是不够的,因为对于网络协议,不同的作者有不同的理解,有不同的比喻。读书就像和作者聊天一样,和不同的人探讨自己对一个东西的认识,有助于加深理解。

上面通过自己设计协议来理解协议的方式,就是 Computer Networking: A Top-Down Approach2 这本书作者用的,作者用从零开始设计 TCP 的方式来教授为什么 TCP 是今天这个样子的。

同一本书也可以读第二遍,所谓书读百遍,其义自见。

PS:不要看国内的大学讲课视频

如果一个视频明显是那种中国大学的讲课录屏,基本就没有必要看了。这些视频通常知识和现实脱节,讲的内容很多也在现实中见不到,照本宣科,避重就轻。除了磨灭读者的兴趣,把有趣变成枯燥,就没什么价值了。没有兴趣和热情,就学不好。


说到最后,兴趣是最好的老师,对我来说,根据自己的知识和工具拨开迷雾,解决一个一个的问题,是再有意思不过的了。每个人会在人生的一个时间,发现这个世界不是公平的。但是在计算机的世界里,是很公平的,多花一点时间,多思考一些,就会多一分收获,它与天分无关。技术会过时,自己辛苦习得的技能也可能被 AI 轻而易举取代,但是曾经在学习和进步中体会到的乐趣是真实存在的,那些经验运用到新的技术中也多少是有所帮助的。祝愿读者在自己的领域体会到平静和乐趣。


==抓包破案录==

这篇文章是抓包破案录系列文章(之前叫做《计算机网络实用技术》,后来改名了)中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网络抓包与分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

如果您正在阅读的是题目类的文章,这个目录内容正好用来隔离其他读者的评论。读完题目可以稍作暂停,进行思考,继续向下滑动,可能会被其他的读者剧透答案。

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析阅读加餐!
  8. 重新认识 TCP 的握手和挥手答案和解析
  9. 3.5 秒初始延迟问题答案和解析
  10. 网络断断续续……答案和解析
  11. 延迟增加了多少?答案和解析
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. TCP 下载速度为什么这么慢?答案和解析
  14. 请求为什么超时了?答案和解析
  15. 0.01% 的概率超时问题答案和解析
  16. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
  1. 如果你感兴趣的话,socks5 的 RFC 是 https://datatracker.ietf.org/doc/html/rfc1928 ↩
  2. https://www.amazon.sg/Computer-Networking-Top-Down-James-Kurose/dp/0133594149 ↩
🔲 ☆

ARP 问题诊断

这是 网络断断续续 一文的答案。

答案是,在同一个子网内,同一个 IP 地址分配到了 2 个不同的设备上。

对于这种时而正常时而异常的「幽灵问题」,在没有思路的时候,可以通过对比的方法来寻找线索。

有的时候 TCP 能够连通,有的时候无法连通。那么正常的 TCP SYN 包和异常的 TCP 包之间肯定是有什么字段是不一样的。当然,也有可能 SYN 包一模一样,问题出在了其他的网络设备上或者 TCP 的另一端。但是既然题目出给读者了,那么问题的根源肯定是隐藏在抓包文件里面。

通过对比来寻找答案

正常的 TCP 连接可以完成 TCP 的握手和挥手。

异常的 TCP 连接,发出去的 SYN 直接收到了 RST。

通过对比可以发现,两个 SYN 包的 Dst MAC 是不一样的。

通过推理得出答案

如果不通过对比的方法,顺藤摸瓜,可以用下面的思路。

首先,Src IP 和 Dst IP 分别是 10.210.151.187 和 10.210.151.90,很大概率是属于同一个 /24 子网的 IP,不过要想确认的话需要看下服务器的 IP 地址是如何配置的,子网掩码是不是 /24,这点我们从给出的信息无从得知。

假设确实是属于同一个子网,那么 TCP SYN 包不经过网关,直接发给目的地址,二层的 Dst MAC 地址应该是目的 IP 的 MAC 地址。

假设不属于同一个子网,那么 TCP SYN 包经过网关,TCP SYN 包的目的地址应该是路由器的 MAC 地址。

无论是那种情况,正常情况下发送给同一个 Dst IP 的目的 MAC 应该是唯一且稳定的。通过 Conversation 统计可以看出,通讯的 IP 对只有一对,但是 MAC 的确有 2 对,这是不对的。

Statistics > conversation

原因分析

造成这种现象的原因是,IP 分配重复了,在同一个子网内,同一个 IP 地址 10.210.151.90 被分配给了多个机器。

发送 TCP 包之前,需要知道对应的 Dst IP 地址的 Dst MAC 地址,怎么知道呢?Src 机器会在整个子网内广播 ARP 询问,持有这个 IP 的机器会回复 ARP。现在子网内有两个相同的机器是相同的 IP 地址。这两台机器的 IP 地址一样,MAC 地址不一样。如果我们现在 arping 10.210.151.90 ,会得到如下的回复:

$ arping -c 3 10.210.151.90
ARPING 10.210.151.90
42 bytes from 5a:65:98:0c:82:02 (10.210.151.90): index=0 time=944.055 usec
42 bytes from c2:10:70:f2:ae:ab (10.210.151.90): index=1 time=997.915 usec
42 bytes from 5a:65:98:0c:82:02 (10.210.151.90): index=2 time=13.799 usec
42 bytes from c2:10:70:f2:ae:ab (10.210.151.90): index=3 time=109.388 usec
42 bytes from 5a:65:98:0c:82:02 (10.210.151.90): index=4 time=6.670 usec
42 bytes from c2:10:70:f2:ae:ab (10.210.151.90): index=5 time=48.983 usec

--- 10.210.151.90 statistics ---
3 packets transmitted, 6 packets received,   0% unanswered (3 extra)
rtt min/avg/max/std-dev = 0.007/0.353/0.998/0.438 ms

可以看到,我们发出去 3 个询问,却收到 6 个回复。说明每一个询问得到了 2 个答案。(其实,在诊断和验证这个问题的过程中,最快的方式就是用 arping 来测试一下,而不是抓包来分析。但是这里我们主要讨论抓包技术,所以拿来当作一个分析的案例。)

那么 Src 收到两个 ARP 应答,会以哪一个为准呢?答案是以先收到的为准。

$ ip nei show
10.210.151.90 dev h2-eth0 lladdr 5a:65:98:0c:82:02 REACHABLE

但是 ARP 请求广播出去,这两个 ARP 应答哪一个先到是无法确定的,有的时候 5a:65:98:0c:82:02 先到,有的时候 c2:10:70:f2:ae:ab 先到,所以发送给 10.210.151.90 的包,有的时候发给了 MAC 地址为 5a:65:98:0c:82:02 的机器,有的时候发送给了 MAC 地址为 c2:10:70:f2:ae:ab 的机器。

补充一点,在实际的问题排查过程中,我们的 TCP 连接测试失败,最好的排查方法就是去对端进行抓包,看一下对端的机器是否收到了 SYN 包。以及在实际的现象中,客户端收到了 Connection refused ,通常意味着收到了 RST 报文(可能来自真实对端,也可能来自其他设备)。我们在对端抓包的过程中会发现即没有收到 SYN,也没有发出 RST,这说明包到别的地方去了。进而,我们可以发现字网内还有一个「李鬼」的问题。

那么,为什么 ping 的测试完全正常呢?

因为 ICMP 是无连接协议,,ping 的 ICMP 协议回复,是由 Kernel 负责的,所以无论 ICMP 包发送到哪一个机器上,由于它们都设置了这个 IP 地址,所以都可以做出回复。其实抓包中的回复是来自不同的机器,只不过 ping 不知道,只要是收到了回复,就认为一切正常。

可以把 Src MAC 地址添加到 Column 显示,就可以发现其实 ping 的回复来自不同的设备。

最后一个问题,为什么会有 IP 重复的问题呢?

DHCP 可以用来动态分配 IP 地址给设备,但是一般只用于客户端,比如办公网、家用网络中的终端设备,这些设备一般作为连接的发起方,不需要 listen 一个固定的 IP 地址,IP 地址的动态变化对它们影响不大。但是在 IDC 的网络中,每一个服务器的软件都需要固定的 IP 地址,一般不用 DHCP 来动态分配,而是用中心化的 IPAM 系统1追踪 IP 地址的分配情况。如果里面记录的地址不正确,比如,一个地址已经在使用中了,但是并没有在 IPAM 记录,就造成 IP 地址重复的问题。

  1. https://en.wikipedia.org/wiki/IP_address_management ↩

==抓包破案录==

这篇文章是抓包破案录系列文章(之前叫做《计算机网络实用技术》,后来改名了)中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网络抓包与分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

如果您正在阅读的是题目类的文章,这个目录内容正好用来隔离其他读者的评论。读完题目可以稍作暂停,进行思考,继续向下滑动,可能会被其他的读者剧透答案。

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析阅读加餐!
  8. 重新认识 TCP 的握手和挥手答案和解析
  9. 3.5 秒初始延迟问题答案和解析
  10. 网络断断续续……答案和解析
  11. 延迟增加了多少?答案和解析
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. TCP 下载速度为什么这么慢?答案和解析
  14. 请求为什么超时了?答案和解析
  15. 0.01% 的概率超时问题答案和解析
  16. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
🔲 ☆

网络断断续续……

各位读者新年好呀~ 最近比较忙,博客快要长草了。今天我们再来看一个实际的网络问题。

症状如下:

在一次服务扩容的时候,新扩容的虚拟机网络断断续续,它的 TCP 服务时而能连上,时而连不上。新扩容出来的虚拟机的 IP 地址是:10.210.151.90。

$ nc -vz 10.210.151.90 9999
nc: connect to 10.210.151.90 port 9999 (tcp) failed: Connection refused
$ nc -vz 10.210.151.90 9999
nc: connect to 10.210.151.90 port 9999 (tcp) failed: Connection refused
$ nc -vz 10.210.151.90 9999
Connection to 10.210.151.90 9999 port [tcp/*] succeeded!

于是我们联系了虚拟机团队的负责人小马,小马一听说虚拟机的网络有问题,上来就是一顿 ping,结果呢,一个包都没丢。「网络肯定没问题,一定是你们自己服务的问题啦。」小马说。

「可是我们用这台虚拟机部署的服务确实不能用啊!」业务的同事可怜巴巴地说。却也没办法说服小马继续排查网络问题了。小马说:

When a problem is yours, own it. When a problem isn’t yours, prove it.

这时,业务团队里面正好有一个精通网络分析的同事(就是你!),这位网络大神随便找了一台机器,一边去用 ping 和 nc 做 TCP 连接测试,一边 tcpdump,得到下面的抓包文件。

网络大神把这个抓包文件下载到本地,分析起来,不一会儿,就得出了结论……

请分析这个抓包文件,回答:为什么 TCP 连接时而成功时而失败?


==抓包破案录==

这篇文章是抓包破案录系列文章(之前叫做《计算机网络实用技术》,后来改名了)中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网络抓包与分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

如果您正在阅读的是题目类的文章,这个目录内容正好用来隔离其他读者的评论。读完题目可以稍作暂停,进行思考,继续向下滑动,可能会被其他的读者剧透答案。

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析阅读加餐!
  8. 重新认识 TCP 的握手和挥手答案和解析
  9. 3.5 秒初始延迟问题答案和解析
  10. 网络断断续续……答案和解析
  11. 延迟增加了多少?答案和解析
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. TCP 下载速度为什么这么慢?答案和解析
  14. 请求为什么超时了?答案和解析
  15. 0.01% 的概率超时问题答案和解析
  16. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
🔲 ☆

Piccolo P2P 镜像分发

我们遇到的 Harbor 的另外一个问题是 image 的下载瓶颈。在容灾的时候,我们需要在短时间内启动几万个容器。Harbor 这里就成了瓶颈,抛开所有的数据库和文件系统的瓶颈不说,网络这里就需要 Tib 级别的带宽。这是不现实的。

用户构建的 image 质量参差不齐,大于 10GiB 的 image 比比皆是,尽管这些 image 都有很大的优化空间,但是期望所有的用户都按照构建 image 的最佳实践1进行优化,也是不现实的。

于是问题就成了:如何才能够大量的 worker 节点迅速扩容上万的容器?

此外,在平时,遇到工作日多个系统发布的时候,或者整点的时候运行定时任务(定时任务每次都会下载 image),harbor 的压力也非常大,经常满载运行。如果能解决这个问题,平时的负载问题也可以缓解。

之前介绍过 Spegel 的下载方案2,Spegel 的想法很好,基本思想是,先去其他已经存在这个 image 的机器上去下载,如果找不到,再 fallback 到 Harbor 下载。可惜的是,Spegel 把 P2P 下载和 P2P 服务发现混为一谈了,导致服务发现的性能极低。

什么意思呢?去其他的机器上下载 image,需要的一个信息是:哪一个机器有这个 image?这就是下载源的服务发现。这个服务发现和 P2P 本质上没有什么关系,用什么都可以。P2P 技术解决的是下载上的瓶颈,只有能有一个方法记录每一个机器上现在都有什么 image 就可以,用 Etcd 也可以,不一定也得用 P2P 形式的。

但是 Spegel 这里是用 P2P (libp2p)做的服务发现。在我看来这是完全没有必要的。我们在实际的部署中遇到的问题有:

  • 服务发现的 latency 高,现在的配置是 30s,还是会有超时,P2P 本质上是在一个不稳定的分布式网络中寻找一个资源,效率不高;
  • 成功率低,在 P2P 网络里面,能否发现一个 key 是概率性问题,不是确定的3
  • 删除 image 不会分发到 p2p 网络中,必须有访问事件得到 404 然后触发清除,这是 libp2p 本身的设计决定的,这又会导致服务发现的错误率高;

在实际的部署中,Spegel 的缓存命中率在 25% 左右。

当然,使用 P2P 做服务发现也是有好处的,好处就是部署简单,不需要额外的存储依赖。只不过这个好处和它带来的问题相比就微不足道了。

我觉得如果解决服务发现的问题,使用 P2P 下载的方法,是可以解决资源的瓶颈问题的。

所以设计的方案是:使用中心化的,高性能的服务发现,去中心化的 P2P image 下载。

原本 Spegel 的代码,服务发现层是独立的模块,所以我从 fork 它的代码来添加一个新的服务发现方式开始。但是随着修改,发现问题越来越多,比如 subscribe containerd events 的时候没有正确 defer 关闭,没处理好 containerd 重启的情况,等等,最后开始了一个独立的仓库,叫 Piccolo。代码4依然是开源的,但是还没时间写文档和注释,这篇博客先写一下原理。

项目主要分两部分:

Pi – 安装在每台机器上的 daemonset,负责:

  • 作为 containerd 的本地 mirror,当 containerd 需要下载 image 的时候,会先尝试本地的 Pi 端口,如果得到 404 (或者 5xx),再尝试下一个下载 mirror,一般是 dragonfly,最后是 harbor source;
  • 连接本地的 containerd,跟踪本地的 image 状态,本地的 image 增加或者减少,报告给 Piccolo server;
  • 连接本地的 containerd,其他的 Pi 发送来下载 image 请求的时候,上传本地有的 image。

Piccolo Server 是服务发现的源,全局只有一组(几个实例就够用了),提供 3 个接口:

  • Advertise:其他 Pi 上报的 image 列表,存储到 MySQL;
  • Findkey:Pi 来询问一个 image(实际上是 image 的 manifest 和 blob)在哪里有的时候,回复地址列表;
  • Sync:其他 Pi 可以用这个接口做全量同步(定时,以保证 image 总是最终一致的);
Piccolo 的架构图

部署之后,97% 的下载请求可以在 Pi 完成而不必请求 Harbor。

Harbor 的流量对比,绿色的线是逐步发布 Piccolo 的流量,黄色线是之前的日常流量。

从这个监控可以看出,随着 Piccolo 的发布,Harbor 的流量骤降。而且每个小时的峰值流量也几乎没有了。

Pi 部署在每一个机器上,使用的资源也非常少,平均 CPU 用了一个 core 的不到 1%,可以忽略不计。平均内存用了 22M 左右,也可以忽略不计。

Piccolo 为集群提供了大约 8Tib 的下载带宽,没有消耗额外的资源,几乎是免费的 8Tib 带宽。

Piccolo server 方面,性能也很高,一台 8C8G 的 instance 足以支撑 5 万个 Pi (实际的物理机 worker 节点)。秘诀就是注重细节的性能优化。

比如全量同步的资源消耗较大,一起部署的机器会定时发送 keeplive,通过对这些定时执行的 API 加随机偏移,可以保持这些 API 的频率几乎是均匀的,解决了资源的峰值问题。

服务发现的核心,是用一张 MySQL 表,存储了 blob 和 IP 的对应关系。服务发现请求主要是通过这张表的查询完成的。Piccolo 支持把不同的 group(同一个 group 的 Pi 可以互相发现,不同的 group 的 Pi 不可以互相发现。其实这个功能也可以通过部署多个 Piccolo Server 来实现)放到不同的数据库中,加上索引优化(极致的索引优化,所有的查询都是 index-only 的),每一个库 2千万的数据,请求在 200 QPS,耗时在 10ms 以内,已经足够使用了。

Piccolo server 在服务发现接口返回的时候,会根据请求者的 IP 地址,把所有的资源拥有者的 IP,根据和请求者的 IP 相似度(距离)排序,返回。这样 Pi 在下载的时候,会从距离和它最近的邻居开始尝试,这样可以最大程度减少跨网络设备的带宽流量。

所有的 API 接口都有重试和指数时间退让,这样在大规模部署的时候,可以分散一些请求,不至于大家一起失败。

在高可用方面,由于 Piccolo server 是无状态的,所以部署多个实例即可。在预防未知的 bug 上,系统的每一个阶段都是可以降级的:

  • 如果 containerd 从 Pi 下载失败,会 fallback 到下一个下载源;
  • 如果 Pi 从一个 Pi 下载失败,会继续尝试下一个 Pi,直到超时;
  • 如果 Pi 访问 Piccolo 失败,会等待并重试,直到超时;

  1. 这里是之前 blog 过的一些技巧 Docker 镜像构建的一些技巧 ↩
  2. Spegel 镜像分发介绍,也讨论了一些其他的可能方案,比如 dragonfly,或者 lazy loading ↩
  3. https://en.wikipedia.org/wiki/Kademlia ↩
  4. Github 地址:https://github.com/laixintao/piccolo ↩
🔲 ☆

一起看电影

我和欣都很喜欢看电影,我们一起看了很多电影。我最喜欢的导演是韦斯·安得森,现在最喜欢的电影是《布达佩斯大饭店》,看了很多遍。欣最喜欢的电影是《沙丘》,也看了很多遍。科幻电影是我们共同喜欢的类型。

《布达佩斯大饭店》每一帧都很美,故事性也很强,台词简洁但是话又很多。和其他的电影很不一样,后来我才知道,这属于「艺术电影」的范畴。导演的个人特色太鲜明,有一次我在看《腓尼基计划》,欣看了一眼,就问,「这又是那个怪导演拍的?」

《沙丘》作为老牌科幻,背景世界构建的宏大而又符合逻辑,导演 Denis Villeneuve 实力很强,把这么难拍的电影也拍的像艺术一样。每一帧也都很美。

我们在一起看了很多电影。用过各种各样的设备和方法一起看。

高铁从上海到山东需要 5 个小时,看电影是最好的消磨时间的方法了。问题是,一直没有找到比较好的可以两个人一起看电影的方案。即两个人一起看,但是各自用自己的降噪耳机。

在网上找到了一个 Mac 可以使用的方法1,原理是用 Audio MIDI 创建一个新的输出设备,实际输出的物理设备是 2 个耳机,步骤如下:

  1. First, on your Mac, go to System Preferences > Bluetooth. Turn it on and pair the two pair of AirPods with your Mac. Now only one pair of AirPods can be connected.
    Now open Finder and click on Applications > Utilities > Audio MIDI Setup.
  2. Look for the plus sign and click on it to create multi-output device.
  3. Check the box next to the two pairs of AirPods and check the Drift Correction box next to the second pair of AirPods.
  4. Click on System Preference > Sound, followed by Multi-Output Device.
  5. Once done, the audio of your Mac will be sent to both pairs of AirPods. So this how to connect multiple AirPods to Mac.

有的时候不想拿出来 Mac,或者有时候没有带,就用下面这个万能的办法,不过仅适用于提前下载好的电影:

  1. 两个人的各自用个自己的设备(2个 ipad),以及各自的耳机观看;
  2. 调整进度条到同步的位置。

这样也算一起看了。

近几年大部分时间都是在家里一起看,买过 HBO,Amazon Prime,Disney+,Netflix 等等,在电视上看了不少电视和剧,简单也方便,体验很不错。《克拉克森的农场》,《Severance》,《浴血黑帮》,《大西洋帝国》,《For All Mankind》,等等。

去年买了一个新的电视,型号是三星的 S95F,可能是最近几年买过的最满意的电子产品了,OLED 屏幕非常惊艳,这一款还有一个不反光的特性,白天看也不用拉窗帘。最近又发现一个惊艳的功能。

有天家里有人睡觉了,我戴着耳机在客厅里看电视,欣过来问我,你看电视怎么没有声音?我说用的耳机。欣惊讶到,这电视居然能连接蓝牙耳机。我觉得这是很正常的功能。

于是她拿出来自己的耳机要和我一起看,我说不能连接两个耳机。她说可以,这是正常的功能。

我觉得肯定不行,因为之前用的所有的设备在不 hack 的情况下都不支持同时输出到 2 个蓝牙设备(实际上,iOS 在 2019 年好像就支持 audio share 了)。但是决定还是用事实证明。于是我打开蓝牙,连接上她的耳机。预期是之前连接的我的耳机会断开。结果两个耳机都连接成功。

然后打开声音输出,预期是只能选择一个耳机。结果电视自动弹出来对话框,问是否要切换到 multiconnect 功能?选择「是」之后,两个耳机都完美地有了声音。

三星的 dual audio 功能,不光可以输出声音到两个设备,屏幕居然也可以分割成两部分,同时看2个甚至多个节目。电视也支持虚拟化了。

后来一想,这个功能确实挺符合直觉并且实用的。现在的问题是,为什么之前那么多设备反而不支持?

  1. 详细教程:https://www.tenorshare.com/iphone-tips/how-to-connect-two-airpods-to-one-phone.html ↩
🔲 ☆

《征服C指针》

最近读了《征服C指针》,是一本好书。日本人写的计算机技术书籍像娓娓道来的技术博客那样,假设读者没有(或者有很少)相关的背景知识,围绕一个主题,除了介绍干巴巴的知识,还会谈自己的经验和理解,对其他的书和论点做点评,指出一些常见的谬误,这样读者可以更容易理解书里讲的东西。如果只有干巴巴的知识,那么去读维基百科就好了。但是维基百科(无论中文还是英文)的说明一般晦涩难懂,因为百科的首要目标是准确,消除歧义,而不是易于理解。而好的技术书里面会旁征博引,运用举例和比喻,用多的篇幅详细说明难懂的部分,加上作者的经验让读者知道哪些内容是经常使用的,哪些是晦涩又不常用的,哪些是可以辅以技巧理解的。这是我理解的优秀的技术书籍。比如,《流畅的 Python》就是这样一本好书,这本书不光介绍 Python 的编程知识,还有对 Python 语言设计的点评,和其他编程语言的对比,作者自己的理解和评价,读完之后对于整个编程概念的理解都会有提升。

这本书其实就不只是 C 语言的指针,还设计内存分配,C 的语法和编译器,CPU 等知识。从初学者到老手都可以从中有所收获。

另外书中对于有些概念可以给出确定的定义,而不是给出模棱两可的答案。如果永远模棱两可,那么永远都不会错,但是这种内容读起来也是浪费时间。一位得高望重的星际2游戏解说曾经说过:「专业解说要敢于下判断。」

以下是一些笔记。每一段引用都是书中的原文,引用如果不是连续的,就来自于不同的段落。非引用的格式是我的评论。

在C语言中,记录指针指向何种类型是只到编译器为止的,到了运行的时候就已经没有相关信息了。在运行时,指针的值就只是单纯的地址而已。​“要从这个地址里取出哪种类型的值”这一信息只残留在编译器生成的机器码中。无论是在指针的值中,还是在指针指向的变量的内存空间中,都没有类型的信息。因此,如果把指向int的指针转换成了void*,就不可能再知道它原来是指向 int 的了。

指针的类型,主要是为了告诉编译器信息。

int hoge = 5;
void *hoge_p;

hoge_p = &hoge;  <-- 不会报错
printf("%d\n", *hoge_p); /* 输出hoge_p所指向的内容 */

会报错如下:

warning: dereferencing `void *' pointer
error: invalid use of void expression

只告知了内存上的地址,却没有告知那里保存的是什么类型的数据,当然无法读取。

改成下面这样可以运行:

5: printf("%d\n", *(int*)hoge_p); /* 将hoge_p转换成int */

这正是指针运算的特征。在 C 语言中,对指针加1 后,其地址就增加该指针所指向的类型的长度。示例程序中的 hoge_p 是指向 int 的指针,而在我的环境中 int 的长度是 4,所以对地址来说,加 1 就是前进 4 字节,加 3 就是前进 12 字节。

对指针+1,地址移动的单位是「指针所指向的类型的长度」,这个也是编译器计算的,这是指针需要类型的主要原因。

由于空指针可以确保与任何非空指针进行比较都不相等,所以经常作为返回指针的函数发生异常时的返回值使用。

例如我工作的地方位于日本名古屋市某栋大楼的 5 楼,某人爬一层楼需要 10秒,那么从地面上到 5 楼需要花费多少秒?50 秒?很遗憾,正确答案是 40秒。想必大家在中学都学过等差数列,等差数列的第 n 项等于“首项 + 公差× (n –1)”​。每个都要减 1,真麻烦……此外,​“1900 年代”并不是 19 世纪,它的一大半属于 20 世纪。更加复杂的情况是,2000 年不属于 21 世纪,而属于 20 世纪。这些问题分别可通过以下方式回避。把大楼里与地面等高的那层计作第 0 层把数列的首项计作第 0 项把最初的世纪计作 0 世纪,把公历最初的年份计作 0 年这种“差 1 错误”的问题在编程中经常发生。因此,普遍认为在一般情况下如果以 0 为基准编号,那么通常(并不是所有)能回避这类问题。

延伸阅读:为什么要“包含头不包含尾”?

要点

p[i] 是 *(p + i) 的简便写法。

下标运算符 [​] 只有它原本的意义,与数组毫无关系。

要点

【比上面的要点更重要的要点】

但是,千万别写成那样。

在 get_word() 中使用下标运算符访问 buf 的内容,会让人觉得从 main() 传递过来的就是buf 数组。然而,这是个错觉,从 main() 传递过来的说到底只是指向 buf 的初始元素的指针。

函数传递只能传递指针而不能传递数组。即使数组和指针不同,但是在函数传递的时候,数组也会转换为指针,指向数组开头的元素。

在 C 语言中,参数全部都是通过值传递的。

即便是像全局变量那样在函数外部定义的变量,一旦加上 static,其作用域就只限定在当前源文件内。指定为 static 的变量(函数)对于其他源文件是不可见的(函数也是一样的)​。

另外,对于函数(非 static 限定)和全局变量,只要名称相同,即便位于不同的源文件中,也会被当作相同的对象处理。

因此,根据操作系统及CPU的不同,需要规定不同的调用方法,这就叫作调用约定(calling convention)​。本书中说明的调用方法是在x86系列处理器中被称为cdecl的调用约定。该方法中所有的参数都通过压栈的方式进行传递。

malloc() 会遍历链表,搜寻空块,若该块大小足够,就将其分割出来,做成使用中的块,并向应用程序返回紧邻管理区域的下一个地址。free() 会改写管理区域的标志,将该块置为空块,如果上下有空块,就顺便将它们合并成一个块。这是为了防止块碎片化。 这种操作称为 coalescing。当没有足够大的空块满足 malloc() 的要求时,就向操作系统请求(在 UNIX 中需要通过 brk()系统调用)扩充内存空间。

补充 

Valgrind正如前面多次提到的那样,与动态内存分配相关的Bug往往出现在距离它被发现的位置很远的地方,因此调试非常困难。在Linux上可以使用Valgrind工具追踪这类Bug。Valgrind工具用于检测对malloc()分配的内存空间越界读写、忘记free()(内存泄漏)或者对同一块内存空间多次free()这类问题。

如果运气好,标准库 glibc 也可以为我们检测出这个问题。

调用 malloc() 之后必定写上相应的free() 是一种谨慎的编程风格。程序员就应该小心翼翼地将 malloc()和 free() 对应起来。“因为调用了 exit(),所以就没必要free() 了”的想法是不负责任的偷工减料行为,是不良的编程风格。不管怎么说,程序员也是人,人就是这么一种在可能犯错的地方必定会犯错的生物。可是,​“必须 free() 派”却偏要大肆宣扬无论如何都要“谨慎地”编码,这种论调其实是于事无补的。

我认为,​“谨慎地”编码并没有什么了不起的,那些能够尽可能地回避“麻烦事”的人才是优秀的程序员。在我心中,理想的程序员是下面这样的:在能够安全地偷懒的地方尽可能地偷懒,并且尽可能地依靠工具而不是肉眼来进行检查,但在无论如何都需要人工处理麻烦的事情时,会在心中坚定地起誓“总有一天要将它自动化”​。

这是我最喜欢的一段话。Python 的初学者写的代码,会在所有的函数入口都写上 try-catch,称之为防御性编程。我觉得这么做的人肯定会有这样的疑惑:「怎么这么麻烦?」对于这个疑惑,有两类人,一类是认为「这么做一定有道理,作为程序员我们要不辞劳苦地做好工作。」另一类人认为「一定有更方便的方式」。

正如图 2-17 所示,填充有时会被放到结构体的末尾。因为在创建结构体数组时,填充是必要的。在将 sizeof 运算符应用到这样的结构体上时,返回的是包含末尾填充部分大小的长度。将结果和元素个数相乘,就可以获取数组整体的长度。

——有关结构体的对齐

小端与大端到底哪一种更好呢?这个话题经常引起人们的争论,此处就不再深入讨论了。它们各有各的优点。人类在用纸和笔做加法时也会从低位开始相加,所以对 CPU 来说,或许采用小端的方式更轻松一些,而在人类看来,大端的方式或许更容易理解。

Little Endian vs Big Endian

C 语言的声明要用英语阅读

我们可以遵循以下步骤解释 C 语言声明。

先看标识符(变量名或函数名)​。

从贴近标识符的地方开始,按照如下优先级解释派生类型(指针、数组、函数)​:

①用于整合声明的括号;

②表示数组的 [​]、表示函数的 ();

③表示指针的 *。

完成对派生类型的解释之后,通过 of、to 或returning 连接句子。添加类型修饰符(位于左侧,比如 int、double)​。如果不擅长英语,可以用中文解释。

能正确地阅读 C 指针的声明,函数参数中有关指针的声明,已经 sizeof 中的声明,是读此书最大的收获了。

像这样可以确定长度的类型,在标准中被称为对象类型(object type)​。然而,函数类型不是对象类型。C 语言中不存在函数类型的变量,因而我们无法(也没必要)确定其长度。我们说过,数组类型是由若干个派生源类型排列而成的类型。因此,数组类型的总长度为:

派生源类型的长度×数组的元素个数

但是,由于函数类型的长度无法确定,所以也就无法从函数类型派生出数组类型。也就是说,无法创造出“函数的数组”这种类型。但是,可以生成“指向函数的指针”这一类型。只是指向函数类型的指针是不能进行指针运算的,因为我们无法确定指针指向的类型的长度。

当表达式代表的是某处的存储空间时,该表达式就称为左值。与此相对,当表达式仅代表值时,该表达式称为右值。表达式中有时存在左值,有时不存在左值。例如,变量名是左值,而 5 这样的常量、1 +hoge 这样使用运算符的表达式就不是左值。

当作为 sizeof 运算符的操作数时

在以“sizeof 表达式”的形式使用 sizeof 运算符时,由于这里的操作数是表达式,所以即使是对数组使用 sizeof,数组也会被解读为指针,从而只能获取指针的长度——或许有人是这样认为的,但其实在数组作为 sizeof 运算符的操作数的情况下,将数组解读为指针这一规则是无效的,在这种情况下返回的是数组整体的长度。

总之,关于指向函数的指针的 C 语言的语法是比较混乱的。造成这种混乱的罪魁祸首就是“函数在表达式中会被解读为指向函数的指针”这一意图不明(难不成是为了与数组保持一致?​)的规则。

在表达式中,数组会被解读为指向该数组初始元素的指针,因此代码可以写成下面这样。

int *p;
int array[10];
p = array; <-- 将指向array[0]的指针赋给p

但是,反过来写成下面这样就不行。array = p;

数组和指针截然不同。

“不要误会我对 goto 语句持有任何教条主义的执念。我只是担忧,很多人把这件事给神化了,甚至认为仅凭某个编程技巧或某个简单的编程原则,就能解决编程语言的概念问题!”

🔲 ☆

我的姥姥

我的姥姥今年去世了,没有痛苦。我们之前都预料到了这一天的到来,大家在五月份都陆续去看望了她,大家也都知道这是告别。

小姨十年前告诉我,姥姥诊断出了肺癌。但是年纪太大,家里商量之后,决定不治疗,让她安享晚年,想吃什么就吃什么,想做什么就做什么。姥姥晚年喜欢抽烟,吃肉。每次去看望姥姥,我都带两条烟,十斤肉,或者红包。

这几年每一次去看姥姥,她下炕的次数越来越少,头发越来越花白,和她说话的声音需要越来越大。

姥姥的去世,我没有为她感到伤心,只为我以后再也见不到她了而伤心。至少她的晚年可以抽烟吃肉,没有把时间都花在了 ICU 里面。《长命百岁》里面提到, 现代医学专注于延长人类的寿命,但是这些寿命的质量也同样重要。人固有一死,我希望自己离开世界的时候也可以这样。

十年之后,姨们过年聚在一起,终于觉得当初的诊断不对,应该不是肺癌。但是也没有必要再去纠结了,管它是什么吧。

姥姥裹小脚,有六个女儿,一个儿子,重男轻女,我上高中的时候,过年的压岁钱姥姥就只给我和哥哥了,姐姐和妹妹没有。到了晚年,姥姥和姥爷俩人依然住在那诸城县边上的一个村子里,在那里生活了近一个世纪。姥姥和姥爷的感情是很多人都羡慕的。

姥姥的一生几乎都住在那几间房子里,没有去过太远的地方,可能没有出过省,没有吃过什么山珍海味。但也许她的一生是幸福的。这又让我思考起来那个这一年我不断思考的问题:「怎么度过这一生才是值得的呢?」

一路走好,姥姥。

🔲 ☆

2025

在新加坡的第五年整。

今年和欣好像一直在找房子。之前住在湖畔的一个 HDB,房东决定要卖房子,所以我们不续租了,从湖畔的 HDB 搬到了碧山的 Condo。住了不到一年,年底又搬了一次家。第一次住完全没有家具的房子,又花了时间去买沙发,床,桌子,组装家具,搬家,年底要处理的事情太多太多。

买了一台大电视,三星的 S95F,被惊艳到了,OLED 屏幕,还有不反光的特性,效果非常好。于是年底的假期大部分时间都和欣窝在沙发上看电视,买了 HBO,把过去看过的很多电影又看了一遍。把《权力的游戏》也从头开始看了一遍。

今年玩的游戏不多,为了准备玩《GTA 6》买了一台电脑放在客厅,但是这游戏居然又跳票了。缝合怪《潜水员戴夫》拿到了完美通关,《荒野大镖客2》玩到了 50% 左右,然后和欣一起玩《双人成行》,还没有通关。

又买了一台电子书设备和微信读书会员,看了莫言的《檀香刑》,《酒国》以及一些杂文集,一些余华和刘震云的小说,以及其他一些专业书。

学会了一个新的技能:双拼。不过感觉刚刚够得到之前全拼的速度,再用一段时间速度应该更快。

旅行。年前我和欣带爸妈来新加坡和普吉岛旅行。普吉岛去过 3 次了,但是一次博客都没有写过,拖着拖着就有一些原因不想写了。9 月份去上海「旅行」了一趟。在上海生活了这么多年,其实并没有作为游客去过一些地方,这次回去,体验了脱口秀(笑得肚子疼),逛了南京路(和当初上学的时候很不一样了,傣妹居然还活着,其他的店大部分都换了),看了一个剧。年底准备了关西的旅行,但是因为其他事情取消了。

工作方面

今年工作上的难度越来越高。我比较擅长用技术来解决问题,但是今年公司的管理风格向重视流程转移,试图通过流程上的规范来提高整体的可用性,导致审批越来越多,流程越来越复杂,一个简单的 API 调用需要花费之前数倍的成本来实现。

另一方面,在金融方面的发展带来了更多的合规要求,从而带来了巨量的运维工作。可高层在「降本增效」方面的努力没有停止的迹象,仅仅靠运维工作是没有「绩效」的,如果要拿到比较好的绩效以及晋升,就需要拿出时间去做亮点项目。这就造成了另一个矛盾点,也造成了另一部分人的离职。身边的同事越来越少。

在《凤凰项目》一书中,安全部门的主管发现项目不需要安全团队就可以通过一项审计,意识到自己的工作可有可无,在酒吧喝得酩酊大醉。他问运维部门的主管:「我们就真的对你们一点帮助也没有吗?」运维主管尽管很想安慰他,但是又不想说谎,只好说:「对不起,一点也没有。」

每当安全团队要我们把一个毫无敏感信息的 API 用最高的安全等级来要求的时候,我就会想起来这一段。

尽管如此,今年还是做了不少值得骄傲的事情:

  • 接手 Harbor 之后完成了 GC 的自动化,解决了数年之久的一个痛点1
  • 实现了镜像的 P2P 下载,彻底解决了镜像下载的瓶颈问题,之后写博客介绍一下技术细节吧;
  • 新开始了一个内部的标准化项目;

在产品方面,不再负责 Service Mesh 项目了。我从加入公司就开始维护这个项目,经历了几十个版本,也经历过它造成的(我加入之后)公司最大的故障,经历了几代开发团队变迁。开发团队的同事技术方面无可挑剔,产品在近几年没有出过重大事故,也支持了业务的需求。不足在产品的易用性上,配置过于复杂,难以理解,学习成本高。整体架构需要改变,现在是 daemonset 部署模式,难以实现资源隔离和审计。

产品运维的工作中心转到了 SDN 上面,也是我感兴趣的方面。SDN 也有很多问题:易用性和控制面的可用性太差;组件太多过于复杂;需要硬件兼容软件而不是软件兼容硬件,等等。这些都是需要解决的问题。

明年的工作会尝试一下结合 eBPF 来做架构上的可观测性,通过图数据库自动整理软件的依赖。

就写这么多吧,明年还是继续在网络的领域耕耘,学习和分享更多的知识。

  1. Harbor GC 问题 ↩

其他的年终总结列表:

  1. 2013年
  2. 2014年
  3. 2015年
  4. 2016年
  5. 2017年
  6. 2018年
  7. 2019年
  8. 2020年
  9. 2021年
  10. 2022年
  11. 2023年
  12. 2024年
🔲 ☆

AoC 2025 通关留念

[剧透警告]

今年又来玩了 Advant of Code1,最近看 C 语言的代码比较多,就用尽量用 C 语言做。

第 9 和和第 10 天最难,尤其是第 10 天,看着挺简单,实际是从来没有听说过的整数线性规划问题2

第 11 天一看要用 dict,放弃 C 了。用 Python 很快就过了,一个搜索加路径缓存即可。但是如何用 C 写出来完全没有思路,最后问 chatGPT,想起来还有邻接表这种巧妙的表示方法。

第 12 天最搞笑,以前是做 part1 顺利,做 part2 的时候时间爆炸。day 12 做 sample 的时候就感觉时间要爆炸了。加了几个剪枝,part1 居然过了。我想完了,part2 肯定是让我找所有的可能的情况,一定会爆炸。结果点开 part——就通关了。今年居然只有 12 天?那距离圣诞节还有 13 天呢,我做啥?

  1. https://adventofcode.com/ ↩
  2. 高铭骏 写的:整数线性规划 ↩

fuglede 的代码太优雅了:https://github.com/fuglede/adventofcode/tree/master/2025,不愧是做量化的。

🔲 ☆

如何把网络设备从 traceroute 中隐藏

在和朋友一起吃饭的时候,A 提了一个有意思的问题:怎样可以把一个机房内的路由设备从互联网「隐藏」呢?

「隐藏就是没有人可以知道这个设备的 IP 地址,这还不简单,只要禁用 ICMP 就可以了」, B说。

A 说,这样是可以。很多安全团队在实施起来也确实是这么做的,但是这样并不好:机房内所有的 IP 都无法 ping 通了。这样会增加 debug 的难度,得不偿失呀!

C 说,那就依然转发 ICMP 包,但是如果是 TTL=1 的包,就不要回复 ICMP Time Exceeded 了。

B 说,人家要的是「隐藏」,要是像你说的这么做,别人还是知道中间有一个设备的存在,没有完全符合要求。

一个 traceroute 的例子:第7跳直接丢弃 TTL=1 的包,不返回错误1

事实是这样的。假设一个简单的物理拓扑是 A -> B -> C,B 不回复 ICMP Time Exceeded,那么 traceroute 看起来就是 A ? C,可以猜测得到中间有一个路由器,但是已经禁止回复 ICMP Time Exceeded。看起来像下面这样。

C 说,traceroute 的原理是发送 TTL=1, 2, 3, … 的包,不断让路由器回复 ICMP Time Exceeded 信息,来得到每一跳的 IP 地址。要想完全隐藏,只需要:

  • 自己不回复 ICMP Time Exceeded
  • 让下一跳回复,仿佛下一跳就在自己的位置;

这样就可以完全隐藏了。要达到这个目的,只需要:

  • 对于 TTL=1 的包,不是丢弃,而是转发给下一跳,并且 TTL 依然保持为 1,即可。A ---[TTL=1]---> B ---[TTL=1]---> C, 对于客户端的 traceroute,看起来就像:A → C。

这样(理论上)好像确实可行了。三人对这个结论满意了。

后来我把这个讨论记录在了博客上(你现在正在阅读的一个),一位读者马上就发现了问题:可是这样 C 会出现两次吧!

确实是这样,假设在 A -> B -> C 的链路中:

  • TTL = 1 从 A 进入的时候,A 会在 ICMP 中回复自己的 IP;
  • TTL = 2 从 A 进入的时候,B 会直接转发给 C,C 会在 ICMP 中回复自己的 IP;
  • TTL = 3 从 A 进入的时候,B 会 TTL -1 转发给 C,C 会在 ICMP 中回复自己的 IP;

这样 C 就出现了 2 次!

看来,B 必须完全不减 TTL,直接转发,才能隐藏自己。不过这样就有出现环路2的风险了。

  1. 使用 mtr 检查网络问题,以及注意事项 ↩
  2. 网络中的环路和防环技术 ↩
🔲 ☆

博客维护:升级到 Ubuntu 24.04

这个博客在四年前做了一次迁移1,目前是架设在一台 DigitalOcean 的 VPS 上,前面用 Cloudflare 作 CDN,得益于我选择的这两家公司非常靠谱(但是最近 Cloudflare 的事故2 3让我这个 CF 吹有一些尴尬),自从搭建起来之后,我几乎没有维护过。

最近后台一直提示我 PHP7.4 EOL 了,今天终于打起精神来决定升级一把。

一不做而不休,干脆直接把 4 年半前启动的这台 VPS 全升了吧:

  • Ubuntu 升级到 24.04;
  • Mysql 升级到 8.0;
  • PHP 升级到 8.3;

因为我是专业的 SRE,所以这次升级读者感受不到任何区别。

整体比较顺利,唯一遇到的问题是,我用的 wordpress theme 太老了,一样的代码居然在 PHP8.3 挂了。看提示是函数参数少传了,在 PHP7.4 是 Warning,在 PHP8.4 是直接 Fatal。

本来打算放弃治疗,直接用一个新的 wordpress 官方主题得了,省心。结果一个二〇二五这些主题,都是什么玩意,行距看着都难受。又回来决定修好主题的代码。得亏 ChatGPT,没想到意外地顺利,很快就跑起来了,目前也没发现什么问题。

安全方面上顺便做了一个加强,以前的架构是:域名解析到 Cloudflare,Cloudflare proxy 到我的 Nginx,Nginx 只接受 Cloudflare 的 IP4,其他的一概拒绝。自己以为很安全了,没有人知道我的真实 IP。然后自己一查,居然早已经暴露了。

censys 的查询结果

我也不知道什么时候暴露的。

这次直接用了 cloudflared,原理是,我的服务器 Nginx 只 listen localhost 的端口,我的服务器安装一个 cloudflared,cloudflared 会去主动连接 cloudflare,这样,在 cloudflare 收到请求的时候,会通过 –> cloudflared –> nginx 转发到我的机器上。有点像 FRP5 穿透。如此一来,我的 IP 完全没有暴露在公网上了。过段时间再去搜索一下,看暴露了没有。

欢迎读者留言 ; D

  1. 博客迁移到 Cloudflare ↩
  2. Cloudflare outage on November 18, 2025 ↩
  3. Cloudflare outage on December 5, 2025 ↩
  4. Cloudflare 的 IP range:https://www.cloudflare.com/ips/ ↩
  5. https://github.com/fatedier/frp ↩
🔲 ☆

Harbor GC 问题

最近的工作比较忙,以至于网络技术的系列文章1许久不更新了。这几天在解决的问题是镜像存储服务 Harbor2,存储的 docker image 太多了。

虽然我之前在博客里面分享了一些 Docker image 构建的技巧3,以及炫耀了构建一个最小的 Redis Docker 镜像才不到 2MiB4,但是无奈,我的博客基本没有人看,所以同事上传的 image 都非常可怕,动辄就上 G,20+ GiB 的都有。现在我们的 Harbor 存储已经是 PiB 级别了。

多余的 image 就删除就好了,问题就在于,删除 image 比较复杂。分成几个步骤:

  1. 删除 image 的 Tag5
  2. 扫描整个数据库,找到没有被任何其他 image 和 tag 引用的 blob;
  3. 删除这些未被引用的 blob;

第二步尤其重要,简单来说,image 是分层的,一层就是一个 blob,一个 image 可以引用多个 blob。比如服务 A 的 Dockerfile 开头是 From: ubuntu:24.04,另一个服务 B 的 Dockerfile 开头也是 From: ubuntu:24.04,那么这两个 image 都是引用了 ubuntu 的 blob。删除服务 A 的 image 的时候,不能把 A 的 blob 都删除,因为这样的话 ubuntu 的 base image 就连带被删除了。所以我们在删除一个 image 的时候,其实并没有释放任何空间,而只是删除了 image 对 blob 的引用。这时候还不知道哪些 blob 是可以释放的,要知道哪些 blob 可以删除,就必须扫描全部的数据库,找到没有任何引用的 blob,才可以删除。难题就在扫全表这里。

这个问题就和编程语言的 GC 问题很像,不过更加简单一些,因为引用只存在于 tag 到 blob,tag 之间和 blob 之间不存在引用,也就没有环的问题。

引用计数

引用计数比较合适这个场景,因为没有环路,所以引用计数到 0 就可以直接删除,不需要扫表找孤零零的环。但是 Harbor 本身没有用这种方案,估计是因为引用记录维护起来比较难,必须准确并且处理好并发,处理不当很容易有数据误删或者出现永久的垃圾。

Mark and Sweep

这是官方的代码采用的方案,基本思路是,扫描所有的 image,对它们引用的 blob 标记为在使用中。扫描完成之后,所有从未被标记过的 blob 直接删除。

问题

如果直接用 Harbor 的 GC 方案,那么运行一次 GC 需要超过一个月的运行时间(不知道具体需要多久,因为从来没有成功跑完过)。之前的负责人设计了一个很聪明的方案,基本思路是,找到系统性能低的瓶颈,然后针对性地处理这些瓶颈。

对于前面的 3 个步骤:

  1. 删除 image 的 Tag:直接用 SQL 从数据库查询出来 image,判断是否需要保留(规则是每一个 image 只保留最近的 3 个版本),如果不需要保留,通过 API 删除;
  2. 扫描整个数据库,找到没有被任何其他 image 和 tag 引用的 blob:这一步因为是 Harbor 代码的 GC 逻辑,比较负载,还是通过 web UI 来触发的;
  3. 删除这些未被引用的 blob:Harbor 本身 sweep 的过程很慢,原因是没有并发,一个一个删除的,改进是直接通过并发删除。

这样,整体运行一次只需要一个月。

目前还是存在很多问题。我接受之后又做了一些改进:

  1. 之前的 PIC 显然是一个脚本大师,所有的工作都是通过 bash,awk,curl 这些工具完成的,每一步都需要人工操作 -> 等待完成 -> 人工操作下一步,比如到 mark and sweep 的这一步,需要人工去页面上触发 GC,然后关注执行的进度,在执行到 sweep 阶段的时候手动结束,开始运行下一步的脚本;我写了一个 300 多行的 Python 脚本,把所有的步骤串起来,这样就有了 crontab 定期执行的条件。
  2. 在第一步删除 image 的时候还是很慢,30s 只能删除一个 image,我们有千万个 image。解决办法是读了 harbor 的代码,发现 blobMgr.CleanupAssociationsForProject 这一步其实是最费时间且多余的,后面执行 GC mark 的时候一定会运行一遍。删除这个逻辑之后只需要 0.1s 就可以删除一个 image;
  3. 最后一步通过 API 删除 S3 上的数据,之前还是脚本用 curl 触发,速度太慢。使用 Python 之后就可以用 connection pool 并发删除了;
  4. 还做了其他的功能,比如支持不同的 project 自定义删除逻辑,「删除最近1年没有 pull 记录的 image」这种。

本质上是用最少的改动自动化原来的 GC 逻辑,目前运行一次的时间是 3 天。已经足够满足需求了,因为不需要人工执行,所以 3天和 3 个小时区别不大。

上一个负责人留下的文档详细记录的 Harbor GC 的逻辑以及改进点,比 Harbor 官方的文档还要详细。有了这些我半重写 GC 的逻辑就简单很多。

在他之前,是另一个负责 Harbor 的同事。阅读代码并找到瓶颈是需要很大的勇气的,且不一定行得通,可能花了很大的力气,最后发现这个事情做起来就只能这么慢。

但是问题还是要解决。所以他那时用了另一个有意思的方案:

  1. 搭建另一套一模一样的 Harbor 集群,复制以前的用户名,权限,project 等数据,但是把 blob 和 image 数据删除;
  2. 搭建一套 Nginx 代理,Nginx 转发逻辑是:
    • 对于 push,转发到新集群;
    • 对于 pull,先 pull 新集群,如果得到 404,就转发到老的集群,这样以前的数据都可以读;
  3. 在 1年之后,完全删除老的集群;

这是一个很有意思的「用运维手段解决技术问题」的例子,在 SRE 的工作中,迫于没有对软件的实现的控制力,我们经常需要用运维手段来解决代码实现上的问题。

  1. 计算机网络实用技术 ↩
  2. https://goharbor.io/ ↩
  3. Docker 镜像构建的一些技巧 ↩
  4. Build 一个最小的 Redis Docker Image ↩
  5. https://docs.docker.com/reference/cli/docker/image/tag/ ↩
🔲 ☆

大巴

我上初中的时候,村里去县城唯一的公共交通就是一辆大巴车。「大」也说不上,应该叫中巴车。车是邻村一个叫王文义的人买的。他把车停在我们村,每天早上骑摩托车从自己的村子跑到我们村,然后以我们村为起点,一路接上邻村的人,去县城的停车场。从我们村走,可以拉更多的人。走到王文义的村子,顺路接上王文义的老婆,王文义的老婆在车上卖票,王文义开车,一张票 7 块,过年的时候一张票 10 块。

我 12 岁的时候上初中。我们村的学生大部分都去镇上的中学读,我爸重视教育,送我到县城读。每天就是坐这个中巴车去学校,在学校住 12 天,每两个星期回家一次。刚开始每天都想家,住在学校里很不习惯,12 个人一间宿舍,没有办法洗澡,宿舍臭烘烘的。周五下午离开学校,坐在车上,是心情最好的时候,因为从现在开始距离学校越来越远了。周日的下午在车上,是心情最差的时候,在家收拾好东西,上了车,就没有退路了,只有一条去学校的路。

读高中的学生也是坐这辆车,需要去县城的村民也是坐这辆车,所以每周日下午格外人多。座位上全部坐满,车的走道也是挤满了人, 挤得满满当当,超载了两倍还多。

车比较破,车内的地板是一张铁皮,有的地方还破了小洞。有一次我坐在破洞的旁边,怀着郁闷的心情,透过洞看路上的小石子向后飞去。奔驰的客车,我和路面只有一张铁皮之隔。

后来王文义换了一辆新的宇通牌客车,涂着崭新的绿色油漆,座位也没有污渍,比以前也大了,可以叫做「大巴车」了。

高中生是周日中午回学校,初中生是周一早上。冬天天短,起个大早去坐车的只有我一个初中生。有一个冬天的早晨,格外的冷,我去车站的时候还伸手不见五指。我到车站,等了一会,王文义骑着摩托车来了。还不到发车时间,他打开门让我上车等,然后去发动车子。却发现油箱被冻住了,车发动不起来。他让我在上面坐着,我从玻璃看到他从附近的人家门外扯了一把干草,点了火塞在油箱下面烤,也顺便点了一只烟。看到这我就不淡定了,赶紧下了车,站在他旁边,看着火焰在车底燃烧,想象着发生巨大爆炸,把这几吨重的铁皮炸到天上的情形。过了一会,王文义的烟抽完了,回到驾驶室尝试发动,结果还真发动着了。他下车灭了火,就起步去县城了,车上还是只有我一个人。

3 年之后,我开始读高中,还是每两个周坐他的车上学,回家。生活还是一样的麻木,每天学习超过18个小时。最喜欢周六的中午坐车回家,最讨厌周日下午坐在回县城的车上。每两周的休息时间只有不到一天。

后来我去读大学,就再也没有坐过他的车了。每次回家我坐飞机去机场,然后坐芳姐的车去滨海酒店。父亲会去滨海酒店等我,开车带我回家。

芳姐是我哥介绍给我的,任何时候需要去坐飞机,或者从机场回家,只需要在微信上和芳姐说一声,芳姐安排一辆车点对点送到机场,除了我还会顺路接上其他需要去机场的人,一个人 60,7 座车跑一趟可以赚 360。 这些人常年跑机场,从来没有误过飞机。

只是过年的时候人流量大,活也多,一次司机和我说他三天只睡了6个小时,让我听了有些害怕。

大学快毕业的时候,村里就通了公交车,去县城价格更便宜了,但是为了连接更多的村庄,公交车也绕了,原来需要 40 分钟,现在要至少一个半小时。王文义的车也再也没有出现在我们村里。

🔲 ☆

0.01% 的概率超时问题

前言:这个系列(以及我的博客)好久不更新了,原因有两个,一个是我在学习用双拼打字,手跟不上脑子,写的东西读起来不顺畅,不过现在已经复健了。双拼确实能极大地减少按键次数,在 AI 的时代,每个人需要和 AI 对话,那么怎么赶上时代的潮流,从芸芸众人脱颖而出呢?我的建议是:练习打字,打字打得快,和 AI 沟通效率高,做 AI 时代的佼佼者;第二个原因是我最近在思考人生的意义。上次录制博客 laike9m 提到了存在主义危机,第一次知道这个词,我觉得我就是陷入了存在主义危机。苦苦思索人生的意义,没有思考出什么结果。看了一些书,看的是莫言,刘震云,看了一本漫画,《我以为这辈子完蛋了- [美]艾莉·布罗什》,让我思考了很多。但是依然没有结论。人生没有思考明白,问题先来了。

有一天,我们在上线新的设备,上线之后,用户反馈他们的服务出现了网络超时的错误。超时的概率大概在 0.01%,并且出现的时间和我们上线新的设备的时间完全一致。我们把新上线的设备隔离(不再处理线上流量)用户的服务没有再出现错误了。

我们对新设备的性能非常有信心,不应该比原来的设备转发速度还低。这中间一定是有什么问题。

拓扑图简化如下:

拓扑图

其中,用户的 Client 和 Server 侧之间的网络是无法连通的,我们的网络设备会把用户的 Ethernet 包封装到 UDP 里面发送(overlay,原理就和 VPN 一样),这个设备提供了封装,转发的服务。但是用户的 Client 和 Server 感受不到中间这个 tunnel 的存在,Client 和 Server 之间的 IP 地址是可以直接 ping 通的,TCP 也是可以连通的,全靠我们的设备在中间做了转发。

我们做了一些常规检查没有发现问题,然后重新上线新的设备,要求用户在 Server 端进行抓包。得到文件如下。在一般的问题分析中,我们一般只看 packet 的 header 就够了,不需要看 application 层的 TCP payload,所以在抓包的时候我们会截断 TCP 的 payload,这样,在下载抓包文件和交流的时候,更方便一些,并不影响问题的分析。

请据此分析,造成小概率超时的问题在哪里。

如果没有头绪,请看下面的提示。

在找不到问题的时候,我们会对比正常情况下的表现,通过正常和异常的情况的不同来寻找线索。以下是原有环境的抓包文件,没有超时的请求。

对比两个抓包问题,请分析问题的根因。

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析阅读加餐!
  8. 延迟增加了多少?答案和解析
  9. 压测的时候 QPS 为什么上不去?答案和解析
  10. 重新认识 TCP 的握手和挥手答案和解析
  11. TCP 下载速度为什么这么慢?答案和解析
  12. 请求为什么超时了?答案和解析
  13. 0.01% 的概率超时问题答案和解析
  14. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
🔲 ☆

Burn out 逃生指南

在同一家公司工作两年以上,有很大概率会 burn out(意思就是精疲力尽,俺不中了)。如果岗位又是 SRE,那么 burn out 时刻几乎是必然。

为什么会这样?一个是因为工作的时间越长,做的东西就越多,维护的东西也越多,维护的工作就越多,然而新的项目还是要做,就会忙不过来了。加上在大公司分工明确,没有人关心甲方的死活,所以你依赖的库时常会有不兼容更新,依赖的组件经常因为组织结构调整而下线,依赖的 IDC 也会下线,安全团队时不时也会找过来让做一些安全方面的加固。总有一天,会发现自己的 todo list 里面放满了待迁移的事项,自己的用户天天来问一些相同的问题,老板有新的想法需要马上实现。每天下了班都在想着工作,每天都不想上班。这个时候,你就知道这是 burn out 了。

作为一个资深的 SRE,我这里有两条靠谱的路可以逃生。

第一条:每两年换一次工作。

很显然,这样的话,上面这些工作就不会积累下来压死骆驼了。但是如果不想工作三十年打 15 份工的话,就需要一些技巧了。

建议一:提高工作效率,而不是工作时间

之所以放在第一条,是因为这是最重要的一条建议,也是常常被我们忽略的一条。

工作总量 = 工作效率 x 时间

在工作量大的时候,自然而然想到的是延长工作时间,这是非常不可取的,工作时间应该固定在每天 8小时,一周5天,不能再增加了。尤其是 SRE,工作时间越长,出错的概率越大,出了事故就得去救火,review incidents,提出改进措施,实施改进措施,带来更多的工作。此外,如果延长工作时间,那后面要讨论的心理管理等话题就都没有意义了。

所以工作量增加的时候,重点要放在提高工作效率,而不是增加工作时间,end of story.

建议二:安排工作的优先级和时间

在焦头烂额的时候,如果有人天天来跟你说「这个需求很重要,什么时候能完成?」可能就会先做这个需求。有段时间每天至少 5 个人来问我 xx 什么时候可以做完,我的回答每天都一样,「和最初承诺的时间一样,如果最近有空了可以加快一些」。因为最初的时间就是按照优先级排列的,不会因为有人天天来问就变得快一些。

优先级如何排列,也不是只看需求方说的。如果对方提了一个不合理的时间,要了解下为什么这样着急。很多 deadline 都是随意拍脑袋定的,可能是为了某人在某个时间点可以向大老板汇报,可能是依赖你的工作的人先承诺了一个 deadline,也可能就是随意定的一个日期。在焦头烂额的一堆工作中,有几个有着让人焦虑的 deadline,让人很难忽略这些工作。但是优先级不应该按照 deadline 来排列,而应该按照真正的重要程度来排列。

  • 项目的发布日期已经对外宣布,用户期待在这一天使用新功能;
  • 线上的系统摇摇欲坠,必须更新一个 fix;
  • 新的集群需要部署,但是如果晚几天部署,也不会 block 任何人的工作;
  • ……

遇到不合理的预期的时候,可以问这几个问题:

  • 这个需求是服务谁的?为什么要做?如果不按照这个时间上线会有什么后果?
  • 其他人的工作是否可以并行做,如果我的这个工作不做,会 block 谁?

有时候把项目在 deadline 之前做完了,却发现后续的一段时间并没有用起来,或者项目继续被其他人 block 着,原来给出的时间线本身就是不切实际的。在最开始就讨论好项目整体的计划,了解真正的紧急程度,避免这些问题。

按照优先级给出需求方截止时间,然后按时间交付工作。但是这之间难免会遇到其他事情,比如临时插入了更紧急的需求,线上发生了事故需要立即处理等等。这一般也不是问题,在时间线有变化,无法按时交付的时候,应该立即通知需求方遇到的困难,新的预估时间。忌讳的是没有和需求方同步,直到交付日期的时候才说,因为某某原因项目无法按时交付了。

建议三:大项目如何推进?

对于大型的项目,尤其是需要多方参与的那种,如果你不幸当了项目的 owner,那么这个建议很实用:用笔记软件记录每天的进展,记录每天遇到的问题,以及这些问题的进展。

以前有一次我们要新建一个数据中心,infra 把机器准备好,然后中间件团队部署好各种服务,缓存,队列,网关等等,然后业务团队部署好业务程序,最后上线。但是我们已经好几年没有完整地上线过一个数据中心了,很多代码中都已经编码了 IDC 的名字,所以这项工作异常困难,要么这个组件启动不了,要么那个组件存在硬编码问题。

负责这个项目的同事是一个很靠谱的人,每遇到一个问题,他都在文档中记录下来。问题原因,负责人,解决方法,解决进度。项目结束之后,这个文档列出来长长的一串问题。看到这个文档我的感受有二:项目真难,这位负责的同事真靠谱。

同时我也学会了这项工作方法, 那时候起我就开始写工作笔记(用的软件是 Roam Research,笔记经过整理记录在公司的文档系统中),每一个项目都有详细的记录,记录的问题也成千上万了。

工作笔记的好处多多,显而易见的是,没有人能记住如此多的问题和细节,所以必须追踪记录。另外也让工作进度和内容透明,如果项目不能如期完成,也能知道问题在哪里。如果没有项目文档,无法解释项目进展和问题,就只能项目负责人的问题了。

经过实践我发现一个额外的好处是,可以带来工作心态的变化。

如果没有记录——想起来这个项目满是头疼,已经经历过 x 问题,y 问题,天知道还要经历什么问题,感觉每走一步都困难重重,想起来就头疼。

有了记录——我们已经解决了 x 问题和 y 问题,我倒要看看还可能出现什么问题!

建议四:使用异步的沟通方式

前面提到过我们要提高工作效率。一个重要的方法就是不要破坏自己的整块时间,不要让自己总是处于被打断的情况。如果养成了过几分钟就要切换到聊天软件查看消息的状态,那工作效率就完蛋了。

要像使用邮件一样使用消息软件,异步轮询沟通。(证明:基于忙轮询的 DPDK 比基于中断的 Linux 网络栈,性能就高多了)。

怎样做呢?前面我们已经学会写工作笔记了,在被 block 需要与人沟通的地方,就在这里记录下需要沟通和确认的地方。然后在每天定时(比如早上刚来和午饭之后)遍历所有在 block 的点,对每一个点都问一遍相关的同事需要确认的问题。但是一定要把所有的细节说全,比如咨询一个网络问题,要提供自己的 IP,对方的 IP 端口,现象是什么,预期结果是什么,traceroute 是什么。防止对方缺少信息需要跟你再次确认。这就回到问问题的艺术的话题1了。这样就不需要等待回复,所有相关的消息发出去之后就可以继续做没有被 block 的工作,然后等下下次轮询的时间查看消息。

对于收到的信息也一样,几乎所有的消息都不必立即回复。也可以用轮询的方式处理。很多人问问题的时候都不懂如何一次性把信息都提供出来,比如,报告网络问题,连从哪里到哪里有问题都说不明白。不必在等待回复上浪费时间。

建议五:安排工作计划

这条建议可以让你带着一个好心情上班:每周安排好这周要做的事情,每天安排好明天要做的事情,可以已经确定的优先级来安排。

如果没有工作计划,那每天上班看到的就是一个长长的 todo list,怎么能让人不焦虑。

如果有工作计划的话,至少确定今天只要完成这些工作就好了。心理上的负担也会轻松很多。明天的工作就让明天的自己去担心好了。

建议六:每天至少完成一件事情

这条建议可以让你带着一个好心情下班:每天至少完成一件事情,比如解决集群搭建中一个 block 的点,比如完整实现一个需求。

如果一天的时间都在开会,和不同的人讨论细节,到下班的时候一事无成,是很挫败的。每天至少动手完成了点什么,这点满足感会带来很大的不同。

建议七:不要完全放弃有长期收益的事情

不要花所有的时间去做紧急的事情,要花时间去做不紧急但是重要的事情。

比如:

  • 提高监控的覆盖度;
  • 自动化一个操作;
  • 从根本解决一个性能问题;

每天忙于救火,就永远无法从这种工作状态中脱身。去从根本解决问题,工作也会越来越少,形成良性循环。

举个例子:在给产品值班的时候,会有很多用户来问问题。我一般会提供用户文档链接,文档中有答案。如果对于一个问题没有现有的文档可以回答,要么是产品设计出了问题——为什么用户会有此疑问?要么是文档不够全面,我会去写一个关于这个问题的文档,然后再给用户文档链接。虽然表面上可以直接回答的问题花了更长的时间去解决,但是长远来看,将来的用户可能因为这个文档就不来问这个问题了,即使有人问相同的问题,我也可以给文档链接。

有关操作的自动化,也不是所有的操作都应该自动化,也要看投入产出比。如果一个操作一个月才有机会操作一次,那么用文档记录下来如何手工操作,也可以。相较之下,手工操作反而可能成本更低。此外,如果使用频率不高,那么下次用到的时候,自动化的流程很可能是坏掉的,需要临时去 debug 哪里出了问题。

  1. 程序员如何高效和同行交流 ↩
🔲 ☆

请求为什么超时了?答案和解析

这篇博客是请求为什么超时了?这篇的答案和解析。

首先,通过这个抓包文件前几个包可以发现,服务器的网络是没有问题的,因为访问 DNS 或者 ubuntu apt 源都是通的。

这个问题有两个要点。第一点是,抓包文件并不是仅仅包含出问题的请求本身,还包含很多与问题无关的流量。不过现实的情况也往往如此,我们要在很多抓包和分析的时候过滤掉和问题无关的流量。

有一个非常好用的过滤方法,就是直接用 TCP 的 payload 内容进行过滤。比如,我们已经知道请求的目标是 example.com 了,那么 Host: example.com 必然会存在于 TCP 的 body 中,所以可以用以下的过滤条件:

tcp.payload contains "example.com"

(在之前的写的 网络抓包的技巧 中也介绍过,我们可以发送带有标记的请求,tcp.payload contains "xxx" 也是过滤出来这种标记请求的好方法)

用这个过滤条件可以得到以下的几个包,这就是我们要分析的请求了。

发给 example.com 的包

可以看到,我们发送给 exmaple.com 80 端口的包从来就没有得到过确认,于是一直在增大请求间隔并不断重试。

另一个奇怪的地方是,这个 TCP 请求没有 SYN 包被过滤出来,直接就开始发送 payload 了。这说明这个连接是在我们抓包之前就已经建立好的,所以我们没有看到连接建立的过程。

我们这个抓包文件的第一个包的时间是 37分52秒,而 HTTP 请求的第一个包时间是 39分28秒,间隔了 96 秒。这意味着这个 TCP 连接是至少在 96 秒之前建立的,并且在建立之后,至少在 96 秒 的时间内,没有发送过任何内容。(因为抓包文件没有抓到)

那么这个连接很可能因为 inactive 太久而被中间的网络设备丢弃了。如何定义 inactive?简单来说就是这个 TCP 连接上没有在一段时间内没有传输任何内容。

为什么网络设备会丢弃不活跃的 TCP 连接呢?因为机房的程序访问到公网要经过 NAT,防火墙等网络设备(其实和家用宽带是一样的,只不过家用路由器本质上是一个路由器+NAT+防火墙),而防火墙或 NAT 设备的内存只能保存有限的连接数,因为连接的保持需要内存,内存是有限的。它们普遍采用的策略是保留最近用到的连接,丢弃最旧没有有消息的连接。即使内存没有用完,一般在配置上也会设置一个连接最长的 inactive 时间,尤其是防火墙设备。

那么如何解决这个问题呢?首先如果不用长连接肯定就没问题了,每次需要发送 HTTP 请求的时候,都重新建立 TCP 连接。但这样成本就高了,TCP 连接不复用会浪费硬件资源,延迟也会升高。所以更好的方法是使用 Keepalive,即还是复用长连接,但是需要把长连接保持住。Keepalive 的原理,其实就是定时在 TCP 连接上发送 len=0 的包,即不包含 payload,类似于 duplicate ACK。发送空包不会对对端造成任何干扰,但是这些数据包会刷新中间的网络设备,避免连接失效。退一步讲,即使连接失效了,也可以通过 Keepalive 包来提前发现,避免用到的时候才通过超时发现问题。

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?答案和解析
  7. 网工闯了什么祸?答案和解析
  8. 延迟增加了多少?答案和解析
  9. 压测的时候 QPS 为什么上不去?答案和解析
  10. 重新认识 TCP 的握手和挥手答案和解析
  11. TCP 下载速度为什么这么慢?答案和解析
  12. 请求为什么超时了?答案和解析
  13. 0.01% 的概率超时问题答案和解析
  14. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
🔲 ☆

网络抓包的技巧

虽然这个系列的文章都是聚焦于如何通过分析网络抓包文件,结合网络知识,来解决实际的问题的,但是分析之前的步骤——抓包,也是同样重要!很显然,如果不会抓包,那么网络分析去分析什么呢?

抓得一手好包也是很厉害的!笔者遇到过很多次情况,虽然我们无法直接定位根因,但是同事能够精准地捕获到问题的现象,把问题描述给相关的网络专家,传给他们抓包文件,专家一看到准确的抓包文件,就可以很快解决问题了!

可惜的是,抓包的技巧无法像网络分析那样可以通过文章来出谜题,来让读者小试牛刀。所以,这篇文章就来写一下一些常用的抓包方式和技巧,希望能补齐这一块内容。

Tcpdump

tcpdump 命令是我们最常用的抓包工具了1

tcpdump -i eth0 icmp and host 1.1.1.1

这个命令就可以抓取到所有通过 eth0 去 ping 1.1.1.1 这个地址的包。

-i eth0 的意思是抓取指定的 interface,如果不指定,tcpdump 会默认选择一个。但是推荐每次都指定好这个参数,这样就没有不确定性了。如果使用 -i any 就可以抓取所有常规端口(文档的原文是 all regular network interfaces),但是什么属于「常规端口」就取决于操作系统的实现了。所以,建议也是如果要抓取多个 interface 来分析的话,就多开几个 tcpdump 进程,这样更加稳定一些。

这个参数非常有用,比如,在定位 ARP 问题的时候,我们需要确定每一个物理接口收发 ARP 的情况,就可以开多个进程分别 dump 每一个 interface 的网络;在定位 Linux 网络栈不通的情况时2,比如有 macvlan,vlan,veth 等复杂的 driver,可以用 tcpdump 对每一个接口 dump,看下包丢在哪里。

icmp and host 1.1.1.1 这个就是包过滤的表达式了,icmp 表示只抓取 icmp 协议,host 1.1.1.1 表示只抓取 src ip 或者 dst ip 是 1.1.1.1 的包。这种包过滤表达式其实是 pcap-filter(7)3 提供的,所以要想看语法是怎么定义的,看 pcap-filter 的文档就可以了。pcap-filter 支持的语法很灵活,能做的事情很多,基本上想抓什么样的包都可以写出来。但是我们没有必要把所有的语法都记住,因为常用的抓包都是比较简单的。可以找一个 tcpdump exmaple4 看一下,基本就够用了。其次,我们一般不会直接从 tcpdump 就分析出来问题原因,所以这个语法最重要的作用是把我们想要的包抓到,然后为了抓包性能更高,抓包文件更小,我们想要对抓包定义的更精确一些。其实,多抓一些包也没有什么问题,如果不确定怎么过滤出来 TCP SYN+ACK 的包,那不妨就把所有的 SYN 包全抓到,然后再用 Wireshark 这种工具来分析吧。最后,我们现在有 AI 了,用 AI 来写 pcap-filter 也是一个不错的方法,因为这种语法难写,但是很容易验证正确性。

Tcpdump 一些常用的其他参数如下:

  • -n 不解析主机名和端口号,保留原始的数字
  • -v, -vv, -vvv v 越多表示输出的信息越详细
  • -c 5 表示抓到 5 个包之后就退出
  • -e 显示二层的 link layer header,这样就可以看到 MAC 地址了
  • -Q 可以指定抓包方向,可以选的有 in, out, inout
  • -A 可以展示包的内容,tcpdump 默认是只根据不同的协议展示 header 信息的。在线上排查问题的时候,我们往往需要通过特殊请求的关键字来定位到单个请求的情况进行排查,这样 -A 展示出来包的内容就格外有用。
通过 -A 参数来抓取特定的 HTTP 请求

这里分享一个特殊的技巧,就是发标记请求来定位问题。比如 A 通过 B 代理发请求给 C,现在网络不通,我们要定位 B 收到了请求没有,才知道是 B 的问题还是 C 的问题。但是 B 本身就有很多线上流量,怎么知道 A 发送的请求到达 B 了没有呢?我们可以在 B 进行 tcpdump:tcpdump -i eth0 tcp | grep asdf123 -A 10,然后我们从 A 发送一个请求:curl http://host-C.com/asdf123asdf123 就是我们在请求里面放上的标记,如果 B 能够正常转发,我们就可以 match 到这个请求。当然了,这种技巧只适用于 HTTP 这种明文协议。

Wireshark 离线分析

有些问题很难直接在 tcpdump 的终端分析出来问题,比如涉及 sequence number 分析的,重传分析之类的,我们需要人工对比 seq number,真是一项费眼睛的工作!所以如上所说,我们也经常在机器上用 tcpudmp 抓包保存成 .pcap 文件,下载到本地用 Wireshark 分析。Wireshark 就可以自动根据 sequence number 告诉我们重传等信息了!

Wireshark 可以展示出来 Dup ACK 和 Retransmission 等信息

具体的操作方式是,用 tcpdump -i eth0 -w file.pcap icmp 来进行抓包,-w file.pcap 表示把抓包文件保存为 file.pcap,抓包结束后,就可以把这个文件用 rsync 或者 scp 下载到本地,用 Wireshark 打开了。

.pcap 文件是一种标准的二进制抓包文件5,很多抓包分析工具都支持这种格式的解析,比如 tcpdump, wireshark, scapy 等等,如果想写代码进行更加定制化的分析,也可以用已有的库6解析,就如同用 json 库来解析 json 文件一样。

使用 wireshark 的命令行工具 tshark 可以解析二进制 pcap 文件到 json 格式

使用 -w 写入文件的时候有一个小问题,就是 tcpdump 原本的到终端的输出没有了。有两种方式可以解决,第一种是用 tcpdump 自带的 --print 功能:

tcpdump -i eth0 -w file.pcap --print

--print 会让 tcpdump 把内容输出到屏幕,即使当前使用了 -w 参数。

第二种就是用 tee,在写入文件的同时,也写入到 stdout。

tcpdump -i eth0 -U -w - | tee test.pcap | tcpdump -r -

其中,第一个 tcpdump 把抓包文件写入到 stdout(-w stdout,注意其中的 -U 表示按照 packet buffer,即来一个 packet 就输出一个到 stdout,而不是等 buffer 满了才进行输出),然后 tee 这里做了分流,把 stdin(tcpdump 的 stdout)同时输出到文件和 stdout。由于这里的 stdout 是 tcdpump 输出的二进制抓包内容,所以我们需要再用 tcpdump 解析这个二进制内容,-r - 表示从 stdin 读入。

还有一个技巧是 -s 参数,默认情况下 tcpdump 会保存所有抓到的内容,但是在分析某些问题的时候,尤其是 TCP 性能问题,我们其实不需要 TCP 传输的 payload 内容,只看 TCP 包的 header(序列号部分)就知道传输的速度了,所以可以用 -s 40 来只抓取前 40 个 bytes,有了 IP header 和 TCP header,就足够分析了。(如果担心有 TCP option 的存在,可以用 -s 54

其他的一些经验

知道包是从哪里抓到的,很重要。在排查问题的时候,拿到抓包文件,应该第一时间确认抓包的位置。否则,就可能连自己看到的问题是现象还是根因都分不清楚。建议在复杂的结构中画一个拓扑图来对照分析,在定位 Linux 网络栈的问题时,如果接口拓扑非常复杂,也建议画一个拓扑图来分析。

可以从网络的多端抓包对照分析。发送端的抓包不一定等于接受端,尤其分析 TCP 问题的时候。可以同时在发送端和接收端进行抓包,然后对照分析。

在使用 tcpdump 的时候,要尤其注意,我们抓到的包已经经过了网卡驱动的处理,网卡驱动经常会帮 CPU 做一些 offload 的工作,比如把可能因网卡的 GRO/LRO 等特性,导致多个小包在抓包时被合并为一个较大的数据包,或者网卡帮助卸载了 vlan tag 等,我们用 tcpdump 抓到的包不一定是真正在网络上传输的包7。要格外注意。

注意抓包不要抓重。比如有人很喜欢用 tcpdump -i any ... 抓全部的包回来慢慢分析。然后下载下来抓包文件就吓坏了——重传率高达 50%!

抓包抓重了的情况

在 Linux 中的网卡配置有 slave 和 master 的时候很容易发生这种情况,比如有 bonding 配置8-i any 会从 slave 抓包包,从 master 又抓到一次,然后在 Wireshark 看来,所有的包都被重传了。实际是同一个包先后经过 slave 和 master 而已。

抓包的时候最好把相关 host 的 ICMP 协议包也一起抓了。因为 ICMP 是重要的 control message,TCP 在传输的时候,不光有 TCP 协议,可能还会用 ICMP 协议来传递一些信息。比如 PMTUD9,以及之前遇到过的这个问题10,都是涉及到 ICMP 包。如果只按照 TCP 协议来抓包,那这个重要的信息就错过了。

SPAN 交换机抓包和RSPAN 远程抓包

除了我们熟悉的 Linux 抓包,其实网络设备上也可以抓包的。我们一般叫它「端口镜像」技术,故名思义,原理就是把网络设备的一个端口的流量全部复制到另一个端口,而另一个端口连接的就是我们的抓包程序。

SPAN 原理,图来自 Cisco
  1. 文档的主页:https://www.tcpdump.org/manpages/tcpdump.1.html ↩
  2. Keepalived 脑裂问题排查 ↩
  3. pcap-filter 文档在这里:https://www.tcpdump.org/manpages/pcap-filter.7.html ↩
  4. 比如这一个:https://danielmiessler.com/blog/tcpdump ↩
  5. IETF 的文件规范定义:https://www.ietf.org/archive/id/draft-gharris-opsawg-pcap-01.html ↩
  6. Python 可以使用 scapy (https://scapy.readthedocs.io/en/latest/usage.html#reading-pcap-files)读取 pcap 文件,golang 可以使用这个库进行解析:https://pkg.go.dev/github.com/google/gopacket/pcap ↩
  7. 参考 有关 MTU 和 MSS 的一切 一文中,「道理我都懂,但是我的抓的包怎么大??」 ↩
  8. 数据中心网络高可用技术之从服务器到交换机:active-backup ↩
  9. 真实世界中的 PMTUD ↩
  10. 由 ICMP Redirect 消息引起的丢包问题排查 ↩

==计算机网络实用技术 目录==

这篇文章是计算机网络实用技术系列文章中的一篇,这个系列正在连载中,我计划用这个系列的文章来分享一些网络抓包分析的实用技术。这些文章都是总结了我的工作经历中遇到的问题,经过精心构造和编写,每个文件附带抓包文件,通过实战来学习网路分析。

如果本文对您有帮助,欢迎扫博客右侧二维码打赏支持,正是订阅者的支持,让我公开写这个系列成为可能,感谢!

没有链接的目录还没有写完,敬请期待……

  1. 序章
  2. 抓包技术以及技巧
  3. 理解网络的分层模型
  4. 数据是如何路由的
  5. 网络问题排查的思路和技巧
  6. 不可以用路由器?
  7. 网工闯了什么祸?
  8. 网络中的环路和防环技术
  9. 延迟增加了多少?
  10. TCP 延迟分析
  11. 压测的时候 QPS 为什么上不去?
  12. 压测的时候 QPS 为什么上不去?答案和解析
  13. 重新认识 TCP 的握手和挥手
  14. 重新认识 TCP 的握手和挥手:答案和解析
  15. TCP 下载速度为什么这么慢?
  16. TCP 长肥管道性能分析
  17. 请求为什么超时了?
  18. 请求为什么超时了?答案和解析
  19. 后记:学习网络的一点经验分享
与本博客的其他页面不同,本页面使用 署名-非商业性使用-禁止演绎 4.0 国际 协议。
❌