本文方法为原创,使用 ChatGPT 整理润色发布。阅读本文需要一定的技术基础。数据无价,谨慎操作。
问题背景
在 NAS 或多硬盘系统中,常遇到 硬盘挂载点随重启或热拔插变化 的问题:
- 系统的自动挂载器(如
mountmgr
或 udisks)会给硬盘分配 /vol00/硬盘名
路径,但这些路径不是固定的。
- 这对于像 qBittorrent 这种依赖固定路径的应用造成困扰,因为种子路径是固定的,但挂载点每次重启时有几率改变。
因此,为解决上述问题,需要寻求一个办法,将特定的硬盘分区挂载到固定的挂载点。
方案选择
笔者在前期探索时,发现了有以下几种解决方案。
方案 1:使用 fstab + uuid 直接挂载
fstab 基本语法为 :
/dev/sdaX /mnt/path ext4 defaults 0 0
-
优点:配置最为简单,直接。
-
缺点:
-
与系统自带的挂载冲突:
闭源 NAS系统通常有自己的 mountmgr 或后台守护进程,比如飞牛自动把非存储空间的硬盘挂到 /vol00/...
。如果在 /etc/fstab
里再写一份挂载(比如 /mnt/path
),就可能冲突(提示“设备已挂载”或直接失败)
-
灵活性差,安全性低:
若使用诸如 /dev/sda1
这样的设备路径,在硬件顺序变化时,可能会导致挂载失效。
即便可以使用 UUID=
的方式指定设备,也需要自己指定文件系统类型等挂载选项,不便于使用。
此外,错误的配置容易引起系统盘挂载的问题,造成无法开机等后果。
方案 2:udev + 脚本进行 bind
方案 3:udev + systemd service + 脚本进行 bind
- 优点:
- 使用 systemd 执行脚本,绕过 udev sandbox 限制
- 支持等待系统自动挂载完成
- 缺点:
- systemd service 模板需要处理 实例名限制(连字符、特殊字符)
- 环境变量无法传递,需要在脚本中额外再去获取设备名等参数。
由于方案1、方案2受到种种限制,经测试无法完成目标,因此采用方案3的方法。
该方法与系统的自动挂载不冲突,且灵活性好。一个简单的对比图如下:
flowchart LR
subgraph FSTAB[方案一:传统 fstab 挂载方式]
A1[系统启动] --> B1[fstab 尝试挂载]
B1 --> C1[可能冲突: 已由 mountmgr 挂载]
B1 --> D1[路径未知: 无法提前写死]
B1 --> E1[失败: 设备 busy 或挂载点错误]
end
subgraph UDEV[方案三:udev + systemd + 脚本方案]
A[硬盘插入或重启] --> B[内核检测到块设备]
B --> C[udev 触发规则]
C -->|匹配 UUID| D[设置 SYSTEMD_WANTS=disk-bind@<UUID>.service]
D --> E[systemd 启动 disk-bind@<UUID>.service]
E --> F[执行脚本 disk-bind.sh]
F --> G[查 UUID → 别名配置表]
G --> H[等待系统自动挂载完成]
H --> I[bind mount 到 /mnt/mymount/<Alias>]
I --> J[日志记录 → 稳定别名可用]
end
技术要点概览
-
UUID 固定唯一
/dev/disk/by-uuid/
提供了硬盘唯一标识。
- 不随重启或热拔插变化。
-
Alias 配置
-
Device 单元命名规则
- systemd 的
.device
单元会把 /dev/disk/by-uuid/<UUID>
转换成 dev-disk-by\x2duuid-<UUID>.device
- 连字符
-
必须转义为 \x2d
,否则依赖解析失败
-
systemd Template + Environment 方式
- UUID 通过 Environment 变量传给脚本
- 脚本根据 UUID 在配置表中查别名,创建固定挂载点,并执行 bind mount
- 避免
%i
带连字符直接引用 .device
造成依赖失败
实施步骤
注意,如果在 Windows 环境编辑,请确保使用 UTF-8 编码,以及 Unix 行尾(LF) !
1. 查询磁盘信息
SSH 登录系统,执行如下命令查询 UUID
并做记录。
sudo blkid
2. 创建 UUID->别名 配置文件
文件路径:/etc/disk-alias.conf
1234-ABCD=movies
5678-EFGH="Backup Disk"
A1B2C3D4F50C=music
等号前的是 UUID,由第一步中获得。等号后的是 alias(挂载点)。
- UUID 不要加引号
- Alias 可以带引号(会自动去掉外层引号),但尽量还是使用不包含空格的路径。
- 另外注意避免在 Alias 中使用
/
或其他特殊路径字符
3. 编写挂载脚本
路径:/usr/local/bin/disk-bind.sh
,注意添加 chmod +x
的可执行权限。
#!/usr/bin/bash
# disk-bind.sh : Bind mount disks by UUID with alias names
# 依赖配置文件:disk-alias.conf
TARGET_BASE="/mnt/mymount" # 指定了挂载点的基本路径,按需修改
CONF_FILE="/etc/disk-alias.conf" # 配置文件的路径,根据实际情况填写
TAG="disk-bind"
DEBUG=1 # =1 打开调试日志
UUID="${ID_FS_UUID:-}"
log() {
local level="$1"; shift
local msg="$*"
/usr/bin/logger -t "$TAG" "[$level] $msg"
[ "$DEBUG" -eq 1 ] && echo "[$level] $msg"
}
# 校验 UUID
[ -n "$UUID" ] || { log ERROR "Missing ID_FS_UUID"; exit 1; }
# 读取配置,找到 UUID to 别名映射表
if [ ! -f "$CONF_FILE" ]; then
log ERROR "Config file $CONF_FILE not found"
exit 1
fi
NAME=$(
grep -E "^${UUID}=" "$CONF_FILE" 2>/dev/null \
| head -n1 \
| cut -d= -f2- \
| sed -E 's/^"(.*)"$/\1/' # 去掉头尾引号
)
if [ -z "$NAME" ]; then
log WARN "Unknown UUID=$UUID, no alias defined"
exit 0
fi
log INFO "UUID=$UUID maps to alias=$NAME"
# 直接用 UUID 软链接作为设备
DEV="/dev/disk/by-uuid/$UUID"
if [ ! -b "$DEV" ]; then
log ERROR "Device $DEV not found for UUID=$UUID"
exit 1
fi
log DEBUG "Using device $DEV"
# 创建目标目录
/usr/bin/mkdir -p "$TARGET_BASE/$NAME"
# 等待系统自动挂载完成,最多等待 120 秒
MOUNTPOINT=""
for i in $(seq 1 120); do
MOUNTPOINT=$(/usr/bin/findmnt -n -o TARGET -S "$DEV")
[ -n "$MOUNTPOINT" ] && break
log INFO "Wait for the system to mount $DEV ... $i ."
/usr/bin/sleep 1
done
# 如果没有找到挂载点,退出
if [ -z "$MOUNTPOINT" ]; then
log WARN "UUID=$UUID dev=$DEV not mounted yet"
exit 0
fi
log DEBUG "Device $DEV is mounted at $MOUNTPOINT"
# 执行 bind
if /usr/bin/mountpoint -q "$TARGET_BASE/$NAME"; then
log INFO "Alias $TARGET_BASE/$NAME already bound, skipping"
else
/usr/bin/mount --bind "$MOUNTPOINT" "$TARGET_BASE/$NAME"
log INFO "Bound $DEV -> $TARGET_BASE/$NAME (from $MOUNTPOINT)"
fi
3. 编写 systemd 模板
路径:/etc/systemd/system/disk-bind@.service
[Unit]
# %i(UUID) 作为 service 实例名 防止冲突
Description=Try to bind mount disk with UUID=%i
[Service]
Type=oneshot
RemainAfterExit=no
Environment="ID_FS_UUID=%i"
ExecStart=/usr/local/bin/disk-bind.sh
4. 编写 udev 规则
路径:/etc/udev/rules.d/90-disk-bind.rules
# 示例:文件盘
SUBSYSTEM=="block", ENV{ID_FS_UUID}=="1234-ABCD", ACTION=="add", \
TAG+="systemd", ENV{SYSTEMD_WANTS}+="disk-bind@%E{ID_FS_UUID}.service"
SUBSYSTEM=="block", ENV{ID_FS_UUID}=="5678-EFGH", ACTION=="add", \
TAG+="systemd", ENV{SYSTEMD_WANTS}+="disk-bind@%E{ID_FS_UUID}.service"
SUBSYSTEM=="block", ENV{ID_FS_UUID}=="A1B2C3D4F50C", ACTION=="add", \
TAG+="systemd", ENV{SYSTEMD_WANTS}+="disk-bind@%E{ID_FS_UUID}.service"
注意修改每个条目的 UUID,与你的分区对应。
完成配置后,可实现:
- 自动触发 systemd service
- 保证硬盘热插拔或启动时绑定别名挂载点
5. 测试与调试
重新加载 systemd 单元与 udev 规则:
sudo systemctl daemon-reload
sudo udevadm control --reload-rules
手动触发 udev add 事件:
使用如下命令触发一次事件,进行测试:
sudo udevadm trigger -s block -c add
如需测试单一分区,可以追加参数,例如 -v /sys/class/block/sda3
查看服务状态:
journalctl -t disk-bind -f
如果触发成功,这里会有相关的日志输出
验证挂载点:
mount | grep /mnt/mymount
如果不出问题,即可看到磁盘挂载点被成功绑定。此后,在系统启动后,对应的硬盘也会被自动绑定到这些固定的挂载点上。
附录
DEBUG
Debug 过程中可能会需要使用到的命令:
sudo tail -f /var/log/syslog | grep disk-bind
systemctl list-units | grep disk-bind
运行未按预期时,当然也可以手动启动服务,检查单次运行的情况:
sudo systemctl start disk-bind@ABCD-1234.service
sudo journalctl -u disk-bind@ABCD-1234.service -f
其中,ABCD-1234
是 UUID,需要替换为真实值。这样做可以测试问题是出在 udev,还是 systemd 和 脚本。
qBittorrent Docker Compose
附赠一个实用的 docker 配置,可以在稳定挂载后启动容器:
version: "2.4"
services:
disk-checker:
image: alpine
container_name: disk_checker
volumes:
- /mnt/mymount/PT:/mnt/mymount/PT
command: ["sh", "-c", "tail -f /dev/null"]
healthcheck:
test: ["CMD-SHELL", "mountpoint /mnt/mymount/PT/PT_EX_01 && mountpoint /mnt/mymount/PT/PT_EX_02"]
interval: 10s
timeout: 5s
retries: 1 # 一次失败就算 unhealthy
start_period: 0s # 启动后立刻检查
restart: on-failure:3
# qbittorrent
qbittorrent-nox:
image: qbittorrentofficial/qbittorrent-nox:5.1.0-1
container_name: nas_qbittorrent
user: "1001:1001" # 用 UID:GID 方式明确指定非 root 用户
environment:
PUID: 1001 # 用户 uid
PGID: 1001 # 组 gid
TZ: Asia/Shanghai
#UMASK_SET: "022"
QBT_WEBUI_PORT: 58080 # WEB UI端口
QBT_TORRENTING_PORT: 62233 # 种子端口
volumes:
- /vol1/1001/config:/config
- /vol1/1001/qbt_download:/downloads # 媒体库位置
- /mnt/my/PT:/PT_EXT
network_mode: "host" # 为了正常使用ipv6,建议使用host模式
# 内存限制
mem_limit: 4096m # 最大物理内存 4GB
memswap_limit: 5120m # 内存 + swap 总共 5GB(= 4G 内存 + 1G swap)
depends_on:
disk-checker:
condition: service_healthy
restart: unless-stopped