用 systemd 托管后台服务:Restart 策略、日志接管、资源限制与开机自启全踩一遍

用 systemd 托管后台服务:Restart 策略、日志接管、资源限制与开机自启全踩一遍

你写了个 Python/Go/Node 的后台服务,部署到 Linux 上。第一版怎么跑?大概率是 nohup ./app &,或者塞进 screen / tmux 里。然后你会陆续踩到:进程崩了没人拉起来、服务器重启后服务没了、日志全糊在一个越来越大的 nohup.out 里、想看它有没有活着只能 ps aux | grep

这些问题 systemd 全都替你解决了,而且它就是现代 Linux(CentOS 7+/Ubuntu 16.04+/Debian 8+)的标配,不用装任何东西。这篇文章从一个能跑但很糟的启动方式出发,一步步把服务交给 systemd 托管,把 Restart、日志、资源限制、开机自启这几件事讲透。

nohup 方式糟在哪

先看典型的「土办法」:

bash 复制代码
nohup python3 /opt/myapp/server.py > /var/log/myapp.log 2>&1 &

它的问题:

  1. 崩了不会自动重启。 进程 OOM 或抛异常退出,服务就没了,你得盯着。
  2. 重启服务器就丢。 机器重启后没人再执行这条命令。
  3. 日志不切割。 myapp.log 无限增长,迟早撑爆磁盘。
  4. 管理全靠手动。 停止要先 ps 找 PID 再 kill,查状态靠猜。
  5. 没有资源隔离。 一个服务内存泄漏能把整台机器拖死。

systemd 把服务定义成一个「unit 文件」,以上五点全内建支持。

写第一个 service 文件

systemd 的服务定义放在 /etc/systemd/system/ 下,文件名以 .service 结尾。建一个 /etc/systemd/system/myapp.service:

ini 复制代码
[Unit]
Description=My App Server
# 等网络就绪后再启动,依赖网络的服务几乎都要这行
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
# 用哪个用户跑,别用 root 跑业务进程
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
# ExecStart 必须写绝对路径,systemd 不吃 PATH 里的相对命令
ExecStart=/usr/bin/python3 /opt/myapp/server.py
# 崩了就拉起来
Restart=on-failure
RestartSec=3s

[Install]
# systemctl enable 时挂到这个 target,实现开机自启
WantedBy=multi-user.target

三个段落各管一摊:

  • [Unit]:描述和依赖关系(什么之前/之后启动)。
  • [Service]:进程本身怎么跑、怎么重启。
  • [Install]:enable 时怎么挂载,决定开机自启。

写完让 systemd 重新读配置,然后启动:

bash 复制代码
sudo systemctl daemon-reload      # 改动 .service 文件后必须执行
sudo systemctl start myapp        # 启动
sudo systemctl status myapp       # 看状态(是否 active running、最近日志)
sudo systemctl enable myapp       # 设置开机自启

enable 之后,服务器重启会自动拉起,前面第 2 个痛点解决。

Type 到底该填 simple 还是别的

Type= 决定 systemd 怎么判断「服务算启动成功了」,填错会导致依赖它的服务时序错乱。最常用的三个:

  • Type=simple(默认):ExecStart 一旦 fork 出进程,立刻认为启动完成。适合前台运行、不 daemon 化 的程序(大多数现代服务:Go 二进制、python server.pynode app.js)。
  • Type=forking:程序自己会 fork 到后台、父进程退出。传统守护进程(如某些 C 写的 daemon)用它,通常要配 PIDFile=
  • Type=notify:程序启动完成后主动用 sd_notify 通知 systemd「我好了」。最精确,但要程序支持(如 nginx、systemd 原生集成的服务)。

判断口诀:你的程序是不是在前台一直跑、不自己 daemon 化? 是就 simple。绝大多数用脚本/二进制直接跑的服务都是 simple。如果你用了 --daemon 之类让程序自己退到后台的参数,反而要用 forking------但更推荐去掉那个参数、让 systemd 管前台进程。

