Linux服务器如何使用systemd管理服务?启动、停止与故障诊断

现在的 Linux 服务器,几乎所有的服务都由 systemd 管理:Nginx、MySQL、Redis、Docker、你自己写的程序,开机自启、崩溃自动拉起、日志集中查看,全都靠它。但很多人只会 systemctl start/stop/restart 三板斧,遇到服务起不来就一头雾水——status 显示 failed 却看不出原因,日志不知道去哪找,自己写的脚本想做成服务又不知从哪下手。

这篇文章把 systemd 讲透:它和传统 init 有什么区别、单元(unit)有哪些类型、systemctl 和 journalctl 的常用命令、怎么给自己的程序写一个 unit 文件、三节结构(Unit/Service/Install)各字段的含义、怎么用 systemctl edit 做安全覆盖,以及服务失败时怎么一步步定位原因。看完你应该能独立地把任意一个程序做成 systemd 服务,并在它出问题时快速找到症结。

systemd 和传统 init 的区别

老式的 SysV init 靠一串 shell 脚本(/etc/init.d/ 下的那些)按顺序启动服务,用的是"运行级别"(runlevel)的概念。它的毛病是:串行执行、启动慢、依赖关系靠脚本自己处理、管不住进程(父进程退出后子进程可能变成孤儿)。

systemd 是这几年的标准,核心变化:

维度 SysV init systemd
启动方式 串行执行脚本 按依赖并行启动
配置 shell 脚本 声明式 unit 文件
状态 runlevel 0-6 target
进程管理 不跟踪子进程 cgroup 跟踪,可管理整个进程树
日志 各服务自行写文件 统一 journald 收集
依赖 手写 显式声明 After/Requires/Wants

systemd 用 cgroup 把服务及其所有子进程圈进一个组里,所以 systemctl stop 能干净地停掉整个进程树,不会留下孤儿进程。它还统一了日志(journald)、定时任务(timer)、挂载(mount)、socket 激活等,是一整套系统和服务管理器。

验证当前是不是 systemd:

ps -p 1 -o comm=
# 输出 systemd 即为 systemd 系统
systemctl --version

单元类型:service、target、socket、timer

systemd 把一切都抽象成"单元(unit)",常见的类型:

单元后缀 类型 作用
.service 服务 一个后台进程,如 nginx.service
.target 目标 一组单元的集合,相当于运行级别
.socket 套接字 端口/socket 激活,收到请求才拉起服务
.timer 定时器 替代 cron 的定时任务
.mount 挂载 文件系统挂载点
.device 设备 内核识别的设备
.path 路径 监控文件/目录变化触发
.slice 切片 cgroup 资源分组

查看某类单元:

systemctl list-units --type=service
systemctl list-units --type=service --all     # 含未激活的
systemctl list-units --type=timer
systemctl list-unit-files --type=service      # 所有已安装的 unit 文件

target 是 systemd 的运行级别概念,和传统 runlevel 的对应关系:multi-user.target 对应 runlevel 3(多用户命令行),graphical.target 对应 runlevel 5,rescue.target 是单用户救援,poweroff.target/reboot.target 是关机和重启。

systemctl get-default
systemctl list-units --type=target
systemctl list-dependencies multi-user.target | head -n 30

systemctl 常用命令

这是日常使用最频繁的一组命令,务必记熟:

# 启停与重启
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx          # 停再起,会断开现有连接
sudo systemctl reload nginx           # 重载配置不停服务(需服务支持)
sudo systemctl reload-or-restart nginx

# 开机自启
sudo systemctl enable nginx           # 开机自启
sudo systemctl disable nginx          # 取消开机自启
sudo systemctl enable --now nginx     # 设置开机自启并立即启动

# 查看状态
systemctl status nginx                # 最常用,看运行状态和最近日志
systemctl is-active nginx             # 是否在运行 active/inactive
systemctl is-enabled nginx            # 是否开机自启
systemctl is-failed nginx
systemctl show nginx                  # 查看所有属性(脚本友好)

# 配置变更后
sudo systemctl daemon-reload          # 修改 unit 文件后必须执行

# 查看依赖
systemctl list-dependencies nginx
systemctl list-dependencies --reverse nginx

systemctl status 是最重要的诊断命令,输出包含:服务状态(active/inactive/failed)、主进程 PID、所属 cgroup、以及最近的几行日志。看到 failed 时,这几行日志往往就指向原因。

daemon-reload 的坑:修改了 unit 文件(或 systemctl edit 生成的覆盖文件)后,必须执行 sudo systemctl daemon-reload 让 systemd 重新读取配置,之后再 restart 服务。很多人改了 unit 却没 reload,发现改动没生效,就是这个原因。

journalctl:查看服务日志

systemd 用 journald 收集日志,用 journalctl 查看。查服务日志最常用的几条:

# 查看某服务的日志
journalctl -u nginx
journalctl -u nginx -n 50             # 最近 50 行
journalctl -u nginx -f                # 实时跟踪(类似 tail -f)
journalctl -u nginx --since today
journalctl -u nginx --since "2026-10-12 10:00:00"
journalctl -u nginx --since "1 hour ago"
journalctl -u nginx -p err            # 只看错误级别

# 结合启动次数
journalctl -u nginx -b                # 只看本次启动
journalctl -u nginx -b -1             # 上次启动(排查"上次为什么挂了")

# 服务失败的详细原因(含提示)
journalctl -u nginx -x                # 附加解释性文字
journalctl -xeu nginx                 # 三者合一的常用组合

# 内核日志
journalctl -k

-x 会给日志加上解释性说明,帮助理解错误;-e 直接跳到日志末尾(配合 -u 使用即"看该服务的最后日志")。所以日常排障最爱用的组合是:

sudo systemctl status nginx
sudo journalctl -xeu nginx

journal 的持久化:默认日志可能存在内存(重启丢失)或 /var/log/journal/(持久化)。想让日志重启后还在:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

# 限制日志占用空间
sudo journalctl --disk-usage
sudo journalctl --vacuum-size=500M
sudo journalctl --vacuum-time=30d

写一个自定义 unit 文件

把自研程序做成 systemd 服务,需要写一个 .service 文件。位置分两类:

  • /usr/lib/systemd/system/:软件包安装的 unit,不要手改,升级会覆盖;
  • /etc/systemd/system/:管理员自定义或覆盖的 unit,优先级更高,自己写的服务放这里。

一个 unit 文件分三节:[Unit](元信息和依赖)、[Service](怎么跑)、[Install](怎么被启用)。可以先用 systemd-analyze verify 校验。

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application Service
Documentation=https://example.internal/myapp
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment="JAVA_HOME=/usr/lib/jvm/java-17"
EnvironmentFile=-/etc/myapp/myapp.env
ExecStartPre=/opt/myapp/bin/precheck.sh
ExecStart=/opt/myapp/bin/myapp --config /etc/myapp/config.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

[Unit] 节常用字段

字段 含义
Description 服务描述,systemctl status 里显示
After 在这些单元之后启动(只定顺序,不建依赖)
Before 在这些单元之前启动
Requires 强依赖,被依赖者失败则本服务也停
Wants 弱依赖,被依赖者失败不影响本服务
Conflicts 互斥,启动本服务会停掉对方

After 和 Requires 要分清:After 只管启动顺序,不保证对方一定启动;Requires 才建立"必须"的依赖。想"等网络就绪再启动",通常写 After=network.target,需要更严格的可用网络则用 Wants=network-online.target 配合 After=network-online.target。

[Service] 节常用字段

字段 含义
Type 服务类型:simple(默认)、forking、oneshot、notify、exec
ExecStart 启动命令(必填,通常只能一条)
ExecStop 停止命令(可选)
ExecReload 重载命令,systemctl reload 触发
User/Group 以哪个用户/组运行(强烈建议别用 root)
WorkingDirectory 工作目录
Environment 环境变量
EnvironmentFile 从文件读环境变量,- 前缀表示文件不存在也不报错
Restart 何时自动重启:no、on-failure、always、on-abnormal
RestartSec 重启前等待秒数
LimitNOFILE 文件描述符上限(systemd 服务的关键配置)

Type 的选择很关键:

Type 适用场景
simple 前台运行的进程(大多数现代应用,如 java -jar)
forking 会自己 fork 到后台的传统守护进程(如老式服务)
oneshot 执行一次就退出的任务(如初始化脚本)
notify 支持 sd_notify 协议的进程,就绪后才算启动完成
exec 类似 simple,但等 exec 完成才算启动

如果进程是前台运行的(不自己 daemonize),用 simple;如果它自己 fork 后台、父进程退出,要用 forking,否则 systemd 会误以为服务退出了。

[Install] 节

[Install]
WantedBy=multi-user.target

WantedBy=multi-user.target 表示执行 systemctl enable 时,把本服务挂到 multi-user.target 下,实现开机自启。Alias 可以给服务起别名。

启用自定义服务:

# 1. 校验 unit 文件语法
sudo systemd-analyze verify /etc/systemd/system/myapp.service

# 2. 让 systemd 读取
sudo systemctl daemon-reload

# 3. 设为开机自启并启动
sudo systemctl enable --now myapp

# 4. 查看状态
systemctl status myapp
journalctl -u myapp -f

用 systemctl edit 做覆盖

想修改已安装服务的行为,不要直接改 /usr/lib/systemd/system/ 里的原始文件(升级会被覆盖)。正确做法是用 systemctl edit:

sudo systemctl edit nginx

这会创建一个 drop-in 覆盖文件 /etc/systemd/system/nginx.service.d/override.conf,你只需写要改的部分:

[Service]
LimitNOFILE=65535
Environment="TZ=Asia/Shanghai"

保存后:

sudo systemctl daemon-reload
sudo systemctl restart nginx
systemctl show nginx -p LimitNOFILE     # 验证生效

