返回主页

iSCSI 多路径 + SBD + HA Cluster 部署文档

项目编号: 26-08-10
部署日期: 2026-08-10
OS: Red Hat Enterprise Linux 10 (EL10.2)
核心组件: LIO (targetcli) + open-iscsi + device-mapper-multipath + Pacemaker/Corosync + SBD + Apache httpd

一、环境信息

1.1 三台 VM 网络拓扑

主机角色管理 IPiSCSI Portal 1iSCSI Portal 2
storage-aiSCSI Target10.123.6.128/2410.10.40.250/16192.168.200.250/24
hanode1Pacemaker Node 110.123.6.222/2410.10.40.241/16192.168.200.241/24
hanode2Pacemaker Node 210.123.6.170/2410.10.40.242/16192.168.200.242/24

统一凭据: root / redhat123 (实验环境,VM 集群)
VIP 资源 (HA): 10.10.40.100/16 (在管理网卡 ens34 上)

1.2 存储规划 (storage-a 上的 /dev/sda)

分区设备容量用途
1/dev/sda120 GBSBD 设备 (二节点共享, fence)
2/dev/sda240 GBshare1 (备用)
3/dev/sda360 GBshare2 (HA web 服务) → 9.2 web_fs 挂到 /var/www/html(对齐 httpd 默认 DocumentRoot)
4/dev/sda460 GBshare3 (备用)
注: 文档以实际 VM 状态为准 (空盘 = /dev/sda,系统盘 = /dev/sdb)。 网卡命名也以实际 VM 状态为准 (ens33/ens34 等)。

二、Stage 1 — storage-a 分区

前置: storage-a 上 /dev/sda 是新加的 200G 空盘。

parted -s /dev/sda mklabel gpt
parted -s /dev/sda mkpart primary xfs 0% 10%       # 20G
parted -s /dev/sda mkpart primary xfs 10% 30%      # 40G
parted -s /dev/sda mkpart primary xfs 30% 60%      # 60G
parted -s /dev/sda mkpart primary xfs 60% 90%      # 60G
# 留 10% free 给未来扩容
partprobe /dev/sda
lsblk /dev/sda
parted -s /dev/sda print

输出:

NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sda      8:0    0  200G  0 disk
├─sda1   8:1    0   20G  0 part
├─sda2   8:2    0   40G  0 part
├─sda3   8:3    0   60G  0 part
└─sda4   8:4    0   60G  0 part

Number  Start   End     Size    File system  Name     Flags
 1      1049kB  21.5GB  21.5GB               primary
 2      21.5GB  64.4GB  42.9GB               primary
 3      64.4GB  129GB   64.4GB               primary
 4      129GB   193GB   64.4GB               primary

三、Stage 2 — targetcli 建 4 LUN + 双 portal

# 1. 启动 target 服务
systemctl enable --now target

# 2. 创建 4 个 block backstore
targetcli /backstores/block create sda1_block dev=/dev/sda1
targetcli /backstores/block create sda2_block dev=/dev/sda2
targetcli /backstores/block create sda3_block dev=/dev/sda3
targetcli /backstores/block create sda4_block dev=/dev/sda4

# 3. 建 target IQN
targetcli /iscsi create iqn.2026-08.com.example:storage-a.target0
TARGET=iqn.2026-08.com.example:storage-a.target0

# 4. 配 target 属性
targetcli /iscsi/$TARGET/tpg1 set attribute generate_node_acls=1
targetcli /iscsi/$TARGET/tpg1 set attribute demo_mode_write_protect=0

# 5. 配双 portal (先删默认 0.0.0.0)
targetcli /iscsi/$TARGET/tpg1/portals delete 0.0.0.0 ip_port=3260
targetcli /iscsi/$TARGET/tpg1/portals create 10.10.40.250 ip_port=3260
targetcli /iscsi/$TARGET/tpg1/portals create 192.168.200.250 ip_port=3260

# 6. 在 hanode1 + hanode2 启动 iscsid 后拿到 IQN,加 ACL
INI1=$(ssh root@10.10.40.241 'cat /etc/iscsi/initiatorname.iscsi' | grep InitiatorName | cut -d= -f2)
INI2=$(ssh root@10.10.40.242 'cat /etc/iscsi/initiatorname.iscsi' | grep InitiatorName | cut -d= -f2)
targetcli /iscsi/$TARGET/tpg1/acls create $INI1
targetcli /iscsi/$TARGET/tpg1/acls create $INI2

# 7. 创建 4 LUN
targetcli /iscsi/$TARGET/tpg1/luns create /backstores/block/sda1_block lun=01
targetcli /iscsi/$TARGET/tpg1/luns create /backstores/block/sda2_block lun=02
targetcli /iscsi/$TARGET/tpg1/luns create /backstores/block/sda3_block lun=03
targetcli /iscsi/$TARGET/tpg1/luns create /backstores/block/sda4_block lun=04

# 8. 持久化
targetcli saveconfig

# 9. 验证
ss -tnlp | grep 3260

输出:

LISTEN 0      256       10.10.40.250:3260      0.0.0.0:*
LISTEN 0      256    192.168.200.250:3260      0.0.0.0:*

四、Stage 3 — iSCSI 双节点发现 + login

在 hanode1 和 hanode2 都执行 (脚本相同):

systemctl enable --now iscsid
iscsiadm -m discovery -t sendtargets -p 10.10.40.250:3260
iscsiadm -m discovery -t sendtargets -p 192.168.200.250:3260
iscsiadm -m node -l
iscsiadm -m session

# 验证 (应见 2 条 session,8 个 sd 设备)
lsblk

输出 (hanode1):

