Systemd编写服务管理脚本
本文讲解如何使用 systemd 编写服务管理脚本,包括 Unit 配置、Service 配置详解、Timer 定时任务,并补充 Supervisor 工具的对比与配置示例,帮助将前台进程托管为开机自启的常驻服务。
版本说明
本文写于 2022-03。
未在更高版本上重新验证,请以对应版本的官方文档为准。
# 1. Unit(单元)的配置文件
[Unit]
Description=Prometheus Server
Documentation=https://prometheus.io/docs/introduction/overview/
After=network.target
[Service]
User=prometheus
Restart=on-failure
WorkingDirectory=/usr/local/share/prometheus/
ExecStart=/usr/local/share/prometheus/prometheus \
-config.file=/usr/local/share/prometheus/prometheus.yml
[Install]
WantedBy=multi-user.target
2
3
4
5
6
7
8
9
10
11
12
13
14
这是 Prometheus 服务的配置文件。将上述内容保存到文件 /lib/systemd/system/prometheus.service 中,然后就可以使用 systemctl 命令管理 Prometheus 服务了。注意,服务类型的配置文件名称必须以 .service 结尾。
查看上面配置信息的详细内容,我们会发现整个配置分为三个部分:
- [Unit]:
unit本身的说明,以及与其它有依赖关系的服务的设置,包括在什么服务之后才启动此unit。 - [Service]: 不同的
unit类型需要使用相对应的设置项目,服务类型的unit就是 [Service],这个项目主要规范服务启动的脚本、环境配置文件名、重新启动的方式等等。 - [Install]: 这个部分主要设置该
unit安装到哪个 target。
# 2. 服务类型 unit 的详细配置
配置文件分为三个部分,每个部分都可以提供详细的配置信息。为了精确控制服务的运行方式,我们需要了解这些详细的配置选项,并让服务以期望的方式运行。
# [Unit] 部分
- Description: 关于该
unit的简易说明。 - Documentation: 文档相关内容,如
Documentation=https://prometheus.io/docs/introduction/overview/。 - After: 说明本
unit是在哪个服务启动后才启动,仅说明服务启动顺序,并没有强制要求。 - Before: 与 After 相反,指定的服务启动前最好启动本服务。
- Requires: 本
unit需要在某个服务启动后才能启动,设置服务间的依赖性。 - Wants: 与 Requires 相反,规范的是本
unit之后还要启动哪些服务,如果设置的服务未启动成功,不会影响本unit。 - Conflicts: 这个项目后面接的服务如果已启动,则本
unit无法启动,反之亦然。
# [Service] 部分
Type: 说明服务的启动方式,会影响到
ExecStart,主要有以下几种类型:simple: 默认值,由ExecStart设置的程序来启动,启动后常驻于内存中。forking: 由ExecStart指定的启动程序通过spawns产生子进程提供服务,然后父进程退出。oneshot: 与simple类似,但程序工作完毕后即结束,不会常驻内存。dbus: 与simple类似,但需在取得一个 D-Bus 名称后,才继续运行。idle: 与simple类似,意思是要执行此服务必须在所有工作顺利执行完毕后才执行。notify: 与simple类似,但需收到一个sd_notify()函数发送的消息后,才会继续运行。
ExecStart: 实际执行此服务的程序,接受 "命令 参数 参数..." 的格式,不能接受特殊字符,许多 bash 语法也不支持。
ExecStartPre 和 ExecStartPost: 分别在服务启动前后,执行额外的命令。
ExecStop: 用来实现
systemctl stop命令,关闭服务。ExecReload: 用来实现
systemctl reload命令,重新加载服务的配置信息。Restart: 定义服务在何种情况下自动重启,常用值包括:
no(默认值,不重启),on-failure(仅在服务失败时重启),always(总是重启),on-abnormal(在异常退出时重启)等。RestartSec: 与
Restart配合使用,设置服务终止多长时间后重新启动,默认是 100ms。KillMode: 定义systemd如何停止服务,可以是
process,control-group,mixed,none之一。TimeoutSec: 设置服务启动或关闭时的超时时间。
RemainAfterExit: 当设置为
yes时,服务所属的所有程序都终止后,服务仍被视为活动状态。适用于Type=oneshot的服务。User 和 Group: 指定服务以哪个用户和组的身份运行。
WorkingDirectory: 指定服务的工作目录。
Environment: 用来设置环境变量,可以多次使用。
EnvironmentFile: 通过文件方式设置环境变量,指定文件路径,内容格式类似
KEY=VALUE。
# [Install] 部分
- WantedBy: 设置
unit附挂在哪个targetunit下面。 - Also: 当前
unit被 enable 时,Also 后面接的unit也要 enable。 - Alias: 当
systemctl enable相关服务时,此服务会进行链接文件的创建。
# 3. Timer 类型 unit 的详细配置
Timer 类型的 unit 主要用来执行定时任务,有可能取代 cron 服务。与服务类型的 unit 不同,timer unit 配置文件中的主要部分是 [Timer]。
- OnActiveSec: 当
timers.target启动后多久执行此unit。 - OnBootSec: 当开机后多久执行此
unit。 - OnStartupSec: 当
systemd第一次启动后多久执行此unit。 - OnUnitActiveSec: 管理的服务最后一次启动后,隔多久再执行一次。
- OnUnitInactiveSec: 管理的服务最后一次停止后,隔多久再执行一次。
- OnCalendar: 使用实际时间(非循环时间)的方式来启动服务,支持复杂的日期时间表达式。
例如:
OnCalendar=*-*-* 02:00:00表示每天凌晨2点执行 或者:OnCalendar=Mon,Tue *-*-* 00:00:00表示每周一、二的零点执行 - Unit: 一般不需要设置,若服务名称和
timer名称不相同,则在.timer文件中通过Unit项指定服务名称。 - Persistent: 当使用
OnCalendar设置时,如果设为yes,则表示如果因为系统关机错过了执行时间,则在下次启动时会立即执行一次。
# 4. 配置 Redis 服务
在 Ubuntu 上,我们一般会手动编译并安装 Redis。在安装完成后需要将 Redis 配置为 systemd 管理的服务,具体配置过程如下:
# 添加 Redis 配置文件
首先手动创建 /etc/redis 目录并添加配置文件:
$ sudo mkdir /etc/redis
然后将 Redis 配置文件 redis.conf 拷贝到 /etc/redis 目录中:
$ sudo cp /tmp/redis-4.0.0/redis.conf /etc/redis/
修改配置文件 /etc/redis/redis.conf 中的 supervised 为 systemd:
supervised systemd
继续修改配置文件 /etc/redis/redis.conf 中的工作目录:
dir /var/lib/redis
# 配置由 systemd 管理的 Redis 服务
创建 /etc/systemd/system/redis.service 文件:
$ sudo vim /etc/systemd/system/redis.service
编辑其内容如下:
[Unit]
Description=Redis In-Memory Data Store
After=network.target
[Service]
User=redis
Group=redis
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf
ExecStop=/usr/local/bin/redis-cli shutdown
Restart=always
[Install]
WantedBy=multi-user.target
2
3
4
5
6
7
8
9
10
11
12
13
启动服务并配置为开机启动:
$ sudo systemctl start redis
$ sudo systemctl enable redis
$ sudo systemctl status redis
2
3
# 5. 通过脚本定时备份文件
备份文件的 bash 脚本:
#!/bin/bash
mydate()
{
date "+%Y%m%d%H%M%S"
}
backupdate=$(mydate)
tar -zcf /tmp/backup.${backupdate}.tar.gz /home/nick/learn
2
3
4
5
6
7
将上述代码保存到 /usr/local/bin/backupdir.sh 文件,并添加可执行权限:
$ sudo chmod +x /usr/local/bin/backupdir.sh
然后创建 service unit 配置文件:
[Unit]
Description=nick backup learn dir service
[Service]
User=nick
Group=nick
Type=simple
ExecStart=/usr/local/bin/backupdir.sh
[Install]
WantedBy=multi-user.target
2
3
4
5
6
7
8
9
10
11
将上述 unit 配置保存到 /etc/systemd/system/nickbak.service 文件中。然后执行以下命令测试服务执行情况:
$ sudo systemctl daemon-reload
$ sudo systemctl start nickbak.service
2
这样的备份任务只会在执行 sudo systemctl start nickbak.service 时执行一次。下面我们通过 timer unit 将其配置为定时执行。
# 创建 Timer unit 配置文件
[Unit]
Description=nick backup learn dir timer
[Timer]
OnCalendar=*:0/15
Persistent=true
Unit=nickbak.service
[Install]
WantedBy=multi-user.target
2
3
4
5
6
7
8
9
10
将上述 unit 配置保存到 /etc/systemd/system/nickbak.timer 文件中。配置中的 OnCalendar=*:0/15 表示每 15 分钟执行一次 nickbak.service 服务。
执行以下命令将 nickbak.timer 设置为开机启动,并启动 nickbak.timer:
$ sudo systemctl daemon-reload
$ sudo systemctl enable nickbak.timer
$ sudo systemctl start nickbak.timer
2
3
查看 nickbak.timer 的状态:
$ sudo systemctl status nickbak.timer
从现在开始 nickbak.timer 会每隔 15 分钟执行一次 nickbak.service 服务。
# 6. systemd 与 Supervisor:怎么选
Supervisor 用来把「一个前台运行的脚本」变成「开机自启、崩了自动拉起、日志有地方看」的常驻服务,比手写 systemd unit 更轻量。
# 6.1 各自适用场景
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 现代 Linux 服务器(主流发行版) | systemd | 原生支持、开机自启完善、日志集成 journalctl |
| 容器环境(Docker/K8s) | supervisor | systemd 在容器内不稳定,适合 supervisor 管理单容器多进程 |
| 仅有 Python 环境的老系统 | supervisor | supervisor 纯 Python 依赖少,部署简单 |
| 需要管理复杂进程树 | systemd | cgroups 隔离更安全,支持依赖控制 |
| 快速原型/临时任务 | supervisor | 配置文件比 unit 文件更简洁 |
# 6.2 Supervisor 在没有 systemd 的场景
在 Docker 容器或没有 systemd 的老系统中,supervisor 是管理常驻进程的首选:
# 安装 supervisor
pip3 install supervisor
# 创建配置目录
mkdir -p /etc/supervisor/conf.d/
# 启动 supervisor(前台模式,适合容器)
supervisord -c /etc/supervisor/supervisord.conf
2
3
4
5
6
7
8
# 6.3 两者在开机自启与日志上的差异
| 维度 | systemd | Supervisor |
|---|---|---|
| 开机自启 | systemctl enable <service> 自动注册 | 需配 systemd 管理 supervisord 自身,或在 init 脚本中启动 |
| 日志查看 | journalctl -u <service>,自动轮转 | 配置文件指定 stdout_logfile,需自行配置 logrotate |
| 日志保留 | journalctl 默认保留策略,可配 journald | 依赖外部工具,易累积占磁盘 |
| 进程隔离 | cgroups 资源限制、优先级控制 | 无 cgroups,仅进程管理 |
# 6.4 Supervisor 最小配置
如果你的环境没有 systemd(比如某些容器镜像),可以用 supervisor 管理服务:
[program:myapp]
; 工作目录
directory=/data/myapp/
; 启动命令
command=/data/myapp/bin/myapp -config /data/myapp/conf/app.toml
; 自动启动
autostart=true
; 异常退出自动重启
autorestart=true
; 启动成功后视为 running 的秒数
startsecs=3
; 以哪个用户运行
user=root
; stdout 日志
stdout_logfile=/var/log/myapp.log
; stderr 重定向到 stdout
redirect_stderr=true
; 日志文件大小(50MB)
stdout_logfile_maxbytes=50MB
; 日志备份数
stdout_logfile_backups=10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
常用管理命令:
supervisorctl status # 查看程序状态
supervisorctl start myapp # 启动
supervisorctl stop myapp # 停止
supervisorctl restart myapp # 重启
supervisorctl tail -f myapp # 查看日志
supervisorctl update # 重新加载配置
2
3
4
5
6
# 7. 坑与边界
Type=选错,systemd 会误判服务状态。之所以会误判,是因为默认的Type=simple认定「ExecStart 起来的那个进程就是主进程」;如果程序自己 fork 到后台再让父进程退出(很多老服务的-d、--daemon参数就是这个行为),systemd 看到父进程退出即判定服务已结束,于是按Restart=策略反复重拉。这种程序要么用Type=forking并配PIDFile=,要么去掉守护参数让它前台运行、继续用Type=simple(更推荐,日志也能直接进 journal)。Restart=always撞上启动限流,会把服务永久拉黑。systemd 默认StartLimitIntervalSec=10s、StartLimitBurst=5:10 秒内启动超过 5 次,之后就不再重启,systemctl status显示start request repeated too quickly。此时改完配置直接restart是没用的,原因是限流计数不会因为配置变更而清零,必须先systemctl reset-failed <name>把它清掉。给会快速失败的服务加RestartSec=5拉开间隔,比调大 burst 更有效。Timer 的
OnCalendar跟随系统时区,不是 UTC。systemctl show <name>.timer -p NextElapseUSecRealtime看到的下次触发时间才是权威值;机器时区改过、或容器里没挂/etc/localtime,定时任务会整体偏移。跨时区部署时用OnCalendar=*-*-* 02:00:00 UTC显式声明。ExecStart=不走 shell。管道、重定向、&&、变量展开都不生效,写了也只会被当成普通参数传给程序。确实需要 shell 语义时写成ExecStart=/bin/bash -c '...',或者把逻辑放进脚本文件。改完配置必须
daemon-reload。systemd 在内存里缓存 unit 定义,只restart不daemon-reload,跑的还是旧配置——这是最常见的「我明明改了却没生效」。
# 8. 验证:怎么确认服务真的起来了
# 1. 状态:Active 那一行必须是 active (running);
# 看到 activating / auto-restart 说明还在反复重启,不算起来了
systemctl status myapp.service
# 2. 主进程是不是预期的那个(Type 配错时这里会对不上)
systemctl show myapp.service -p MainPID -p Type -p Result
# 3. 日志:看有没有真正进入工作循环,而不是只打印了启动横幅
journalctl -u myapp.service -n 50 --no-pager
# 4. 重启计数:非 0 说明它在反复挂
systemctl show myapp.service -p NRestarts
# 5. 开机自启是否真的登记了
systemctl is-enabled myapp.service
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Timer 则额外确认下次触发时间与最近一次执行结果:
systemctl list-timers myapp.timer --no-pager
journalctl -u myapp.service --since today --no-pager
2
# 参考资料
# Agent 可直接解析的元数据块
{
"runbook": {
"task": "systemd-service-and-timer",
"permalinks": ["/pages/8f683a/"],
"merged_from": ["/pages/15e153/"],
"category": "linux/service-management",
"tags": ["systemd", "timer", "supervisor", "service"],
"key_pitfalls": [
"Type= 选错导致 systemd 误判主进程",
"StartLimitBurst 拉黑后需 systemctl reset-failed",
"OnCalendar 跟随系统时区",
"ExecStart 不走 shell",
"改配置必须 daemon-reload"
],
"verified_date": "2026-09"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17