编写一个生产级的Service配置文件

描述

一、概述

1.1 背景介绍

systemctl start xxx 敲了无数遍,但真要从零写一个 Service 文件丢到生产环境跑,很多人就开始心虚了。网上抄一段配置,Type=simple 还是 forking 搞不清楚,Restart=always 往上一贴就觉得万事大吉,结果进程挂了不重启、OOM 了没人管、日志把磁盘写爆了才发现 journald 根本没配轮转。

Systemd 从 2010 年诞生到现在,已经不只是一个 init 系统了。它是 Linux 世界的 PID 1,是服务管理器、日志系统、定时任务调度器、设备管理器、网络配置工具的集合体。2026 年的主流发行版(Ubuntu 24.04/RHEL 9/Debian 12)全部默认使用 systemd 256+,cgroup v2 也已经是标配。不管你是跑 Java 微服务、Go 二进制、Python 脚本还是 Nginx,最终都要落到一个 .service 文件上。

这篇文章的目标很明确:从 systemd 的架构讲起,把 Unit 文件的每个 Section、每个关键指令都讲透,最后给出一个经过生产验证的完整 Service 配置模板。看完之后,你应该能独立编写一个带资源限制、安全加固、健康检查、日志管理的生产级 Service 文件。

1.2 技术特点

体系化:从架构到配置到实战,一条线串起来,不是零散的参数罗列

生产导向:每个配置项都说明"为什么要这么配",而不只是"可以这么配"

安全优先:覆盖 ProtectSystem、PrivateTmp、NoNewPrivileges 等安全加固指令

现代技术栈:基于 systemd 256+、cgroup v2、journald 的 2026 年最佳实践

1.3 适用场景

场景一:需要将自研服务部署到 Linux 服务器,编写规范的 Service 文件

场景二:现有 Service 配置过于简陋,需要补充资源限制和安全加固

场景三:想用 systemd timer 替代 cron,实现更可靠的定时任务管理

场景四:需要理解 systemd 的依赖管理机制,解决服务启动顺序问题

1.4 环境要求

组件 版本要求 说明
操作系统 Ubuntu 24.04 LTS / RHEL 9.x 内核 6.8+,cgroup v2 默认启用
systemd 256+ 支持本文涉及的所有特性
journald 随 systemd 版本 日志管理组件
cgroup v2 资源限制依赖 cgroup v2

二、Systemd 架构和核心概念

2.1 Systemd 不只是 init

很多人对 systemd 的认知停留在"启动服务的工具",这个理解太窄了。Systemd 是一整套系统管理框架,PID 1 进程(/usr/lib/systemd/systemd)是它的核心,但远不是全部。

 

                        +------------------+
                        |   PID 1 (systemd) |
                        +--------+---------+
                                 |
          +----------+-----------+-----------+----------+
          |          |           |           |          |
     +---------+ +--------+ +--------+ +--------+ +--------+
     | journald| | logind | | udevd  | | networkd| | resolved|
     +---------+ +--------+ +--------+ +--------+ +--------+
       日志管理   会话管理   设备管理   网络管理    DNS解析

     +----------+----------+----------+
     |  systemd-tmpfiles   |  systemd-sysctl  |  systemd-modules-load |
     +----------+----------+----------+
       临时文件管理         内核参数            内核模块加载

 

PID 1 负责的核心工作:解析 Unit 文件、管理依赖关系、启动/停止/监控服务进程、处理 cgroup 资源分配。其他组件各司其职,通过 D-Bus 与 PID 1 通信。

2.2 三个核心概念:Unit、Target、Slice

2.2.1 Unit(单元)

Unit 是 systemd 管理的基本对象。一个 Unit 对应一个配置文件,文件后缀决定了 Unit 的类型:

类型 后缀 用途 典型示例
Service .service 管理守护进程 nginx.service
Socket .socket 套接字激活 sshd.socket
Timer .timer 定时任务 logrotate.timer
Mount .mount 挂载点 home.mount
Target .target 逻辑分组 multi-user.target
Slice .slice 资源分组 user.slice
Path .path 文件监控 cups.path
Device .device 设备管理 由 udev 自动生成

日常打交道最多的就是前三个:Service、Socket、Timer。

2.2.2 Target(目标)

Target 是一组 Unit 的逻辑集合,类似于 SysVinit 时代的 runlevel,但更灵活。Target 本身不做任何事情,它只是把一堆 Unit 聚合在一起,表示"系统到达了某个状态"。

 

# 查看当前活跃的 target
systemctl list-units --type=target --state=active

# 常见 target 对应关系
# multi-user.target  ≈  runlevel 3(多用户命令行)
# graphical.target   ≈  runlevel 5(图形界面)
# rescue.target      ≈  runlevel 1(单用户模式)

 

服务启动顺序的核心就是围绕 target 来编排的。比如大多数网络服务都声明 After=network-online.target,意思是"等网络就绪了再启动我"。

2.2.3 Slice(切片)

Slice 是 cgroup 的 systemd 抽象层,用于对一组服务进行资源分配。默认的 slice 层级结构:

 

-.slice (根 slice)
├── system.slice    # 系统服务(nginx、mysql 等)
├── user.slice      # 用户会话
│   ├── user-1000.slice
│   └── user-1001.slice
└── machine.slice   # 虚拟机和容器

 

可以自定义 slice 来实现资源隔离。比如把所有业务服务放到同一个 slice 里,统一限制 CPU 和内存上限,防止业务进程把系统服务挤死。

2.3 Unit 文件的存放位置

Unit 文件有三个存放位置,优先级从高到低:

路径 优先级 用途
/etc/systemd/system/ 最高 管理员自定义配置
/run/systemd/system/ 运行时动态生成
/usr/lib/systemd/system/ 最低 软件包安装的默认配置

