盘点多起疑难磁盘故障的实战经验

原创
2023/06/07 20:00
阅读数 2.7K

磁盘故障是个很麻烦的问题,影响有:磁盘性能大幅下降、IO hung、磁盘损坏数据丢失。严重的批量磁盘故障,影响的服务器规模少则几十台,多则上千台甚至更多!目前已遇到多起疑难批量磁盘故障,都是新 case,厂商搜集日志没有任何发现,但经过我们的排查最终都查清楚了根本原因,并提供了根治或规避方法。

本文主要记录一下这些疑难磁盘故障的排查总结、开发的工具、经验教训等,相信阅读本文的同学能获得不少有用的知识。

PART

01
磁盘 IO 入门知识
在排查磁盘 IO 问题时,常用的工具有 iostat、pidstat、iotop。pidstat 或iotop 可以查看哪些进程 IO 流量高,iostat 可以查看 IO 使用率、磁盘 IO 带宽、IO 延迟等。如果遇到磁盘硬件问题导致的 IO hung 或磁盘 IO 性能下降,该怎么识别呢?

iostat 在磁盘故障排查中的使用


如下截图是一例磁盘 IO hung 现场,iostat -dmx 1命令看到的。很明显,IO使用率 util达到100%,但是IOPS和IO流量”r/s、w/s、rMB/s、wMB/s”是0
util 这个指标可以理解成是磁盘繁忙程度,俗称 IO 使用率。而 util 100% 说明磁盘在全速传输 IO,但这段时间的 IOPS 是0,这说明一个 IO 都没传输完成。如果有这种现象,大概率就是磁盘有问题。
有人会说 IO 使用率100%也不一定是磁盘有问题呀,也可能是 IO 卡在 IO 运行队列里,有一定道理。IO 使用率的原理到底是什么呢?先看如下示意图:
演示了与 IO 使用率统计有关的 IO 流程。从 IO 请求加入 IO 队列到 IO 请求传输完成这段时间越长,IO 使用率越高。
举个例子:在1s的时间内,一共传输了10个 IO 请求,每个 IO 请求“从 IO 请求加入 IO 队列到 IO 请求传输完成”都花费了2ms。则这1s内的 IO 使用率是 10*2ms/1000ms=2%。IO 使用率的统计原理并不复杂,可以近似成一个计算公式:IO 使用率= 周期内所有 IO 请求从IO请求加入 IO 队列到 IO 请求传输完成 花费的总时间/周期时间
“从 IO 请求加入 IO 队列到 IO 请求传输完成”又有两个阶段组成:IO 请求在 IO 队列花费的时间IO 请求在磁盘驱动花费的时间(前文截图已标出)。因此可以这样推导:IO 使用率= (周期内所有 IO 请求在 IO 队列花费的时间 + 所有 IO 请求在磁盘驱动花费的时间) / 周期时间
而我们线上环境 block 层调度算法是 deadline,大量实践证实 deadline 算法下,IO 请求在 IO 队列花费的时间极短!用 fio 在 sata 盘压测,IO 请求在 IO 队列的花费的时间几乎可以忽略。基于以上测试,IO 使用率的计算规则可以简化成:周期内所有 IO 请求在磁盘驱动花费的时间/周期时间。如果你的环境 block 层调度算法是 cfq 或者 bfq,这个公式就不准确了,此时需要考虑 IO 请求在 IO 队列花费的时间。因此,一般看到 IO 使用率100%但是 IOPS 或 IO 流量很低甚至是0,大概率判定是磁盘有问题。再举一些例子,看下如下截图:

这是 sata 盘16k随机读 iostat 截图:IOPS 只有200,IO 流量只有3M,IO 使用率100%

sata 盘虽然随机读写性能很差,但是这台机器性能太差,判定大概率磁盘有问题。

这是 nvme 盘,业务 IO 读写时 iostat 截图:IOPS 只有3000,IO 流量只有20M,IO 使用率70%。正常情况,nvme 盘在 4K 随机读写情况下,IOPS 能达到百万,IO 流量 GB/S 级别。但是这台机器 IOPS 只有3000,IO 流量20M/s,但 IO 使用率竟然达到70%,这个太异常了,判定大概率是磁盘有问题。


 blktrace 在磁盘故障排查中的使用


前文是用 iostat 判定磁盘硬件问题导致的 IO hung 或者磁盘性能下降的经验总结。其实并不绝对准确,在某些极端场景需要用更专业的工具抓取 IO 传输时的数据,判定确实是磁盘问题导致的 IO hung 或者磁盘性能下降,此时一般使用 blktrace。
blktrace 能抓取每个 IO 请求在 IO 传输过程每个阶段花费的时间,这些阶段包括:
1:即将生成 IO 请求,字母 Q 表示
2:生成 IO 请求,字母 G 表示
3:IO 请求加入 IO 队列,字母 I 表示
4:IO 请求从 IO 队列剔除并派发给磁盘驱动,字母 D 表示
5:IO 请求传输完成,字母 C 表示。
与 IO 使用率有关的是后3个阶段,即“ IO 请求加入 IO 队列”、“IO 请求派发给磁盘驱动”、“IO 请求传输完成”,这三个阶段前文也介绍过,简称分别是 I、D、C。如下所示,每个阶段的简称也标注出来了。

有了这些预备知识,就可以用起来 blktrace 了,使用方法可以简化成:

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

sda是 IO 使用率很高但 IO hung 或者 IOPS 很低的磁盘。该命令采样 sda 盘每个 IO 请求在 IO 传输过程各个环节的耗时并打印出来。

blktrace 打印的演示数据如下:


“Sequeue Number” 那一列是序列号,“Time Stamp” 那一列是时间戳,“PID” 那一列是抓取当时IO数据时的进程 PID,“Event” 那一列是 IO 传输事件(就是前文提到的I、D、C),“Start block+ number of blocks” 那一列是IO传输的起始扇区号(查证不是起始物理块号)+扇区数。Start block+ number of blocks” 这一列的数据标识了 IO 的唯一性,简单说用来区分不同的 IO。

blktrace 更详细的用法见 http://linuxperf.com/?p=161

下图是使用该方法抓到的某款 ssd 磁盘的 IO 传输过程的数据:

我们重点关注的是3列数据,时间戳(第4列)、IO 事件 IDC 等(第6列)、IO 请求读写的起始扇区地址+传输扇区数(第8列)。起始扇区地址是7223836744 的这个 IO 请求,IO 请求派发给磁盘驱动(D)的时间点是44.007497905,IO 请求传输完成(C)的时间点是44.012554366,这两个时间点一相减就是该 IO 请求在磁盘驱动层的传输耗时,这两个时间差达到5ms,正常情况是 us 级别。同时,iostat 看到的 IOPS 并不高。基于以上测试数据,判定 IO 请求在磁盘驱动层的传输耗时偏长,大概率就是磁盘有问题了。


systemtap 在磁盘故障排查中的使用


blktrace 有个很大的缺点,打印的信息太乱太杂,尤其是针对偶发的磁盘 IO 性能下降问题。比如之前查过的一例 IO hung 几秒,几天甚至几周出现一次,你就要一直执行 blktrace 采集磁盘日志,这日志量将会相当庞大,对排查问题造成了很大阻碍。我只想要 IO hung 几秒那个时间的 IO 请求各个阶段的耗时而已!怎么解决?如果你懂 systemtap 或者 ebpf,这个问题就很好解决了。

