在系列教程的第一篇里我们已经提到了Systemd,它主要的设计目标是克服传统Linux主流启动程序SysVinit 固有的缺点,提高系统的启动速度。相比同类的 SysVinit 竞争者,例如Ubuntu 的 upstart,Systemd 的设计更加前卫,简单来说,它的设计思路借鉴了Mac系统的启动程序Launchd。事实上Systemd的作用远不仅是启动系统,它还接管了系统服务的启动、结束、状态查询和日志归档等职责,并支持定时任务和通过特定事件(如插入特定USB设备)和特定端口数据触发的任务。在CoreOS的世界里,推荐的做法是使用Systemd来管理所有用户服务,包括运行在应用容器(如Docker)中的服务。
值得指出的是,Systemd并不是CoreOS特有的服务。本质上说Systemd是没有依附于任何一个Linux发行版的独立项目,由于Systemd的作者Lennart Poettering 就职于红帽,整个项目实际由RedHat公司主导。虽然RedHat Linux直到2014年中旬才用上Systemd,但RedHat旗下的Fedora早在2011年时就已经引进了Systemd作为其启动管理程序了。
在开始使用Systemd之前,先了解一下Systemd有哪些特别之处。
尽可能启动更少进程
当SysVinit 程序初始化系统的时,会将所有可能用到的后台服务进程全部运行起来。然而用户需要等待系统将所有服务都启动完成之后,才能够登录。这种做法会带来两个问题:系统的启动时间过长和系统资源的浪费。
Systemd 提供了服务按需启动的能力,使得特定的服务只有在被真正请求的时候才启动。特别是具体硬件相关的服务,比如蓝牙服务仅在蓝牙适配器被插入时才需要运行,打印服务仅在打印机连接或程序要打印时才需要运行,甚至sshd服务也只需要在用户使用ssh连接到服务器时才需要启动。这种能力是建立在对Systemd对DBus总线或特定Socket端口监听的特性上的,这种设计相比于传统启动程序具有颠覆性的进步。
尽可能将更多进程并行启动
在SysVinit的时代,将每个服务项目编号的方式依次执行启动脚本。后来Ubuntu的Upstart解决了没有直接依赖的启动项之间的并行启动。而Systemd通过Socket缓存、DBus缓存和建立临时挂载点等方法进一步解决了启动进程之间的依赖,做到了所有系统服务并发启动,这一设计同样是Systemd独具特色的创意。当然,对于用户自定义的服务,Systemd允许配置其启动依赖项目,从而确保服务按必要的顺序运行,稍后会详细描述具体的使用方法。
Systemd启动模型与其它启动模型的对比
采用 Cgroup 跟踪和管理进程的生命周期
Cgroup的全称是controller group,是将任意进程进行分组化管理的Linux内核功能,最初由Google的工程师提出,从Linux内核版本2.6.24正式启用。拿Android来说,它的应用程序隔离就是使用的这种技术。而很长一段时间里,在更广阔的服务器领域,一直并没有一种主流的服务管理程序能够充分利用这种早已在手机端带来广泛好处的特性。
而Systemd正是Cgroup方面的行家,它的出现正好弥补了这个领域的缺漏。通过Cgroup,Systemd不仅实现了服务之间的访问隔离,还能够限制特定应用程序对系统资源访问配额(比如CPU的用量、内存的量),以及精确的管理服务的生命周期。在这篇文章的后面部分会讲述相关操作具体的做法。
统一管理服务日志
使用Systemd 必须知道的还有它的伙伴:Journald日志服务,这个服务的设计初衷是克服现有syslog服务的日志内容易伪造和日志格式不统一等缺点,而它现在已经是Systemd的一个标准子服务了。Journald用二进制格式保存所有日志信息,用户需要使用 journalctl 命令来查看日志信息。在这篇文章的后面会介绍如何查看服务的日志。
Unit和Target
先介绍两个概念,Unit和Target。
Unit是Systemd管理服务的基本单元,可以认为每个服务就是一个Unit,并使用一个Unit文件定义。Unit文件中需要包含相应服务的描述、属性以及需要运行的命令。在CoreOS中服务运行的命令通常是一系列的容器操作,而将具体的服务进程封装在容器中。
Target是Systemd中用于指定服务启动组的方式(相当于SysVinit中的“运行级别”,如果不清楚这个概念也没有关系,搜索“Linux运行级别”可以查到很多相关文章)。每次系统启动的时候都会运行与当前系统相同级别Target关联的所有服务,如果服务不需要跟随系统自动启动,则完全可以忽略这个Target的内容。通常来说我们大多数的Linux用户平时使用的都是“多用户模式”这个级别,对应的Target值为“multi-user.target”。
Hello World服务的Unit文件
只说不做假把式,现在我们来用Systemd创建一个简单的系统服务。
在这个系列的上一节内容里,我们创建了一个由3个CoreOS虚拟机节点组成的集群,在这节中,我们只需要使用到其中的任意一个,比如coreo-01节点。首先使用ssh连接进入这个节点(这种方法适用于Linux/Mac用户,对于Windows用户需使用Putty客户端, 具体参考)。
vagrant ssh core-01
登录成功后提示符变成 “core@core-01 ~ $” ,祝贺你又向CoreOS迈出了重要一步,接下来就可以开始在CoreOS里面玩耍了。
Systemd约定,服务的Unit文件需放置在 /etc/systemd/system 或 /usr/lib/systemd/system 目录中,但由于在CoreOS的后一个目录是只读分区(整个/usr目录挂载的都是只读的系统分区),因此我们通常会将用户定义的Unit服务文件放在在/etc/systemd/system目录中。进入这个目录,新建一个叫“hello.service”的文件,内容入下。
[Unit]
Description=Hello World
After=docker.service
Requires=docker.service
[Service]
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill busybox1
ExecStartPre=-/usr/bin/docker rm busybox1
ExecStartPre=/usr/bin/docker pull busybox
ExecStart=/usr/bin/docker run --name busybox1 busybox /bin/sh -c "while true; do echo Hello World; sleep 1; done"
ExecStop=”/usr/bin/docker kill busybox1”
[Install]
WantedBy=multi-user.target
在这个Unit文件里,我们首先为这个服务提供了一行简短的描述,然后指明它需要依赖docker的服务,并且要在docker服务运行以后才能运行。整个Unit文件是用的ini文件风格的分组配置格式,最开始的这段配置被放在了Unit组里面。在接下来的Service组中,使用ExecStart和ExecStop属性分别指定了服务运行时和结束时需要执行的命令。最后在Install组的配置中,我们指定了服务所属的Target为multi-user.target。
这里需要注意两个地方,首先ExecStart属性只能包含一条主要命令,而在这个属性的前后可以分别使用ExecStartPre和ExecStartPost指定更多的辅助命令,ExecStop同理。有些辅助命令会加上一个减号,表示忽略这些命令的出错(因为有些“辅助”命令本来就不一定成功,比如尝试清空一个文件,但文件可能不存在)。其次TimeoutStartSec=0这行的目的是将Systemd的服务启动超时检查关闭,对于docker应用这样做是必须的,因为docker在运行时可能会需要下载或更新镜像文件,使得服务启动时间变得很长,这样可以防止Systemd认为服务启动失败而将进程误杀。
启动服务
有了Unit文件,现在就可以启动Hello World服务了,在控制台输入以下命令:
sudo systemctl start hello.service
Tip:这个名字末尾的 .service 后缀是可以省略的,因为systemctl默认的后缀就是 .service。关于Unit文件后缀的含义,会在后续进阶篇的文章里详细说明。
Systemd会自动找到 /usr/lib/systemd/system 目录中的 hello.service 文件,并启动其中定义的服务。如果之前创建的Unit文件是放在其他目录下的,这里需要使用文件的完整路径。首次运行的时候需要等待一段时间,因为docker需要从网络上下载需要的镜像。启动完成后可以通过“systemctl list-units”命令查看服务是否已经在运行(这个命令接受一个可选参数作为服务名的过滤条件,如果不带任何参数则输出所有服务)。
core@core-01 ~ $ sudo systemctl list-units hello*
UNIT LOAD ACTIVE SUB DESCRIPTION
hello.service loaded active running Hello World
我们还可以通过“systemctl enable”命令来将服务指定为在系统启动时自动启动。
sudo systemctl enable hello.service
此时就用到了之前定义的Target组,实际上enable操作只是创建了一个连接文件到指定的Target组的目录下面。通过下面命令可以证实。
core@core-01 ~ $ ls -l /etc/systemd/system/multi-user.target.wants/hello.service
/etc/systemd/system/multi-user.target.wants/hello.service -> /etc/systemd/system/hello.service
系统启动时,会自动运行其所在Target级别相应的目录里所有链接的服务。
至此,我们的第一个服务已经在后台哈皮的玩耍了,可是说好的“echo Hello World”呢?我们从头到尾都没有见到服务的任何输出啊。
其实我们启动的服务已经在后台默默的输出“Hello World”了。
Systemd通过其标准日志服务Journald将其管理的所有后台进程打印到到std:out(即控制台)的输出重定向到了日志文件。日志文件是二进制格式的,因此必须使用特定的工具才能查看。Journald提供了配套的程序Journalctl用于处理日志内容。Journalctl的使用非常简单,默认不带任何参数的时候会输出系统和所有后台进程的混合日志,常用的参数有--dmesg用于查看内核输出的日志,--system用于查看系统输出的日志,--unit加上Unit的名字来指定输出特定Unit的日志,例如以下命令。
journalctl --unit hello.service
其他还有一些比较实用的参数,比如使用 --follow 实时跟踪日志输出,使用 --since 和 --until 指定显示的日志时间区间等,可以通过 journalctl --help 命令获得完整的参数说明。
前面我们使用了 systemctl 的 start 和 enable 两个命令将 Hello World 服务在系统后台启动并设置为了开机自动运行。实际我们会遇到的情况远不止这些,下面我们来将一个服务进程在Systemd的生命周期补充完整。
服务的激活
当一个新的Unit文件被放入 /etc/systemd/system/ 或 /usr/lib/systemd/system/ 目录时,它是不会自动被Systemd识别到的。例如在 hello.service 文件刚刚创建好时,如果我们让Systemd列出所有的Unit。
sudo systemctl list-units
此时在输出的内容中是找不到hello.service这个Unit的。直到我们通过 systemctl 的 start 或 enable 命令将这个Unit登记到Systemd的服务列表中,这个过程就是Unit的激活。
在服务被激活前,Unit仅仅是以Unit 文件的形式存在,Systemd提供 list-unit-files 命令查看所有的Unit 文件。
sudo systemctl list-unit-files
这个命令同样接受一个可选的参数作为Unit名称的匹配条件,不带任何参数时会输出所有Systemd找到的(也就是在那两个目录)Unit文件。
PS:顺便回答一个经常被问到的问题,这个命令的输出的第一列是Unit文件名,第二列是相应的Unit是否开机启动,它的值可以是enable、disable或static,这里的static是神马意思呢?其实它是指对应的 Unit 文件中没有定义[Install]区域,因此无法配置为开机启动服务。
服务的启动、结束、强制终止和重新启动
启动、结束、强制终止和重新启动,没啥可说的,分别对应以下几个命令。
sudo systemctl start <Unit名称>
sudo systemctl stop <Unit名称>
sudo systemctl kill <Unit名称>
sudo systemctl restart <Unit名称>
这里存在一个陷阱,直到目前版本的Systemd(v215)和Docker(v1.4.0)中,当Unit的主要命令是通过docker容器托管的时候,systemctl的kill命令会无法正确的杀掉服务进程,而必须使用 kill -s SIGKILL 才能正常的工作,原因见笔者在“ 不完美的CoreOS”中的分析。
服务的开机自动启动的启用和取消,分别对应下面两个。
sudo systemctl enable <Unit名称>
sudo systemctl disable <Unit名称>
服务的修改和移除
这两部分是 Systemd 当中比较Tricky的地方。
首先,如果我们修改了一个放在 /etc/systemd/system/ 的文件,比如将输出的“Hello World”改成了“Bye World”,当执行 systemctl restart 以后,重新启动的服务输出的将依然是“Hello World”。这是因为当Unit文件被激活时,Systemd会将其中的内容记入到自己的缓存当中,因此为了得到更新后的内容,我们需要告诉Systemd重新读取所有的Unit文件。
sudo systemctl daemon-reload
再次重启Unit,可以看到更新就会生效了。
其次是Unit文件的移除,直接删除Unit文件后由于缓存的作用,Systemd仍然可以继续使用这个Unit,即使通过daemon-reload更新缓存,在list-units中会看见这个Unit只是被标为了not-found,依旧阴魂不散。
core@core-01 ~ $ sudo systemctl list-units hello*
UNIT LOAD ACTIVE SUB DESCRIPTION
hello.service not-found failed failed hello.service
此时,我们需要明确的告诉Systemd,移除这些已经被标记为丢失的Unit文件。
sudo systemctl reset-failed
现在这个Unit才真正的从Systemd的记录中被抹去了。
Systemd 作为默认系统启动和服务管理器不但具备优秀的并行化处理能力,也提供了更好的系统进程追踪管理能力,加之按需启动等特点,结合 Docker 的快速启动,在 CoreOS 集群中大规模部署 Docker 容器与使用其他操作系统相比在性能上的优势更加明显。
实际上,Systemd的能力远远不止这些,在这篇文章仅仅介绍了它在服务管理方面的运用。在下一篇里我们将继续探索CoreOS是如何通过Fleet服务将这些功能扩展到大规模集群的。