iowait 的含义

iowait 是 CPU 的一种状态,表示此时 CPU 正处于 idle 状态,并且至少有一个进程正在等待 IO。

CPU 一共有四种状态。在任一时刻,CPU 的状态都是四种中的一种。这四种状态是:user, sys, idle iowait. 一般的程序,比如 sar top,会用百分比表示 CPU 分别处于这四种状态的时间,这四种状态相加的结果是 100%。

其实准确来说,CPU 只有两种状态:busy 和 idle。Busy 又分成了两种,user 和 system,可以表示 CPU 目前正在执行用户空间的代码,还是正在执行 kernel 空间的代码,方便开发者定位问题。Idle 又分成了两种:idle 和 iowait。笼统来说,idle 就是目前系统中没有 Runnable 的进程了(参考之前的博文:Linux 进程的生命周期),iowait 就是目前系统中没有 Runnable 进程(所以说 iowait 是 idle 的一种),并且,有进程卡在 IO 上。

(注意,本文说的“进程”是从资源调度的角度讲的,本文的语境中,进程包括线程)

统计的方式

Kernel 会定期更新 counter(AIX,CPU state counter 每个 clock interrrupt 更新,每 10ms 一个 interrupt)。

对于每次更新,检查 CPU 是否是 busy,如果是 busy,检查 CPU 在执行用户态还是内核态的代码,增加相应的计数器。如果不是 busy,那就是 idle,检查此 CPU 之前有没有发出 IO 操作,如果有,那么就增加 iowait,如果没有,那么就增加 idle 的计数器。(所以可以认为这个数值是抽样统计的方法?)

像 sar,top 这种展示工具,就去读 counter,计算时间段内的比例,展示结果。

举例说明含义

综上,假设 CPU state 出现了比较高的 iowait,那么就意味着:这个 CPU (在大部分时间)找不到可以执行的进程了,至少有一个进程在等待 IO。

iowait 只包括文件系统的 IO,不包括:sockets, pipes, ttys, select(), poll(), sleep(), pause()。

举例1: 一个机器有 4 个 CPU,1个CPU iowait 比较高,3 个 CPU user+sys 很高,iowait 很低。

意思是当前有 3 个 Runnable 的进程,有至少1个进程阻塞在 IO,其他的进程要么阻塞在 IO,要么阻塞在其他状态,但是不是 Runnable 状态,Runnable 状态的进程有且只有 3 个。因为如果再多来一个 Runnable,那么它就会被 iowait 高的那个 CPU 去执行,此 CPU 的 iowait 也不会很高了,user+sys 会变高。

举例2: 这个例子是参考资料 What exactly is “iowait”? 中的,非常典型。

假设有 6 个 CPU,而且只有 6 个进程:

  • 2个进程不涉及磁盘 IO 操作,只有 CPU 计算;
  • 4个进程要花 70% 的时间做 IO 操作,30% 的时间在 CPU 上计算,这 30% 中,5% 是在 kernel mode,25% 是在 user mode。

那么 CPU 利用率将会如下,最后一行是综合所有的 CPU 的数据:

现在,还是6个进程完全不变,但是 CPU 从6个减少到了4个,数据将会是如下:

可以看到,2个 100% 利用率的 CPU 保持不变,但是总的 CPU 的利用率是 80% 了。解释如下。

4个进程,每一个进程只要花 30% 的时间来计算,其他的时间是 iowait,iowait 是 idle 中的一种,iowait 的时候,如果能有别的事情可以做,CPU 就会去做别的事情。所以 2 个 CPU 每一个会花 60% 的时间做计算工作(一个进程是 25% + 5%,做完之后做下一个进程的 25% + 5%)。做完 60% 就无事可做了,剩下 40% 的 iowait(就是 idle)。

总结

