前卫的设计理念systemd程序性能前瞻

系统 Linux
systemd是一个init程序。根据之前 Upstart的一些介绍大家应该简要了解了传统Sysvinit系统的不足之处。不过,这次的systemd的设计思想更为超前,也可以说是借鉴了OSX上launchd的思路。

简而言之,systemd是一个init程序。根据之前 Upstart的一些介绍大家应该简要了解了传统Sysvinit系统的不足之处。不过,这次的systemd的设计思想更为超前,也可以说是借鉴了OSX上launchd的思路。

init进程由内核启动,是所有其他进程的父进程(也许应该译为母进程?:-)因此比其他进程能够做更多事情,比如在启动过程中加载服务进程。

传统的 sysinit 系统不符合一个有效的、快速的init系统标准:

1. 尽可能启动更少进程

2. 尽可能将更多进程并行启动

条款1意味着除非必须否则不启动。不像syslog和DBus,蓝牙服务仅在蓝牙适配器被插入时才被需要;打印服务仅在打印机连接或程序要打印时才需要;甚至sshd也不是必须的,管理员常常因几个月才连接一次的ssh而一直运行 sshd 进行监听。条款2意味着不应该像 sysvinit 一样串行执行启动进程,以最大化CPU和IO带宽的效率。

软硬件的动态改变

现代的操作系统应具备高度的动态特性:不同的程序启动、停止,不同的硬件被添加、移除,因此需要服务在被需要时动态的启动。大多数当前的系统在尽可能并行化启动过程时仍对一些守护进程的调用保持同步:比如 Avahi 需要DBus,故仅在DBus启动、且 DBus 信号准备完成后 Avahi 才被启动。与之相同的是 libvirtd 和 X11 需要HAL(虽然之后HAL会被移除),在 HAL 启动前,libvirtd 和 X11 不会启动。

并行化Socket服务

以上的同步过程导致了启动过程的串行化。能否摆脱同步与串行化的缺点呢?答案是能够! 首先我们要理解 daemon 对彼此的真正依赖是什么,为什么它们在启动时被延迟了。对于传统的 Unix daemon,答案是:它们在所需要的服务所提供的 socket 准备好连接之前一直处于等待状态。常见的是 AFUNIX,但也可能是 AFINET。比如,需要DBus服务的客户程序将在 /var/run/dbus/systembussocket 可以被连接之前等待,syslog 服务的客户程序将在 /dev/log 上等待...而这是它们唯一需求的内容!(意指只要这一条件满足,客户程序即无需等待,不论实际的服务是否已经启动。下文详细介绍)

以上就是等待的真正含义。如果能尽早建立客户程序所必须的 socket 而令客户程序处于等待状态而不是在服务程序完全启动后再启动客户程序,我们就能加快启动进程,进一步并行化进程启动。如何做到这一点?在 Unix 族系统上非常简单:在真正需要启动服务之前先监听其 socket,然后将 socket 传递给 exec() (此处直译,含义不懂),如此,在 init 系统中第一步就可建立所有 daemon 所需的所有 socket,在第二步中一次运行所有的 daemon。如果一个服务需要另一个,但被需者未完全启动号,也完全没有问题在被需者查询 socket,处理请求之前,客户进程将因这个请求而被阻塞,但仅有被这个请求阻塞的客户进程被阻塞。并且服务之间的依赖关系将不再需要(手动)配置:如果一次建立所有服务所必须的所有 socket(及启动所有相关服务),则所有服务的所有需求一定在最后会被满足。

下列所说将是重点:如果同时启动了 syslog 和多个其客户进程(意指syslog尚未启动准备好处理请求),而客户进程开始发送请求,则请求会被写入 /dev/log 的Socket 缓存中。除非缓存填满,否则客户进程无需任何等待即可继续完成其启动过程。当 syslog 启动完全后,处理器所有的消息。另一个例子:DBus与其客户进程一起启动,当同步请求发出但没有接受到预想的回应,客户进程将阻塞。当 DBus 启动完成后处理请求,客户进程继续。