tcp: [1] 10.10.40.250:3260,1 iqn.2026-08.com.example:storage-a.target0 (non-flash)
tcp: [2] 192.168.200.250:3260,1 iqn.2026-08.com.example:storage-a.target0 (non-flash)

NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sdb      8:16   0   60G  0 disk      ← LUN04 path1
sdc      8:32   0   60G  0 disk      ← LUN04 path2
sdd      8:48   0   60G  0 disk      ← LUN03 path1
sde      8:64   0   60G  0 disk      ← LUN03 path2
sdg      8:96   0   40G  0 disk      ← LUN02 path1
sdh      8:112  0   20G  0 disk      ← LUN01 path1
sdi      8:128  0   20G  0 disk      ← LUN01 path2

五、Stage 4 — multipathd 配置 (★ 两节点都跑 ★,内容相同)

★ 关键改进(2026-08-13):multipath alias + WWID 锁死设备名。reboot / 多路径重建 / 后端磁盘变了 —— /dev/mapper/share2 这个名字永远不变
这是取代 UUID= 的更好方案: 前提: user_friendly_names no(让 alias 生效,不然 mpath{a,b,c} 覆盖)
cat > /etc/multipath.conf << 'EOF'
defaults {
    user_friendly_names     no      # ★ 必须 no,让 multipaths { alias } 生效
    find_multipaths         yes
    polling_interval        5
    path_selector           "service-time 0"
    path_grouping_policy    multibus
    path_checker            tur
    failback                immediate
    no_path_retry           5
    checker_timeout         5
    fast_io_fail_tmo        5
}

blacklist {
    devnode "^sda[0-9]*"   # 排除本地系统盘
}

# ★ multipath alias:把 WWID 绑到固定别名
#    两节点跑同样这段,share2 在两边都是 /dev/mapper/share2
#    WWID 怎么拿: multipath -ll 第一行的 (3600...)
multipaths {
    multipath {
        wwid "360014052959d472cc0f436fbf4ef5cb2"
        alias sbd           # 20G, 留给 SBD fence
    }
    multipath {
        wwid "36001405f7875d00fa7d4531a2294e5b2"
        alias share1        # 40G, 备用
    }
    multipath {
        wwid "36001405d194710902554d799a2b7fbf9"
        alias share2        # 60G, HA web 服务用
    }
    multipath {
        wwid "360014054486a9d228904c179e03e0e30"
        alias share3        # 60G, 备用
    }
}
EOF

systemctl enable --now multipathd
multipathd reconfigure

# 验证:应见 /dev/mapper/sbd / share1 / share2 / share3
ls -la /dev/mapper/sbd /dev/mapper/share1 /dev/mapper/share2 /dev/mapper/share3
multipath -ll

输出(实测 2026-08-13):

lrwxrwxrwx 1 root root  7 Aug 13 17:16 sbd -> ../dm-0
lrwxrwxrwx 1 root root  7 Aug 13 17:16 share1 -> ../dm-1
lrwxrwxrwx 1 root root  7 Aug 13 17:16 share2 -> ../dm-2
lrwxrwxrwx 1 root root  7 Aug 13 17:16 share3 -> ../dm-3

share2 (36001405d194710902554d799a2b7fbf9) dm-2 LIO-ORG,sdb3_block
size=60G features='1 queue_if_no_path' hwhandler='1 alua' wp=rw
`-+- policy='service-time 0' prio=50 status=active
  |- 33:0:0:3 sdg 8:96 active ready running
  `- 34:0:0:3 sdf 8:80 active ready running

★ WWID 怎么拿? 第一行 Stage 4 跑完后 multipath -ll,每个 mpath 设备的第一行就是 WWID((36001405xxx) 那个长 hex 串)。把 4 个 WWID 复制到这个 conf 里。

注意: user_friendly_names yes 会覆盖 alias(走 mpath{a,b,c}),必须 no

size=40G ... mpathc (36001405c13516175d544c0189d3c2dd5) dm-2 LIO-ORG,sda3_block size=60G ... mpathd (36001405d0b2884188834f58933fed5c8) dm-3 LIO-ORG,sda4_block size=60G ...

⚠️ 2026-08-13 reboot 后 mpathX 名字会变(为什么 Stage 5 不能 hardcode mpath 名字):

★ 解决方案(也在 Stage 4):multipaths { multipath { wwid X alias sbd/share1/share2/share3 } } 锁死设备名,从此 /dev/mapper/share2 永久不变。

✅ 4 个 mpath 设备,各 2 active path。mpatha (20G) 留给 SBD,不格式化(/dev/mapper/sbd 永远叫这个)。


六、Stage 5 — 格式化 + 挂载 + fstab

★ Stage 5 大简化(2026-08-13): 用了 Stage 4 的 multipath alias 之后,设备名永远不变: 所以 Stage 5 脚本非常简洁 — 不用再按 size 找、不用取 UUID

6.1 hanode1 (全部 4 设备挂载, share2 跑 web 服务)

# /tmp/stage5-fixed.sh (推送脚本,推荐版本)
# 因为 multipath alias 锁死了设备名,直接用就行
set -e

# 1. 格式化 3 个 share(用 alias 名,稳定)
mkfs -t xfs -f -L share1 /dev/mapper/share1
mkfs -t xfs -f -L share2 /dev/mapper/share2
mkfs -t xfs -f -L share3 /dev/mapper/share3
# /dev/mapper/sbd 不格式化,留给 SBD

# 2. 建挂载点
mkdir -p /share1 /share2 /share3

# 3. 挂载
mount /dev/mapper/share1 /share1
mount /dev/mapper/share2 /share2
mount /dev/mapper/share3 /share3

