如何更换lvm卷组中的硬盘

2025-11-08 09:25:00
丁国栋
原创 12
摘要:本文记录如何更换lvm卷组中的硬盘。

lvm如何移除一个已经使用的磁盘,例如某块磁盘有性能问题或者有损坏的风险,想移除掉。

在 LVM 中安全地移除一块正在被使用的磁盘(例如因为有性能问题或即将损坏)是一个需要谨慎操作的过程。核心原则是:先将数据从目标磁盘上移走,然后再将该磁盘从卷组中移除。

整个过程可以分为以下几个关键步骤:

  1. 检查当前状态:确认磁盘、物理卷、卷组和逻辑卷的关系。
  2. 备份数据:在进行任何关键操作前,务必备份重要数据。
  3. 迁移数据:使用 pvmove 命令将目标物理卷上的数据段迁移到卷组中的其他磁盘上。
  4. 从卷组中移除物理卷:使用 vgreduce 命令将已清空的物理卷踢出卷组。
  5. 移除物理卷属性:使用 pvremove 命令抹去 LVM 元数据。
  6. 物理磁盘下线:此时可以安全地物理拔出或逻辑上禁用该磁盘。

详细步骤说明

假设我们有一个卷组 vg0,它由三块磁盘组成:/dev/sdb/dev/sdc(有性能问题的磁盘),和 /dev/sdd。我们希望安全地移除 /dev/sdc

第 1 步:检查当前状态

首先,全面了解当前的 LVM 结构。

  1. 查看物理卷信息:

    sudo pvdisplay

    或者更简洁的查看:

    sudo pvs

    这会列出所有物理卷、其所属的卷组以及上面的空间使用情况。记下你想移除的磁盘(例如 /dev/sdc)的 PV Name 和 VG Name。

  2. 查看卷组信息:

    sudo vgdisplay vg0

    或者:

    sudo vgs vg0

    确认卷组中有足够的空闲空间来容纳待迁移的数据。如果其他磁盘没有足够的空闲空间,你需要先添加一块新磁盘到卷组中,或者缩减逻辑卷的大小。

  3. 查看逻辑卷信息:

    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(设备标识页)。


完整命令示例(总结)

假设场景:卷组 vg0sdbsdc 组成,要移除有问题的 sdc,并且已经添加了新磁盘 sddvg0 中。

# 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 上还有充足的剩余空间。

关键检查点:使用 vgsvgdisplay 确认空间

在开始之前,运行以下命令:

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
  • 卷组总空闲空间 VFree10G + 20G = 30G

啊哦!这里就出现问题了! 你想移走 50G 的数据,但卷组里只有 30G 的空闲空间。这 不够!在这种情况下,你不能进行 pvmove,否则会失败。

