Linux运维必备:一个LVM管理添加和扩容脚本的诞生记(完整版)

从简单需求到踩坑无数,最终炼成自动化神器。本文记录了一个LVM管理脚本的完整开发历程,包含所有踩过的坑和最终解决方案。附赠可直接使用的纯净版脚本。

一、起因:一个看似简单的小需求

那天,一位运维兄弟在群里求助:

"有没有脚本可以自动创建LVM分区并扩容现有LV?最好还能格式化新磁盘。"

听起来很简单对吧?不就是:

  1. 创建分区 → pvcreate → vgextend → lvextend → 扩容文件系统

  2. 或者全新配置:分区 → PV → VG → LV → 格式化 → 挂载

于是我随手写了个脚本,没想到就此开启了三天三夜的踩坑之旅...

二、第一版:理想很丰满,现实很骨感

bash 复制 # 初始版本(已废弃) ./lvm_manager.sh extend /dev/sdb /dev/centos/root

问题1:不准确的警告提示

  • 原逻辑:只要操作就提示"会格式化所有数据"

  • 实际情况:extend模式根本不会格式化现有数据

  • 反馈:"扩展LV不需要格式化啊!"

问题2:GPT分区表损坏

Error: 备份 GPT 表不像应该的那样出现在磁盘的末尾

  • 原因:虚拟化平台扩容磁盘后,GPT备份表仍在旧位置,系统认为分区表损坏。

问题3:函数返回值污染(最致命的bug)

Device not found [INFO] not found.

这是最让人崩溃的bug。create_partition_smart()函数输出了日志,导致返回值变成了:

复制 [WARNING] 检测到GPT分区表损坏 [INFO] GPT修复成功 /dev/sdb2

pvcreate接收到的参数是[WARNING],自然报错!

三、踩坑与填坑:问题逐一击破

3.1 净化函数返回值(核心中的核心)

问题根源:函数返回值被日志污染

bash 复制 # 错误示范 create_partition() { log"正在创建分区"# ← 污染了stdout echo"/dev/sdb2" # ← 这才是想要的返回值 } # 正确示范 create_partition() { echo"创建分区..." >&2 # 日志转到stderr echo"/dev/sdb2" # 仅返回数据到stdout }

解决方案:

  • 所有日志函数(log/warn/error)强制输出到>&2

  • 函数内部只返回设备名到stdout

  • 调用时严格区分:PART=$(create_partition)只捕获stdout

作用说明:日志输出到stderr后,在终端仍然可见(stderr默认也会显示在终端),但不会影响命令执行。如果需要持久化日志,需手动重定向:

bash 复制 ./lvm_manager.sh extend /dev/sdb /dev/vgdata1/lvdata1 2> /tmp/lvm.log

3.2 GPT修复自动化与日志污染的关联

早期失败原因:修复函数本身也污染了返回值

bash 复制 # 错误示范 fix_gpt() { echo "修复GPT..." # ← 污染了返回值 sgdisk -e /dev/sdb }

当修复函数被调用时,它的"修复GPT..."日志会混入返回值,导致后续命令接收错误参数。

最终方案:修复函数也遵循相同原则,所有日志输出到stderr

