linux内核多路径故障(fail_path)流程图及源码分析_kernel_iscsid_multipathd_device_mapper_lvm2

简介

linux多路径multipath, 允许将客户主机端与后端存储引擎或存储阵列之间的多个物理连接组合成一个虚拟设备, 这样做可以为您的存储提供更具弹性的连接(即断开的路径不会妨碍其他连接),或者聚合存储带宽以提高性能. 本文梳理了路径故障时的内核和相关组件处理流程及源码分析, 如下图

多路径故障流程图

multipath

fail_path路径故障简介

  1. initiator与tgt创建连接时设置定时器, 连接启动时开启定时器, 参考命令:ISCSI_UEVENT_CREATE_CONN
  2. 一条路径故障时, 触发内核iscsi驱动的iscsi_check_transport_timeouts函数ping收不到响应(NOP), 默认是5秒超时, 然后将会话设备状态设置为离线(block->offline), 并通过Netlink给用户态发连接错误事件(ISCSI_KEVENT_CONN_ERROR), iscsid进程收到事件后关闭连接, 在/var/log/message中可看到错误打印
  3. 用户态multipathd的check_path循环函数检测到该设备离线状态, 通过ioctl通知内核态, 内核态执行fail_path动作, 将路径标记为NULL
  4. IO流程中检查到当前路径为NULL时, 重新选择其他路径下发IO

多路径关键源码分析

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
内核超时检测流程
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检查到设备状态异常
multipathd\main.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?rec_flag=default&_wkts_=1687763240420

晓兵

博客: https://logread.cn | https://blog.csdn.net/ssbandjl

weixin: ssbandjl

公众号: 云原生云

云原生云