跳转至

服务与日志管理

现代的 Linux 发行版都使用 systemd 来管理系统服务,因此本文主要介绍 systemd 环境下的服务与日志管理。

早期(2014 年以前)还有 SysVinit 和 Upstart 等,但现在已经很少见了。SysVinit 还有一个现代化的替代品,叫做 OpenRC。

Init

Init 进程是 Linux 启动时运行的第一个进程,负责启动系统的各种服务并最终启动 shell。传统的 init 程序位于 /sbin/init,而现代发行版中它一般是指向 /lib/systemd/systemd 的软链接,即由 systemd 作为 PID 1 运行。

PID 1 在 Linux 中有一些特殊的地位:

  • 不受 SIGKILLSIGSTOP 信号影响,不能被杀死或暂停。类似地,即使收到了其他未注册的信号,默认行为也是忽略,而不是结束进程或挂起
  • 当其他进程退出时,这些进程的子进程会由 PID 1 接管,因此 PID 1 需要负责回收(wait(2))这些僵尸进程。

alt text

Review for Fork

记得上计算机系统导论(CSAPP)的时候,讲到 fork 系统调用,其中有一句话:Linux机器中所有的进程都是由一个最开始的进程fork而成的。在这里可得知,这个“初始进程”就是 PID 1.

Systemd 与服务

Systemd 是一大坨软件,包括服务管理(PID 1)、日志管理(systemd-journald)、网络管理(systemd-networkd)、本地 DNS 缓存(systemd-resolved)、时间同步(systemd-timesyncd)等,本文主要关心服务管理和日志管理。

Unit

在 systemd 中,运行一个完整系统所需的每个部件都作为“单元”(unit)管理。一个 unit 可以是服务(.service)、挂载点(.mount)、设备(.device)、定时器(.timer)以至于目标(.target)等,完整的列表可以在 systemd.unit(5) 中找到。

systemd.unit(5)

alt text

Systemd unit 的配置文件主要从以下目录按顺序载入,其中同名的文件只取找到的第一个:

  • /etc/systemd/system本地配置文件,优先级最高,这也是==唯一==一个==管理员可以手动修改文件==的地方。
  • /run/systemd/system运行时目录,存放由 systemd 或其他程序动态创建的 unit。注意== /run 目录重启后会被清空==。
  • /usr/lib/systemd/system系统配置文件,优先级最低,一般==由发行版(软件包管理器)==提供。

实际会搜索的目录比这多得多(又到了看 man 的时候了),但是一般只需要关心上面这三个

How to use man

Man! What can I see? 👋🏀

man 1 - User Commands (用户命令)

  • 描述
    • 这一部分包含用户 可直接在终端中运行的命令和可执行文件 的文档。主要是用户在日常使用中可能需要的各种工具和命令。
  • 示例
    • man 1 ls - 查看 ls 命令的手册页。
    • man 1 grep - 查看 grep 命令的手册页。

man 2 - System Calls (系统调用)

  • 描述
    • 这一部分涵盖内核提供的系统调用。系统调用是程序与操作系统内核交互时所使用的接口,如 文件操作、进程管理、内存管理 等。
  • 示例
    • man 2 open - 查看 open 系统调用的手册页,描述如何打开文件。
    • man 2 read - 查看 read 系统调用的手册页,描述如何从文件或设备读取数据。

man 3 - Library Functions (库函数)

  • 描述
    • 这一部分描述了C标准库和其他库中的函数,通常包括 常用的库函数 如字符串操作、数学函数、内存管理等。这部分是C/C++程序员在编写代码时常用的参考。
  • 示例
    • man 3 printf - 查看 printf 函数的手册页,描述如何格式化和打印输出。
    • man 3 malloc - 查看 malloc 函数的手册页,描述如何分配内存。

man 4 - Special Files (特殊文件)

  • 描述
    • 这一部分描述了 设备文件(通常位于 /dev 目录下)和驱动程序 的手册页。这些文件通常代表系统中的硬件设备,如硬盘、终端等。
  • 示例
    • man 4 tty - 查看 tty 设备文件的手册页,描述终端设备。
    • man 4 random - 查看 /dev/random 设备的手册页,描述如何生成伪随机数。

man 5 - File Formats and Conventions (文件格式和约定)

  • 描述
    • 这一部分的手册页描述了各种 文件格式、配置文件和系统中约定俗成的文件结构。系统管理员和开发人员通常会参考这一部分来配置服务或解析文件格式。
  • 示例
    • man 5 passwd - 解释 /etc/passwd 文件的格式,该文件包含用户账号信息。
    • man 5 fstab - 解释 /etc/fstab 文件的格式,用于定义文件系统的挂载方式。

事实上,man还是太冗长了,我还是更喜欢tldr一点😄

