如何更换lvm卷组中的硬盘
- 2025-11-08 09:25:00
- 丁国栋
- 原创 12
lvm如何移除一个已经使用的磁盘,例如某块磁盘有性能问题或者有损坏的风险,想移除掉。
在 LVM 中安全地移除一块正在被使用的磁盘(例如因为有性能问题或即将损坏)是一个需要谨慎操作的过程。核心原则是:先将数据从目标磁盘上移走,然后再将该磁盘从卷组中移除。
整个过程可以分为以下几个关键步骤:
- 检查当前状态:确认磁盘、物理卷、卷组和逻辑卷的关系。
- 备份数据:在进行任何关键操作前,务必备份重要数据。
- 迁移数据:使用
pvmove命令将目标物理卷上的数据段迁移到卷组中的其他磁盘上。 - 从卷组中移除物理卷:使用
vgreduce命令将已清空的物理卷踢出卷组。 - 移除物理卷属性:使用
pvremove命令抹去 LVM 元数据。 - 物理磁盘下线:此时可以安全地物理拔出或逻辑上禁用该磁盘。
详细步骤说明
假设我们有一个卷组 vg0,它由三块磁盘组成:/dev/sdb, /dev/sdc(有性能问题的磁盘),和 /dev/sdd。我们希望安全地移除 /dev/sdc。
第 1 步:检查当前状态
首先,全面了解当前的 LVM 结构。
-
查看物理卷信息:
sudo pvdisplay或者更简洁的查看:
sudo pvs这会列出所有物理卷、其所属的卷组以及上面的空间使用情况。记下你想移除的磁盘(例如
/dev/sdc)的 PV Name 和 VG Name。 -
查看卷组信息:
sudo vgdisplay vg0或者:
sudo vgs vg0确认卷组中有足够的空闲空间来容纳待迁移的数据。如果其他磁盘没有足够的空闲空间,你需要先添加一块新磁盘到卷组中,或者缩减逻辑卷的大小。
-
查看逻辑卷信息:
sudo lvdisplay或者:
sudo lvs
第 2 步:确保卷组中有其他可用空间
这是成功迁移的关键。使用 vgdisplay 查看 Free PE / Size。这个空闲空间必须大于或等于待移除磁盘(/dev/sdc)上已使用的空间。
-
如果空间不足:你需要先向卷组中添加一块新的、健康的磁盘。
# 假设新磁盘是 /dev/sde sudo pvcreate /dev/sde # 创建物理卷 sudo vgextend vg0 /dev/sde # 将新物理卷加入卷组 vg0现在卷组中就有了来自
/dev/sde的额外空间。
第 3 步:迁移数据(最关键的一步)
使用 pvmove 命令将数据从有问题的物理卷(/dev/sdc)迁移到卷组中的其他空闲空间上。
sudo pvmove /dev/sdc
- 这个命令会自动将
/dev/sdc上的所有数据段迁移到卷组中其他物理卷的空闲区域。 - 这个过程可能需要很长时间,取决于数据量大小和磁盘性能。请耐心等待,不要中断。
更精确的控制: 如果你希望将数据明确地迁移到某块特定的健康磁盘上(例如新加的 /dev/sde),你可以这样做:
sudo pvmove /dev/sdc /dev/sde
检查迁移进度: 你可以打开另一个终端会话,使用以下命令实时监控进度:
# 查看详细的迁移进度
sudo pvmove -i 5
# 或者查看物理卷的剩余数据量
sudo pvs
第 4 步:从卷组中移除物理卷
当 pvmove 完成后,/dev/sdc 上的数据已经全部被移走。现在可以安全地将其从卷组中踢出。
sudo vgreduce vg0 /dev/sdc
vgreduce命令将物理卷/dev/sdc从卷组vg0中移除。
第 5 步:移除 LVM 元数据
现在磁盘已不属于任何卷组,但上面还存有 LVM 的元数据信息。使用 pvremove 将其清除。
sudo pvremove /dev/sdc
- 执行此命令后,该磁盘对 LVM 来说就完全是一块“新”磁盘了。
第 6 步:物理磁盘下线
现在,你可以安全地处理这块磁盘了。
- 如果是虚拟机:可以直接移除虚拟磁盘。
- 如果是物理服务器:可以热插拔拔掉(如果支持的话),或者关机后拔掉。
- 如果只是想暂时不用:可以什么都不做,或者使用
echo 1 > /sys/block/sdc/device/delete来从系统内核中移除该块设备(慎用)。
可以通过命令 sg_inq -p 0x80 /dev/sdc 查询这个硬盘的单元序列号,这个序列号一般也被云服务商用于识别硬盘ID。
sg_inq 是 sg3-utils 软件包中的一个工具,用于向 SCSI(或类似 SCSI 的接口,如 SATA/SAS/NVMe)设备发送 SCSI INQUIRY 命令,查询设备信息。-p 0x80 这个参数指定查询的是 INQUIRY 命令的“单元序列号”页面(Page code 0x80,SCSI VPD Page 0x80: Unit Serial Number(单元序列号))。这个页面包含了一个由设备制造商设定的序列号,即 Unit Serial Number (USD)。命令可以查询到硬盘的重要识别信息,并且云服务商极有可能会使用其中的一些标识符(但不一定是“单元序列号”这个特定字段)来唯一识别和管理硬盘。
使用命令 sg_inq -p 0x83 /dev/sdc 可以查询 SCSI VPD Page 0x83: Device Identification Page(设备标识页)。
完整命令示例(总结)
假设场景:卷组 vg0 由 sdb 和 sdc 组成,要移除有问题的 sdc,并且已经添加了新磁盘 sdd 到 vg0 中。
# 1. 检查状态
sudo pvs
sudo vgs vg0
# 2. (可选)如果空间不够,添加新磁盘
sudo pvcreate /dev/sdd
sudo vgextend vg0 /dev/sdd
# 3. 迁移数据(将 sdc 的数据分散到 vg0 的其他空间)
sudo pvmove /dev/sdc
# 4. 将清空的 sdc 从卷组中移除
sudo vgreduce vg0 /dev/sdc
# 5. 抹去 LVM 元数据
sudo pvremove /dev/sdc
# 6. 再次检查状态,确认 sdc 已消失
sudo pvs
sudo vgs vg0
# 7. 现在可以安全地物理移除 /dev/sdc 磁盘了
重要注意事项和风险
- 备份!备份!备份! 任何磁盘操作都有风险,尤其是在磁盘本身可能有问题的情况下。
- 确保有足够空间:在执行
pvmove前,务必确认卷组中有足够的空闲空间来接纳待迁移的数据。否则操作会失败。 - 性能影响:
pvmove操作会带来大量的 I/O 读写,可能会影响系统性能。建议在业务低峰期进行。 - 磁盘彻底损坏:如果磁盘不是性能下降而是已经彻底损坏、无法读取,那么
pvmove可能会失败。在这种情况下,数据已经丢失。LVM 的mirror功能(创建时指定)或 RAID 才是应对这种情况的正确方案。
遵循以上步骤,你就可以安全地将一块有问题的磁盘从 LVM 中移除。
如果其他硬盘还有空间,可以不添加新的硬盘么?
当然可以! 如果卷组中其他现有的硬盘有足够的空闲空间,就完全不需要添加新硬盘。
这是更常见和更简单的情况。整个流程完全一样,只是跳过了“添加新磁盘”的步骤。
直接使用现有空间的操作流程
假设场景:卷组 vg0 由三块磁盘组成:/dev/sdb(有性能问题,想移除),/dev/sdc 和 /dev/sdd。/dev/sdc 和 /dev/sdd 上还有充足的剩余空间。
关键检查点:使用 vgs 或 vgdisplay 确认空间
在开始之前,运行以下命令:
sudo vgs vg0
你会看到类似这样的输出:
VG #PV #LV #SN Attr VSize VFree
vg0 3 2 0 wz--n- 100.00g 30.00g
重点看 VFree(可用空间)。你需要确保这个数值 大于或等于 你想要移除的磁盘 /dev/sdb 上已使用的空间。
如何查看 /dev/sdb 已用了多少空间?
sudo pvs
输出示例:
PV VG Fmt Attr PSize PFree
/dev/sdb vg0 lvm2 a-- 50.00g 0
/dev/sdc vg0 lvm2 a-- 30.00g 10.00g
/dev/sdd vg0 lvm2 a-- 20.00g 20.00g
从这个输出可以看到:
/dev/sdb大小 50G,已用空间为PSize - PFree = 50G - 0 = 50G。- 卷组总空闲空间
VFree是10G + 20G = 30G。
啊哦!这里就出现问题了! 你想移走 50G 的数据,但卷组里只有 30G 的空闲空间。这 不够!在这种情况下,你不能进行 pvmove,否则会失败。
解决方案是什么?
- 添加新硬盘(你最初想避免的)。
- 先缩减逻辑卷:如果逻辑卷本身没有用满所有空间,你可以先缩小文件系统和逻辑卷,释放出空间,然后再迁移。
- 警告:缩减文件系统是高风险操作,必须提前备份,并严格按照顺序操作(先文件系统检查
fsck-> 调整文件系统大小 -> 调整逻辑卷大小)。
- 警告:缩减文件系统是高风险操作,必须提前备份,并严格按照顺序操作(先文件系统检查
当空间充足时的正确操作示例
假设我们修正一下数据,/dev/sdb 只用了 20G:
sudo pvs
输出:
PV VG Fmt Attr PSize PFree
/dev/sdb vg0 lvm2 a-- 50.00g 30.00g # 已用约20G
/dev/sdc vg0 lvm2 a-- 30.00g 20.00g # 空闲20G
/dev/sdd vg0 lvm2 a-- 20.00g 10.00g # 空闲10G
卷组总空闲空间 = 30G。20G(待迁移数据)< 30G(总空闲空间)。条件满足!
操作步骤(无需添加新盘):
# 1. 检查状态,确认空间足够(如上所示)
sudo pvs
sudo vgs vg0
# 2. 直接开始迁移数据。LVM会自动将数据从 /dev/sdb 分散到 /dev/sdc 和 /dev/sdd 的空闲区域。
sudo pvmove /dev/sdb
# 3. 监控迁移进度(在另一个终端窗口)
sudo pvs -a -o+pv_used
# 4. 迁移完成后,从卷组中移除 /dev/sdb
sudo vgreduce vg0 /dev/sdb
# 5. 移除 LVM 元数据
sudo pvremove /dev/sdb
# 6. 最终检查,/dev/sdb 应该不再出现在 pvs 列表中
sudo pvs
总结
- 可以不加新硬盘:只要满足
卷组总空闲空间 >= 待移除磁盘上已用空间这个条件。 - 核心命令是
pvmove:它非常智能,会自动利用卷组内所有物理卷上的空闲空间来存放迁移的数据,你不需要指定目标。 - 事前检查至关重要:务必使用
pvs和vgs命令精确计算空间,这是操作成功的前提。
所以,你的想法是完全正确的。在资源充足的情况下,优先使用现有空间是更简洁高效的方案。
在执行 pvmove 操作时,要移动的硬盘还会被写入数据么?
这是一个非常好的问题,也是理解 pvmove 工作原理的关键。
简短回答是:会,但 LVM 有机制保证数据一致性。
详细解释如下:
pvmove 的工作原理
pvmove 不是一个简单的“一次性复制”工具。它是一个在线、动态的迁移过程。可以把它想象成一个非常智能的“搬运工”,其工作流程大致如下:
- 锁定并迁移一个数据段(Extent):LVM 会先锁定源物理卷(比如
/dev/sdb)上一个最小的数据单元(称为一个 Extent,通常默认是 4MB)。 - 复制数据:将该 Extent 的数据块从
/dev/sdb读取出来。 - 写入新位置:将这些数据块写入到目标物理卷(比如
/dev/sdd)的空闲空间中。 - 更新元数据(重映射指针):这是最关键的一步。在确保数据成功写入新位置后,LVM 会更新其内部的元数据表。这个表类似于一个“地图”,它告诉系统“逻辑卷 X 的第 Y 个块实际上位于物理磁盘 Z 的哪个位置”。LVM 会将这个指针从指向
/dev/sdb的旧位置更新为指向/dev/sdd的新位置。 - 释放旧空间:更新指针后,源物理卷(
/dev/sdb)上的那个 Extent 就被标记为空闲,原来的数据作废。 - 循环往复:对源物理卷上的每一个数据段重复步骤 1-5。
在此期间的数据写入如何处理?
现在来回答你的核心问题:如果在 pvmove 正在迁移某个数据段时,应用程序恰好要写入这个数据段,会发生什么?
LVM 通过以下机制确保数据安全和一致性:
- 锁定机制:当
pvmove开始处理一个特定的数据段时,它会暂时锁定这个区域。在这极短的时间内,任何试图访问(读或写)这个数据段的 I/O 请求都会被暂时阻塞(排队等待)。 - 先迁后改:
pvmove会完成这个数据段的迁移(包括更新元数据指针)后,再释放锁。 - 指针已更新:锁释放后,所有后续的 I/O 请求(包括刚才被阻塞的写请求)都会根据新的元数据指针,直接发往新的物理位置(
/dev/sdd),而不会再触及旧位置(/dev/sdb)。
这意味着,对于任何一个特定的数据块,一旦它被 pvmove 迁移完成,旧的物理位置就再也不会被访问了。 之后对这个数据块的所有写入操作都会直接应用到新的位置。
结论与影响
- 数据一致性:这个机制保证了在整个漫长的迁移过程中,数据的一致性得到保障。不会发生数据丢失或错乱。
- 性能影响:这是最大的影响。
- 双重I/O负载:
pvmove本身会产生大量的读(从源盘)和写(到目标盘)操作。 - I/O 延迟增加:由于上述的锁定和额外的迁移I/O,应用程序发起的正常I/O请求可能会遇到更高的延迟,因为磁盘需要同时处理业务I/O和迁移I/O。
- 对问题磁盘的压力:如果源磁盘(
/dev/sdb)本身就是因为性能差(如响应慢、有坏道)而要移除,那么pvmove过程中密集的读取操作可能会让它的性能问题暴露得更明显,甚至可能导致迁移过程变慢或暂时卡顿。
- 双重I/O负载:
给您的建议
- 选择低峰期操作:由于对性能有显著影响,务必在系统负载最低的时间段(如深夜或业务维护窗口)执行
pvmove。 - 监控进度和性能:使用
iostat或pvmove -i 5等工具监控迁移进度和磁盘I/O状况。 - 耐心等待:尤其是当源磁盘性能不佳时,迁移速度可能很慢,请保持耐心,不要中断进程。中断
pvmove可能需要回滚,过程比较复杂。
总之,pvmove 在设计上就考虑到了在线迁移的需求,它会安全地处理并发写入,但代价是会增加系统整体的I/O负载和延迟。
如果 pvmove 过程中,ssh断开了,是不是会有风险?
是的,这确实是一个重要的风险点。 SSH 连接断开本身不会直接损坏数据,但会导致 pvmove 进程被终止,使迁移过程进入一个 未完成的中间状态,带来复杂性和潜在风险。
SSH 断开后会发生什么?
当你通过 SSH 启动 pvmove 后,这个进程是在远程服务器上运行的。但是,如果 SSH 连接异常断开(网络问题、客户端休眠等),默认情况下,Linux 会向由该 SSH 会话启动的所有进程发送 SIGHUP 信号,这通常会导致 pvmove 进程被终止。
主要风险和处理方法
风险 1:迁移过程中断,处于不完整状态
这是最可能发生的情况。pvmove 不是原子操作,它是一点一点迁移的。中断时,可能只迁移了一部分数据。
-
LVM 如何保证安全? LVM 的设计很稳健。它只在一个数据段(Extent) 被成功迁移并更新元数据后,才会标记旧数据段为空闲。这意味着,在中断的那一刻:
- 已经迁移的数据:其元数据指针已经指向了新位置,这些数据是安全的、一致的。
- 尚未迁移的数据:仍然保留在源磁盘(
/dev/sdb)的原始位置上,也是安全的。 - 不会出现“数据一半在旧盘,一半在新盘”的损坏情况。LVM 的元数据始终指向一个唯一正确的数据块位置。
-
中断后怎么办?
- 重新连接 SSH。
- 检查
pvmove进程是否还在:使用ps aux | grep pvmove查看。有时如果使用nohup或screen,进程可能还在运行。 -
如果进程已终止:直接再次运行相同的
pvmove命令:sudo pvmove /dev/sdb
LVM 非常智能,它会自动检测到迁移任务没有完成。它会从头开始扫描,但会跳过那些已经完成迁移的数据段,只继续迁移剩余的部分。你可以无缝地继续这个任务。
风险 2:元数据损坏(风险极低,但理论上存在)
如果在 pvmove 正在更新元数据的那一刻(即,刚写完新位置,正准备更新指针时)连接断开,有极低概率导致元数据不一致。
- LVM 的防护机制:LVM 使用事务性元数据更新。它通常先写日志,再提交更改,这类似于数据库的事务机制,可以最大限度地避免元数据损坏。
- 怎么办:如果最坏情况发生,你可以使用
vgcfgrestore命令来尝试恢复到一个已知良好的元数据备份副本(LVM 会自动备份元数据)。
风险 3:无法监控进度和应对突发问题
这是最大的操作风险。连接断开后,你将:
- 无法看到迁移进度。
- 无法收到任何可能的错误提示(例如,目标磁盘空间不足、源磁盘出现读取错误)。
- 如果系统因 I/O 负载过高出现其他问题,你将无法登录系统进行干预。
最佳实践:如何避免 SSH 断开的风险?
绝对不要 直接在前台运行长时间的 pvmove 命令。请务必使用以下方法之一来使其在后台安全运行。
方法一:使用 screen 或 tmux(推荐)
这些是“终端复用器”,可以让你创建一个持久化的会话,即使 SSH 断开,进程也会继续运行。
使用 screen 的示例:
# 1. 安装 screen(如果未安装)
sudo apt install screen # Debian/Ubuntu
sudo yum install screen # CentOS/RHEL
# 2. 创建一个新的 screen 会话
screen -S lvm_migration
# 3. 在 screen 会话中执行命令
sudo pvmove /dev/sdb -i 5 # -i 5 表示每5秒报告一次进度
# 4. 临时断开 SSH 连接,但让进程继续运行:
# 按下 Ctrl + A,然后松开,再按 D(Detach)。你会看到 [detached] 提示。
# 5. 当你重新登录 SSH 后,可以重新连接到 screen 会话查看进度:
screen -r lvm_migration # 如果只有一个会话
screen -r # 列出并选择会话
# 6. 要退出 screen 会话(在任务完成后),在会话内部输入 `exit`。
这样,即使你的网络断开一整天,pvmove 也会在服务器的 screen 会话中继续运行。
方法二:使用 nohup 和 &(简单直接)
nohup 命令可以让你忽略 SIGHUP 信号,& 让进程在后台执行。
# 1. 启动一个在后台运行且忽略挂断信号的 pvmove,并将输出重定向到日志文件
sudo nohup pvmove /dev/sdb -i 10 > /tmp/pvmove.log 2>&1 &
# 2. 你可以安全地断开 SSH。
# 3. 重新登录后,检查进程是否在运行:
ps aux | grep pvmove
# 4. 查看进度日志:
tail -f /tmp/pvmove.log
# 5. 你也可以通过 pvs 命令查看数据量的变化来估算进度
sudo pvs -a -o+pv_used
总结
- 数据风险低:得益于 LVM 的设计,SSH 断开导致数据损坏的风险极低。迁移任务可以重新启动并继续。
- 操作风险高:主要风险在于失去对进程的监控和控制。
- 最佳实践:永远使用
screen/tmux或nohup来运行长时间的pvmove命令。这应该成为你的标准操作流程。
所以,请务必在开始 pvmove 前,先建立一个持久的会话环境,然后再执行命令。
执行了pvmove后,没有移除硬盘,这个硬盘还会被调度写入新数据么?
不会。 执行 pvmove 命令后,即使你没有手动移除该硬盘,LVM 的元数据已经更新,所有新的数据写入操作都会指向新的物理位置,而不会再写入到原来的硬盘上 。
详细解释
pvmove 命令的核心机制是 元数据更新,而不是简单的数据复制。它的工作流程可以概括为:
- 数据复制:
pvmove会将源物理卷(例如/dev/sdb)上的数据块(PE)逐个复制到卷组中其他物理卷的空闲区域 。 - 元数据更新(关键步骤):在确保数据成功复制到新位置后,LVM 会立即更新其内部的元数据。这个元数据就像一个“地图”,它记录了逻辑卷(LV)的每个数据块(LE)实际存储在哪个物理卷(PV)的哪个位置 。
- 释放旧空间:一旦元数据更新完成,源物理卷上的原始数据块就会被标记为空闲,可以被其他逻辑卷使用。但更重要的是,所有后续的读写请求都会根据新的元数据“地图”直接访问新的数据位置 。
验证方法
你可以通过以下命令来验证这个状态:
-
查看物理卷使用情况:
sudo pvs -o+pv_used如果
pvmove成功完成,你会发现源硬盘(如/dev/sdb)的Used列显示为0,而其他硬盘的Used列会增加。这表明数据已经全部移走,该硬盘在 LVM 层面已经是一个“空盘”了 。 -
查看逻辑卷的物理分布:
sudo lvdisplay -m这个命令会显示逻辑卷的物理段映射关系。在输出中,你将看不到源硬盘(如
/dev/sdb)的路径,取而代之的是其他硬盘的路径。这直接证明了数据已经不再依赖源硬盘 。
结论与建议
- 结论:
pvmove完成后,源硬盘上的数据已经清空,并且系统不会再向它写入任何新数据。即使你暂时不执行vgreduce和pvremove,数据也是安全的 。 -
建议:虽然数据已经安全,但为了保持存储架构的整洁并释放该硬盘的 LVM 标签,建议你按照标准流程完成后续步骤:
# 从卷组中移除已清空的物理卷 sudo vgreduce vg0 /dev/sdb # 移除物理卷上的 LVM 元数据 sudo pvremove /dev/sdb完成这些操作后,该硬盘就可以安全地物理拔除或用于其他用途了 。
帮我用php编写一个监控pvmove操作是否完成的脚本,如果完成了就发送完成通知,没完成就每隔5分钟发送一个进行中通知。
以下是一个用 PHP 编写的监控 pvmove 操作进度的脚本:
1. 主监控脚本 (pvmove_monitor.php)
#!/usr/bin/env php
<?php
/**
* pvmove 操作监控脚本
* 监控 pvmove 进度,完成后发送通知,未完成则定期发送进度报告
*/
class PvmoveMonitor {
private $logFile;
private $notificationFile;
private $checkInterval; // 检查间隔(秒)
private $notificationInterval; // 通知间隔(秒,5分钟=300秒)
private $lastNotificationTime = 0;
public function __construct($logFile = '/tmp/pvmove_monitor.log', $notificationInterval = 300) {
$this->logFile = $logFile;
$this->notificationFile = dirname($logFile) . '/pvmove_notification.log';
$this->checkInterval = 10; // 每10秒检查一次
$this->notificationInterval = $notificationInterval;
// 创建日志目录
$logDir = dirname($this->logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
}
/**
* 检查 pvmove 是否正在运行
*/
private function isPvmoveRunning() {
$output = [];
$returnCode = 0;
// 检查 pvmove 进程
exec("ps aux | grep '[p]vmove'", $output, $returnCode);
if (!empty($output)) {
$this->log("检测到 pvmove 进程运行中");
return true;
}
// 检查是否有活跃的 pvmove 操作
exec("sudo pvs --noheadings -o pv_name,pv_used", $output, $returnCode);
if ($returnCode !== 0) {
$this->log("执行 pvs 命令失败,返回码: $returnCode");
return false;
}
// 分析物理卷使用情况
foreach ($output as $line) {
$line = trim($line);
if (empty($line)) continue;
$parts = preg_split('/\s+/', $line);
if (count($parts) >= 2) {
$pvName = $parts[0];
$pvUsed = $parts[1];
// 如果有物理卷使用量不为0且不是100%,可能正在迁移
if ($pvUsed !== '0' && strpos($pvUsed, '0') !== 0) {
$this->log("物理卷 $pvName 使用量: $pvUsed,可能正在迁移");
return true;
}
}
}
return false;
}
/**
* 获取 pvmove 进度信息
*/
private function getPvmoveProgress() {
$progressInfo = [
'running' => false,
'progress' => '0%',
'details' => '无活跃的 pvmove 操作'
];
// 检查进程
exec("ps aux | grep '[p]vmove'", $processOutput);
if (!empty($processOutput)) {
$progressInfo['running'] = true;
$progressInfo['details'] = 'pvmove 进程运行中';
// 尝试从 pvmove 进程参数获取进度
foreach ($processOutput as $processLine) {
if (strpos($processLine, 'pvmove') !== false) {
$progressInfo['process_info'] = $processLine;
break;
}
}
}
// 获取物理卷详细使用情况
exec("sudo pvs --units m --nosuffix --noheadings -o pv_name,pv_size,pv_used", $pvsOutput);
$totalSize = 0;
$totalUsed = 0;
$migrationDetails = [];
foreach ($pvsOutput as $line) {
$line = trim($line);
if (empty($line)) continue;
$parts = preg_split('/\s+/', $line);
if (count($parts) >= 3) {
$pvName = $parts[0];
$pvSize = floatval($parts[1]);
$pvUsed = floatval($parts[2]);
$totalSize += $pvSize;
$totalUsed += $pvUsed;
if ($pvUsed > 0) {
$usagePercent = round(($pvUsed / $pvSize) * 100, 1);
$migrationDetails[] = "$pvName: {$pvUsed}M/{$pvSize}M ({$usagePercent}%)";
}
}
}
if ($totalSize > 0) {
$totalUsagePercent = round(($totalUsed / $totalSize) * 100, 1);
$progressInfo['total_usage'] = "{$totalUsed}M/{$totalSize}M ({$totalUsagePercent}%)";
$progressInfo['volume_details'] = $migrationDetails;
}
return $progressInfo;
}
/**
* 发送通知
*/
private function sendNotification($title, $message, $isCompletion = false) {
$timestamp = date('Y-m-d H:i:s');
$notification = "[$timestamp] $title\n$message\n" . str_repeat("-", 50) . "\n";
// 记录到通知文件
file_put_contents($this->notificationFile, $notification, FILE_APPEND | LOCK_EX);
// 这里可以添加各种通知方式
// 1. 系统日志
$this->log("通知: $title - " . str_replace("\n", " ", $message));
// 2. 邮件通知(需要配置邮件)
// $this->sendEmailNotification($title, $message);
// 3. Webhook 通知(如 Slack、钉钉、企业微信等)
// $this->sendWebhookNotification($title, $message, $isCompletion);
// 4. 桌面通知(如果运行在桌面环境)
// exec("notify-send '$title' '$message'");
echo $notification;
}
/**
* 发送邮件通知(示例)
*/
private function sendEmailNotification($title, $message) {
$to = "admin@example.com";
$subject = "pvmove 监控 - $title";
$headers = "From: pvmove-monitor@server.com\r\n";
// 使用 mail 函数发送邮件(需要配置邮件服务器)
// mail($to, $subject, $message, $headers);
}
/**
* 记录日志
*/
private function log($message) {
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[$timestamp] $message\n";
file_put_contents($this->logFile, $logMessage, FILE_APPEND | LOCK_EX);
}
/**
* 运行监控
*/
public function run() {
$this->log("pvmove 监控服务启动");
$this->sendNotification("监控启动", "pvmove 监控服务已开始运行");
$completionDetected = false;
while (true) {
$currentTime = time();
$isRunning = $this->isPvmoveRunning();
$progressInfo = $this->getPvmoveProgress();
if ($isRunning) {
// pvmove 正在进行中
if (($currentTime - $this->lastNotificationTime) >= $this->notificationInterval) {
$progressText = "迁移进度: " . ($progressInfo['total_usage'] ?? '未知');
if (!empty($progressInfo['volume_details'])) {
$progressText .= "\n详细信息:\n- " . implode("\n- ", $progressInfo['volume_details']);
}
$this->sendNotification(
"pvmove 进行中",
$progressText
);
$this->lastNotificationTime = $currentTime;
}
$completionDetected = false; // 重置完成检测标志
} else {
// pvmove 未运行
if (!$completionDetected) {
// 第一次检测到完成
$this->sendNotification(
"pvmove 操作完成",
"pvmove 操作已完成\n最终状态: " . ($progressInfo['total_usage'] ?? '未知'),
true
);
$completionDetected = true;
$this->log("检测到 pvmove 操作完成");
}
}
sleep($this->checkInterval);
}
}
}
// 脚本执行
if (php_sapi_name() === 'cli') {
$monitor = new PvmoveMonitor();
// 处理命令行参数
$options = getopt("i:l:h", ['interval:', 'log:', 'help']);
if (isset($options['h']) || isset($options['help'])) {
echo "使用方法:\n";
echo "php pvmove_monitor.php [-i 秒数] [--interval=秒数] [-l 日志文件] [--log=日志文件]\n\n";
echo "参数说明:\n";
echo "-i, --interval 通知间隔(秒,默认300秒/5分钟)\n";
echo "-l, --log 日志文件路径\n";
echo "-h, --help 显示此帮助信息\n";
exit(0);
}
$notificationInterval = $options['i'] ?? $options['interval'] ?? 300;
$logFile = $options['l'] ?? $options['log'] ?? '/tmp/pvmove_monitor.log';
$monitor = new PvmoveMonitor($logFile, (int)$notificationInterval);
$monitor->run();
} else {
echo "此脚本只能在命令行模式下运行\n";
exit(1);
}
?>
2. 系统服务文件 (pvmove-monitor.service)
创建 systemd 服务文件以便后台运行:
[Unit]
Description=pvmove Monitor Service
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/php /usr/local/bin/pvmove_monitor.php
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
3. 安装和使用说明
安装脚本
# 复制脚本到系统路径
sudo cp pvmove_monitor.php /usr/local/bin/
sudo chmod +x /usr/local/bin/pvmove_monitor.php
# 复制服务文件
sudo cp pvmove-monitor.service /etc/systemd/system/
# 重新加载 systemd
sudo systemctl daemon-reload
使用方法
1. 直接运行(前台):
sudo php pvmove_monitor.php
2. 使用自定义参数:
# 每2分钟发送一次进度通知
sudo php pvmove_monitor.php -i 120
# 指定日志文件
sudo php pvmove_monitor.php -l /var/log/pvmove_monitor.log
3. 作为系统服务运行:
# 启动服务
sudo systemctl start pvmove-monitor
# 设置开机自启
sudo systemctl enable pvmove-monitor
# 查看服务状态
sudo systemctl status pvmove-monitor
# 查看日志
sudo journalctl -u pvmove-monitor -f
4. 增强功能建议
你可以根据需要扩展通知方式:
添加 Slack 通知:
private function sendSlackNotification($title, $message) {
$webhookUrl = "https://hooks.slack.com/services/your/webhook/url";
$data = [
'text' => "*$title*\n$message",
'username' => 'pvmove-monitor'
];
$ch = curl_init($webhookUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
}
这个脚本会持续监控 pvmove 操作,按照你要求的频率发送通知,非常适合在生产环境中使用。