今天我们来看这个问题:SAS/SATA 盘的 IO 能否在一个确定的 deadline 之前返回。
这里“返回”的定义是:同步或异步的 IO 系统调用,能够回到用户态,告知 IO 的结果,成功或者失败。
这个问题对存储的同学来说是非常重要,而一般的用户并不用关心。比如,你在读写磁盘的时候,很少关心磁盘坏掉,kernel 卡死,HBA 卡死等等故障。但是,对于深入研究分布式存储的同学来说,这个又是回避不了的问题。
磁盘 IO 一直不返回,可能卡在多个地方,你的程序需要如何处理呢?一定要避免有阻塞的调用,否则导致整个线程卡死,造成用户 IO 异常;
IO 不返回总结有以下原因:
如果是本地存储,遇到这些问题,基本没救了,但是对于分布式存储来说,还是有抢救的余地的。因为分布式存储,一般是多副本,或者 EC,可以尝试从其他的节点来重试 IO。有了分布式保证,我们也是需要尽量不卡死内核,不让 IO 返回在一个不受控的时间上。
好了,我们回到问题,磁盘究竟能否在确切的 deadline 时间返回用户的调用,告知写入成功或者失败的原因呢?
我们需要从 Linux kernel 的 SCSI 错误恢复讲起。
从 IO 的角度看,而不是 SCSI 详细的错误分类,有两种错误:
/sys/block/sdX/device/timeout
来配置。在 host 初始化时,每个 host 启动一个内核线程 scsi_eh_X。可以通过 ps aux | grep scsi_eh
来查看。
用 lsscsi
来查看所有的 SCSI 设备。
线程被唤醒有两条路径:
- scsi_softirq_done
- -> disposition = scsi_decide_disposition(cmd);
- -> scsi_eh_scmd_add(cmd, 0)
- -> scsi_host_set_state(shost, SHOST_RECOVERY)
- -> scsi_eh_wakeup(shost);
- scsi_schedule_eh
- -> scsi_host_set_state(shost, SHOST_RECOVERY)
- -> scsi_eh_wakeup
内核线程对应的线程函数是 scsi_error_handler
。
- scsi_error_handler
- -> scsi_unjam_host(shost)
- -> scsi_eh_get_sense(&eh_work_q, &eh_done_q))
- -> scsi_eh_abort_cmds(&eh_work_q, &eh_done_q))
- -> scsi_try_to_abort_cmd
- -> hostt->eh_abort_handler <hba specific>
- -> scsi_eh_ready_devs(shost, &eh_work_q, &eh_done_q);
- -> if (!scsi_eh_stu(shost, work_q, done_q))
- if (!scsi_eh_bus_device_reset(shost, work_q, done_q))
- if (!scsi_eh_target_reset(shost, work_q, done_q))
- if (!scsi_eh_bus_reset(shost, work_q, done_q))
- if (!scsi_eh_host_reset(shost, work_q, done_q))
- scsi_eh_offline_sdevs(work_q,
- done_q);
在以上的处理中,有如下特点:
/sys/class/scsi_host/hostX/eh_deadline
。注意没有使用 hba 卡的设备是不能配置此参数的;abort 是 SCSI 定义的一个命令,属于 Task Management Function 的 task。
abort 并不是在 eh 线程处理的。
第一次超时的路径如下:
- scsi_times_out
- -> scsi_abort_command(scmd)
scsi_abort_command 调度一次 abort。abort 的 work 函数是scmd_eh_abort_handler。
scmd_eh_abort_handler
:
首先触发下层驱动的 abort:
如果成功且 IO 重试没有达到 5 次上限,则被 aborted 的 cmd 还是需要重试,放回到 scsi queue 里面。scsi_queue_insert(scmd, SCSI_MLQUEUE_EH_RETRY);
如果 abort 失败了,或者每次 abort 都返回成功,但是 retry 次数超过 5 次,则调用 scsi_eh_scmd_add, 进入 EH 线程处理。
第二次超时的路径如下:
- scsi_times_out
- -> scsi_eh_scmd_add
eh_deadline 开始计时的时机是:
如果已经在 EH 线程处理,不调度 abort。
一旦进入到 EH 线程处理,后续的 IO 都会被 block 住:
包括: sd_open/release/ioctl/write/read 等。
路径如下:
- scsi_block_when_processing_errors
- -> wait_event(sdev->host->host_wait, !scsi_host_in_recovery(sdev->host));
- -> online = scsi_device_online(sdev);
当 userspace 调用 open 系统调用,sd_open 调用 scsi_block_when_processing_errors 来检查设备的状态。如果设备的 host 正在做 error recovery,scsi_host_in_recovery 返回 true,那么需要等待 EH 的退出。
随后判断设备是否在线,如果经过错误处理后,被设置成 offline,就会禁止访问设备。
有没有办法通知 scsi 跳过某一条命令的 5 次 retry 呢?答案是有的。如果 scmd 带上 REQ_FAILFAST_DEV 标记,那么如果遇到 IO timeout,直接走到 EH 线程处理,跳过 retry。注意,这个 flag 没法从用户态传递到内核态,这个 flag 由上层文件系统或者 Device Mapper 来决定是否带上这个 flag。例如 RAID1 可以通过添加 mdadm --failfast 选项即可。
我们以 mpt3sas 为例,SCSI 层定义以下 5 个钩子函数:
- int (* eh_abort_handler)(struct scsi_cmnd *);
- int (* eh_device_reset_handler)(struct scsi_cmnd *);
- int (* eh_target_reset_handler)(struct scsi_cmnd *);
- int (* eh_bus_reset_handler)(struct scsi_cmnd *);
- int (* eh_host_reset_handler)(struct scsi_cmnd *);
mpt3sas 驱动定义了以下 4 个实现:
- 10315 .eh_abort_handler = scsih_abort,
- 10316 .eh_device_reset_handler = scsih_dev_reset,
- 10317 .eh_target_reset_handler = scsih_target_reset,
- 10318 .eh_host_reset_handler = scsih_host_reset,
scsih_abort 定义的 timeout = 30s;
同理,可以看到 scsih_dev_reset,scsih_target_reset 的超时都是 30s。
scsih_host_reset 没有定义超时。
第一个timeout时间达到,触发 abort cmd, 重新插入 scsi queue 进行重试,发现上一次 abort 被调度了,那么 cancel 掉上次的 abort,不会发送新的 abort cmd;
eh_deadline 开始计时,唤醒 eh 线程;
如果 deadline 的时间到达时,已经发出 bus reset,此时 scsi 会等待 10 s;
综上,一个 IO 超时,会有一次 abort,abort 触发一次 IO 重试,IO 重试发现 abort 没有完成,进入 EH 线程处理,EH 线程处理的超时通过 eh_deadline 设置。
所以时间为:timeout * 2 + eh_deadline + 10s + 10s + 硬件响应 device/target/bus/host reset 时间。
学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂
更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,久学习,或点击这里加qun免费
领取,关注我持续更新哦! !
例如,设置 HBA 对应的 eh_deadline = 60s,设置 device 对应的超时 timeout = 15s;对于大部分 disk 来说(HDD、NVMe),这个时间足够了。
此时的最大超时大约在:2 * 15 + 60s + 2 * 10s + 设备的 reset 响应时间(30s)= 140s。
如果磁盘坏的比较奇怪,abort 能够成功,但是命令超时,那么超时需要加上另外 4 次重试。最大的超时时间:
6 * 15 + 60s + 2 * 10s + 设备的 reset 响应时间(30s)+ HOST RESET TIMEOUT = 200s + [0- INFINITE]s。
在磁盘遇到 IO 超时,且当前的 device 队列没有打满时,IO 是可以继续提交,当 IO 提交到队列深度时,io_submit 会表现为提交不了 IO,卡住直到可以提交为止,例如 EH 完成,将 device 设置为 offline。
可以查看这两个参数,决定了 IO timeout 后能提交的最大数据容积量:
- $ cat /sys/block/sdp/queue/max_sectors_kb
- 32
- $ cat /sys/block/sdp/queue/nr_requests
- 128
经过计算,这块磁盘可以提交的最大的数据容积量是:32*128kB= 4M(粗糙计算,跟 IO 对齐,pattern 有关)。当 EH 处理完成,内核返回的错误是 EIO,还可能是 ENOSPC。
当 io_submit 卡住时,大部分 IO 还处在 block 层,并没有通过 SCSI 下发到块设备,因为设备能够接受的 SCSI cmd 是受 cmd_per_lun 控制。一个典型的 megaraid HBA 设置的 cmd_per_lun 为 63, 意味着同时只能有 63 个 IO 被 SCSI 提交到设备。
Io_submit 被阻塞的 stack 如下:
- [<ffffffffba753413>] get_request+0x243/0x7d0
- [<ffffffffba75614e>] blk_queue_bio+0xfe/0x400
- [<ffffffffba754387>] generic_make_request+0x147/0x380
- [<ffffffffba754630>] submit_bio+0x70/0x150
- [<ffffffffba6918cc>] do_blockdev_direct_IO+0x106c/0x20a0
- [<ffffffffba692955>] __blockdev_direct_IO+0x55/0x60
- [<ffffffffba68d767>] blkdev_direct_IO+0x57/0x60
- [<ffffffffba5c05d3>] generic_file_direct_write+0xd3/0x190
- [<ffffffffba5c08c7>] __generic_file_aio_write+0x237/0x400
- [<ffffffffba68e0f6>] blkdev_aio_write+0x56/0xb0
- [<ffffffffba6a4e13>] do_io_submit+0x3e3/0x8a0
- [<ffffffffba6a52e0>] SyS_io_submit+0x10/0x20
- [<ffffffffbab93166>] tracesys+0xa6/0xcc
- [<ffffffffffffffff>] 0xffffffffffffffff
卡住的代码点为:
- static struct request *get_request(struct request_queue *q, int rw_flags,
- 1373 struct bio *bio, unsigned int flags)
- 1374 {
- 1375 const bool is_sync = rw_is_sync(rw_flags) != 0;
- 1376 DEFINE_WAIT(wait);
- 1377 struct request_list *rl;
- 1378 struct request *rq;
- 1379
- 1380 rl = blk_get_rl(q, bio); /* transferred to @rq on success */
- 1381 retry:
- 1382 rq = __get_request(rl, rw_flags, bio, flags);
- 1383 if (!IS_ERR(rq))
- 1384 return rq;
- 1385
- 1386 if ((flags & BLK_MQ_REQ_NOWAIT) || unlikely(blk_queue_dying(q))) {
- 1387 blk_put_rl(rl);
- 1388 return rq;
- 1389 }
- 1390
- 1391 /* wait on @rl and retry */
- 1392 prepare_to_wait_exclusive(&rl->wait[is_sync], &wait,
- 1393 TASK_UNINTERRUPTIBLE);
- 1394
- 1395 trace_block_sleeprq(q, bio, rw_flags & 1);
- 1396
- 1397 spin_unlock_irq(q->queue_lock);
- 1398 io_schedule(); <<<<<<<<<<<<<<<<<<<<<<<<<<<< 卡在这里
- 1399
- 1400 /*
- 1401 * After sleeping, we become a "batching" process and will be able
- 1402 * to allocate at least one request, and up to a big batch of them
- 1403 * for a small period time. See ioc_batching, ioc_set_batching
- 1404 */
- 1405 ioc_set_batching(q, current->io_context);
- 1406
- 1407 spin_lock_irq(q->queue_lock);
- 1408 finish_wait(&rl->wait[is_sync], &wait);
- 1409
- 1410 goto retry;
- 1411 }
如果此时触发 sync 调用,那么 sync 也可能卡住,卡住的stack 为:
- [Tue Jun 9 11:11:41 2020] INFO: task SYNC-73-/dev/sd:10902 blocked for more than 120 seconds.
- [Tue Jun 9 11:11:41 2020] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
- [Tue Jun 9 11:11:41 2020] SYNC-73-/dev/sd D ffff9a22f4bc62a0 0 10902 1 0x00000080
- [Tue Jun 9 11:11:41 2020] Call Trace:
- [Tue Jun 9 11:11:41 2020] [<ffffffffbdf5033d>] ? blk_peek_request+0x9d/0x2a0
- [Tue Jun 9 11:11:41 2020] [<ffffffffbe37f229>] schedule+0x29/0x70
- [Tue Jun 9 11:11:41 2020] [<ffffffffbe37cbb1>] schedule_timeout+0x221/0x2d0
- [Tue Jun 9 11:11:41 2020] [<ffffffffbdf4cf59>] ? __blk_run_queue+0x39/0x50
- [Tue Jun 9 11:11:41 2020] [<ffffffffbdf50c63>] ? blk_queue_bio+0x3b3/0x400
- [Tue Jun 9 11:11:41 2020] [<ffffffffbdd047e2>] ? ktime_get_ts64+0x52/0xf0
- [Tue Jun 9 11:11:41 2020] [<ffffffffbe37e79d>] io_schedule_timeout+0xad/0x130
- [Tue Jun 9 11:11:41 2020] [<ffffffffbe37f85d>] wait_for_completion_io+0xfd/0x140
- [Tue Jun 9 11:11:41 2020] [<ffffffffbdcda0b0>] ? wake_up_state+0x20/0x20
- [Tue Jun 9 11:11:41 2020] [<ffffffffbdf52614>] blkdev_issue_flush+0xb4/0x110
- [Tue Jun 9 11:11:41 2020] [<ffffffffbde87d95>] blkdev_fsync+0x35/0x50
- [Tue Jun 9 11:11:41 2020] [<ffffffffbde7d9f7>] do_fsync+0x67/0xb0
- [Tue Jun 9 11:11:41 2020] [<ffffffffbde7dd03>] SyS_fdatasync+0x13/0x20
- [Tue Jun 9 11:11:41 2020] [<ffffffffbe38cede>] system_call_fastpath+0x25/0x2a
卡住的函数在 wait_for_completion_io,这个函数无法中断,也没有 timeout 值,只能等待命令返回。
- /**
- * wait_for_completion_io: - waits for completion of a task
- * @x: holds the state of this particular completion
- *
- * This waits to be signaled for completion of a specific task. It is NOT
- * interruptible and there is no timeout. The caller is accounted as waiting
- * for IO.
- */
- void __sched wait_for_completion_io(struct completion *x)
- {
- wait_for_common_io(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
- }
SCSI 层的 IO 并不能 100% 保证一定能返回,因为 HBA 的 reset 时间是不可控的。
但是,我们可以通过适当的配置,来尽量缩短 90% 的场景下的最坏时间。
相关的配置有:
/sys/block/sdX/device/timeout
/sys/class/scsi_host/hostX/eh_deadline
由于 IO 并没有返回,且内核返回时间不可控,所以应用层需要设置自己的 IO 超时。当超时达到,且内核没有返回,此时对应的 buffer 不能释放。如果要发起另一个副本的访问,那么就需要分配新的内存来处理数据。总之,如果要实现完美的超时处理,目前看来,是一个很棘手的事情。
对于 NVMe 的存储介质,由于走的是 PCIe 通道,没有各种奇奇怪怪的 HBA 设备,时间相对来说是可控的。NVMe spec 协议很新,可能对 abort 的超时时间有定义,有空我会继续分析这个问题,本文的分享到此为止。