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

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 来唤醒 EH 线程。

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);

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

  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 线程处理的。
第一次超时的路径如下:

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
  1. cancel 掉上一次的 abort;
  2. 唤醒 eh 线程开始进行错误恢复。

eh_deadline 开始计时的时机是:

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

如果已经在 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 选项即可。

HBA 驱动的处理

我们以 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 没有定义超时。

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 后能提交的最大数据容积量:

$ 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% 的场景下的最坏时间。
相关的配置有:

  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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值