实际操作原则:永远不要直接修改 /usr/lib/systemd/system/ 下的文件,包管理器更新时会覆盖掉。自定义配置放 /etc/systemd/system/,覆盖默认配置用 systemctl edit 创建 drop-in 文件。

 

# 用 drop-in 方式覆盖某个参数,不动原始文件
# 会创建 /etc/systemd/system/nginx.service.d/override.conf
sudo systemctl edit nginx.service

# 查看某个 unit 的最终生效配置(合并所有 drop-in)
systemctl cat nginx.service

 

2.4 Unit 文件结构:三个 Section

一个标准的 .service 文件由三个 Section 组成:

 

[Unit]
# 描述信息和依赖关系
Description=My Application Service
Documentation=https://docs.example.com
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
# 服务运行参数
Type=notify
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yaml
Restart=on-failure
RestartSec=5s
User=myapp
Group=myapp

[Install]
# 安装信息(enable/disable 时使用)
WantedBy=multi-user.target

 

[Unit] Section:定义 Unit 的元信息和依赖关系。Description 是给人看的,After/Before 控制启动顺序,Requires/Wants 控制依赖强度。

[Service] Section:这是 .service 文件独有的,也是最核心的部分。定义了进程怎么启动、怎么停止、怎么重启、以什么身份运行、资源限制多少。

[Install] Section:定义 systemctl enable 时的行为。WantedBy=multi-user.target 的意思是"当系统进入多用户模式时,把我也带上"。执行 enable 时,systemd 会在 multi-user.target.wants/ 目录下创建一个指向这个 service 文件的符号链接。

 

# enable 的本质就是创建符号链接
sudo systemctl enable myapp.service
# 等价于:
# ln -s /etc/systemd/system/myapp.service 
#   /etc/systemd/system/multi-user.target.wants/myapp.service

# 查看一个 unit 是否 enabled
systemctl is-enabled myapp.service

 

2.5 Service Type 详解

Type= 是 [Service] Section 里最关键的一个参数,它决定了 systemd 如何判断"服务已经启动成功"。选错了 Type,轻则 systemctl start 超时报错,重则服务状态判断错乱、重启策略失效。

2.5.1 五种主要 Type

Type 启动判定 适用场景 典型程序
simple ExecStart 进程启动即视为就绪 前台运行的程序 Go 二进制、Node.js
exec ExecStart 进程成功执行(exec()返回)即就绪 同 simple,但更严格 同 simple
forking 主进程 fork 子进程后退出,子进程接管 传统 daemon Nginx、MySQL
oneshot ExecStart 进程退出后才视为就绪 一次性任务 初始化脚本、数据迁移
notify 进程主动发送 sd_notify 通知就绪 支持 sd_notify 的程序 systemd 自身组件、部分 Go 服务

simple vs exec:simple 是默认值,进程被 fork 出来就算启动成功,哪怕二进制文件路径写错了,systemctl start 也可能返回成功(因为 fork 本身成功了)。exec 更严格,它会等到 exec() 系统调用真正执行成功才算就绪。systemd 256+ 推荐用 exec 替代 simple。

forking 的坑:传统 daemon 程序(如 Nginx 默认配置)启动时会 fork 子进程,父进程退出。用 Type=forking 时,systemd 需要知道哪个是"主进程",通常通过 PIDFile= 指定 PID 文件路径来追踪。如果 PID 文件写入不及时或路径配错,systemd 就会丢失对进程的追踪。

 

# forking 类型的典型配置(Nginx 为例)
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID

 

notify 的优势:这是生产环境最推荐的 Type。进程在完成所有初始化工作(加载配置、连接数据库、预热缓存)之后,主动调用 sd_notify(0, "READY=1") 告诉 systemd "我准备好了"。这样 systemd 对服务状态的判断是最准确的。

 

// Go 程序中使用 sd_notify 的示例
import "github.com/coreos/go-systemd/v22/daemon"

func main() {
    // 初始化工作...
    loadConfig()
    connectDB()
    warmupCache()

    // 通知 systemd 服务就绪
    daemon.SdNotify(false, daemon.SdNotifyReady)

    // 开始服务主循环
    serve()
}

 

2.5.2 选型决策树

 

你的程序启动后会 fork 并退出父进程吗?
├── 是 → Type=forking + PIDFile=
└── 否 → 程序支持 sd_notify 吗?
    ├── 是 → Type=notify(最佳选择)
    └── 否 → 程序是一次性任务吗?
        ├── 是 → Type=oneshot(可选 RemainAfterExit=yes)
        └── 否 → Type=exec(推荐)或 Type=simple

 

2.6 启动依赖管理

服务之间的依赖关系是 systemd 的核心能力之一。这里有两个维度需要区分清楚:启动顺序依赖强度

2.6.1 启动顺序:After / Before

After 和 Before 只控制顺序,不控制依赖。声明 After=postgresql.service 意味着"如果 postgresql 也要启动,那先启动它,再启动我"。但如果 postgresql 根本没有被激活,这条声明不会自动把它拉起来。

 

[Unit]
# 正确:先等网络和数据库就绪,再启动本服务
After=network-online.target postgresql.service redis.service

 

2.6.2 依赖强度:Requires / Wants / BindsTo

指令 强度 行为
Wants= 弱依赖 尝试启动依赖,依赖失败不影响本服务
Requires= 强依赖 依赖启动失败,本服务也不启动
BindsTo= 绑定 依赖停止/重启,本服务也跟着停止/重启
Requisite= 前置断言 依赖必须已经在运行,否则立即失败

生产建议:大多数场景用 Wants= + After= 的组合就够了。Requires= 看起来更"安全",但它有一个副作用——如果被依赖的服务后来挂了,本服务也会被连带停止。这在微服务架构下往往不是你想要的行为。

 