很多通过 systemctl 命令改变的配置都会被保存到 /etc/systemd/system 目录下,例如:

  • systemctl enable [some-unit] 本质上是在 /etc/systemd/system 目录下创建软链接。
  • systemctl disable [some-unit] 则是删除上面创建的软链接。
  • systemctl edit [some-unit] 会提供一个临时文件,并在编辑完之后将其保存到 /etc/systemd/system/[some-unit].d/override.conf 文件中,实现对 unit 的修改。

相比于手工修改文件,使用 systemctl 更加安全,它会检查配置文件的语法,而且不需要再额外运行 systemctl daemon-reload

软链接

软链接(Symbolic Link,简称软链或符号链接)是文件系统中的一种特殊类型的文件,它指向另一个文件或目录。软链接类似于 Windows 系统中的快捷方式。以下是软链接的一些特点和用途:

软链接的特点

  1. 指向文件或目录:软链接实际上是一个指向另一个文件或目录的路径,它本身不包含文件数据。
  2. 跨文件系统:软链接可以指向不同文件系统中的文件或目录。
  3. 大小固定:软链接文件的大小通常很小,只包含目标文件的路径信息。
  4. 断裂风险:如果软链接指向的文件被删除或移动,软链接就会失效,称为“断裂”链接。

可以简单直观地理解为:只包含指向节点的指针

创建软链接

使用 ln -s 命令来创建软链接,格式如下:

Bash
1
ln -s /path/to/target /path/to/link_name

例如,假设你在 /etc/systemd/system 中创建了一个指向 /lib/systemd/system/some-unit.service 的软链接:

Bash
1
sudo ln -s /lib/systemd/system/some-unit.service /etc/systemd/system/some-unit.service

这实际上就是 systemctl enable some-unit 的背后原理,它创建了一个软链接,使该服务在系统启动时自动启动。

软链接与硬链接的区别

  • 软链接:指向目标文件的路径,可以指向目录,可以跨文件系统,目标文件删除后,软链接会失效
  • 硬链接:指向目标文件的 inode(文件系统中的数据结构),共享相同的数据存储位置,目标文件删除后,硬链接仍然有效(文件数据不会被删除,直到所有硬链接被删除)。

软链接在管理系统服务、配置文件、版本控制等场景中非常有用,因为它们提供了灵活的文件引用方式。

Unix配置文件格式

Unit 的配置文件是一个 INI 格式的文件,通常包括一个 [Unit] section,然后根据 unit 的类型不同有不同的 section。

例如一个服务的配置文件会有 [Service] section,并通常会包含一个 [Install] section。以 cron 服务的配置文件为例:

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[Unit]
Description=Regular background program processing daemon
Documentation=man:cron(8)
After=remote-fs.target nss-user-lookup.target

[Service]
EnvironmentFile=-/etc/default/cron
ExecStart=/usr/sbin/cron -f -P $EXTRA_OPTS
IgnoreSIGPIPE=false
KillMode=process
Restart=on-failure

[Install]
WantedBy=multi-user.target

INI 格式是一种简单的配置文件格式,广泛用于应用程序的配置。它由一系列的节(sections)和键值对(key-value pairs)组成。INI 文件通常用于存储程序的配置信息,格式直观易懂。

INI 格式的基本结构

  1. 节(Sections):节用方括号 [] 括起来,例如 [SectionName]。每个节包含一组相关的配置项。
  2. 键值对(Key-Value Pairs):在节内,每行包含一个键值对,格式为 key=value

文件后缀

INI 格式的配置文件通常没有固定的后缀名,但常见的后缀包括 .ini.conf。在 systemd 中,配置文件通常位于 /etc/systemd/system//lib/systemd/system/ 目录下,文件后缀为 .service.target.socket 等。

例如:

  • cron.service:表示 cron 服务的配置文件。
  • network.target:表示网络目标的配置文件。

使用 INI 格式的优势

  • 易读性:INI 格式简单直观,易于人工编辑和理解。
  • 结构化:通过节和键值对组织配置项,使得配置文件结构清晰。

顺序与依赖

相比于 SysVinit(完全顺序启动)和 upstart(基于 event 触发的方式有限的并行),systemd 的每个 unit 都明确指定了依赖关系,分析依赖关系后 systemd 就可以最大化并行启动服务,这样可以大大缩短启动时间。

Systemd 中的 unit 有很多状态,大致可以归为以下几类:

  • inactive:未启动
  • activating:正在启动
  • active:已启动(成功)
  • deactivating:正在停止
  • failed:启动失败

大部分系统 unit 都会使用以下几个字段:

1)Wants=Requires= (依赖)

指定 unit 之间的依赖关系,例如网络服务通常会依赖 network.target,即当网络开始配置时才会运行。 两者都在 [Unit] section 中指定,区别在于 Requires= 是强依赖,即如果被依赖的 unit 没有启动或启动失败,那么当前 unit 也会被标记为失败; 而 Wants= 是弱依赖,即尝试启动被依赖的 unit,但如果失败了也不会影响当前 unit 的启动。

