收起左侧

在 NAS 中固定硬盘挂载点的实践教程(udev + systemd + bind mount)

0
回复
32
查看
[ 复制链接 ]

1

主题

0

回帖

0

牛值

江湖小虾

本文方法为原创,使用 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

  • 优点:

    • 使用 bind mount 不会引起挂载冲突,是由内核提供的“多入口”方案。
    • 使用 RUN 调用 shell 脚本,简单直观,脚本直接处理挂载。
    • 能够直接传递 DEVNAMEID_FS_UUID 等环境变量,脚本里面可直接取用。
  • 缺点:受到 udev sandbox 限制,无法可靠挂载。

    • udev 官方文档说明:

      This can only be used for very short-running foreground tasks. Running an event process for a long period of time may block all further events for this or a dependent device.

      Note that running programs that access the network or mount/unmount filesystems is not allowed inside of udev rules, due to the default sandbox that is enforced on systemd-udevd.service.

      Starting daemons or other long-running processes is not allowed; the forked processes, detached or not, will be unconditionally killed after the event handling has finished. In order to activate long-running processes from udev rules, provide a service unit and pull it in from a udev device using the SYSTEMD_WANTS device property. See systemd.device(5) for details.

      这意味着,使用 udev 的 RUN+= 中只能执行短任务,禁止访问网络、挂载文件系统、启动守护进程

    • 实际测试中,即使是空脚本,长时间的任务或访问挂载会被 sandbox 杀死,无法成功执行指令。

方案 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

技术要点概览

  1. UUID 固定唯一

    • /dev/disk/by-uuid/ 提供了硬盘唯一标识。
    • 不随重启或热拔插变化。
  2. Alias 配置

    • 使用简单的配置文件 /etc/disk-alias.conf

      1234-ABCD=movies
      5678-EFGH="Backup Disk"
      AAAA-BBBB=music
      
    • 支持:

      • Alias 带空格或特殊字符(用引号包裹)
      • 自动去掉引号,空格直接保留
  3. Device 单元命名规则

    • systemd 的 .device 单元会把 /dev/disk/by-uuid/<UUID> 转换成 dev-disk-by\x2duuid-<UUID>.device
    • 连字符 - 必须转义为 \x2d,否则依赖解析失败
  4. 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
收藏
送赞
分享
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则