自己写的小工具、定时脚本、后台服务,以前都是 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 大概花了半小时,换来的是开机自启、崩溃自动拉起、日志统一收集和资源隔离。对个人的小服务来说,这个投入产出比相当划算。