PVE 软路由折腾记:ImmortalWrt squashfs 磁盘扩容完美解决方案

  • ~8.33K 字
  1. 1. PVE 软路由折腾记:ImmortalWrt squashfs 磁盘扩容完美解决方案
    1. 1.1. 问题背景
    2. 1.2. 我遇到的具体问题
    3. 1.3. 解决方案详解
      1. 1.3.1. 核心思路
      2. 1.3.2. 关键命令对比
    4. 1.4. 一键扩容脚本
      1. 1.4.1. 脚本特性
      2. 1.4.2. 脚本内容
    5. 1.5. 实际操作过程
      1. 1.5.1. 1. 运行扩容脚本
      2. 1.5.2. 2. 执行结果
      3. 1.5.3. 3. 关于对齐警告
    6. 1.6. PVE 虚拟机部署
      1. 1.6.1. 方法一:导入到 PVE 存储(推荐)
      2. 1.6.2. 方法二:直接写入物理磁盘
    7. 1.7. 首次启动后的验证
      1. 1.7.1. 1. 检查 overlay 分区大小
      2. 1.7.2. 2. 如果自动扩容失败
      3. 1.7.3. 3. 排查问题的常用命令
    8. 1.8. 总结与注意事项
      1. 1.8.1. 关键要点
      2. 1.8.2. 性能对比
      3. 1.8.3. 适用场景
      4. 1.8.4. 后续优化

PVE 软路由折腾记:ImmortalWrt squashfs 磁盘扩容完美解决方案

在 Proxmox VE 上部署 ImmortalWrt 软路由时,遇到了 squashfs 文件系统的磁盘扩容问题。经过一番折腾,终于找到了完美的解决方案。本文记录了从问题发现到解决的完整过程。

问题背景

在 PVE 环境中部署 ImmortalWrt 软路由时,通常会遇到镜像空间不足的问题。官方提供的 squashfs-combined 镜像默认只有 300MB 左右,但实际使用中我们往往需要更大的存储空间来安装插件、存储配置文件等。

传统的磁盘扩容方法在 squashfs 文件系统上会遇到各种问题,主要原因是:

  1. 文件系统特性:squashfs 是只读压缩文件系统,需要配合 overlay 来提供可写层
  2. 分区结构:ImmortalWrt 使用特殊的分区布局,直接调整分区会破坏文件系统
  3. 扇区对齐:不正确的扇区对齐会导致性能下降或启动失败

我遇到的具体问题

在尝试扩容过程中,发现了一个关键问题:

1
2
3
4
5
# 原本想要的操作
dd if=immortalwrt.img of=openwrt-8g.img conv=fsync

# 问题:这会覆盖整个文件,导致 8GB 镜像被截断为原始大小
Disk openwrt-8g.img: 332.5 MiB, 348651520 bytes, 680960 sectors

问题分析

  • 创建了 8GB 的空镜像
  • dd 写入时使用覆盖模式,导致文件大小被强制缩减为原始固件大小(332MB)
  • 后续 fdisk 分区时发现可用范围不对,最后扇区只能到 680959

解决方案详解

核心思路

正确的流程应该是:

  1. 创建指定大小的空镜像
  2. 将固件写入镜像头部,不截断文件(关键:使用 notrunc 参数)
  3. 读取原始分区信息
  4. 重新创建分区表,将第二分区扩展到磁盘末尾

关键命令对比

错误方式(会截断文件):

1
dd if=immortalwrt.img of=openwrt-8g.img conv=fsync

正确方式(保持文件大小):

1
dd if=immortalwrt.img of=openwrt-8g.img bs=1M conv=fsync,notrunc

notrunc 参数的作用:

  • 不截断输出文件
  • 保持 openwrt-8g.img 的 8GB 大小
  • 仅写入头部区域,不影响文件末尾

一键扩容脚本

经过多次测试,我整理了一个非交互式的自动化脚本:

脚本特性

  • ✅ 全自动处理,无需手动交互
  • ✅ 支持 .img.gz 压缩格式自动解压
  • ✅ 自动检测分区起始扇区,确保不破坏 squashfs 签名
  • ✅ 智能处理分区表,将第二分区扩展到 100%
  • ✅ 完整的错误处理和依赖检查

脚本内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/usr/bin/env bash
set -euo pipefail
# mk-openwrt-squashfs-expanded.sh
# Usage:
# ./mk-openwrt-squashfs-expanded.sh <INPUT_IMG_OR_GZ> <OUT_IMG> <SIZE>
# Example:
# ./mk-openwrt-squashfs-expanded.sh immortalwrt-24.10.4-...-squashfs-combined.img.gz openwrt-8g.img 8G

