前言

本文中,你将了解如何使用systemd配置Linux服务在重启或崩溃后自启动。

第一部分包含一般的Linux服务管理概念,像是init守护进程和runlevels。它最终是以systemd的服务管理来实现的。这里你将了解targets,wants,requires以及unit文件。

第二部分提供了逐步的教程来实现一个常见的systemd任务。特别是,你将配置一个MySQL数据库服务器来自动在崩溃或重启后自启动。

前提条件

为了完成本文的内容,你需要:

  • 一个CentOS8服务器,包含有sudo权限的非root用户。
  • 安装了MySQL。

    介绍服务管理守护进程

    Linux服务可以通过改变服务管理守护进程的处理方式来进行自动回复,也就是init守护进程。

init是在Linux系统启动以及内核加载到内存之后的第一个进程。除了其他的事项之外,它还决定了用户进程或者系统服务应该如何加载,以何种顺序,以及是否应该自动启动。

随着Linux的发展,init守护进程的行为也在变化。最开始,Linux使用SystemV的init来进行启动,与Unix使用的一致。之后,Linux就实现了Upstart的init守护进程(由Ubuntu实现),现在是systemd的init守护进程(首先Fedora实现)。

大部分现代的Linux版本都逐渐从System V移植到目前的systemd。保留旧的init是为了向后兼容。FreeBSD是Unix的变种,使用一种不同的System V的实现,即BSD init。

在本文中我们将涵盖systemd,因为它是Linux版本中最现代也是最常用的服务管理器。然而,必要时我们也会讨论System V以及Upstart,看看systemd是如何发展至此的。下面是它们的大致介绍:

  • System V是最老的init系统,用于
    • Debian 6及以前
    • Ubuntu 9.04及以前
    • CentOS 5及以前
  • Upstart在System V之后,用于
    • Ubuntu 9.10到Ubuntu 14.10,包含Ubuntu 14.04
    • CentOS 6
  • systemd是最新的Linux服务管理器,用于
    • Debian 7及以后
    • Ubuntu 15.04及以后
    • CentOS 7及以后

为了理解init守护进程,先看看一个叫runlevel的概念。

Runlevels

一个runlevel代表当前Linux系统的状态。例如,runlevel可以是Linux服务器的关闭状态,单用户模式,重启模式等。每个模式都指导着什么服务可以在该状态中运行。

一些服务可以运行在一个或更多的runlevel而不能在其他的里面运行。runlevel由0到6的数值来表示。下面展示了这些等级都代表了什么:

  • runlevel 0:系统关机
  • runlevel 1:单用户,救援模式
  • runlevel 2, 3, 4:多用户,文本模式,启用网络
  • runlevel 5:多用户,启用网络,图形模式
  • runlevel 6:系统重启

runlevel 2,3,4根据发布版本不同而不同。例如,一些Linux版本没有实现runlevel 4,而其他的有实现。一些版本在这三个等级之间有着清晰的划分。通常,runlevel 2,3,4意味着Linux启动了多用户,启用网络,文本模式的状态。

当你启用服务自启动,Linux实际上做的就是将它添加到一个runlevel中。例如,在System V中,系统会以一个特殊的runlevel启动;当启动的时候,它会尝试启动所有与该runlevel相关的服务。在systemd中,runlevel变成了target,在服务被指定为自启动的时候,就会被添加到target中。之后将讨论target相关内容。

介绍System V的init守护进程

System V使用inittab文件,之后init方法也保留着进行向后兼容。我们先看看SystemV的启动序列:

  1. init守护进程从二进制文件/sbin/init中创建
  2. init守护进程第一个读取的文件是/etc/inittab
  3. 该文件的入口之一是决定机器以什么runlevel来启动。例如,如果runlevel的值为3,那么linux将会启动为多用户的文本模式,启用网络。(这也是默认的runlevel)
  4. 下面,init守护进程会进一步读取inittab文件,读取init脚本在该runlevel下需要的东西。

因此,当init守护进程发现init脚本在该runlevel下需要什么的时候,也就是发现了它需要启动什么服务。这些init脚本就是你可以为单独服务配置启动行为的地方。

