systemd:

systemd 是当前 Linux 系统中主流的初始化系统服务管理器,取代了传统的 SysVinit

核心优势

  • 并行启动: 显著加快系统启动速度,服务可同时启动。
  • 依赖管理: 精确控制服务启动和停止顺序。
  • 统一管理: 为所有系统服务提供统一配置和行为。
  • 进程跟踪: 基于 cgroups,准确跟踪服务所有进程,避免孤儿进程。
  • 按需启动: 服务只在需要时才启动,节省资源。
  • 集中日志 (Journald): 统一收集和查询所有系统日志。

SysVinit 的主要区别

特性SysVinitsystemd
启动串行并行
配置Shell 脚本单元文件 (.service)
日志分散文本Journald (二进制日志)
跟踪基于 PID基于 Cgroups
机制简单初始化集成更多系统组件、功能更强大

systemctl:服务管理核心命令

这是与 systemd 交互的主要工具。

  • sudo systemctl start <服务名>: 启动服务。
  • sudo systemctl stop <服务名>: 停止服务。
  • sudo systemctl restart <服务名>: 重启服务。
  • sudo systemctl reload <服务名>: 重新加载服务配置(不中断服务)。
  • systemctl status <服务名>: 查看服务状态。
  • sudo systemctl enable <服务名>: 设置服务开机自启动。
  • sudo systemctl disable <服务名>: 取消服务开机自启动。
  • sudo systemctl enable --now <服务名>: 立即启用并启动服务。
  • systemctl is-active <服务名>: 检查服务是否活跃。
  • systemctl is-enabled <服务名>: 检查服务是否已启用。
  • sudo systemctl daemon-reload: 在修改单元文件后,必须执行此命令,让 systemd 重新加载配置。
  • sudo systemctl mask <服务名>: 阻止服务启动。
  • sudo systemctl unmask <服务名>: 解除服务阻止。
  • systemctl list-units --type=service: 列出所有服务单元。
  • systemctl list-unit-files --type=service: 列出所有服务单元文件及其状态。

单元文件 (.service)

systemd 的核心配置是单元文件,通常位于 /etc/systemd/system//usr/lib/systemd/system/。它们是描述服务、挂载点、定时器等的配置文件。

基本结构示例:

Ini, TOML

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=我的自定义服务
After=network.target # 依赖关系,在网络启动后启动

[Service]
Type=simple
User=服务用户
ExecStart=/path/to/your/command # 服务的启动命令
Restart=on-failure # 失败时自动重启
RestartSec=5s # 重启间隔

[Install]
WantedBy=multi-user.target # 定义开机时应被哪个目标(运行级别)启用

journalctl:日志查询工具

用于查询 systemd 统一收集的二进制日志。

  • journalctl: 查看所有日志。
  • journalctl -f: 实时查看最新日志。
  • journalctl -u <服务名>: 查看特定服务的日志。
  • journalctl -u <服务名> -f: 实时查看特定服务的日志。
  • journalctl -b: 查看本次启动以来的日志。
  • journalctl --since "1 hour ago": 查看指定时间范围内的日志。
  • journalctl -p err: 只看错误日志。

1. 单元文件 (Unit Files) 详解

单元文件是 systemd 配置的核心,每个单元文件都定义了一个 systemd 可以管理的对象(服务、挂载点、定时器等)。

常见单元文件路径:

  • /usr/lib/systemd/system/: 由软件包安装的默认单元文件。
  • /etc/systemd/system/: 系统管理员自定义或覆盖的单元文件。此路径下的文件优先级高于 /usr/lib/systemd/system/
  • /run/systemd/system/: 运行时生成的单元文件。