# 4. fstab 写 UUID(reboot 自动挂)
M1=$(blkid -s UUID -o value /dev/mapper/share1)
M2=$(blkid -s UUID -o value /dev/mapper/share2)
M3=$(blkid -s UUID -o value /dev/mapper/share3)
cat >> /etc/fstab <<EOF
# iSCSI multipath shares (Stage 4 alias 锁死设备名;UUID 来自 mkfs)
UUID=$M1 /share1 xfs _netdev,defaults 0 0
UUID=$M2 /share2 xfs _netdev,defaults 0 0
UUID=$M3 /share3 xfs _netdev,defaults 0 0
EOF

# 5. 验证
df -hT /share1 /share2 /share3
echo "test_share1" > /share1/test.txt
echo "test_share3" > /share3/test.txt
ls -la /share1/test.txt /share3/test.txt

6.2 hanode2 (★ 跑同一个脚本 ★)

★ 重要: hanode2 跑 同一个 stage5-fixed.sh。它会格式化 + 挂所有 3 个 share(包括 share2)。 :如果你打算做 HA 集群(Stage 8),share2 必须留给 Pacemaker 管,不能两边同时挂。 所以这里有 2 个分支:
- A. 只测 iSCSI + multipath(不做 HA):hanode2 也跑完整脚本,share1/2/3 都挂 - B. 要做 HA(Stage 8):hanode2 上 share2 不挂,留到 Stage 8 让 Pacemaker group 控制

6.2.A 不做 HA 路径(本次项目)

# ★ 在 hanode2 跑同样脚本 ★
bash /tmp/stage5-fixed.sh

# 验证:
df -hT /share1 /share2 /share3
# 应见 share1/2/3 都挂上

6.2.B 做 HA 路径(若你决定做)

# ★ 在 hanode2 跑(手动,跳过 share2 mount) ★
mkfs -t xfs -f -L share1 /dev/mapper/share1
mkfs -t xfs -f -L share2 /dev/mapper/share2
mkfs -t xfs -f -L share3 /dev/mapper/share3
mkdir -p /share1 /share3
mount /dev/mapper/share1 /share1
mount /dev/mapper/share3 /share3
# share2 不挂,留给 Pacemaker group 控制 (Stage 8)

# 验证:share1 + share3 挂上,share2 没挂
df -hT /share1 /share3
echo "test_share1" > /share1/test.txt && echo OK
echo "test_share3" > /share3/test.txt && echo OK

七、Stage 6 — Pacemaker + Corosync 安装与配置

6.1 安装包

# ★ 两节点都跑 ★
dnf install -y --nogpgcheck pcs pacemaker pacemaker-cli pacemaker-cluster-libs \
    pacemaker-remote corosync corosynclib resource-agents \
    sbd fence-agents-sbd httpd

6.2 启动 pcsd + 设置 hacluster 密码

# ★ 两节点都跑(用同一密码)★
systemctl enable --now pcsd
echo "hacluster:Redh@t2026!" | chpasswd

6.3 加 hosts (DNS 没解析 hanode1/2)

# ★ 两节点都跑 ★
grep -E "hanode1|hanode2" /etc/hosts || {
    echo "10.10.40.241 hanode1" >> /etc/hosts
    echo "10.10.40.242 hanode2" >> /etc/hosts
}

6.4 cluster 初始化 (★ 在 hanode1 跑 ★,CIB 同步到 hanode2)

# ★ 在 hanode1 跑 ★
pcs host auth hanode1 hanode2 -u hacluster -p "Redh@t2026!"
pcs cluster setup ha_cluster hanode1 hanode2 --start
pcs cluster enable --all
pcs status

输出:

Cluster name: ha_cluster
* Stack: corosync (Pacemaker is running)
* Current DC: hanode1 (version 3.0.1-5.el10_2.1-ff52770) - partition with quorum
* 2 nodes configured

八、Stage 7 — SBD Fence 设备

8.1 加载软狗模块 (两节点)

modprobe softdog
ls -la /dev/watchdog*    # /dev/watchdog + /dev/watchdog0 都应存在

8.2 SBD 初始化(★ 关键:只在 hanode1 create,UUID 必须一致! ★)

关键: SBD header 是共享磁盘上的元数据,两节点创建会产生不同 UUID导致冲突。 只在 hanode1 create 一次,hanode2 直接 dump 验证 UUID 一致
# ★ 在 hanode1 上(用 Stage 4 multipath alias,稳定设备名)
sbd -d /dev/mapper/sbd create
sbd -d /dev/mapper/sbd dump

# 在 hanode2 上验证(也用 alias)
sbd -d /dev/mapper/sbd dump
# UUID 必须跟 hanode1 一致!

8.3 创建 STONITH resource(★ 在 hanode1 跑 ★,CIB 自动同步到 hanode2)

# ★ 在 hanode1 跑 ★(或任一节点都行)
#    ★ 用 alias 设备名 /dev/mapper/sbd (Stage 4 锁死的名字)
pcs stonith create sbd_fence fence_sbd devices=/dev/mapper/sbd
# 注意: 不要加 meta provides=unfencing (fence_sbd 不支持 unfencing mode)
pcs status

验证输出:

* sbd_fence  (stonith:fence_sbd):  Started hanode1

九、Stage 8 — Web 服务 + VIP 资源

9.1 在 share2 上准备 web 内容(★ 在 hanode1 跑 ★,初次一次性)

share2 在 Stage 5 已经格式化 xfs(Label=share2),这一节只是放一个测试 html 文件进去。不需要再格式化(share2 是干净状态)。

说明:web_fs 配的是 directory=/var/www/html(跟 httpd 默认 DocumentRoot 对齐),所以 html 直接放 share2 的 /html/ 子目录。我们 mount 到任意临时位置(不要 mount 到 /var/www,会被 web_fs 之后覆盖)。