一个init脚本是在System V中控制特定服务的。它要么是由应用提供商提供,要么由Linux提供(本地服务)。你也可以创建自己的init脚本来设置自定义服务。

当一个进程或者服务(比如MySQL服务器)在System V下启动,它的二进制程序文件被加载到内存中。根据服务的配置方式,该程序可能持续在后台运行(接收客户端请求)。该二进制程序的启动,停止,重新加载都是由服务的init脚本控制的。

在System V中,init脚本就是shell脚本。也被称作rc(run command)脚本。脚本位于/etc/init.d文件夹中。这些脚本是/etc/rc目录的超链接。在/etc文件夹中,有许多rc目录,每一个在名称中都有一个数字。数字代表着不同的runlevel。因此我们就有了/etc/rc0.d,/etc/rc1.d,/etc/rc2.d等等。

为了使服务在崩溃或者系统重启后重启,你通常可以添加一行代码到init脚本:

$ ms:2345:respawn:/bin/sh /usr/bin/service_name

为了开启SystemV服务用于在系统启动时启动,运行下面的命令

$ sudo chkconfig service_name on

如果要禁用,运行该命令

$ sudo service service_name status

介绍Upstart守护进程

随着System V序列化加载任务和服务的时间变得更久,更加复杂,Upstart守护进程可以进行操作系统的更快加载,更优雅地清理崩溃服务,以及系统服务之间的可预测依赖。

Upstart的init比System V的更好,有以下几个方面:

  • 它没有处理复杂难懂的shell脚本来加载和管理服务,而是使用简单地配置文件进行,容易理解和修改。
  • 服务没有如System V那样序列加载,这样可以减少启动时间。
  • 使用灵活的事件系统,来自定义服务如何在不同的状态下处理。
  • Upstart有更好的关于崩溃服务重启的方式。
  • 不需要保持大量的冗余超链接,所有都指向一个脚本。

为了保证一切简单,Upstart与System V向后兼容。/etc/init.d/rc脚本还是可以运行来管理本地System V服务。主要的区别是一个服务允许关联不同的事件。这种基于事件的架构允许Upstart成为一个灵活的服务管理器。使用Upstart,每个事件可以触发一个处理该事件的shell脚本。这些事件包含:

  • 启动中
  • 已启动
  • 停止中
  • 已停止

在这些事件中,一个服务可以有许多状态,像是等待中,预启动,启动中,运行中,预停止,停止中等。Upstart可以也可以为这些每一个状态做出行为,创建一个非常灵活的架构。

在启动中,Upstart将会正常地运行任何System V的init脚本。然后会查看/etc/init文件夹,在每个服务配置文件中执行shell命令。这些文件控制了服务的启动行为。

文件的命名样式为service_name.conf,其中有不同部分的纯文本内容,称作节。每一节描述服务的不同方面以及行为如何。为了使服务在崩溃或重启之后自动启动,你可以在服务配置文件中添加respawn命令,如下的cron服务所示。

  1. ...
  2. description "regular background program processing daemon"
  3. start on runlevel [2345]
  4. stop on runlevel [!2345]
  5. expect fork
  6. **respawn**
  7. exec cron

介绍systemd守护进程

最新的Linux的init守护进程是systemd。事实上,它不仅仅是一个init守护进程了:systemd是一个包含了现代Linux系统地许多组件的框架。

其功能之一就是作为一个Linux的系统和服务管理器。在这方面,systemd控制了服务在崩溃或机器重启后行为如何。

systemd向后兼容System V命令和初始化脚本。也就意味着任何System V服务也能在systemd下运行。这样可以实现是因为大部分Upstart和System V的管理命令已经被修改为可以在systemd下面运行的版本。

systemd配置文件:单元文件

systemd的核心就是单元文件。每个单元文件都代表了特定的系统资源。资源的信息在单元文件中保持跟踪。服务单元文件只是带有声明式语法的简单的文本文件(像是Upstart的.conf文件)。这可以让文件容易理解和修改。

