在 2.5 秒内到达登录提示 - 一段旅程

2020年2月4日 | Fabian Vogt | 无许可

不仅在开发环境中,快速的周转时间非常有用,这可以包括重启。特别是对于事务性系统,只有在启动到新状态后,对系统的更改才会生效,这可能会产生重大影响。

所以让我们看看能做什么。记住:“记录下来是科学,瞎折腾是玩耍”!

起始点

本次实验的起始点是一个虚拟机 (KVM),4GiB 内存,2 个 CPU 核心,无 EFI。安装了 Tumbleweed 作为服务器(文本模式),仅使用默认设置。

# systemd-analyze
Startup finished in 1.913s (kernel) + 2.041s (initrd) + 22.104s (userspace) = 25.958s

仅仅到达一个相当精简系统的登录提示就需要近 26 秒,这不太好。我们能做些什么?

低垂的果实

systemd-analyze blame 告诉我们哪些是最糟糕的罪魁祸首

# systemd-analyze blame --no-pager
18.769s btrfsmaintenance-refresh.service    
17.027s wicked.service                      
 3.170s plymouth-quit.service               
 3.170s plymouth-quit-wait.service          
 1.078s postfix.service                     
 1.023s apparmor.service                    
  839ms systemd-udev-settle.service         
  601ms systemd-logind.service              
  532ms firewalld.service

btrfsmaintenance-refresh.service 有点特殊:它在执行期间调用 systemctl 来启用/禁用并启动/停止 btrfs-*.timer 单位。这些依赖于 time-sync.target,而后者又通过 chronyd.service 需要 network.servicewicked.service 是列表中的下一个项目。在认为该单元 active 之前,它会尝试完全配置和设置所有已配置的接口,默认情况下包括 DHCPv4 和 v6。这直接用作 network.service 状态,因此 network.target。wicked 不区分 network.servicenetwork-online.target。为了加快启动速度,切换到 NetworkManager 是一个选项,它以更异步的方式解释 network.service,因此更快地达到 active 状态。请注意,使用 DHCP 时,在 wicked 和 NM 之间切换可能会导致不同的 IP 地址!

# zypper install NetworkManager
# systemctl disable wicked
Removed /etc/systemd/system/multi-user.target.wants/wicked.service.
Removed /etc/systemd/system/network.service.
Removed /etc/systemd/system/network-online.target.wants/wicked.service.
Removed /etc/systemd/system/dbus-org.opensuse.Network.Nanny.service.
Removed /etc/systemd/system/dbus-org.opensuse.Network.AUTO4.service.
Removed /etc/systemd/system/dbus-org.opensuse.Network.DHCP4.service.
Removed /etc/systemd/system/dbus-org.opensuse.Network.DHCP6.service.
# systemctl enable NetworkManager
Created symlink /etc/systemd/system/network.service → /usr/lib/systemd/system/NetworkManager.service.

我们还删除 plymouth - 除了美观之外,它不提供任何有用的功能。

# zypper rm -u plymouth
Reading installed packages...
Resolving package dependencies...

The following 23 packages are going to be REMOVED:
  gnu-unifont-bitmap-fonts libdatrie1 libdrm2 libfribidi0 libgraphite2-3 libharfbuzz0 libpango-1_0-0 libply5 libply-boot-client5 libply-splash-core5 libply-splash-graphics5 libthai0 libthai-data libXft2 plymouth plymouth-branding-openSUSE
  plymouth-dracut plymouth-plugin-label plymouth-plugin-label-ft plymouth-plugin-two-step plymouth-scripts plymouth-theme-bgrt plymouth-theme-spinner

23 packages to remove.
After the operation, 4.8 MiB will be freed.
Continue? [y/n/v/...? shows all options] (y):
...