# ★ 在 hanode1 跑 ★(一次性初始化)
# 集群还没有启 web_group,所以手动 mount + 写文件 + umount
#    ★ 因为 Stage 4 的 multipath alias,/dev/mapper/share2 是稳定的设备名
mount /dev/mapper/share2 /mnt
mkdir -p /mnt/html
cat > /mnt/html/index.html <<EOF
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>cluster HA</title></head>
<body><h1>你好,这里是 cluster HA</h1></body>
</html>
EOF
umount /mnt
echo "html 内容已写好(share2/html/index.html)"

说明: 9.2 创建 web_group 后,Pacemaker 会自动 mount share2 到 /var/www/html(因为 web_fs 配的是 directory=/var/www/html),你访问 VIP 10.10.40.100 就能看到这个页面。

9.2 创建 web_group (★ 在 hanode1 跑 ★,CIB 自动同步到 hanode2)

3 个资源(vip + fs + server)放一个 group,保证同时启动/迁移。

★ 关键:用 multipath alias 锁死的设备名(Stage 4)

# ★ 在 hanode1 跑 ★(或任一节点都行)
# 1. web_vip(10.10.40.100 这个 VIP 在 10.10.40.0/16 网段里)
pcs resource create web_vip ocf:heartbeat:IPaddr2 \
    ip=10.10.40.100 cidr_netmask=16 nic=ens34 \
    op monitor interval=10s

# 2. web_fs(multipath alias 锁死的 share2 设备)
pcs resource create web_fs ocf:heartbeat:Filesystem \
    device="/dev/mapper/share2" fstype=xfs \
    directory="/var/www/html" options="_netdev" \
    op monitor interval=20s

# 3. web_server(httpd)
pcs resource create web_server ocf:heartbeat:apache \
    configfile=/etc/httpd/conf/httpd.conf \
    op monitor interval=30s

# 4. 把 3 个资源加入 group (启动顺序 vip → fs → server,保证 fs 挂好才启 httpd)
pcs resource group add web_group web_vip web_fs web_server

# 5. 清失败历史(如果之前尝试有错)
pcs resource cleanup web_fs

9.3 验证(★ 任一节点跑 ★)

实测确认 mount /dev/mapper/share2(multipath alias)永久稳定(2026-08-13):

# alias 锁死设备名 — /dev/mapper/share2 是 LUN03 的固定别名
# 多路径重建后 /dev/mapper/mpathX 会变,但 /dev/mapper/share2 永远不变
mkdir -p /mnt_share2_test
mount /dev/mapper/share2 /mnt_share2_test
df -hT /mnt_share2_test
# 输出:
# Filesystem         Type  Size  Used Avail Use% Mounted on
# /dev/mapper/share2 xfs    60G  1.2G   59G   2% /mnt_share2_test

umount /mnt_share2_test

为什么 alias 比 UUID 更好?

9.2 用 device="/dev/mapper/share2" 就是这个原因 — 不需要 UUID,直接用 alias 名

pcs status

输出:

Cluster name: ha_cluster
* Stack: corosync (Pacemaker is running)
* Current DC: hanode1

Full List of Resources:
* sbd_fence  (stonith:fence_sbd):  Started hanode1
* Resource Group: web_group:
  * web_vip      (ocf:heartbeat:IPaddr2):    Started hanode2
  * web_fs       (ocf:heartbeat:Filesystem): Started hanode2
  * web_server   (ocf:heartbeat:apache):     Started hanode2

十、Stage 9 — Web + VIP 验证

用 VIP 访问 web 服务:

curl -s http://10.10.40.100/

输出:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>cluster HA</title>
</head>
<body>
<h1>你好,这里是 cluster HA</h1>
</body>
</html>
✅ VIP 访问成功, 内容符合预期

十一、Stage 10 — HA Failover 验证

10.1 把 hanode2 设为 standby (★ 在 hanode1 跑 ★)

# ★ 在 hanode1 跑 ★
pcs node standby hanode2
sleep 8
pcs status

输出:

Node List:
* Node hanode2: standby
* Online: [ hanode1 ]

Full List of Resources:
* sbd_fence  (stonith:fence_sbd):  Started hanode1
* Resource Group: web_group:
  * web_vip    Started hanode1
  * web_fs     Started hanode1
  * web_server Started hanode1

VIP 仍能访问:

curl -s http://10.10.40.100/ | grep h1
<h1>你好,这里是 cluster HA</h1>

10.2 恢复 hanode2

pcs node unstandby hanode2
sleep 5
pcs status
✅ HA Failover 验证成功: hanode2 模拟故障时, web_group 自动迁到 hanode1, VIP 服务零中断。

十二、SBD Fence 真实测试验证

12.1 fence_sbd 静态检查(★ 在 hanode1 跑 ★)

# 看 fence_sbd agent 元数据
fence_sbd --action=metadata | head -20

# 模拟 pacemaker 调用 (不真 fence,只是 status)
#    ★ 用 alias 设备名 /dev/mapper/sbd (Stage 4 锁死)
fence_sbd --devices=/dev/mapper/sbd --plug=hanode1 --action=status --verbose

# cluster 视角的 SBD 状态
pcs stonith sbd status
# hanode1: YES | YES | YES
# hanode2: YES | YES | YES

12.2 真 fence 模拟测试

⚠️ 此测试会让一个节点真的 reboot! 仅在 VM 实验环境执行。
# 1. 在 hanode2 上 kill corosync (模拟故障,★ 此命令在 hanode2 上 ★)
ssh hanode2 'systemctl kill corosync'