内核的socket 缓存将辅助达成最大的并行化。因为内核完成了同步,不再需要用户空间的任何的额外管理!如果在 daemon 启动前所有必须的 socket 都已经可用,依赖关系就变得多余(至少是次要了)。daemon A 需要另一个 daemon B,简单的连接到B。若B已经启动,A的请求成功。若B未完成启动,假如A发出的不是一个同步请求,甚至无需理会(B没有完全启动)。甚至即使B完全没有执行(比如crash了)时再重启B也为时不晚对于A而言二者没有任何分别。借此可达成最佳的并行化和随意的需求时(on-demand)加载。而且在此基础上能够更加鲁棒。因为 socket 即使在相应服务是暂时不可用(如crash)时也可用,所以客户进程不会丢失任何的请求(request入buffer,待重启服务后处理)

更多有趣之事

首先,这不是一种全新的逻辑,这正像是Apple的launchd系统所做的:OSX 中所有 daemon 所需的 socket 都由 launchd 建立,然后服务本身并行启动,无需手动指定相关的依赖性。这一设计创意独具,也正是OSX急速启动的原因。但这一思想在Apple之外未有任何系统有所体现。

在 launchd 之前,这一思想有更早的体现者:古老的 inetd 就如此工作:socket 被 daemon 建立而实际的服务在daemon通过exec()启动相关进程时传递socket的文件描述符。然而 inetd 所关注的当然不是本地服务,而是网络上的服务。他也不是并行化启动或者隐藏依赖的正确工具。

对于 TCP 的 socket inetd 对于每个进入的连接都建立一个新的 daemon 实例,这意味着每个连接将导致一次进程初始化,这无助于高性能服务器。然而此后inetd支持另一种模式单:当第一个连接到来时,第一个实例(单例)被启动,并将继续接受后续的请求。

并行化Bus服务

Linux上现代的daemon都通过DBus而非socket来交互,现在的问题是,对于这些服务,能否施加与启动传统的、基于socket的服务逻辑相同的并行化?答案是可以,DBus已经提供了所有必要的hook:使用DBus将会在第一次访问时加载服务,并且给予最小的、每请求一个的、消费者与生产者同时启动的同步机制。例如Avahi与CUPS(CUPS需要Avahi进行 mDNS/DNS-SD上打印机扫描)同时启动,仅仅是简单的同时启动二者,若CUPS比Avahi启动快,则DBus将把请求缓存入队列,直到 Avahi服务进行了处理。

所以,总结如下:基于Socket和基于DBus的服务可一次并行启动所有进程,无需任何额外的同步。基于激活的策略还能令我们进行延迟加载服务。如果服务很少被用到,我们可以在第一次被访问时启动,而不是在启动过程中启动。

并行化文件系统任务

对于当前发行版的启动序列图可以看出,文件系统任务所发生的同步点大于 daemon 启动时的同步点:mount fsck等。现在,启动时很多时间都花费在空闲等待所有fstab 中列举的文件系统被加载、 fsck的过程中,仅在完全完成后启动进程才会继续。如何改进?答案是autofs系统。就如同connect() 调用表示一个服务对另一个服务感兴趣,open() 调用(或相似的调用)表示服务对特定的文件或文件系统有需求。所以,我们应该仅令那些访问暂时还不可用(暂未完成 mount、fsck)文件系统的服务阻塞,这样就改进了并行性。首先加载autofs,然后继续正常的mount过程,当被访问的文件系统未准备好,内核将会缓存请求调用,访问进程将被阻塞,一个未成功的访问仅可能阻塞一个进程。当真正的文件系统加载完成后,启动进程如同正常般完成,没有任何缺失的文件,如此我们能够在所有文件系统都准备好之前就加载服务。

假如有人提出将autofs内置于 init 是不妥当的我并不感觉奇怪,也许这会导致更多 crash。然而经过实验,我可以说这并不正确。使用autofs意味着无需马上提供后台的真实文件系统,这只会导致访问被延迟。如果进程尝试访问autofs但真实的文件系统很长时间都没能加载,进程将被一个sleep中断上被挂起,意味着你可以安全的cancel 它。如果最后这个文件系统都没能加载,那么令 autofs 返回一个error Code(例如 ENOENT)。所以我认为内置 autofs 到init中是正确的。实验代码显示这一想法在实践中工作的很好。