Restart 策略:别无脑用 always

Restart= 控制进程退出后要不要重启,几个常用值:

  • no(默认):从不重启。
  • on-failure:非正常退出 (退出码非 0、被信号杀死、超时)才重启。推荐默认选它。
  • always:不管怎么退出都重启,包括正常退出(退出码 0)。
  • on-abnormal:只在被信号杀死或超时时重启。

为什么推荐 on-failure 而不是 always?因为 always 会连「你主动正常关闭」也拉起来------比如一次性任务跑完正常退出(exit 0),always 会把它无限重启。业务常驻服务用 on-failure 最合适:崩了(异常)才救,主动停了就别捣乱。

光有 Restart 还不够,得防「崩溃-重启-再崩溃」的死循环把 CPU 打满。用启动限流:

ini 复制代码
[Service]
Restart=on-failure
RestartSec=3s
# 10 秒窗口内最多重启 5 次,超了就放弃并标记 failed
StartLimitIntervalSec=10s
StartLimitBurst=5

超过阈值后 systemd 会停手并把服务标为 failed,而不是无限空转。这时 systemctl status 会显示 start-limit-hit,你就知道该去查根因而不是让它自己抽搐。

日志:直接交给 journald

systemd 服务的标准输出/标准错误默认被 journald 接管,不用你自己重定向文件、也不用配 logrotate。查日志:

bash 复制代码
journalctl -u myapp                 # 该服务全部日志
journalctl -u myapp -f              # 实时跟踪(类似 tail -f)
journalctl -u myapp --since "10 min ago"   # 最近 10 分钟
journalctl -u myapp -p err          # 只看 error 及以上级别
journalctl -u myapp --since today   # 今天的

journald 自带按大小/时间滚动清理,不会像 nohup.out 那样无限增长(默认上限可在 /etc/systemd/journald.conf 里用 SystemMaxUse= 调,比如 SystemMaxUse=500M)。

一个实操细节:很多语言的运行时默认对 stdout 做缓冲,导致日志不实时刷进 journald。Python 尤其明显,要关掉缓冲:

ini 复制代码
[Service]
# 让 Python 的 stdout/stderr 不缓冲,日志实时进 journald
Environment=PYTHONUNBUFFERED=1
ExecStart=/usr/bin/python3 /opt/myapp/server.py

或者在命令行加 python3 -u。不加这个,你会疑惑「明明在打日志,journalctl 里却半天不出东西」。

资源限制:一个服务别拖垮整台机器

systemd 基于 cgroup,能直接给服务上内存/CPU 限制,不用 Docker 也能做资源隔离:

ini 复制代码
[Service]
# 内存超过 512M 就被内核 OOM 掉这个服务(而不是随机杀别人)
MemoryMax=512M
# 高水位软限制,超了开始回收但不立刻杀
MemoryHigh=400M
# CPU 最多用 1.5 个核(150%)
CPUQuota=150%
# 最多开 100 个任务(线程/进程),防 fork 炸弹
TasksMax=100

MemoryMax 是硬上限:进程超了会被 OOM killer 精准干掉这个服务 ,而不是让内核在整机内存耗尽时随机挑一个进程杀。这就把前面第 5 个痛点(一个服务泄漏拖死全机)堵上了。改完记得 daemon-reload + restart

想临时看某个服务实时吃了多少资源:

bash 复制代码
systemctl status myapp     # 状态里会显示 Memory、CPU、Tasks 当前用量
systemd-cgtop              # 类似 top,按 cgroup 看各服务资源占用

一个更完整的生产级模板

把上面的点拼起来,加几个常用的安全加固项:

ini 复制代码
[Unit]
Description=My App Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
Environment=PYTHONUNBUFFERED=1
Environment=APP_ENV=production
# 从文件读环境变量,密钥别写死在 unit 文件里
EnvironmentFile=/opt/myapp/.env
ExecStart=/usr/bin/python3 /opt/myapp/server.py
# 优雅停机:先发 SIGTERM,给 30 秒收尾再 SIGKILL
KillSignal=SIGTERM
TimeoutStopSec=30s

Restart=on-failure
RestartSec=3s
StartLimitIntervalSec=10s
StartLimitBurst=5

MemoryMax=512M
CPUQuota=150%
TasksMax=100

# 安全加固:只读系统目录、禁止提权、私有 /tmp
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
# strict 下若服务要写某目录,显式放开
ReadWritePaths=/opt/myapp/data

[Install]
WantedBy=multi-user.target

几个值得说的:

  • EnvironmentFile= 把密钥放独立文件(权限设 600),别硬编码进 unit。
  • TimeoutStopSec=30s + KillSignal=SIGTERM:systemctl stop 时先发 SIGTERM,你的程序捕获它做优雅关闭(处理完在途请求),30 秒还没退才强杀。这对有状态服务很重要。
  • ProtectSystem=strict / NoNewPrivileges 这类是「零成本」的安全加固,让服务即使被攻破也难以动系统文件,配 ReadWritePaths 放开确实需要写的目录即可。

排查:服务起不来先看这三样

bash 复制代码
systemctl status myapp        # 概览:active/failed、退出码、最近几行日志
journalctl -u myapp -n 50 --no-pager   # 最近 50 行完整日志,看真实报错
systemctl cat myapp           # 打印当前生效的 unit 内容,确认没写错/没忘 daemon-reload

最高频的三个低级错误:改了 .service 忘了 daemon-reload(改动没生效)、ExecStart 写了相对路径(找不到命令)、User 指定的用户对 WorkingDirectory 没权限(启动即失败)。status + journalctl 基本一眼定位。

小结

  • systemd 是现代 Linux 自带的服务管理器,用一个 .service 文件就替代了 nohup + 手动重启 + logrotate + cgroup 一整套,不用装任何东西。
  • 核心四要素:Restart=on-failure(崩了才救)配 StartLimitBurst 防重启风暴;stdout 交给 journald 用 journalctl -u 查;MemoryMax/CPUQuota 做资源隔离;enable + WantedBy=multi-user.target 实现开机自启。
  • Type= 绝大多数前台服务填 simple;Python 记得 PYTHONUNBUFFERED=1 否则日志不实时。
  • 改完 .service 一定 daemon-reload;起不来先看 systemctl status + journalctl -u

一句话记忆:别再 nohup &------写个二十行的 unit 文件,自动重启、开机自启、日志、限流、资源上限一次全有了。

相关推荐
动力 continue2 小时前
docker容器大舞台,创建镜像,构造类,基础速通
docker·容器
乌恩大侠2 小时前
GH200 新增 NVMe SSD 分区、格式化与挂载完整流程
运维·服务器·前端
tangwangbi2 小时前
Linux 系统配置文件:/etc/profile、~/.bashrc 和 ~/.bash_profile 三者之间的区别与作用
linux·运维·bash
Akiyama_Mio-Kon2 小时前
2026 最新:Dify 本地 Docker 部署完整教程(Windows + Git Bash)
windows·git·docker
蜀道山老天师2 小时前
Shell Bash变量与运算符(含条件测试与流程控制)
linux·运维·bash
草邦设计开发团队_媒体资源平台2 小时前
GEO 信源整合一键发布软文:从内容生产到流量获客的自动化实践
运维·人工智能·自动化
zhou lily2 小时前
非标自动化管理系统ERP如何选?2026年10大ERP软件对比分析
运维·自动化
mengge.cloud3 小时前
存储技术基础小白教程
linux·运维·服务器·wpf·存储
云贝贝贝3 小时前
腾讯云 TDSQL(MySQL 版)运维高频 6 坑:代理路由、读写分离、分片键、监控、PITR
运维·mysql·腾讯云