iowait 是一种 idle,如果比例过高,它可以告诉我们几件事情:

  • 系统正在做的工作,大部分时间都是在等待 io 了,io 系统的性能不够高(也有可能是硬盘坏了);
  • CPU 没有更多的工作可以做了,我们可以给系统分配更多的计算工作;
  • 它不是 CPU 的一种“阻塞”状态,它不能说明:CPU 现在在等待 IO,无法运行其他进程。而是:没有可以运行的进程能给 CPU 做;

iowiat 是 CPU 角度的一个状态,它不是进程角度的状态。iowait 很低,不能代表进程没有卡在 IO 上。假设有一个进程需要花 70% 的时间做 io操作,把它放到一个空闲的,单 CPU 的系统中,显示的 iowait 是 70%,但是我在这个系统中增加一个不依赖 io 的计算任务,iowait 就变成 0 了。但是我们之前的那个进程,依然需要花 70% 的时间等待 io。所以,它不代表进程的状态。

参考内容:

  1. What exactly is “iowait”?
 

探测 TCP 乱序问题

TCP 协议是基于 IP 协议的。IP 协议不保证顺序,只能说尽力保证包的顺序。如果发生乱序,TCP 的性能就会下降很多。最近就遇到一个 TCP 下载速度很慢的问题,抓包分析发现有很多乱序的包。

网络发生了乱序,那就把锅甩给网络组的同事,但那不是我的风格。虽然我没有二层和三层设备的权限,无法排查哪里出了问题。但是我感觉我可以只从 TCP 的两端,探测出来乱序发生在哪一个节点上。

traceroute 的原理

traceroute 是一个很好的网络工具。之前在 mtr 的教程中也介绍过。

traceroute 的原理很精妙。它利用了 IP 协议本身的特性:每一个 IP 包都有一个 TTL 字段,表示这个包还能在网络中被转发多少次,每次路由器转发一个 IP 包就将其 -1,如果一个路由器发现 IP 包 -1 之后是 0 了,就直接丢弃,并且给 IP 包的 Source IP 发送一个 ICMP 包(包含此 hop 自己的 IP),说这个包气数已尽,无法送达目的地。

traceroute 原理

traceroute 想要知道去往另一个 IP,中间都会经过哪些 IP 节点。它发送一个 TTL=1 的 ping 包出去,包会挂在第一个 hop 上,第一个 hop 发回去 ICMP,traceroute 就知道了第一个 hop 的 IP 地址;它再发送一个 TTL=2 的 ping 包,就知道了第二个 hop 的 IP 地址…… 直到收到正确的 ping 响应,就算到头了。

mtr 类似于一个 traceroute + ping 的工具,还能告诉我们中间每一个 hop 的延迟。原理其实和 traceroute 是一样的,以下用 mtr 举例。

路由 path 的一致性问题

回到乱序的问题,最常见的问题是,中间某一跳到下一跳有多个节点可以选择。这时候,正确的情况下,对于 (src ip, src port, dst ip, dst port) 这样的四元组,应该固定走一条线路。就像 ECMP 的 hash 一样

ICMP Ping 没有端口的概念,但是对于 (src ip, dst ip),路线应该是一致的。即,我们在对某一个 IP mtr 的结果中,每一跳都应该只有一个唯一 IP。

mtr with ICMP, 每一跳都只有一个 IP

mtr 有 tcp traceroute 的功能,我用 tcpdump 看了下,原理和 traceroute 相同,只不过 IP 包的内容不是 ICMP 了,而是 TCP。路由器丢了 IP 包,不管里面是 TCP 还是 ICMP,都会发回来 ICMP 消息。于是 tcp traceroute,我们就可以用 tcp 包探测出来中间的转发节点。mtr 使用的 tcp 包是 syn 包,remote port 可以指定,但是 local port 每一个包都是随机选择的。这就导致,每一次发起探测,都是一个不同的四元组:(src ip, src port, dst ip, dst port) ,因为 src port 每次都在变,所以路径每次都是不一样的。这样的话相当于可以将路径中所有的路由设备 IP 都探测出来。

mtr with TCP, 每一跳可能有多个 IP

如图可以看出,一跳可能有多个 IP。

