systemd 服务化与开机自启
线上进程必须解决三件事:崩溃了自动拉起、机器重启后自动回来、日志有地方统一查看。这三件事 systemd 天生就做得到,而且几乎所有发行版都自带。能用 systemd 就别装 supervisor——少一个 Python 依赖、少一套配置文件、与 journalctl 天然打通,是最省心的选择。
为什么优先用 systemd
| 维度 | systemd | Supervisor |
|---|---|---|
| 日志 | journalctl -u app -f,自带轮转与时间过滤 | 落文件,需自己配轮转 |
| 启动依赖 | After/Wants 声明式排序 | 靠 [group] 与 priority,表达力弱 |
| 资源限制 | LimitNOFILE、MemoryMax、CPUQuota 直接写 | 需额外脚本配合 |
| 定时任务 | timer 替代 cron,带补跑与随机延迟 | 不支持 |
unit 文件放在哪
| 路径 | 用途 | 注意 |
|---|---|---|
/etc/systemd/system/*.service | 自己写的服务,首选这里 | 优先级最高,daemon-reload 后生效 |
/usr/lib/systemd/system/*.service | 发行版包安装的服务 | 不要手改,升级包会覆盖 |
~/.config/systemd/user/*.service | 用户级服务,无需 root | 需 systemctl --user 且开启 enable-linger |
unit 文件三段
| 段 | 作用 | 关键项 |
|---|---|---|
[Unit] | 元信息与依赖关系 | Description、After、Wants、StartLimitBurst |
[Service] | 怎么起、怎么停、怎么重启 | Type、User、ExecStart、Restart、EnvironmentFile |
[Install] | 开机自启 | WantedBy=multi-user.target |
一个完整的 service 示例
# /etc/systemd/system/myapi.service
[Unit]
Description=MyAPI service
After=network-online.target mysql.service
# 新版 systemd 把启动频率限制放在 [Unit],老版本写在 [Service] 里
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/apps/myapi/current
EnvironmentFile=/home/deploy/apps/myapi/shared/.env
# 启动前跑一次数据迁移,失败则不会启动新版本
ExecStartPre=/home/deploy/apps/myapi/current/app --migrate
ExecStart=/home/deploy/apps/myapi/current/app --config shared/config.yaml
Restart=always
RestartSec=3
TimeoutStopSec=30
KillSignal=SIGTERM
LimitNOFILE=65535
StandardOutput=journal
[Install]
WantedBy=multi-user.target
关键项逐条解释
| 配置项 | 含义 | 生产要点 |
|---|---|---|
ExecStart | 启动命令,必须绝对路径 | 不支持管道与重定向,要用就写 sh -c '...' |
ExecStartPre | 启动前执行的命令 | 用于迁移与配置校验,失败则不启动主进程 |
WorkingDirectory | 工作目录 | 相对路径的配置与日志都以它为准 |
User / Group | 运行身份 | 绝不用 root,配好后确认文件属主正确 |
EnvironmentFile | 从文件读环境变量 | 权限 600,只写 KEY=value,不要带 export |
Restart=always | 任何退出都重启 | on-failure 只在非 0 退出时重启 |
RestartSec | 重启前等待秒数 | 3~5 秒,避免疯狂重启刷满日志 |
StartLimitBurst | 窗口内最多重启次数 | 超限后进入 failed,需要人工介入 |
TimeoutStopSec / KillSignal | 停机等待与信号 | 默认 SIGTERM,等超时后 SIGKILL |
常用命令、日志与验证
sudo systemctl daemon-reload # 改了 unit 文件必须先执行,否则不生效
sudo systemctl enable --now myapi # 开机自启 + 立即启动
systemctl status myapi --no-pager # 状态、主 PID、最近日志
systemctl is-enabled myapi && systemctl is-active myapi # 期望 enabled / active
sudo journalctl -u myapi -f # 实时跟随
sudo journalctl -u myapi -n 200 --no-pager # 最近 200 行
sudo journalctl -u myapi --since "1 hour ago" # 时间范围
sudo journalctl -u myapi -p err -b # 本次开机以来的错误日志
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/healthz
# 杀掉进程,验证 Restart=always 真的生效
sudo kill -9 "$(systemctl show -p MainPID --value myapi)" && sleep 5 && systemctl is-active myapi
journal 会无限增长,记得在 /etc/systemd/journald.conf 里设 SystemMaxUse=500M,或定期 sudo journalctl --vacuum-size=500M。
优雅停机
sudo systemctl stop myapi
sudo journalctl -u myapi -n 50 --no-pager # 看应用有没有打印优雅退出日志
time sudo systemctl restart myapi # 明显快于 TimeoutStopSec,才说明信号被正确处理
停止流程是:给主进程发 SIGTERM,等满 TimeoutStopSec 后 SIGKILL 强杀。默认 KillMode=control-group,整个进程组都会被清理;若应用需要在停机时落盘或结算,务必把超时调够并在代码里注册信号处理。
用 timer 替代 cron
# /etc/systemd/system/backup.service —— 真正执行的任务
[Unit]
Description=Daily database backup
[Service]
Type=oneshot
User=deploy
ExecStart=/home/deploy/apps/myapi/bin/backup.sh
# /etc/systemd/system/backup.timer —— 调度器
[Unit]
Description=Run backup every day at 03:30
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload && sudo systemctl enable --now backup.timer
systemctl list-timers --all | head
sudo journalctl -u backup.service -n 50 --no-pager
Persistent=true 让关机期间错过的任务在开机后补跑一次,RandomizedDelaySec 让多台机器错开执行;比 cron 强在日志进 journal、失败能在 systemctl status 里看到、依赖与环境变量写法与普通服务一致。
常见坑
| 坑 | 现象 | 做法 |
|---|---|---|
改完 unit 不 daemon-reload | 配置不生效,改了像没改 | 每次改 unit 都执行一次 |
ExecStart 用相对路径或带管道 | 启动直接报错 | 绝对路径,或用 sh -c '...' |
不写 [Install] 段 | enable 失败,开机不自启 | 补 WantedBy=multi-user.target |
EnvironmentFile 里写 export | 变量解析失败 | 只写 KEY=value,权限 600 |
Restart=always 但启动即失败 | 疯狂重启刷满日志 | 配 RestartSec 与 StartLimitBurst |
| journal 无限增长 | 系统盘被日志写满 | 设 SystemMaxUse 并定期 vacuum |
小结:主流发行版上服务化首选 systemd,unit 按 [Unit]、[Service]、[Install] 三段写全,ExecStart 用绝对路径、User 指定普通用户、Restart=always 配 RestartSec 与启动频率限制、日志交给 journal;改完 unit 必须 daemon-reload,用 kill -9 验证自动重启、用 systemctl is-enabled 验证开机自启,定时任务用 timer 替代 cron。