一、概述
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 - 社区维护的实用指南,示例丰富,更新及时,遇到具体问题时往往比官方文档更容易找到答案
全部0条评论
快来发表一下你的评论吧 !