TCP multipath 探测

但是正确网络中的 TCP,如果 local port 也不变,那么相同的四元组,应该走同一条路,来尽量保持到达顺序和发送顺序一致。

利用这个原理,假设我们使用的 local port 一直保持不变,那么 TCP traceroute 的结果应该是,每一跳都只有一个 IP

mtr 没有提供固定 local ip 来 trace 的功能,我用 scapy 写了一个。

代码非常简单,就是发送 ttl 不同的包,然后检查收到的 ICMP 包。限制最多 20 次,如果 20 次还没到达终点就放弃。如果 reply 里面有 TCP 包,说明收到了正常的回复(即使是 RST 也算)。

另外,因为我们每次都用同一个 local port,所以不能用多线程,每次只能有一个探测存在。

实际测试发现,收到的 ICMP 响应并不是很稳定,有的时候 hop 4 丢了,有的时候 hop 5 丢了。可以使用一个 bash 循环多测试几次。

跑一段时间之后,我们可以查看这个日志文件,过滤掉错误的包 (??),然后排序之后,去重。理论上,每一跳都应该只有一个 IP 才对,如果有某一个 hop 出现了两个 IP,那说明就是出现了 multiple,会知道乱序。

如上输出,在 hop 5 出现了两个 IP,说明是 hop 4 到 hop 5 的时候出现了 multipath。

在得到了这个结果之后,去和网络组的同事做了确认。结果发现这个不是导致问题的根因。不过觉得挺有意思的,稍作记录一下。

小技巧:traceroute 只能测一个方向,有些情况下去的方向和回来的方向可能不是同一个路线。所以无论是 traceroute 还是 tcp traceroute,不妨从两端都试试。说不行会发现新的线索。

 

如何阅读火焰图

这篇文章是火焰图阅读的简明教程。

火焰图是我们用来分析性能的可视化工具。很多 profile 工具输出的信息都非常多,是一个巨大的文本,在这个文本中,找到性能瓶颈,会比较困难。但是如果画出来一张图,可以一下就看到问题所在。

火焰图是 Brendan Gregg 发明的。使用官方的工具 FlameGraph,可以将文本渲染成 svg。如下。

官方的 FlameGraph 渲染出来的 svg 截图

现在也会有其他的工具能渲染出来类似的图了,比如 golang 的 pprof 现在内置了一个新版的火焰图预览工具,在线的 speedscope 也可以渲染。我最喜欢的是 Flameshow,一个终端工具,可以直接在终端用字符渲染出来火焰图,设计的非常精妙。(其实就是我自己写的)。由于是我自己写的,那么下文我就以 Flameshow 来做展示的例子了。

Flameshow

阅读方法

火焰图作为一个可视化的工具,着重表达的信息是:父子之间的关系,每一个块的占比。

火焰图有从下向上的和从上向下的,本质是相同的,只是方块之间的关系方向不同。从上向下:下面的方块是上面的子块;从下向上:上面的方块是下面的子块。

主要信息有(以从上到下为例子):

  • 每一个方块,都是一个函数,方块的宽度,就表示函数消耗的时间占比。(如果是内存火焰图,那就表示的这个函数申请的内存占比。)所以我们看火焰图,主要去找最宽的一个方块。
  • 上下堆叠在一起的是表示函数调用。Y 轴表示调用的深度。

火焰图一般是支持交互式的,svg 和 flameshow 都支持点击其中一个 function,来放大。如下例子:

点击放大其中一个 function

标记的是,最开始调用的函数是 collector.NodeCollector.Collect.func1,然后这个函数的所有时间都在调用 collector.execute,以此类推。到下下面的 os.(*File).readdir,其中有一大部分是在调用函数 os.Lstat,然后其余的时间花在了 os.direntReclen