if [ "$#" -ne 3 ]; then
echo "Usage: $0 <input-img-or-gz> <out-img> <size (e.g. 8G)>"
exit 2
fi

IN="$1"
OUT="$2"
SIZE="$3"

# Check dependencies
for cmd in parted sfdisk dd truncate grep awk sed; do
command -v $cmd >/dev/null 2>&1 || { echo "Required command not found: $cmd"; exit 3; }
done

WORKDIR="$(pwd)"
TMPDIR="$(mktemp -d)"
cleanup() { rm -rf "$TMPDIR"; }
trap cleanup EXIT

# 1) If input is .gz, decompress to tmp
if [[ "$IN" =~ \.gz$ ]]; then
echo "Decompressing $IN ..."
gunzip -c "$IN" > "$TMPDIR/firm.img"
SRC="$TMPDIR/firm.img"
else
SRC="$IN"
fi

# 2) Create or truncate output to requested SIZE (sparse via truncate)
echo "Creating output image $OUT of size $SIZE ..."
truncate -s "$SIZE" "$OUT"

# 3) Write firmware into head of OUT without truncating (notrunc)
echo "Writing firmware into head of $OUT (notrunc) ..."
dd if="$SRC" of="$OUT" bs=1M conv=fsync,notrunc status=progress

# 4) Ensure parted can see full size
echo "Verifying output image size..."
parted -s "$OUT" unit s print > "$TMPDIR/parted_before.txt" 2>/dev/null || true

# 5) Read original partition 2 start sector from the written image (use parted on temporary loop)
# Use losetup to map image to loop so parted prints reliably
LOOP=$(losetup --show -fP "$OUT")
echo "Loop device: $LOOP"
# Wait tiny bit
sleep 0.2

# get partition start for partition 2 (in sectors)
# parted prints lines like: " 2 66560s 680959s 614400s 300M 83 Linux"
START_SECTOR=$(parted -s "$LOOP" unit s print | awk '/^ 2/ {gsub("s","",$2); print $2; exit}')
if [ -z "$START_SECTOR" ]; then
# fallback: parse sfdisk
START_SECTOR=$(sfdisk -d "$LOOP" | awk -F'=' '/start=/ {print $2; exit}' | awk -F',' '{print $1}')
fi

if [ -z "$START_SECTOR" ]; then
losetup -d "$LOOP"
echo "Failed to determine partition 2 start sector."
exit 4
fi

echo "Detected partition 2 start sector: $START_SECTOR"

# 6) Unmap loop before using parted to rewrite partition table on file
losetup -d "$LOOP"
sleep 0.2

# 7) Non-interactively recreate partition 2 with same start and end=100%
# We'll use parted in script mode: remove partition 2, then mkpart with start in sectors and end 100%.
echo "Rewriting partition 2 to start at ${START_SECTOR}s and end at 100%..."
parted -s "$OUT" rm 2
# create new primary partition #2 using sector units
parted -s "$OUT" unit s mkpart primary "${START_SECTOR}s" 100%

# 8) Keep the original partition type/boot flag for partition1 if needed (try to preserve FAT/boot)
# If partition 1 had boot flag, ensure it remains
# preserve boot flag of partition1 if it existed
BOOT_FLAG=$(parted -s "$SRC" print | awk '/^ 1/ && /boot/ {print "on"; exit}')
if [ -n "$BOOT_FLAG" ]; then
parted -s "$OUT" set 1 boot on || true
fi

# 9) sync to disk
sync

# 10) Show final partition layout
echo "Final partition table of $OUT:"
parted -s "$OUT" unit s print

echo "Done. You can now write $OUT to a disk (dd or qm importdisk) and OpenWrt should auto-create an overlay on first boot."

# 11) Cleanup handled by trap
exit 0

实际操作过程

1. 运行扩容脚本

1
2
3
4
5
# 给脚本执行权限
chmod +x mk-openwrt-squashfs-expanded.sh

# 运行脚本(示例:扩容到 8GB)
./mk-openwrt-squashfs-expanded.sh immortalwrt-24.10.4-x86-64-generic-squashfs-combined.img immortalwrt-24.10.4-x86-64-8g.img 8G

