玩转 Systemd Unit 文件:进阶技巧与服务覆盖实战

简介

作为进阶系列的第一篇,本文将聚焦 Unit 文件的进阶配置,特别是覆盖机制(Drop-in),这些内容在生产环境中非常实用,能帮助你优化服务并提升系统稳定性。如果对 Systemd 基础还不熟悉,建议先阅读基础教程。

SystemdUnit 文件是服务管理的核心,通常位于 /lib/systemd/system/(系统默认)或 /etc/systemd/system/(自定义)。进阶配置允许你不修改原文件,而是通过覆盖方式扩展功能,避免升级时覆盖问题。接下来,我们一步步深入。

为什么不要直接修改 /usr/lib / /lib 下的 unit 文件

发行版把 package 自带的 unit 文件放在 /usr/lib/systemd/system/lib/systemd/system(不同发行版路径可能不同),这些文件在包升级时可能被覆盖。正确做法是把修改放在 /etc/systemd/system(或者用 drop-in),这样系统升级不会覆盖你的改动。系统会按优先级选取最终生效的配置(管理员目录优先)。

unit 文件的查找与优先级

systemd 会从多个目录加载 unit 文件与 drop-in,优先级通常是:

  • /etc/systemd/system(管理员/本地覆盖,最高)

  • /run/systemd/system(运行时生成的 unit

  • /usr/lib/systemd/system/lib/systemd/system(发行版自带,最低)

drop-in<unit>.d/*.conf)也会被加载并按目录顺序合并,/etc 下的 drop-in 优先于 /run、再优于 /usr/lib。这意味着可以只写小段配置覆盖原设置而不必复制整个文件。

Drop-in 的工作原理

  • Drop-in 文件位于 /etc/systemd/system/<unit-name>.d/(例如 /etc/systemd/system/nginx.service.d/)。

  • 每个 Drop-in 文件(如 override.conf)可以覆盖特定节(如 [Service])的指令。

  • Systemd 会合并原始 Unit 和所有 Drop-in 文件,按字母顺序加载(最后加载的优先)。

覆盖(Override)与 Drop-in 的实战

推荐的两种修改方式(按优先级)

  • 最推荐(安全):用 systemctl edit <unit>,它会在

/etc/systemd/system/<unit>.d/override.conf 创建或编辑 drop-in

shell 复制代码
sudo systemctl edit nginx.service

编辑器会打开,你写入例如:

ini 复制代码
[Service]
Environment="ENV=production"
ExecStartPre=/usr/bin/echo 'starting nginx'

保存后,运行:

shell 复制代码
sudo systemctl daemon-reload
sudo systemctl restart nginx

这样不会改动发行版文件,且便于回滚或审计。

  • 需要完整替换:如果确实要完全替换一个 unit(例如深度修改 ExecStart 的语义),可把它整文件复制到 /etc/systemd/system/,编辑后 daemon-reload。但更常见也更安全的还是用 drop-in

覆盖行为要点

  • drop-in 中只写你要改变或添加的节/键;systemd 会把主文件和所有 drop-in 合并来生成最终配置。

  • 同一个 unit 的多个 drop-in(不同文件名)会按字典序合并,后面的可覆盖前面的。

模板单元(@ 单元)与实例化

什么是模板单元

模板单元用于同一套 unit 定义不同实例。例如 mydaemon@.service,你可以 systemctl start mydaemon@work.servicesystemctl start mydaemon@home.service 各自成为独立实例。

示例 mydaemon@.service(放 /etc/systemd/system/mydaemon@.service):

ini 复制代码
[Unit]
Description=My daemon instance %i

[Service]
ExecStart=/usr/local/bin/mydaemon --config /etc/mydaemon/%i.conf
Restart=on-failure

[Install]
WantedBy=multi-user.target

这里 %i 会被替换为实例名(work / home 等),通常用于文件名或参数。用 template 可以让多个实例共享相同 unit 定义。

常见定制场景与实例

场景 A:只需设环境变量或修改一个参数

使用 drop-in:

shell 复制代码
sudo systemctl edit myapp.service
# 写入 /etc/systemd/system/myapp.service.d/override.conf:
[Service]
Environment="APP_ENV=prod"
EnvironmentFile=/etc/default/myapp  

重载并重启:

shell 复制代码
sudo systemctl daemon-reload
sudo systemctl restart myapp

场景 B:替换 ExecStart(需要先清除原 ExecStart)

如果主文件有 ExecStart,在 drop-in 中直接写另一个 ExecStart= 会被追加而非替换。要先清空原来的,再写新值:

ini 复制代码
[Service]
ExecStart=
ExecStart=/usr/local/bin/myapp --serve

空行 ExecStart= 是清除旧值的标准方法(适用于累加型字段)。

添加后置命令:使用 ExecStartPost= 执行启动后的操作,如日志记录

ini 复制代码
[Service]
ExecStartPost=/bin/echo "Nginx started at $(date)" >> /var/log/nginx-start.log

场景 C:模板实例化(多个配置文件的守护进程)

shell 复制代码
sudo systemctl start mydaemon@work.service
sudo systemctl enable mydaemon@work.service

enable / disable / mask / revert 等操作说明

  • systemctl enable <unit>:创建启动时的 symlink(一般放在 /etc/systemd/system/<target>.wants/),使其随 target 启动。

  • systemctl disable <unit>:删除这些 symlink

  • systemctl mask <unit>:把 unit 链接到 /dev/null,用于阻止启动(包括依赖触发)------用于强制禁止服务。

  • systemctl unmask <unit>:解除 mask。

  • systemctl revert <unit>:撤销所有在 /etc/systemd/systemoverride(包括恢复被复制的完整 unit),把 unit 恢复到下层(发行版)版本(具体行为会处理 drop-in 和复制覆盖)。

调试、验证与回滚流程

编辑(用 drop-in):

shell 复制代码
sudo systemctl edit foo.service

查看合并后配置:

shell 复制代码
systemctl cat foo.service

该命令会显示原始 unit 文件以及所有 drop-in,可确认合并结果。

重载 systemd 配置(在复制/新增完整 unit 或生成器输出时必须):

shell 复制代码
sudo systemctl daemon-reload

重启并观察:

shell 复制代码
sudo systemctl restart foo.service
sudo systemctl status foo.service
sudo journalctl -u foo.service -n 200 --no-pager

若不满足,回滚方法:

如果用了 systemctl edit(drop-in),可用 sudo systemctl edit --full foo.service 或直接删除 /etc/systemd/system/foo.service.d/override.conf,然后 daemon-reload

  • 如果复制了完整 unit/etc/systemd/system,删除该文件并 daemon-reload

  • 另外,可用 systemctl revert foo.service(视 systemd 版本)尝试恢复到下层默认。

实用命令速查

shell 复制代码
# 编辑 drop-in(推荐)
sudo systemctl edit foo.service

# 查看合并后的 unit(主文件 + drop-ins)
systemctl cat foo.service

# 列出 unit 的依赖树
systemctl list-dependencies foo.service

# 重载 systemd 配置(新增/修改完整 unit 时必要)
sudo systemctl daemon-reload

# 启动/重启/查看状态
sudo systemctl start foo
sudo systemctl restart foo
sudo systemctl status foo

# 查看日志
sudo journalctl -u foo -f

# 探查当前有效 unit 文件来源
systemctl status foo.service  # 输出里会显示 Loaded: 路径和 drop-in

实战案例:从需求到 Unit 文件定制

案例 1:定制 Python 应用服务(完整 Unit 文件)

需求:部署一个 Python API 服务,需设置启动前环境检查、后台运行、异常重启、日志输出到文件,且随系统启动。

创建 /etc/systemd/system/api.service

ini 复制代码
[Unit]
Description=Python API Service
Documentation=https://example.com/api-docs
# 依赖网络和数据库
After=network-online.target mysql.service
Wants=mysql.service  # 弱依赖数据库(数据库未启动仅警告)
# 仅在配置文件存在时启动
ConditionPathExists=/opt/api/config.yaml

[Service]
# 服务运行用户(避免用 root)
User=appuser
Group=appuser
# 启动命令(前台运行,Type=simple)
ExecStart=/usr/bin/python3 /opt/api/main.py
# 启动前检查 Python 环境
ExecStartPre=/usr/bin/python3 -c "import sys; sys.exit(0)"
# 启动前创建日志目录
ExecStartPre=-/bin/mkdir -p /var/log/api
# 标准输出和错误重定向到日志文件
StandardOutput=append:/var/log/api/access.log
StandardError=append:/var/log/api/error.log
# 异常退出时重启,间隔 3 秒
Restart=on-failure
RestartSec=3
# 资源限制:最多 512MB 内存,20% CPU
MemoryLimit=512M
CPUQuota=20%

[Install]
WantedBy=multi-user.target

案例 2:用 drop-in 调整 SSH 服务端口和登录限制

需求:默认 SSH 服务端口为 22,需修改为 2222,且禁止 root 直接登录(不修改系统默认 sshd.service)。

  • 创建 drop-in 目录和配置:
shell 复制代码
可手动编辑文件,不用 edit 也行
sudo mkdir -p /etc/systemd/system/sshd.service.d
sudo vim /etc/systemd/system/sshd.service.d/port.conf
  • 添加配置(覆盖 ExecStart 启动参数):
ini 复制代码
[Service]
# 原系统默认 ExecStart=/usr/sbin/sshd -D $OPTIONS
# 修改为端口 2222,禁止 root 登录
ExecStart=
ExecStart=/usr/sbin/sshd -D -p 2222 -o PermitRootLogin=no
  • 生效配置
shell 复制代码
sudo systemctl daemon-reload
sudo systemctl restart sshd.service

高级覆盖技巧

动态属性覆盖(无需重载)

运行时动态修改服务属性:

shell 复制代码
# 临时调整服务CPU权重(立即生效)
sudo systemctl set-property nginx.service CPUWeight=300

# 永久调整内存限制
sudo systemctl set-property --runtime nginx.service MemoryMax=2G

查看生效配置:

shell 复制代码
systemctl show nginx.service | grep -E 'CPUWeight|MemoryMax'

条件化覆盖

根据环境变量动态调整配置:

ini 复制代码
# /etc/systemd/system/redis.service.d/10-env-override.conf
[Service]
EnvironmentFile=-/etc/default/redis
ExecStart=/usr/bin/redis-server /etc/redis/%i.conf $REDIS_OPTIONS

调试与验证技巧

配置合并查看

shell 复制代码
# 显示最终生效配置
systemd-analyze cat-unit nginx.service

覆盖差异对比

shell 复制代码
# 比较原始配置与合并后配置
diff <(systemd-analyze cat-unit nginx.service) \
     <(cat /usr/lib/systemd/system/nginx.service)

最佳实践与常见问题

最佳实践

  • 优先使用 drop-in 片段:除非必须完全重写服务逻辑,否则尽量用 drop-in 增量修改,减少升级时的配置冲突。

  • 避免直接修改系统 Unit 文件:/usr/lib/systemd/system/ 下的文件由包管理维护,修改会被升级覆盖。

  • 命名规范:drop-in 目录严格遵循 <服务名>.service.d,配置文件以 .conf 结尾(如 custom.conf)。

  • 及时重载配置:修改 Unit 文件或 drop-in 后,必须执行 systemctl daemon-reload 使配置生效。

  • 备份配置:自定义 Unit 文件或 drop-in 片段建议纳入版本控制(如 Git),便于迁移和回滚。

常见问题与排查

  • 配置不生效?

    • 检查是否执行 systemctl daemon-reload

    • systemctl cat <服务名> 确认配置是否被正确合并。

    • 查看服务日志排查错误:journalctl -u <服务名> -e

  • drop-in 片段未覆盖参数?

    • 确认 drop-in 目录和文件命名正确(如 nginx.service.d/custom.conf)。

    • 部分参数(如 Description)在 [Unit] 区块,需在 drop-in 中显式重写该区块的参数。

  • 服务启动失败,提示 "依赖未满足"?

    • 检查 [Unit] 区块的 Requires/After 参数,确保依赖服务存在且能正常启动(可用 systemctl is-active <依赖服务> 检查)。

结尾:小结与预告

通过掌握 drop-in/override、模板单元、优先级与回滚流程,可以在不影响系统升级的前提下对服务进行灵活定制。

下篇文章将聚焦 "cgroups v2 与资源控制实战",敬请期待!

参考资源:

Systemd 官方文档:systemd.io/ Arch Wiki:wiki.archlinux.org/title/syste... Man 页:man systemd.service

欢迎在评论区分享你的 Systemd 配置经验,或提出问题。你的反馈是我继续创作的动力!

相关推荐
maosheng11461 小时前
RHCSA的第一次作业
linux·运维·服务器
wifi chicken2 小时前
Linux 端口扫描及拓展
linux·端口扫描·网络攻击
旺仔.2912 小时前
Linux 信号详解
linux·运维·网络
放飞梦想C2 小时前
CPU Cache
linux·cache
Hoshino.413 小时前
基于Linux中的数据库操作——下载与安装(1)
linux·运维·数据库
播播资源5 小时前
CentOS系统 + 宝塔面板 部署 OpenClaw源码开发版完整教程
linux·运维·centos
源远流长jerry5 小时前
在 Ubuntu 22.04 上配置 Soft-RoCE 并运行 RDMA 测试程序
linux·服务器·网络·tcp/ip·ubuntu·架构·ip
lay_liu5 小时前
Linux安装redis
linux·运维·redis
寂柒7 小时前
序列化与反序列化
linux·网络
lay_liu7 小时前
ubuntu 安装 Redis
linux·redis·ubuntu