简介
linux多路径multipath, 允许将客户主机端与后端存储引擎或存储阵列之间的多个物理连接组合成一个虚拟设备, 这样做可以为您的存储提供更具弹性的连接(即断开的路径不会妨碍其他连接),或者聚合存储带宽以提高性能. 本文梳理了路径故障时的内核和相关组件处理流程及源码分析, 如下图
多路径故障流程图
fail_path路径故障简介
- initiator与tgt创建连接时设置定时器, 连接启动时开启定时器, 参考命令:ISCSI_UEVENT_CREATE_CONN
- 一条路径故障时, 触发内核iscsi驱动的iscsi_check_transport_timeouts函数ping收不到响应(NOP), 默认是5秒超时, 然后将会话设备状态设置为离线(block->offline), 并通过Netlink给用户态发连接错误事件(ISCSI_KEVENT_CONN_ERROR), iscsid进程收到事件后关闭连接, 在/var/log/message中可看到错误打印
- 用户态multipathd的check_path循环函数检测到该设备离线状态, 通过ioctl通知内核态, 内核态执行fail_path动作, 将路径标记为NULL
- IO流程中检查到当前路径为NULL时, 重新选择其他路径下发IO
多路径关键源码分析
代码语言:shell复制内核超时检测流程
drivers/scsi/libiscsi.c
static void iscsi_check_transport_timeouts
struct iscsi_conn *conn = from_timer(conn, t, transport_timer) -> from_timer, 新版本内核对于void (*function)(struct timer_list *)处理函数的参数发生了变化,struct timer_list *定时器地址可以通过from_timer也就是container_of计算出传参进来的结构体首地址,这样来达到传参的目标
iscsi_has_ping_timed_out(conn)
iscsi_conn_failure(conn, ISCSI_ERR_NOP_TIMEDOUT) -> 连接超时, 故障处理, initiator -> tgt的ping超时
iscsi_set_conn_failed
set_bit(ISCSI_CONN_FLAG_SUSPEND_TX, &conn->flags) -> 挂起发送和接收
set_bit(ISCSI_CONN_FLAG_SUSPEND_RX, &conn->flags)
iscsi_conn_error_event(conn->cls_conn, err) -> 控制平面(上行)调用
case ISCSI_CONN_UP -> 连接状态为up(0)
test_and_set_bit ISCSI_CLS_CONN_BIT_CLEANUP
queue_work(iscsi_conn_cleanup_workq -> static void iscsi_cleanup_conn_work_fn -> 清理连接
iscsi_ep_disconnect(conn, false)
WRITE_ONCE(conn->state, ISCSI_CONN_FAILED) -> 将连接状态置为失败
session->transport->unbind_conn(conn, is_active) -> void iscsi_conn_unbind
iscsi_suspend_queue(conn)
void iscsi_suspend_tx
flush_work(&conn->xmitwork)
iscsi_set_conn_failed(conn)
session->transport->ep_disconnect(ep) -> static void iscsi_iser_ep_disconnect ?
iscsi_stop_conn(conn, STOP_CONN_RECOVER)
conn->transport->stop_conn(conn, flag) -> static void iscsi_sw_tcp_conn_stop
iscsi_suspend_tx
iscsi_sw_tcp_release_conn
kernel_sock_shutdown
iscsi_conn_stop
iscsi_block_session(session->cls_session)
queue_work(session->workq, &session->block_work) -> __iscsi_block_session
scsi_target_block(&session->dev)
starget_for_each_device device_block
scsi_internal_device_block
__scsi_internal_device_block_nowait
(scsi_device_set_state(sdev, SDEV_BLOCK)) -> 设置block状态后, 用户态multipathd检测到该状态 -> sdb: path state = blocked
queue_delayed_work(session->workq recovery_work -> 启动延迟任务 -> session_recovery_timedout
iscsi_alloc_session
INIT_DELAYED_WORK(&session->recovery_work, session_recovery_timedout)
session_recovery_timedout
scsi_target_unblock(&session->dev, SDEV_TRANSPORT_OFFLINE)
session->transport->session_recovery_timedout(session) -> void iscsi_session_recovery_timedout
wake_up(&session->ehwait) -> 会话状态: ISCSI_STATE_IN_RECOVERY -> wait_event_interruptible(session->ehwait -> sleep until a condition gets true: https://linuxtv.org/downloads/v4l-dvb-internals/device-drivers/API-wait-event-interruptible.html, 等待队列(Wait Queue)
int iscsi_eh_session_reset
...
iscsi_if_transport_lookup(conn->transport)
alloc_skb
__nlmsg_put
ev->type = ISCSI_KEVENT_CONN_ERROR -> 给用户态发事件(连接超时错误) -> 转到用户态处理
iscsi_multicast_skb
nlmsg_multicast
netlink_broadcast
return
iscsi_send_nopout
iscsid用户态进程收到事件后关闭连接, 错误处理:
void iscsi_conn_error_event
ev->type = ISCSI_KEVENT_CONN_ERROR; 内核设置连接错误状态事件
...
static int ctldev_handle
case ISCSI_KEVENT_CONN_ERROR
session = session_find_by_sid(sid)
ipc_ev_clbk->sched_ev_context EV_CONN_ERROR -> iscsi_sched_ev_context -> static int iscsi_sched_ev_context
case EV_CONN_ERROR
...
case EV_CONN_ERROR
actor_init(&ev_context->actor, session_conn_error -> static void session_conn_error(void *data) -> 设置回调 -> 收到内核事件 -> Kernel reported iSCSI connection 27:0 error (1022 - ISCSI_ERR_NOP_TIMEDOUT: A NOP has timed out) state (3)
cat /var/log/messages|grep 'Kernel reported' -> Jun 26 17:00:59 node1 iscsid: iscsid: Kernel reported iSCSI connection 1:0 error (1022 - ISCSI_ERR_NOP_TIMEDOUT: A NOP has timed out) state (3) -> multipathd: checker failed path 8:16 in map mpatha
iscsi_ev_context_put(ev_context)
__conn_error_handle
session_conn_shutdown -> no
case ISCSI_CONN_STATE_LOGGED_IN
session_conn_reopen
__session_conn_reopen
re-opening session
iscsi_flush_context_pool(session)
conn->session->t->template->ep_disconnect(conn) -> iscsi_io_tcp_disconnect(iscsi_conn_t *conn)
setsockopt(conn->socket_fd, SOL_SOCKET
close(conn->socket_fd)
if (ipc->stop_conn(session->t->handle -> kstop_conn(uint64_t transport_handle
ev.type = ISCSI_UEVENT_STOP_CONN
rc = __kipc_call(iov, 2) -> 内核
queue_delayed_reopen(qtask, delay) -> static void iscsi_login_timedout
iscsi_login_eh(conn, qtask, ISCSI_ERR_TRANS_TIMEOUT)
case ISCSI_CONN_STATE_XPT_WAIT
case R_STAGE_SESSION_REOPEN
session_conn_reopen(conn, qtask, 0)
__session_conn_reopen
第二次不进 if (do_stop)
if (iscsi_set_net_config
if (iscsi_conn_connect
...
用户态multipathd检查到设备状态异常
multipathdmain.c -> main (int argc, char *argv[])
pthread_create(&check_thr, &misc_attr, checkerloop, vecs)
check_path (struct vectors * vecs, struct path * pp, unsigned int ticks)
conf = get_multipath_config()
newstate = path_offline(pp)
sysfs_attr_get_value(parent, "state", buff, sizeof(buff)) -> 通过udev读路径状态, cat /sys/devices/platform/host15/session2/target15:0:0/15:0:0:1/state -> offline -> 何时设置为 offline
fail_path(pp, 1) -> newstate == PATH_DOWN
fail_path (struct path * pp, int del_active)
condlog(2, "checker failed path %s in map %s" -> checker failed path
dm_fail_path(pp->mpp->alias, pp->dev_t)
snprintf(message, 32, "fail_path %s", path) -> 生成路径故障消息, 比如(failed path 8:16)
dm_message(mapname, message)
libmp_dm_task_create(DM_DEVICE_TARGET_MSG) -> type
dm_task_set_message(dmt, message)
dm_task_no_open_count(dmt)
libmp_dm_task_run(dmt)
dm_task_run(dmt) -> libmultipath:使用互斥锁保护 acy libdevmapper 调用 dm_udev_wait() 和 dm_task_run() 可以访问 libdevmapper 中的全局/静态状态。 它们需要通过我们的多线程库中的锁进行保护,修改后的调用序列需要修复 dmevents 测试:必须将 devmapper.c 添加到 dmevents-test_OBJDEPS 以捕获对 dm_task_run() 的调用。 另外,setup() 中对 dmevent_poll_supported() 的调用将导致 init_versions() 被调用,这需要在测试设置阶段绕过包装器, libdevmapper, __strncpy_sse2_unaligned ()
at ../sysdeps/x86_64/multiarch/strcpy-sse2-unaligned.S:43, 最终发ioctl给内核, lvm2项目 -> int dm_task_run(struct dm_task *dmt)
newstate = get_state(pp, 1, newstate) -> 如果路径状态是up -> get_state (struct path * pp, int daemon, int oldstate)
state = checker_check(c, oldstate)
int checker_check
c->check = (int (*)(struct checker *)) dlsym(c->handle, "libcheck_check")
libcheck_check
ret = sg_read(c->fd, &buf[0], 4096, &sbuf[0],
SENSE_BUFF_LEN, c->timeout)
while (((res = ioctl(sg_fd, SG_IO, &io_hdr)) < 0) && (EINTR == errno));
MSG(c, MSG_READSECTOR0_UP)
if (del_active) -> update_queue_mode_del_path(pp->mpp)
update_multipath_strings -> 同步内核状态
内核驱动处理fail_path:
static ioctl_fn lookup_ioctl
{DM_TARGET_MSG_CMD, 0, target_message} -> static int target_message -> 由lvm发送给内核态 -> libdm/libdevmapper.h -> DM_DEVICE_TARGET_MSG -> 17
ti->type->message(ti, argc, argv, result, maxlen) -> static int multipath_message
action = fail_path;
action_dev(m, dev, action) -> static int action_dev
action(pgpath) -> static int fail_path
dm-mpath.c -> static int fail_path #路径故障
pgpath->pg->ps.type->fail_path(&pgpath->pg->ps, &pgpath->path) -> .fail_path = st_fail_path -> static void st_fail_path
list_move(&pi->list, &s->failed_paths)
DMWARN("%s: Failing path %s." ...
m->current_pgpath = NULL -> 将当前路径清空
dm_path_uevent(DM_UEVENT_PATH_FAILED -> 此补丁添加了对失败路径和恢复路径的 dm_path_event 的调用 -> void dm_path_uevent -> _dm_uevent_type_names[] -> {DM_UEVENT_PATH_FAILED, KOBJ_CHANGE, "PATH_FAILED"}
dm_build_path_uevent
dm_uevent_add -> void dm_uevent_add -> list_add(elist, &md->uevent_list) -> 挂链表
dm_send_uevents -> void dm_send_uevents
dm_copy_name_and_uuid
kobject_uevent_env 发送uevent, kobject_uevent这个函数原型如下,就是向用户空间发送uevent,可以理解为驱动(内核态)向用户(用户态)发送了一个KOBJ_ADD
kobject_uevent_net_broadcast -> 参考热拔插原理, todo...
queue_work(dm_mpath_wq, &m->trigger_event) -> 触发一个event以唤起用户态的对该Multipath事件的监听线程, 用户态(multipath-tools)关键字: PATH_FAILED
enable_nopath_timeout(m)
mod_timer(&m->nopath_timer
内核IO映射时选择其他路径:
static const struct blk_mq_ops dm_mq_ops = {
.queue_rq = dm_mq_queue_rq,
.complete = dm_softirq_done,
.init_request = dm_mq_init_request,
};
static blk_status_t dm_mq_queue_rq -> blk-mq 新的多队列块IO排队机制, struct request -> 尝试将引用的字段放在同一个缓存行中
dm_start_request(md, rq)
blk_mq_start_request -> 设备驱动程序使用的函数来通知块层现在将处理请求,因此 blk 层可以进行适当的初始化,例如启动超时计时器
trace_block_rq_issue(rq) -> 下发io到驱动, Linux下block层的监控工具blktrace, https://blog.csdn.net/hs794502825/article/details/8541235, linux跟踪系统, https://elinux.org/Kernel_Trace_Systems
test_bit(QUEUE_FLAG_STATS, &q->queue_flags) -> int test_bit(nr, void *addr) 原子的返回addr位所指对象nr位
blk_add_timer(rq) -> 启动单个请求超时计时器
mod_timer(&q->timeout, expiry)
WRITE_ONCE(rq->bio->bi_cookie, blk_rq_to_qc(rq)) -> Linux内核中的READ_ONCE和WRITE_ONCE宏, https://zhuanlan.zhihu.com/p/344256943, 缓存一致性: https://blog.csdn.net/zxp_cpinfo/article/details/53523697, 所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
dm_get(md)
init_tio(tio, rq, md) -> target_io
map_request(tio)
ti->type->clone_and_map_rq -> static int multipath_clone_and_map
Do we need to select a new pgpath? 我们是否需要选择一个新的优先级组路径?
pgpath = choose_pgpath(m, nr_bytes) -> Switchgroup消息传递到内核,会修改内核multipath对象的current_pgpath=NULL和nextpg,failback消息传递到内核,会调用 fail_path 方法修改内核multipath对象的 current_pgpath=NULL,之后的读写请求到multipath_target的map_io时就会选择的新的路径
choose_path_in_pg -> dm mpath:消除 IO 快速路径中自旋锁的使用,此提交的主要动机是提高大型 NUMA 系统上 DM 多路径的可扩展性,其中 m->lock 自旋锁争用已被证明是真正快速存储的严重瓶颈在此提交中利用了使用 lockless_dereference() 以原子方式读取指针的能力。 但是所有指针写入仍然受到 m->lock 自旋锁的保护(这很好,因为这些现在都发生在慢速路径中)以下函数在其快速路径中不再需要 m->lock 自旋锁:multipath_busy()、__multipath_map() 和 do_end_io()并且 Choose_pgpath() 被修改为_不_更新 m->current_pgpath 除非它也切换路径组。 这样做是为了避免每次 __multipath_map() 调用 Choose_pgpath() 时都需要获取 m->lock。 但是如果通过fail_path()失败,m->current_pgpath将被重置
path = pg->ps.type->select_path(&pg->ps, nr_bytes) -> static struct dm_path *st_select_path -> st_compare_load
clone = blk_mq_alloc_request
blk_mq_alloc_cached_request
pgpath->pg->ps.type->start_io -> static int st_start_io -> 调用路径线算法的 start_io 函数, 如果成功则返回 DM_MAPIO_REMAPPED, 表明映射成功,通知DM框架重新投递请求, dm mpath:添加服务时间负载均衡器,此补丁添加了一个面向服务时间的动态负载均衡器 dm-service-time,它为传入 I/O 选择估计服务时间最短的路径。 通过将进行中的 I/O 大小除以每条路径的性能值来估计服务时间。性能值可以在表加载时作为表参数给出。 如果未给出性能值,则所有路径均被视为相同, 参考流程: https://my.oschina.net/LastRitter/blog/1541330
atomic_add(nr_bytes, &pi->in_flight_size) -> 在 IO 开始与结束时,分别增减该路径正在处理的 IO 字节数 -> sz1 = atomic_read(&pi1->in_flight_size) <- static int st_compare_load
case DM_MAPIO_REMAPPED -> 映射成功
setup_clone -> dm:始终将请求分配推迟给 request_queue 的所有者, 如果底层设备是 blk-mq 设备,则 DM 已在底层设备的 request_queue 上调用 blk_mq_alloc_request, 但现在我们允许驱动程序分配额外的数据并提前初始化它,我们需要对所有驱动程序执行相同的操作。 这样做并在块层中使用新的 cmd_size 基础设施极大地简化了 dm-rq 和 mpath 代码,并且还应该使 SQ 和 MQ 设备与 SQ 或 MQ 设备映射器表的任意组合成为可能,作为进一步的步骤
blk_rq_prep_clone dm_rq_bio_constructor
clone->end_io = end_clone_request
trace_block_rq_remap
blk_insert_cloned_request(clone) -> #ifdef CONFIG_BLK_MQ_STACKING -> blk-mq:使 blk-mq 堆栈代码可选,堆栈 blk-mq 驱动程序的代码仅由 dm-multipath 使用,并且最好保持这种方式。 使其可选并且仅由设备映射器选择,以便构建机器人更容易捕获滥用行为,例如在最后一个合并窗口中的 ufs 驱动程序中滑入的滥用行为。 另一个积极的副作用是,内核构建时没有设备映射器也会缩小一点
if (blk_rq_sectors(rq) > max_sectors) -> 如果实际支持 Write Same/Zero,SCSI 设备没有一个好的返回方法。 如果设备拒绝非读/写命令(丢弃、写入相同内容等),则低级设备驱动程序会将相关队列限制设置为 0,以防止 blk-lib 发出更多违规操作。 在重置队列限制之前排队的命令需要使用 BLK_STS_NOTSUPP 完成,以避免 I/O 错误传播到上层
blk_account_io_start(rq)
blk_do_io_stat
update_io_ticks
blk_mq_run_dispatch_ops -> blk_mq_request_issue_directly -> rcu -> 读文件过程, 禁用调度器: https://blog.csdn.net/jasonactions/article/details/109614350
if (blk_mq_hctx_stopped(hctx) || blk_queue_quiesced(rq->q)) -> 队列禁止
__blk_mq_issue_directly
ret = q->mq_ops->queue_rq(hctx, &bd) -> 内核block层Multi queue多队列的一次优化实践: https://blog.csdn.net/hu1610552336/article/details/121072592
参考
linux_kernel 5.10, lvm2, open-iscsi, multipath-tool
https://docs.kernel.org/driver-api/scsi.html
https://github.com/ssbandjl/linux.git
bio下发流程: https://blog.csdn.net/flyingnosky/article/details/121362813
io路径: https://zhuanlan.zhihu.com/p/545906763
用户态与内核态通信netlink: https://gist.github.com/lflish/15e85da8bb9200794255439d0563b195
实现rfc3720: https://github.com/ssbandjl/linux/commit/39e84790d3b65a4af1ea1fb0d8f06c3ad75304b3
管理内核模块,红帽: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_monitoring_and_updating_the_kernel/managing-kernel-modules_managing-monitoring-and-updating-the-kernel
存储技术原理-敖青云
内核定时器:
bpftrace:
spec: 官方指南: https://rpm-packaging-guide.github.io/,
io路径分析: https://codeantenna.com/a/ADadbOp8tQ
编译内核参考: https://wiki.centos.org/HowTos/Custom_Kernel
spec文件: 构建rpm包和spec基础: https://www.cnblogs.com/michael-xiang/p/10480809.html, 官方指南: https://rpm-packaging-guide.github.io/,
kdir: https://stackoverflow.com/questions/59366772/what-does-the-kernel-compile-rule-exact-mean
kernel makefile: https://www.kernel.org/doc/html/latest/kbuild/makefiles.html
Linux模块文件如何编译到内核和独立编译成模块: https://z.itpub.net/article/detail/090A31801416081BC9D0781C05AC91AA
安装源码: https://wiki.centos.org/HowTos/I_need_the_Kernel_Source
编译内核模块: https://wiki.centos.org/HowTos/BuildingKernelModules
dm-verity简介 ——(1): https://www.cnblogs.com/hellokitty2/p/12364836.html
管理工具源码_lvm_dmsetup: https://sourceware.org/dm/
多路径参考: https://www.cnblogs.com/D-Tec/archive/2013/03/01/2938969.html
uevent:
https://www.cnblogs.com/arnoldlu/p/11246204.html
device_mapper_uevent: https://docs.kernel.org/admin-guide/device-mapper/dm-uevent.html
用udev动态管理内核设备: https://documentation.suse.com/sles/12-SP5/html/SLES-all/cha-udev.html
linux设备模型: https://linux-kernel-labs.github.io/refs/pull/183/merge/labs/device_model.html
源码分析: https://groups.google.com/g/open-iscsi/c/Z0FMQUxalcU
iscsid: https://blog.csdn.net/kjtt_kjtt/article/details/38661329
upstream: https://github.com/open-iscsi/open-iscsi.git
多路径参考: https://wenku.baidu.com/view/a1dd303ab9f3f90f77c61bc9.html?recflag=default&_wkts=1687763240420
晓兵
博客: https://logread.cn | https://blog.csdn.net/ssbandjl
weixin: ssbandjl
公众号: 云原生云