2. 执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Creating output image immortalwrt-24.10.4-x86-64-8g.img of size 8G ...
Writing firmware into head of immortalwrt-24.10.4-x86-64-8g.img (notrunc) ...
332+1 records in
332+1 records out
348651520 bytes (349 MB, 332 MiB) copied, 0.375172 s, 929 MB/s
Verifying output image size...
Loop device: /dev/loop1
Detected partition 2 start sector: 66560
Rewriting partition 2 to start at 66560s and end at 100%...
Warning: The resulting partition is not properly aligned for best performance: 66560s % 2048s != 0s
Final partition table of immortalwrt-24.10.4-x86-64-8g.img:
Model: (file)
Disk /root/immortalwrt-24.10.4-x86-64-8g.img: 16777216s
Sector size (logical/physical): 512B/512B
Partition Table: msdos
Disk Flags:

Number Start End Size Type File system Flags
1 512s 66047s 65536s primary ext2 boot
2 66560s 16777215s 16710656s primary

Done. You can now write immortalwrt-24.10.4-x86-64-8g.img to a disk (dd or qm importdisk) and OpenWrt should auto-create an overlay on first boot.

3. 关于对齐警告

输出中出现的警告:

1
Warning: The resulting partition is not properly aligned for best performance: 66560s % 2048s != 0s

说明

  • 这个警告不影响功能,只是性能优化提醒
  • 必须保留起始扇区 66560 以保持 squashfs 签名完整
  • 对于软路由应用,性能影响微乎其微
  • 不建议调整对齐,风险较大

PVE 虚拟机部署

方法一:导入到 PVE 存储(推荐)

请根据实际情况调整命令,我使用的是 nvme 存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 创建虚拟机(不创建磁盘)
qm create 100 --name ImmortalWrt --memory 512 --net0 virtio,bridge=vmbr0 --ostype l26

# 2. 设置 UEFI 启动(因为使用的是 combined-efi 镜像)
qm set 100 --bios ovmf

# 3. 导入磁盘镜像到存储
qm importdisk 100 /root/immortalwrt-24.10.4-x86-64-8g.img nvme

# 4. 附加磁盘到虚拟机
qm set 100 --scsihw virtio-scsi-pci --scsi0 nvme:vm-100-disk-0

# 5. 设置启动磁盘
qm set 100 --boot c --bootdisk scsi0

# 6. 配置串口(可选,便于调试)
qm set 100 --serial0 socket --vga serial0

# 7. 启动虚拟机
qm start 100

方法二:直接写入物理磁盘

1
2
# ⚠️ 警告:会覆盖目标磁盘所有数据
dd if=/root/immortalwrt-24.10.4-x86-64-8g.img of=/dev/sdX bs=4M status=progress conv=fsync

首次启动后的验证

1. 检查 overlay 分区大小

1
2
3
4
# 登录到 ImmortalWrt 后执行
df -H

# 预期输出:/overlay 应该接近分配的大小(如 7.x GB)

2. 如果自动扩容失败

某些版本可能需要手动触发扩容:

1
2
3
4
5
6
7
8
# 强制触发扩容脚本
/etc/init.d/fstab boot

# 或使用 blockd 命令(如果可用)
blockdctl resize || true

# 重启系统
reboot

3. 排查问题的常用命令

1
2
3
4
5
6
7
8
9
10
11
# 查看挂载信息
mount | grep overlay

# 查看分区表
cat /proc/partitions

# 查看系统日志
logread | sed -n '1,200p'

# 查看内核信息
dmesg | tail -n 80

总结与注意事项

关键要点

  1. 必须使用 notrunc 参数:这是成功的关键,避免截断目标文件
  2. 保持起始扇区不变:66560 扇区是 squashfs 签名的位置,不能改动
  3. 使用 UEFI 启动模式:ImmortalWrt 的 combined-efi 镜像需要 UEFI 支持
  4. 首次启动自动扩容:ImmortalWrt 会在首次启动时自动创建并扩展 overlay

性能对比

扩容前后的存储对比:

  • 原始镜像:约 300MB,overlay 空间极小
  • 扩容后镜像:8GB 或更大,充足的插件和配置存储空间
  • I/O 性能:在虚拟化环境中,对齐警告的影响微乎其微

适用场景

这个解决方案适用于:

  • ✅ Proxmox VE 虚拟化环境
  • ✅ ImmortalWrt squashfs-combined 镜像
  • ✅ 需要大容量存储空间的软路由部署
  • ✅ 自动化部署脚本需求

后续优化

部署完成后,可以根据需要进行以下优化:

  • 安装常用插件(科学上网、广告屏蔽等)
  • 配置网络接口和防火墙规则
  • 设置自动备份策略
  • 监控系统资源使用情况

通过这种方法,我们完美解决了 ImmortalWrt 在 PVE 环境下的磁盘扩容问题,为后续的软路由应用打下了坚实的基础。


本文记录了真实的折腾过程,希望能帮助到有同样需求的朋友。如果遇到问题,欢迎交流讨论。