单元文件常见节 (Sections):

  • [Unit] 节: 定义单元的通用信息和依赖关系。

    • Description=: 单元的简短描述。
    • Documentation=: 单元的文档链接(man page 或 URL)。
    • After=, Before=: 定义单元的启动/停止顺序。例如,After=network.target 表示此单元在网络服务启动后才启动。
    • Wants=, Requires=: 定义单元的弱/强依赖关系。
      • Wants=: 推荐的依赖方式。如果依赖的单元不存在或启动失败,当前单元仍会尝试启动。
      • Requires=: 强依赖。如果依赖的单元不存在或启动失败,当前单元也会失败。
    • Conflicts=: 定义与此单元冲突的单元。如果冲突单元正在运行,此单元将停止它。
    • PartOf=: 如果此单元是另一个单元的一部分,当主单元停止时,此单元也会停止。
  • [Service] 节 (仅用于 .service 单元): 定义服务的具体行为。

    • Type=: 定义服务进程的启动类型(simple, forking, oneshot, dbus, notify, idle)。
      • simple: 默认,ExecStart 命令是主进程。
      • forking: ExecStart 命令会 fork 出一个子进程作为主进程,父进程退出。
      • oneshot: 命令执行一次后退出,systemd 认为服务已完成。
    • ExecStart=: 启动服务的命令。
    • ExecStop=: 停止服务的命令。
    • ExecReload=: 重新加载服务配置的命令。
    • Restart=: 定义服务崩溃或退出时的重启策略(no, on-success, on-failure, on-abnormal, on-watchdog, on-abort, always)。
    • RestartSec=: 两次重启之间的等待时间。
    • User=, Group=: 运行服务的用户和组。
    • WorkingDirectory=: 服务的工作目录。
    • Environment=, EnvironmentFile=: 设置环境变量。
  • [Install] 节: 定义单元在启用 (systemctl enable) 时的行为。

    • WantedBy=, RequiredBy=: 定义此单元应该被哪个 target 单元“拉起”(即成为其依赖)。例如,WantedBy=multi-user.target 表示当 multi-user.target 启动时,此服务也会被尝试启动。

[Unit]

[Unit] 节定义了 systemd 单元的元数据依赖关系,控制单元启动顺序和与其他单元的交互。

  • Description=简短描述: 单元的人类可读描述,systemctl status 会显示。比如:Description=My Custom Web Application
  • Documentation=文档链接: 单元的文档链接,可以是 man 页或 URL。比如:Documentation=https://example.com/docs/my-app
  • After=单元A 单元B ...: 启动排序。此单元在列出的单元启动之后才启动。这是一种弱排序,即使依赖单元启动失败,也不影响当前单元尝试启动。比如:After=network.target postgresql.service
  • Before=单元A 单元B ...: 启动排序。此单元在列出的单元启动之前启动。同样是弱排序BeforeAfter 互斥。比如:Before=multi-user.target
  • Wants=单元A 单元B ...: 弱依赖。此单元启动时,会尝试启动列出的单元。如果依赖单元不存在或启动失败,不会影响当前单元的启动。主要用于启动触发。比如:Wants=network-online.target
  • Requires=单元A 单元B ...: 强依赖。此单元启动时,会尝试启动列出的单元。如果任何依赖单元不存在、启动失败,或者在当前单元运行期间停止/失败,当前单元也会被停止/失败。用于保证依赖单元的存活。比如:Requires=postgresql.service
  • Conflicts=单元A 单元B ...: 冲突管理。如果列出的单元正在运行,此单元激活时会停止并禁用它们;反之亦然。用于管理互斥的服务。比如:Conflicts=apache2.service
  • PartOf=主单元: 组成部分。此单元是主单元的一部分,当主单元停止或重启时,此单元也会停止或重启。它隐式包含了 After 依赖。比如:PartOf=web-server.target
  • Requisite=单元A 单元B ...: 严格强依赖。如果列出的单元在当前单元启动时未处于活动状态,当前单元将直接失败,而不会尝试启动它们。比 Requires 更严格,用于当依赖必须已经运行时。比如:Requisite=some-critical-pre-existing-condition.service
  • BindsTo=单元A 单元B ...: 绑定依赖。当列出的单元停止时,此单元也会停止。与 Requires 不同的是,如果 BindsTo 的单元在启动时未就绪,此单元不会因此失败。它主要用于绑定到一些生命周期与当前单元紧密相关的资源,例如 mount 单元。比如:BindsTo=var-log-audit.mount

[service]

systemd 单元文件中 [Service] 节(Section)里的指令 (Directives)选项 (Options)