[Unit]
Description=My Web Application
After=network-online.target postgresql.service
# 用 Wants 而不是 Requires,数据库临时不可用时服务自己处理重连
Wants=network-online.target postgresql.service

 

2.6.3 网络依赖的正确写法

这是一个高频踩坑点。很多人写 After=network.target,结果服务启动时网络还没通。原因是 network.target 只表示"网络管理器已启动",不代表网络已经可用。正确的写法:

 

[Unit]
After=network-online.target
Wants=network-online.target

 

同时需要确保 systemd-networkd-wait-online.service 或 NetworkManager-wait-online.service 是启用的,否则 network-online.target 会被立即视为已达成。

2.7 进程管理:重启策略与健康检查

2.7.1 Restart 策略

Restart= 控制进程退出后是否自动重启:

行为
no 不重启(默认值)
on-success 仅在正常退出(exit code 0)时重启
on-failure 非正常退出时重启(非0退出码、被信号杀死、超时、看门狗超时)
on-abnormal 被信号杀死、超时、看门狗超时时重启(不含非0退出码)
on-abort 仅被未捕获信号杀死时重启
always 无论什么原因退出都重启

生产建议:大多数守护进程用 Restart=on-failure。不要无脑用 always——如果程序是正常退出(比如收到 SIGTERM 后优雅关闭),你通常不希望它被自动拉起来。always 适合那些"只要没在跑就是不正常"的核心服务。

2.7.2 重启频率控制

光有 Restart=on-failure 还不够,还需要控制重启的节奏,防止进程反复崩溃导致 CPU 空转:

 

[Service]
Restart=on-failure
RestartSec=5s              # 每次重启前等待 5 秒
RestartSteps=5             # 重启间隔逐步递增的步数(systemd 256+)
RestartMaxDelaySec=60s     # 递增的最大间隔(systemd 256+)
StartLimitIntervalSec=300  # 在 300 秒的窗口内
StartLimitBurst=5          # 最多重启 5 次,超过则放弃

 

systemd 256+ 新增了 RestartSteps 和 RestartMaxDelaySec,可以实现指数退避式重启。第一次重启等 5 秒,第二次等 16 秒,逐步递增到 60 秒封顶。这比固定间隔更合理——如果是瞬时故障,快速重启能尽快恢复;如果是持续性故障,拉长间隔避免雪崩。

2.7.3 超时控制

 

[Service]
TimeoutStartSec=30s    # 启动超时,超过 30 秒未就绪则判定失败
TimeoutStopSec=30s     # 停止超时,超过 30 秒未退出则发 SIGKILL
TimeoutAbortSec=60s    # 收到 abort 信号后的超时(用于生成 core dump)

 

TimeoutStopSec 特别重要。systemctl stop 时,systemd 先发 SIGTERM,等 TimeoutStopSec 秒后如果进程还没退出,就发 SIGKILL 强杀。Java 应用通常需要把这个值调大一些(比如 60s),给 JVM 足够的时间做优雅关闭。

2.7.4 Watchdog 看门狗

Watchdog 是 systemd 提供的进程健康检查机制。服务进程需要定期向 systemd 发送心跳,如果超时没收到,systemd 就认为进程卡死了,按照 Restart 策略处理。

 

[Service]
Type=notify
WatchdogSec=30s            # 每 30 秒需要收到一次心跳
WatchdogSignal=SIGABRT     # 超时后发送的信号(默认 SIGABRT,可生成 core dump)

 

程序端需要配合发送心跳:

 

// Go 程序中发送 watchdog 心跳
import "github.com/coreos/go-systemd/v22/daemon"

func watchdogLoop() {
    interval, _ := daemon.SdWatchdogEnabled(false)
    if interval == 0 {
        return // watchdog 未启用
    }
    ticker := time.NewTicker(interval / 2) // 以一半间隔发送,留足余量
    for range ticker.C {
        daemon.SdNotify(false, daemon.SdNotifyWatchdog)
    }
}

 

Watchdog 解决的是"进程还活着但已经卡死"的问题——进程没崩溃、PID 还在、端口还监听着,但内部死锁了或者陷入无限循环,外部健康检查可能还没来得及发现。Watchdog 从进程内部检测这种状态,比外部探测更及时。

三、资源限制、安全加固与日志管理

3.1 资源限制(cgroup v2)

不做资源限制的服务就是在裸奔。一个内存泄漏的进程可以把整台机器的内存吃光触发 OOM Killer,一个死循环可以把所有 CPU 核心打满。systemd 通过 cgroup v2 提供了细粒度的资源限制能力,直接在 Service 文件里配置就行,不需要手动操作 cgroup 文件系统。

3.1.1 CPU 限制

 

[Service]
# CPU 配额:200% 表示最多使用 2 个核心
CPUQuota=200%

# CPU 权重:默认 100,范围 1-10000
# 只在 CPU 竞争时生效,空闲时不限制
CPUWeight=50

# 绑定到特定 CPU 核心(可选,通常不需要)
AllowedCPUs=0-3

 

CPUQuota 是硬限制,不管 CPU 是否空闲都不会超过这个值。CPUWeight 是软限制,只在多个服务竞争 CPU 时按权重分配,CPU 空闲时不起作用。生产环境建议两个都配:CPUWeight 保证公平调度,CPUQuota 兜底防止单个服务吃满所有核心。

3.1.2 内存限制

 

[Service]
# 内存硬上限:超过直接 OOM Kill
MemoryMax=2G

# 内存软上限:超过后内核会优先回收该 cgroup 的内存
MemoryHigh=1536M

# 最低内存保障:内存紧张时至少保留这么多
MemoryMin=256M

# 禁用 swap(推荐)
MemorySwapMax=0

 

