← 返回笔记列表

用 Systemd 托管自己写的小服务

自己写的小工具、定时脚本、后台服务,以前都是 nohup ... & 一把梭,服务器一重启就全没了,进程挂了也没人知道。换成 systemd 托管之后省心很多,配置也就十几行。这篇把常用字段整理一遍。

一、最小可用的 service 文件

文件放在 /etc/systemd/system/ 下,命名为 服务名.service

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application Service
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --config /opt/myapp/config.yaml
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

三个段落各管一件事:[Unit] 描述服务本身和依赖关系,[Service] 定义怎么启动,[Install] 决定开机自启时挂在哪个 target 下面。

二、几个必须搞清楚的字段

Type

取值适用场景
simple默认值。程序在前台运行,不自己 fork
forking程序会 fork 出子进程后父进程退出,需配合 PIDFile
oneshot执行一次就结束的脚本,常配 RemainAfterExit=yes
notify程序启动完成后主动通知 systemd,适合有就绪探测需求的服务

最常见的错误是程序自己带了守护进程模式(-d 之类的参数),却写了 Type=simple。systemd 以为主进程退出了,会立刻判定服务失败并按重启策略反复拉起。用 systemd 托管时,程序一律不要开自身的 daemon 模式,保持前台运行即可。

Restart

取值行为
no默认,不自动重启
on-failure非零退出码、被信号杀死、超时才重启(推荐)
always无论如何都重启,包括正常退出
on-abnormal只在被信号杀死或超时时重启

常驻服务用 on-failure 就够了。用 always 的话,你手动 kill 掉进程它也会自己爬起来,调试时容易困惑。

配套的还有防雪崩参数:

RestartSec=5s          # 每次重启前等 5 秒
StartLimitIntervalSec=60   # 60 秒的统计窗口内
StartLimitBurst=5          # 最多重启 5 次,超了就放弃

不设限制的话,一个启动即崩溃的程序会被无限拉起,日志瞬间刷爆磁盘。注意较新版本的 systemd 里,这两个 StartLimit* 要写在 [Unit] 段而不是 [Service] 段。

User 和 WorkingDirectory

不写 User 默认以 root 运行,没必要也不安全,建一个专用账号即可。WorkingDirectory 一定要显式指定,否则默认工作目录是根目录,程序里所有相对路径都会读不到文件——这个坑我踩过不止一次。

环境变量

Environment=APP_ENV=production
Environment=LOG_LEVEL=info
EnvironmentFile=/opt/myapp/.env

EnvironmentFile 指向的文件格式是简单的 KEY=VALUE,不支持 shell 语法,不要在里面写 export 或者变量引用。文件不存在会导致启动失败,路径前加 - 可以让它变成可选:EnvironmentFile=-/opt/myapp/.env

三、常用命令

systemctl daemon-reload            # 改完 .service 文件必须先执行这个
systemctl start myapp              # 启动
systemctl stop myapp               # 停止
systemctl restart myapp            # 重启
systemctl reload myapp             # 重载配置(需服务支持)
systemctl status myapp             # 查看状态,含最近几行日志
systemctl enable myapp             # 设置开机自启
systemctl disable myapp            # 取消开机自启
systemctl enable --now myapp       # 自启 + 立即启动,一步到位

daemon-reload 忘了执行的话,改的配置根本不会生效,systemd 用的还是旧的缓存。这是最容易浪费时间的一步。

四、日志怎么看

用 systemd 托管后,程序打到 stdout/stderr 的内容会自动进 journal,不用自己做日志切割:

journalctl -u myapp                # 全部日志
journalctl -u myapp -f             # 实时跟踪,等价于 tail -f
journalctl -u myapp -n 100         # 最近 100 行
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp --since today --until "10:00"
journalctl -u myapp -p err         # 只看 error 及以上级别
journalctl -u myapp -o json-pretty # JSON 格式,字段更全

journal 默认可能只存在内存里,重启就丢。要持久化:

mkdir -p /var/log/journal
systemctl restart systemd-journald

再在 /etc/systemd/journald.conf 里限制体积,免得撑爆磁盘:

[Journal]
Storage=persistent
SystemMaxUse=500M
MaxRetentionSec=2week

五、加一层简单的资源限制

systemd 可以直接用 cgroup 给服务设限,不用额外装什么:

MemoryMax=512M          # 超过就 OOM kill
CPUQuota=50%            # 最多用半个核
LimitNOFILE=65535       # 文件描述符上限
TasksMax=4096           # 线程/进程数上限

LimitNOFILE 尤其重要,默认的 1024 对稍有并发的网络服务来说完全不够,报 too many open files 多半是这个没调。

顺手加点安全加固

NoNewPrivileges=true       # 禁止提权
PrivateTmp=true            # 独立的 /tmp 命名空间
ProtectSystem=strict       # 整个文件系统只读
ProtectHome=true           # 看不到 /home
ReadWritePaths=/opt/myapp/data   # 单独放开需要写的目录

ProtectSystem=strict 配合 ReadWritePaths 是性价比很高的一组,能挡住不少意外的文件写入。

六、启动不了的时候

systemctl status myapp -l --no-pager   # 完整错误信息
journalctl -xeu myapp                  # 带解释的详细日志
systemd-analyze verify myapp.service   # 检查配置文件语法

还有一个笨但有效的办法:把 ExecStart 那行命令复制出来,切到 User 指定的用户下手动跑一遍。绝大多数「systemd 起不来但手动能跑」的问题,最后都归结为权限、工作目录或者环境变量三者之一。


nohup 迁到 systemd 大概花了半小时,换来的是开机自启、崩溃自动拉起、日志统一收集和资源隔离。对个人的小服务来说,这个投入产出比相当划算。