
本文教你在主流 Linux 上用 systemd 编写服务单元、配置依赖与重启策略,并解决进程异常退出、日志爆量、开机不自启三类常见问题。读完会拿到一份可直接套用的 service 模板、一份资源限制对照表、一份故障排查清单,以及把规范固化到团队 SOP 的建议方法。
一、为什么放弃 nohup 改用 systemd
很多人仍习惯用 nohup 或 screen 把进程挂到后台,问题是机器一重启进程就没了、日志四散在各处文件、崩了不会自动起来、也无法做资源隔离。systemd 是当前主流发行版默认的初始化系统,提供统一的服务管理、依赖解析、cgroup 资源限制与 journald 日志收集,是替代 nohup 与自写脚本的标准方式。生产服务器上几乎所有常驻进程都应走 unit 文件管理。
迁移成本其实很低:一份典型的 unit 文件通常十五行以内,放到 /etc/systemd/system/ 目录下执行 systemctl daemon-reload 后即可 enable 与 start。下面这套规范覆盖了 service unit 的结构、依赖语义、重启策略、资源限制与日志排查,照着改就能直接上生产环境。基础部署与机器选型可参考 美国 VPS 部署教程。
二、unit 文件的三段结构
一个 service unit 文件按段落分三块:[Unit] 段写描述与依赖关系;[Service] 段写进程启动方式、运行用户、工作目录、资源限制;[Install] 段控制开机自启时挂到哪个 target 链路。最小可用骨架示例:Description=My Worker、After=network-online.target、Wants=network-online.target、Type=simple、User=app、WorkingDirectory=/opt/myapp、ExecStart=/opt/myapp/venv/bin/python worker.py、Restart=on-failure、RestartSec=5s、WantedBy=multi-user.target。
关键点解释:After 与 Wants 一起写表示弱依赖,network-online.target 比 network.target 更稳健,可以避免某些网络驱动延迟导致进程启动失败;Type=simple 是默认值,表示 ExecStart 启动的进程本身就是服务主进程;Restart=on-failure 表示非零退出码或被信号杀掉时才重启;WantedBy=multi-user.target 是开机启动链上最常用的挂载点,systemctl enable 会在这里建立符号链接。反向代理场景的配套配置可继续看 Nginx 反向代理与负载均衡。
三、Type、Restart 与依赖语义
Type= 字段最常用的三种:simple 适合前台运行的程序,启动后不 fork;forking 适合传统 daemon,程序会自己 fork 出子进程后父进程退出,需要配合 PIDFile 字段;notify 是程序通过 sd_notify 调用主动通知 systemd 自己已经就绪,Nginx 与 PostgreSQL 都支持这种模式,启动时序最准确。如果 Type 写错,systemd 会判定主进程已退出,触发频繁重启。
Restart= 推荐设为 on-failure,比 always 更安全,避免正常 exit 0 也被重新拉起;用 StartLimitBurst=5 与 StartLimitIntervalSec=60s 限制 60 秒内最多重启 5 次,超出后停止避免疯狂循环。依赖关系上 After 是排序约束,Requires 是强依赖(依赖目标失败则本服务也失败),Wants 是弱依赖(依赖目标失败本服务仍启动)。业务服务通常用 Wants=network-online.target 加 After=network-online.target 就够,避免与数据库之间用 Requires 造成连锁失败级联放大。
四、资源限制与安全沙箱
systemd 通过 cgroup 直接限制服务的资源占用,比手写 ulimit 可靠得多。常用字段对照:MemoryMax=512M 超出立刻 OOM 杀掉本服务、MemoryHigh=400M 是软限触发内核加压回收、CPUQuota=50% 单核 50% 即半个核、TasksMax=200 限制服务总进程与线程数、LimitNOFILE=65535 文件描述符上限、IOWeight=100 块设备 IO 权重(范围一到一万)。
安全字段也建议默认开启:NoNewPrivileges=yes 禁止子进程通过 setuid 或 capabilities 提权、ProtectSystem=strict 把 /usr 与 /etc 设为只读、ProtectHome=yes 隐藏 /home 目录、PrivateTmp=yes 给服务挂载独立 /tmp 命名空间避免相互泄露临时文件。这套限制配置的核心作用是把单个服务的故障半径锁定在自己的 cgroup 里,内存泄漏不会拖死整机,被攻破也无法写入系统目录。CDN(Content Delivery Network,内容分发网络)边缘节点的 worker 进程也常用这套沙箱配置,相关回源加速策略可参考 Cloudflare CDN 中国区加速。
五、journald 日志与排查命令
journald 自动收集所有服务的 stdout 与 stderr,不需要应用自己写文件。常用查询命令:journalctl -u myworker.service 看单服务历史日志、-f 跟踪输出类似 tail -f、--since "1 hour ago" 限时间窗、-p err 只看 error 级别、-o json-pretty 输出结构化日志便于程序解析。
服务状态用 systemctl status myworker 看最近 10 行日志、当前 PID、cgroup 内存占用与 CPU 时间;systemctl list-units --failed 列所有失败状态的服务;systemctl show myworker -p MainPID,ExecMainStatus,Restart 查具体内部字段;systemctl cat myworker 查看完整 unit 内容包含 drop-in 覆盖。SSH 守护进程的相关加固配置可参考 Linux SSH 与 Fail2ban 加固。
六、典型故障排查清单
服务起不来:先用 systemctl status 看退出错误码,再用 journalctl -u 找异常栈;最常见的原因是 WorkingDirectory 路径不存在、User 字段没权限读取文件、ExecStart 二进制路径写错、依赖的 target 未就绪。把 ExecStart 命令直接复制到终端用同一个 User 跑一遍,是最快的定位办法。
频繁重启:status 输出会显示 Active: activating (auto-restart) 与累计重启次数。原因通常是 Type 写错(典型如 forking 的程序写成 simple,systemd 误以为主进程退出就重启)、依赖服务还没起来就被调度、内存超 MemoryMax 被 OOM。临时把 RestartSec 加大到 30 秒,给自己留排查时间。
日志爆量:journald 默认 4GB 滚动覆盖,但应用每秒上千条仍会拖累整机 IO。/etc/systemd/journald.conf 里设 SystemMaxUse=2G、RateLimitIntervalSec=30s、RateLimitBurst=1000 做限速。在应用层关掉 debug 级别日志才是根治办法。
七、生产建议
systemd 不是部署工具,是用来管理常驻进程的标准方式。建议把自写脚本、Python worker、Go 二进制、Node 服务都改写成 unit 文件。可以考虑把 unit 文件纳入 Git 与服务器配置一起做版本管理,Ansible 与 SaltStack 都有现成 module 直接分发。如果你需要在多台服务器上做统一编排,可以考虑用 systemd 的 template unit(带 @ 的实例化服务)批量管理同类工作进程。
总结:一份合格的 service unit 包含明确的 Type、合理的 Restart 与 StartLimit、必要的 MemoryMax 与 TasksMax,加上 NoNewPrivileges 与 ProtectSystem 沙箱字段,再配合 journalctl 做日志排查与告警,足以替代 99% 的 nohup 与自写守护脚本。建议把这套规范固化进团队的部署 SOP 与上线检查清单里,长期收益非常明显,也便于新成员快速接手存量服务。