MemoryMax 和 MemoryHigh 的区别很关键。MemoryHigh 是软限制,超过后内核会加大内存回收力度(进程会变慢但不会被杀);MemoryMax 是硬限制,超过直接触发 OOM Kill。生产环境建议 MemoryHigh 设为正常峰值的 120%,MemoryMax 设为 150%,给一个缓冲区间。

3.1.3 IO 限制

 

[Service]
# IO 权重:默认 100,范围 1-10000
IOWeight=50

# 针对特定设备的带宽限制
IOReadBandwidthMax=/dev/sda 50M
IOWriteBandwidthMax=/dev/sda 20M

# IOPS 限制
IOReadIOPSMax=/dev/sda 1000
IOWriteIOPSMax=/dev/sda 500

 

IO 限制在数据库服务和日志密集型服务上特别有用。一个疯狂写日志的服务可以把磁盘 IO 打满,影响同机器上的其他服务。

3.1.4 其他资源限制

 

[Service]
# 最大文件描述符数
LimitNOFILE=65536

# 最大进程/线程数
LimitNPROC=4096

# core dump 大小(0 表示禁用)
LimitCORE=infinity

# 最大打开文件锁数
LimitLOCKS=infinity

# 任务数上限(cgroup 级别,比 NPROC 更准确)
TasksMax=4096

 

LimitNOFILE 是高并发服务的必配项。Linux 默认的 1024 对于任何生产服务都太小了,Nginx、Redis、数据库类服务通常需要 65536 甚至更高。

3.2 安全加固

Systemd 提供了一整套沙箱机制,可以在不修改应用代码的情况下大幅收窄进程的权限范围。这些配置的成本几乎为零,但安全收益很高。

3.2.1 文件系统保护

 

[Service]
# 将 /usr 和 /boot 挂载为只读
ProtectSystem=strict

# 为进程创建独立的 /tmp,与其他进程隔离
PrivateTmp=yes

# 将 /home、/root、/run/user 设为不可访问
ProtectHome=yes

# 只允许读写指定目录
ReadWritePaths=/var/lib/myapp /var/log/myapp
ReadOnlyPaths=/etc/myapp

# 禁止访问指定目录
InaccessiblePaths=/var/lib/mysql

 

ProtectSystem=strict 是最严格的模式,整个文件系统变成只读,只有通过 ReadWritePaths 显式声明的目录才可写。这意味着即使应用被攻破,攻击者也无法篡改系统文件。

3.2.2 权限收窄

 

[Service]
# 禁止获取新的特权(防止 setuid 提权)
NoNewPrivileges=yes

# 以非 root 用户运行
User=myapp
Group=myapp
DynamicUser=yes    # 自动创建临时用户(systemd 256+ 推荐)

# 移除所有 Linux capabilities
CapabilityBoundingSet=
# 如果需要绑定低端口,只给 NET_BIND_SERVICE
# CapabilityBoundingSet=CAP_NET_BIND_SERVICE

# 限制系统调用(白名单模式)
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

 

NoNewPrivileges=yes 是零成本的安全加固,没有任何副作用,所有服务都应该加上。DynamicUser=yes 是 systemd 的一个巧妙设计——它会在服务启动时动态分配一个 UID/GID,服务停止后自动回收,不需要手动创建系统用户。

3.2.3 网络和内核隔离

 

[Service]
# 创建独立的网络命名空间(完全断网)
PrivateNetwork=yes
# 如果需要网络但想限制,用 RestrictAddressFamilies
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

# 禁止加载内核模块
ProtectKernelModules=yes

# 禁止修改内核参数
ProtectKernelTunables=yes

# 禁止访问内核日志
ProtectKernelLogs=yes

# 禁止修改系统时钟
ProtectClock=yes

# 禁止创建设备节点
PrivateDevices=yes

 

3.2.4 一键查看安全评分

systemd 提供了一个内置的安全审计工具,可以对 Service 文件进行安全评分:

 

# 查看某个服务的安全评分
systemd-analyze security myapp.service

# 输出示例(分数越低越安全,10 分最不安全)
# → Overall exposure level for myapp.service: 2.1 OK

 

这个工具会逐项检查所有安全相关的配置,给出评分和改进建议。新写的 Service 文件跑一遍这个命令,把能加的安全配置都加上,目标是控制在 3 分以内。

3.3 日志管理

3.3.1 journald 基础

Systemd 服务的标准输出和标准错误默认会被 journald 捕获。不需要在应用里配置日志文件路径,直接往 stdout/stderr 写就行,journald 会自动加上时间戳、服务名、PID 等元数据。

 

# 查看某个服务的日志
journalctl -u myapp.service

# 实时跟踪日志(类似 tail -f)
journalctl -u myapp.service -f

# 查看最近 1 小时的日志
journalctl -u myapp.service --since "1 hour ago"

# 按优先级过滤(0=emerg 到 7=debug)
journalctl -u myapp.service -p err

# 输出 JSON 格式(方便程序处理)
journalctl -u myapp.service -o json-pretty

 

3.3.2 Service 文件中的日志配置

 

[Service]
# 日志输出目标
StandardOutput=journal
StandardError=journal

# 设置日志标识符(默认是服务名)
SyslogIdentifier=myapp

# 设置日志级别过滤
LogLevelMax=info    # 只记录 info 及以上级别

# 限制日志速率(防止日志风暴)
LogRateLimitIntervalSec=30s
LogRateLimitBurst=10000    # 30 秒内最多 10000 条

 

LogRateLimitIntervalSec 和 LogRateLimitBurst 是生产环境的保命配置。见过太多次应用出 bug 后疯狂打日志,每秒几万条,把磁盘 IO 打满、把 journald 撑爆的情况。

3.3.3 journald 全局配置和日志轮转

编辑 /etc/systemd/journald.conf:

 

[Journal]
# 持久化存储(默认是 volatile,重启丢失)
Storage=persistent