这些都是 systemd 自身定义和理解的配置关键字。它们遵循 key=value 的格式,每个关键字都有特定的含义和作用。你可以在 systemd 的官方文档(man pages)中找到这些指令的详细说明,例如 man systemd.service

[Service] 节中可用的指令非常多,这里列举一些常见的,按功能分类:

1. 进程行为控制
  • Type=: 定义服务的启动类型(例如 simple, forking, oneshot, notify)。
  • ExecStart=: 启动服务的命令行。这是最重要的指令。
  • ExecStop=: 停止服务的命令行。
  • ExecReload=: 重新加载服务配置的命令行(不停止服务)。
  • ExecStartPre=, ExecStartPost=, ExecStopPost=: 在主命令执行之前/之后执行的附加命令。
  • PIDFile=: 如果 Type=forking,指定服务主进程的 PID 文件路径,systemd 会通过此文件跟踪进程。
  • RemainAfterExit=yes: 如果 Type=oneshot,即使 ExecStart 命令退出,systemd 仍认为服务处于“活跃”状态。
2. 用户和权限
  • User=: 运行服务的用户名。
  • Group=: 运行服务的组名。
  • UMask=: 为服务创建的文件设置默认权限掩码。
  • PermissionsStartOnly=yes: 只在 ExecStart 期间应用权限设置。
3. 重启和错误处理
  • Restart=: 定义服务崩溃或退出时的重启策略(例如 on-failure, always, no)。
  • RestartSec=: 两次重启尝试之间的等待时间。
  • TimeoutStartSec=: 服务启动的最大超时时间。
  • TimeoutStopSec=: 服务停止的最大超时时间。
4. 资源限制
  • LimitNOFILE=: 增加或减少服务进程可以打开的最大文件描述符数量。
  • LimitNPROC=: 增加或减少服务进程可以创建的最大进程数量。
  • CPUShares=: CPU 共享权重,影响服务在 CPU 繁忙时的优先级。
  • MemoryLimit=: 限制服务可使用的最大内存量。
  • IOWeight=, BlockIOWeight=: 设置服务 I/O 调度的权重。
5. 环境和目录
  • WorkingDirectory=: 服务的工作目录。
  • Environment=: 设置服务的环境变量(可以直接在单元文件中定义)。
  • EnvironmentFile=: 从指定文件加载环境变量。
6. 安全性
  • ProtectSystem=, ProtectHome=: 限制服务对系统和用户目录的写入权限。
  • ReadWritePaths=, ReadOnlyPaths=: 细粒度控制读写或只读路径。
  • CapabilityBoundingSet=: 限制服务进程的 Linux capabilities(能力)。
  • PrivateTmp=yes: 为服务提供独立的 /tmp/var/tmp 目录,增强隔离性。
  • NoNewPrivileges=yes: 防止服务获取新的权限。

[Install]

控制服务开机自启动,[Install] 节定义了 systemctl enable/disable 命令如何处理单元,主要通过软链接使其在引导时自动启动。

  • WantedBy=目标单元A 目标单元B ...: 最常用。此单元在列出的目标单元启动时被尝试启动(弱依赖)。即使此单元启动失败,也不影响目标单元。常见目标有 multi-user.target (服务器默认)、graphical.target (图形界面) 和 sockets.target (套接字激活服务)。

    • 示例:WantedBy=multi-user.target
  • RequiredBy=目标单元A 目标单元B ...: 强依赖。此单元被列出的目标单元要求。如果此单元启动失败,目标单元也将失败。不如 WantedBy= 常见,用于关键服务。

    • 示例:RequiredBy=some-critical-custom.target
  • Alias=别名A 别名B ...: 为单元创建额外名称。启用单元时,这些别名的软链接也会被创建。用于兼容性或更具描述性的名称。

    • 示例:Alias=my-legacy-app.service
  • Also=单元A 单元B ...: 当当前单元被启用或禁用时,同时启用或禁用列出的其他单元。用于管理一组相关单元。

    • 示例:

      Ini, TOML

      1
      2
      3
      4
      # my-main-app.service
      [Install]
      WantedBy=multi-user.target
      Also=my-worker.service my-db-proxy.service

systemd 单元文件中的指令名称(key 部分)是区分大小写的。**

