• IO 能够保证在确定的时间回来吗?


    背景

    今天我们来看这个问题:SAS/SATA 盘的 IO 能否在一个确定的 deadline 之前返回。
    这里“返回”的定义是:同步或异步的 IO 系统调用,能够回到用户态,告知 IO 的结果,成功或者失败。

    这个问题对存储的同学来说是非常重要,而一般的用户并不用关心。比如,你在读写磁盘的时候,很少关心磁盘坏掉,kernel 卡死,HBA 卡死等等故障。但是,对于深入研究分布式存储的同学来说,这个又是回避不了的问题。

    磁盘 IO 一直不返回,可能卡在多个地方,你的程序需要如何处理呢?一定要避免有阻塞的调用,否则导致整个线程卡死,造成用户 IO 异常;

    IO 不返回总结有以下原因:

    • 磁盘坏掉:HDD 总是容易坏的,坏的扇区,IO 不响应;
    • HBA 卡故障:例如 HBA 卡的固件产生问题,导致 IO 不响应;
    • HBA 驱动问题:例如 megasas raid,mpt3sas,smartpqi 等等有 BUG;
    • Linux Kernel 内核的问题:IO 遇到阻塞设置成 UNINTERUPTIBLE,然后调度走了,由于 BUG 回不来?
    • Async IO 可能变成同步 IO。

    如果是本地存储,遇到这些问题,基本没救了,但是对于分布式存储来说,还是有抢救的余地的。因为分布式存储,一般是多副本,或者 EC,可以尝试从其他的节点来重试 IO。有了分布式保证,我们也是需要尽量不卡死内核,不让 IO 返回在一个不受控的时间上。

    好了,我们回到问题,磁盘究竟能否在确切的 deadline 时间返回用户的调用,告知写入成功或者失败的原因呢?
    我们需要从 Linux kernel 的 SCSI 错误恢复讲起。

    内核 SCSI 的超时处理

    IO 的错误类型

    从 IO 的角度看,而不是 SCSI 详细的错误分类,有两种错误:

    • IO Error。例如扇区损坏;
    • IO Timeout。IO 请求一直没有返回,例如卡在设备里。CentOS 7 默认的 timeout 是 30s,可以通过 /sys/block/sdX/device/timeout 来配置。

    SCSI timeout 错误处理 EH 的机制

    在 host 初始化时,每个 host 启动一个内核线程 scsi_eh_X。可以通过 ps aux | grep scsi_eh 来查看。
    用 lsscsi 来查看所有的 SCSI 设备。
    线程被唤醒有两条路径:

    • scsi_softirq_done

    1. scsi_softirq_done
    2. -> disposition = scsi_decide_disposition(cmd);
    3. -> scsi_eh_scmd_add(cmd, 0)
    4. -> scsi_host_set_state(shost, SHOST_RECOVERY)
    5. -> scsi_eh_wakeup(shost);
    • 对于不是失败的命令,可以手动调用 scsi_schedule_eh 来唤醒 EH 线程。

    1. scsi_schedule_eh
    2. -> scsi_host_set_state(shost, SHOST_RECOVERY)
    3. -> scsi_eh_wakeup

    内核线程对应的线程函数是 scsi_error_handler

    1. scsi_error_handler
    2. -> scsi_unjam_host(shost)
    3. -> scsi_eh_get_sense(&eh_work_q, &eh_done_q))
    4. -> scsi_eh_abort_cmds(&eh_work_q, &eh_done_q))
    5. -> scsi_try_to_abort_cmd
    6. -> hostt->eh_abort_handler <hba specific>
    7. -> scsi_eh_ready_devs(shost, &eh_work_q, &eh_done_q);
    8. -> if (!scsi_eh_stu(shost, work_q, done_q))
    9. if (!scsi_eh_bus_device_reset(shost, work_q, done_q))
    10. if (!scsi_eh_target_reset(shost, work_q, done_q))
    11. if (!scsi_eh_bus_reset(shost, work_q, done_q))
    12. if (!scsi_eh_host_reset(shost, work_q, done_q))
    13. scsi_eh_offline_sdevs(work_q,
    14. done_q);

    在以上的处理中,有如下特点:

    1. 上述几乎每一步都会去检查 host 的 eh_deadline,如果过期,会立即返回,而不去执行对应的操作。例如 device reset 之前首先检查 eh_deadline 是否到期,如果到期就不进行 device reset;
    2. Host reset 不会去检查 eh_deadline 过期,因为这个操作是最后兜底的,让设备回到正常状态;
    3. eh_deadline 默认设置成 off,可以通过如下路径来修改:/sys/class/scsi_host/hostX/eh_deadline。注意没有使用 hba 卡的设备是不能配置此参数的;
    4. 如果 host reset 之后,设备还没有恢复,那么将设备离线。

    abort 与重试

    abort 是 SCSI 定义的一个命令,属于 Task Management Function 的 task。
    abort 并不是在 eh 线程处理的。
    第一次超时的路径如下:

    1. scsi_times_out
    2. -> 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 线程处理。

    第二次超时的路径如下:

    1. scsi_times_out
    2. -> scsi_eh_scmd_add
    1. cancel 掉上一次的 abort;
    2. 唤醒 eh 线程开始进行错误恢复。

    eh_deadline 开始计时的时机是:

    1. 准备调度 abort;
    2. 准备调度 eh 线程;

    如果已经在 EH 线程处理,不调度 abort。

    一旦进入到 EH 线程处理,后续的 IO 都会被 block 住:
    包括: sd_open/release/ioctl/write/read 等。
    路径如下:

    1. scsi_block_when_processing_errors
    2. -> wait_event(sdev->host->host_wait, !scsi_host_in_recovery(sdev->host));
    3. -> 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 选项即可。

    HBA 驱动的处理

    我们以 mpt3sas 为例,SCSI 层定义以下 5 个钩子函数:

    1. int (* eh_abort_handler)(struct scsi_cmnd *);
    2. int (* eh_device_reset_handler)(struct scsi_cmnd *);
    3. int (* eh_target_reset_handler)(struct scsi_cmnd *);
    4. int (* eh_bus_reset_handler)(struct scsi_cmnd *);
    5. int (* eh_host_reset_handler)(struct scsi_cmnd *);

    mpt3sas 驱动定义了以下 4 个实现:

    1. 10315 .eh_abort_handler = scsih_abort,
    2. 10316 .eh_device_reset_handler = scsih_dev_reset,
    3. 10317 .eh_target_reset_handler = scsih_target_reset,
    4. 10318 .eh_host_reset_handler = scsih_host_reset,

    scsih_abort 定义的 timeout = 30s;
    同理,可以看到 scsih_dev_reset,scsih_target_reset 的超时都是 30s。
    scsih_host_reset 没有定义超时。

    IO 最大的返回时间

    相对好的场景

    第一个timeout时间达到,触发 abort cmd, 重新插入 scsi queue 进行重试,发现上一次 abort 被调度了,那么 cancel 掉上次的 abort,不会发送新的 abort cmd;
    eh_deadline 开始计时,唤醒 eh 线程;
    如果 deadline 的时间到达时,已经发出 bus reset,此时 scsi 会等待 10 s;

    • device reset
    • target reset
    • bus reset(如果没有到这一步,但是 deadline 已经到达,就会跳过);
    • hba reset(deadline 时间到,一定会走到这一步,在 reset 之后同样会 sleep 10s);

    综上,一个 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。

    其他 case:Io_submit 卡住

    在磁盘遇到 IO 超时,且当前的 device 队列没有打满时,IO 是可以继续提交,当 IO 提交到队列深度时,io_submit 会表现为提交不了 IO,卡住直到可以提交为止,例如 EH 完成,将 device 设置为 offline。
    可以查看这两个参数,决定了 IO timeout 后能提交的最大数据容积量:

    1. $ cat /sys/block/sdp/queue/max_sectors_kb
    2. 32
    3. $ cat /sys/block/sdp/queue/nr_requests
    4. 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 如下:

    1. [<ffffffffba753413>] get_request+0x243/0x7d0
    2. [<ffffffffba75614e>] blk_queue_bio+0xfe/0x400
    3. [<ffffffffba754387>] generic_make_request+0x147/0x380
    4. [<ffffffffba754630>] submit_bio+0x70/0x150
    5. [<ffffffffba6918cc>] do_blockdev_direct_IO+0x106c/0x20a0
    6. [<ffffffffba692955>] __blockdev_direct_IO+0x55/0x60
    7. [<ffffffffba68d767>] blkdev_direct_IO+0x57/0x60
    8. [<ffffffffba5c05d3>] generic_file_direct_write+0xd3/0x190
    9. [<ffffffffba5c08c7>] __generic_file_aio_write+0x237/0x400
    10. [<ffffffffba68e0f6>] blkdev_aio_write+0x56/0xb0
    11. [<ffffffffba6a4e13>] do_io_submit+0x3e3/0x8a0
    12. [<ffffffffba6a52e0>] SyS_io_submit+0x10/0x20
    13. [<ffffffffbab93166>] tracesys+0xa6/0xcc
    14. [<ffffffffffffffff>] 0xffffffffffffffff

    卡住的代码点为:

    1. static struct request *get_request(struct request_queue *q, int rw_flags,
    2. 1373 struct bio *bio, unsigned int flags)
    3. 1374 {
    4. 1375 const bool is_sync = rw_is_sync(rw_flags) != 0;
    5. 1376 DEFINE_WAIT(wait);
    6. 1377 struct request_list *rl;
    7. 1378 struct request *rq;
    8. 1379
    9. 1380 rl = blk_get_rl(q, bio); /* transferred to @rq on success */
    10. 1381 retry:
    11. 1382 rq = __get_request(rl, rw_flags, bio, flags);
    12. 1383 if (!IS_ERR(rq))
    13. 1384 return rq;
    14. 1385
    15. 1386 if ((flags & BLK_MQ_REQ_NOWAIT) || unlikely(blk_queue_dying(q))) {
    16. 1387 blk_put_rl(rl);
    17. 1388 return rq;
    18. 1389 }
    19. 1390
    20. 1391 /* wait on @rl and retry */
    21. 1392 prepare_to_wait_exclusive(&rl->wait[is_sync], &wait,
    22. 1393 TASK_UNINTERRUPTIBLE);
    23. 1394
    24. 1395 trace_block_sleeprq(q, bio, rw_flags & 1);
    25. 1396
    26. 1397 spin_unlock_irq(q->queue_lock);
    27. 1398 io_schedule(); <<<<<<<<<<<<<<<<<<<<<<<<<<<< 卡在这里
    28. 1399
    29. 1400 /*
    30. 1401 * After sleeping, we become a "batching" process and will be able
    31. 1402 * to allocate at least one request, and up to a big batch of them
    32. 1403 * for a small period time. See ioc_batching, ioc_set_batching
    33. 1404 */
    34. 1405 ioc_set_batching(q, current->io_context);
    35. 1406
    36. 1407 spin_lock_irq(q->queue_lock);
    37. 1408 finish_wait(&rl->wait[is_sync], &wait);
    38. 1409
    39. 1410 goto retry;
    40. 1411 }

    如果此时触发 sync 调用,那么 sync 也可能卡住,卡住的stack 为:

    1. [Tue Jun 9 11:11:41 2020] INFO: task SYNC-73-/dev/sd:10902 blocked for more than 120 seconds.
    2. [Tue Jun 9 11:11:41 2020] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
    3. [Tue Jun 9 11:11:41 2020] SYNC-73-/dev/sd D ffff9a22f4bc62a0 0 10902 1 0x00000080
    4. [Tue Jun 9 11:11:41 2020] Call Trace:
    5. [Tue Jun 9 11:11:41 2020] [<ffffffffbdf5033d>] ? blk_peek_request+0x9d/0x2a0
    6. [Tue Jun 9 11:11:41 2020] [<ffffffffbe37f229>] schedule+0x29/0x70
    7. [Tue Jun 9 11:11:41 2020] [<ffffffffbe37cbb1>] schedule_timeout+0x221/0x2d0
    8. [Tue Jun 9 11:11:41 2020] [<ffffffffbdf4cf59>] ? __blk_run_queue+0x39/0x50
    9. [Tue Jun 9 11:11:41 2020] [<ffffffffbdf50c63>] ? blk_queue_bio+0x3b3/0x400
    10. [Tue Jun 9 11:11:41 2020] [<ffffffffbdd047e2>] ? ktime_get_ts64+0x52/0xf0
    11. [Tue Jun 9 11:11:41 2020] [<ffffffffbe37e79d>] io_schedule_timeout+0xad/0x130
    12. [Tue Jun 9 11:11:41 2020] [<ffffffffbe37f85d>] wait_for_completion_io+0xfd/0x140
    13. [Tue Jun 9 11:11:41 2020] [<ffffffffbdcda0b0>] ? wake_up_state+0x20/0x20
    14. [Tue Jun 9 11:11:41 2020] [<ffffffffbdf52614>] blkdev_issue_flush+0xb4/0x110
    15. [Tue Jun 9 11:11:41 2020] [<ffffffffbde87d95>] blkdev_fsync+0x35/0x50
    16. [Tue Jun 9 11:11:41 2020] [<ffffffffbde7d9f7>] do_fsync+0x67/0xb0
    17. [Tue Jun 9 11:11:41 2020] [<ffffffffbde7dd03>] SyS_fdatasync+0x13/0x20
    18. [Tue Jun 9 11:11:41 2020] [<ffffffffbe38cede>] system_call_fastpath+0x25/0x2a

    卡住的函数在 wait_for_completion_io,这个函数无法中断,也没有 timeout 值,只能等待命令返回。

    1. /**
    2. * wait_for_completion_io: - waits for completion of a task
    3. * @x: holds the state of this particular completion
    4. *
    5. * This waits to be signaled for completion of a specific task. It is NOT
    6. * interruptible and there is no timeout. The caller is accounted as waiting
    7. * for IO.
    8. */
    9. void __sched wait_for_completion_io(struct completion *x)
    10. {
    11. wait_for_common_io(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
    12. }

    总结

    SCSI 层的 IO 并不能 100% 保证一定能返回,因为 HBA 的 reset 时间是不可控的。
    但是,我们可以通过适当的配置,来尽量缩短 90% 的场景下的最坏时间。
    相关的配置有:

    1. /sys/block/sdX/device/timeout
    2. /sys/class/scsi_host/hostX/eh_deadline

    由于 IO 并没有返回,且内核返回时间不可控,所以应用层需要设置自己的 IO 超时。当超时达到,且内核没有返回,此时对应的 buffer 不能释放。如果要发起另一个副本的访问,那么就需要分配新的内存来处理数据。总之,如果要实现完美的超时处理,目前看来,是一个很棘手的事情。

    对于 NVMe 的存储介质,由于走的是 PCIe 通道,没有各种奇奇怪怪的 HBA 设备,时间相对来说是可控的。NVMe spec 协议很新,可能对 abort 的超时时间有定义,有空我会继续分析这个问题,本文的分享到此为止。

    原文链接:https://zhuanlan.zhihu.com/p/152213307

  • 相关阅读:
    算法思想 - 贪心算法
    实战:QT车牌识别系统综合设计
    【低代码】为客户设计个性化方案:列表篇(客户自己调整排序对齐等)
    文档 + 模型
    ElementUI之动态树+数据表格+分页
    别再纠结线程池大小 + 线程数量了,没有固定公式的
    03 LVS+Keepalived群集
    css实现动画效果 animation: showLayer 0.2s linear both
    OpenGL - Shadows
    React高频面试题100+题,这一篇就够了!
  • 原文地址:https://blog.csdn.net/lingshengxiyou/article/details/127773596