解决方案是什么?

  1. 添加新硬盘(你最初想避免的)。
  2. 先缩减逻辑卷:如果逻辑卷本身没有用满所有空间,你可以先缩小文件系统和逻辑卷,释放出空间,然后再迁移。
    • 警告:缩减文件系统是高风险操作,必须提前备份,并严格按照顺序操作(先文件系统检查 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:它非常智能,会自动利用卷组内所有物理卷上的空闲空间来存放迁移的数据,你不需要指定目标。
  • 事前检查至关重要:务必使用 pvsvgs 命令精确计算空间,这是操作成功的前提。

所以,你的想法是完全正确的。在资源充足的情况下,优先使用现有空间是更简洁高效的方案。

在执行 pvmove 操作时,要移动的硬盘还会被写入数据么?

这是一个非常好的问题,也是理解 pvmove 工作原理的关键。

简短回答是:会,但 LVM 有机制保证数据一致性。

详细解释如下:


pvmove 的工作原理

pvmove 不是一个简单的“一次性复制”工具。它是一个在线、动态的迁移过程。可以把它想象成一个非常智能的“搬运工”,其工作流程大致如下:

  1. 锁定并迁移一个数据段(Extent):LVM 会先锁定源物理卷(比如 /dev/sdb)上一个最小的数据单元(称为一个 Extent,通常默认是 4MB)。
  2. 复制数据:将该 Extent 的数据块从 /dev/sdb 读取出来。
  3. 写入新位置:将这些数据块写入到目标物理卷(比如 /dev/sdd)的空闲空间中。
  4. 更新元数据(重映射指针):这是最关键的一步。在确保数据成功写入新位置后,LVM 会更新其内部的元数据表。这个表类似于一个“地图”,它告诉系统“逻辑卷 X 的第 Y 个块实际上位于物理磁盘 Z 的哪个位置”。LVM 会将这个指针从指向 /dev/sdb 的旧位置更新为指向 /dev/sdd 的新位置。
  5. 释放旧空间:更新指针后,源物理卷(/dev/sdb)上的那个 Extent 就被标记为空闲,原来的数据作废。
  6. 循环往复:对源物理卷上的每一个数据段重复步骤 1-5。

在此期间的数据写入如何处理?

现在来回答你的核心问题:如果在 pvmove 正在迁移某个数据段时,应用程序恰好要写入这个数据段,会发生什么?

LVM 通过以下机制确保数据安全和一致性:

  1. 锁定机制:当 pvmove 开始处理一个特定的数据段时,它会暂时锁定这个区域。在这极短的时间内,任何试图访问(读或写)这个数据段的 I/O 请求都会被暂时阻塞(排队等待)。
  2. 先迁后改pvmove 会完成这个数据段的迁移(包括更新元数据指针)后,再释放锁。
  3. 指针已更新:锁释放后,所有后续的 I/O 请求(包括刚才被阻塞的写请求)都会根据新的元数据指针,直接发往新的物理位置(/dev/sdd,而不会再触及旧位置(/dev/sdb)。

这意味着,对于任何一个特定的数据块,一旦它被 pvmove 迁移完成,旧的物理位置就再也不会被访问了。 之后对这个数据块的所有写入操作都会直接应用到新的位置。

结论与影响

  • 数据一致性:这个机制保证了在整个漫长的迁移过程中,数据的一致性得到保障。不会发生数据丢失或错乱。
  • 性能影响:这是最大的影响。
    • 双重I/O负载pvmove 本身会产生大量的读(从源盘)和写(到目标盘)操作。
    • I/O 延迟增加:由于上述的锁定和额外的迁移I/O,应用程序发起的正常I/O请求可能会遇到更高的延迟,因为磁盘需要同时处理业务I/O和迁移I/O。
    • 对问题磁盘的压力:如果源磁盘(/dev/sdb)本身就是因为性能差(如响应慢、有坏道)而要移除,那么 pvmove 过程中密集的读取操作可能会让它的性能问题暴露得更明显,甚至可能导致迁移过程变慢或暂时卡顿。

给您的建议

  1. 选择低峰期操作:由于对性能有显著影响,务必在系统负载最低的时间段(如深夜或业务维护窗口)执行 pvmove
  2. 监控进度和性能:使用 iostatpvmove -i 5 等工具监控迁移进度和磁盘I/O状况。
  3. 耐心等待:尤其是当源磁盘性能不佳时,迁移速度可能很慢,请保持耐心,不要中断进程。中断 pvmove 可能需要回滚,过程比较复杂。

总之,pvmove 在设计上就考虑到了在线迁移的需求,它会安全地处理并发写入,但代价是会增加系统整体的I/O负载和延迟。

如果 pvmove 过程中,ssh断开了,是不是会有风险?

是的,这确实是一个重要的风险点。 SSH 连接断开本身不会直接损坏数据,但会导致 pvmove 进程被终止,使迁移过程进入一个 未完成的中间状态,带来复杂性和潜在风险。


SSH 断开后会发生什么?

当你通过 SSH 启动 pvmove 后,这个进程是在远程服务器上运行的。但是,如果 SSH 连接异常断开(网络问题、客户端休眠等),默认情况下,Linux 会向由该 SSH 会话启动的所有进程发送 SIGHUP 信号,这通常会导致 pvmove 进程被终止。

主要风险和处理方法

风险 1:迁移过程中断,处于不完整状态

这是最可能发生的情况。pvmove 不是原子操作,它是一点一点迁移的。中断时,可能只迁移了一部分数据。

  • LVM 如何保证安全? LVM 的设计很稳健。它只在一个数据段(Extent) 被成功迁移并更新元数据后,才会标记旧数据段为空闲。这意味着,在中断的那一刻:

    • 已经迁移的数据:其元数据指针已经指向了新位置,这些数据是安全的、一致的。
    • 尚未迁移的数据:仍然保留在源磁盘(/dev/sdb)的原始位置上,也是安全的。
    • 不会出现“数据一半在旧盘,一半在新盘”的损坏情况。LVM 的元数据始终指向一个唯一正确的数据块位置。
  • 中断后怎么办?

    1. 重新连接 SSH
    2. 检查 pvmove 进程是否还在:使用 ps aux | grep pvmove 查看。有时如果使用 nohupscreen,进程可能还在运行。
    3. 如果进程已终止:直接再次运行相同的 pvmove 命令

      sudo pvmove /dev/sdb

    LVM 非常智能,它会自动检测到迁移任务没有完成。它会从头开始扫描,但会跳过那些已经完成迁移的数据段,只继续迁移剩余的部分。你可以无缝地继续这个任务。

风险 2:元数据损坏(风险极低,但理论上存在)

如果在 pvmove 正在更新元数据的那一刻(即,刚写完新位置,正准备更新指针时)连接断开,有极低概率导致元数据不一致。

  • LVM 的防护机制:LVM 使用事务性元数据更新。它通常先写日志,再提交更改,这类似于数据库的事务机制,可以最大限度地避免元数据损坏。
  • 怎么办:如果最坏情况发生,你可以使用 vgcfgrestore 命令来尝试恢复到一个已知良好的元数据备份副本(LVM 会自动备份元数据)。

风险 3:无法监控进度和应对突发问题

这是最大的操作风险。连接断开后,你将:

  • 无法看到迁移进度。
  • 无法收到任何可能的错误提示(例如,目标磁盘空间不足、源磁盘出现读取错误)。
  • 如果系统因 I/O 负载过高出现其他问题,你将无法登录系统进行干预。

最佳实践:如何避免 SSH 断开的风险?

绝对不要 直接在前台运行长时间的 pvmove 命令。请务必使用以下方法之一来使其在后台安全运行。

方法一:使用 screentmux(推荐)

这些是“终端复用器”,可以让你创建一个持久化的会话,即使 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/tmuxnohup 来运行长时间的 pvmove 命令。这应该成为你的标准操作流程。

所以,请务必在开始 pvmove 前,先建立一个持久的会话环境,然后再执行命令。

执行了pvmove后,没有移除硬盘,这个硬盘还会被调度写入新数据么?

不会。 执行 pvmove 命令后,即使你没有手动移除该硬盘,LVM 的元数据已经更新,所有新的数据写入操作都会指向新的物理位置,而不会再写入到原来的硬盘上 。

详细解释

pvmove 命令的核心机制是 元数据更新,而不是简单的数据复制。它的工作流程可以概括为:

  1. 数据复制pvmove 会将源物理卷(例如 /dev/sdb)上的数据块(PE)逐个复制到卷组中其他物理卷的空闲区域 。
  2. 元数据更新(关键步骤):在确保数据成功复制到新位置后,LVM 会立即更新其内部的元数据。这个元数据就像一个“地图”,它记录了逻辑卷(LV)的每个数据块(LE)实际存储在哪个物理卷(PV)的哪个位置 。
  3. 释放旧空间:一旦元数据更新完成,源物理卷上的原始数据块就会被标记为空闲,可以被其他逻辑卷使用。但更重要的是,所有后续的读写请求都会根据新的元数据“地图”直接访问新的数据位置

验证方法

你可以通过以下命令来验证这个状态:

  1. 查看物理卷使用情况

    sudo pvs -o+pv_used

    如果 pvmove 成功完成,你会发现源硬盘(如 /dev/sdb)的 Used 列显示为 0,而其他硬盘的 Used 列会增加。这表明数据已经全部移走,该硬盘在 LVM 层面已经是一个“空盘”了 。

  2. 查看逻辑卷的物理分布

    sudo lvdisplay -m

    这个命令会显示逻辑卷的物理段映射关系。在输出中,你将看不到源硬盘(如 /dev/sdb)的路径,取而代之的是其他硬盘的路径。这直接证明了数据已经不再依赖源硬盘 。

结论与建议

  • 结论pvmove 完成后,源硬盘上的数据已经清空,并且系统不会再向它写入任何新数据。即使你暂时不执行 vgreducepvremove,数据也是安全的 。
  • 建议:虽然数据已经安全,但为了保持存储架构的整洁并释放该硬盘的 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 操作,按照你要求的频率发送通知,非常适合在生产环境中使用。

发表评论
博客分类