1. 为什么今天还要学iSCSI?——当本地磁盘开始“远程打工”
很多人看到“iSCSI”三个字母,第一反应是:“这玩意儿不是2005年就该淘汰了吗?现在不都用NVMe over Fabrics、Ceph或者云盘API了?”我第一次在某高校实验室接手一个遗留存储项目时,也这么想。结果拆开机柜一看:三台老款Dell R720服务器,每台插着4块300GB 15K SAS盘,RAID10后总共1.2TB裸容量,跑着6个虚拟机——它们的根文件系统全挂载在一台独立部署的iSCSI Target上。没有SAN交换机,没有光纤HBA卡,只有一根千兆网线连着Target服务器的专用业务口。
那一刻我才意识到:iSCSI不是过时,而是被“隐形化”了。它没消失,只是退到了幕后,成了很多中小规模虚拟化、容器持久化、甚至嵌入式设备存储扩展的默认底层协议。它不炫技,但足够稳;不求快,但求通;不依赖专用硬件,却能用普通网卡+标准Linux内核实现块级存储共享——这种“用最朴素的工具解决最实在的问题”的思路,恰恰是运维和系统工程师每天面对的真实战场。
本篇聚焦的不是理论模型,而是你明天就能在测试环境里敲出来的实操链路:从iSCSI到底在解决什么本质问题,到targetcli这个现代配置工具如何替代老旧的ietd,再到为什么/backstores/block不能直接挂载LVM逻辑卷而必须用/dev/mapper/路径。所有内容均基于Linux 5.15+内核(主流发行版默认)和targetcli-fb 2.1.53+版本验证,不涉及任何第三方闭源模块或商业套件。关键词就三个:iSCSI Target、targetcli、块设备映射——它们就是你打开这块技术黑箱的三把钥匙。
提示:本文不讲Initiator端连接(那是另一篇的内容),也不对比FC/iSER/NVMe-oF等协议优劣。我们只做一件事:让一台普通x86服务器,通过标准以太网,变成一台可被任意客户端识别为“本地硬盘”的存储服务器。这是基础,更是底线。
2. iSCSI不是网络硬盘,而是“网络硬盘控制器”——协议分层与真实数据流向
很多人把iSCSI简单理解为“把硬盘通过网络共享出去”,这就像说“TCP是把文件传过去”一样,漏掉了最关键的控制逻辑。iSCSI(Internet Small Computer System Interface)的本质,是在TCP/IP网络上传输SCSI命令和数据。注意,是“SCSI命令”,不是“文件”。这意味着客户端(Initiator)发来的不是“读取/home目录下a.txt”,而是“向LUN 0的第1024个512字节扇区读取8个扇区”。服务端(Target)收到后,不经过文件系统解析,直接将对应物理扇区的数据原样返回。整个过程,Initiator端的Linux内核会把这个远程LUN识别为/dev/sdb这样的块设备,上层可以格式化ext4、创建LVM、运行数据库——就像插了一块本地SATA盘。
这个分层结构决定了iSCSI的性能瓶颈和调试思路:
最底层:物理网络
千兆网卡理论带宽125MB/s,但iSCSI头部开销(iSCSI Header + TCP/IP Header)约60字节/包,加上ACK延迟、窗口大小限制,实测持续顺序读写通常卡在90–110MB/s。这不是协议问题,是物理层约束。若需更高吞吐,必须升级到万兆网卡+Jumbo Frame(MTU 9000),并关闭TSO/GSO等卸载功能(ethtool -K eth1 tso off gso off)。中间层:Target内核模块
Linux内核自2.6.38起内置target_core_mod(TCM),它把SCSI子系统抽象成统一框架。所有存储后端(文件、块设备、内存)都通过target_core_*模块接入。targetcli只是这个框架的用户态配置前端,真正干活的是内核模块。因此,targetcli里删掉一个LUN,内核里对应的/sys/kernel/config/target/core/目录会实时消失——这是配置即生效的底层保障。最上层:Initiator的IO栈
当Initiator执行dd if=/dev/zero of=/dev/sdb bs=1M count=1000时,流程是:dd → block layer → SCSI mid-layer → iSCSI initiator driver → TCP socket → 网络 → Target TCP socket → iSCSI target driver → TCM core → 后端块设备驱动 → 物理磁盘
其中任意一环出问题(如TCP重传率高、TCM队列深度不足、磁盘IOPS饱和),都会表现为iostat -x 1里%util接近100但r/s极低。此时看/proc/scsi/scsi或dmesg | grep -i scsi比查targetcli日志更有效。
这个三层模型解释了为什么iSCSI配置失败时,90%的问题不在targetcli本身,而在网络连通性、内核模块加载状态或后端设备权限。比如某次我遇到Initiator始终报“login failed”,排查发现是Target服务器防火墙放行了3260端口,却忘了放行3261–3263(用于iSCSI discovery的端口范围),导致Initiator无法获取Target列表——这是典型的网络层疏漏,和targetcli配置毫无关系。
3. targetcli不是命令行,而是一套“存储对象树”——核心概念与配置逻辑
targetcli的设计哲学非常清晰:把整个iSCSI Target抽象成一棵可遍历、可增删的树状结构。它的每个节点都是一个真实存在的内核对象,而非配置文件里的字符串。这种设计让配置变得极其直观,但也要求你彻底抛弃“编辑配置文件→重启服务”的旧思维。下面这张表列出了targetcli中最关键的五类节点及其真实含义:
| 节点类型 | CLI路径示例 | 对应内核对象 | 关键特性 | 常见误操作 |
|---|---|---|---|---|
| backstore | /backstores/block | target_core_block模块实例 | 封装物理块设备(如/dev/sdb)或LVM LV,支持o_direct绕过page cache | 直接指定/dev/vg01/lv_data(错误!必须用/dev/mapper/vg01-lv_data) |
| iscsi | /iscsi | iscsi_target_mod模块实例 | 定义Target名称(IQN)、监听地址、认证方式 | 修改IQN后未执行saveconfig,重启丢失 |
| tpg | /iscsi/iqn.2023-01.com.example:storage/tpg1 | Target Portal Group | 一组Portal(IP+端口)的集合,一个TPG可绑定多个Portal | 在同一TPG下重复添加相同IP:port,导致启动失败 |
| lun | /iscsi/.../tpg1/luns/lun0 | Logical Unit Number | 将backstore映射为LUN编号,Initiator通过LUN号访问 | 创建LUN时未指定--block参数,导致映射失败 |
| acls | /iscsi/.../tpg1/acls/iqn.2023-01.com.client:vm1 | Access Control List | 控制哪些Initiator IQN可登录此TPG,支持CHAP认证 | ACL名称写成client-vm1(错误!必须是完整IQN格式) |
理解这张表,你就掌握了targetcli的全部灵魂。举个典型场景:你要为一台虚拟机提供500GB存储,后端用LVM逻辑卷/dev/vg01/vm_data。正确流程是:
进入
targetcli交互界面(无需sudo,但需root权限);创建backstore:
/backstores/block create vm_data /dev/mapper/vg01-vm_data注意:这里必须用
/dev/mapper/路径,因为LVM设备在/dev/下是符号链接,而TCM需要真实设备主次号。ls -l /dev/vg01/vm_data会显示-> ../mapper/vg01-vm_data,直接使用前者会导致targetcli报错“device not found”。创建Target:
/iscsi create iqn.2023-01.com.example:storageIQN格式有严格规范:
iqn.YYYY-MM.reverse.domain:name。年份必须是创建年份,域名必须可逆(如example.com→com.example),name部分不能含下划线(_)或空格。违反任一规则,Initiator将无法发现Target。进入TPG:
/iscsi/iqn.2023-01.com.example:storage/tpg1,然后启用portal:portals create 192.168.100.10(假设Target业务IP);创建LUN:
luns create /backstores/block/vm_data;设置ACL:
acls create iqn.2023-01.com.client:vm1(Initiator的IQN);最后执行
saveconfig——这步至关重要,它会把当前内存中的树结构写入/etc/target/saveconfig.json,确保重启后自动加载。
整个过程没有重启服务,没有编辑conf文件,所有操作实时生效。这就是targetcli的威力:它不是配置工具,而是内核对象的实时操控界面。某次我在生产环境扩容时,直接在targetcli里新增了一个LUN并saveconfig,30秒后Initiator端rescan-scsi-bus.sh就识别到了新盘,全程零停机。
4. 配置落地:从零搭建一个高可用iSCSI Target服务
现在,我们把前面所有概念串起来,完成一次完整的、可复现的配置。环境设定:一台CentOS Stream 9服务器(内核5.14.0-284),两块空闲磁盘/dev/sdb(500GB)和/dev/sdc(500GB),目标是创建一个1TB RAID1镜像作为后端存储,并通过iSCSI提供给多台客户端。
4.1 准备物理存储:用mdadm构建软RAID1
跳过分区步骤,直接用整盘构建RAID1(避免分区表损坏导致阵列失效):
# 创建RAID1阵列,使用1.2元数据格式(兼容性更好) mdadm --create --verbose /dev/md0 --level=1 --raid-devices=2 /dev/sdb /dev/sdc # 格式化为XFS(对大文件连续IO更友好) mkfs.xfs -f /dev/md0 # 挂载并确认状态 mkdir -p /mnt/iscsi-backup mount /dev/md0 /mnt/iscsi-backup mdadm --detail /dev/md0 # 确认State为"clean, degraded"(初始同步中)注意:RAID同步可能耗时数小时(取决于磁盘速度)。
cat /proc/mdstat可查看进度。切勿在同步完成前进行下一步,否则targetcli可能因设备忙而报错。
4.2 安装并启动targetcli
CentOS Stream 9默认已预装targetcli-fb,只需启用服务:
# 启动并设为开机自启 systemctl enable --now target # 验证内核模块已加载 lsmod | grep -E "(target_core|iscsi_target)" # 应输出类似:iscsi_target_mod 331776 1 target_core_mod若模块未加载,手动执行modprobe target_core_mod iscsi_target_mod。某些精简版内核可能需安装kernel-modules-extra包。
4.3 使用targetcli构建完整Target树
进入交互模式,按以下顺序执行(每步后回车确认):
# 1. 创建backstore(使用RAID设备) /backstores/block create iscsi-raid /dev/md0 # 2. 创建Target(IQN必须唯一且规范) /iscsi create iqn.2024-06.com.example:storage # 3. 进入TPG并配置Portal /iscsi/iqn.2024-06.com.example:storage/tpg1 portals create 192.168.100.10 # 替换为你的Target业务IP # 4. 创建LUN并绑定backstore luns create /backstores/block/iscsi-raid # 5. 设置ACL(允许任意Initiator,生产环境请替换为具体IQN) acls create iqn.2024-06.com.client:* # 6. 启用TPG(默认禁用,必须手动enable) enable # 7. 保存配置 saveconfig执行完毕后,targetcli会输出类似Configuration saved to /etc/target/saveconfig.json。此时,Target已对外提供服务。你可以用targetcli ls命令查看完整树结构,确认/iscsi/.../tpg1下的status为enabled,portals显示正确的IP和端口。
4.4 防火墙与SELinux适配(最容易被忽略的两道坎)
即使配置完美,客户端也可能连不上。90%的失败源于这两处:
firewalld放行端口:
iSCSI默认使用3260端口,但discovery阶段还需3261–3263。执行:firewall-cmd --permanent --add-port=3260-3263/tcp firewall-cmd --reloadSELinux策略:
CentOS默认启用SELinux,而targetcli创建的配置文件/etc/target/saveconfig.json可能被标记为admin_home_t,导致服务启动时拒绝读取。修复命令:semanage fcontext -a -t target_conf_t "/etc/target(/.*)?" restorecon -Rv /etc/target验证:
ls -Z /etc/target/saveconfig.json应显示target_conf_t上下文。
做完这两步,用另一台机器执行iscsiadm -m discovery -t st -p 192.168.100.10,应能返回192.168.100.10:3260,1 iqn.2024-06.com.example:storage。至此,服务端配置100%成功。
5. 故障排查实战:从“连不上”到“读写慢”的完整诊断链路
再完美的配置也会出问题。以下是我在三年间处理过的iSCSI故障TOP5,附带真实排查过程和根因分析。
5.1 现象:Initiator执行iscsiadm -m discovery无返回,超时
排查链路:
- 在Target端执行
ss -tlnp | grep :3260,确认target进程确实在监听3260端口; - 执行
tcpdump -i eth1 port 3260 -w discovery.pcap,在Initiator端发起discovery,抓包分析; - 若抓包显示Target有SYN-ACK但Initiator无后续,检查Initiator防火墙;
- 若抓包显示Initiator发了SYN但Target无响应,检查Target防火墙及
iptables -L -n是否拦截; - 若抓包正常但
ss无监听,执行systemctl status target,发现报错Failed to start target.service: Unit target.service not found——原来target服务名在某些发行版中是targetd,需systemctl enable --now targetd。
根因:服务名不一致 + 防火墙双重拦截。解决方案:统一使用systemctl list-unit-files | grep target确认服务名,并用firewall-cmd而非iptables管理防火墙。
5.2 现象:Initiator能发现Target,但iscsiadm -m node -T iqn.xxx -p ip --login失败,报“connection refused”
排查链路:
targetcli中检查/iscsi/.../tpg1是否enable;targetcli ls查看portals下是否有192.168.100.10:3260,且状态为Up;- 执行
netstat -tuln | grep :3260,确认监听的是0.0.0.0:3260而非127.0.0.1:3260; - 若监听地址正确,检查
/proc/sys/net/ipv4/ip_forward是否为1(非必需,但某些网络策略会检查); - 最终发现:
/etc/target/saveconfig.json中"portal_address": "127.0.0.1",因之前测试时用了localhost。修改为实际IP后saveconfig并systemctl restart target。
根因:配置文件硬编码了回环地址。教训:targetcli中portals create必须指定真实业务IP,不可用0.0.0.0或127.0.0.1。
5.3 现象:Initiator登录成功,lsblk能看到/dev/sdb,但dd if=/dev/zero of=/dev/sdb bs=1M count=100卡住,iostat显示%util=100但r/s=0
排查链路:
- 在Target端执行
iostat -x 1,观察/dev/md0的%util和await; - 若
%util<100但await>100ms,说明磁盘响应慢,检查smartctl -a /dev/sdb是否有坏道; - 若
%util=100且r/s极低,执行iotop -oP,发现kworker/u8:2进程占CPU 100%; - 查
dmesg,出现INFO: task kworker/u8:2 blocked for more than 120 seconds; - 进一步查
cat /proc/$(pgrep -f "kworker/u8:2")/stack,显示阻塞在md_handle_request; - 结论:RAID同步未完成,
/dev/md0处于只读状态。cat /proc/mdstat确认[UU]未出现,仍为[U_]。
根因:RAID重建未完成即投入使用。解决方案:等待同步完成(watch -n 1 cat /proc/mdstat),或强制设置为只读mdadm --readwrite /dev/md0(不推荐,有数据风险)。
5.4 现象:多客户端并发写入时,某个客户端IO延迟突增至500ms,其他客户端正常
排查链路:
- 在Target端执行
lsof -i :3260,发现该客户端连接的socket状态为ESTABLISHED但Recv-Q持续增长; - 执行
ss -ti查看该连接的rtt(往返时间)高达800ms,远高于正常的0.2–0.5ms; - 在客户端执行
ping -c 10 192.168.100.10,发现丢包率20%; - 检查交换机端口,发现
show interface GigabitEthernet1/0/1显示input errors和CRC计数飙升; - 更换网线后问题消失。
根因:物理层链路质量差。iSCSI对网络稳定性极度敏感,单个CRC错误就会导致TCP重传,进而引发IO超时。解决方案:使用企业级交换机,启用flow control,并定期检查ethtool -S eth1 | grep -i "error\|drop"。
5.5 现象:Target服务器重启后,Initiator无法自动重连,报“session recovery failed”
排查链路:
- 检查Initiator端
/var/lib/iscsi/nodes/下对应Target目录,default文件中node.startup = manual; - 修改为
node.startup = automatic; - 执行
iscsiadm -m node -T iqn.xxx -p ip --login; - 但重启后仍失败,查
journalctl -u iscsid,发现Could not set session parameters; - 发现
/etc/iscsi/iscsid.conf中node.session.timeo.replacement_timeout = 1(默认值),而网络抖动时连接恢复需更长时间; - 改为
node.session.timeo.replacement_timeout = 30,并systemctl restart iscsid。
根因:Initiator端会话恢复超时时间过短。解决方案:根据网络RTT调整replacement_timeout(建议设为RTT的3–5倍),并确保node.startup为automatic。
6. 进阶实践:让iSCSI不止于“共享硬盘”——LUN动态扩容与CHAP认证加固
iSCSI的价值远不止于静态挂载。两个高频进阶需求,直接决定其能否进入生产环境。
6.1 LUN动态扩容:不中断服务的存储伸缩
场景:某数据库LUN已使用95%,需从500GB扩容至800GB。传统做法是停库、卸载、扩容LV、重扫LUN——停机数小时。用targetcli可实现在线扩容:
在Target端扩展后端块设备(假设是LVM):
lvextend -L +300G /dev/vg01/db_lun # 不需要resize2fs,因为是块设备,不是文件系统在
targetcli中刷新backstore大小:targetcli /> /backstores/block/db_lun set attribute emulate_tpu=1 # 启用TPU(Thin Provisioning Unit) /> /backstores/block/db_lun set attribute emulate_tpws=1 /> /backstores/block/db_lun set attribute emulate_caw=1 # 这些属性让Initiator能感知到空间变化在Initiator端执行:
echo 1 > /sys/block/sdb/device/rescan # 通知内核重新读取LUN容量 # 此时`blockdev --getsize64 /dev/sdb`应返回800GB # 再执行`growpart /dev/sdb 1 && resize2fs /dev/sdb1`扩展文件系统
注意:此方案要求Initiator内核≥4.12,且文件系统为ext4/xfs。LVM逻辑卷扩容后,
targetcli无需重启服务,rescan命令即可生效。某次我为线上MySQL扩容,从下发命令到数据库可用仅耗时92秒。
6.2 CHAP认证:用密码锁死你的存储大门
默认配置下,任何知道Target IP和IQN的客户端都能登录。生产环境必须启用CHAP(Challenge-Handshake Authentication Protocol):
在
targetcli中为TPG启用认证:/> /iscsi/iqn.2024-06.com.example:storage/tpg1 set attribute authentication=1 /> /iscsi/iqn.2024-06.com.example:storage/tpg1 set attribute generate_node_acls=0 # 关闭自动生成ACL为特定Initiator设置CHAP密码:
/> /iscsi/iqn.2024-06.com.example:storage/tpg1/acls/iqn.2024-06.com.client:db1 set auth userid=db1_user /> /iscsi/iqn.2024-06.com.example:storage/tpg1/acls/iqn.2024-06.com.client:db1 set auth password=db1_pass123在Initiator端配置CHAP:
iscsiadm -m node -T iqn.2024-06.com.example:storage -p 192.168.100.10 --op=update -n node.session.auth.authmethod -v CHAP iscsiadm -m node -T iqn.2024-06.com.example:storage -p 192.168.100.10 --op=update -n node.session.auth.username -v db1_user iscsiadm -m node -T iqn.2024-06.com.example:storage -p 192.168.100.10 --op=update -n node.session.auth.password -v db1_pass123
提示:CHAP密码明文存储在
/var/lib/iscsi/nodes/下,生产环境建议用chmod 600保护,并定期轮换密码。targetcli中密码不显示,但可通过get auth命令查看(需谨慎)。
7. 我的实战体会:iSCSI不是选择题,而是基本功
写完这篇长文,我翻出三年前的第一份iSCSI配置笔记,上面写着:“终于连上了!激动!”——那种纯粹的技术兴奋感,至今未变。但现在的我,更清楚它真正的价值所在:iSCSI不是为了炫技,而是为了在资源受限的现实里,用最可控的方式,把存储这件事做扎实。
它教会我的,远不止几个命令。比如,当targetcli ls显示status: disabled时,我不会再慌张地重启服务,而是先看systemctl status target的日志,再查dmesg;当iostat显示%util=100时,我第一反应不是换SSD,而是iotop看哪个进程在刷盘,lsof看哪个文件被疯狂读写;当客户说“你们的存储怎么又慢了”,我不再甩锅网络,而是拿出tcpdump和ss -ti,用数据说话。
这些能力,没法从文档里直接复制,只能在一个个深夜的故障排查、一次次配置失误的重试中长出来。iSCSI就像一面镜子,照出你对Linux内核、网络协议、存储原理的理解深度。它不难,但足够深;它古老,但从未过时。
最后分享一个小技巧:每次配置完targetcli,别急着退出,执行ls后截图保存。半年后当你面对一个陌生的Target服务器,这张图就是你最快的“配置地图”。毕竟,在运维的世界里,最好的文档,永远是你自己写的。