个人站长的服务器上通常跑着 Nginx、MySQL、PHP-FPM 这些服务,平时相安无事,但一遇到重启服务器就手忙脚乱:Nginx 起没起来?MySQL 怎么卡住了?哪个服务开机自动启动了?很多人的办法是往 /etc/rc.local 里堆启动命令,或者干脆重启后手动一条条敲。其实 Linux 早就内置了一个强大的服务管理器 systemd,开机自启、崩溃自动拉起、日志查询这些事它全都能干,只是很多人只用了它最基础的 systemctl restart。
这篇文章从实际运维场景出发,讲清楚 systemd 的几个高频用法:自己写一个服务单元文件、配置开机自启、让服务崩溃后自动重启、给服务设置资源上限防止拖垮整机,最后再讲 journalctl 日志查询和常见故障排查。全程都是可以直接上手的命令,建议对照自己的服务器操作一遍。
一、先搞懂 systemd 的几个基本概念
systemd 管理的每一个东西都叫 unit(单元),常见的类型有:service(服务)、timer(定时器)、mount(挂载点)、target(目标,相当于一组服务的集合)。我们平时用的 systemctl start nginx,就是在启动一个 service 类型的 unit。
unit 文件放在两个地方:/usr/lib/systemd/system/ 是软件包自带的,不要改;自己写的、要覆盖系统配置的,放在 /etc/systemd/system/。记住这个区别,排错的时候能少走很多弯路。常用命令先记牢:
systemctl status nginx # 查看服务状态(最常用的诊断命令)
systemctl start|stop|restart nginx
systemctl enable nginx # 设置开机自启
systemctl disable nginx # 取消开机自启
systemctl is-enabled nginx # 查询是否开机自启
systemctl list-units --type=service --state=running # 列出所有运行中的服务
二、写一个自己的服务单元文件
网上很多教程都是拿 Nginx 举例,但 Nginx 的 unit 文件是它自己带好的,我们更需要掌握的是"给一个没有 systemd 支持的软件写 unit"。假设你写了一个 Python 爬虫 /opt/spider/run.py,想让它开机自启、崩溃自动重启,在 /etc/systemd/system/spider.service 里写:
[Unit]
Description=My Spider Service
After=network-online.target mysql.service
Wants=network-online.target
[Service]
Type=simple
User=www
Group=www
WorkingDirectory=/opt/spider
EnvironmentFile=/etc/spider/env.prod
ExecStart=/usr/bin/python3 /opt/spider/run.py
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
逐段解释:[Unit] 段的 After= 声明依赖顺序——这个服务要在网络就绪和 MySQL 起来之后再启动,避免启动就连接失败;[Service] 段是核心:Type=simple 表示 ExecStart 启动的进程就是主进程(大多数脚本型程序都是这个类型);User=www 指定以哪个用户运行,千万不要用 root 跑应用;EnvironmentFile= 从文件加载环境变量;Restart=on-failure 是崩溃自动重启的关键,后面单独讲。[Install] 段的 WantedBy=multi-user.target 表示开机进入多用户模式(也就是正常启动完成)时自动拉起这个服务。
写完文件后必须执行 systemctl daemon-reload 让 systemd 重新加载配置,然后 systemctl enable --now spider 一键完成"设置开机自启 + 立即启动"。
三、Type=forking 与 PIDFile:Nginx 这类服务怎么写
和 simple 相对的是 Type=forking,适用于主进程启动后 fork 出子进程、然后自己退出的程序,典型的例子就是 Nginx(master 进程 fork 出 worker 进程)。这种类型必须告诉 systemd 主进程的 PID 写在哪里,否则 systemd 找不到要管理的进程。以 Nginx 为例,官方 unit 的核心结构是:
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -q
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
注意 ExecStartPre=/usr/sbin/nginx -t -q:启动前先做配置语法检查,如果 nginx.conf 写错了,服务会启动失败并给出明确报错,而不是启动一个坏配置的 Nginx。判断一个程序该用 simple 还是 forking 有个笨办法:ps aux 看进程,如果启动命令对应的进程一直存在,就是 simple;如果父进程很快退出、留下子进程继续干活,就是 forking。
四、崩溃自愈:Restart 策略详解
个人站长不可能 24 小时盯着服务器,让服务崩溃后自己爬起来才是正道。Restart 有几个可选值,含义差别很大:
no:默认值,崩溃后不重启。on-failure:非正常退出(退出码非 0、被信号杀死、超时)才重启。最常用。always:不管什么原因退出都重启,包括手动 stop 后……不会,手动 stop 不触发。on-abnormal:被信号杀死或超时才重启,进程自己 exit(0) 不重启。
光有 Restart 还不够,还要防止"疯狂重启"。如果服务一启动就崩、崩了又拉,systemd 会在短时间内反复尝试,把日志刷爆。加两个参数:
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5
含义是:60 秒内最多重启 5 次,超过就放弃并标记服务为 failed,防止无限循环。遇到这种情况 systemctl status 会提示 start-limit-hit,修复问题后用 systemctl reset-failed spider 清除失败计数再启动。PHP-FPM、MySQL 这类服务都值得配上这套策略。
五、给服务设资源上限:防 OOM 的关键
服务器上最怕的就是某个服务内存泄漏把整机拖死。systemd 可以给每个服务单独设资源上限,超过就杀,而且只杀它自己:
[Service]
LimitNOFILE=65535 # 文件描述符上限,高并发服务必备
MemoryMax=1G # 内存硬上限,超过直接 OOM 杀
MemoryHigh=768M # 内存软上限,超过时尽量回收
CPUQuota=50% # CPU 使用上限(50% 即半个核心)
TasksMax=256 # 进程/线程数上限
设置后如果服务内存超限,systemctl status 里会显示 OOM-KILL 的记录,日志里也能看到,非常直观。比裸跑在外面、靠系统级 OOM Killer 随机杀进程靠谱得多——系统级 OOM 杀的可能不是出问题的那个,而是内存占用最大的 MySQL,血泪教训。
六、日志排查:journalctl 的正确打开方式
服务出问题,第一件事就是看日志。systemd 把服务的标准输出和错误都收进了 journal,用 journalctl 查询:
journalctl -u spider # 查看 spider 服务的全部日志
journalctl -u nginx -n 50 # 最近 50 行
journalctl -u mysql -f # 实时跟随(相当于 tail -f)
journalctl -u php-fpm --since "1 hour ago"
journalctl -u nginx -p err # 只看 error 级别以上
默认情况下 journal 是存在内存里的,重启就没了。建议开启持久化:mkdir -p /var/log/journal && systemd-tmpfiles --create --prefix /var/log/journal,重启 systemd-journald 后日志就会落到磁盘。顺便限制一下日志体积,在 /etc/systemd/journald.conf 里设置 SystemMaxUse=500M,防止日志把磁盘塞满——这一点对 20G 小盘 VPS 尤其重要。如果日志已经很大,可以立刻清理:journalctl --vacuum-size=200M 把 journal 压缩到 200M 以内,journalctl --vacuum-time=7d 只保留最近 7 天。日志归档也可以定期导出:journalctl -u nginx --since "2026-07-01" > /backup/nginx-202607.log,和定时备份任务配合使用。
七、常见故障与排查套路
最后列几个我经常遇到的 systemd 故障,直接给解法:
- 服务启动失败:先
systemctl status 服务名看失败原因,再journalctl -xe看最近日志。这两条命令能解决 80% 的问题。 - start-limit-hit:重启次数超限,上面说过,
systemctl reset-failed后处理。 - Unit not found:要么文件写错了路径,要么改了文件没执行
systemctl daemon-reload。 - Exec format error:脚本没有可执行权限(chmod +x)或者 shebang 写错(比如文件是 CRLF 换行)。
- 端口被占用起不来:
ss -lntp看谁占着端口,别急着盲改配置。 - Permission denied:多半是 User= 指定的用户没有目录写权限,检查属主和权限位。
- 开机后有服务没起来:
systemctl --failed一条命令列出所有启动失败的服务,比逐个检查快得多。
八、实战案例:给 Node.js 应用写服务
把前面的知识点串起来,看一个完整的实战例子。假设你在 /opt/blog 下有一个 Node.js 博客程序,入口是 app.js,监听 3000 端口,需要开机自启、崩溃重启、限制内存,并且只在网络就绪后启动:
# /etc/systemd/system/blog.service
[Unit]
Description=My Node Blog
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=www
Group=www
WorkingDirectory=/opt/blog
ExecStart=/usr/bin/node /opt/blog/app.js
Restart=on-failure
RestartSec=3
Environment=NODE_ENV=production
MemoryMax=512M
LimitNOFILE=65535
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
配置好之后执行 systemctl daemon-reload && systemctl enable --now blog。这里额外加了一个 NoNewPrivileges=true,禁止进程获得新的权限提升,对安全性有要求的环境建议都加上。以后查看状态用 systemctl status blog,看日志用 journalctl -u blog -f,和系统服务完全一致的使用体验。Node、Python、Go 写的各类自建程序都可以照这个模板套。
九、再补两个实用小技巧
- 查开机耗时:
systemd-analyze显示开机总耗时,systemd-analyze blame按耗时列出每个服务,找出拖慢开机的元凶,配合systemctl disable关掉不必要的服务。 - 依赖关系别搞混:After= 只管顺序(谁先启动),Requires= 才是强依赖(挂了就拉不起来),Wants= 是弱依赖(启动失败不影响自己)。理解这三者的区别,unit 文件的依赖才不会写错。
总结
systemd 是个被严重低估的工具,很多人只会 restart,实际上它把"服务管理"这件事完整地包办了:开机自启、依赖排序、崩溃重启、资源限制、日志收集。对个人站长来说,花半小时给关键服务写好 unit 文件、配上 Restart 和资源限制,从此重启服务器不再提心吊胆,半夜服务崩了也能自动爬起来,日志一查就知道发生了什么。这套基本功,值得每个 Linux 站长掌握。