使用场景:

  • Requires= 适用于必须在目标单元成功启动后才能运行的服务。例如,数据库服务可能需要网络服务在它之前启动,因此使用 Requires=network.target 确保网络服务成功运行
  • Wants= 适用于不强制要求其他单元必须成功启动的服务。例如,某些服务可能希望网络服务启动但不是必须的,所以使用 Wants=network.target,即使网络服务启动失败,也不会影响主服务的启动
为什么使用 Wants=

Wants= 的设计目的是为了提供灵活性,使得某些服务可以在系统启动时尽可能地尝试启动它们依赖的服务,但不强制要求这些依赖服务必须成功启动。

假设有一个服务 A,它希望网络服务 B 能够启动以实现某些功能,但即使网络服务 B 启动失败,服务 A 仍然可以运行。你可以在 A.service 文件中使用 Wants=B.service 来实现这一点。

INI
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Unit]
Description=Service A
Wants=network.service
After=network.service

[Service]
ExecStart=/usr/bin/service-a

[Install]
WantedBy=multi-user.target

2)WantedBy=RequiredBy= (反向依赖)

与上面两个相反,指定了其他 unit 依赖当前 unit。 这两个字段在 [Install] section 中指定,并且仅当对应的 unit 被启用(systemctl enable)时才会生效。

3)Before=After=

指定启动顺序,即相关的 unit 需要在前者启动完成,进入 active 状态后才会尝试启动。这两个字段在 [Unit] section 中指定。 与 Wants/Requires 不同,Before/After 只是指定启动顺序,不影响依赖关系。

Target

Target 是一组服务(其他 unit)的集合,通过 target 这样一层抽象可以更方便地管理服务的启动顺序,类似 SysVinit 中的 runlevel,可以理解为“系统启动目标”。

例如网络服务应该 Requires=network-online.target 并且 After=network-online.target,这样就可以保证网络服务在网络连通后再启动。

Systemd 在开机时会尝试启动 default.target,这个 target 一般是指向 graphical.target 的软链接,即启动图形界面相关的服务,另一个常见的 multi-user.target 则是命令行模式。

默认的 target 可以通过 systemctl set-default 命令修改,或者在 GRUB 中为 kernel cmdline 指定 systemd.unit=。 与其他 systemctl 命令一样,前者的本质是创建一个软链接 /etc/systemd/system/default.target。

Service

Service 也就是我们最常见的服务,它的配置文件中有一个 [Service] section,包括了服务的启动命令和一系列其他配置。

Tips

通常情况下我们 建议对命令使用绝对路径,因为 systemd 启动服务时并不会使用系统配置的 $PATH 环境变量,而是使用一个硬编码的列表。

1) ExecStartPre=ExecStartPost=ExecStopPost=

服务启动前、启动后、停止后 执行的命令。可用于检查服务的配置文件是否正确、创建临时文件、清理临时文件等。例如 ssh.service 就会使用 ExecStartPre=/usr/sbin/sshd -t 来检查配置文件是否正确。

2) ExecReload=

指定 重载服务 的命令,一个常见的做法是 ExecReload=/bin/kill -HUP $MAINPID。 配置了 ExecReload= 之后即可使用 systemctl reload [service] 命令来向服务的主进程发送 SIGHUP 信号。一些服务还有自己的 reload 命令,例如 nginx 的 ExecReload=/usr/sbin/nginx -s reload

3) Type=

指定 服务类型。大部分服务都由一个在后台运行的进程组成,此时可以省略 Type 使用默认值 simple,或者更推荐的做法是 Type=exec。其他的服务类型参见下面的 Service Type 一节。

User=Group=SupplementaryGroups=

指定 运行服务的用户和组,以及额外的附加组。默认情况下服务会以 root 用户运行,如果有安全和权限管理的需求,那么你应该配置这几项设置。

Service Type

simpleexec

是最常见的服务类型,服务主体是一个长期运行的进程。 两者的区别在于 simple 类型“启动即成功”,即 systemctl start 会立刻成功退出; 而 exec 类型会确保 ExecStart= 命令可以正常运行,包括 User=Group= 存在、所指定的命令存在且可执行等。

因此现代的服务应该尽量使用 Type=exec。

forking

一些传统的服务会使用这种方式,启动命令会 fork 出一个子进程然后退出,实际服务由这个子进程提供。 这种服务需要配置 PIDFile=,以便 systemd 能够正确追踪服务的主进程。当 PIDFile= 指定的文件存在且包含一个有效的 PID 时,systemd 认为服务已经启动成功。

oneshot

