systemd-run 实战:不写 unit 文件跑临时服务、一次性任务与资源限制全流程

为什么你该学会 systemd-run

在服务器上临时跑一件耗时的活儿——比如把一个大目录打包压缩、跑一次数据库导出、下载一个几十 GB 的镜像——你是不是习惯性地在命令前面加个 nohup 和 &?

nohup tar czf backup.tar.gz /data > /tmp/backup.log 2>&1 &

这样确实能让它在 SSH 断开后继续跑,但代价是一堆管理问题:进程跑到哪了不知道,想限一下 CPU 和内存没法限,任务失败了没有通知,机器重启后它也不会自动恢复,日志还散落在一个随手指定的文件里。其实 systemd 早就提供了一个专门的工具来解决这类「临时、一次性」的任务,它叫 systemd-run。

systemd-run 的核心能力是:你不需要写任何 unit 文件,就能把一条普通命令变成一个受 systemd 托管、可查询、可限制资源、日志自动收集的临时(transient)单元。它创建的这个单元只存在于内存中,不落盘到 /etc/systemd/system/,任务结束或你手动停掉后就消失了,非常适合一次性的运维操作。

两种运行模式:--service 与 --scope

systemd-run 默认创建的是一个 service(.service),也就是让 systemd 把命令当成一个后台服务来管理。它还有一种 scope(.scope)模式,区别在于「谁负责启动进程」:

  • service 模式(默认):systemd 负责 fork 并执行你的命令,命令是 systemd 的子进程。适合「把它当后台服务跑」。
  • scope 模式(--scope):命令仍然由你当前的 shell 直接执行(也就是前台运行),systemd 只是「订阅」了这组进程,把它们纳入一个 cgroup 里做资源管控。适合你想前台跑、但仍希望被 cgroup 限制和统计的场景。

简单记忆:想让任务在后台自己跑,用默认的 service;想前台跑但要能限资源、能统计,加 --scope。

基础用法:临时任务三步走

第一步:后台启一个一次性任务

systemd-run --unit=backup-job tar czf /data/backup.tar.gz /data

加上 --unit=backup-job 给它起个名字,方便后续用 systemctl 管理。不加名字 systemd 会自动生成一个类似 run-r4f8a9c.service 的随机名。这条命令默认就是「后台执行、立即返回」,然后你就可以用熟悉的 systemctl 查看它:

systemctl status backup-job.service

第二步:用 journalctl 看日志

这是 systemd-run 相比 nohup 最舒服的地方——命令的 stdout/stderr 会自动进 journal,不需要你手动重定向到某个乱七八糟的日志文件:

journalctl -u backup-job.service -f

-f 实时跟随。任务结束后,日志依然留在 journal 里,可以随时回看,不用担心像 nohup 那样「日志文件放哪了忘了」。

第三步:管理它的生命周期

systemctl stop backup-job.service      # 停掉
systemctl kill backup-job.service      # 强制杀
systemctl reset-failed backup-job.service  # 清理失败状态

任务正常结束后,单元会自动进入 inactive(或 failed)状态,但名字会占用,再次用同名启动会报错,需要先 reset-failed。

真正的杀手锏:临时给任务限资源

临时任务最容易把服务器拖垮——比如一个 tar 或 mysqldump 把磁盘 IO 和 CPU 吃满,导致线上网站跟着卡。用 systemd-run 可以在不改任何配置文件的前提下,直接给这一条命令套上资源限制:

systemd-run --unit=big-tar \
  -p CPUQuota=50% \
  -p MemoryMax=2G \
  -p IOWeight=20 \
  tar czf /data/backup.tar.gz /data

逐条解释这几个属性:

  • CPUQuota=50%:限制 CPU 使用率不超过单核的 50%(注意单位是相对一个核的百分比,200% 表示两个核)。
  • MemoryMax=2G:内存硬上限,超过会被 OOM Killer 干掉这个单元(而不是拖垮整机)。
  • IOWeight=20:IO 权重,范围 1–10000,默认 100。数值越低,磁盘 IO 优先级越低,给线上服务让路。

所有 systemd 的 cgroup 资源控制属性都能用 -p 属性名=值 的方式临时套上,不需要提前写 unit。

定时任务的一次性写法还不够——配合 OnCalendar 也可以

虽然 systemd-run 主要面向「立即执行」,但它也能创建带定时器的临时单元。不过更常见的组合是:先用 systemd-run 验证一条命令的资源限制是否合理,确认没问题后再正式写 .timer 文件固化下来。这种「先临时试、再固化」的工作流,比直接改 /etc/systemd/system/ 里的文件反复 daemon-reload 要轻快得多。

一个容易被坑的地方:--scope 的语法差异

--scope 模式下,命令是在前台执行的,所以它会阻塞当前终端直到结束。--scope 通常用来「抱住」一个已经在跑或者想前台跑的命令,并把它放进可控的 cgroup:

systemd-run --scope -p MemoryMax=1G -- ./heavy_task.sh

注意 -- 之后的才是被执行的命令,前面的都是 systemd-run 的选项——当你的命令本身也带 -p 之类的参数时,这个 -- 分隔符就是必须的,否则 systemd 会误以为那也是给它的属性。

和 nohup、cron、systemd service 到底怎么选

很多人搞不清这几个工具的分工,这里给一张清晰的对照:

  • nohup command &:最简单,只是让命令忽略 SIGHUP 从而在 SSH 断开后不死。没有资源限制、没有日志收集、没有状态查询、重启不恢复。适合「随手跑个几分钟就结束的小任务」。
  • systemd-run:一次性、临时任务的正经做法。有 cgroup 资源控制、journal 日志、systemctl 状态管理,但不自动持久化,机器重启后不会恢复,任务本身也不跨重启存活。
  • cron / systemd timer:周期性的定时任务(每天几点、每小时)。
  • systemd service(写 unit 文件):需要长期常驻、开机自启、崩溃自动拉起的服务。

一句话总结:「跑一次、想管得住、还不想写配置」——就是 systemd-run 的主场。

排查:任务没跑起来怎么办

  1. 报 Failed to start transient service unit: Unit ... already exists:同名单元还在(哪怕已经结束)。systemctl reset-failed <name> 清掉再试,或者换个 --unit 名字。
  2. 看不到输出:默认任务在后台跑,输出去 journal 了。用 journalctl -u <name> 看;如果你希望输出直接回显到当前终端,加 --pty 让它分配一个伪终端(适合交互式命令)。
  3. 资源限制不生效:确认是不是在容器里跑。容器内的 systemd 常常没挂载完整的 cgroup 层级,-p 属性可能被忽略或报错;这在裸机/云主机上不会有问题。
  4. 任务被 OOM 杀掉:这正是 MemoryMax 在起作用——它宁可杀掉这个临时单元,也不让整机内存耗尽。看 journalctl -u <name> 会看到 oom-kill 相关记录,此时调小任务的批处理量或调大 MemoryMax。

常见问答:把 systemd-run 用对

问:任务跑完了,我想看它花了多久、退出码是多少,去哪查?
用 systemctl status <name>.service 就能看到单元的最终状态,包括 Active 行的结果(是 exited 还是 failed)、主进程的退出码、以及运行了多长时间。更细的启动和结束时间戳则在 journalctl -u <name> 里,systemd 会自动记录每个单元的启动、退出事件。

问:机器重启后这个临时任务还能自动继续吗?
不能,这是刻意的设计。transient 单元只存在于内存里,重启即消失,它本身也不跨重启存活。如果你的任务「如果没跑完,重启后要接着跑」,那就不该用 systemd-run,而应该写一个正式的 .service 文件放进 /etc/systemd/system/,并配置 Restart=on-failure。systemd-run 解决的是「这次临时跑一下」的需求,不要拿它当持久化服务用。

问:能不能把输出同时写进 journal 又写进一个文件?
可以。因为 journal 本来就会捕获 stdout/stderr,你只需要在命令里再用 tee 落一份文件即可,两者互不冲突:systemd-run --unit=x -- sh -c 'cmd | tee /tmp/x.log'。注意要用 sh -c 把带管道的命令包起来,否则管道符号会被外层 shell 提前解析掉。

问:CPUQuota 写 50% 为什么感觉没怎么限住多核任务?
因为 CPUQuota 的百分比是相对单个 CPU 核的。一台 4 核机器上,CPUQuota=100% 只能吃满 1 个核,另一半的核它照样能用满——要限制到「整机一半」就得写 200%(4 核 × 50%)。另外 CPUQuota 和 CPUShares(现在叫 CPUWeight)是两回事:前者是硬上限、封顶,后者是软权重、只在争抢时起作用。想「最多只用这么多」用 CPUQuota,想「大家都忙时我让一让、但没人抢时我能用满」用 CPUWeight。

问:任务卡住了、stop 停不掉怎么办?
先 systemctl stop,它默认发 SIGTERM 给单元里的进程;如果进程不响应,隔一会儿 systemd 会升级到 SIGKILL。仍然停不掉、或者残留了 cgroup,可以在 unit 里加 --property=KillMode=control-group(默认值)确保整个 cgroup 被清理,再用 systemctl reset-failed 清状态。极端情况下 systemctl kill -s SIGKILL <name> 直接硬杀。

把 systemd-run 加进你的工具箱,下次再遇到「临时跑个大任务又怕影响线上」,就不用纠结要不要写一个 unit 文件了——一条命令,带资源限制,日志还能查,干净利落。

Last modification:October 9th, 2026 at 09:24 pm

Leave a Comment