bash 复制 fix_gpt_table() { warn "检测到GPT分区表损坏,正在修复..." >&2 # 转到stderr sgdisk -e /dev/sdb >/dev/null 2>&1 log "GPT修复成功" >&2 # 转到stderr }

3.3 xfs_growfs 路径问题(重点澄清)

原始错误:

bash 复制 # 错误的路径构建方式 xfs_growfs /dev/mapper/$(basename $(dirname $LV_PATH))-$(basename $LV_PATH) # 错误结果 xfs_growfs /dev/mapper/mapper-vgdata1-lvdata1 # ← 重复了mapper

指正:xfs_growfs 实际上可以接受LV设备路径(如 /dev/vgdata1/lvdata1),并不一定需要挂载点。

最终选择挂载点的原因:

  1. 代码健壮性:无论LV路径是 /dev/vgdata1/lvdata1 还是 /dev/mapper/vgdata1-lvdata1,挂载点都能正确工作

  2. 避免路径构建错误:通过findmnt获取挂载点,无需手动拼接路径

bash 复制 # 最终方案:通过挂载点执行(最可靠) MOUNT_POINT=$(findmnt -n -o TARGET --source $LV_PATH) xfs_growfs $MOUNT_POINT

结论:说得对,LV路径确实可以直接用,但我们选择挂载点是为了代码的健壮性和通用性。

3.4 剩余空间检测

问题:lsblk显示650G磁盘上只有600G分区,但脚本仍报错"剩余空间不足"

原因:parted print显示分区已占用整个磁盘(包含未显示的保留扇区)

解决方案:使用parted print free精确计算可用空间

bash 复制 FREE_MB = 磁盘总大小 - 最后一个分区End位置

3.5 工具选择与可用性

sgdisk vs parted:

  • sgdisk : 更稳定,GPT操作王者,通常默认已安装(属于gdisk包)

  • parted : 功能全,但交互式自动化不稳定

  • gdisk : 稳定,但需要重定向输入

最终选择:优先使用sgdisk,如果未找到再提示手动修复。

四、最终成果:双模式LVM管理脚本

4.1 功能特性

✅ 双模式支持:

  • extend:在剩余空间创建新分区,扩容现有LV

  • new:全新磁盘配置(分区→PV→VG→LV→格式化→挂载)

✅ 自动GPT修复:磁盘扩容后自动修复分区表

✅ 纯净返回值:函数只返回设备名,不污染命令参数

✅ 安全保护:所有危险操作都有二次确认

✅ 完整流程:自动添加fstab、扩展文件系统等

4.2 完整纯净版脚本

这是历史聊天中最后一版完全一致的脚本:

bash 复制 #!/bin/bash # LVM管理脚本 - 最终纯净版 # 关键修复:所有日志输出到stderr,确保函数返回值纯净 set -e # 颜色输出(stderr) RED='\033[0;31m' >&2 GREEN='\033[0;32m' >&2 YELLOW='\033[0;33m' >&2 NC='\033[0m' >&2 # 日志函数(强制stderr) log() { echo -e "${GREEN}[INFO]${NC} $1" >&2; } warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; } error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; } check_root() { [[ $EUID -eq 0 ]] || error "必须以root运行"; } # GPT修复(所有输出到stderr) fix_gpt_table() { local DISK=$1 if parted -s $DISKprint 2>&1 | grep -q "fix the GPT"; then warn "检测到GPT分区表损坏,正在修复..." ifcommand -v sgdisk &>/dev/null; then # sgdisk -e: 将备份GPT表移动到磁盘末尾 if sgdisk -e $DISK 2>&1 | grep -q "The backup GPT header is corrupt"; then warn "尝试gdisk自动修复..." printf"r\ne\nw\nY\n" | gdisk $DISK >/dev/null 2>&1 fi else error "sgdisk工具未找到,请先手动修复GPT:\n parted $DISK print\n 输入: fix" fi partprobe $DISK sleep 2 log"GPT修复成功" fi } # 获取磁盘大小 get_disk_size_mb() { blockdev --getsize64 $1 | awk '{print int($1/1024/1024)}' } # 获取最后分区结尾(静默) get_last_end() { parted -s $1 unit MB print free 2>/dev/null | grep "^[[:space:]][0-9]" | grep -v "Free Space" | tail -1 | awk '{print int($3)}' } # 获取新分区号(静默) get_next_num() { local COUNT=$(lsblk -ln -o NAME $1 2>/dev/null | grep -c "^$(basename $1)[0-9]") echo $((COUNT + 1)) } # 核心函数:只返回设备名,无任何stdout输出 create_partition_smart() { local DISK=$1 local SIZE=$(get_disk_size_mb $DISK) local LAST=$(get_last_end $DISK) [[ -z "$LAST" ]] && LAST=0 local FREE=$((SIZE - LAST)) [[ $FREE -lt 1024 ]] && { echo"ERROR:剩余空间不足" >&2; return 1; } local NUM=$(get_next_num $DISK) local PART="${DISK}${NUM}" [[ -b "$PART" ]] && { echo"ERROR:分区已存在" >&2; return 1; } # 修复GPT(所有日志到stderr) fix_gpt_table $DISK # 创建分区表(如有需要) [[ $LAST -eq 0 ]] && parted -s $DISK mklabel gpt >/dev/null 2>&1 # 创建分区(静默) parted -s $DISK mkpart primary ${LAST}MB 100% >/dev/null 2>&1 || return 1 parted -s $DISKset$NUM lvm on >/dev/null 2>&1 partprobe $DISK 2>/dev/null sleep 2 # 只返回设备名(stdout) [[ -b "$PART" ]] && echo"$PART" || return 1 } # Extend模式 extend_lv() { local DISK=$1 local LV_PATH=$2 [[ -b "$LV_PATH" ]] || error "LV不存在" local VG=$(lvs --noheadings -o vg_name $LV_PATH 2>/dev/null | awk '{print $1}') [[ -n "$VG" ]] || error "无法获取VG名称" log"目标卷组: $VG" lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT $DISK local PART=$(create_partition_smart $DISK) || error "分区创建失败!" log"成功创建: $PART" pvcreate $PART vgextend $VG$PART lvextend -l +100%FREE $LV_PATH local FS=$(blkid -o value -s TYPE $LV_PATH) case$FSin xfs) local MNT=$(findmnt -n -o TARGET --source$LV_PATH 2>/dev/null || df $LV_PATH 2>/dev/null | tail -1 | awk '{print $NF}') [[ -n "$MNT" ]] && xfs_growfs $MNT || warn "无法找到挂载点" ;; ext4|ext3) resize2fs $LV_PATH ;; *) warn "不支持的文件系统: $FS" ;; esac log"扩展完成!" df -h $LV_PATH } # New模式 new_config() { local DISK=$1 local VG=$2 local LV=$3 local MNT=$4 log"警告: 将格式化 $DISK" read -p "确认? (yes/no): " CONFIRM [[ "$CONFIRM" == "yes" ]] || exit 0 local PART=$(create_partition_smart $DISK) || error "分区创建失败!" log"成功创建: $PART" pvcreate $PART if vgdisplay $VG &>/dev/null; then vgextend $VG$PART else vgcreate $VG$PART fi local LV_PATH="/dev/$VG/$LV" lvcreate -l 100%FREE -n $LV$VG log"文件系统 (1:XFS 2:EXT4 3:自定义)?" read -p "选择 (默认1): " CHOICE CHOICE=${CHOICE:-1} case$CHOICEin 1) mkfs.xfs $LV_PATH ;; 2) mkfs.ext4 $LV_PATH ;; 3) read -p "输入类型: " TYPE; mkfs.$TYPE$LV_PATH ;; *) error "无效选择" ;; esac [[ -d "$MNT" ]] || mkdir -p $MNT mount $LV_PATH$MNT blkid -o value -s UUID $LV_PATH | xargs -I {} echo"UUID={} $MNT xfs defaults 0 2" >> /etc/fstab log"完成!" df -h $MNT } # 主函数 main() { check_root case"$1"in extend) [[ $# -eq 3 ]] || error "用法: $0 extend <磁盘> <LV路径>" extend_lv $2$3 ;; new) [[ $# -eq 5 ]] || error "用法: $0 new <磁盘> <VG名> <LV名> <挂载点>" new_config $2$3$4$5 ;; *) echo"LVM管理脚本" >&2 echo"模式1: extend <磁盘> <LV路径>" >&2 echo"模式2: new <磁盘> <VG名> <LV名> <挂载点>" >&2 exit 1 ;; esac } # 调用主函数 main "$@"

4.3 使用方法与日志说明

bash 复制 # 1. 赋予执行权限 chmod +x lvm_manager.sh # 2. 扩展现有LV(自动创建sda3/sdb2等) ./lvm_manager.sh extend /dev/sda /dev/centos/root # 3. 全新磁盘配置 ./lvm_manager.sh new /dev/sdb vg0 data /data # 4. 验证结果 lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT

关于日志输出:

  • 所有日志强制输出到 stderr(标准错误流)

  • 在终端运行时,stderr 和 stdout 都会显示,所以你能看到所有日志

  • 日志不会污染命令返回值,这是关键

  • 如需保存日志到文件:

bash 复制 ./lvm_manager.sh extend /dev/sda /dev/centos/root 2> /tmp/lvm.log

五、技术要点总结

5.1 核心价值

  1. 自动化:一键完成LVM扩容全流程

  2. 安全性:所有危险操作二次确认,GPT修复有回退机制

  3. 兼容性:支持XFS/EXT4,自动适配挂载点

  4. 可维护性:模块化设计,日志清晰

5.2 踩坑启示录

教训1:函数返回值必须纯净

bash 复制 # 永远不要 myfunc() { echo"日志"# 污染返回值 echo"结果" } # 应该 myfunc() { echo"日志" >&2 # 转到stderr echo"结果" # 仅返回数据 }

教训2:虚拟化扩容 != 分区表更新

磁盘扩容后必须更新GPT备份表,否则parted拒绝操作

教训3:工具的可用性

  • sgdisk : 通常默认已安装(属于gdisk包),GPT操作王者

  • parted : 功能全,但交互式自动化不稳定

  • gdisk : 稳定,但需要重定向输入

教训4:xfs_growfs 需要的是挂载点

虽然 LV 设备路径(如 /dev/vgdata1/lvdata1)也可以用,但我们选择挂载点是为了代码的健壮性和通用性,避免路径格式问题。

六、写在最后

这个脚本的开发过程生动诠释了:简单需求背后可能隐藏着复杂的工程问题。

从最初的50行代码到最终的200行健壮脚本,我们经历了:

  • 3次重大重构

  • 8个bug修复

  • 5次版本迭代

核心收获:

  1. 返回值纯净是脚本可靠的基石

  2. 自动化不是万能的,必须有回退方案

  3. 日志清晰比功能强大更重要

  4. stderr 是日志的好朋友,不会污染返回值

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1190666.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

openocd操作ku060板子记录

OpenOCD 操作 KU060 FPGA 核心原理 概述 本文档深入解析 OpenOCD 如何通过 JTAG 接口操作 KU060 FPGA 开发板&#xff0c;包括 Flash 刷写、内存检查、GDB 调试和板子状态检查等核心功能的底层原理。1. OpenOCD 架构与连接原理 1.1 JTAG 接口连接 主机(PC) --USB--> FT2232 …

《排序算法全解析:从基础到优化,一文吃透八大排序!》

本文详解冒泡、选择、插入等基础排序,以及快排、归并、堆排、希尔等高级排序的原理、c语言代码实现,对比各算法时间复杂读/空间复杂度,附代码详细注释,帮你彻底搞懂排序!一、为什么我们需要读懂排序--不止于"…

Linux命令大全-grep命令

一、简介grep(英文全拼: Global Regular Expression Print)命令用于查找文件里符合条件的字符串或正则表达式&#xff0c;并将符合条件的内容进行输出。二、语法语法&#xff1a;grep [选项]... PATTERN [FILE]...PATTERN&#xff1a;是一个基本正则表达式(缩写为BRE)&#xff…

ue c++编译报错解决

目录 e1696 无法打开 源 文件 "IMQTTClient.h" 解决方法&#xff1a; e1696 无法打开 源 文件 "IMQTTClient.h" 解决方法&#xff1a; 删除 目录&#xff1a; .vs Binaries Intermediate Saved 选中文件&#xff1a; MetahumancharacterHeiXi.uproje…

解析nanogpt - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

计算机毕业设计springboot线上票务系统app 基于Spring Boot的移动票务管理平台开发 Spring Boot框架下的线上票务系统设计与实现

计算机毕业设计springboot线上票务系统app_ck27e &#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。随着互联网技术的飞速发展&#xff0c;线上票务系统已经成为人们日常生活中不可…

集合幂级数全家桶

集合幂级数 exp 求 \(e^{F(x)} = \sum\limits_{i \ge 0} \dfrac{F(x)^i}{i!}\),其中若 \(S \cap T = \varnothing\),则 \(x^S \times x^T = x^{S \cup T}\)。 定义二元函数 \(F(x, y) = \sum x^S y^{|S|} a_S\),这样…

计算机毕业设计springboot大气网格化治理智慧平台-报警处理子系统 基于SpringBoot的城市大气环境网格化智能预警与处置平台 SpringBoot驱动的空气质量网格监管报警协同系统

计算机毕业设计springboot大气网格化治理智慧平台-报警处理子系统9w1d1 &#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。PM2.5爆表那天&#xff0c;整个城市像被按下了灰色滤镜。…

AtCoder Beginner Contest竞赛题解 | AtCoder Beginner Contest 440

​欢迎大家订阅我的专栏&#xff1a;算法题解&#xff1a;C与Python实现&#xff01; 本专栏旨在帮助大家从基础到进阶 &#xff0c;逐步提升编程能力&#xff0c;助力信息学竞赛备战&#xff01; 专栏特色 1.经典算法练习&#xff1a;根据信息学竞赛大纲&#xff0c;精心挑选…

近十届两院增选院士籍贯 / 出生地排行:苏浙皖湘鲁霸榜

整理了近十届两院增选院士的籍贯、出生地数据&#xff1a; ✅ 维度一&#xff1a;院士「籍贯」&#xff08;最能体现地域文教底蕴&#xff09; 籍贯人数 TOP20 核心榜单&#xff08;头部集中度极高&#xff09; 1、浙江宁波 28 人&#xff08;全国第一&#xff0c;70年的稳定输…

LeetCode 379 电话目录管理系统

文章目录摘要描述题解答案题解代码分析1. 数据结构的选择2. 初始化方法3. get() 方法&#xff1a;分配号码4. check() 方法&#xff1a;检查号码是否可用5. release() 方法&#xff1a;释放号码6. 为什么使用 Set Array 的组合&#xff1f;7. 边界情况处理示例测试及结果示例 …

量子计算模拟器性能基准测试方法论

随着量子算法在金融建模、药物研发等领域的应用突破&#xff0c;量子计算模拟器已成为经典计算机环境验证量子程序的核心工具。软件测试从业者亟需建立一套针对量子特性的标准化基准测试体系。本文旨在系统阐述测试框架的设计原则、关键性能指标及工具链实践方案&#xff0c;为…

基于微信小程序的电子元器件商城系统源码文档部署文档代码讲解等

课题介绍本课题旨在开发一款基于微信小程序的电子元器件商城系统&#xff0c;适配电子元器件品类多、规格杂、采购场景多元的特性&#xff0c;解决传统采购渠道分散、比价繁琐、库存查询不便等痛点。系统以微信小程序为前端载体&#xff0c;依托Node.js搭建后端服务&#xff0c…

【Linux 网络】拒绝传输卡顿!滑动窗口如何让数据 “跑赢” 等待?

一、滑动窗口滑动窗口大小&#xff1a;指的是无需等待确认应答而可以继续发送数据的最大值&#xff1b;注意&#xff1a;这里的无需等待确认应答&#xff0c;不是不要确认应答&#xff0c;而是暂时不要&#xff1b;站在发送方&#xff08;主机A 视角&#xff09;&#xff1a;图…

硬核干货:Checkpoint对齐诅咒与Timer风暴——Flink周期性反压的终极排查

第一章&#xff1a;那只准时敲门的“幽灵”——Checkpoint与其背后的IO风暴我们拿到的是一个极其诡异的现场&#xff1a;每30分钟一次&#xff0c;持续5分钟的反压。这不像是因为数据倾斜导致的“长尾”&#xff0c;也不像代码逻辑死循环导致的“猝死”。它太规律了&#xff0c…

基于微信小程序的付费自习室系统源码文档部署文档代码讲解等

课题介绍本课题聚焦付费自习室行业数字化需求&#xff0c;设计并实现一款基于微信小程序的付费自习室系统&#xff0c;解决传统自习室预约繁琐、计费不透明、座位管理低效等痛点。系统以微信小程序为前端交互入口&#xff0c;采用Node.js搭建后端服务&#xff0c;搭配MySQL数据…

基于微信小程序的高校毕业生公考助手系统源码文档部署文档代码讲解等

课题介绍本课题针对高校毕业生公考备考信息零散、规划混乱、刷题低效等痛点&#xff0c;设计并实现一款基于微信小程序的高校毕业生公考助手系统&#xff0c;为毕业生提供一站式公考备考服务。系统以微信小程序为前端载体&#xff0c;采用Node.js搭建后端服务&#xff0c;结合M…

边缘计算节点延迟专项测试实践指南

1. 测试概述与重要性 边缘计算节点的延迟直接影响实时应用性能&#xff08;如工业自动化、车联网&#xff09;&#xff0c;延迟过高可能导致业务中断或数据不一致。专项测试需评估端到端响应时间、抖动及丢包率等指标&#xff0c;确保节点在5G等低延迟场景下满足SLA要求&#…

大数据领域Kafka的性能调优实战

大数据领域Kafka的性能调优实战&#xff1a;从青铜到王者的进阶指南 关键词&#xff1a;Kafka性能调优、生产者优化、Broker配置、消费者调优、吞吐量与延迟 摘要&#xff1a;在大数据时代&#xff0c;Kafka作为分布式消息队列和流处理平台的"扛把子"&#xff0c;其性…

Flutter 2025 测试策略全景:从单元测试到混沌工程,构建坚不可摧的高质量应用 - 指南

Flutter 2025 测试策略全景:从单元测试到混沌工程,构建坚不可摧的高质量应用 - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; …