# 2. 等待 ~3 分钟,观察
#    - hanode1 会被 SBD 触发 reboot
#    - watch log 看到 cluster status 断掉

12.3 真测试观察结果

阶段现象含义
T+0kill hanode2 corosync触发模拟故障
T+30shanode1 上 pcs status 仍正常corosync partition, hanode1 仍 quorum
T+3minhanode1 系统被 rebootSBD fence 真的工作, watchdog timeout 触发
boot 后uptime 报 "up 1 min", ssh 报 "System is booting up"确认 reboot 来自 SBD 触发

12.4 恢复 cluster (关键步骤!)

关键! 节点 reboot 后,集群不会自动恢复,需要手动清理 + 启动。
# 1. 两节点重启后,/dev/watchdog 可能不存在(softdog 没自动加载)
#    解决: modprobe softdog + 加 /etc/modules-load.d/softdog.conf 持久化

# 2. 启动 cluster
pcs cluster start --all

# 3. 清 SBD slot 残留 message + 清 cluster 失败历史
pcs stonith history cleanup
pcs resource cleanup

# 4. 验证
pcs status
# 应见 2 nodes Online,所有 resource Started

12.5 SBD fence Pitfall 补充

Pitfall 8 — reboot 后 /dev/watchdog 消失

症状: 节点被 SBD fence reboot 后再次启动,watchdog 设备不存在,sbd daemon exit code 1。

修复:

# 一次性持久化
echo "softdog" > /etc/modules-load.d/softdog.conf
modprobe softdog

Pitfall 9 — SBD slot 残留 "off" message

症状: cluster 重启后所有节点 UNCLEAN,resource 不启动。

修复: pcs stonith history cleanup + pcs resource cleanup

Pitfall 10 — fence 触发 reboot 是设计行为

症状: 看到节点 reboot 而非只是 fencing 流程,应用层中断。

说明: SBD 触发 watchdog 是为了 绝对保证节点不再写共享盘。这是设计行为,生产环境会用 BMC/IPMI fence 替代(更优雅)。VM 实验环境接受 reboot 是正常的。

Pitfall 11 — SysRq-c 触发 panic 时 hanode2 也 reboot 的真相

症状: 在 hanode1 上 echo c > /proc/sysrq-trigger 触发 kernel panic + kdump reboot,hanode2 在同一时刻也 reboot

真实根因(2026-08-10 实战):

关键诊断点:

# 验证 SBD 没触发 fence
pcs stonith history          # 应见 "0 events found"
ls /var/crash/              # 只有触发了 panic 的节点会生成 vmcore
ls -la /var/crash/127.0.0.1-*  # 看时间戳确认谁真的 panic 了

教训: SysRq-c 这类 暴力测试在 VM 实验环境 + shared iSCSI 上可能产生 不可预期的连带 reboot。生产环境务必 BMC fence 代替 watchdog 直接 reboot,避免双机连环掉。

实战 Pitfall 12 — VM reboot 后磁盘枚举变了,targetcli backstore 失效(2026-08-13 实测)

症状: 三台 VM 都 reboot 后,hanode1+2 上 iSCSI 设备"消失了"(lsblk 只看到 1MB mpatha,4 个 share LUN 都不见)。

根因(2 层):

第 1 层 — storage-a 端 VM 磁盘枚举变了:
  重启前:  /dev/sda = 200G 空盘   /dev/sdb = 80G 系统盘
  重启后:  /dev/sda = 80G 系统盘   /dev/sdb = 200G 空盘
                                                      ↑ VM 磁盘枚举变了!

  targetcli 的 backstore 之前指向 /dev/sda1-4
                                     ↓ 现在 /dev/sda1 是 1MB 系统盘杂凑
                                     ↓ backstore fail
                                     ↓ LUN 自动 marked failed
                                     ↓ ACL mapping 还显示 lun1-4,但实际只有 1MB 的 lun1 能用

第 2 层 — hanode 端 iscsiadm 默认不 rescan LUN:
  reboot 后 iSCSI sessions 在(2 条)
  但 lsblk 只看到 sdb/sdc(都是 1MB = mpatha)
  因为 storage-a 上 lun2/3/4 失效了,iSCSI 看不到

修复(完整脚本,一行跑完):

# === 在 storage-a 上 ===
TARGET=iqn.2026-08.com.example:storage-a.target0
targetcli /iscsi/$TARGET/tpg1/luns delete lun=1 lun=2 lun=3 lun=4
targetcli /backstores/block delete sda1_block sda2_block sda3_block sda4_block
targetcli /backstores/block create sdb1_block dev=/dev/sdb1
targetcli /backstores/block create sdb2_block dev=/dev/sdb2
targetcli /backstores/block create sdb3_block dev=/dev/sdb3
targetcli /backstores/block create sdb4_block dev=/dev/sdb4
targetcli /iscsi/$TARGET/tpg1/luns create /backstores/block/sdb1_block lun=01
targetcli /iscsi/$TARGET/tpg1/luns create /backstores/block/sdb2_block lun=02
targetcli /iscsi/$TARGET/tpg1/luns create /backstores/block/sdb3_block lun=03
targetcli /iscsi/$TARGET/tpg1/luns create /backstores/block/sdb4_block lun=04
targetcli saveconfig

# === 在 hanode1 + hanode2 上 ===
iscsiadm -m discovery -t sendtargets -p 10.10.40.250:3260
iscsiadm -m discovery -t sendtargets -p 192.168.200.250:3260
iscsiadm -m node -l
for host in /sys/class/scsi_host/host*/scan; do echo "- - -" > $host; done
sleep 3
multipathd reconfigure
multipath -ll    # 验证看到 4 个 mpath

