背景
在 OpenStack 私有云环境中,Cinder 卷迁移是一个常见的运维操作。当需要对存储后端进行维护、升级,或者需要在不同存储池之间平衡负载时,都会涉及到卷迁移。
然而 Cinder API 在卷迁移方面存在一个明显的不足:它不提供实时的迁移进度信息。当你执行 cinder migrate 命令后,只能通过 cinder show <volume-id> 查看卷的 migration_status 字段,而这个字段只会显示 migrating 或 success/error 这样的状态,无法得知当前已经迁移了多少数据,还需要等待多长时间。
对于小容量的卷,这个问题不太明显。但当需要迁移几百 GB 甚至 TB 级别的卷时,整个迁移过程可能持续数小时。运维人员无法判断迁移是否正常进行、速度是否合理、大概还需要多久完成,这给运维工作带来了很大的不确定性。
本文介绍一种通过读取 Linux 块设备统计信息来监控 Cinder 卷迁移实时进度的方法。
原理
Cinder 卷迁移的本质
Cinder 的卷迁移在底层实际上是数据的块级复制。以 LVM 后端为例,迁移过程会在目标存储节点上创建一个新的 LVM 逻辑卷,然后将源卷的数据逐块写入到目标卷中。
这意味着我们可以通过监控目标卷的写入量来推算迁移进度。
/sys/block/*/stat 文件
Linux 内核通过 sysfs 文件系统暴露了块设备的 I/O 统计信息。每个块设备在 /sys/block/ 目录下都有一个对应的目录,其中的 stat 文件包含了该设备的 I/O 统计数据。
cat /sys/block/dm-5/stat
12849 1234 103856 5678 45678 2345 2456789 12345 0 15678 18023stat 文件包含 11 个字段(某些内核版本可能更多),各字段含义如下:
| 字段 | 名称 | 说明 |
|---|---|---|
| 1 | read I/Os | 完成的读 I/O 请求数 |
| 2 | read merges | 合并的读 I/O 请求数 |
| 3 | read sectors | 读取的扇区数 |
| 4 | read ticks | 读 I/O 花费的时间(毫秒) |
| 5 | write I/Os | 完成的写 I/O 请求数 |
| 6 | write merges | 合并的写 I/O 请求数 |
| 7 | write sectors | 写入的扇区数 |
| 8 | write ticks | 写 I/O 花费的时间(毫秒) |
| 9 | in_flight | 当前正在进行的 I/O 请求数 |
| 10 | io_ticks | I/O 操作花费的总时间(毫秒) |
| 11 | time_in_queue | I/O 请求在队列中的加权时间 |
对于监控卷迁移进度,我们主要关注第 7 个字段(write sectors)和第 9 个字段(in_flight):
- write sectors:累计写入的扇区数。标准扇区大小为 512 字节,因此写入量(字节)= write sectors * 512
- in_flight:当前正在进行的 I/O 请求数,可以反映迁移是否正在活跃进行
LVM 设备命名规则
在使用 LVM 后端的 Cinder 环境中,卷对应的块设备路径遵循特定的命名规则。Cinder 默认将卷创建在名为 cinder-volumes 的卷组(VG)中,逻辑卷(LV)的名称格式为 volume-<UUID>。
当 LVM 逻辑卷被激活后,device mapper 会在 /dev/mapper/ 目录下创建对应的设备节点。命名规则是:<VG名称>-<LV名称>,并且名称中的连字符 - 会被转义为双连字符 --。
例如,对于卷 ID 为 69aa22a5-3dcf-49aa-a771-8264381dc59a 的 Cinder 卷:
- 卷组名称:
cinder(根据实际环境可能不同) - 逻辑卷名称:
volume-69aa22a5-3dcf-49aa-a771-8264381dc59a - device mapper 设备路径:
/dev/mapper/cinder-volume--69aa22a5--3dcf--49aa--a771--8264381dc59a
注意逻辑卷名称中的每个 - 都被转换成了 --。
可以使用 readlink 命令获取该符号链接指向的真实 dm 设备:
readlink -f /dev/mapper/cinder-volume--69aa22a5--3dcf--49aa--a771--8264381dc59a
/dev/dm-5得到真实设备名后,就可以从 /sys/block/dm-5/stat 读取该设备的 I/O 统计信息了。
监控脚本
基于以上原理,编写了一个监控 Cinder 卷迁移进度的脚本:
#!/bin/bash
#
# 监控 Cinder 卷迁移进度
# 用法: ./volume-migrate-monitor.sh <volume-id> <total-size>
# 示例: ./volume-migrate-monitor.sh 69aa22a5-3dcf-49aa-a771-8264381dc59a 500G
#
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "用法: $0 <volume-id> <total-size>"
echo "示例: $0 69aa22a5-3dcf-49aa-a771-8264381dc59a 500G"
exit 1
fi
VOL_ID="$1"
SIZE_INPUT="$2"
# 解析总大小,支持 G/T 后缀,统一转换为 GiB
parse_size() {
local input="$1"
local num="${input//[^0-9.]/}"
local unit="${input//[0-9.]/}"
unit=$(echo "$unit" | tr '[:lower:]' '[:upper:]')
case "$unit" in
G|GIB|GB) echo "$num" ;;
T|TIB|TB) awk -v n="$num" 'BEGIN{printf "%.2f", n*1024}' ;;
*) echo "$num" ;; # 默认当作 GiB
esac
}
TOTAL_GIB=$(parse_size "$SIZE_INPUT")
# 将 volume-id 中的 '-' 转为 '--',匹配 LVM 命名规则
# 原始脚本中设备路径格式: /dev/mapper/cinder-volume--<UUID>
# VG=cinder, LV=volume-<UUID>,LV 内部的 '-' 在 dm 中转义为 '--'
LVM_NAME="cinder-volume--${VOL_ID//-/--}"
DEV_PATH="/dev/mapper/${LVM_NAME}"
if [[ ! -e "$DEV_PATH" ]]; then
echo "错误: 找不到设备 $DEV_PATH"
echo "可能原因:"
echo " - 卷 ID 不正确"
echo " - 卷尚未在此节点上创建"
echo " - 此节点不是该卷的存储后端"
exit 1
fi
# 解析真实 dm 设备名
DM=$(readlink -f "$DEV_PATH" | xargs basename)
if [[ ! -f "/sys/block/$DM/stat" ]]; then
echo "错误: 无法读取 /sys/block/$DM/stat"
exit 1
fi
echo "========================================================================"
echo " Cinder 卷迁移进度监控"
echo "========================================================================"
echo " 卷 ID: $VOL_ID"
echo " 设备: $DEV_PATH -> /dev/$DM"
echo " 目标大小: ${TOTAL_GIB} GiB"
echo "------------------------------------------------------------------------"
echo " 采样中 (15 秒)..."
# 采样间隔(秒)
SAMPLE_SEC=15
# 第一次采样
STAT_FILE="/sys/block/$DM/stat"
S1=$(cat "$STAT_FILE")
S1_WRITE=$(echo "$S1" | awk '{print $7}')
S1_INFLIGHT=$(echo "$S1" | awk '{print $9}')
sleep $SAMPLE_SEC
# 第二次采样
S2=$(cat "$STAT_FILE")
S2_WRITE=$(echo "$S2" | awk '{print $7}')
S2_INFLIGHT=$(echo "$S2" | awk '{print $9}')
# 用第二次采样的值作为最终累计写入量
SECTORS_WRITTEN="$S2_WRITE"
WRITTEN_GIB=$(awk -v s="$SECTORS_WRITTEN" 'BEGIN{printf "%.2f", s*512/1024/1024/1024}')
# 计算采样期间的平均写入速度 (MB/s)
WMB=$(awk -v s1="$S1_WRITE" -v s2="$S2_WRITE" -v t="$SAMPLE_SEC" 'BEGIN{
delta = s2 - s1
mb = delta * 512 / 1024 / 1024 / t
printf "%.2f", mb
}')
# 当前 in-flight I/O 数量
INFLIGHT="$S2_INFLIGHT"
# 计算完成百分比
PERCENT=$(awk -v w="$WRITTEN_GIB" -v t="$TOTAL_GIB" 'BEGIN{
if (t > 0) printf "%.2f", (w/t)*100
else printf "0.00"
}')
# 估算剩余时间
REMAINING_GIB=$(awk -v w="$WRITTEN_GIB" -v t="$TOTAL_GIB" 'BEGIN{
r = t - w
if (r < 0) r = 0
printf "%.2f", r
}')
ETA=$(awk -v r="$REMAINING_GIB" -v s="$WMB" 'BEGIN{
if (s+0 > 0) {
secs = (r * 1024) / s
h = int(secs / 3600)
m = int((secs % 3600) / 60)
printf "%dh %dm", h, m
} else {
printf "N/A"
}
}')
# 生成进度条
BAR_WIDTH=40
FILLED=$(awk -v p="$PERCENT" -v w="$BAR_WIDTH" 'BEGIN{
f = int(p * w / 100)
if (f > w) f = w
if (f < 0) f = 0
print f
}')
EMPTY=$((BAR_WIDTH - FILLED))
BAR=$(printf "%0.s█" $(seq 1 $FILLED 2>/dev/null) ; printf "%0.s░" $(seq 1 $EMPTY 2>/dev/null))
echo ""
echo " 进度: [${BAR}] ${PERCENT}%"
echo ""
echo " 累计写入: ${WRITTEN_GIB} GiB / ${TOTAL_GIB} GiB"
echo " 剩余: ${REMAINING_GIB} GiB"
echo " 写入速度: ${WMB} MB/s (${SAMPLE_SEC}s 均值)"
echo " In-flight: ${INFLIGHT} 个 I/O 请求"
echo " 预计剩余: ${ETA}"
echo ""
echo "========================================================================"脚本使用方法
- 将脚本保存为
volume-migrate-monitor.sh并赋予执行权限:
chmod +x volume-migrate-monitor.sh- 在目标存储节点上执行脚本,传入卷 ID 和卷的总大小:
./volume-migrate-monitor.sh 69aa22a5-3dcf-49aa-a771-8264381dc59a 500G- 脚本会采样 15 秒后输出当前进度:
========================================================================
Cinder 卷迁移进度监控
========================================================================
卷 ID: 69aa22a5-3dcf-49aa-a771-8264381dc59a
设备: /dev/mapper/cinder-volume--69aa22a5--3dcf--49aa--a771--8264381dc59a -> /dev/dm-5
目标大小: 500 GiB
------------------------------------------------------------------------
采样中 (15 秒)...
进度: [████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 30.45%
累计写入: 152.25 GiB / 500 GiB
剩余: 347.75 GiB
写入速度: 156.32 MB/s (15s 均值)
In-flight: 8 个 I/O 请求
预计剩余: 0h 37m
========================================================================持续监控
如果需要持续监控迁移进度,可以使用 watch 命令:
watch -n 20 ./volume-migrate-monitor.sh 69aa22a5-3dcf-49aa-a771-8264381dc59a 500G这会每 20 秒刷新一次进度信息。需要注意的是,由于脚本内部有 15 秒的采样时间,所以 watch 的间隔不要设置得太短。
技术细节说明
扇区大小
Linux 块设备统计中的扇区大小固定为 512 字节,这是一个历史沿用的标准值,与物理磁盘的实际扇区大小(可能是 4K)无关。因此计算写入量时统一使用:
写入字节数 = write_sectors * 512速度计算
脚本通过两次采样的差值来计算平均写入速度:
速度 = (第二次采样的 write_sectors - 第一次采样的 write_sectors) * 512 / 采样间隔15 秒的采样间隔是一个折中的选择。间隔太短,速度波动大,不够稳定;间隔太长,响应不够及时。根据实际需要可以调整 SAMPLE_SEC 变量。
ETA 估算
剩余时间基于当前的写入速度进行估算:
剩余时间 = 剩余数据量 / 当前写入速度由于存储系统的 I/O 性能会受到多种因素影响(如其他负载、网络状况等),实际的写入速度可能会有波动,因此 ETA 只是一个参考值。
不同的 VG 名称
脚本中硬编码了 VG 名称为 cinder。如果你的环境使用了不同的 VG 名称(如 cinder-volumes),需要相应修改脚本中的 LVM_NAME 变量:
LVM_NAME="cinder--volumes-volume--${VOL_ID//-/--}"可以通过 vgdisplay 或 lvs 命令查看实际的 VG 名称。
局限性
这个方法有一些局限性需要注意:
仅适用于 LVM 后端:脚本针对 LVM 后端编写,如果使用 Ceph RBD 或其他存储后端,需要根据具体情况调整设备路径的获取方式。
需要在存储节点执行:必须在目标存储节点(即迁移目标所在的节点)上执行脚本,因为需要访问本地的块设备和 sysfs。
速度波动影响 ETA 准确性:脚本使用单次采样(15 秒)的瞬时速度来估算剩余时间,如果迁移速度波动较大,ETA 可能不够准确。可以考虑多次采样取平均值来改进。
不能区分迁移写入和其他写入:如果卷在迁移过程中同时被其他程序写入,统计数据会包含这部分写入量,可能导致进度显示不准确。不过在迁移过程中卷通常处于不可用状态,这种情况较少发生。
Ceph RBD 后端的情况
如果 Cinder 使用 Ceph RBD 作为存储后端,上述方法不再适用,因为数据在 Ceph 集群内部流转,不经过存储节点的本地块设备。
对于 Ceph RBD 后端,可以通过以下方式监控:
# 查看目标 RBD 镜像的实际占用空间
watch -n 5 'rbd du <pool>/volume-<uuid>'
# 查看 pool 的 I/O 统计
ceph osd pool stats <pool-name>
# 实时观察集群活动
ceph -w需要注意的是,Ceph 使用 thin provisioning,rbd du 显示的是实际使用空间而非卷标称容量。如果源卷只用了 100G(标称 500G),迁移完成后目标镜像也只会显示约 100G。这与 LVM 的块级全量复制不同。
总结
通过读取 /sys/block/*/stat 文件中的块设备统计信息,我们可以实现对 Cinder 卷迁移进度的实时监控。这个方法绕过了 Cinder API 的限制,直接从系统层面获取 I/O 数据,为大容量卷迁移提供了有效的进度跟踪手段。
理解 Linux 块设备统计的原理,以及 LVM 和 device mapper 的命名规则,是实现这个监控方案的关键。这些知识在其他存储相关的运维场景中同样适用。