---
URL: "/zh-CN/blog/2025-09/Systemd-Unit.html"
LLMS_URL: "/zh-CN/blog/2025-09/Systemd-Unit.md"
lastUpdated: true
commentabled: true
recommended: true
title: "玩转 Systemd Unit 文件：进阶技巧与服务覆盖实战"
description: "玩转 Systemd Unit 文件：进阶技巧与服务覆盖实战"
date:

pageClass: "blog-page-class"
cover: "/covers/platform.svg"
---

---
lastUpdated: true
commentabled: true
recommended: true
title: 玩转 Systemd Unit 文件：进阶技巧与服务覆盖实战
description: 玩转 Systemd Unit 文件：进阶技巧与服务覆盖实战
date: 2025-09-18 15:00:00 
pageClass: blog-page-class
cover: /covers/platform.svg
---

## 简介 ##

作为进阶系列的第一篇，本文将聚焦 `Unit` 文件的进阶配置，特别是覆盖机制（`Drop-in`），这些内容在生产环境中非常实用，能帮助你优化服务并提升系统稳定性。如果对 `Systemd` 基础还不熟悉，建议先阅读基础教程。

`Systemd` 的 `Unit` 文件是服务管理的核心，通常位于 `/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`。

```bash
sudo systemctl edit nginx.service
```

编辑器会打开，你写入例如：

```ini
[Service]
Environment="ENV=production"
ExecStartPre=/usr/bin/echo 'starting nginx'
```

保存后，运行：

```bash
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.service` 和 `systemctl 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：

```bash
sudo systemctl edit myapp.service
```

写入 `/etc/systemd/system/myapp.service.d/override.conf`:

```ini
[Service]
Environment="APP_ENV=prod"
EnvironmentFile=/etc/default/myapp  
```

重载并重启：

```bash
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：模板实例化（多个配置文件的守护进程） ###

```bash
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/system` 的 `override`（包括恢复被复制的完整 unit），把 unit 恢复到下层（发行版）版本（具体行为会处理 `drop-in` 和复制覆盖）。

## 调试、验证与回滚流程 ##

### 编辑（用 drop-in） ###

```bash
sudo systemctl edit foo.service
```

### 查看合并后配置 ###

```bash
systemctl cat foo.service
```

该命令会显示原始 `unit` 文件以及所有 `drop-in`，可确认合并结果。

### 重载 systemd 配置（在复制/新增完整 unit 或生成器输出时必须） ###

```bash
sudo systemctl daemon-reload
```

### 重启并观察 ###

```bash
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` 版本）尝试恢复到下层默认。

## 实用命令速查 ##

```bash
# 编辑 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` 目录和配置：

```bash
# 可手动编辑文件，不用 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
```

生效配置

```bash
sudo systemctl daemon-reload
sudo systemctl restart sshd.service
```

## 高级覆盖技巧 ##

### 动态属性覆盖（无需重载） ###

运行时动态修改服务属性：

```bash
# 临时调整服务CPU权重（立即生效）
sudo systemctl set-property nginx.service CPUWeight=300

# 永久调整内存限制
sudo systemctl set-property --runtime nginx.service MemoryMax=2G
```

查看生效配置：

```bash
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
```

## 调试与验证技巧 ##

### 配置合并查看 ###

```bash
# 显示最终生效配置
systemd-analyze cat-unit nginx.service
```

### 覆盖差异对比 ###

```bash
# 比较原始配置与合并后配置
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`、模板单元、优先级与回滚流程，可以在不影响系统升级的前提下对服务进行灵活定制。