保持第一个登录的用户的 PID 很小(意即启动进程中少启动进程)

我们可以从MacOS的启动逻辑中学到的另一个内容是:shell脚本是有害的。Shell很容易 hack,但是执行却太慢。传统sysvinit启动逻辑就是围绕shell脚本的一种模式不论是/bin/bash还是其他shell(被用来更快执行shell脚本),最后的结果注定很慢在我的系统中,/etc/init.d中的系统脚本至少调用grep 77次,awk92次,cut23次,sed74次。每次调用时这届进程都被产生一次,库被搜索一次,有些像i18n这样的还会加载更多。这些基于琐碎字串比较的进程导致最终难以置信的慢。只有shell脚本如此工作。除此外shell脚本也非常脆弱,环境变量之类能够彻底改变脚本的行为(behaviour),最终难于检查和控制。

所以在启动过程中摆脱shell脚本!在如此之前先要明确使用 shell脚本的实际用意何在:大体上来说,shell 脚本在完成的是些令人厌烦的工作,用于琐碎的设置和卸载服务。这些内容应该用C来完成,位于分离的可执行程序,或者移至daemon本身,或者交给init。看起来我们还无法马上在启动过程中摆脱脚本,用C重写需要时间,且在有些时候也不行 shell 脚本太过灵活以至于无法脱离,但我们当然能让它们不像现在这样突出(被大量使用)。

简单测量shell 脚本对于启动过程的危害可以从你在系统完全启动后加载的第一个进程的PID看出来:启动、登录、打开终端,输入echo $$,对比Linux和MacOS,如下:Linux PID:1823,MacOS PID:154。自己测去吧。

接下来作者分析了Upstart对于这些问题的一些解决方法以及不足之处,接下来介绍了作者的最终结论: putting it all together:systemd

其特性就是,上文中如此多的翻译已经说明了其核心思路,就不具体介绍其中的详细概念了。值得一提的是systemd解析传统的SysVinit 脚本,将其依赖关系转化为其原生的依赖关系,因此兼容 Sysvinit 脚本的同时仍能提供并行性,而 Upstart 无法对传统脚本进行并行化。且可通过DBus界面控制。

目前状态:已经可作为Sysinit或 Upstart的替代来使用,有一个修改过的 Fedora 13 qemu 镜像可供下载使用。作者秒表计时 Fedora13用Upstart启动27s,systemd24s(从grub到 gdm ,相同系统,相同设置,两次连续的启动过程中取较少者)。

【编辑推荐】

  1. Linux查看版本信息及CUP内核、型号等
  2. Linux管理员们,该刷新内核了
  3. Linux新突破:新的内核与新的文件系统
  4. Linux内核编译后地址空间的整理
责任编辑:张浩 来源: LinuxTOY
相关推荐

2010-08-10 13:58:00

Flex性能测试

2019-02-01 09:50:00

提升Python程序性能

2009-01-08 19:11:39

服务器应用程序SQL Server

2010-02-04 09:41:03

Android应用程序

2018-07-06 16:26:11

编程语言Python程序性能

2010-11-15 16:20:33

Oracle系统优化

2022-10-08 13:13:14

Python程序性能

2011-09-20 10:41:45

Web

2020-12-29 15:00:46

PerfVTune工具

2018-11-20 10:50:00

Java性能优化编程技巧

2013-12-17 17:05:20

iOS性能优化

2009-07-01 18:24:59

JSP应用程序JMeter

2011-07-15 17:21:46

网站程序

2011-01-19 11:10:50

程序交付优化应用程序性能管理监控

2022-07-04 17:32:12

DevOpsAIOps

2023-04-13 07:33:31

Java 8编程工具

2019-10-17 10:10:23

优化Web前端

2009-06-15 09:47:12

Java程序内存溢出

2014-12-16 09:35:13

DevOps

2022-07-20 07:45:15

多线程程序性能
点赞
收藏

51CTO技术栈公众号