写给开发看的 Service 文件编写指南
新版的 ambot-service 改变了 service 文件的分发模式,之前的 service 是由 ambot deploy 项目维护的,现改为由开发维护,因此开发应当懂得一定的 service 文件概念与编写方式。
位置和名称约定
ambot 会自动扫描项目文件夹内的 .ambot 目录(详见扫描机制,这里应当有一个超链接),如果其中有一个文件夹为 services ,并且其中有一个或多个以 .service 结尾的文件,将被视为启用服务。
根据相关文档,这一文件名(包括 .service 后缀)不能超过 256 字符,并且只能由 ASCII 字母、数字、冒号、中横线、下划线、点和反斜杠组成,ambot-service 推荐只包括字母、数字和中横线。在整个部署项目中,service 名称不应有重复,可进入 Ambot 自动化部署项目管理平台(这个平台未来一定会有的,到时候这里会放个链接)查询名称是否被占用,如果名称被占用则在部署时安装 service 会报错。
编写简述
一个最最最最最基本的 service 只要包括
[Service]
ExecStart = /usr/bin/echo 123
即满足以下条件
- 拥有 Service 段
- Service 段下有 ExecStart 配置
- ExecStart 配置是要运行的命令,命令必须是绝对路径
但一个良好的 service 应当包括如下内容
[Unit]
Description = Test
Wants = ...
After = ...
[Service]
ExecStart = /usr/bin/echo 123
[Install]
Unit 段
名称 Description
虽然 Description 的含义是「描述」,但根据官方文档,其实它的作用是名称
A human readable name for the unit. This is used by systemd (and other UIs) as the label for the unit, so this string should identify the unit rather than describe it, despite the name. "Apache2 Web Server" is a good example. Bad examples are "high-performance light-weight HTTP server" (too generic) or "Apache2" (too specific and meaningless for people who do not know Apache). systemd will use this string as a noun in status messages ("Starting description...", "Started description.", "Reached target description.", "Failed to start description."), so it should be capitalized, and should not be a full sentence or a phrase with a continuous verb. Bad examples include "exiting the container" or "updating the database once per day.".
一个例子是,比如 Apache Service 的 Description 应当被设定为「Apache2 Web Server」这一名称而非「high-performance light-weight HTTP server」这一描述
启动顺序 Before / After
这一配置可以出现多次或是利用空格分隔,即下面两种表述是相同的
// 出现多次
Before = a.service
Before = b.service
// 出现一次
Before = a.service b.service
其用于定义服务启动的先后顺序,默认情况下多个 service 都是同步启动的,而配置了 Before / After 后则只有在一个 service 启动完成后才会启动另一个 service。
需要注意的是 Before 和 After 描述的是「启动顺序」而非「依赖关系」,如果两个服务设定了启动顺序的先后关系,先启动的服务是否启动成功不会影响到后续服务的启动。
除启动顺序外,其还影响关机时的停止顺序,顺序和启动顺序相反。
依赖关系 Wants / Requires / Requisite / BindsTo / PartOf
这些配置用于描述各个服务间的依赖关系,用法和启动顺序配置 Before / After 类似。需要注意的是,这里描述的「依赖关系」并不会为其自动配置「启动顺序」,所以依赖关系配置一般会和相应的启动顺序共同配置。
Wants
Wants 是一个最「弱」的依赖,如果启动服务时其 want 的服务没有启动则会将之拉起(如果没配置 Later 那么会是同步拉起,后面都是这样,不再重复表述了),但它不会关心拉起的服务的状态。
Requires
Requires 属于强依赖,除会和 Wants 一样在启动时拉起依赖服务外,如果指定了 Later,那么会根据拉起的服务状态进行判断,如果拉起的服务 fail 了那么它也会 fail。
当服务启动以后,如果对其依赖服务执行了 stop(这里就和是否指定 later 没关系了),那么当前服务也会被 stop 掉。
但这个 Require 并不是一定保证依赖服务执行的,如果依赖服务指定了条件检查失败或是依赖服务自行退出了那么并不会使当前服务退出。如果想实现需要使用 BindsTo
Requisite
与 Requires 类型,不过如果依赖服务没有被启动 Requires 会启动它而 Requisite 不会而是会直接失败。因此在启动阶段想要保证它能启动必须指定 After
BindsTo
- 如 Requisite,如果依赖服务未启动那么会直接失败而不会拉起
- 依赖服务因任何原因进入到 inactive 状态(自行退出或失败)均会导致当前服务也退出
PartOf
启动时不会检查也不会拉起依赖服务,但对依赖服务执行 restart 或 stop 会同样作用于当前服务
Service
ExecStart
ExecStart 是一个服务的「入口」,一般情况是必须存在的,用于指定这个 service 在 start 时所执行的命令。
其格式是「命令路径 + 命令参数」,实际上就类似于在 bash 中运行软件所需要写的命令,但需要注意的是 systemd 不会理解 PATH(文档中写的是对于非绝对路径会在 systemd-path search-binaries 输出的目录中搜索,但是实际测试并没有搜索这些路径,文档中也建议了始终使用绝对路径,因此可以直接假定必须使用绝对路径),因此要运行的程序必须写全路径。例如想要简单的「打印」出 123,应当这样写
[Service]
ExecStart = /usr/bin/echo 123
而不能简单的写作
[Service]
ExecStart = echo 123
因为 systemd 不能理解 PATH,无法找到 echo 程序的路径
另外需要说明的是,尽管 ExecStart 格式和 bash 中运行的差不多,但实际上有所区别
- 不支持重定向、管道等
- 对于环境变量,支持
$VAR与${VAR}两种,前者会被进行 shell 转义,而后者会被原样插入至相应位置
ExecStart 支持在命令前加上前缀来实现一些「特殊效果」(事实上,后面说到的 ExecStartPre, ExecStartPost, ExecReload, ExecStop 和 ExecStopPost 均支持),常见特殊前缀有
-:程序以非 0 返回值退出也不会将程序标记为 fail+:程序不会受到任何权限限制
Type
Type 指定了 Service 的类别,一般用 simple 或 notify。simple 代表着一个启动就成功的 service,而 notify 则需要程序适配 sdnotify 接口来向 systemd 主动汇报状态
ExecStop / ExecRestart / ExecReload
与 ExecStart 类似,但用于控制 停止 / 重启 /reload 的行为
- 默认停止就是 kill
- 默认重启就是先 ExecStop 再 ExecStart
- 默认不支持 reload
Restart
控制程序在 Stop(或 Fail)后是否重启
- no:(默认)不重启
- on-success:成功退出时重启
- on-failure
- on-abnormal
- on-watchdog
- on-abort
- always

Install
如果服务想要支持 Enable 则需要指定 Install 段
Install 而言通常只需要指定如下行
[Install]
WantedBy = multi-user.target
在 CentOS 7 上,这行必须存在,否则无法安装;在 CentOS 8 上,Ambot Service 会自动注入安装指令