ebpf 编程限制太多,systemtap 相对更简单。如下是我用 systemtap 针对该问题编写的一个脚本,命名为 blktrace-systemtap.stp。

global  id_buf; global  dc_buf; global id_buf_name; global dc_buf_name; //IO请求插入IO队列,记录时间点 probe kernel.trace("block_rq_insert") {    if($rq != 0 && kernel_string($rq->rq_disk->disk_name) == "sda"){//抓抓取sda盘的IO数据        id_buf[$rq] = gettimeofday_ms();         id_buf_name[$rq] = execname();     }}//IO请求从IO队列剔除并派发给磁盘驱动,记录时间点 probe kernel.trace("block_rq_issue") {    if(id_buf[$rq] != 0){        dc_buf[$rq] = gettimeofday_ms();         dc_buf_name[$rq] = execname();    }}//IO请求传输完成,计算与IO请求插入IO队列的时间点的时间差,如果时间差太大就打印IO请求在传输过程各阶段的耗时probe kernel.trace("block_rq_complete"){    if($rq!= 0 && id_buf[$rq]!=0 && dc_buf[$rq] != 0){        dx = gettimeofday_ms() - id_buf[$rq];        if(dx > 10)//大于10ms则打印IO各阶段的耗时                                                                                                                                                                                                   {            printf("%s  I-D:%dms %s  D-C:%dms %s\n",kernel_string($rq->rq_disk->disk_name),dc_buf[$rq]-id_buf[$rq],id_buf_name[$rq],gettimeofday_ms()-dc_buf[$rq],dc_buf_name[$rq]);        }    }    if(dc_buf[$rq]){        delete dc_buf[$rq];        delete dc_buf_name[$rq];    }    if(id_buf[$rq]){        delete id_buf[$rq];        delete id_buf_name[$rq];    }}

该脚本的作用是:如果要是某个 IO 请求“从 IO 请求插入 IO 队列到 IO 请求传输完成”耗时大于10ms,则打印 IO 请求在 IO 队列和 IO 请求在磁盘驱动层的耗时。if(dx > 10) 里的10可以按照实际情况增大或减小。打印情况如下:

sda  I-D:0ms kworker/u898:0  D-C:100ms kworker/u898:0
I-D 表示 IO 请求在IO队列的耗时,D-C 表示 IO 请求在磁盘驱动层的耗时。用这个脚本可以过滤掉繁琐的耗时正常的 IO,只打印异常 IO 的耗时。这只是个最简单的脚本,在此基础上还可以抓取异常时的内存信息、软中断耗时。还可以把 IO 请求在磁盘驱动的耗时 D-C 进一步细化,细化成 IO 请求在磁盘驱动的耗时+IO 请求在磁盘硬件的传输耗时,这个思路在解决一个批量磁盘性能下降 case 时就用到了。
注意:在排查 IO 性能差时,经常用到的一个术语——DC 耗时,这个就是 IO 请求在磁盘驱动层的处理耗时,下边也会多次用到。
关于磁盘硬件原因导致 IO hung 或者磁盘性能下降的排查思路就介绍到这里,下边正式介绍疑难批量磁盘故障的排查经验。
PART

02
6例疑难批量磁盘故障 



案例一:sata 盘每21分钟 IO hung 7s


这个批量磁盘故障出现概率很高,每21分钟出现一次,isotat 看到的现场是这样:

在将近7s左右的时间内,一个 IO 都没传输完成,并且 IO 使用率100%。这个 case 现在看是非常简单,但是2020年初次遇到时,还是有点束手无策,这里按照当时的排查过程简单总结下。
一般情况,当 iostat 看到 IO 使用率很高,都要先用 iotop 或 pidstat -d 1 看下哪些进程 IO 流量高,这里用的 iotop,如下所示:

可以看到有 IO 读写的进程,IO 流量是极低的,不太可能是这些进程导致的 IO 使用率100%。
继续,发生 IO hung 再 ps 看下系统的 D 状态的进程(不可中断休眠的进程),然后 cat /进程PID/stack 看下栈回溯,大部分 D 进程都是如下:
[<ffffffff99d69f19>] schedule+0x29/0x70 [<ffffffff99d67a21>] schedule_timeout+0x221/0x2d0 [<ffffffff99d695ed>] io_schedule_timeout+0xad/0x130 [<ffffffff99d69688>] io_schedule+0x18/0x20 [<ffffffff99d68071>] bit_wait_io+0x11/0x50 [<ffffffff99d67b97>] __wait_on_bit+0x67/0x90 [<ffffffff997b6b81>] wait_on_page_bit+0x81/0xa0 [<ffffffff997b6cb1>] __filemap_fdatawait_range+0x111/0x190 [<ffffffff997b6d44>] filemap_fdatawait_range+0x14/0x30 [<ffffffff997b91a6>] filemap_write_and_wait_range+0x56/0x90 [<ffffffffc067e5aa>] ext4_sync_file+0xba/0x320 [ext4] [<ffffffff99877367>] do_fsync+0x67/0xb0 [<ffffffff99877650>] SyS_fsync+0x10/0x20

这是 ext4 文件系统 write 后等待传输完成,进程保持这种状态7s后,恢复正常。会不会是卡在内核某处导致的?这个文件系统 write 过程,大体可以分配两个流程:

SyS_fsync->do_fsync->ext4_sync_file->filemap_write_and_wait_range->__filemap_fdatawrite_range->__filemap_fdatawrite_range->do_writepages->ext4_writepages->ext4_io_submit->submit_bio 把 IO 请求派发给 block 层。然后回到 filemap_write_and_wait_range 函数,执行__filemap_fdatawait_range->wait_on_page_writeback->wait_on_page_bit 休眠等待 IO 请求传输完成。

而执行 submit_bio 派发 IO 到 block 层也是一个关键,因为正是这里把 IO 派发给磁盘驱动。派发给磁盘驱动的函数流程是:

submit_bio->generic_make_request->blk_queue_bio->__blk_run_queue->__blk_run_queue_uncond->scsi_request_fn

或者 ext4_writepages->blk_finish_plug->blk_flush_plug_list->queue_unplugged->__blk_run_queue->__blk_run_queue_uncond->scsi_request_fn最终都调用 scsi_request_fn 函数

等 IO 请求传输完成,执行中断回调函数handle_irq->......__handle_irq_event_percpu->megasas_isr_fusion->complete_cmd_fusion->scsi_done->blk_complete_request

然后在 blk_complete_request 函数往后流程会触发软中断,执行 do_softirq->...->blk_done_softirq->scsi_softirq_done->.....bio_endio->ext4_end_bio->ext4_finish_bio->end_page_writeback 唤醒之前在wait_on_page_bit 休眠的进程。

这基本就是 IO 传输的整个流程了,用 systemtap 捕捉关键函数,打印进程名字、进程 PID、时间戳、IO 传输有关的 page 等信息,然后看到底 IO hung 的7s是卡在哪里?我选择的函数有:IO 传输传输发起函数 submit_bio,发起磁盘数据传输函数 scsi_request_fn,发送 SCSI 命令给磁盘控制器函数scsi_dispatch_cmd,进程发送 IO 请求后休眠函数wait_on_page_bit,磁盘数据传输完成后执行的中断回调函数blk_complete_request,唤醒等待文件数据传输完成的进程的 end_page_writeback 函数。添加的调试信息演示如下:

probe kernel.function("wait_on_page_bit"){     printf("wait_on_page_bit %s pid:%d  page:%p bit_nr:%d  cpu:%d\n",execname(),tid(),$page,$bit_nr,cpu())}}
这些调试信息不能多加也不能少加,多了容易刷屏,少了容易漏掉关键信息,可以用进程名字、进程 ID、中断名称限制部分打印,有一定调试技巧。有些函数的返回值需要打印出来,有些函数的形参也需要打印出来。最终打印如下
[10:08:50]submit_bio net_agent  pid:298171  bio:0xffff9777cd34e600 queue:0xffff9770019d9330 cpu:10[10:08:50]scsi_request_fn net_agent  pid:298171  rq queue:0xffff9770019d9330  cpu:10[10:08:50]scsi_dispatch_cmd net_agent pid:298171  return:0  cpu:10[10:08:50]wait_on_page_bit  net_agent  pid:298171  page:0xfffff2f917d96700 bit_nr:13  cpu:10...................[10:08:57]  blk_complete_request swapper/25 pid:0 req->q:0xffff9770019d9330  req:0xffff977713793900 cpu:25[10:08:57]  end_page_writeback swapper/10 pid:0  page:0xfffff2f917d96700  cpu:10
可以发现 net_agent 进程在10:08:50 发起了 page:0xfffff2f917d96700 对应的 IO 请求数据的传输,然后休眠。但是在10:08:57才产生中断,然后唤醒 net_agent 进程。这种现象果断怀疑是 IO hung 的7s是卡在磁盘驱动里!于是等下次再出现 IO hung 7s,执行 cat /proc/interrupts 查看这7s的磁盘中断次数,竟然一次都没有!
把以上排查信息反馈给服务器厂家后,很快找到了根本原因:磁盘阵列卡有关的 IDRAC 某些特定版本中,IDRAC 引用了一个指令,IDRAC 每隔 22 分钟会向 PERC9 发送指令来获取硬盘 foreign 信息,这时 PERC9 卡会停止 IO 操作几秒。
解决方法是升级 IDRAC 固件,不用重启服务器。还好最后查清楚了根本原因,否则很影响业务使用,毕竟时不时 IO hung 7s 太影响业务。


案例二:sata 盘每周六11点 IO hung


这个故障的现象是周六11点开始的几分钟,时不时发生 IO hung。现象跟第一个故障很像,iostat 看到的IO使用率100%,pidstat -d 1 看到有进程时不时有每秒几十M的 IO 流量,不算大。这个就比较麻烦了了,第一个故障是 iostat 看到的 IO 使用率100%但 iotop 或 pidstat 看到的 IO 流量是0,因此可以大概率怀疑是磁盘有问题。现在不能确定 IO 使用率100%是 pidstat 看到的哪些进程导致的?
iostat 只能看到磁盘 IO 使用率、IO 流量、IOPS 等的,但看不到哪些进程传输的这些 IO 流量。pidstat 或 iotop 统计的 IO 流量并不一定都直接派发到磁盘,比如某个进程只把数据写入 pagecache 就返回,pidstat 统计到的这波流量并不会直接写入磁盘,需要等脏页回写进程把这波脏页刷入磁盘。还有我发现有时 jbd2 进程刷 ext4 文件系统元数据到磁盘,pidstat 并没有统计到!基于以上问题,决定用 epbf 编写一个脚本,在 iostat 基础上,统计每个进程派发到磁盘的 IO 流量,是真正派发给磁盘传输的 IO 流量,需要打点内核函数如下:
//io请求传输完成,统计IO流量执行到 b.attach_kprobe(event="blk_account_io_completion",fn_name="trace_blk_account_io_completion") //io请求传输完成最后,统计IO使用率执行到 b.attach_kprobe(event="blk_account_io_done",fn_name="trace_blk_account_io_done")//io请求加入队列时执行 b.attach_kprobe(event="blk_account_io_start",fn_name="trace_blk_account_io_start")//统计IO使用率执行到 b.attach_kprobe(event="part_round_stats",fn_name="trace_part_round_stats")//发生IO请求合并时执行到 b.attach_kprobe(event="elv_merge_requests",fn_name="trace_elv_merge_requests")//把io请求加入IO队列时令iflight加1 b.attach_kprobe(event="part_dec_in_flight",fn_name="trace_part_dec_in_flight")//io请求传输完成时令iflight减1 b.attach_kprobe(event="part_inc_in_flight",fn_name="trace_part_inc_in_flight")
ebpf 编程限制太多,static 型函数不能打点,不能调用 C 库函数,不能使用循环语句。编写这个脚本做了很多妥协,源码800有多行,这里不再贴了,参考了 bcc 提供的 demo /usr/share/bcc/tools/biotop。
这个 ebpf 脚本我命名为 iostat_enhence,运行效果如下:

打印进程信息、它读写的磁盘、该进程的 IO 请求在 IO 队列+磁盘驱动层的耗时、读写流量、父进程及容器信息、每个磁盘分区的IO使用率、总的 IO 使用率。并且支持超过 IO 使用率某个阀值再打印以上信息,可以过滤掉干扰打印。
在出问题的机器上跑这个 ebpf 脚本,发现周六11点起始的几分钟,发生 IO hung 时真正派发到磁盘的 IO 流量很低。于是就怀疑大概率又是磁盘有问题。把这些信息反馈给厂家后,厂家提供了工具,抓取 IO hung 时的磁盘 sar 日志,最终定位出了根本原因。
根本原因是:在每周六11点磁盘阵列卡有关的 RAID 卡都会进行一次 CC(consistentcy check)校验,占用30% RAID 卡性能,客户端通过关闭 CC 校验测试,IO hang 问题不复现,判断出现 IO hang 问题与 RAID 卡资源被过度占用有关。为进一步分析 RAID 卡资源占用原因,抓取 RAID 卡 termlog 进行分析,日志中发现 RAID 卡平均每一分钟会收到多个错误的 BMC 指令并打印出来,频繁的 BMC 与 RAID 卡之间的交互,也会消耗 RAID 卡资源,客户端分别测试关闭 RAID 卡监控和升级降低与 RAID 卡交互频率的 BMC 版本后,IO hang 问题不复现。周六出现 IO hang 问题与 BMC RAID 卡交互、CC 校验、业务 IO 读写三部分过度占用 RAID 资源有关,结合日志分析和现场测试, 可以通过降低 BMC 与 RAID 卡交互频率来改善这个问题。
解决方法:升级 BMC 固件,降低 BMC 与 RAID 卡交互频率。


案例三:使用 perccli 采集阵列卡信息导致 IO hung 几秒

这个批量故障是复现概率很低,偶尔触发一次,并且触发时机没有任何规律。 受此影响,不少中间件的业务 IO hung 几秒,IO hung 时 IO 流量很低,IO 使用率100%。

发生 IO hung 的服务器都是同一个型号,这是个很重要的信息。有了前两次批量磁盘故障经验,再加上是同一个型号的机器,理所应当怀疑是磁盘或者阵列卡硬件有问题。但搜集了大概5台服务器的磁盘和阵列卡等日志,厂商没有任何发现。并且厂商反馈这个型号的服务器只有我们遇到了 IO hung 问题!难道与我们业务模型有关?