# 磁盘占用上限
SystemMaxUse=2G          # 日志总大小上限
SystemMaxFileSize=128M   # 单个日志文件上限
SystemKeepFree=4G        # 至少保留 4G 磁盘空间

# 运行时(内存中)日志限制
RuntimeMaxUse=256M

# 日志保留时间
MaxRetentionSec=30day

# 压缩
Compress=yes

 

journald 的日志轮转是自动的,不需要像 logrotate 那样配置 cron。当日志总量超过 SystemMaxUse 或单文件超过 SystemMaxFileSize 时,journald 会自动删除最旧的日志。

 

# 手动清理日志
sudo journalctl --vacuum-size=1G    # 只保留 1G
sudo journalctl --vacuum-time=7d    # 只保留 7 天

# 查看日志占用空间
journalctl --disk-usage

 

四、Timer 定时任务

4.1 为什么用 Timer 替代 Cron

Cron 用了几十年,能跑但问题不少:没有日志集成(输出靠邮件或重定向)、没有依赖管理、没有资源限制、错过的任务不会补执行、多实例并发没有保护。Systemd Timer 解决了这些问题,而且和 Service 文件共享同一套管理体系。

特性 Cron Systemd Timer
日志 无(靠重定向) journald 自动记录
依赖管理 支持 After/Requires
资源限制 完整 cgroup 支持
错过补执行 不支持 Persistent=yes
并发保护 天然单实例
随机延迟 RandomizedDelaySec
精度 分钟级 秒级甚至微秒级

4.2 Timer 文件结构

一个 Timer 由两个文件组成:.timer 文件定义触发时间,.service 文件定义要执行的任务。两个文件同名(后缀不同),systemd 自动关联。

 

# /etc/systemd/system/db-backup.timer
[Unit]
Description=Database Backup Timer

[Timer]
# 每天凌晨 2 点执行
OnCalendar=*-*-* 0200

# 如果错过了(比如机器当时关机),开机后补执行
Persistent=yes

# 随机延迟 0-15 分钟,避免多台机器同时执行
RandomizedDelaySec=15min

# 精度(默认 1min,设小一点更准时)
AccuracySec=1s

[Install]
WantedBy=timers.target
# /etc/systemd/system/db-backup.service
[Unit]
Description=Database Backup Job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-db.sh
User=backup
Group=backup

# 资源限制和安全加固同样适用
MemoryMax=512M
CPUQuota=50%
ProtectSystem=strict
PrivateTmp=yes
NoNewPrivileges=yes
ReadWritePaths=/var/backups

 

4.3 OnCalendar 时间表达式

OnCalendar 的语法比 cron 更直观:

 

# 格式:星期 年-月-日 时:分:秒
OnCalendar=Mon..Fri *-*-* 0900    # 工作日每天 9 点
OnCalendar=*-*-* *:00/15:00           # 每 15 分钟
OnCalendar=*-*-01 0000            # 每月 1 号零点
OnCalendar=weekly                      # 每周一零点
OnCalendar=hourly                      # 每小时整点

# 验证时间表达式
systemd-analyze calendar "*-*-* 0200"
# 输出下次触发时间,确认表达式写对了

 

也可以用相对时间触发:

 

[Timer]
# 系统启动 5 分钟后执行
OnBootSec=5min

# 上次执行完成后 30 分钟再执行
OnUnitActiveSec=30min

 

4.4 Timer 管理命令

 

# 启用并启动 timer
sudo systemctl enable --now db-backup.timer

# 查看所有 timer 的状态和下次触发时间
systemctl list-timers --all

# 手动触发一次(不影响定时计划)
sudo systemctl start db-backup.service

# 查看 timer 的执行历史
journalctl -u db-backup.service --since "7 days ago"

 

五、Socket 激活

5.1 什么是 Socket 激活

Socket 激活是 systemd 的一个精巧设计:由 systemd 预先监听端口,当第一个连接请求到达时,再启动对应的服务进程,并把 socket 文件描述符传递给它。服务进程启动后接管 socket,后续请求直接由服务处理。

这个机制带来三个好处:

启动加速:系统启动时不需要等所有服务都起来,端口先占着,请求来了再启动

按需启动:不常用的服务平时不占资源,有请求才拉起来

零停机重启:重启服务时,systemd 继续持有 socket,新连接排队等待,服务重启完成后继续处理,客户端感知不到中断

5.2 Socket 激活配置示例

 

# /etc/systemd/system/myapp.socket
[Unit]
Description=My Application Socket

[Socket]
# 监听地址和端口
ListenStream=0.0.0.0:8080

# 也可以监听 Unix Socket
# ListenStream=/run/myapp/myapp.sock

# 连接队列长度
Backlog=4096

# Socket 文件权限(Unix Socket 时有效)
# SocketMode=0660
# SocketUser=myapp
# SocketGroup=myapp

# 接受连接后传递给哪个服务(默认同名 .service)
# Service=myapp.service

[Install]
WantedBy=sockets.target

 

对应的 Service 文件不需要特殊修改,只要程序能从文件描述符 3 接收 socket 即可。Go 标准库的 net 包、systemd 的 sd_listen_fds() API 都支持这种模式。

 

# 启用 socket 激活(注意:启动的是 .socket 不是 .service)
sudo systemctl enable --now myapp.socket

# 此时 myapp.service 还没启动,但端口已经在监听
ss -tlnp | grep 8080
# 输出:systemd 在监听

# 发送第一个请求,触发 myapp.service 启动
curl http://localhost:8080/health

 

六、生产级 Service 文件完整示例

6.1 Go Web 服务(推荐模板)

这是一个经过生产验证的完整 Service 文件,覆盖了前面讲到的所有关键配置。可以作为模板,根据实际需求增删参数。

 

# /etc/systemd/system/myapp.service
# 生产级 Go Web 服务配置模板

