TL;DR
- 容器启动时设置为特权容器,或赋予网络权限,CAP_NET_ADMIN、CAP_NET_RAW,或设置为HOST网络模式。1
- 容器启动时挂载入宿主机的/dev目录。
- udev会通过PF_NETLINK套接字获取宿主机的内核设备树变更事件。
- 合理的udev配置 + udev规则,实现自动挂载块设备。
背景
边缘设备的需要在K8s管理的容器内向热插拔磁盘拷贝数据,无法在当前容器生命周期内完成块设备的挂载和访问。
调参
udev的守护进程接收由LinuxKernel生成的各类事件,以响应与外围设备相关的物理事件。通俗来说可以检测系统设备的热插拔(当然也可以处理非物理设备在设备树上的操作)。
kernel: usb 1-4: new high-speed USB device number 76 using xhci_hcd
kernel: usb 1-4: New USB device found, idVendor=****, idProduct=****, bcdDevice=**.01
kernel: usb 1-4: New USB device strings: Mfr=1, Product=2, SerialNumber=3
kernel: usb 1-4: Product: External HDD
kernel: usb 1-4: Manufacturer: JMicron
kernel: usb 1-4: SerialNumber: 000000******
kernel: usb 1-4: USB disconnect, device number 76
在大部分的Linux操作系统上,由于systemd的存在,udev守护进程可直接被管理。在容器环境下,PID1的进程通常是业务意义上的守护进程,
在容器环境内的udev,有若干点需要注意。
/sys
若不以特权容器的形式启动,则/sys
目录默认不会被挂载,该位置挂载了一个sysfs文件系统,其主要功能是对系统设备进行管理,sysfs可以产生一个包含所有系统硬件层次的视图。
挂载了这个目录,容器内就具备了操作宿主机设备的能力。
因为容器通常设计为与宿主环境隔离,原则上不应当给予容器可以操作宿主机系统设备的权限。这会导致/etc/init.d/udev
守护进程启动脚本(aka:服务启动脚本)拒绝(在容器环境)运行。可以通过注释对应代码(暂未验证无/sys
是否影响消息传递,原因见下),或使用特权容器。
AF_NETLINK
或PF_NETLINK
2的套接字来获得各类所需要的内核消息3。AF_NETLINK
一类套接字主要用于内核与用户空间的通信。若想访问该套接字,网上大部分的资料要求设置容器host网络模式,即--net host
。这样一来,目的达到了,但容器开放的端口将直接使用主机网络(容器网络和主机网络在同一个命名空间),我认为并不是十分优雅。从原理上来说,我认为也可以给予容器CAP_NET_ADMIN、CAP_NET_RAW。1
udev挂载命名空间
udev在宿主机测试时,默认配置实际挂载的命名空间和用户可访问的不一致,因此需要调整配置文件。
目前尚未测试在容器环境中,是否存在挂载命名空间差异,保险起见,还是修改一下。
简单来说需要把PrivateMounts=yes
,改为PrivateMounts=no
具体配置文件请点开下拉栏
$ cat /lib/systemd/system/systemd-udevd.service
# SPDX-License-Identifier: LGPL-2.1+
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=udev Kernel Device Manager
Documentation=man:systemd-udevd.service(8) man:udev(7)
DefaultDependencies=no
After=systemd-sysusers.service systemd-hwdb-update.service
Before=sysinit.target
ConditionPathIsReadWrite=/sys
[Service]
Type=notify
# Note that udev also adjusts the OOM score internally and will reset the value internally for its workers
OOMScoreAdjust=-1000
Sockets=systemd-udevd-control.socket systemd-udevd-kernel.socket
Restart=always
RestartSec=0
ExecStart=/lib/systemd/systemd-udevd
ExecReload=udevadm control --reload --timeout 0
KillMode=mixed
TasksMax=infinity
PrivateMounts=yes #此处需要改为no
ProtectHostname=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
IPAddressDeny=any
WatchdogSec=3min
部分装有SELinux的系统可能需要增加以下配置,一般容器环境内不需要。
[Service]
MountFlags=shared
udev规则
udev规则通常存放于两个地方。/lib/udev/rules.d/
和/etc/udev/rules.d/
。
在参考了软件自带的一些规则之后,可以照葫芦画瓢的仿写出以下的一组规则。4
KERNEL!="sd[a-z][0-9]|sd[a-z]", GOTO="media_by_label_auto_mount_end"
IMPORT{program}="/sbin/blkid -o udev -p /mount_host/%N"
ENV{ID_FS_LABEL}!="", ENV{dir_name}="%E{ID_FS_LABEL}"
ENV{ID_FS_LABEL}=="", ENV{dir_name}="usbhd-%k"
ACTION=="add", ENV{mount_options}="relatime"
ACTION=="add", ENV{ID_FS_TYPE}=="vfat|ntfs", ENV{mount_options}="$env{mount_options},utf8,gid=100,umask=002"
ACTION=="add", ENV{ID_FS_TYPE}=="ext3|ext4", SUBSYSTEMS=="usb", SUBSYSTEM=="block", RUN+="/bin/mkdir -p /media/dextercai/%E{dir_name}", RUN+="/usr/bin/mount -o $env{mount_options} /mount_host/dev/%k /media/dextercai/%E{dir_name}"
ACTION=="remove", ENV{ID_FS_TYPE}=="ext3|ext4", SUBSYSTEMS=="usb", SUBSYSTEM=="block", ENV{dir_name}!="", RUN{program}+="/usr/bin/umount /media/dextercai/%E{dir_name}", RUN{program}+="/bin/rmdir /media/dextercai/%E{dir_name}"
LABEL="media_by_label_auto_mount_end"
简单解释一下为什么这么写。
- 整个规则大致包含了这几个动作类型,赋值、判断、执行,主要关键词在
=
、==
、!=
、RUN+=
、GOTO=
。如果你略懂一些开发,相信你不难理解。每一行都可以看作是判断 + 赋值和执行的组合。 %N
是什么意思?或在完整系统上使用man udev
5。- 比较难理解的大概是
IMPORT{program}="/sbin/blkid -o udev -p /mount_host/%N"
和ENV{mount_options}
。blkid
提供了一种为udev而设计的输出类型。形如ID_FS_TYPE=ext4
,一行一个,可被udev导入。之后就可以在ENV中使用(如同一个map一样)。ENV{dir_name}
和%E{dir_name}
是同一个东西。 - 完整系统可以使用systemd-mount,容器环境通常和systemd八字不合,因此容器环境内挂载命令改为了mount。systemd-mount的行为来看,并不会直接执行挂载,而是产生一个挂载点。具体挂载还是由用户层面的进程进行控制的。6
/sbin/blkid -o udev -p /mount_host/%N 输出
$ sudo blkid -o udev -p /dev/nvme0n1p2
ID_FS_UUID=b9c3bb71-e4e8-47c1-9905-************
ID_FS_UUID_ENC=b9c3bb71-e4e8-47c1-9905-************
ID_FS_VERSION=1.0
ID_FS_TYPE=ext4
ID_FS_USAGE=filesystem
ID_PART_ENTRY_SCHEME=gpt
ID_PART_ENTRY_UUID=ff42861a-ff6c-4499-93c2-************
ID_PART_ENTRY_TYPE=0fc63daf-8483-4772-8e79-************
ID_PART_ENTRY_NUMBER=2
ID_PART_ENTRY_OFFSET=1050624
ID_PART_ENTRY_SIZE=999163904
ID_PART_ENTRY_DISK=259:0
调试与监控
与内核打交道的部分总是令人抓狂的。 ——我说的
udev提供了udevadm程序,可以将问题简化。
通过udevadm monitor
,可以监听内核和udev事件,可以用于未配置规则前,容器内检查是否可以正常获取到事件,可以看到当你插入不同设备时发生了什么。
在udevadm monitor
命令运行时,插入一个可移动设备,将看到各种信息在你的屏幕上滚动而出。主要关注ADD事件,当然如果够硬核的话,也可以直接去看内核日志。
udevadm info -a -n /dev/sda
这条命令,可以看到一些关于设备的基本udev信息,这也是上面那一个rules案例一些参数判断的来源。
udevadm control --reload-rules
,可以在不重启udev守护进程的前提下,重载规则。当然在容器内我一般直接通过重启守护进程sudo -S service udev restart
。
挂载上的注意
不要企图将宿主机的/dev
直接挂载到容器内/dev
。会变得不幸。
一般来说我们是肯定需要使用tty去进入容器调试的。tty参数设为true
才能正常使用bash。k8s启动时会让docker需要使用的/dev/tmux点出现问题。具体原因未深入学习。
因此,既然我们的事件取于宿主机,/sys
也属于宿主机,不妨为宿主机的/dev
在容器内的挂载点设置一个别名。这也是上述rules规则为什么没有直接使用/dev
的原因。
具体K8s配置因涉敏问题,暂不放出。
eg: docker run -v /dev:/from_host/dev ubuntu:20.04 bash
其他参考
- 尚未验证,目前实际我正在使用特权容器的方式 ↩
AF_XXXX
地址簇;PF_XXXX
协议簇 ↩- 在Linux下,
AF_NETLINK
与PF_NETLINK
是一个东西。参考:http://anoty.blogspot.com/2008/12/difference-between-afxxx-and-pfxxx.html ↩ - 参考:https://github.com/Ferk/udev-media-automount ↩
man udev
参考:https://linux.die.net/man/8/udev ↩- 待后续补充 ↩
本文标题:容器环境下宿主机块设备的事件处理
本文连接:https://blog.dextercai.com/archives/180.html
除另行说明,本站文字内容采用创作共用版权 CC-BY-NC-ND 4.0 许可协议,版权归本人所有。
除另行说明,本站图片内容版权归本人所有,任何形式的使用需提前联系。
有大佬吹這個模塊有多棒多棒