教训 — 这次最重要的发现:

  1. VM 上 iSCSI target 绝对不要 hardcode 设备名(如 /dev/sdaN),改用:
    - /dev/disk/by-id/wwn-*(基于 LUN WWN)
    - /dev/disk/by-path/*(基于物理路径)
    - 或者在 backstore 上用 LVM / LUKS 容器层
  2. iscsiadm 不会自动 rescan LUN。每次 storage 端改 LUN 后,initiator 必须主动跑:
    iscsiadm -m node -l + echo "- - -" > /sys/class/scsi_host/host*/scan
  3. reboot 后必须做一次 iscsi rescan(哪怕 session 都还在)— 这是我们 8/13 重新发现的 bug。包装成 /usr/local/bin/iscsi-rescan.sh,reboot 后跑一次就恢复。

实战 Pitfall 13 — stage 5 格式化不能用 mpath 设备名,要用 UUID 或按 size 找(2026-08-13 实测)

症状: 用固定设备名(如 mpathb)写脚本时,reboot 后 mpath 名字变了(从 mpathb 变成 mpathe/f/g),脚本报 No such file or directory

原因:

修复: 脚本里 用 UUID 或按 size 找设备,不依赖 mpath 名:

# 按 size 找 share1(40G):
SHARE1=$(for m in /dev/mapper/mpath*; do
    sz=$(blockdev --getsize64 "$m")
    # 40G ≈ 42949672960 bytes
    if [ "$sz" -ge 42949672960 ] && [ "$sz" -lt 50000000000 ]; then
        echo "$m"; break
    fi
done)

# 写 fstab 时也用 UUID,不写 /dev/mapper/mpathX:
M1=$(blkid -s UUID -o value "$SHARE1")
echo "UUID=$M1 /share1 xfs _netdev,defaults 0 0" >> /etc/fstab