很多人对火焰图容易有一些误解,这里着重说明一下:

  • Y 轴的深度一般不是问题。我们用火焰图主要是排查性能问题,是要找消耗时间长的地方。调用深度很深,但是没花多久时间,一般不要紧;
  • 颜色(几乎)没有意义。不是说颜色越深时间越久。颜色只是为了区分出来不同的块而已。一般会将相同名字的函数都使用同一个颜色,这样,即使它们分散在不同的 stack 中,也能清晰看出总时间比较高。从 FlameGraph 的源代码也可以看出,颜色是根据 function 名字随机生成的。但是有一种优化:比如对于 Java 的 JVM 来说,可以用不同的红色表示 Java 代码消耗的时间,可以用黄色表示 Kernel 消耗的时间,用蓝色表示 JIT 时间。但是不同的红色,红色深浅,还是没有什么意义的。
  • 方块之间的顺序没有意义。因为火焰图的生成方式(后文介绍),和渲染方式(一般会将同名字的方块 merge 在一起,方便阅读),导致火焰图方块之间的顺序是没有意义的。不代表函数调用的顺序

火焰图的本质是旭日图(Sunburst Graph)

你有没有发现,主要表示占比,又能表示占比之间的关系,是不是跟某一种图很像?

使用 tokei-pie 渲染出来的旭日图

是的,其实火焰图的本质就是拉平了的旭日图。上图是我用 tokei-pie 渲染出来的代码仓库中不同文件夹、文件的行数占比。打开一个新的项目的时候可以轻松找到核心代码。

火焰图的生成和格式

火焰图的生成主要依赖 profile 工具,目前很多工具都支持了,比如 py-spy, golang 的 pprof.

生成的原理大致是去扫描程序的内存,主要是内存的 stack 部分,对 stack 做一个快照。如果扫描了 10 次,其中 function1 出现了 3 次,function2 出现了 6 次。那么它们的宽度占比就是 1:2. 很多 profile 工具就是如此工作的,不是 100% 精确的,但足以让我们分析性能问题了。

生成的格式一般是 stackcollapse 格式,这是官方的一种定义。比如如下的文本:

每一行就代表一个 stack,数字代表整个 stack 的占比。我们要把所有的 stack 相同层级相同名字的 merge 起来,最后就变成下面这样:

简单的 stackcollapse

另一种常见的格式是 pprof 的格式。虽然是 golang 最先开始用的,但是设计的(我个人认为)比较好,也是开源的,protobuf 定义,所以很多工具也支持输出这种格式了。

Continuous Profiling

持续 Profiling 也是我比较感兴趣的一个领域,很多 APM 工具都已经支持了。比如 DatadogGrafana。简单来说,就是不断地对线上部分实例进行 Profile,然后对结果不是简单的展示,而是收集起来。将它们的 stack 都合并起来,做成一个由多个实例的 stack 组成的 Flame Graph,就可以找到集群层面的性能热点了。

另外一个用处是,在发布新版本的时候,可以在灰度的时候,检查新版本的 Flame Graph 和之前的,看有没有引入新的性能热点。

相关链接

  1. https://www.brendangregg.com/flamegraphs.html
  2. https://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html
  3. https://queue.acm.org/detail.cfm?id=2927301
  4. https://youtu.be/6uKZXIwd6M0
  5. https://youtu.be/6uKZXIwd6M0
  6. https://www.webperf.tips/tip/understanding-flamegraphs/
  7. https://github.com/jlfwong/speedscope/wiki
  8. https://www.speedscope.app/
 

再多来点 TCP 吧:Delay ACK 和 Nagle 算法

教科书介绍的 TCP 内容通常比较基础:包括三次握手,四次挥手,数据发送通过收到 ACK 来保证可靠传输等等。当时我以为已经学会了 TCP,但是后来在工作中,随着接触 TCP 越来越多,我发现很多内容和书上的不一样——现实世界的 TCP 要复杂一些。

我们从一个简单的 HTTP 请求开始。发送一个简单的 HTTP 请求,tcpdump 抓包如下:

第一个和书上不一样的地方是,TCP 结束连接不是要 4 次挥手吗?为什么这里只出现了 3 次?