systemd与其他两个init方法的主要不同点是systemd负责服务守护进程的初始化,以及其他类型的资源例如设备操作系统了UI那个,挂载点,串口等。单元文件的命名样式为service_name.unit_type。因此,你会看到这样的文件,dbus.service,sshd.socket,或者home.mount。

文件夹结构

在基于Red Hat的系统(如CentOS)中,单元文件位于两个位置。主要的位置是/lib/systemd/system/。自定义的单元文件或者系统管理员修改的已有的单元文件位于/etc/systemd/system下。

如果一个同样名称的单元文件位于两个路径下,systemd将会使用/etc下面的那个。假设一个服务可以在系统启动时或者其他target/runlevel下启动。这样的话,就会在/etc/systemd/system下的合适位置创建该服务单元文件的符号链接。/etc/systemd/system下的单元文件实际上是/lib/systemd/system的同一文件的符号链接。

systemd的init序列:目标单元Target Units

一类特殊的单元文件类型是目标单元。

一个目标单元文件的文件名的后缀是.target。目标单元不同于其他单元文件,因为它们不代表一个特定的资源。它们代表的是系统在任何时间点的状态。目标单元通过分子并且启动多个单元文件来做到。systemd的target因此可以松散地与System V的runlevel相比,尽管实际上并不相同。

每个target都有一个名称而非数字。例如,我们有multi-user.target而非runlevel 3或者reboot.target而非runlevel 6。但linux服务器使用multi-user.target启动,相当于给了服务器runlevel 2, 3或者4,意味着多用户文本模式带网络。

如何启动服务是不同之处所在。与System V不同,systemd没有序列化启动服务。它可以检查其他服务或资源的存在,以决定以何种加载方式。这可以让并行加载服务称为可能。

另一个目标单元与runlevel之间的不同是System V中,Linux系统只可以存在于一个runlevel中。你可以修改runlevel,但是系统只会存在于新的runlevel中。使用systemd,目标单元可以是包含的,意味着当目标单元激活的时候,可以确保其他的目标单元加载为其部分之一。

例如,一个Linux系统启动了graphical.target的图形界面,也自动确定加载了multi-user.target,并且激活。在System V中,相当于同时启动了runlevel 3和5。

下面的表格将runlevel和target做了对比。

Runlevel(System V) Target Units(Systemd)
runlevel 0 poweroff.target
runlevel 1 rescue.target
runlevel 2, 3, 4 multi-user.target
runlevel 5 graphical.target
runlevel 6 reboot.target

systemd default.target

default.target等同于System V的默认runlevel。

System V默认的runlevel定义在一个叫做inittab的文件中。在systemd中,该文件替换为default.target。默认的目标单元文件位于/etc/systemd/system文件夹下。它是/lib/systemd/system中目标单元文件的符号链接。

当你修改默认的target时,本质上你是在重新创建该符号链接并且修改系统地runlevel。

System V中的inittab文件也指定了Linux要从哪个文件夹执行init脚本:可以是任何一个rcn.d文件夹。在systemd中,默认的目标单元决定了什么资源单元会在启动时加载。

随着单元被激活,它们所有以并行或者以序列的方式进行激活。大哥资源单元如何加载可能依赖于其他资源单元want或者requires的。

systemd依赖:Wants和Requires

systemd的wants和requires控制着systemd如何描述服务守护进程之间的依赖。

如此前所说,Upstart使用配置文件确保了服务的并行加载。在System V中,服务可以以特定的runlevel启动,但是也可以等待另一个服务或者资源可用之后启动。类似地,systemd服务可以加载到一个或多个target中,或者等待其他服务或者资源激活。

在systemd中,一个单元需要另一个单元加载激活来启动。如果需要的单元由于某些原因在第一个单元激活的情况下启动失败,第一个单元也会停止。

这样就确保了系统的稳定性。一个requires特定目录的服务因此也可以等待该目录的挂载点激活再开启。另一方面,一个wants另一个单元的单元不会强制这样的约束。当调用者激活时,如果想要的单元停止,它也不会停止。一个例子就是在图形模式中没必要启动的服务。

示例:理解systemd启动序列