教训: 配置 iSCSI 时,fstab、multipath.conf、mount 命令都永远用 UUID 或 LABEL,不写裸设备名。/dev/mapper/mpathN 是 unstable 名字。/dev/disk/by-uuid/* 才是 stable。

Pitfall 12 — fence 决策链(谁判断、谁执行,2026-08-11 hanode1 ens34 断网实测)

1. 完整时间线(实测)
08:59:14.000  corosync on hanode2: KNET link host:1 down
08:59:15.000  corosync on hanode2: Token timeout 2250ms
08:59:19.897  corosync: hanode1 left via cluster exit
08:59:20.905  schedulerd: "hanode1 will be fenced: peer is no longer part of the cluster"
08:59:20.906  schedulerd: "Fence (reboot) hanode1 'peer is no longer part of the cluster'"
08:59:20.911  fenced: "Requesting peer fencing (reboot) targeting hanode1"
08:59:21.232  fenced: "Couldn't find anyone to fence (reboot) hanode1 using any device"
08:59:21.232  fenced: "No fence device" (sbd_fence 在 hanode1,但 hanode1 失联)
08:59:58      hanode2 kernel boot (reboot)
2. 关键发现
3. 4 个组件分工
#组件判断"失败"?做什么
1corosync✅ 是节点间发 token,token timeout = 节点失联(2.25s)
2pacemaker-schedulerd✅ 是看到 corosync "lost" → 决策"Fence (reboot)"(5s grace)
3pacemaker-fenced❌ 否调 STONITH 资源("Couldn't find anyone")
4sbd_fence (STONITH 资源)❌ 否在共享盘写"reset" message
5SBD daemon on each node✅ 执行读共享盘 → 触发本地 watchdog 跳
4. 谁真让节点 reboot

不是 Pacemaker 主动命令(它执行失败)。是 SBD 自身的 watchdog 机制:hanode1 的 SBD 看到共享盘断写 → 跳;hanode2 的 SBD 误判共享盘变化 → 也跳。SBD 假设每个节点独立判断,但 2Node + shared storage 写盘动作被两个 daemon 同时读到,所以 30s grace period 没用。

5. 改进方向

十四、节点发现对方存活的完整流程图

14.1 整体概览:两节点相互探测的 4 步

整体 4 步判定流程图

📋 Mermaid 源(可复制编辑) ```mermaid flowchart TB subgraph A["hanode1 本地状态"] A1["第 1 步: 我自己健康吗?"] A2["我自己的进程都在跑:
corosync / pacemaker / sbd / multipathd / iscsid"] A3["结果: ONLINE (我活着)"] end subgraph B["hanode2 本地状态"] B1["第 1 步: 我自己健康吗?"] B2["我自己的进程都在跑:
corosync / pacemaker / sbd / multipathd / iscsid"] B3["结果: ONLINE (我活着)"] end A1 --> A2 --> A3 B1 --> B2 --> B3 C{"第 2 步:
corosync token 5s 收不到对方 ack?"} D["token 收得到
= 对方活着"] E["token 收不到 5s+
= 对方可能死了"] A3 --> C B3 --> C C -->|是| E C -->|否| D F["第 3 步: 我有 quorum 吗?
(我还在 cluster 大多数里吗?)"] D --> F E --> F G{"Q = 我是
多数派?"} H["我没 quorum
→ 我自己可能已隔离
→ 不 fence"] I["我有 quorum
→ 决策 fence 对方"] F --> G G -->|否| H G -->|是| I J["第 4 步: Pacemaker 5s grace 后
对方仍 unclean → 调 fence"] I --> J K{"fence 命令
能调通吗?"} L["能调通
→ STONITH 资源执行 reboot"] M["调不通
(如 sbd_fence 在对端
但对端已失联)"] N["SBD daemon 自己
看共享盘 + watchdog 跳
→ 本机 reboot"] O["对端 reboot"] P["SBD 误判
自己 watchdog 也跳
→ 本机也 reboot"] J --> K K -->|是| L --> O K -->|否| M M --> N --> P ```

14.2 正常情况:每步都 OK(无故障)

正常情况 sequence diagram

📋 Mermaid 源(可复制编辑) ```mermaid sequenceDiagram participant H1 as hanode1 participant H2 as hanode2 Note over H1,H2: T+0s: 正常 cluster 运行 H1->>H2: token (每 2.25s 一轮) H2->>H1: token ack Note over H1,H2: T+2.25s: 第二轮 token H1->>H2: token H2->>H1: token ack Note over H1,H2: 循环:每节点独立看自己 Note over H1: ① 我健康(进程在)= yes Note over H2: ① 我健康(进程在)= yes Note over H1,H2: ② 收 token = 对方活 Note over H1,H2: ③ quorum = 2/2 = majority Note over H1,H2: ④ 不需要 fence ```

14.3 hanode1 失联(我们 8/11 实测)完整时序

hanode1 失联 sequence diagram

📋 Mermaid 源(可复制编辑) ```mermaid sequenceDiagram participant H1 as hanode1 (失联方) participant H2 as hanode2 (判定方) participant CS as corosync participant P as pacemaker participant SBD as SBD daemon Note over H1,SBD: T+0s — 我下 ip link set ens34 down on hanode1 rect rgb(255, 240, 240) Note over H1,CS: T+0s ~ T+5s: corosync token 开始收不到 H1--xH2: token 发出但物理网卡断,无法到达 H2->>H1: token (对方收不到) Note over H2: 我自己进程在跑 end rect rgb(255, 250, 220) Note over H2,CS: T+5s — corosync timer 触发 CS->>CS: "Token has not been received in 2250 ms" CS->>H2: 报告:hanode1 link down / token lost end rect rgb(220, 240, 255) Note over H2,P: T+6s — pacemaker 决策阶段 P->>P: 第 1 步: 我健康吗? YES P->>P: 第 2 步: hanode1 lost via cluster exit P->>P: 第 3 步: 我有 quorum 吗? YES (2 中 1) P->>P: 第 4 步: 5s grace 过了 P->>P: 决策: "Fence (reboot) hanode1" end rect rgb(255, 220, 220) Note over H2,P: T+7s — Pacemaker 调 STONITH 失败 P->>P: pacemaker-fenced: "Requesting peer fencing targeting hanode1" P->>P: "Couldn't find anyone to fence hanode1 using any device" P->>P: "No fence device" (sbd_fence 在 hanode1 上, 但 hanode1 已失联) P->>P: 决策无法执行,fence 命令失败 end rect rgb(255, 200, 200) Note over H1,H2: T+10s ~ T+30s — SBD 自己判断 SBD->>SBD: hanode1 的 SBD: 我不再写共享盘了 SBD->>SBD: watchdog 5s 倒计时 (原配置) / 30s (新配置) SBD->>H1: watchdog timeout -> hanode1 reboot SBD->>SBD: hanode2 的 SBD: 共享盘内容变化 SBD->>SBD: watchdog 也跳 (误判) SBD->>H2: hanode2 reboot end Note over H1,H2: T+38s — 两节点都 reboot 完 H1->>H2: 重启后重新 join cluster H2->>H1: 重启后重新 join cluster Note over H1,H2: 集群恢复, VIP 仍能访问 ```

14.3.1 决策逻辑伪代码(Pacemaker 实际判定的简化版)

function pacemaker_evaluate_node(node):
    # 第 1 步: 我自己健康吗?(本地检查)
    if not my_processes_alive():
        return "I'm sick, don't fence"  # 怕自己误判

    # 第 2 步: corosync 说对方 status 是什么?
    cs_status = corosync.get_node_status(node)
    if cs_status == "alive":
        return "对方活着"  # 没事

    # 第 3 步: 我有 quorum 吗?
    if not has_quorum():
        return "我没 quorum, 自己也可能已隔离"  # 不 fence

    # 第 4 步: 5s grace 过了吗?
    if grace_period_not_elapsed(node, 5):
        return "5s 内可能恢复,等"

    return "Fence (reboot) {node}"  # 4 步全满足 + grace 过了

14.4 关键洞察

14.5 我们的 8/11 实测 vs 理论模型

阶段理论预期实测结果符合?
第 1 步:本地健康hanode2 进程都在corosync/pacemaker/sbd/multipathd/iscsid 全 active
第 2 步:corosync token 收不到5s 内触发 "Token timeout"T+5s 触发
第 3 步:quorum 在2 中 1,仍是 majoritynodes=1, votes=1, retained
第 4 步:5s gracegrace 过了仍 unclean → fenceT+6.905s 决策 fence
Pacemaker fence 命令执行sbd_fence 收到命令写共享盘"No fence device" 失败❌(没找到 STONITH)
真正让 hanode1 reboot应通过 Pacemaker 命令SBD 自己 watchdog 跳⚠️ 设计降级
hanode2 行为不 fence 对方(本机无关)也 reboot(SBD 误判)❌(VM + 共享盘特殊问题)

十三、实战 Pitfall 总结

Pitfall 1 — SBD create UUID 冲突

症状: 两节点分别跑 sbd create 会产生不同 UUID,后续 SBD 服务起不来。

修复: 只在 1 个节点 create 一次,另一个节点 dump 验证 UUID 一致。

Pitfall 2 — EL10 SBD service 启动受限

症状: systemctl start sbd"Operation refused"

修复: EL10 上 sbd.service 是 pacemaker/corosync 的 requires dep,只能通过 pacemaker/corosync 自动 pull up,不能手动启停。

Pitfall 3 — fence_sbd 不支持 unfencing

症状:meta provides=unfencing"unfencing failed"

修复: fence_sbd 不实现 unfencing 协议,必须不加这个 meta。

Pitfall 4 — Pacemaker 资源必须 group/colocation

症状: web_vip 跑到 hanode2, web_fs 跑到 hanode1, 互不约束 → web 服务不通。

修复: 3 个资源加入同一 pcs resource group add web_group,保证同时在同一节点启动。

Pitfall 5 — XFS metadata "Structure needs cleaning"

症状: share2 mount 时报 "fsconfig system call failed: Structure needs cleaning"

根因: 早先手动 mount/umount 太急,metadata 写盘没 sync 完整。

修复: 重新 mkfs -t xfs -f (本次数据可丢) 或者 xfs_repair

Pitfall 6 — cluster setup 时 hostname 解析失败

症状: pcs cluster setup ha_cluster hanode1 hanode2"Could not resolve host"

修复: 在两节点 /etc/hosts10.10.40.241 hanode110.10.40.242 hanode2

Pitfall 7 — `find` vs `vi` 编辑失败

EL10 + pcs 0.12 中,部分 pcs resource 子命令(pcs stonith update ...)在某些场景下更新 conf 后需要重启 corosync 才能生效。资源创建后如有问题,先看 pcs status --full 找具体失败 action,再 pcs resource cleanup <name> 清失败历史。


附录:VM reboot 后 iSCSI 恢复 SOP(2026-08-13 新增)

使用场景: 三台 VM (storage-a + hanode1 + hanode2) 任何一台 reboot 后,iSCSI 设备可能消失。 执行下面的脚本可一键恢复。这是我们 8/13 实测总结出的标准恢复流程。

SOP-1:hanode 端 reboot 恢复

在每台 hanode 上跑一次:

# /usr/local/bin/iscsi-rescan.sh
#!/bin/bash
# 发现 + login + 触发 SCSI rescan + multipath 重配置
set -e

# 1. 双 portal 发现(每次只 1 次,因为 sendtargets 重复跑会重复 login)
iscsiadm -m discovery -t sendtargets -p 10.10.40.250:3260
iscsiadm -m discovery -t sendtargets -p 192.168.200.250:3260

# 2. 登录所有 nodes
iscsiadm -m node -l

# 3. 触发 kernel LUN rescan
for host in /sys/class/scsi_host/host*/scan; do
    echo "- - -" > $host