回顾 TCP 的包结构,FIN 和 ACK 其实是不同的 flags,也就是说,理论上我可以在同一个 Segment 中,即可以设置 FIN 也可以同时设置 ACK。

TCP segment,图来源

所以如果在结束连接的时候,客户端发送 FIN,这时候服务端一看:“正好我也没有东西要发送了。”于是,除了要 ACK 自己收到的 FIN 之外,也要发送一个 FIN 回去。那不如我一石二鸟,直接用一个包好了。

TCP FIN 教科书的图,和实际的图

这个例子在 TCP可以使用两次握手建立连接吗? 中详细介绍过。

既然 FIN 可以附带去 ACK 自己收到的 FIN,那么数据是否也可以附带 ACK?也是可以的。

Delay ACK

TCP 是全双工的,意味着两端都可以同时向对方发送数据,而两端又需要分别去 ACK 自己收到的数据。

TCP 的一端在收到数据之后,反正马上也要发送数据回去,与其发送两个包:一个 ACK 和一个数据包,不如不立即发送 ACK 回去,而是等待一段时间——我反正一会要发送数据给你,等到那时候,我再带上 ACK 就好啦。这就是 Delay ACK

数据 + ACK

Delay ACK 可以显著降低网络中纯 ACK 包的数量,大概 1/3. 纯 ACK 包(即 payload length 是 0 ),有 20 bytes IP header 和 20 bytes TCP header。

Delay ACK 的假设是:如果我收到一个包,那么应用层会需要对这个包做出回应,所以我等到应用的回应之后再发出去 ACK。这个假设是有问题的。而且现实是,Delay ACK 所造成的问题比它要解决的问题要多。(下文详解)

Nagle’s Algorithm

现在再考虑这样一个问题:像 ncssh 这样的交互式程序,你按下一个字符,就发出去一个字符给 Server 端。每通过 TCP 发送一个字符都需要额外包装 20 bytes IP header 和 20 bytes TCP header,发送 1 bytes 带来额外的 40 bytes 的流量,不是很不划算吗?

除了像这种程序,还有一种情况是应用代码写的不好。TCP 实际上是由 Kernel 封装然后通过网卡发送出去的,用户程序通过调用 write syscall 将要发送的内容送给 Kernel。有些程序的代码写的不好,每次调用 write 都只写一个字符(发送端糊涂窗口综合症)。如果 Kernel 每收到一个字符就发送出去,那么有用数据和 overhead 占比就是 1/41.

为了解决这个问题,Nagle 设计了一个巧妙的算法 (Nagle’s Algorithm),其本质就是:发送端不要立即发送数据,攒多了再发。但是也不能一直攒,否则就会造成程序的延迟上升。

算法的伪代码如下:

简单来说,就是如果要发送的内容足够一个 MSS 了,就立即发送。否则,每次收到对方的 ACK 才发送下一次数据。

Delay ACK 和 Nagle 算法

这两个方法看似都能解决一些问题。但是如果一起用就很糟糕了。

假设客户端打开了 Nagle’s Algorithm,服务端打开了 Delay ACK。这时候客户端要发送一个 HTTP 请求给服务端,这个 HTTP 请求大于 1 MSS,要用 2 个 IP 包发送。于是情况就变成了:

  • Client: 这是第一个包
  • Server:… (不会发送 ACK,直到 Server 想发送数据给 Client,但是这里因为 Server 没有收到整个 HTTP 请求内容,所以 Server 不会发送数据给 Client)
  • Client: … (因为 Nagle 算法,Client 在等待对方的 ACK,然后再发送第二个包的数据)
  • Server: 好吧,我等够了,这是 ACK
  • Client: 这是第二个包
Nagle’s Algorithm 和 Delay ACK 在一起使用的时候的问题

这里有一个类似死锁的情况发生。会导致某些情况下,HTTP 请求有不合理的延迟

