用 systemd 管理系统服务

前面我们是在终端里手动启动服务的,这种方式虽然直观,但放到生产环境里会有两个明显问题:

  1. 终端窗口一关,或者服务器重启,服务就跟着退出了;
  2. 如果服务程序异常崩溃,不会有人帮我们自动重新启动。

因此在真实的 Linux 服务器环境中,像 Web 站点、数据库、API 接口这类需要长期运行的后台服务,通常会交给 systemd 来统一管理。

systemd 是 Linux 中常见的初始化系统和服务管理器。它负责:

  • 开机时自动启动指定服务;
  • 监控服务运行状态;
  • 在服务异常退出时自动重启;
  • 统一管理服务的启动、停止、查看日志等操作。

我们平时通过 systemctl 命令来和 systemd 交互。

查看服务单元配置文件

systemd 使用以 .service 结尾的配置文件来描述一个服务应该如何运行。

实验机上已经预置好了一个 bulletin.service,我们可以先查看它的内容:

crow@opsbox:~/work$ systemctl cat bulletin.service

它的核心配置如下:

[Unit]
Description=图书馆公告文件服务
After=network.target

[Service]
User=bulletin
Group=bulletin
EnvironmentFile=/etc/bulletin/server.env
ExecStart=/usr/bin/python3 -u -m http.server ${PORT} --bind ${BIND} --directory ${ROOT}
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

这个配置文件大致可以分为几个部分来理解。

Unit 段:定义服务的基本信息

[Unit]
Description=图书馆公告文件服务
After=network.target
  • Description:服务描述,说明这个服务是做什么的;
  • After=network.target:表示该服务会在网络目标之后启动,也就是说,通常会等系统网络功能基本就绪后再启动服务。

这对于需要监听端口、访问网络资源的服务来说很有用,可以避免服务启动时网络还没准备好。

Service 段:定义服务如何运行

[Service]
User=bulletin
Group=bulletin
EnvironmentFile=/etc/bulletin/server.env
ExecStart=/usr/bin/python3 -u -m http.server ${PORT} --bind ${BIND} --directory ${ROOT}
Restart=on-failure
RestartSec=2

各字段的含义如下:

  • User=bulletin / Group=bulletin 指定服务以 bulletin 这个专用用户和用户组运行。

    这样做的好处是权限最小化。服务进程不会使用 root 权限,一旦程序本身出现问题,也能降低对系统的影响范围。

  • EnvironmentFile=/etc/bulletin/server.env 表示 systemd 会从 /etc/bulletin/server.env 文件中读取环境变量。

    我们可以查看这个文件的内容:

    crow@opsbox:~/work$ cat /etc/bulletin/server.env
    PORT=8080
    BIND=127.0.0.1
    ROOT=/srv/bulletin
    

    把配置放在外部文件中,可以让服务参数和启动命令分离,后续修改端口、监听地址或服务目录时更方便。

  • ExecStart 表示 systemd 实际启动服务时执行的命令。

    在启动时,systemd 会把 ${PORT}、${BIND}、${ROOT} 这些变量替换成环境变量文件中的值。

    因此,最终执行命令大致等价于:

    /usr/bin/python3 -u -m http.server 8080 --bind 127.0.0.1 --directory /srv/bulletin
    

    其中 -u 参数可以让 Python 的 stdout 和 stderr 不被缓冲,这样日志输出会更及时,便于排查问题。

  • Restart=on-failure 表示当服务进程非正常退出时,systemd 会自动尝试重启它。

  • RestartSec=2 表示在重启前等待 2 秒,避免服务反复快速崩溃、重启、崩溃、重启。

Install 段:定义开机自启目标

[Install]
WantedBy=multi-user.target

这一部分主要影响 systemctl enable 的行为。

multi-user.target 表示系统进入普通多用户运行状态,也就是常见的服务器文本运行级别。

当我们执行:

sudo systemctl enable bulletin

systemd 会把这个服务关联到 multi-user.target,使其在系统正常进入多用户模式时自动启动。

启动、停止与重启服务

我们可以手动启动 bulletin 服务:

crow@opsbox:~/work$ sudo systemctl start bulletin

然后查看服务状态:

crow@opsbox:~/work$ systemctl status bulletin --no-pager

这里使用 --no-pager 是为了避免 status 输出内容较长时进入分页模式,直接一次性显示完整结果,方便观察。

服务启动后,可以再用 curl 验证它是否真的可以正常响应请求:

crow@opsbox:~/work$ curl -sS http://127.0.0.1:8080/hours.txt
图书馆开放时间:09:00-18:00

通过上面的操作,我们可以确认几件事:

  • status 输出中出现 Active: active (running),说明 systemd 认为服务正在运行;
  • 输出中有 Main PID,表示 systemd 管理的主进程号;
  • curl 能拿到文件内容,说明服务确实已经在 8080 端口提供文件访问。

bulletin 服务状态与一次成功的文件请求

常用的服务控制命令如下:

crow@opsbox:~/work$ sudo systemctl stop bulletin      # 停止服务(主动停止不会触发异常重启)
crow@opsbox:~/work$ systemctl is-active bulletin       # 查看当前是否在运行(返回 active 或 inactive)
crow@opsbox:~/work$ sudo systemctl restart bulletin   # 重启服务(停止后重新启动,PID 会改变)

这里有一个细节值得注意:

如果你使用 systemctl stop 主动停止服务,systemd 会认为这是管理员的正常操作,因此即使配置了:

Restart=on-failure

它也不会自动重启服务。

只有在服务异常退出,也就是非正常失败退出时,systemd 才会按照配置执行重启策略。

开机自启与当前运行状态的区别

很多初学者会把下面两个概念搞混:

  • systemctl start
  • systemctl enable

其实它们控制的是完全不同的事情。

服务当前是否运行,与是否开机启用分别控制

简单理解:

  • systemctl start:控制服务当前这一刻是否运行;
  • systemctl enable:控制服务下次服务器开机时是否自动启动。

也就是说:

  • 如果你只执行 start,服务现在会运行,但服务器重启后可能不会自动启动;
  • 如果你只执行 enable,服务会在开机时自动启动,但当前并不会立即运行,除非你同时执行 start。

如果想既设置开机自启,又立刻把服务启动起来,可以使用 --now 参数:

crow@opsbox:~/work$ sudo systemctl enable --now bulletin

这条命令大致等价于:

sudo systemctl enable bulletin
sudo systemctl start bulletin

执行完成后,可以分别确认两个状态:

crow@opsbox:~/work$ systemctl is-enabled bulletin
enabled
crow@opsbox:~/work$ systemctl is-active bulletin
active

这里:

  • is-enabled 返回 enabled,说明服务已经设置为开机自动启动;
  • is-active 返回 active,说明服务当前正在运行。

因此,在生产环境中,一个完整可靠的长期运行服务通常需要同时关注两个状态:

  1. 当前是否运行:active
  2. 开机是否自动启动:enabled