done

sleep 3

# 4. multipathd 重读 conf + 重新扫描
multipathd reconfigure

# 5. 验证 4 个 mpath 都在
multipath -ll

echo "✓ iSCSI rescan complete"

用法: chmod +x /usr/local/bin/iscsi-rescan.sh && bash /usr/local/bin/iscsi-rescan.sh

SOP-2:storage-a reboot 恢复

在 storage-a 上跑(VM 磁盘枚举变了,targetcli backstore 失效的恢复):

# /usr/local/bin/storage-restore.sh
#!/bin/bash
set -e

TARGET=iqn.2026-08.com.example:storage-a.target0

# 1. 删所有旧 backstore + LUN(此时它们指向失效的 /dev/sdaN)
for lun in 1 2 3 4; do
    targetcli /iscsi/$TARGET/tpg1/luns delete lun=$lun 2>/dev/null || true
done
for n in 1 2 3 4; do
    targetcli /backstores/block delete sda${n}_block 2>/dev/null || true
done

# 2. 重建(用 /dev/sdb1-4,因为 reboot 后空盘变 sdb)
for n in 1 2 3 4; do
    targetcli /backstores/block create sdb${n}_block dev=/dev/sdb$n
done

# 3. 重建 LUN + ACL
for n in 1 2 3 4; do
    targetcli /iscsi/$TARGET/tpg1/luns create /backstores/block/sdb${n}_block lun=0$n
done

# 4. 持久化
targetcli saveconfig

# 5. 验证
ss -tnlp | grep 3260
targetcli ls /iscsi

echo "✓ storage restored"

关键: /dev/sdbN 不是固定的(VM reboot 还会变),更稳的写法是用 /dev/disk/by-id/ 或 LVM。改进版:

# 用 disk by-id 找 200G 空盘(基于 WWN,稳定)
DISK=$(ls -l /dev/disk/by-id/wwn-* 2>/dev/null | grep -v part | awk "{print \$NF}" | head -1)
echo "Target disk: $DISK"

SOP-3:重启后完整恢复

# 1. 先 storage-a 跑 SOP-2(storage-restore.sh)
# 2. 再所有 hanode 跑 SOP-1(iscsi-rescan.sh)
# 3. 验证 mount -a 自动挂载所有 share
mount -a
df -hT | grep share

附录:核心交付物

编号文件 / 配置位置
1主配置文档 (本文档)/srv/samba/share/SEDP/cluster-ha-deploy-2026-08-10.html
2stage5 hanode1 脚本(旧版,设备名硬编码)/tmp/stage5-hanode1.sh (不要用,见 #3)
3stage5 推荐版(用 multipath alias,简化)/tmp/stage5-fixed.sh
4fix-share2 脚本/tmp/fix-share2.sh
5reboot 后 iSCSI 恢复 SOP-1 (hanode 端)/usr/local/bin/iscsi-rescan.sh
6reboot 后 iSCSI 恢复 SOP-2 (storage-a 端)/usr/local/bin/storage-restore.sh
7/etc/multipath.conf两节点相同 (4 个 mpath + blacklist sda)
8/etc/iscsi/initiatorname.iscsi两节点不同 (系统自动生成)
9/etc/target/saveconfig.jsonstorage-a (4 LUN 配置)
10Pacemaker CIB两节点同步 (web_group + sbd_fence)