例如:

  • 你必须写 Group= 而不是 group=GROUP=
  • 你必须写 LimitNOFILE= 而不是 limitnofile=

虽然有些值(比如布尔值 yes/no/true/false,或者时间单位 s/min/h)不区分大小写,但指令本身必须严格按照其在 systemd 文档中定义的大小写来写。否则,systemd 将无法正确识别这些指令,可能会导致服务无法启动或行为异常,通常会报解析错误。

在实际操作中,最好总是复制粘贴官方文档或现有单元文件中的指令名,以避免大小写错误。

2. 特殊的单元类型

除了常见的 .service 单元,systemd 还提供了多种其他单元类型来管理不同的系统资源:

  • .target 单元:
    • 类似于 SysVinit 的**运行级别 (Runlevels)**,但更灵活。一个 target 只是一个分组,用于将多个单元组织在一起,并作为同步点。
    • 常见 targetSysVinit 运行级别对应关系:
      • poweroff.target (runlevel 0): 关机。
      • rescue.target (runlevel 1): 单用户模式。
      • multi-user.target (runlevel 2, 3, 4): 多用户命令行模式。
      • graphical.target (runlevel 5): 多用户图形界面模式。
      • reboot.target (runlevel 6): 重启。
    • 切换 target sudo systemctl isolate <target_name> (例如:sudo systemctl isolate multi-user.target)。
    • 设置默认 target sudo systemctl set-default <target_name> (例如:sudo systemctl set-default graphical.target)。
  • .socket 单元:
    • 用于**套接字激活 (Socket Activation)**。服务只有在有连接请求到达其监听的套接字时才会被启动。这可以减少系统启动时的资源消耗,并提高并行性。
    • 例如,SSH 服务可以配置为套接字激活,只有当有人尝试连接 SSH 端口时,sshd.service 才会被启动。
  • .timer 单元:
    • cron 的替代品,用于定时执行任务。
    • 通常与一个 .service 单元配对使用。.timer 单元定义了何时触发,然后激活对应的 .service 单元。
    • 示例:
      • OnCalendar=*:0/15: 每隔 15 分钟触发一次。
      • OnBootSec=10min: 系统启动 10 分钟后触发。
      • Persistent=true: 如果系统在定时器触发时间关闭,下次启动时会立即执行错过的任务。
  • .path 单元:
    • 用于**路径激活 (Path Activation)**。当特定文件或目录发生变化时,激活对应的 .service 单元。
    • 底层使用 inotify 机制。
  • .slice.scope 单元:
    • 用于**资源管理 (cgroups)**。
    • slice (切片): 用于分层组织和管理一组单元(服务、scope、其他 slice)的资源。它不直接包含进程,而是定义了 cgroup 树中的一个节点,用于分配 CPU、内存等资源。例如,system.slice (系统服务) 和 user.slice (用户会话)。
    • scope (范围): 用于管理 systemd 未直接启动的外部进程组,例如用户登录会话、容器或虚拟机。它们通过 systemd 的 D-Bus API 在运行时注册。

3. systemctl 进阶使用

  • systemctl list-dependencies <单元名>: 查看单元的依赖关系树。
  • systemctl show <单元名>: 显示单元的所有属性和当前状态(非常详细)。
  • systemctl cat <单元名>: 显示单元文件的完整内容,包括所有覆盖文件。
  • systemctl edit <服务名>: 安全地编辑单元文件,它会在 /etc/systemd/system/<服务名>.d/override.conf 创建一个覆盖文件,只修改你指定的选项,而不会直接修改原始文件。
  • systemctl edit --full <服务名>: 复制原始单元文件到 /etc/systemd/system/ 并打开编辑,完全覆盖原始文件。
  • systemctl link <路径/到/你的/单元文件>: 将一个不在标准路径下的单元文件链接到 systemd 可识别的路径,并启用它。
  • systemctl set-environment <变量名>=<值>: 设置全局环境变量(临时)。
  • systemctl unset-environment <变量名>: 取消全局环境变量。

