首页
/ Kubernetes 测试镜像 volume/iscsi 全解析:iSCSI Target 容器的构建、运行与 E2E 集成机制

Kubernetes 测试镜像 volume/iscsi 全解析:iSCSI Target 容器的构建、运行与 E2E 集成机制

2026-09-07 15:11:17作者:韦蓉瑛

本篇文章以 Kubernetes 仓库中 test/images/volume/iscsi/README.md 为主线,系统讲解这套用于存储 E2E 测试的 iSCSI Target(iSCSI 服务器端)容器镜像:它如何在容器内借助宿主内核与 targetcli 暴露一个真实的 iSCSI 目标,供 kubernetes.io/iscsi 内置卷插件挂载验证。读完你将掌握该镜像目录内每个文件的作用、create_block.sh 生成测试块设备的原理、run_iscsi_target.sh 配置 LIO target 的完整命令,以及它被 Kubernetes 存储 E2E 框架调度消费的完整调用链。

一、容器定位:为 iSCSI 卷插件测试提供"服务器端"

Kubernetes 的存储测试需要一个能随时拉起、可重复销毁的存储服务器。在 test/e2e/storage/drivers/in_tree.go 中,iSCSI 被实现为一个 in-tree 测试驱动(kubernetes.io/iscsi:测试先在集群里启动一个 iSCSI Target 容器作为"卷提供者",再让挂载该卷的客户端 Pod 去访问它。

本镜像扮演的正是这个角色——iSCSI target 端(即服务器端)。它的特点是把 Linux 内核自带的 LIO target 能力封装进容器:容器本身并不常驻 daemon,而是启动后通过 targetcli宿主内核中注册一个 IQN 和对应 LUN,等待 initiator(测试客户端)通过 3260 端口连接。整个仓库目录位于 test/images/volume/iscsi/,与同级的 NFS 测试容器(test/images/volume/nfs/)并列,共同构成 test/images/volume 下最基础的两种共享存储测试服务器。

test/images/volume/
├── OWNERS
├── iscsi/            # 本文主题:iSCSI target 测试容器
└── nfs/              # NFS server 测试容器

二、README 声明的两条硬性运行前提

原 README 开门见山地给出了容器运行的两个关键约束,这两点是理解整个镜像设计的基础:

  1. 必须挂载宿主机的 /lib/modules:容器需要向宿主机内核插入 iSCSI 相关的内核模块(如 iscsi_target_modtarget_core_modtarget_core_file 等,由 LIO 使用)。这要求宿主机预先安装好这些模块(原文:This assumes that these modules are installed on the host!)。
  2. 必须以 docker --privileged(特权模式)运行:因为容器要在宿主内核中创建 target 设备、配置内核路径,普通受限容器没有这些权限。

这两点与 E2E 框架中该镜像的调度方式是呼应的:见下文第七节,服务端 Pod 需要绑定 /lib/modules/sys/kernel 等宿主目录并运行在 hostNetwork 上。

三、目录文件清单与职责

镜像目录下共 8 个条目,各自职责如下:

文件 作用
Dockerfile 镜像构建描述:安装 targetcli、拷贝入口脚本与预置块设备
BASEIMAGE 多架构基础镜像映射,当前为 Fedora 42 各架构变体
VERSION 镜像版本号,当前为 2.7
create_block.sh 构建期脚本(须以 root 运行):生成 block.tar.gz 测试块设备
block.tar.gz 已生成的约 120MB ext2 块设备文件压缩包,直接 ADD 进镜像
run_iscsi_target.sh 容器 ENTRYPOINT:用 targetcli 配置并启动 iSCSI target
README.md 使用说明(本文主体)
BASEIMAGE / VERSION 配套的构建约定 供镜像发布流水线读取

其中 BASEIMAGEVERSION 被 K8s 的镜像发布体系(PromoterE2eRegistry)读取——详见 test/utils/image/manifest.go,其中把该镜像注册为 volume/iscsi,版本 2.7

// VolumeISCSIServer image
VolumeISCSIServer
...
configs[VolumeISCSIServer] = Config{list.PromoterE2eRegistry, "volume/iscsi", "2.7"}

镜像由此以 VolumeISCSIServer 这一常量名被 E2E 测试统一引用(见下文第七节),而无需在测试里硬编码镜像地址。

四、block.tar.gz:预置的 ext2 测试块设备

README 指出:block.tar.gz 是一个由 create_block.sh(须以 root 运行)生成的、很小的 ext2 文件系统。阅读 create_block.sh 可看到它的完整生成流程:

MNTDIR="$(mktemp -d)"

# Create 120MB device with ext2
# (volume_io tests need at least 100MB)
dd if=/dev/zero of=block seek=120 count=1 bs=1M
mkfs.ext2 block

# Add index.html to it
mount -o loop block "$MNTDIR"
echo "Hello from iscsi" > "$MNTDIR/index.html"
umount "$MNTDIR"

tar cfz block.tar.gz block

几个值得注意的实现细节:

  • 大小 120MB 是有讲究的:脚本注释明确说明 volume_io tests need at least 100MB,即运行 volume I/O 类测试需要至少 100MB 的空间预算,因此这里用 ddseek 方式快速创建一个稀疏的 120MB 文件,再格式化为 ext2。
  • 写入标识文件:文件系统内唯一的文件是 index.html,内容为 Hello from iscsi。这并非随意为之——E2E 测试客户端挂载该卷后,需要校验卷内容(如读取该文件并断言内容),README 之外的测试逻辑依赖这一约定。
  • 为什么必须 root 执行:脚本使用了 mount -o loop(loop 挂载)与 mkfs.ext2,这两步都需要特权。脚本还通过 trap cleanup TERM EXIT 保证中途失败时自动卸载并清理临时文件与 block 文件。

Dockerfile 中 ADD block.tar.gz / 会把解压后的 /block(一个裸的 ext2 块文件)放进镜像根目录,作为后续 target 暴露的"磁盘"来源。

五、Dockerfile 与基础镜像构建

test/images/volume/iscsi/Dockerfile 非常精简,完整内容如下:

ARG BASEIMAGE
FROM $BASEIMAGE

RUN yum install -y targetcli && yum clean all
ADD run_iscsi_target.sh /usr/local/bin/
ADD block.tar.gz /

ENTRYPOINT ["/usr/local/bin/run_iscsi_target.sh"]

要点解读:

  • ARG BASEIMAGE 允许构建时注入多架构基础镜像。对应 BASEIMAGE 文件给出了四个平台各自的 Fedora 42 镜像:
    linux/amd64=fedora:42
    linux/arm64=arm64v8/fedora:42
    linux/ppc64le=ppc64le/fedora:42
    linux/s390x=s390x/fedora:42
    
    Kubernetes 官方 e2e 镜像普遍采用这种"按架构映射基础镜像"的方式做多架构发布。
  • 唯一运行时依赖是 targetcli:镜像内并不安装 iscsid 之类的传统用户态 target 守护进程,而是使用 Linux 内核 LIO(Linux-IO Target)的用户态管理工具 targetcli 直接在内核侧配置 target。
  • ENTRYPOINT 不接收 shell:容器启动时直接执行 /usr/local/bin/run_iscsi_target.sh,该脚本会把命令行第一个参数当作 IQN 使用(见下节)。

六、入口脚本:run_iscsi_target.sh 的 target 配置原理

run_iscsi_target.sh 是整个容器的核心。脚本注释明确说明它不运行任何 daemon,只是在内核中配置 iSCSI target,且可以在同一节点上多次运行,每次运行都会创建自己独立的 IQN 与 LUN——这正是 E2E 测试并行创建多个隔离卷服务器的前提。

6.1 启动:注册 IQN 与 LUN

脚本把第一个参数作为 IQN(iSCSI Qualified Name)传入 start 函数:

IQN=$1

function start()
{
    # Create new IQN (iSCSI Qualified Name)
    targetcli /iscsi create "$IQN"
    # Run it in demo mode, i.e. no authentication
    targetcli /iscsi/"$IQN"/tpg1 set attribute authentication=0 demo_mode_write_protect=0 generate_node_acls=1 cache_dynamic_acls=1

    # Create unique "block volume" (i.e. flat file) on the *host*.
    cp /block /srv/iscsi/"$IQN"

    # Make the block volume available through our IQN as LUN 0
    targetcli /backstores/fileio create block-"$IQN" /srv/iscsi/"$IQN"
    targetcli /iscsi/"$IQN"/tpg1/luns create /backstores/fileio/block-"$IQN"

    echo "iscsi target started"
}

逐步拆解:

  1. targetcli /iscsi create "$IQN" —— 创建 IQN 对应的 target portal group。
  2. 一组属性配置让测试无需鉴权即可直连:
    • authentication=0:关闭 CHAP 认证(测试环境下最省事);
    • demo_mode_write_protect=0:关闭 demo 模式的写保护,允许客户端写入;
    • generate_node_acls=1 / cache_dynamic_acls=1:动态为任意发起端生成并缓存访问控制列表,使任何 initiator 都能直接访问而无需预先手工加 ACL。
  3. 块设备文件必须放在宿主机而非容器内:脚本将镜像内预置的 /block 拷贝为 /srv/iscsi/$IQN。注释解释了原因——若块文件存在于容器文件系统内,内核会因某种原因无法同时为多个容器提供多个 LUN;因此 /srv/iscsi 必须是从宿主 bind-mount 进来的目录(/srv/iscsi must be bind-mount from the host)。
  4. 用 fileio backstore 把该文件注册为块存储 block-$IQN,再把它作为 LUN 0 关联到目标 tpg1 上。

配置成功后打印 iscsi target started——这个字符串正是 E2E 框架等待的服务就绪信号(见第七节 ServerReadyMessage)。

6.2 停止:信号驱动的清理逻辑

function stop()
{
    echo "stopping iscsi target"
    # Remove IQN
    targetcli /iscsi/"$IQN"/tpg1/luns/ delete 0
    targetcli /iscsi delete "$IQN"
    # Remove block device mapping
    targetcli /backstores/fileio delete block-"$IQN"
    /bin/rm -f /srv/iscsi/"$IQN"
    echo "iscsi target stopped"
    exit 0
}

trap stop TERM
start

while true; do
    sleep 1
done

脚本主体是一个"注册 TERM 信号陷阱 → 执行 start → 空转保活"的结构:启动完成后进入 while true; sleep 1 的无限循环让容器保持存活;当 Pod 被删除/终止时收到 TERM 信号,stop 按逆序清理 LUN、IQN、fileio backstore 与宿主机上的块文件,做到服务端退出后内核与宿主机不留残留状态

七、E2E 框架如何调度该镜像

要理解这套镜像的价值,需要看它在真实测试链路里的调用点。核心入口在 test/e2e/storage/drivers/in_tree.go

7.1 驱动与卷源

// iSCSI
// The iscsiadm utility and iscsi target kernel modules must be installed on all nodes.
type iSCSIDriver struct {
	driverInfo storageframework.DriverInfo
}

InitISCSIDriver() 把驱动能力注册为:in-tree 插件名 kubernetes.io/iscsi,标记 feature.Volumes,支持默认 fsType 与 ext4,并声明 CapPersistence/CapFsGroup/CapBlock/CapExec/CapMultiPODs/CapTopology/CapMultiplePVsSameID 等一系列能力——其中 CapBlock 对应块设备模式测试。驱动测试文件类型时生成如下卷源:

volSource := v1.VolumeSource{
    ISCSI: &v1.ISCSIVolumeSource{
        TargetPortal: "127.0.0.1:3260",
        IQN:          iv.iqn,
        Lun:          0,
        ReadOnly:     readOnly,
    },
}

注意 TargetPortal 固定写为 127.0.0.1:3260、LUN 恒为 0:因为测试客户端被强制调度到与 target 服务端相同的节点上(见下节),通过回环地址即可访问 hostNetwork 上监听的 3260 端口,这与镜像脚本把块设备暴露为 LUN 0 的设计完全咬合。

7.2 服务端 Pod 的创建参数

newISCSIServer(同一文件第 361 行起)按集群内唯一命名空间生成 IQN:

// Generate cluster-wide unique IQN
iqn = fmt.Sprintf(iSCSIIQNTemplate, namespace)

其中模板常量定义为:

// Template for iSCSI IQN.
iSCSIIQNTemplate = "iqn.2003-01.io.k8s:e2e.%s"

即形如 iqn.2003-01.io.k8s:e2e.<namespace> 的全局唯一 IQN,正好对应脚本首个参数。随后构造服务端配置:

config = e2evolume.TestConfig{
    Namespace:   namespace,
    Prefix:      "iscsi",
    ServerImage: imageutils.GetE2EImage(imageutils.VolumeISCSIServer),
    ServerArgs:  []string{iqn},
    ServerVolumes: map[string]string{
        // iSCSI container needs to insert modules from the host
        "/lib/modules": "/lib/modules",
        // iSCSI container needs to configure kernel
        "/sys/kernel": "/sys/kernel",
        // iSCSI source "block devices" must be available on the host
        "/srv/iscsi": "/srv/iscsi",
        // targetcli uses dbus
        "/run/dbus": "/run/dbus",
    },
    ServerReadyMessage: "iscsi target started",
    ServerHostNetwork:  true,
}

这里把 README 中"挂载 /lib/modules"的约束落实为 /lib/modules/sys/kernel(内核配置接口)、/srv/iscsi(块文件存放目录)、/run/dbustargetcli 依赖 dbus)四个宿主目录的 bind-mount,并指定服务就绪消息为脚本打印的 iscsi target started。容器参数正是脚本入口需要的 IQN。