再多说一点有关的历史,我曾经多次在 hackernews 上看到 Nagle 的评论(Nagle 亲自解释 Nagle 算法!12)。大约 1980s,Nagle 和 Berkeley 为了解决几乎相同的问题,发明了二者。Berkeley 的问题是,很多用户通过终端共享主机,网络会被 ssh 或者 telnet 这样的字符拥塞。于是用 Delay ACK,确实可以解决 Berkeley 的问题。但是 Nagle 觉得,他们根本不懂问题的根源。如果他当时还在网络领域的话,就不会让这种情况发生。可惜,他当时改行去了一家创业公司,叫 Autodesk

解决方法是关闭 Delay ACK 或者 Nagle’s Algorithm。

配置方法

关闭 Nagle’s Algorithm 的方法:可以给 socket 设置 TCP_NODELAY. 这样程序在 write 的时候就可以 bypass Nagle’s Algorithm,直接发送。

关闭 Delay ACK 的方法:可以给 socket 设置 TCP_QUICKACK,这样自己(作为 server 端)在收到 TCP segment 的时候会立即 ACK。实际上,在现在的 Linux 系统默认就是关闭的。

前面这篇文章提到:

如果在收到对方的第二次包SYN+ACK之后很快要发送数据,那么第三次包ACK可以带着数据一起发回去。这在Windows和Linux中都是比较流行的一种实现。但是数据的大小在不同实现中有区别。

如果我们关闭 TCP_QUICKACK ,就可以看到几乎每一次 TCP 握手,第三个 ACK 都是携带了数据的。

 

肯特岗

肯特岗,一定是我在新加坡度过时间最多的地方,因为我在这里上班。

这里地形很奇特,坐了三次电梯上楼,可以从另一个门出来,居然还是一楼,对得起“岗”这个名字了。

去吃午饭的路上,可以望到山岗脚下的一些楼,有希捷,Grab,得意洋洋地把招牌挂在大楼最高的地方。还有一个没有窗户的奇怪建筑,是一个数据中心。

公司每个月都会搞一次大型促销,时间定在 1月1日,2月2日,3月3日,以此类推,非常有创意。像我们这种工作就要苦哈哈地值班,以免系统出现异常。

有一天,凌晨2点下班,打车回家。司机是一个老安哥。

安哥问我,“怎么才能在你们公司的网站上买东西呢?需要到什么地方填表格吗?”

我震惊,好像从没有听说过报纸是何物的 00 后一样。我说,“不需要填表,只要下载一个 app 就可以了,很简单的。”

安哥说,“安哥老了,跟不上时代了,搞不懂了。”

我决定继续解释一下,“只需要下载一个 App,然后填上你的手机号码,收到验证码,填上验证码,然后将商品加入购物车,选择 checkout,然后填上你的银行卡,银行可能会给你发验证码,完成付款,就可以等待收货了。”说完这些,我发现这个过程对一个老人来说可能不简单,也不好意思重复再说很简单的话了。

安哥继续抱怨说,出租车公司发过来一个要上网完成的东西,自己按照说明却操作不好。让我想起来我的母亲,虽然我的工作就是天天跟这些电脑打交道,但是却不擅长教会别人用手机。在诺基亚流行的时代,我教我的母亲怎么把别人的手机号记到手机里,怎么打开新的通讯录,什么键是确认,怎么退出。我的母亲总是喜欢记:按这个键,再按这个键,最后按这个,就能打开通讯录。全不管屏幕在显示什么。

安哥询问我家乡,得知我是中国北方人,又开始兴致勃勃地跟我聊起来北方的冬天,跟我确认他听说的有关冬天的寒冷是不是真的。然后又不知怎的,开始跟我确认一些北方的习俗是不是真的,真相是,他的信息,我大部分都是第一次听说。

生活小技巧

在肯特岗地铁站下车,从唯一的扶梯上楼,右拐进入到一个小商区,在右手边扫码支付 1.7 元,阿姨问你,你就说,要一个 Plain Waffle. 当当,就会收获一个世界上最好吃的华夫饼。