# ============================================================
# [Unit] 元信息和依赖
# ============================================================
[Unit]
Description=My Application API Server
Documentation=https://docs.example.com/myapp
After=network-online.target postgresql.service redis.service
Wants=network-online.target
# 用 Wants 而非 Requires,依赖服务临时不可用时由应用自行处理重连
Wants=postgresql.service redis.service

# 条件检查:配置文件必须存在才启动
ConditionPathExists=/etc/myapp/config.yaml

# ============================================================
# [Service] 核心运行参数
# ============================================================
[Service]
Type=notify
NotifyAccess=main

# --- 启动命令 ---
ExecStartPre=/usr/local/bin/myapp validate --config /etc/myapp/config.yaml
ExecStart=/usr/local/bin/myapp serve --config /etc/myapp/config.yaml
ExecReload=/bin/kill -s HUP $MAINPID

# --- 运行身份 ---
User=myapp
Group=myapp

# --- 工作目录和环境 ---
WorkingDirectory=/var/lib/myapp
EnvironmentFile=-/etc/myapp/env    # 减号表示文件不存在时不报错
Environment=GOMAXPROCS=4
Environment=GIN_MODE=release

# --- 重启策略 ---
Restart=on-failure
RestartSec=5s
RestartSteps=5
RestartMaxDelaySec=60s
StartLimitIntervalSec=300
StartLimitBurst=5

# --- 超时和看门狗 ---
TimeoutStartSec=30s
TimeoutStopSec=60s
WatchdogSec=30s

# --- 资源限制 ---
CPUQuota=200%
CPUWeight=100
MemoryMax=2G
MemoryHigh=1536M
MemorySwapMax=0
TasksMax=4096
LimitNOFILE=65536
LimitNPROC=4096

# --- 安全加固 ---
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectKernelLogs=yes
ProtectClock=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
ReadWritePaths=/var/lib/myapp /var/log/myapp
ReadOnlyPaths=/etc/myapp

# --- 日志 ---
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
LogRateLimitIntervalSec=30s
LogRateLimitBurst=10000

# ============================================================
# [Install] 安装信息
# ============================================================
[Install]
WantedBy=multi-user.target

 

6.2 部署流程

写好 Service 文件后,部署流程如下:

 

# 1. 创建运行用户
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp

# 2. 创建必要目录
sudo mkdir -p /var/lib/myapp /var/log/myapp /etc/myapp
sudo chown myapp:myapp /var/lib/myapp /var/log/myapp

# 3. 部署二进制和配置文件
sudo cp myapp /usr/local/bin/myapp
sudo chmod 755 /usr/local/bin/myapp
sudo cp config.yaml /etc/myapp/config.yaml

# 4. 部署 Service 文件
sudo cp myapp.service /etc/systemd/system/myapp.service

# 5. 重新加载 systemd 配置
sudo systemctl daemon-reload

# 6. 启用并启动服务
sudo systemctl enable --now myapp.service

# 7. 验证服务状态
systemctl status myapp.service
journalctl -u myapp.service -n 50 --no-pager

 

6.3 Java 服务示例

Java 服务和 Go 服务的主要区别在于:JVM 启动慢需要更长的超时时间、内存模型不同需要调整限制策略、不支持 sd_notify 通常用 Type=exec。

 

# /etc/systemd/system/myapp-java.service
[Unit]
Description=My Java Application
After=network-online.target
Wants=network-online.target

[Service]
Type=exec

ExecStart=/usr/bin/java 
    -Xms512m -Xmx1536m 
    -XX:+UseZGC 
    -jar /opt/myapp/myapp.jar 
    --spring.config.location=/etc/myapp/

User=myapp
Group=myapp
WorkingDirectory=/opt/myapp

Restart=on-failure
RestartSec=10s
StartLimitIntervalSec=300
StartLimitBurst=3

# JVM 启动慢,给足时间
TimeoutStartSec=120s
# 优雅关闭需要时间(Spring Boot shutdown hook)
TimeoutStopSec=60s

# 内存限制要考虑 JVM 堆外内存,设为 Xmx 的 1.5 倍左右
MemoryMax=3G
MemoryHigh=2560M
MemorySwapMax=0
CPUQuota=400%
LimitNOFILE=65536
TasksMax=4096

# 安全加固
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ReadWritePaths=/var/lib/myapp /var/log/myapp /tmp

StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp-java

[Install]
WantedBy=multi-user.target

 

Java 服务的 MemoryMax 不能简单等于 -Xmx。JVM 除了堆内存还有 Metaspace、线程栈、直接内存、JIT 编译缓存等堆外开销,实际内存占用通常是 -Xmx 的 1.3 到 1.8 倍。MemoryMax 设太小会导致 JVM 被 OOM Kill,设太大又失去了限制的意义。建议用 -Xmx 的 1.5 倍作为起点,再根据实际监控数据调整。

七、systemctl 常用命令速查

7.1 服务生命周期管理

 

# 启动/停止/重启/重载
sudo systemctl start myapp.service
sudo systemctl stop myapp.service
sudo systemctl restart myapp.service
sudo systemctl reload myapp.service        # 发送 SIGHUP,不中断服务
sudo systemctl reload-or-restart myapp.service  # 支持 reload 就 reload,否则 restart

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

# 彻底屏蔽服务(防止被其他服务拉起)
sudo systemctl mask myapp.service
sudo systemctl unmask myapp.service

 

7.2 状态查看和诊断

 

# 查看服务状态(最常用)
systemctl status myapp.service

# 查看服务是否在运行
systemctl is-active myapp.service

# 查看服务是否启动失败
systemctl is-failed myapp.service

# 查看所有失败的服务
systemctl --failed

# 查看服务的完整配置(合并 drop-in)
systemctl cat myapp.service

# 查看服务的所有属性
systemctl show myapp.service