一次性服务,即启动后运行一次 ExecStart= 命令,然后退出。 这个 Type 有两种使用场景:

  1. 一次性的初始化或者清理工作、或者改变系统状态的命令等(如一些 ip 命令);
  2. 和 timer 配合使用,即定时任务。

如果你有 Type=oneshot 的服务,那么你很可能也想配置 RemainAfterExit=yes,这样配置的命令执行完成后会一直保持 active 状态。

notifydbus

类似 simple 和 exec,但是服务会在启动完成后主动通知 systemd。 与前面的类型不同的是,这类服务需要程序主动支持 sd_notify 或者 D-Bus 接口。

定时任务

Systemd 提供了 timer 类型的 unit,用于定时执行任务。一个 timer unit 通常会对应一个 service unit,即在指定的时间点或者时间间隔触发 service 的启动。

CRON

CRON 是一种在类 Unix 操作系统中用于定期执行任务的工具。它基于时间的调度器,允许用户安排在指定时间间隔内自动执行命令或脚本。

  1. CRON 表(crontab)

    • 用途:存储定时任务的配置。
    • 位置:系统级的 CRON 表通常位于 /etc/crontab,用户级的 CRON 表通过 crontab -e 命令编辑。
    • 格式
      Text Only
      1
      * * * * * /path/to/command
      
      这行的格式是:
      • 分钟(0-59)
      • 小时(0-23)
      • 日期(1-31)
      • 月份(1-12)
      • 星期几(0-6,0 是星期日)
      • 要执行的命令
  2. 示例

    • 每天午夜运行备份脚本:
      Text Only
      1
      0 0 * * * /usr/local/bin/backup.sh
      
    • 每小时运行系统更新检查:
      Text Only
      1
      0 * * * * /usr/local/bin/update-check.sh
      
Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
1    # /etc/crontab: system-wide crontab
2    # Unlike any other crontab you don't have to run the `crontab'
3    # command to install the new version when you edit this file
4    # and files in /etc/cron.d. These files also have username fields,
5    # that none of the other crontabs do.
6   7    SHELL=/bin/sh
8    # You can also override PATH, but by default, newer versions inherit it from the environment
9    #PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
10  11   # Example of job definition:
12   # .---------------- minute (0 - 59)
13   # |  .------------- hour (0 - 23)
14   # |  |  .---------- day of month (1 - 31)
15   # |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
16   # |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
17   # |  |  |  |  |
18   # *  *  *  *  * user-name command to be executed

相比于更常见的定时任务方案 CRON,systemd timers 具有以下优点:

  1. 更丰富的时间表达式,除了等价于 crontab 的 OnCalendar= 时间之外,也可以使用 OnUnitActiveSec=(服务启动后)、OnBootSec=(系统启动后)等指定其他时间计算方式

    • 例如,systemd-tmpfiles-clean.timer 就是在系统启动后 15 分钟触发一次 systemd-tmpfiles-clean.service,然后每天触发一次,用于清理临时文件。
      Bash
      1
      2
      3
      [Timer]
      OnBootSec=15min
      OnUnitActiveSec=1d
      
  2. AccuracySec= 可以支持秒级甚至更细的时间精度。 一般不推荐小于 1 分钟的时间精度,否则系统计时器需要频繁唤醒,可能会影响系统性能。

  3. RandomizedDelaySec= 可以配置每次触发时随机延迟的时间,避免大量服务在同一时间点启动。 这在使用同一份系统镜像部署大量虚拟机或类似场景下非常有用,可以避免大量计划任务同时触发,导致系统负载过高。
  4. Persistent= 可以确保如果因关机、重启等原因错过了设定时间,定时任务会在下次系统启动后会立即执行。
  5. 可以通过 systemctl enablesystemctl disable 启用和禁用定时任务,而无需修改配置文件。 也可以使用 systemctl status 查看 timerservice 的状态,以及 journalctl 查看日志。

创建一个定时任务

如上所述,一个定时任务包含两个文件,一个是 timer unit,一个是对应的 service unit。下面以 certbot 的配置文件为例,说明如何创建一个定时任务。

首先创建一个 service,需要注意的是 Type=oneshot,并且不能使用 RemainAfterExit=yes(一般将其忽略即可,它的默认值是 no)。

Bash
1
2
3
4
5
6
7
8
9
[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://certbot.eff.org/docs

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true

接下来创建一个 timer,指定触发时间,并按需启用 Persistent=

Bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Unit]
Description=Run certbot twice daily

[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true

[Install]
WantedBy=timers.target

此时 certbot.timer 会自动触发同名的 service,也就是 certbot.service

在编辑完两个文件之后,需要运行 systemctl daemon-reload 使 systemd 重新加载配置文件,然后可以使用 systemctl start certbot.timer 启动定时器,或者使用 systemctl enable certbot.timer 让其开机启动。