drop-in 的合并规则:同一节里,后面的设置覆盖前面的(除了像 ExecStart= 这种会累加的,需要先清空——写一个空的 ExecStart= 再写新值)。要丢弃自己的覆盖,删掉那个 drop-in 文件再 daemon-reload 即可回滚。

故障诊断:服务起不来怎么办

服务失败时,按下面的顺序排查,绝大多数问题都能定位。

第一步:看状态和最近日志

systemctl status myapp
journalctl -xeu myapp

status 输出里的 Active: 一行说明状态(failed 就是失败),下面的日志往往直接给出原因,比如 Exec format error(脚本没有 shebang 或不可执行)、No such file or directory(路径写错)、Permission denied(权限问题)、Address already in use(端口被占)。

第二步:理解常见失败原因

现象 常见原因 排查命令
Exec format error 脚本缺 shebang、没执行权限、架构不符 head -1 /path/script、chmod +x
No such file or directory ExecStart 路径错、二进制依赖库缺失 ls -l、ldd /path/bin
Permission denied 运行用户无权访问文件/端口 namei -l /path、检查 User=
Address already in use 端口被其他进程占用 ss -tlnp | grep 端口
启动后立刻 inactive Type 选错(程序会 fork 但用了 simple) 改 Type=forking
一直 activating 程序没真正就绪但用了非 notify 看程序日志,或改 Type

第三步:区分是 systemd 的问题还是程序自身的问题

手动以服务配置的用户和路径跑一遍命令,看是不是程序本身启动不了:

# 用服务里的用户手动执行(模拟 systemd 环境)
sudo -u myapp /opt/myapp/bin/myapp --config /etc/myapp/config.yml

如果手动跑也失败,就是程序或配置的问题,跟 systemd 无关;手动跑正常但 systemd 起不来,那多半是环境变量、工作目录、权限或 Type 配置的差异。systemd 服务默认的环境变量比登录 shell 少(特别是 PATH),这是常见坑——服务里 PATH 不含某个二进制路径,导致 ExecStart 找不到命令。解决办法是用绝对路径,或在 unit 里显式设置 Environment=PATH=...。

第四步:查看 cgroup 里实际启动了哪些进程

systemctl status myapp
# 里的 CGroup 段会列出该服务下的所有进程
systemctl show myapp -p MainPID -p ActiveState -p SubState

第五步:让失败原因更明确

加 -x 拿到解释性提示,journalctl -u myapp -b 看本次启动的完整日志。如果日志被截断,可以调 journald 的配置或让程序日志落到 journal 之外。

常见问题

修改了 unit 文件,为什么重启服务没变化?

改了 unit 文件(或 drop-in)后必须执行 sudo systemctl daemon-reload,让 systemd 重新加载配置,然后再 restart。只 restart 不 daemon-reload 是无用的。

reload 和 restart 有什么区别?

reload 是让服务重新读取配置,不中断现有连接/进程(前提是服务支持并有 ExecReload);restart 是彻底停掉再启动,会断开连接、有短暂不可用。改配置优先用 reload,改二进制或依赖用 restart。

自己写的服务老是启动后立刻退出(inactive)?

先看 journalctl -u 服务 的实际报错。最可能是 Type 选错:如果程序自己 fork 到后台、前台进程退出,应该用 Type=forking,否则 systemd 会认为它退出了。也可能程序确实启动失败了,用前面说的"手动以服务用户跑一遍"来区分。

服务的环境变量为什么和登录 shell 里不一样?

systemd 服务不读取 /etc/profile、~/.bashrc,它只有系统默认的少量变量。所以要用绝对路径,或用 Environment=、EnvironmentFile= 显式设置。这个差异经常导致"手动能跑、服务不能跑"。

想让服务崩溃后自动拉起,怎么配?

在 [Service] 里设 Restart=on-failure 和 RestartSec=5。on-failure 只在非正常退出时重启,always 则在任何退出(包括正常退出)后都重启。注意配合 StartLimitIntervalSec/StartLimitBurst 防止疯狂重启。

总结

  • systemd 用 cgroup 跟踪进程树、用声明式 unit 文件、用 target 代替 runlevel,比传统 init 更快、更可管;multi-user.target 是服务器最常用的目标。
  • 日常记牢 systemctl start/stop/restart/reload/enable/disable/status/is-active/daemon-reload,改 unit 后一定要 daemon-reload 再重启。
  • 日志用 journalctl -u 服务 -f、-xeu、-b -1 查看,-xeu 是排障首选;启用 journal 持久化并做空间限制,避免日志丢失或撑爆磁盘。
  • 自定义服务写 .service 文件放 /etc/systemd/system/,三节结构 Unit/Service/Install,Type 选对是关键(前台程序用 simple,会 fork 的用 forking),优先禁用 root、用 User= 降权。
  • 修改已装服务用 systemctl edit 生成 drop-in 覆盖,别改原始文件;服务起不来时先 status + journalctl -xeu,再手动以服务用户执行来区分是 systemd 还是程序自身的问题。