# 查看某个具体属性
systemctl show myapp.service -p MainPID -p MemoryCurrent -p CPUUsageNSec

 

7.3 配置管理和分析

 

# 修改后重新加载配置(不重启服务)
sudo systemctl daemon-reload

# 用 drop-in 方式修改配置(推荐)
sudo systemctl edit myapp.service
# 编辑完整的 service 文件(不推荐,会覆盖原文件)
sudo systemctl edit --full myapp.service

# 分析启动耗时
systemd-analyze                           # 总启动时间
systemd-analyze blame                     # 各服务启动耗时排序
systemd-analyze critical-chain myapp.service  # 关键路径分析

# 安全审计
systemd-analyze security myapp.service

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

# 查看服务的依赖树
systemctl list-dependencies myapp.service
systemctl list-dependencies --reverse myapp.service  # 反向:谁依赖我

 

7.4 日志查看速查

 

# 查看某个服务的日志
journalctl -u myapp.service

# 实时跟踪(tail -f 模式)
journalctl -u myapp.service -f

# 最近 N 条
journalctl -u myapp.service -n 100

# 时间范围
journalctl -u myapp.service --since "2026-02-06 0000" --until "2026-02-06 1200"
journalctl -u myapp.service --since "30 min ago"

# 按级别过滤
journalctl -u myapp.service -p err      # error 及以上
journalctl -u myapp.service -p warning  # warning 及以上

# 输出格式
journalctl -u myapp.service -o json-pretty  # JSON 格式
journalctl -u myapp.service -o short-iso    # ISO 时间格式

# 查看上一次启动的日志(排查重启前的崩溃原因)
journalctl -u myapp.service -b -1

# 查看内核 OOM Kill 记录
journalctl -k | grep -i "oom|killed"

 

7.5 资源监控

 

# 查看服务的实时资源占用
systemctl status myapp.service
# 输出中包含 Memory: 和 CPU: 行

# 查看 cgroup 级别的详细资源数据
systemctl show myapp.service -p MemoryCurrent -p MemoryPeak -p CPUUsageNSec

# 查看所有服务的资源占用排序
systemd-cgtop

# 查看某个服务的 cgroup 路径
systemctl show myapp.service -p ControlGroup

# 直接查看 cgroup 文件(更详细)
cat /sys/fs/cgroup/system.slice/myapp.service/memory.current
cat /sys/fs/cgroup/system.slice/myapp.service/cpu.stat

 

八、总结

8.1 技术要点回顾

Unit/Target/Slice 是 systemd 的三个核心抽象:Unit 是管理单元,Target 是逻辑分组,Slice 是资源分组

Service Type 选型:优先用 notify(程序支持的话),其次 exec,传统 daemon 用 forking,一次性任务用 oneshot

依赖管理:After/Before 控制顺序,Wants/Requires 控制强度,生产环境优先用 Wants + After 组合

重启策略:Restart=on-failure 覆盖大多数场景,配合 RestartSteps 实现指数退避,用 StartLimitBurst 防止无限重启

Watchdog:解决"进程活着但卡死"的问题,需要程序端配合发送心跳

资源限制:MemoryMax 硬限制兜底,MemoryHigh 软限制缓冲,CPUQuota 防止单服务吃满 CPU

安全加固:NoNewPrivileges=yes 零成本必加,ProtectSystem=strict + ReadWritePaths 最小化文件系统权限

日志管理:用 journald 统一管理,配置 LogRateLimitBurst 防日志风暴,配置 SystemMaxUse 防磁盘写爆

Timer 替代 Cron:日志集成、依赖管理、资源限制、错过补执行,全面优于 cron

Socket 激活:按需启动、零停机重启,适合低频访问或需要平滑重启的服务

8.2 Service 文件编写 Checklist

写完一个 Service 文件后,对照这个清单检查一遍:

 

[ ] Type 选对了吗?程序的启动行为和 Type 匹配吗?
[ ] 依赖关系配了吗?After 和 Wants 写对了吗?
[ ] 用非 root 用户运行了吗?User/Group 配了吗?
[ ] Restart 策略配了吗?RestartSec 和 StartLimitBurst 配了吗?
[ ] 超时时间合理吗?TimeoutStartSec/TimeoutStopSec 够用吗?
[ ] 内存限制配了吗?MemoryMax 和 MemoryHigh 设了合理的值吗?
[ ] CPU 限制配了吗?CPUQuota 设了上限吗?
[ ] LimitNOFILE 够大吗?高并发服务至少 65536
[ ] NoNewPrivileges=yes 加了吗?
[ ] ProtectSystem=strict 加了吗?ReadWritePaths 列全了吗?
[ ] 日志速率限制配了吗?LogRateLimitBurst 设了吗?
[ ] systemd-analyze security 跑过了吗?评分在 3 分以内吗?

 

8.3 进阶学习方向

Service 文件写好只是起点,systemd 的能力远不止于此。以下几个方向在生产环境中有明确的落地价值,值得持续跟进。

1. systemd-nspawn 轻量级容器

systemd-nspawn 可以理解为"systemd 原生的容器运行时"。它不需要 Docker 或 containerd,直接用一个目录树作为根文件系统就能启动一个隔离的 Linux 环境。典型场景是构建环境隔离和遗留应用封装——比如在 RHEL 9 的宿主机上跑一个 CentOS 7 的 nspawn 容器来编译老项目,或者把一个不方便容器化的传统 Java 应用丢进 nspawn 里做资源隔离。machinectl 命令管理 nspawn 实例,systemd-nspawn@.service 模板让它和普通 Service 一样被 systemctl 管理。相比 Docker,nspawn 的优势在于和 systemd 生态的深度集成——日志走 journald、资源限制走 cgroup slice、网络走 systemd-networkd,运维工具链完全统一。

2. Portable Services

