引言
cron 用了三十年,但它的短板在今天越来越明显:没有依赖、没有日志、没有资源限制、错过的任务不会补跑、精度只到分钟。systemd 用 Timer 单元取代它——定时器与普通服务共用同一套依赖、cgroup 资源控制与 journald 日志体系。另一半是 Socket 激活:把「监听端口」这件事从服务进程里剥离出来交给 systemd,服务本体只在第一个连接到来时才启动,空闲时完全不占内存,还能实现零丢弃的重启。
本文把这两块讲透:先拆 Timer 单元的配对结构与 OnCalendar 表达式语法,再区分 monotonic 与 realtime 两类定时器、精度与补跑语义,接着用 systemd-run 演示临时定时任务;后半部分深入 Socket 激活的 FD 传递机制、Accept=yes 与 inetd 风格、以及用 systemd-socket-proxyd 做零停机重载。
前置:Unit 文件与 systemctl 基础见 Linux systemd 服务管理 ,其中 Timer/Socket 只做了入门介绍,本文是其进阶续篇;日志排错见 journald 日志管理 。
1. Timer 单元:配对、结构与启停
Timer 的核心约定是一个 .timer 配一个同名 .service:定时器到点后,systemd 去启动那个服务单元。
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup trigger
Requires=backup.service
[Timer]
Unit=backup.service # 默认就是同名 .service,可省略
OnCalendar=*-*-* 02:30:00
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Run backup script
[Service]
Type=oneshot # 一次性任务,执行完即退
ExecStart=/usr/local/bin/backup.sh
# 启用的是 timer,不是 service
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers --all # 列出所有定时器及下次触发时间
| 命令 | 作用 |
|---|---|
systemctl enable --now x.timer | 开机自启 + 立即纳入调度 |
systemctl list-timers | 看下次触发、上次触发、剩余时间 |
systemctl start x.timer | 手动触发一次(不是启动服务) |
systemctl status x.timer | 定时器状态(不等于服务状态) |
journalctl -u x.service | 看定时任务实际执行日志 |
记忆:Timer 管「什么时候触发」,Service 管「触发后干什么」。手动跑一次定时任务用
systemctl start backup.service,而不是 start timer。
2. OnCalendar 表达式详解
OnCalendar 的语法形如 星期 年-月-日 时:分:秒,各字段可省略或用 * 通配,还支持 .. 区间与 , 列表。
[Timer]
OnCalendar=Mon..Fri 09:00 # 工作日 9 点
OnCalendar=*-*-* 00/6:00:00 # 每 6 小时(00/6 表示从 0 起每 6)
OnCalendar=*-*-01 03:00:00 # 每月 1 号 3 点
OnCalendar=Sat,Sun 12:00 # 周六日中午
| 表达式 | 含义 |
|---|---|
hourly / daily / weekly / monthly / yearly | 预定义简写 |
*-*-* *:*:00 | 每分钟 |
*-*-* 02:30:00 | 每天 02:30 |
Mon..Fri *-*-* 09:00 | 工作日 09:00 |
*-*-* 00/4:00:00 | 每 4 小时 |
*-*-01..07 00:00:00 | 每月 1~7 号 |
验证表达式(强烈建议写完先测):
# 用 systemd-analyze 校验并显示接下来的触发时间
systemd-analyze calendar "Mon..Fri *-*-* 09:00"
# 输出包含 Next elapse / Iteration 等,一眼看出是否写错
systemd-analyze calendar --iterations=5 "*-*-* 00/6:00:00"
铁律:写完
OnCalendar一律用systemd-analyze calendar验证。表达式写错时 systemd 只在启动时报错,肉眼极难发现00/6与*/6的差异。
3. Monotonic 与 Realtime:两类定时器
Timer 分为两族,区别在于「相对于什么计时」:
| 类型 | 基准 | 指令 | 特性 |
|---|---|---|---|
| Realtime | 墙上时钟 | OnCalendar | 受时区、NTP 调整影响,适合「每天几点」 |
| Monotonic | 系统启动/上次触发 | OnBootSec/OnUnitActiveSec/OnStartupSec | 不受时钟跳变影响,适合「每隔多久」 |
# 单调定时器:开机 15 分钟后首次,之后每 1 小时
[Timer]
OnBootSec=15min
OnUnitActiveSec=1h
AccuracySec=1min
# 墙上时钟定时器:每天凌晨
[Timer]
OnCalendar=daily
Persistent=true
两者可以混用(同一 Timer 里同时给多个触发条件,任一满足即触发):
[Timer]
OnCalendar=*-*-* 03:00:00 # 每天 3 点
OnBootSec=30min # 或开机 30 分钟后
记忆:「每天几点跑」用 OnCalendar,「每隔多久跑」用 OnUnitActiveSec。跨时区/夏令时的机器,单调定时器更稳;需要对齐业务时间窗的,用 realtime。
4. 精度、抖动与 Persistent 补跑
systemd 默认给定时器加随机精度窗口(AccuracySec,默认 1 分钟),目的是把大量定时任务错开、避免同一时刻集中唤醒 CPU。
[Timer]
AccuracySec=1s # 收紧到 1 秒(更准,但更耗电/更集中)
RandomizedDelaySec=10m # 额外随机延迟,防「惊群」
Persistent=true # 关机错过的触发,开机后补跑一次
| 参数 | 默认 | 作用 |
|---|---|---|
AccuracySec | 1min | 允许的触发时间误差窗口 |
RandomizedDelaySec | 0 | 在窗口内随机延迟,打散并发 |
Persistent=true | false | 记录上次触发时间,错过则开机补跑 |
RemainAfterElapse | true | 触发后是否保留定时器单元 |
Persistent=true 依赖 /var/lib/systemd/timers/ 下的时间戳文件,只对 OnCalendar 有意义(单调定时器重启后本就重算)。
# 查看持久化时间戳
ls -l /var/lib/systemd/timers/
# 手动重置「上次触发」记录
sudo touch /var/lib/systemd/timers/stamp-backup.timer
心法:精度不是越高越好。备份、清理这类任务给足
RandomizedDelaySec反而更健康——否则几十个定时器在 00:00 齐发,会把 IO 和 CPU 打成尖峰。
5. systemd-run 与瞬时定时器
临时任务不必写 unit 文件,用 systemd-run 直接投递:
# 立刻以瞬时服务运行(走 cgroup,可用 systemctl status 查看)
systemd-run --unit=myjob /usr/local/bin/task.sh
# 定时执行:10 分钟后跑一次
systemd-run --on-active=10min --unit=delayed /usr/local/bin/task.sh
# 每天 3 点跑,持久化
systemd-run --on-calendar="*-*-* 03:00:00" --timer-property=Persistent=true \
--unit=dailyjob /usr/local/bin/task.sh
# 带资源限制的瞬时任务
systemd-run --scope -p MemoryMax=512M -p CPUQuota=50% /usr/local/bin/heavy.sh
| 选项 | 等价于 |
|---|---|
--on-active=5min | OnActiveSec=5min |
--on-boot=10min | OnBootSec=10min |
--on-calendar=... | OnCalendar=... |
--timer-property= | 直接设 Timer 单元属性 |
-p Key=Value | 设 Service 单元属性 |
# 查看与管理瞬时单元
systemctl list-timers --all | grep run-
systemctl status run-dailyjob.timer
systemctl stop run-dailyjob.timer # 取消瞬时定时任务
心法:
systemd-run是「一次性的 systemd 命令行」——它把脚本放进 cgroup 里跑,天然获得资源限制、日志归集与依赖管理,比nohup ... &干净得多。
6. Socket 激活原理:从 inetd 到 FD 传递
Socket 激活的经典思想来自 inetd:监听端口的职责从服务进程剥离,交给 systemd。systemd 持有监听套接字,第一个连接到来时才拉起服务,并把已就绪的监听 FD 直接传给服务——服务无需自己 bind/listen,连「端口被占用」的窗口都消失了。
无 socket 激活: 服务进程 bind/listen/accept(重启期间端口不可用)
socket 激活: systemd 常驻 listen → 连接到达 → 拉起服务 + 传 FD → 服务直接 accept
(重启服务时端口始终由 systemd 持有,连接不丢)
服务通过约定获取 FD:systemd 把监听 FD 放在从 3 开始的连续编号上,并设置环境变量 LISTEN_FDS(数量)与 LISTEN_PID(进程号)。应用用 sd_listen_fds() 取回:
#include <systemd/sd-daemon.h>
int n = sd_listen_fds(0); /* 返回传给本进程的 FD 数量 */
for (int i = 0; i < n; i++) {
int fd = SD_LISTEN_FDS_START + i; /* 即 3 + i */
/* 这个 fd 已经 bind+listen 好了,直接 accept */
}
非 C 语言也能用:Python 的 systemd.daemon.listen_fds()、Go 的 github.com/coreos/go-systemd/activation、以及大量框架的 socket 激活支持。
记忆:socket 激活的本质是「FD 传递」——systemd 替你完成
socket()/bind()/listen(),再把 FD 交到你手里。你只是从「创建监听套接字」变成「继承一个监听套接字」。
7. Socket 单元详解与 Accept=yes
# /etc/systemd/system/myapp.socket
[Unit]
Description=My app socket
[Socket]
ListenStream=8080 # TCP 端口
# ListenStream=/run/myapp.sock # 或 Unix 域套接字
Accept=no # 默认:服务自己 accept
Backlog=4096
[Install]
WantedBy=sockets.target
# /etc/systemd/system/myapp.service
[Unit]
Requires=myapp.socket
After=myapp.socket
[Service]
ExecStart=/usr/bin/myapp --listen-fd
| 指令 | 作用 |
|---|---|
ListenStream= | TCP/Unix 流套接字 |
ListenDatagram= | UDP 数据报 |
ListenFIFO= | FIFO 命名管道 |
Accept=yes | 每个连接拉起一个服务实例(inetd 风格) |
MaxConnections= | 配 Accept=yes 时限制并发实例数 |
SocketMode= | Unix 套接字权限位 |
Accept=yes 与 Accept=no 的关键区别:
Accept=no (默认)→ 传「监听套接字」给服务,服务自己 accept 循环
一个服务实例处理所有连接(常驻、高效)
Accept=yes → systemd 自己 accept,把「已连接套接字」传给每个新实例
每连接一个进程(简单、隔离好、开销大)
此时服务用 StandardInput=socket 读写连接
# Accept=yes 时服务模板写法
[Service]
StandardInput=socket
ExecStart=/usr/bin/handler
心法:
Accept=no用于常规长驻服务(Web、数据库);Accept=yes用于「每连接一个短进程」的轻量处理(如邮件网关、简单协议处理器),隔离性强但进程开销大。
8. 实战:零停机重载与 systemd-socket-proxyd
Socket 激活最实用的价值是服务重启期间连接不丢:systemd 持有监听端口,内核的 accept 队列继续排队,服务重启完再取走。要让「新老进程无缝交接」,用 systemd-socket-proxyd:
# 1) 外层 socket 单元
# /etc/systemd/system/proxy.socket
[Socket]
ListenStream=80
[Install]
WantedBy=sockets.target
# 2) 代理服务:把连接转发到真正的后端端口
# /etc/systemd/system/proxy.service
[Service]
ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time=5min 127.0.0.1:8080
sudo systemctl enable --now proxy.socket
# 后端 myapp 可以随时重启,外部 80 端口连接由 proxy 缓存,不丢
sudo systemctl restart myapp
这样做的效果:外部客户端始终连着 80 端口的 proxy,后端重启时新连接被 proxy 暂存,实现「用户无感」的发布。
心法:socket 激活让「重启」从「连接中断」变成「连接排队」。对于必须 7×24 在线的服务,这是一条比负载均衡器更轻量的零停机路径。
9. 排错与观测
# 定时器为什么不触发?
systemctl list-timers --all
systemd-analyze calendar "..." # 表达式是否合法
systemctl status myapp.timer # timer 自身状态
journalctl -u myapp.service --since today
# socket 激活排错
systemctl status myapp.socket
systemctl list-sockets # 所有 socket 单元及监听地址
ss -tlnp | grep 8080 # 确认是 systemd 在 listen
journalctl -u myapp.socket -u myapp.service
# 看服务是否真的拿到了 FD
systemctl show myapp.service -p Environment
cat /proc/<pid>/environ | tr '\0' '\n' | grep LISTEN
| 现象 | 可能原因 | 对策 |
|---|---|---|
| Timer 到点不跑 | OnCalendar 写错 / 未 enable | systemd-analyze calendar 校验 |
| 服务启动但没数据 | 未读取 sd_listen_fds | 检查应用是否支持 socket 激活 |
| 端口仍被旧进程占用 | 服务自己 bind 了端口 | 移除应用内的 bind,改用继承 FD |
| Accept=yes 下连接被拒 | MaxConnections 打满 | 调大或改用 Accept=no |
| 补跑没生效 | 忘了 Persistent=true | 加上并确认时间戳文件存在 |
铁律:socket 激活排错的第一步是确认「谁在 listen」。
ss -tlnp显示的应是 systemd(pid 1)而非你的应用——如果是应用在 listen,说明它没走激活路径。
10. 速查表
| 需求 | 做法 |
|---|---|
| 每天定时 | OnCalendar=daily |
| 每 6 小时 | OnCalendar=*-*-* 00/6:00:00 |
| 开机后多久跑 | OnBootSec=15min |
| 每隔多久跑 | OnUnitActiveSec=1h |
| 错过补跑 | Persistent=true |
| 校验表达式 | systemd-analyze calendar "..." |
| 临时定时任务 | systemd-run --on-calendar=... |
| 查看定时器 | systemctl list-timers --all |
| 启用 socket | systemctl enable --now x.socket |
| 每连接一进程 | Accept=yes |
| 获取继承 FD | sd_listen_fds() / LISTEN_FDS |
| 零停机重载 | systemd-socket-proxyd |
一句话记忆:Timer 用 OnCalendar 表达「每天几点」、OnUnitActiveSec 表达「每隔多久」,Persistent 补跑、AccuracySec/RandomizedDelaySec 控精度;Socket 激活靠「systemd 持有监听 FD + 传给服务」实现按需启动与零停机重载,Accept=no 服务自己 accept、Accept=yes 每连接一进程。
延伸阅读
- Linux systemd 服务管理 — Unit 类型、依赖与资源控制基础
- Linux 定时任务:cron/at/anacron 与 systemd timer — 与传统定时方案对比
- journald 日志管理 — 定时任务与服务的日志排错
- SSH 服务与隧道 — socket 激活在 sshd 中的应用
- Docker 专题 — 容器与 systemd 的编排关系
- DevOps 专题 — CI/CD 中的定时任务与发布
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。