Plymouth 仍然在 initrd 中启动,但由于它不再是根文件系统的一部分,因此不会被 plymouth-quit.service 停止。这种组合会导致启动失败!通常,如果任何相关内容发生更改,initrd 应该自动重新生成,但对于删除操作,这尚未实现 (boo#966057)。

# mkinitrd
...
# reboot
...

让我们看看这节省了多少时间

# systemd-analyze
Startup finished in 1.675s (kernel) + 2.066s (initrd) + 2.696s (userspace) = 6.438s

节省了超过 19 秒,这已经很多了!

# systemd-analyze blame --no-pager
1.411s btrfsmaintenance-refresh.service    
893ms systemd-logind.service              
849ms apparmor.service

现在,启动时间的最大贡献者是 btrfsmaintenance-refresh.service。由于它在最近的内核中提供的价值并不十分明确 (boo#1063638#c106),让我们将其删除。

# zypper rm -u btrfsmaintenance
# reboot
...
# systemd-analyze
Startup finished in 1.700s (kernel) + 2.010s (initrd) + 2.367s (userspace) = 6.079s
multi-user.target reached after 2.347s in userspace

这又好了一些。

# systemd-analyze blame --no-pager
873ms apparmor.service                    
550ms systemd-logind.service              
504ms postfix.service                     
405ms firewalld.service

apparmorsystemd-logind.service 都是必需的,因此没有剩余的低垂果实。

加速早期启动

启动时间方程中仍然有一个部分我们可以完全消除!

Startup finished in 1.700s (kernel) + 2.010s (initrd) + 2.367s (userspace) = 6.079s
                                      ^^^^^^^^^^^^^^^

那么,initrd 到底有什么用呢?在绝大多数安装中,它定义得非常清楚:挂载真实的根文件系统并切换到它。根据配置,这可能从简单(本地 ext4)到非常复杂(通过网络加密块设备,通过 ssh 接受密码)。此外,引导加载程序加载的内核二进制文件非常小,不包括每个系统的驱动程序。这些是包含在 initrd 中的模块的一部分。

事实证明,在简单的情况下(大多数 VM 来宾系统),我们可以很好地在没有 initrd 的情况下启动。但是,当前设置并未针对此设置进行调整,因此需要进行一些调整。

驱动程序用于挂载 / 而无需加载模块

内核需要连接到存储设备的虚拟设备以及其上的文件系统的驱动程序。前者通过使用 kernel-kvmsmall flavor 来处理,但不幸的是,它没有内置 btrfs。

幸运的是,通过使用自定义配置重新构建内核,这很容易解决。通过将 CONFIG_FS_BTRFS=y(以及其他必需的选项)放入 config.addon.tar.bz2 中,与 openSUSE 内核在 OBS 中一起使用,它会生成一个带有可用二进制文件的 .rpm。

# zypper ar obs://devel:kubic:quickboot/ devel:kubic:quickboot
# zypper in --from devel:kubic:quickboot kernel-kvmsmall

kernel-kvmsmall 并非启用了所有内核功能(甚至作为模块),这意味着在某些情况下,可能需要将更改应用于 kernel-default,后者具有完整的模块集。

按 UUID 挂载根

但是,如果您现在重新启动并注释掉 grub 配置中的 initrd 命令,您会注意到启动失败,因为内核无法找到根设备。这是因为默认情况下,GRUB 配置使用 root=UUID=deadbeef-1234... 参数。这由 initrd 在用户空间中解释。确切地说,当内核识别到块设备时,Udev 会通过读取文件系统 UUID 并创建 /dev/disk/by-uuid/... 中的链接来做出反应,然后将其用作根设备。没有 initrd,不会发生这种情况,内核无法继续。

这里的解决方法是在 /etc/default/grub 中设置 GRUB_DISABLE_LINUX_UUID=true。这意味着将使用设备路径,如 root=/dev/vda2,这在更改磁盘布局或顺序时可能会导致问题。通过同时设置 GRUB_DISABLE_LINUX_PARTUUID=false,它使用 root=PARTUUID=cafebabe-4554...,内核也支持它,但更可靠。

# echo GRUB_DISABLE_LINUX_UUID=true >> /etc/default/grub
# echo GRUB_DISABLE_LINUX_PARTUUID=false >> /etc/default/grub
# grub2-mkconfig -o /boot/grub2/grub.cfg
# reboot
... (comment out the initrd call in grub, after pressing "e" in the menu and prepending "#" in the last line)
# systemd-analyze
Startup finished in 1.778s (kernel) + 2.725s (userspace) = 4.504s

再次削减了近三分之一,太棒了!但是,现在控制台中显示错误消息,这不太好。关于 systemd-gpt-auto-generatorsystemd-remount-fs 无法找到根设备的内容 - 就像内核之前一样。原因是相同的 - /etc/fstab 仍然包含 UUID= 格式的挂载点,这些错误发生在 systemd-udevd.service 启动并 udev 定下来之前。

无论如何配置 systemd,都无法消除第一个错误 - 生成器在任何单元之前运行。所以我们必须在 systemd 之前启动 udev!

但首先,快速绕道。

变得事务性

让我们看看 MicroOS 的表现如何。顾名思义,它应该比开箱即用的普通 Tumbleweed 更轻。

# systemd-analyze 
Startup finished in 1.788s (kernel) + 2.036s (initrd) + 21.243s (userspace) = 25.068s

# systemd-analyze blame --no-pager
17.669s btrfsmaintenance-refresh.service
16.177s wicked.service
 3.377s apparmor.service
 1.356s health-checker.service                
 1.179s systemd-udev-settle.service
  968ms systemd-logind.service
  811ms kdump.service

快了一秒,还可以。Plymouth 消失了,但我们获得了 health-checker 和 kdump。时间主要由 wicked 减慢启动速度,所以让我们替换它。此外,禁用 btrfsmaintenance-refresh.service。由于 microos_base 模式需要它,因此无法删除它。

# transactional-update shell
transactional update # zypper install NetworkManager
transactional update # systemctl disable wicked
transactional update # systemctl enable NetworkManager
transactional update # systemctl disable btrfsmaintenance-refresh.service
transactional update # exit
# reboot
...
# systemd-analyze 
Startup finished in 1.744s (kernel) + 1.989s (initrd) + 2.342s (userspace) = 6.075s

# systemd-analyze blame
1.251s apparmor.service                    
1.066s kdump.service                       
 824ms NetworkManager-wait-online.service  
 742ms systemd-logind.service              
 730ms kdump-early.service                 
 638ms systemd-udevd.service               
 563ms create-dirs-from-rpmdb.service

又好多了。

启动只读系统而没有 initrd

在具有只读根文件系统的系统(如 MicroOS 或事务性服务器)中,initrd 还有另一个任务:确保 /var/etc 已经挂载,以便早期启动可以存储日志并读取配置。

因此,我们必须在启动 systemd 之前挂载 /var/etc。怎么做?通过我们自己的 init 脚本!它由内核直接启动,方法是将 init=/sbin/init.noinitrd 设置为内核参数,最后一步是执行 exec /sbin/init 以将自身替换为 PID 1 的 systemd。

不幸的是,这并不像简单地执行 mount /var 并完成它那么简单,因为 /var 的挂载使用 UUID= 作为源,因此需要运行 udev... 幸运的是,在手动挂载 /sys/proc/run 之后,udev 实际上在这种环境中有效。

这里圆圈闭合了 - 我们现在在 systemd 之前运行 udev。因此,只需在所有系统中使用该脚本,就可以解决这个问题。

简化无 initrd 启动

由于无 initrd 启动的设置非常复杂,现在有一个软件包可以自动执行所需的设置(不包括安装合适的内核)。

这包含所需的“预启动”包装脚本 /sbin/init.noinitrd 以及一个 grub 配置文件模块,该模块会自动将启动系统而没有 initrd 的条目添加到 grub 配置中。这些仅为内置了根文件系统支持的内核生成。它还负责正确设置 root/rootflagsinit 参数。带有 initrd 的启动选项仍然存在,作为安全保障。

# zypper ar obs://devel:kubic:quickboot/openSUSE_Tumbleweed devel:kubic:quickboot
# transactional-update initrd shell pkg in --from devel:kubic:quickboot kernel-kvmsmall noinitrd
...
transactional update # grub2-set-default 0
transactional update # exit
# reboot
...

# systemd-analyze 
Startup finished in 1.889s (kernel) + 2.246s (userspace) = 4.135s

# systemd-analyze blame
1.022s apparmor.service                    
 847ms kdump.service                       
 820ms NetworkManager-wait-online.service  
 782ms systemd-logind.service              
 608ms kdump-early.service                 
 542ms dev-vda3.device

略高于 4 秒! /var 分区现在位于 systemd-analyze 的前六名中,这意味着我们正在接近极限。

issue-generator 的一个问题

似乎没有 initrd 的启动引入了一个错误:登录屏幕上没有显示活动网络接口 enp1s0 及其地址,而只有一个孤独的 eth0:。检查日志,这是因为接口在启动期间从 eth0 重命名为 enp1s0。通常,当 udev 已经在 initrd 中运行时会发生这种情况,这意味着在 switch-root 之后,已经有一个带有新名称的 add 事件,issue-generator 会拾取它。没有 initrd,重命名发生在启动的系统中,issue-generator 必须以某种方式处理它。

如何实现这一点?要找出由重命名触发的 udev 事件,udev 监视器非常有用。使用 --property 选项,它显示附加到触发事件的哪些属性

# udevadm monitor --udev --property &
# ip link set lo down
# ip link set lo name lonew
# ip link set lonew name lo
...
UDEV  [630.458943] move     /devices/virtual/net/lo (net)
ACTION=move
DEVPATH=/devices/virtual/net/lo
SUBSYSTEM=net
DEVPATH_OLD=/devices/virtual/net/lonew
INTERFACE=lo
IFINDEX=1
SEQNUM=3616
USEC_INITIALIZED=1054433
ID_NET_LINK_FILE=/usr/lib/systemd/network/99-default.link
SYSTEMD_ALIAS=/sys/subsystem/net/devices/lonew /sys/subsystem/net/devices/lonew
TAGS=:systemd:
# fg
^C
# ip link set lo up

因此,使用 DEVPATH_OLDINTERFACE 属性,可以在 issue-generator 的 udev 规则中实现它 在此处

应用这些更改并重新启动后,现在正确显示 enp1s0!

优化 apparmor.service

那些注意 systemd-analyze blame 输出的人会注意到,与普通的 Tumbleweed 相比,MicroOS 上的 apparmor.service 启动时间更长。为什么会这样?

# systemd-analyze plot > plot.svg

systemd-analyze plot result

在这个图中,可以猜测服务之间的显式和隐式依赖关系。如果一个服务在另一个服务结束后启动,那么它很可能是一个显式依赖关系。如果一个服务只有在另一个服务启动后才启动,那么它很可能以某种方式等待它。这就是我们在图中看到的情况:apparmor.service 只有在 create-dirs-from-rpmdb.service 启动后才完成启动。因此,让它更早或更快地启动也会加速 apparmor.service。为了确认这个理论,只需禁用该服务

# systemctl disable create-dirs-from-rpmdb.service
# reboot
# systemd-analyze blame
927ms systemd-logind.service              
824ms NetworkManager-wait-online.service  
721ms kdump.service                       
689ms kdump-early.service                 
554ms apparmor.service
# systemctl enable create-dirs-from-rpmdb.service

确认了。那么如何正确优化它?

此服务的作用是创建由软件包拥有的目录,这些目录不属于系统快照。它在 local-fs.targetsystemd-tmpfiles-setup.service 之间进行排序。一些 tmpfiles.d 文件可能依赖于已存在的打包目录,因此它必须在 systemd-tmpfiles-setup.service 之前运行。除了将其更改为 RequiresMountsFor=/var /opt /srv 之外,没有太多的优化潜力。

但是,该服务不必在每次启动时都运行,而只需在软件包集发生更改时才需要处于活动状态。幸运的是,使用 rpm 4.15,实现这种检查的新方法 (rpmdbCookie) 已经实现,并且很容易在服务中 使用它。部署后,它仅在必要时运行,否则只需花费一些时间从 rpm 数据库获取 cookie

# systemd-analyze blame
872ms NetworkManager-wait-online.service  
832ms systemd-logind.service              
811ms kdump.service                       
645ms dev-vda3.device                     
597ms kdump-early.service                 
526ms apparmor.service
# systemd-analyze blame | grep create-dirs-from-rpmdb
 52ms create-dirs-from-rpmdb.service

出于某种原因,这并不总是有效,有时 apparmor.service 又回到了 >1s,所以这需要进一步调查。

rebootmgr

在 blame 列表中,我们有 NetworkManager-wait-online.service。此服务可能需要可变的时间,具体取决于网络配置和环境,并且在大多数情况下,它不需要获得正在运行的服务。那么,目前是什么将其拉入 multi-user.target

# systemd-analyze critical-chain
The time when unit became active or started is printed after the "@" character.
The time the unit took to start is printed after the "+" character.

multi-user.target @2.472s
└─rebootmgr.service @2.284s +24ms
  └─network-online.target @2.282s
    └─NetworkManager-wait-online.service @1.311s +970ms
      └─NetworkManager.service @1.246s +61ms

rebootmgr.service!它将自己排序为 After=network-online.target 的原因是它可以直接与 etcd 通信。但是,目前在 rebootmgr 中禁用了对该的支持,并且它似乎可以很好地处理启动时没有网络连接的情况。所以直到更改最终进入软件包,让我们手动调整一下

# systemctl edit --full rebootmgr.service
(Remove lines with network-online.target)
# reboot

请注意,这并不能真正提高启动速度的感知,因为只有 multi-user.target 本身才依赖它,而 sshd/getty 已经在之前启动了。现在到 multi-user.target 的关键链是

# systemd-analyze critical-chain
The time when unit became active or started is printed after the "@" character.
The time the unit took to start is printed after the "+" character.

multi-user.target @2.188s
└─kdump.service @1.287s +899ms
  └─NetworkManager.service @1.202s +82ms
    └─dbus.service @1.197s
      └─basic.target @1.195s
        └─sockets.target @1.195s
          └─dbus.socket @1.195s

接近边缘

现在是发挥创造力的时候了 - 还有什么可以优化的?此后的所有内容都可以认为是一种黑客行为,比之前的更改要多得多。

禁用非必要服务

让我们禁用所有实际上不需要系统启动的服务。

apparmor.service:用于系统加固。如果系统不安全(例如,隔离的 VM),可以禁用它。但并不推荐。

rebootmgr.service:如果计划重启(例如,由自动 transactional-update.timer 计划),它会在配置的时间范围内(默认情况下为凌晨 3:30)触发自动重启。如果手动重启系统,可以禁用它。

kdump.service:加载内核和 initrd 以将内核转储到 RAM 中。除非该系统是高度关键的生产机器,并且必须分析每次崩溃,否则可以禁用它。

应用这些更改后

# systemd-analyze 
Startup finished in 1.899s (kernel) + 1.505s (userspace) = 3.405s
# systemd-analyze blame
629ms systemd-journald.service            
532ms systemd-logind.service              
480ms dev-vda3.device                     
479ms dev-vda2.device                     
303ms systemd-hostnamed.service

又节省了一秒多。systemd-analyze 告诉我们,用户空间中没有太多可以优化的东西了。

内核配置

目前,内核在启动期间花费了很长时间来基准测试一些算法。这是为了让它知道系统上哪个可用的实现最快。具有讽刺的是,这意味着在具有新功能的 CPU 上,它实际上需要更长的时间。如果 RAID6 的性能不重要,可以通过设置 CONFIG_RAID6_PQ_BENCHMARK=n 来禁用它

构建这样的内核并安装后

# systemd-analyze 
Startup finished in 1.083s (kernel) + 1.356s (userspace) = 2.439s

这节省了将近一秒钟的内核启动时间。

直接内核启动

启动加载程序在启动过程中也需要一些时间(启动菜单、内核加载),这也可以进行优化。除了明显可以减少启动菜单显示时间(或默认隐藏它)之外,还可以完全跳过它!通过从虚拟机宿主机提供内核和cmdline,可以使启动速度更快。但这只适用于具有自定义内核构建且所有内容都内置的情况,否则虚拟机中的模块可能与虚拟机宿主机提供的内核镜像不同步。这也会破坏自动回滚(健康检查器需要GRUB才能实现)以及选择旧快照进行启动的功能。

我不知道如何测量内核加载所需的时间,所以这里没有测量数据。这主要消除了systemd-analyze在非EFI系统上不显示的时间。

结论

从25.958秒到2.439秒(通过各种技巧)意味着超过90%的启动时间可以被优化掉。

接下来的任务是将这些优化推送到发行版中,并使其成为默认设置,或者至少易于应用。

祝你玩得开心!

类别: 博客

标签

分享这篇文章