Portable Services 是 systemd 240+ 引入的特性,目标是在"传统 Service 文件"和"完整容器化"之间找一个平衡点。它把应用和依赖打包成一个 OS 镜像(通常是 raw 或 squashfs 格式),通过 portablectl attach 挂载到宿主机上,自动生成对应的 Service/Timer 文件。应用运行时共享宿主机内核但使用自己的用户空间库,既解决了依赖冲突问题,又不需要完整的容器编排栈。对于边缘计算节点、嵌入式网关这类资源受限且不适合跑 K8s 的场景,Portable Services 是一个务实的选择。

3. systemd-sysext 和 Composefs

systemd 254+ 的 sysext(System Extensions)机制允许在不可变根文件系统上叠加扩展层,配合 Composefs 实现内容寻址的只读文件系统叠加。这个方向和 Flatcar Container Linux、Fedora CoreOS 等不可变基础设施操作系统密切相关。如果团队在推进不可变基础设施或 GitOps 驱动的节点管理,sysext 是绕不开的技术点。

8.4 参考资料

systemd 官方文档 - 最权威的参数说明

systemd.service(5) - Service 文件完整参数列表

systemd.exec(5) - 执行环境配置(安全加固参数在这里)

systemd.resource-control(5) - 资源限制参数

Arch Wiki - systemd - 社区维护的实用指南

systemd-nspawn(1) - nspawn 容器完整参数说明

Portable Services 文档 - Portable Services 设计文档和使用指南

systemd-sysext(8) - 系统扩展层管理工具

六、总结

6.1 技术要点回顾

回头看整篇文章,有几个核心认知需要钉死:

Unit 文件三段式结构([Unit]/[Service]/[Install])是基础中的基础。[Unit] 管依赖和描述,[Service] 管运行行为,[Install] 管启用方式。写 Service 文件的第一步不是去查参数,而是先把这三个 Section 的职责分清楚。搞混了职责,参数放错 Section,systemd 不会报错但也不会生效,排查起来浪费时间。

Service Type 选择直接决定 systemd 对进程生命周期的判定逻辑。大多数现代应用(Go 二进制、Node.js、Python 脚本)直接用 simple 就够了,进程在前台跑,PID 1 直接追踪。传统 daemon 类程序(比如老版本的 MySQL、Nginx)会 fork 子进程后父进程退出,这种必须用 forking 并配合 PIDFile。一次性初始化脚本(建目录、改权限、跑迁移)用 oneshot,配合 RemainAfterExit=yes 让 systemd 认为服务处于 active 状态。Type 选错了,轻则 systemctl status 显示状态不对,重则 systemd 误判进程已死反复重启。

资源限制和安全加固不是锦上添花,是生产级配置的标配。CPUQuota 防止单个服务吃满所有核心拖垮整机,MemoryMax 在 OOM 之前主动干掉失控进程,ProtectSystem=strict 把根文件系统锁成只读,PrivateTmp=yes 隔离临时目录防止跨服务信息泄露。这些配置的成本几乎为零,但缺了它们,一个失控的服务就能把整台机器拉下水。

Timer 单元完全可以替代 cron,且在所有维度上更优。cron 的问题在于:没有日志集成(输出丢了就是丢了)、没有依赖管理(不能声明"等网络就绪再跑")、没有资源限制(定时脚本跑飞了没人管)、错过的任务不会补执行。systemd Timer 把这些问题全部解决了,Persistent=yes 一个参数就能处理机器关机期间错过的任务,OnCalendar 的语法比 crontab 的五星表达式可读性强得多。2026 年了,新项目没有理由再用 cron。

6.2 进阶学习方向

systemd 的能力边界远不止 Service 文件。以下三个方向在生产环境中有明确的落地价值,值得持续跟进。

1. systemd-nspawn 轻量级容器

systemd 自带的容器运行时,不依赖 Docker 或 containerd,直接用一个目录树作为根文件系统就能拉起隔离环境。典型场景是构建环境隔离和遗留应用封装。和 Docker 相比,nspawn 的核心优势在于和 systemd 生态的深度集成——日志走 journald、资源限制走 cgroup slice、网络走 systemd-networkd,运维工具链完全统一,不需要额外引入一套容器编排体系。对于不需要镜像分发能力、只需要本地隔离的场景,nspawn 比 Docker 更轻量也更省心。

2. Portable Services(可移植服务单元)

systemd 240+ 引入的特性,定位在"裸 Service 文件"和"完整容器化"之间。把应用和依赖打包成 OS 镜像,通过 portablectl attach 挂载到宿主机,自动生成对应的 Service/Timer 文件。应用共享宿主机内核但使用自己的用户空间库,既解决依赖冲突又不需要完整的容器编排栈。对于边缘计算节点、嵌入式网关这类资源受限且不适合跑 K8s 的场景,Portable Services 是一个务实的选择。

3. systemd-homed 用户目录管理

systemd 245+ 引入的用户目录管理方案,把用户的家目录封装成一个可加密、可迁移的独立单元(LUKS 加密镜像或 fscrypt 目录)。用户登录时自动挂载解密,登出时自动卸载锁定。对于多用户共享的开发服务器或需要满足数据加密合规要求的场景,homed 提供了一种比传统 /home + LDAP 更现代的方案。homectl 命令管理用户,用户记录以 JSON 格式存储,支持跨机器迁移。

6.3 参考资料

systemd 官方文档 - 所有 man page 的在线版本,参数说明以这里为准

Lennart Poettering - The systemd for Administrators Blog Series - systemd 作者本人写的系列博客,从设计哲学到具体用法都有覆盖,虽然部分内容写于早期版本,但核心思路至今适用

Arch Wiki - systemd - 社区维护的实用指南,示例丰富,更新及时,遇到具体问题时往往比官方文档更容易找到答案

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分