随后框架通过 test/e2e/framework/volume/fixtures.go 中的 CreateStorageServer 启动该 Pod 并取其 Pod IP:

func CreateStorageServer(ctx context.Context, cs clientset.Interface, config TestConfig) (pod *v1.Pod, ip string) {
	pod = startVolumeServer(ctx, cs, config)
	...
	ip = pod.Status.PodIP
	...
	return pod, ip
}

7.3 客户端与服务端的同节点绑定

由于 iSCSI 走内核 target 且测试环境通常不开防火墙,E2E 把客户端限制在与服务端同一节点上:

// Make sure the client runs on the same node as server so we don't need to open any firewalls.
config.ClientNodeSelection = e2epod.NodeSelection{Name: pod.Spec.NodeName}

这就解释了为什么卷源中 target portal 可以直接使用 127.0.0.1:3260 回环地址——客户端与服务端同机,走宿主回环即可命中 target。同时 ServerHostNetwork: true 保证服务端 Pod 直接监听宿主的 3260 端口。

八、使用与二次构建要点汇总

综合以上源码分析,可归纳出这套 iSCSI 测试镜像的完整使用与构建流程:

  1. 构建期(离线一次性完成):以 root 运行 create_block.sh 生成 block.tar.gz(内含 120MB ext2 文件与 index.html,内容为 Hello from iscsi);随后按 Dockerfile 构建镜像:安装 targetcli、放入入口脚本与块设备压缩包。
  2. 运行期(每个测试用例动态执行):把镜像作为 privileged 容器启动,传入唯一 IQN 作为第一个参数,并 bind-mount /lib/modules/sys/kernel/srv/iscsi(及 dbus 目录)到容器;容器启动即通过 targetcli 在宿主内核注册 LUN 0 并打印 iscsi target started
  3. 对测试客户端:在宿主已安装 iscsiadm 与 target 内核模块的前提下,客户端通过 kubernetes.io/iscsi 卷插件指向 127.0.0.1:3260 + 该 IQN + LUN 0 即可挂载由 index.html 标识内容的 ext2 卷。
  4. 清理期:Pod 收到 TERM 信号后执行 stop,逆序删除 LUN、IQN、fileio backstore 与宿主块文件;由于每次运行使用独立 IQN,同一节点可并行运行多个彼此隔离的 target 实例。

若要在自己的集群中复现这一套"容器化内核态存储服务器"方案,只需遵循两条原则:特权模式 + 宿主目录透传。这也是 Linux 内核 LIO 无法像用户态 NFS 那样被完全容器化的根本原因——target 配置本质上是内核对象,容器只是配置内核的工具载体。

九、小结

本文从 test/images/volume/iscsi/README.md 出发,把这条仅有十几行的简短说明还原成了一个完整的工程方案:一个以 Fedora 42 为基础、以 targetcli 驱动 Linux 内核 LIO、以预置 120MB ext2 块设备作为卷后端、以 IQN 作为隔离边界的 iSCSI target 容器。它体现了 Kubernetes 测试基础设施中一个常见设计模式——用容器化、参数化的"存储服务器"镜像,为 in-tree 卷插件提供隔离、可重复、可并发的真实存储后端。对希望理解 Kubernetes 存储测试架构、或自行搭建 iSCSI 目标做验证的开发者,这个目录都是值得精读的范本。

登录后查看全文
热门项目推荐
相关项目推荐