systemd 定时器与 Socket 激活:OnCalendar 语法与按需拉起

深入 systemd 定时器与 Socket 激活:Timer 单元配对与启停、OnCalendar 表达式完整语法、Monotonic 与 Realtime 两类定时器、精度抖动与 Persistent 补跑、systemd-run 瞬时定时器,以及 Socket 单元类型、Accept=yes、FD 传递与 sd_listen_fds 实战。

引言

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       # 关机错过的触发,开机后补跑一次
参数默认作用
AccuracySec1min允许的触发时间误差窗口
RandomizedDelaySec0在窗口内随机延迟,打散并发
Persistent=truefalse记录上次触发时间,错过则开机补跑
RemainAfterElapsetrue触发后是否保留定时器单元

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=5minOnActiveSec=5min
--on-boot=10minOnBootSec=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 写错 / 未 enablesystemd-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
启用 socketsystemctl enable --now x.socket
每连接一进程Accept=yes
获取继承 FDsd_listen_fds() / LISTEN_FDS
零停机重载systemd-socket-proxyd

一句话记忆:Timer 用 OnCalendar 表达「每天几点」、OnUnitActiveSec 表达「每隔多久」,Persistent 补跑、AccuracySec/RandomizedDelaySec 控精度;Socket 激活靠「systemd 持有监听 FD + 传给服务」实现按需启动与零停机重载,Accept=no 服务自己 accept、Accept=yes 每连接一进程。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「linux」更多文章

  1. PAM 认证与权限提升:模块栈、密码策略与 sudo 深入
  2. NFS 与 CIFS 网络文件系统:服务端配置、挂载调优与安全
  3. LVM 与 RAID 存储管理:卷管理、快照与软阵列运维