Linux下做性能分析7:IO等待问题

[介绍]

IO问题其实也就是前面我们提到的队列问题。队列的作用是做速度匹配,让执行速度不同步的两个系统可以匹配在一起。在自己的业务程序中,可能就是因为计算压力不一样,你调度不同数量的CPU来处理计算的不同部分,所以你通过队列让不同数量的CPU可以平衡计算资源。

IO问题和这个没有什么两样,只是计算资源换成了IO资源。CPU准备好了数据,IO设备没有准备好,或者反过来,最终也是先把数据送入队列,等队列搬运者(CPU的线程或者IO设备自己的“线程”)把数据消化掉。监控队列的长度和“清库存”的速度,就可以观察系统的性能压力。

比如在top中,我们最常见的一个参数是load:

这个就是运行队列的长度(top显示了1,5,15分钟的平均值),根据我们有多少个CPU,我们就大概知道系统有多忙了(不过这个值不太容易用,因为消化进程的速度不是个定值,取决于这个进程的需求)。

其他IO系统也有相应的统计参数可以参考。我们用得最多的是网络和存储子系统。本篇介绍这两个子系统的跟踪,我对这两个子系统不是太熟悉,这里仅仅是通过写作,把一些破碎的知识组合起来,不当之处,请读者指正。


[网络子系统]

网络子系统很复杂,但只考虑队列就会相对简单。网络子系统的队列在socket上,当你没有对应socket的时候,收到的包要不要转发,要不直接就drop了,只有你有Socket的时候,包才会留在Socket的buffer中,等待socket的应用程序把数据取走,Socket Buffer的大小可以通过setsockopt()来设置(SO_RCVBUF / SO_SNDBUF),默认值在/proc/sys/net中可以设。还有一些其他协议(比如PRS)也会对进入的包的长度进行控制。但无论如何,你可以在ethtool -S和netstat -s中看到由于这些原因drop掉的包,前者是硬件层的统计,后者是协议栈中的统计。下面是我们一款自产网卡的统计结果:

(ethtool -S的结果是实现相关的,每款网卡都不一样)

如果真的发生sock队列的丢包,ftrace的sock:*事件可以具体跟踪到这个事件。

网卡驱动通过NAPI执行收报操作,NAPI的原理是根据上层的水线控制,在网卡的中断的驱动下,一次收入一定数量的包,然后停止,再等下一次中断。每次收一个包的时候,要分配一个skb作为收报的基础。这个结果就是,网卡polling的时候,skb的数量仅受内存大小的控制(sky分配直接从内存空间分),但当这个包被送入协议栈,同步调度到socket或者转发缓冲的时候,流控机制就会起作用,这样就会反向压制网卡的收报,最后包要不在协议栈一层丢弃,要不就在网卡上丢掉。我们要看这个地方是否发生丢包,要同时看netstat -i以及ethtool中特定网卡自己报上来的网卡自己丢掉的包。

网络子系统没有单独的ftrace tracer,但我们可以通过event跟踪来跟,sock,napi, net三个子系统和网络协议栈相关,其中sock可以跟踪socket超限的事件,napi可以跟踪网卡收报的调度,net可以跟踪收发的动作。


[存储子系统]

存储子系统主要依靠块设备子系统发挥作用,通过iostat我们可以有一个初步的统计结果:

这其中有三类参数,一个是实际的带宽,我们可以用这个来比较物理设备的线速。第二个是队列的平均长度,avgqu-sz。还有一个是队列中每个元素从进入队列到离开队列的平均时间。监控后面两个值基本上就可以获知系统的瓶颈。

和网络子系统不同,存储子系统都有反压(都是同步调用,或者异步会直接让使用者一方丢包,而不是直接在自己一方丢包),所以,我们基本上没有丢包的问题。要支持设备是否开始反压,看%utils参数就可以看出来,设备反压这里这个参数应该是100%。

所以,我们的问题通常不是在线速上,就是在调度(io层自己的调度)上,这个可以用ftrace的blk tracer来跟。echo blk > current_tracer中就可以实施专门针对块设备层的跟踪。这个跟踪器的控制是放在每个块设备上的,你需要到/sys/block/<块设备>/trace/下面对这个块设备的跟踪进行支持(比如enable,filter等),下面是我随便对一台PC的sda的跟踪:

其中那个动作标记的含义从代码上就可以找出来:

        [__BLK_TA_QUEUE]        = {{  "Q", "queue" },      blk_log_generic },                                                                                                                                      
        [__BLK_TA_BACKMERGE]    = {{  "M", "backmerge" },  blk_log_generic },                                                                                                                                      
        [__BLK_TA_FRONTMERGE]   = {{  "F", "frontmerge" }, blk_log_generic },                                                                                                                                      
        [__BLK_TA_GETRQ]        = {{  "G", "getrq" },      blk_log_generic },                                                                                                                                      
        [__BLK_TA_SLEEPRQ]      = {{  "S", "sleeprq" },    blk_log_generic },                                                                                                                                      
        [__BLK_TA_REQUEUE]      = {{  "R", "requeue" },    blk_log_with_error },                                                                                                                                   
        [__BLK_TA_ISSUE]        = {{  "D", "issue" },      blk_log_generic },                                                                                                                                      
        [__BLK_TA_COMPLETE]     = {{  "C", "complete" },   blk_log_with_error },                                                                                                                                   
        [__BLK_TA_PLUG]         = {{  "P", "plug" },       blk_log_plug },                                                                                                                                         
        [__BLK_TA_UNPLUG_IO]    = {{  "U", "unplug_io" },  blk_log_unplug },                                                                                                                                       
        [__BLK_TA_UNPLUG_TIMER] = {{ "UT", "unplug_timer" }, blk_log_unplug },                                                                                                                                     
        [__BLK_TA_INSERT]       = {{  "I", "insert" },     blk_log_generic },                                                                                                                                      
        [__BLK_TA_SPLIT]        = {{  "X", "split" },      blk_log_split },                                                                                                                                        
        [__BLK_TA_BOUNCE]       = {{  "B", "bounce" },     blk_log_generic },                                                                                                                                      
        [__BLK_TA_REMAP]        = {{  "A", "remap" },      blk_log_remap }, 

基本如果你对Linux的IO调度器比较熟悉,很容易就找到整个调度中的时延在什么地方引起的。这里有一个简单的解释,读者可以参考:blktrace User Guide

如果你看了前面这个文档,就知道blk tracer有封装好的工具可以使用,安装blktrace你就可以直接通过blktrace -d /dev/sda跟踪sda的行为,然后用blkparse sda来看结果,比如这样:

live输出的命令是:

blktrace -d /dev/sda -o - | blkparse -i -

具体这个调度模型如何理解,我们在后面的文档中介绍。


[小结]

本文介绍了IO相关的跟踪的基本知识,原来是打算在这一篇中回答有个读者问到的IO子系统调优问题的,但他的问题其实更多关注在虚拟机上,虚拟机的问题和IO是两个相对独立的主题,所以,这里先独立介绍IO有关的跟踪方法,虚拟机我们独立讨论。

编辑于 2016-09-28

文章被以下专栏收录