4. journalctl 进阶使用

  • journalctl -kjournalctl --dmesg: 只显示内核日志 (dmesg)。
  • journalctl _PID=<PID>: 查看特定进程 ID 的日志。
  • journalctl _COMM=<进程名>: 查看特定进程名称的日志。
  • journalctl _SYSTEMD_UNIT=<单元名>: 相当于 -u 选项。
  • journalctl -S <日期> / -U <日期>: 查看从指定日期开始/到指定日期为止的日志。
  • journalctl --disk-usage: 查看日志文件占用的磁盘空间。
  • sudo journalctl --vacuum-size=500M: 将日志文件大小限制在 500MB。
  • sudo journalctl --vacuum-time=30d: 删除 30 天以前的日志。

5. 故障排除与诊断

  • systemctl status <服务名>: 始终是诊断服务问题的首选。它会显示服务的当前状态、最近的日志条目和任何错误信息。

  • journalctl -xe: 显示最近的错误和警告日志,并提供详细的解释和建议。

  • systemd-analyze:

    • systemd-analyze: 显示系统启动时间。
    • systemd-analyze blame: 列出启动时耗时最长的单元。
    • systemd-analyze critical-chain: 显示启动关键路径上的单元及其耗时。
    • systemd-analyze dot > boot.dot + dot -Tsvg boot.dot -o boot.svg: 生成启动过程的 SVG 图,可视化依赖关系和耗时。
  • 检查依赖: 如果服务无法启动,使用 systemctl list-dependencies <服务名> 检查其依赖是否已启动或存在问题。

  • 覆盖文件 (Drop-in Files):

    • 当你需要修改一个由软件包提供的单元文件时,强烈建议使用覆盖文件而不是直接修改原始文件

    • /etc/systemd/system/<服务名>.d/ 目录下创建 .conf 文件(例如 override.conf)。

    • 这些文件只包含你想要修改或添加的特定配置项。systemd 会在加载时将这些覆盖文件与原始单元文件合并,且覆盖文件中的设置会优先。

    • 例如,要修改 nginx.service 的重启间隔,可以创建 /etc/systemd/system/nginx.service.d/custom-restart.conf,内容如下:

      Ini, TOML

      1
      2
      [Service]
      RestartSec=10s
    • 修改后,记得运行 sudo systemctl daemon-reload


实例

1. 服务故障自动恢复 (使用 RestartRestartSec)

场景: 你的 Web 应用(比如 Nginx 或自定义的 Python 应用)偶尔会因为内存溢出或代码 Bug 崩溃。你希望它能自动重启,而不是等你发现问题后再手动干预。

systemd 应用:

你可以在服务的单元文件中设置 Restart 策略。例如,对于一个名为 my-webapp.service 的服务:

Ini, TOML

1
2
3
4
# /etc/systemd/system/my-webapp.service.d/restart.conf
[Service]
Restart=on-failure
RestartSec=10s

解释:

  • Restart=on-failure: 这告诉 systemd,如果服务以非零退出代码(通常表示失败)或被信号终止(如 OOM killer 杀死),它应该自动重启。
  • RestartSec=10s: 在每次尝试重启前,systemd 会等待 10 秒,避免服务在崩溃循环中立即占用系统资源。

好处: 大大减少了人工干预,提高了服务的可用性。在夜间或非工作时间,服务也能自行恢复。

2. 精确控制服务启动顺序 (使用 AfterRequires)

场景: 你的应用程序依赖数据库(如 PostgreSQL)和消息队列(如 Redis)。你必须确保这些后端服务完全启动并监听端口后,你的应用程序才能启动。

systemd 应用:

在你的应用程序单元文件 (my-app.service) 中定义依赖关系:

1
2
3
4
5
6
7
8
# /etc/systemd/system/my-app.service
[Unit]
Description=My Awesome Application
After=postgresql.service redis.service
Requires=postgresql.service redis.service # 使用 Requires 更强地保证依赖

[Service]
# ... (其他配置,如 ExecStart 等)

解释:

  • After=postgresql.service redis.service: 确保 my-app.service 会在 postgresql.serviceredis.service 都启动之后才尝试启动。
  • Requires=postgresql.service redis.service: 这是一个更强的依赖。如果 postgresql.serviceredis.service 无法启动,my-app.service 也不会启动。如果它们在 my-app.service 运行期间停止,my-app.service 也将被停止。

