Podman Quadlet: Rclone Mount 中涉及 systemd 依赖问题实践
1. First | 序
众所周知 Podman 是 Fork-Exec 模型 (每个容器都是 Systemd 的子进程(在使用Quadlet的情况下)),所以它与 Systemd 是天作之合。 而 Docker 是 Client-Server (Daemon) 模型(容器是 Docker 守护进程的子进程),Systemd 只能管理 dockerd 这个守护进程,默认无法感知和控制单个容器的死活
现如今我们有以下 .service 环境
Rclone MountNeed-RcloneMount1Need-RcloneMount2Need-RcloneMount3
Rclone Mount 负责挂载云端硬盘到本地Need-RcloneMount1/2/3 依赖于 Rclone Mount 挂载的磁盘读写文件Need-RcloneMount1/2/3 之间没有依赖关系且可以并行启动1.1. Objectives | 目标
- Rclone Mount 容器服务启动,Need-RcloneMount1/2/3 容器服务启动
- Rclone Mount 容器服务停止,Need-RcloneMount1/2/3 容器服务停止
- Rclone Mount 容器服务重启,Need-RcloneMount1/2/3 容器服务重启
2. Configuration | 配置
依据上文所示这里使用 .container 完成
跳过调试部分这里直接写成品
2.1. Rclone Mount .container
[Unit]
Description=Rclone is a command-line program to manage files on cloud storage | Rclone-Mount
After=network-online.target
# 需求1核心:启动 Rclone 时,尝试拉起这些服务
# 注意:确保这里填写的名称与实际文件名完全一致
Wants=network-online.target Need-RcloneMount1.service Need-RcloneMount2.service Need-RcloneMount3.service
# 显式声明顺序:Rclone 在这些服务之前启动
Before=Need-RcloneMount1.service Need-RcloneMount2.service Need-RcloneMount3.service
[Container]
AutoUpdate=registry
ContainerName=Rclone-Mount
AddCapability=SYS_ADMIN
PodmanArgs=--security-opt apparmor=unconfined
AddDevice=/dev/fuse
Exec=mount xxxxxxx:/xxxxxx /data/mount \
......
--log-file /config/rclone/rclone.log \
--log-level INFO
Image=docker.io/rclone/rclone
Volume=Rclone-config:/config/rclone
Volume=Rclone-data:/data:shared
[Service]
Restart=always
# 延长等待时间至 300s
TimeoutStartSec=300
# 确保挂载点可用
ExecStartPost=/bin/bash -c "while ! mountpoint -q /Rclone-Mount; do sleep 1; done"
# 确保(服务正常/异常退出亦或者未成功启用时)卸载挂载点
ExecStopPost=/usr/bin/fusermount3 -u /Rclone-Mount
[Install]
WantedBy=default.target
Wants= & Before=在 Systemd 中, 依赖(Dependency) 和 顺序(Ordering) 是两回事。
Wants= 和 Before= 的联合使用,构建了一个 “主动拉起 + 严格排队” 的逻辑。
Systemd 会在内部生成一个任务队列,流程如下:
-
触发 (Wants):
Systemd 收到启动Rclone的指令。因为它配置了Wants=Need-Rclone...,Systemd 立即把Need-Rclone1,2,3也加入到“待启动列表”中。
此时,4 个服务都准备启动。 -
排序 (Before):
Systemd 检查这 4 个服务的启动顺序。它发现Rclone写了Before=Need-Rclone...(或者反过来Need-Rclone写了After=Rclone)。
Systemd 判定:Rclone 必须先走,其他程序红灯等待。 -
执行 Rclone:
Rclone 开始启动。- 关键点:
ExecStartPost脚本。Systemd 会一直卡在这里,直到脚本返回成功(也就是挂载点出现)。
- 关键点:
-
放行 (Release):
一旦 Rclone 状态变为Active (Running),Systemd 这里的绿灯亮起。 -
执行依赖:
Need-Rclone1,2,3开始并行启动。
2.1.1. Ex | 拓展
Upholds=
• 官方定义: 配置维持依赖,确保持续性启动。
• 语法: 接受一个或多个以空格分隔的单元名称。
• 行为: 只要本单元处于运行状态,systemd 就会持续监控 Upholds= 列出的单元。如果发现这些单元进入了非激活或失败状态,systemd 会立即尝试重新启动它们。这提供了一种替代服务单元自身 Restart= 设置的机制。
• 互作用: Upholds= 提供了一种跨单元的“保持存活”逻辑,与服务单元的 Restart= 配置相比,它将重启决策的控制权交给了另一个单元。
• 常见误区: 这是一个相对较新的指令,可能被忽视。它与 Wants= 的区别在于 Wants= 是一次性触发,而 Upholds= 是持续性监控和触发。
• 版本: Added in version 249.
结合本文环境分析此参数:
| 特性 | Restart=always (在子服务里) | Upholds= (在父服务里) |
|---|---|---|
| 触发条件 | 进程崩溃 (非正常退出) | 服务处于非 Active 状态 |
| 手动 Stop | 尊重你的意愿 (不会重启) | 无视你的意愿 (强行重启) |
| 适用场景 | 绝大多数常规服务 | 关键的辅助进程,必须时刻与主进程共存 |
| 控制权 | 用户掌握 | 父服务掌握 |
2.2. Need-RcloneMount1 .container
[Unit]
Description=Need-RcloneMount1
Wants=network-online.target
# 如果 BindsTo= 列出的任何一个单元进入非激活状态(例如被停止、崩溃或设备被拔出),本单元也会被自动停止。
# 当与 After= 结合使用时,行为会更强:它确保了只要本单元处于激活状态,其绑定的单元也必须处于激活状态。如果绑定的单元因条件检查未通过而被跳过,本单元也会被停止
After=network-online.target Rclone-Mount.service
BindsTo=Rclone-Mount.service
# 需求2 & 3 核心:绑定生命周期。当 Rclone 停止或重启时,该动作会传播给此服务
PartOf=Rclone-Mount.service
Need-RcloneMount2/3
.container内容完全等于Need-RcloneMount1不过多赘述
After= & BindsTo= & PartOf=-
After=Rclone-Mount.service🚦- 含义:顺序控制。
- 作用:无论发生什么情况(启动或重启),当前服务必须等到
Rclone-Mount完全启动(Active)之后才能开始启动。 - 场景:防止容器在挂载点还没准备好时就抢跑。
-
PartOf=Rclone-Mount.service🏁- 含义:动作传播。
- 作用:主要针对用户主动的操作。当你对 Rclone 执行
systemctl stop或systemctl restart时,Systemd 会把这个命令自动传导给当前服务。 - 场景:你重启 Rclone 来更新配置,依赖的容器也会自动跟着重启。
-
BindsTo=Rclone-Mount.service🎎- 含义:状态绑定。
- 作用:如果 Rclone 服务进入
active以外的任何状态(无论是被手动停止,还是意外崩溃 (Crash),或者被 OOM Kill),当前服务会立即、强制停止。 - 区别:它比
Requires=强硬得多。Requires只管“启动时”依赖在不在;而BindsTo则是“运行时”每时每刻都在监控依赖的状态。
3. 验证
3.1. systemctl
反向查看 (谁依赖 Rclone-Mount.service 这个服务?)
➜ ~ systemctl list-dependencies Rclone-Mount.service --reverse
Rclone-Mount.service
● ├─Need-RcloneMount1.service
● ├─Need-RcloneMount2.service
● ├─Need-RcloneMount3.service
● └─multi-user.target
➜ ~
验证重启传播 (PartOf 检查)
➜ ~ systemctl show Rclone-Mount.service -p ConsistsOf
ConsistsOf=Need-RcloneMount1.service Need-RcloneMount2.service Need-RcloneMount3.service
➜ ~
3.2. bpftrace execsnoop
18:57:24.239629 26812 24269 sudo systemctl Rclone-Mount.service
18:57:24.245975 26813 26812 root �E�H�E�HǀP
18:57:24.250041 26815 26814 systemctl restart
......
18:57:24.282244 26817 1 /usr/bin/podman rm -f �+�M@V
18:57:24.282305 26819 1 18:57:24.282307 26818 1 /usr/bin/podman rm -f �
ΊU
/usr/bin/podman rm -f �{}��U
18:57:24.414690 26856 26846 /usr/bin/runc kill 15
18:57:24.420406 26864 26848 /usr/bin/runc kill 15
......
18:57:35.507087 27327 27325 /usr/bin/runc kill 15
......
18:57:35.513300 27337 24982 fusermount3 -u
......
18:57:35.867267 27387 1 /usr/bin/podman run Rclone-Mount --rm --device /xxx/xxx/xxx/xxx:/config/rclone apparmor=unconfined --umask --drive-pacer-burst --vfs-read-chunk-streams --vfs-cache-max-size --log-file
......
18:57:36.046080 27458 27446 rclone mount /data/mount --umask --dir-perms 200 --buffer-size 2G 24h /config/rclone/rclone.log
......
18:57:36.067743 27485 27471 mountpoint -q
......
18:57:36.070751 27488 27471 sleep 1 32
......
18:57:36.739425 27500 27473 fusermount3 -o --
......
18:57:37.072488 27502 27471 mountpoint -q
......
18:57:37.107253 27504 1 /usr/bin/podman run Need-RcloneMount1 --rm --sdnotify=conmon xxxxxx-config:/config:Z io.containers.autoupdate=registry �k���U �k���U
18:57:37.107320 27509 1 /usr/bin/podman run Need-RcloneMount2 --rm --sdnotify=conmon /xxx/xxxxx/xxxxxx:/media 127.0.0.1:57382:57382/udp --env �[8*yU
18:57:37.107343 27508 1 /usr/bin/podman run Need-RcloneMount3 --rm --sdnotify=conmon /xxxxx/xxx/xxxxx:/config io.containers.autoupdate=registry --env --env
3.2.1. 验证 PartOf=
| 时间戳 | 进程/动作 | 原始日志片段 | 状态 |
|---|---|---|---|
18:57:24.250 |
Systemd | systemctl restart |
🚀 收到指令 |
18:57:24.282 |
Podman | /usr/bin/podman rm -f |
🛑 依赖容器被强杀 |
18:57:35.507 |
RunC | /usr/bin/runc kill 15 |
🛑 主服务 Rclone 被杀 |
3.2.2. 验证 Rclone 重建
| 时间戳 | 进程/动作 | 原始日志片段 | 状态 |
|---|---|---|---|
18:57:35.513 |
FUSE | fusermount3 -u ... |
🧹 旧挂载点卸载 |
18:57:35.867 |
Podman | /usr/bin/podman run Rclone... |
🆕 新容器创建 |
18:57:36.046 |
Rclone | rclone mount /data/mount ... |
🔧 挂载命令执行 |
3.2.3. 验证 ExecStartPost=
| 时间戳 | 进程/动作 | 原始日志片段 | 状态 |
|---|---|---|---|
18:57:36.067 |
Check 1 | mountpoint -q |
❌ 检测失败 (无挂载) |
18:57:36.070 |
Sleep | sleep 1 |
⏳ 阻塞后续流程 |
18:57:36.739 |
Mount | fusermount3 -o |
🔗 挂载动作发生 |
18:57:37.072 |
Check 2 | mountpoint -q |
✅ 检测成功 |
3.2.4. 验证 After= & Wants=
| 时间戳 | 进程/动作 | 原始日志片段 | 状态 |
|---|---|---|---|
18:57:37.107 |
Podman | podman run Need-RcloneMount1... |
▶️ 启动 Need-RcloneMount1 |
18:57:37.107 |
Podman | podman run Need-RcloneMount2... |
▶️ 启动 Need-RcloneMount2 |
18:57:37.107 |
Podman | podman run Need-RcloneMount3... |
▶️ 启动 Need-RcloneMount3 |