由于该问题触发概率很低,一直没有抓到现场,并且厂商还怀疑 IO hung 与内核有关系,不一定是 IO 卡在磁盘驱动导致的!本来可以依靠监控,但是物理机的监控做不到秒级,而这个 IO hung 也就持续几秒,很可能抓不到完整数据!

没办法,找了几台发生过 IO hung 的服务器,一直执行 iostat -dmx 1 每1s抓一次 IO 数据,期待运气好能抓到一次现场。iostat 打印的 await、r_await、w_await 可以反映 IO 平均延迟,如果抓到了问题现场,这3个参数应该会很大。但这些延迟数据包含了 IO 请求在 IO 队列的时间 +IO 请求在磁盘驱动的时间,如果厂商判定是 IO 请求在 IO 队列卡主了导致 IO hung,是内核导致的 IO hung,则有必要证明 IO hung 发生在磁盘驱动,以便帮助厂家定位问题原因!

在这种情况下,可以用 blktrace 一直抓取磁盘日志,但是日志量太大,因为没人能预料下次啥时间发生。如果一个星期后发生,blktrace 采集的日志量估计有上百G,难以分析。能否做一个类似 blktrace 的工具,但是只在 IO 请求在 IO 队列+磁盘驱动花费时间过长时再打印出 IO 耗时数据呢?可以的,于是就用 systemtap 编写了前文提到的脚本 blktrace-systemtap.stp:

global  id_buf;global  dc_buf;global id_buf_name;global dc_buf_name;//IO请求插入IO队列,记录时间点probe kernel.trace("block_rq_insert"){    if($rq != 0 && kernel_string($rq->rq_disk->disk_name) == "sda"){//抓抓取sda盘的IO数据        id_buf[$rq] = gettimeofday_ms();        id_buf_name[$rq] = execname();    }}//IO请求从IO队列剔除并派发给磁盘驱动,记录时间点probe kernel.trace("block_rq_issue"){    if(id_buf[$rq] != 0){        dc_buf[$rq] = gettimeofday_ms();        dc_buf_name[$rq] = execname();    }}//IO请求传输完成,计算与IO请求插入IO队列的时间点的时间差,如果时间差太大就打印IO请求在传输过程各阶段的耗时probe kernel.trace("block_rq_complete"){    if($rq!= 0 && id_buf[$rq]!=0 && dc_buf[$rq] != 0){        dx = gettimeofday_ms() - id_buf[$rq];        if(dx > 10)//大于10ms则打印IO各阶段的耗时                                                                                                                                                                                                   {            printf("%s  I-D:%dms %s  D-C:%dms %s\n",kernel_string($rq->rq_disk->disk_name),dc_buf[$rq]-id_buf[$rq],id_buf_name[$rq],gettimeofday_ms()-dc_buf[$rq],dc_buf_name[$rq]);        }    }    if(dc_buf[$rq]){        delete dc_buf[$rq];        delete dc_buf_name[$rq];    }    if(id_buf[$rq]){        delete id_buf[$rq];        delete id_buf_name[$rq];    }}
这个脚本是设定如果 IO 请求的延迟(在 IO 队列的时间+在磁盘驱动的耗时)大于设定的阀值,则打印该 IO 请求在 IO 队列的时间和在磁盘驱动的耗时。这个脚本设定的阀值是10ms,需要根据实际情况调整,目的是过滤掉掉正常的 IO。
另外,还需要考虑一点,有没有可能是 IO 传输时内存不足、内存碎片、磁盘 IO 或者网卡软中断耗时长导致 IO 延迟呢?于是需要实时执行 free -h 或 cat /proc/meminfo、cat /proc/buddyinfo 查看系统内存信息,并且编写 systemtap 脚本监控软中断耗时:
global softirq_name;probe begin{    softirq_name[0] = "tasklet_hi_action"    softirq_name[1] = "run_timer_softirq"    softirq_name[2] = "net_tx_action"    softirq_name[3] = "net_rx_action"    softirq_name[4] = "blk_done_softirq"    softirq_name[5] = "irq_poll_softirq"    softirq_name[6] = "tasklet_action"    softirq_name[7] = "run_rebalance_domains"    softirq_name[8] = "HRTIMER_SOFTIRQ"    softirq_name[9] = "rcu_process_callbacks"    softirq_name[10] = "rcu_process_callbacks"}probe kernel.trace("softirq_entry"){   softirq_time[cpu()] = gettimeofday_us();//每个cpu都有自己的软中断,所以要按照cpu分开}probe kernel.trace("softirq_exit"){   if(softirq_time[cpu()]){       dx = gettimeofday_us() - softirq_time[cpu()];       if(dx > 5*1000){//单次软中断耗时大于5ms则打印           printf("%s %s dx:%dms cpu:%d\n",ppfunc(),softirq_name[$vec_nr],dx,cpu())       }       cpu_softirq_time[cpu()] += dx;   }   delete softirq_time[cpu()]}//定时1s打印一次每个软中断的总耗时probe timer.sec(1){    foreach (i in cpu_softirq_time){        if(cpu_softirq_time[i] > 5*1000)            printf("cpu:%d %dms\n",i,cpu_softirq_time[i]/1000);    }    delete cpu_softirq_time}

大概1~2周后,终于在两台机器上凌晨1点抓到一次 IO hung 现场:

同时,用 systemtap 脚本抓到的数据显示,I-D 耗时 0ms,D-C 耗时大于 1000ms。这充分证实是 IO 请求在磁盘驱动耗时太长导致的 IO hung。并且这段时间,系统内存充足,软中断耗时正常,监控显示硬中断也没波动。
基于以上排查,基本确定是 IO hung 与磁盘驱动有关。并且抓到的这次 IO hung 都在凌晨1点,像是定时任务。这个任务应该与磁盘有关系,与同事沟通发现这个时间点是在磁盘巡检,用 perccli 获取阵列卡信息,命令是 perccli64 /c0 show all。我在出问题的机器上执行 perccli64 /c0 show all,立即复现 IO hung 5s 左右。问题原因终于找到了!
为什么执行 perccli64 /c0 show all 获取阵列卡信息会导致 IO hung?服务器厂家表示没遇到过此类问题,并通过进一步测试发现与阵列卡厂商的设计有关。短时间内无法解决,只能使用 Megacli 工具规避。注意,strocli 工具也有同样的问题!
经验教训:当执行一些跟硬件(磁盘、网卡等)有关命令时,一定要做灰度测试,并且要跟厂家确认好有没有问题,否则就可能出现该案例的 IO hung 问题。

案例四:长时间磁盘读写后 ssd 盘性能下降明显


这个问题是编译业务发现的,编译进行一段时间后,就会出现IO性能明显下降问题,磁盘是 ssd,带了阵列卡,性能很不错。

正常情况 fio 压测 isotat 看到的如下:

出现性能问题的机器,fio 压测 iostat 看到的如下:

可以发现,性能下跌的非常严重。使用 blktrace 或 blktrace-systemtap 等工具很快定位到 IO 性能差的原因是 IO 传输过程的 D-C 环节耗时长(DC耗时大概几百ms),就是因为在磁盘驱动耗时长导致的IO性能差!按照经验,这种问题大概率就是磁盘或者阵列卡硬件有问题!

但是收集磁盘和阵列卡日志后,厂家也是排查不出问题原因,其他客户没遇到类似问题。但神奇的是重启服务器后就能恢复正常,难道又与我们的业务有关系?在排查了很长时间后依然找不到原因。

此时有个想法,前文抓到 DC 耗时几百 ms,一定能说明这个时间是消耗在磁盘或者阵列卡硬件吗?不能!看下如下示意图,把 IO 请求在磁盘驱动层的处理进行细化:

IO 请求在磁盘驱动层的耗时又可以细化为 IO 请求在驱动的耗时+IO 请求在磁盘硬件的处理耗时。本次的磁盘故障 DC 耗时长是哪个环节呢?此时就需要在 blktrace-systemtap.stp 的基础上,再捕捉 IO 请求派发给磁盘驱动的函数,然后就可以计算 IO 请求在驱动的耗时 和  IO 请求在磁盘硬件的处理耗时。于是编写如下脚本:

global  queue_buf;global  dispatch_buf;global queue_buf_name;global dispatch_buf_name;global driver_buf;//IO请求插入IO队列,记录时间点probe kernel.trace("block_rq_insert")  {    if($rq && $rq->rq_disk){        queue_buf[$rq] = gettimeofday_ms();        queue_buf_name[$rq] = execname();    }}/*//用 scsi_dispatch_cmd代替block_rq_issue trace函数计算IO请求在IO队列的耗时probe kernel.trace("block_rq_issue")  {    if(queue_buf[$rq] != 0){        dispatch_buf[$rq] = gettimeofday_ms();        dispatch_buf_name[$rq] = execname();    }}*///SCSI 驱动是执行 scsi_dispatch_cmd函数把IO请求派发给磁盘驱动probe kernel.function("scsi_dispatch_cmd")//执行scsi_dispatch_cmd函数返回记录时间点{    if(queue_buf[$cmd->request] != 0){        dispatch_buf[$cmd->request] = gettimeofday_ms();        dispatch_buf_name[$cmd->request] = execname();    }}probe kernel.function("scsi_dispatch_cmd").return//从scsi_dispatch_cmd函数计返回记录时间点{    if(dispatch_buf[$cmd->request] != 0){        driver_buf[$cmd->request] = gettimeofday_ms();    }}//IO请求传输完成,计算与IO请求插入IO队列的时间点的时间差,如果时间差太大就打印IO请求在传输过程各阶段的耗时probe kernel.trace("block_rq_complete"){    if($rq!= 0 && (queue_buf[$rq]!=0) && (dispatch_buf[$rq] != 0) && (driver_buf[$rq] !=0)){        dx = gettimeofday_ms() - queue_buf[$rq];        //DC耗时大于2ms则打印IO信息        if(dx >= 2){            printf("%s io all:%dms in-queue:%dms %s  in-driver:%d  in-disk:%dms %s\n",kernel_string($rq->rq_disk->disk_name),dx,dispatch_buf[$rq]-queue_buf[$rq],queue_buf_name[$rq],driver_buf[$rq]-dispatch_buf[$rq],gettimeofday_ms()-driver_buf[$rq],dispatch_buf_name[$rq]);        }    }    if(dispatch_buf[$rq])        delete dispatch_buf[$rq];    if(queue_buf[$rq])        delete queue_buf[$rq];    if(driver_buf[$rq])        delete driver_buf[$rq];    if(queue_buf_name[$rq] != "")        delete queue_buf_name[$rq];    if(dispatch_buf_name[$rq] != "")        delete dispatch_buf_name[$rq];}
SCSI 驱动是执行 scsi_dispatch_cmd 函数把 IO 请求派发给磁盘驱动,本脚本记录执行该函数时的时间点,然后计算该函数返回的时间点,二者时间差可以认为是 IO 请求在驱动层的耗时。而从 scsi_dispatch_cmd 函数到 IO请求传输完成,可以认为是 IO 请求在磁盘硬件的处理耗时。
运行该脚本后打印如下:
sdb io all:2ms in-queue:0ms fio in-driver:2ms in-disk:1ms
“in-driver:2ms in-disk:1ms”说明 IO 请求在驱动的耗时 2ms,在磁盘硬件的耗时1ms。难道 IO 性能差跟内核的磁盘驱动有关系?在驱动的耗时就是在 scsi_dispatch_cmd 函数的耗时,用 systemtap 一步步计算跟踪函数的执行流程,肯定能找到是哪里导致的耗时长。
不过此时有个捷径,在 fio 压测时,用 perf top -a -g -F 99 看到一个奇怪的热点函数,如下:

这是在IO派发,最后是执行scsi_dma_map->intel_alloc_iova->alloc_iova_fast->alloc_iova 建立 DMA 映射、分配 iommu 需要的 iova 虚拟地址。入口函数正好有 scsi_dispatch_cmd!于是在前文的脚本中添加如下代码,用于计算在 alloc_iova 函数的耗时。

global dma_dx;probe kernel.function("alloc_iova"){        dma_dx[tid()] = gettimeofday_us();}probe kernel.function("alloc_iova").return{    if(dma_dx[tid()] != 0){        dx = gettimeofday_us() - dma_dx[tid()];    if(dx > 1000){//阀值500打印太頻繁        printf("%s %d size:%d dx:%dus\n",execname(),tid(),$size,dx);        //print_backtrace();    }    delete dma_dx[tid()];    }}
然后再次启动 fio 压测,打印如下:
fio 36553 size:64  dx:1292ussdb io all:2ms in-queue:0ms fio in-driver:2ms in-disk:1ms
果然是在 alloc_iova 函数分配 iommu 需要 iova 虚拟地址耗时长。
此时不禁有了疑问,找了其他厂家的服务器,发现磁盘IO传输数据时并没有执行 scsi_dma_map->intel_alloc_iova->alloc_iova_fast->alloc_iova 函数。怀疑可能阵列卡或者磁盘型号不同,建立 DMA 映射就会走不同的流程。出问题的服务器厂家更换了另一个型号的阵列卡,长时间跑编译业务就没有再出现IO性能下降了!用 systemtap 测试,这个型号的阵列卡, IO 传输时就没有执行 scsi_dma_map->intel_alloc_iova->alloc_iova_fast->alloc_iova 函数!
同时,iommu 在 centos7.6 的内核默认并没有开启,在命令行参数添加“intel_iommu=on iommu=pt”就可以打开 iommu,出问题的机器如果都有这些命令行参数。如果命令行参数去掉“intel_iommu=on iommu=pt”,重启后长时间跑编译业务同样也不再复现 IO 性能下降问题了!
iommu 在虚拟机、rdma 等 DMA 数据透传场景都是需要用到,显然出问题型号的阵列卡不太兼容 iommu。这个故障有两个解决方法,关闭 iommu 或者更换 IO 数据传输时不使用 iommu 的阵列卡。还好业务场景评估后不需要使用 iommu,因此关闭 iommu 规避该故障最简单。
最后还有一个问题,为什么IO派发时建立 DMA 映射过程,执行 scsi_dma_map->intel_alloc_iova->alloc_iova_fast->alloc_iova分配 iommu 需要的虚拟地址会耗时长而导致磁盘性能下降呢?其实,iommu 在 DMA 数据传输时并不是必需的,完全可以直接分配一片连续的物理内存,建立 DMA 映射,然后直接跟磁盘设备通过 DMA 传输数据。
我的理解是:使用 iommu 后,分配 iova 虚拟地址,然后 iova 虚拟地址与物理内存构成页表页目录映射。DMA 是先访问 iova 虚拟地址,通过 iommu 的翻译,得到映射的物理地址,然后把这些数据搬运到磁盘的 DMA 有关的寄存器,或者从 DMA 有关的寄存器搬运数据到物理内存。
相当于有了 iommu,DMA 不再直接访问物理内存,而是通过 iova 间接访问物理内存。这样可以解决 DMA 数据传输时,一定要物理内存连续的问题。因为有可能连续的分配不出来!比如,服务器长时间运行后,内存碎片化严重,明明有几十G的空闲内存,但是1M的连续物理内存竟然分配不出来!cat /proc/buddyinfo 一看发现大部分内存 page 都集中在2^0、2^1、2^2这些内存 page 里。iommu 有点类似 vmalloc,连续物理内存不足,用虚拟地址映射来凑。
最后,为什么会在 alloc_iova 函数耗时很长呢,与里边执行的 __alloc_and_insert_iova_range 函数有关? 简单看下源码:
static int __alloc_and_insert_iova_range(struct iova_domain *iovad,        unsigned long size, unsigned long limit_pfn,            struct iova *new, bool size_aligned){    struct rb_node *prev, *curr = NULL;    unsigned long flags;    unsigned long saved_pfn;    unsigned int pad_size = 0;
spin_lock_irqsave(&iovad->iova_rbtree_lock, flags); saved_pfn = limit_pfn; //获取iovad->rbroot红黑树中的一个rb_node节点 curr = __get_cached_rbnode(iovad, &limit_pfn); prev = curr; //查找一片连续的iova地址空间,耗时长就是在这里 while (curr) { struct iova *curr_iova = container_of(curr, struct iova, node);
if (limit_pfn <= curr_iova->pfn_lo) { goto move_left; } else if (limit_pfn > curr_iova->pfn_hi) { if (size_aligned) pad_size = iova_get_pad_size(size, limit_pfn); if ((curr_iova->pfn_hi + size + pad_size) < limit_pfn) break; /* found a free slot */ } limit_pfn = curr_iova->pfn_lo;move_left: prev = curr; //在这里耗时长 curr = rb_prev(curr); } .................. //这里给 new->pfn_lo 和 new->pfn_hi 赋值 new->pfn_lo = limit_pfn - (size + pad_size); new->pfn_hi = new->pfn_lo + size - 1; ..............}
__alloc_and_insert_iova_range() 耗时长,是在查找一片连续 iova 磁盘设备 IO 虚拟地址空间。这里边牵涉到红黑树遍历,在 rb_prev() 函数耗时也很长。因为找不到一片连续的 iova 磁盘设备 IO 虚拟地址空间,就一直查找,导致耗时了几个 ms。估计是此时 iova 虚拟地址碎片化比较严重导致的!
教训:新引入的硬件,必需要和业务做兼容性测试,否则就可能出现该案例的 IO 问题。

案例五:使用 sas3ircu 采集阵列卡信息概率性导致磁盘数据损坏、服务器宕机


这个故障是排查过的复现概率最奇怪的 case,有时几周复现一次,有时几个月不再复现。现象有:服务器宕机重启、服务器宕机后进入救援模式、磁盘文件系统损坏(无法修复)。发生问题的服务器都是一个型号的。
最初是存储同事反馈过来的,服务器磁盘空间使用率100%告警,执行 df 命令发现 /dev/sda3 分区的大小是32Z,used 是 32Z,sda3 磁盘空间使用率100%。这怎么可能?正常情况这个磁盘也就几十G。怀疑是 sda3 磁盘分区 ext4 文件系统发生损坏了!dumpe2fs /dev/sda3 后打印

dumpe2fs 1.42.9 (28-Dec-2013)

dumpe2fs: Bad magic number in super-block while trying to open /dev/sda3

Couldn't find valid filesystem superblock.
果然是 sda3 盘的 ext4 文件系统发生了损坏,内核也有一些 ext4 文件系统 err 日志!ext4 文件系统损坏是磁盘硬件有问题导致的?还是有程序裸写磁盘导致的?或者是文件系统 bug 导致的?此时又发生了一次类似的 sda3 分区 ext4 文件系统数据损坏问题,把这两例的 sda3 分区前 0-8K 数据导出来,看下有什么规律没。如下所示:二者前8k数据基本是相等的,只有截图红色位置位置的数据不同。

并且这前 8K 的数据大部分不是0,正常情况 ext4 文件系统的前 8K 数据大部分是0,如下是正常的 ext4 文件系统前8K数据,0xEF53 是 ext4 文件系统的 magic。

由于发生数据损坏的文件系统前8K数据很有规律,倾向于怀疑是有进程裸写磁盘 sda3 分区导致的 ext4 文件系统数据损坏。有没有可能有进程在向 sda2 分区写数据时越界到了 sda3 分区呢?该怎么抓到这种行为呢?编写如下 systemtap 脚本:
probe kernel.function("generic_make_request"){      if(($bio->bi_bdev->bd_dev == 0x800003 || $bio->bi_bdev->bd_dev == 0x800000 || $bio->bi_bdev->bd_dev == 0x800001|| ($bio->bi_bdev->bd_dev==0x800002 && ($bio->bi_sector * 512 + $bio->bi_size > 0xC60000000))))    {        printf("%s %s_%d %s_%d %s sector:0x%x size:%d dev:0x%x rw:0x%x ",ctime(),execname(),tid(),kernel_string(task_current()->parent->comm),task_current()->parent->pid,kernel_string($bio->bi_bdev->bd_disk->disk_name),$bio->bi_sector,$bio->bi_size,$bio->bi_bdev->bd_dev,$bio->bi_rw); 
//jbd2/sda3-8进程传输IO时,$bio->bi_io_vec[0]->bv_page->mapping->host->i_dentry->first 是NULL,kworker/u896:1 有时也会这样 if($bio->bi_io_vec && $bio->bi_io_vec[0]->bv_page && $bio->bi_io_vec[0]->bv_page->mapping && (($bio->bi_io_vec[0]->bv_page->mapping & 0x1) == 0)){ if($bio->bi_io_vec[0]->bv_page->mapping->host && $bio->bi_io_vec[0]->bv_page->mapping->host->i_dentry->first && $bio->bi_io_vec[0]->bv_page->mapping->host->i_dentry->first < 0xffffffffffffff00){ printf("filename:%s\n",kernel_string(@cast($bio->bi_io_vec[0]->bv_page->mapping->host->i_dentry->first-0xb0,"struct dentry")->d_iname)) }else { printf("*\n") } } else { printf(" #\n") } }}
sda 盘的每个分区起始地址和大小是
cat /sys/block/sda/sda1/start 2048cat /sys/block/sda/sda1/size 2048cat /sys/block/sda/sda2/start 4096cat /sys/block/sda/sda2/size 104857600cat /sys/block/sda/sda3/start 104861696 cat /sys/block/sda/sda3/size832839680

1:这个脚本监控了 sda 盘所有分区的写操作,如果是有进程写 sda2 分区的数据,越界到了 sda3 分区,就可以抓到。因为 sda2 分区是跟文件系统,空闲时都有很多进程读写文件,为了过滤掉干扰打印,加了个 if($bio->bi_bdev->bd_dev==0x800002 && ($bio->bi_sector * 512 + $bio->bi_size > 0xC60000000) 判断。这个判断是如果有进程写 sda2 分区结束地址,再把改进程信息打印出来。$bio->bi_sector 是本次写操作的起始扇区地址(相对当前磁盘分区的起始地址,如果从磁盘分区起始地址开始写,$bio->bi_sector就是0),以512字节为单位,$bio->bi_size 是本次写操作的大小,二者相加就是本次写操作的结束扇区地址。sda2 分区50G,16进制是 0xC80000000,如果有 sda2 分区的写操作的磁盘地址大于 0xC60000000,那就很接近 0xC80000000,有可能该写操作越界到了 sda3 分区。

2:打印出写操作的文件名字,原理是基于 pagecache 的命名空间,找到它映射的文件 inode、dentry。需要注意的时,内核 jbd2 线程的写操作,是无法解析出它写的哪个文件。因为它是直接在 ext4 的 jbd2 分区写操作,并不是针对某个文件。

3:如果是 direct IO 形式的文件读写,基于 pagecache 原理解析不出它写的哪个文件。此时需要加上 if($bio->bi_io_vec[0]->bv_page->mapping & 0x1) == 0) 判断,如果 bit0 是1,说明是非 pagecache 的写操作。

运行后的效果如下:

Fri Jul 15 07:33:51 2022 kworker/u896:2_207799 kthreadd_2 sda sector:0x88ac8 size:4096 dev:0x800003 rw:0x1 filename:cli.logFri Jul 15 07:33:54 2022 jbd2/sda3-8_1629 kthreadd_2 sda sector:0x18c44128 size:4096 dev:0x800003 rw:0x411 *
期待等下次发生问题时,能抓到现场。可惜等了几个月都没再发生!
因为这批机器存储业务马上要使用,如果查不清根本原因,那将面临极大风险,毕竟磁盘文件系统会损坏,数据就没了。但是这个问题的发生概率始终是个谜,最初1~2个月发生了5例左右,然后过了几个月一例也没再发生。
时间等不来,只能转变思路了,要改变!最初认为是有进程裸写磁盘导致的 sda3 磁盘文件系统数据损坏,因为事后分析 sda3 分区的前8k数据损坏的很有规律。如果是磁盘硬件损坏了,数据损坏不太可能会这么有规律。那会不会是磁盘驱动有什么 bug,被触发了,然后磁盘驱动向磁盘刷了规律的数据而导致磁盘数据损坏呢?
这个问题比较明显的现象是,sda3 磁盘分区因为磁盘数据损坏而触发了磁盘空间使用率100%异常告警,是否可以看下这个告警首次触发的时间点,也许可以通过首次触发的时间点发现规律呢?找了3台出问题的机器,首次触发 sda3 磁盘空间使用率100%的告警时间都是凌晨3点初!这是个非常重要的信息,不可能有这么巧合的事。像是个定时任务,排查后发现凌晨3点跑了磁盘巡检监本。
该脚本会扫描磁盘等信息,针对这个型号的服务器,会用到 smartctl 和 sas3ircu 获取磁盘信息。这引起了我的警觉,这两个工具都是直接操作磁盘硬件,直接与磁盘驱动交互,嫌疑很大!于是在出问题的机器上,单独跑 smartctl 和 sas3ircu,没想到只要跑 sas3ircu 几十分钟,立即复现服务器宕机、服务器宕机后进入救援模式、磁盘数据损坏!
服务器厂家按照这个方法也终于复现了同样的问题。在与阵列卡厂家沟通后,大体确定问题根本原因:这批服务器用的是新的9400型号 SAS 卡,而 SAS 卡的固件可能存在防呆设计缺陷,在跑 sas3ircu 获取磁盘槽位号信息时,概率性触发磁盘数据损坏和服务器宕机。解决方法是,把 sas3ircu 换成 strocli 工具,或者把 9400 SAS 卡换成老的9300型号的。我的分析是:sda3磁盘分区相对读写的比较频繁,在凌晨3点 sas3ircu 获取磁盘信息时,此时正好有进程读写 sda3 的 ext4 文件系统,然后就触发了 9400 SAS 卡固件 bug,导致错误的数据刷到 sda3 分区,并触发机器宕机等等。
这个 case 排查耗时最长,中间也多次改变排查思路。最早一直怀疑是有进程裸写 sda3 盘导致的文件系统数据损坏,一直被这个思路误导!最后幸亏抓住3台出问题的机器首次出现 sda3 分区空间使用率100%异常告警的时间点都是凌晨3点,然后判定是某些进程或工具 对磁盘硬件进行了什么操作,才会导致 sda3 磁盘分区数据损坏。最终一步步查到是 sas3ircu 获取磁盘信息是一切的元凶。
教训:新引入磁盘、阵列卡等硬件时,一定要做好压测,测试案例要丰富。并且一定要跟厂家确定好配套工具。

案例六:nvme 盘因磁盘起始分区未 4K 对齐导致性能下降一半


这个问题出现在一批新采购的服务器上,磁盘 nvme,数据库等业务在用。问题表现是,nvme 磁盘性能很差,iops 不到5000就把磁盘 IO 打满,IO 敏感的业务有比较明显的延迟。如下是发生问题机器的 iostat 截图:

在发生问题的机器上用 fio 测试,发现 4K 随机读或写 170K 左右,而正常的机器 iops 有 400k。出问题的机器 128k 随机读 iops 只有 2000,128k 随机写 10k,正常的机器 iops 是 128K 随机读或写 20K 左右。
有了之前的排查经验,首先怀疑是磁盘硬件有问题,先 fio 压测 nvme 盘,使用如下命令采集 fio 压测时的 IO 数据
blktrace -d /dev/nvme0n1blkparse -i nvme0n1 -d nvme0n1.blktrace.bin >/dev/nullbtt -i nvme0n1.blktrace.bin

可以发现平均 DC 耗时 500 多 ms,并且最大 DC 耗时 3s,极不寻常。测试的机器系统空闲,没其他业务在跑。
机器重启后问题依然存在,更换内核后问题依然存在。在找服务器厂家分析磁盘信息、服务器信息后,没发现任何异常,这已经习以为常了!但是把有问题的盘换成新的 nvme 盘,性能就正常了。难道是业务长时间读写后,文件系统碎片化严重,导致的磁盘性能下降?难道又与业务有关系?
收集有性能问题的 nvme 盘 blktrace 日志给磁盘厂家分析后,发现了IO传输时有很多跨 media bank 现象。什么意思?内核块层传输的每个 IO 请求,都会把本次读写的起始扇区地址、读取的扇区数、保存读写的 IO 数据的内存地址 等基础信息传递给 nvme 驱动和固件,nvme 固件需要对这些 IO 请求读写的扇区地址进行处理。如果 IO 请求的起始扇区地址是 128K 对齐,那就会获得非常好的性能。如果 IO 请求的起始扇区地址不是 128K 对齐,就会发生跨 media bank 现象,IO 性能就会下降。nvme 固件应该是以128K为一个数据单元,如果 IO 请求的起始扇区地址 128K 对齐,才会获得好的 IO性能,如果不是 128K 对齐 IO 性能就差。
nvme 盘厂商推荐我们用高版本的内核测试下,因为高版本的内核针对跨 media bank 现象有优化,优化 patch 如下:
--- a/drivers/block/nvme-core.c 2017-05-15 13:07:28.440514564 -0600+++ b/drivers/block/nvme-core.c 2017-05-15 13:07:49.437515189 -0600@@ -2336,7 +2336,9 @@     if (ctrl->mdts)         dev->max_hw_sectors = 1 << (ctrl->mdts + shift - 9);     if ((pdev->vendor == PCI_VENDOR_ID_INTEL) && -           (pdev->device == 0x0953) && ctrl->vs[3]) { +           ((pdev->device == 0x0953) || +            (pdev->device == 0x0a53) || +            (pdev->device == 0x0a54)) && ctrl->vs[3]) {         unsigned int max_hw_sectors; 
dev->stripe_size = 1 << (ctrl->vs[3] + shift);
但是优化 patch 查不到内核 changelog,在高版本内核也找不到,需要谨慎使用。
需要提一点,如果 fio 测试时加上 -ba=15k 参数,可以强制给每次传输的 IO 请求起始扇区地址加个一个偏移,模拟出跨 media bank 现象,在 nvme 性能正常的盘上测试也能模拟出性能差问题!
目前怀疑是 nvme 固件有问题:长时间读写后,碎片化严重,然后逻辑地址与物理地址的映射很复杂 (基于 nvme 固件来说,这里的逻辑地址是内核里派发每个 IO 请求的扇区地址,物理地址是数据真实存储在 nvme 盘的地址),还有物理地址对齐也有问题,在达到某种临界点后,就出现性能下跌问题。
但这个系列的 nvme 盘出货量很大,都没遇到过这个问题!但哪里还会有问题呢?是否是 ext4 文件系统有问题,导致每次的 IO 读写的扇区地址都没有 128k 对齐?nvme 盘实际使用时,用 lvm 做了存储卷,然后每个实例分配一个 lvm 卷,接着格式化成 ext4 文件系统。这些流程都是标准操作,没有带什么异常的参数。

此时用如下命令观察有性能问题的盘每次传输的IO请求的起始扇区地址和数据量:

stap  --all-modules  -ve 'probe kernel.function("blk_mq_make_request") {if(execname()=="fio") {printf("start addr:%d  size:%d\n",$bio->bi_sector*512,$bio->bi_size)}}'

发现一个很奇怪的问题,每次传输的 IO 请求的起始扇区地址除以 4K 后,余数都是0.25。

start addr:6361336923136  size:131072  //start addr除以4096后为 1553191012.25  start addr:6361870386176  size:131072  start addr:6361856361472  size:131072  start addr:6363297104896  size:131072  start addr:6363166819328  size:131072  start addr:6361150145536  size:131072  start addr:6362798507008  size:131072  //start addr除以4096后为 1553254948.25  start addr:6362132268032  size:131072 

而在性能正常的 nvme 盘上,每次传输的 IO 请求的起始扇区地址处于4K,没余数,整除。

start addr:14315814912  size:131072start addr:13303021568  size:131072start addr:13914865664  size:131072start addr:14319091712  size:131072start addr:14378598400  size:131072start addr:15401353216  size:131072start addr:14310440960  size:131072start addr:14828961792  size:131072start addr:14549123072  size:131072

为什么有问题的 nvme 盘,每次传输的 IO 请求的起始扇区地址除以4K,余数都是0.25呢?这是 lvm 导致的?还是 ext4 文件系统有关?还有哪些地方会影响到 IO 请求的起始扇区地址?这个因素肯定是很原始的,与 ext4 或者 lvm 关系不大!此时服务器厂家做了一个测试,强制令 nvme 盘的 part 起始分区地址不 4K 对齐,就能复现 nvme 盘性能差问题。据此我们对比性能正常的和异常的 nvme 盘,有了发现。执行 fdisk -lu /dev/nvme0n1 查看 nvme 盘 part 分区信息:

性能异常的 nvme 盘

性能正常的nvme盘
Start 就是 nvme 盘 part 分区的起始地址,乘以512是真实地址,显然性能正常的 nvme 盘 part 分区起始地址4k对齐了,有性能问题的 nvme 盘 part 分区地址地址没有4K对齐。
然后按照如下方法,可以令 nvme 盘性能变差
1:parted -s /dev/nvme0n1 mkpart primary 0 100%Error: /dev/nvme0n1: unrecognised disk label2:parted /dev/nvme0n1//需要重新创建分区GNU Parted 3.1Using /dev/nvme0n1Welcome to GNU Parted! Type 'help' to view a list of commands.(parted) mklabelNew disk label type? y                                                    parted: invalid token: yNew disk label type? gpt                                                  (parted) q                                                                Information: You may need to update /etc/fstab.3:parted -s /dev/nvme0n1 mkpart primary 0 100%
之后 fdisk -l -u /dev/nvme0n1 就可以看到分区起始扇区地址 Start 是34,然后执行如下命令创建 lvm 卷,创建文件系统,模拟线上环境
pvcreate /dev/nvme0n1p1vgcreate docker-test /dev/nvme0n1p1lvcreate -n test1 -L 20G docker-testmkfs.ext4 /dev/mapper/docker--test-test1mount /dev/mapper/docker--test-test1 /home/test1

最后启动 fio 压测,nvme 盘性能果然变差,iops 数据跟发生 nvme 性能差的盘一样。

怎么令这块盘性能恢复正常呢?按照如下命令

umount  /home/test1 lvremove /dev/mapper/docker--test-test1 vgremove docker-test pvremove /dev/nvme0n1p1  parted -s /dev/nvme0n1 rm 1 parted -s /dev/nvme0n1 mkpart primary 1 100%
此时执行 fdisk -l -u /dev/nvme0n1 就可以看到分区起始扇区地址 Start 是 2048,然后执行前文的命令命令创建 lvm 卷,创建文件系统,再启动 fio压测,nvme 盘性能就恢复正常了。
反复几次测试,证实 nvme 盘性能变差的机器都是 nvme 盘 part 分区起始地址没有4K对齐导致的,解决方法是重新制作 part 分区。
PART

03

  总结

这6例批量磁盘故障,排查过程都很艰辛。但最终都查出了根本原因,不然影响很严重。我的感想是,要能灵活使用各种工具抓取问题现场。除此之外,还要有丰富 linux 内核、磁盘和阵列卡硬件等相关知识储备,尤其是 linux 内核。更重要的,排查问题的思路要灵活,一个思路走不通就要换个思路试试。不要放过任何一个细节,因为可能就是查出根因的关键。


作者介绍

Hujun Peng  

云服务中心高级后端工程师

主要负责线上疑难内核问题解决、基于 systemtap 和 ebpf 开发内核级调试工具,擅长内核 block 层和内存管理。

END
About AndesBrain

安第斯智能云

OPPO 安第斯智能云(AndesBrain)是服务个人、家庭与开发者的泛终端智能云,致力于“让终端更智能”。作为 OPPO 三大核心技术之一,安第斯智能云提供端云协同的数据存储与智能计算服务,是万物互融的“数智大脑”。

本文分享自微信公众号 - 安第斯智能云(OPPO_tech)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
2 收藏
1
分享
返回顶部
顶部