好处: 避免了应用程序在依赖的服务未就绪时启动失败,简化了启动流程的复杂性。

3. 按需启动服务 (使用 .socket 单元)

场景: 你有一些不常用的内部服务(例如一个管理工具或一个很少被访问的 Webhook 服务),它们不需要一直运行,只在有请求时才启动。

systemd 应用:

为服务创建一个 .socket 单元,并将其与 .service 单元关联。

  1. 创建 .socket 单元 (/etc/systemd/system/my-lazy-service.socket):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [Unit]
    Description=My Lazy Service Socket

    [Socket]
    ListenStream=8081 # 监听 TCP 端口 8081
    Accept=yes # 为每个连接启动一个服务实例

    [Install]
    WantedBy=sockets.target
  2. 创建 .service 单元 (/etc/systemd/system/my-lazy-service.service):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [Unit]
    Description=My Lazy Service
    Requires=my-lazy-service.socket # 依赖 socket 单元
    After=my-lazy-service.socket

    [Service]
    ExecStart=/usr/bin/python3 /opt/my-lazy-service/app.py # 你的服务启动命令
    StandardInput=socket # 服务从 socket 接收输入
    Type=simple
  3. 启用并启动 socket 单元:

    1
    2
    3
    sudo systemctl enable my-lazy-service.socket
    sudo systemctl start my-lazy-service.socket
    sudo systemctl daemon-reload # 每次创建或修改单元文件后都要执行

解释: my-lazy-service.service 不会直接启动。当有客户端连接到服务器的 8081 端口时,systemd 会自动启动 my-lazy-service.service 来处理这个连接。服务处理完后可以退出,释放资源。

好处: 节省系统资源,尤其对于大量不常使用的服务,可以显著降低内存和 CPU 占用。

4. 解决单元文件冲突 (使用覆盖文件 *.d/ 目录)

场景: 你想修改 Nginx 服务的启动用户或资源限制,但 Nginx 是通过软件包安装的,直接修改 /usr/lib/systemd/system/nginx.service 文件可能会在下次软件包更新时被覆盖。

systemd 应用:

使用覆盖目录 (.d/) 来进行修改,这是一种非侵入性的、推荐的方式。

  1. 创建覆盖目录:

    1
    sudo mkdir -p /etc/systemd/system/nginx.service.d
  2. 创建覆盖文件 (/etc/systemd/system/nginx.service.d/custom-user.conf):

    1
    2
    3
    4
    [Service]
    User=mynginxuser
    Group=mynginxuser
    LimitNOFILE=65535 # 增加文件句柄限制
  3. 重新加载 systemd 配置并重启服务:

    1
    2
    sudo systemctl daemon-reload
    sudo systemctl restart nginx.service

解释: systemd 会将 /etc/systemd/system/nginx.service.d/ 目录下的所有 .conf 文件内容与原始的 nginx.service 文件合并。如果存在相同的配置项,覆盖文件中的设置会优先。这样,你的自定义配置就不会在软件包更新时丢失。

好处: 维护自定义配置的稳定性和持久性,避免了软件包更新带来的麻烦。

5. 高效日志管理 (使用 journalctl)

场景: 你的服务器上运行着数十个服务,当出现问题时,你需要快速定位是哪个服务出了问题,或者查看特定时间段内的所有错误。

systemd 应用:

journalctl 是你的日志神器:

  • 查看最近的错误信息,并提供上下文和建议:

    1
    journalctl -xe
  • 查看 Nginx 服务在过去 30 分钟内的所有日志:

    1
    journalctl -u nginx.service --since "30 minutes ago"
  • 实时跟踪 Web 应用日志,同时过滤掉不重要的 debug 信息:

    1
    journalctl -u my-webapp.service -f -p info..err # 只看 info、warning、error 等级别的日志
  • 检查上次启动后哪个服务启动时间最长:

    1
    systemd-analyze blame

好处: journalctl 提供了强大的过滤、时间查询和实时跟踪功能,大大简化了日志分析和故障排查的过程,特别是面对大量服务和高并发日志时。