为了理解systemd下的服务启动行为,我们使用了CentOS 8.3 Droplet。我们将尽可能使用使用.target的systemd启动序列以及一长串的依赖。

首先,运行该命令来列出默认的target单元文件。

  1. $ sudo ls -l /etc/systemd/system/default.target

输出如下:

  1. lrwxrwxrwx. 1 root root 37 Dec 4 17:42 /etc/systemd/system/default.target -> /lib/systemd/system/multi-user.target

如你所见,默认的目标实际上是/lib/systemd/system/下的多用户目标文件的符号链接。因此,系统应该启动multi-user.target,类似于System V中的runlevel 3。

multi-user.targets.wants

下一步,运行下面的命令来检查所有multi-user.target文件的wants:

  1. $ sudo ls -l /etc/systemd/system/multi-user.target.wants/*.service

这应该会显示一个符号链接文件的列表,指向实际的位于/usr/lib/systemd/system下面的单元文件。

  1. lrwxrwxrwx. 1 root root 38 Dec 4 17:38 /etc/systemd/system/multi-user.target.wants/auditd.service -> /usr/lib/systemd/system/auditd.service
  2. lrwxrwxrwx. 1 root root 39 Dec 4 17:39 /etc/systemd/system/multi-user.target.wants/chronyd.service -> /usr/lib/systemd/system/chronyd.service
  3. lrwxrwxrwx. 1 root root 37 Dec 4 17:38 /etc/systemd/system/multi-user.target.wants/crond.service -> /usr/lib/systemd/system/crond.service
  4. lrwxrwxrwx. 1 root root 42 Dec 4 17:39 /etc/systemd/system/multi-user.target.wants/irqbalance.service -> /usr/lib/systemd/system/irqbalance.service
  5. lrwxrwxrwx. 1 root root 37 Dec 4 17:41 /etc/systemd/system/multi-user.target.wants/kdump.service -> /usr/lib/systemd/system/kdump.service
  6. ...

除了multi-user.target之外,还有不同类型的目标,例如system-update.target或者basic.target。为了查看multi-user.target依赖什么目标,运行下面的命令:

  1. $ sudo systemctl show --property "Requires" multi-user.target | fmt -10

输出如下:

  1. Requires=basic.target

basic.target

运行下面的命令查看basic.target需要什么单元。

  1. $ sudo systemctl show --property "Requires" basic.target | fmt -10

运行后可以看到basic.target需要的是sysinit.target

  1. Requires=sysinit.target
  2. -.mount

而且它也想要几个目标

  1. $ sudo systemctl show --property "Wants" basic.target | fmt -10

输出如下:

  1. Wants=slices.target
  2. paths.target
  3. timers.target
  4. microcode.service
  5. sockets.target
  6. sysinit.target

递归往下,你可以看到sysinit.target需要其他的目标进行运行

  1. $ sudo systemctl show --property "Requires" sysinit.target | fmt -10

这即将显示none。然而,sysinit.target有其他想要的目标:

  1. $ systemctl show --property "Wants" sysinit.target | fmt -10

输出如下:

  1. Wants=systemd-random-seed.service
  2. dev-mqueue.mount
  3. rngd.service
  4. systemd-modules-load.service
  5. proc-sys-fs-binfmt_misc.automount
  6. local-fs.target
  7. sys-fs-fuse-connections.mount
  8. systemd-sysusers.service
  9. systemd-update-done.service
  10. systemd-update-utmp.service
  11. systemd-journal-flush.service
  12. dev-hugepages.mount
  13. dracut-shutdown.service
  14. swap.target
  15. systemd-udevd.service
  16. import-state.service
  17. sys-kernel-debug.mount
  18. nis-domainname.service
  19. systemd-journald.service
  20. selinux-autorelabel-mark.service
  21. kmod-static-nodes.service
  22. loadmodules.service
  23. ldconfig.service
  24. cryptsetup.target
  25. systemd-sysctl.service
  26. systemd-ask-password-console.path
  27. systemd-journal-catalog-update.service
  28. systemd-udev-trigger.service
  29. systemd-tmpfiles-setup.service
  30. systemd-hwdb-update.service
  31. sys-kernel-config.mount
  32. systemd-binfmt.service
  33. systemd-tmpfiles-setup-dev.service
  34. systemd-machine-id-commit.service
  35. systemd-firstboot.service

分析systemd单元文件

再进一步,看看一个sshd服务的单元文件

  1. $ sudo vi /etc/systemd/system/multi-user.target.wants/sshd.service

其内容如下:

  1. [Unit]
  2. Description=OpenSSH server daemon
  3. Documentation=man:sshd(8) max:sshd_config(5)
  4. After=network.target sshd-keygen.target
  5. Wants=sshd-keygen.target
  6. [Service]
  7. Type=notify
  8. EnvironmentFile=-/etc/crypto-policies/back-ends/opensshserver.config
  9. EnvironmentFile=-/etc/sysconfig/sshd
  10. ExecStart=/usr/sbin/sshd -D $OPTIONS $CRYPTO_POLICY
  11. ExecReload=/bin/kill -HUP $MAINPID
  12. KillMode=process
  13. Restart=on-failure
  14. RestartSec=42s
  15. [Install]
  16. WantedBy=multi-user.target

你可以看到服务单元文件是干净的,且容易理解。

第一个重要的部分是[Unit]中的After部分。意思是sshd服务需要在network.target和sshd-keygen.target之后加载。

[Install]部分显示了multi-user.target想要的服务。这意味着multi-user.target将会加载sshd守护进程,但是如果sshd在加载过程中失败也不会关闭或者崩溃。

因为multi-user.target时默认的目标,sshd应该在系统启动时启动。在[Service]部分中,Restart参数是on-failure。这个设置允许sshd守护进程在崩溃或者异常退出之后重启。

使用systemd配置MySQL在系统重启后自启动

在MySQL安装的情况下,检查服务的状态

  1. $ sudo systemctl status mysqld.service

输出显示服务正在运行,但是守护进程禁用了。

  1. mysqld.service - MySQL 8.0 database server
  2. Loaded: loaded (/usr/lib/systemd/system/mysqld.service; disabled; vendor preset: disabled)
  3. Active: active (running) since Thu 2020-12-24 23:48:56 UTC; 1h 6min ago
  4. Process: 30423 ExecStartPost=/usr/libexec/mysql-check-upgrade (code=exited, status=0/SUCCESS)
  5. Process: 30294 ExecStartPre=/usr/libexec/mysql-prepare-db-dir mysqld.service (code=exited, status=0/SUCCESS)
  6. Process: 30270 ExecStartPre=/usr/libexec/mysql-check-socket (code=exited, status=0/SUCCESS)
  7. Main PID: 30378 (mysqld)
  8. Status: "Server is operational"
  9. Tasks: 40 (limit: 4763)
  10. ...

如果服务启用了,禁用掉它。首先我们想在做修改之前探索禁用掉的行为。

$ sudo systemctl disable mysqld.service

然后,运行该命令来检查MySQL是否是multi-user.target想要的。

$ sudo systemctl show —property “Wants” multi-user.target | fmt -10 | grep mysql

没有返回的内容。现在检查符号链接是否存在。

$ sudo ls -l /etc/systemd/system/multi-user.target.wants/mysql*

会出现一条符号链接文件不存在的消息

  1. ls: cannot access '/etc/systemd/system/multi-user.target.wants/mysql*': No such file or directory

现在,如果你喜欢的话,重启服务器并检查MySQL服务。它是不会运行的。

不管你是否重启服务器,现在重新启用MySQL服务。

$ sudo systemctl enable mysqld.service

这一次,系统将在/etc/systemd/system/multi-user.target.wants/下面创建一个符号链接。

  1. Created symlink /etc/systemd/system/multi-user.target.wants/mysqld.service /usr/lib/systemd/system/mysqld.service.

再次运行ls命令并确认。

$ sudo ls -l /etc/systemd/system/multi-user.target.wants/mysql*

你可以看到下面的输出

  1. lrwxrwxrwx 1 root root 38 Aug 1 04:43 /etc/systemd/system/multi-user.target.wants/mysqld.service -> /usr/lib/systemd/system/mysqld.service