1. 概述
Alibaba Cloud Linux 2(原Aliyun Linux 2,简称Alinux 2)是阿里云操作系统团队基于开源Linux内核4.19 LTS版本打造的一款针对云应用场景的下一代Linux OS发行,不仅提供Linux社区的最新增强功能,在提供云上最佳用户体验的同时,也针对阿里云基础设施做了深度的优化。今日Alinux 2 LTS 正式发布,是Alinux 2的一个重要里程碑。这标志着阿里云操作系统团队将为Alinux 2提供长期技术支持、稳定的更新、更好服务,为Alinux 2的用户提供更多保障。
Alinux 2 LTS 版本不仅增加了更多社区新功能的支持,对系统启动时间、运行时性能及稳定性都做了许多优化。更详细的更新优化可参考发布记录,推荐直接上手试用体验。
Alinux 2在快速启动优化上取得一些不错的效果,同时推出“Alinux 2 qboot快速启动版”镜像(公测中),内核部分启动性能提升40%:
这里分享一下Alinux 2 LTS在系统快速启动优化上的所做的一些实践。
2. Linux系统启动流程
首先定义Linux系统启动,这里我们定义为从系统上电(虚拟机开启),到用户能够登陆(ssh login)为系统启动。通用Linux系统启动大致分为三个阶段:引导阶段(phase#1),内核启动阶段(phase#2)及用户态启动阶段(phase#3):
其中,
- 对物理机产品,开机后运行固件中的BIOS程序,完成基本硬件初始化及上电自检(POST),通过后跳转至系统磁盘引导扇区;
- 对虚拟机(Qemu + KVM)产品,Qemu运行后模拟BIOS,加载系统镜像文件虚拟出系统盘,跳转至系统盘引导扇区;
下面来看看各阶段大致的启动流程。
2.1 Boot Loader
Bootloader是位于系统引导扇区的一段独立的系统程序,用于系统启动初期的硬件初始化,系统分区识别,系统内核加载及跳转执行。目前应用比较广泛的bootloader是用于通用系统的grub2和嵌入式系统的uboot。Grub2是多重引导器(multiboot),提供交互界面,默认配置下有5s交互超时,启动耗时较长。
2.2 Kernel
Bootloader加载Linux内核(一般为压缩内核vmlinuz)到内存,并运行内核自解压缩程序,解压后跳转至start_kernel,开始内核初始化流程:
2.3 User Space
Linux内核完成一系列初始化动作之后,开始运行init程序,创建PID为1的用户态进程,将系统控制权从内核态跳转到用户态。init进程会继续进行用户态启动流程,开启各种必要的,或是预先配置的系统服务,最后启动登陆服务,完成整个系统的启动。
Initrd与Switch Root
init是用户态程序,存放在系统根文件系统(rootfs)里。内核需要先挂载rootfs,才能运行init程序。通用Linux发行需要支持多种磁盘设备,多种文件系统,因此内核必须能够识别不同的磁盘设备,不同的文件系统。这需要内核预加载多种可能的磁盘设备驱动以及多种文件系统相关用户态工具软件才能正确识别rootfs。而这些驱动及用户态工具一般都存放在rootfs中,形成一个循环依赖。
为解决这个问题,initrd应运而生,将挂载rootfs必要的驱动,用户态工具以及其他需要预加载的代码从rootfs总抽取出来,并依照rootfs的文件结构,打包成一个小的rootfs,做成一个内存盘(ram disk)。内核在挂载最终的rootfs之前,先从内存中挂载initrd,加载必要的驱动后,先运行initrd中的init程序,挂载最终的rootfs。然后执行switch root动作,切换至最终的rootfs。
Alinux 2系统采用systemd来管理用户空间启动流程,systemd就是init程序,initrd使用压缩格式的initramfs文件。因此在加载initrd之前,内核需要先解压缩initramfs。
Cloud Init
Cloud init是云环境中的虚拟实例初始化配置工具,实例启动阶段能从多种数据源读取相关数据并据此对虚拟机进行配置,如用户密码,主机名,网络,用户数据等等一些配置。
3. 启动耗时画像
优化系统启动时间,自然需要先对系统启动画像,了解启动时间分布情况,找出系统启动耗时热点。
3.1 启动时间测量
Linux系统有如下常见的启动时间测量统计方法:
-
systemd-analyze
systemd自带的启动分析工具,能够给出总的启动时间消耗,已经用户态服务启动耗时统计。
-
dmesg
dmesg输出内核启动日志,时间戳能够帮助分析内核初始化各阶段耗时情况。配合-d
选项计算出日志间的时间差,方便快速定位内核启动过程中耗时热点。
-
initcall_debug
内核启动参数,开启后会统计内核各初始化函数的耗时情况,相比dmesg -d
更加精确。 -
printk/trace_printk
要分析一些启动热点的细化耗时情况时,手动增加一些printk/trace_printk探针能够帮助获取时间统计信息。 -
ftrace
必要时也可开启内核早期ftrace功能,帮助分析热点耗时。不过需要注意开启ftrace后可能会导致函数延时增加,因此不宜参考ftrace得出函数绝对耗时,可以参照trace结果帮助分析热点函数的耗时逻辑。
还有其他一些时间测试方法,以及图形化画像工具,这里不一一介绍。
3.2 启动耗时热点分析
对Alinux 2系统启动画像后,按耗时排序,得到如下耗时热点:
(这里以2C8G虚拟机为例,内核耗时1000ms,总体耗时5000ms)
热点 | 耗时(ms) | 内核启动占比 | 总启动占比 |
---|---|---|---|
mem init | ~35 | 3.5% | 0.7% |
ORC unwind init | ~90 | 9% | 1.8% |
buddy init | 250 | 25% | 5% |
console enable | ~60 | 6% | 1.2 % |
initramfs unpack | 250 | 25% | 5% |
free initmem | 270 | 27% | 5.4% |
mouse probe | 650 | 65% | 13% |
systemd initrd | 600 | N/A | 12% |
mount rootfs | 200 | N/A | 4% |
cloud init | 2740 | N/A | 54.8% |
可见:
- 总体启动耗时中,一半以上的时间消耗在用户态cloud-init进程上;
- 内核启动阶段,鼠标探测耗时占比较高。
4. 快速启动优化
4.1 启动优化方法
常用的启动优化方法大致如下:
-
瘦身
- 移除不必要的代码,如模块,服务等,缩减启动初始化步骤;
- 移除不必要的测试,调式及打印
- 精简共享库
-
异步、并行
- 将耗时动作从关键路径移除,延后执行
- 将顺序动作并行化执行
-
原地执行(XIP)
- 多用于嵌入式系统
-
定制化
- 将通用初始化程序定制化
-
算法优化
- 改进算法,加速初始化时间
4.2 去initrd
从前面的启动耗时热点分析结果可以看出,initrd解压缩及initrd systemd耗时占Alinux 2启动较大比率。
Alinux 2系统主要面向云环境虚拟实例,系统盘设备基本固定为virtio-blk设备,根文件系统格式基本固定为ext4文件系统,应该不需要通过initrd来加载rootfs,可以去掉initrd,直接挂载系统磁盘,即对内核启动瘦身。
理论上会优化掉initramfs unpack(270) + initrd systemd(560) ~ 800ms的启动耗时。去掉initrd测试结果如下:
可见initrd systemd时间确实优化掉了,但总的启动时间并没有理论优化收益。原因是内核启动耗时增加了约400ms。进一步分析发现,启动耗时热点之一的mouse probe(600ms),去initrd之前是与initrd systemd并行执行的。
去掉initrd后,这部分时间就直接计入内核启动时间了。抵去优化掉的initramfs unpacking的200ms,内核实际增加了400ms左右。
因此,要最大化去initrd的优化收益,必须同时解决mouse probe的耗时。
4.3 延迟probe
通用Linux系统需要支持多种IO设备,而鼠标键盘是比较常用的输入设备,特别是鼠标,产品繁多,接口多样。系统启动过程中加载鼠标驱动后,需要扫描多种IO总线来探测鼠标设备,这一过程非常耗时。
依据前面提到的优化方法,我们有两种方案:
- 对云环境定制鼠标驱动,固定探测virtio设备;
- 将鼠标探测从启动关键路径剥离,延迟探测,与后面系统启动服务并行;
第一种方案需要重构相关代码,成本较高;而且定制化限制较多,无法与开源社区协作。因此需要思考第二种方法:延迟探测。一种简单可行的方法是将原本内置(built-in)的设备驱动重新编译为内核模块(kernel module),因内核模块存放在根文件系统,所以加载时机被动推迟到根文件系统挂载之后,此时内核已经启动完成,自然与用户态初始化进程并行执行。
测试结果如下:(注意这是优化后的内核本地测,cloud-init被禁用)
带initrd启动:
不带initrd启动:
可见,内核启动时间缩减约200ms,优化掉initrd systemd时间;鼠标设备探测延后至userspace初始化阶段,导致userspace启动时间略有增加。获得预期的启动时间优化。
4.4 内存初始化优化
内存初始化也是内核启动热点之一,特别是在大规格实例上,内存初始化耗时占比较高。图中为750GB实例内存初始化耗时:
meminit耗时近2s
buddy init耗时1.8s
内存初始化动作是在内核启动的关键路径上,优化思路是并行初始化。因内存初始化时机较早,系统多CPU还未初始化完成,所以需要将内存初始化延后至CPU初始化完成之后,采用多线程并行执行内存初始化。这部分工作社区已经完成,通过内核配置CONFIG_DEFERRED_STRUCT_PAGE_INIT
来开启。
开启后,内存初始化延后,按NUMA node并行执行:
前半部耗时约0.2s
后半部耗时约1.3s
4.5 free initmem修复
Alinux 2 内核启动优化前有一个概率性的启动热点,free initmem到buddy系统时,会大概率(超过70%)出现200ms以上延时,dmesg日志显示如下,耗时超过200ms:
[ 0.687494] rtc_cmos 00:00: setting system clock to 2020-03-03 15:09:38 UTC (1583248178)
[ 0.915315] Freeing unused kernel image memory: 1836K
经分析,发现是社区已知问题,并在新内核已经修复 于是backport回Alinux 2 LTS内核,修复后耗时约5ms,基本消除这部分延时:
[ 0.482477] rtc_cmos 00:00: setting system clock to 2020-03-03 15:01:41 UTC (1583247701)
[ 0.487438] Freeing unused kernel image memory: 1856K
4.6 ORC unwind初始化
内核中有一些静态表,需要在内核初始化阶段排序,有些表体积较大,初始化耗时占比也不容小觑。如ORC unwind表格初始化排序,耗时约90ms:
[ 0.087330] clocksource: refined-jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1910969940391419 ns
[ 0.179718] random: get_random_bytes called from start_kernel+0x8b/0x563 with crng_init=0
这些静态表格是在内核构建阶段生成,因此可以将排序动作从内核初始化阶段移除,放到内核构建阶段完成,以节省内核初始化时间。经调查发现社区已经有类似的优化方案,异常处理表(exception table)排序移植到了内核构建阶段完成。于是对异常处理表改进,增加了ORC unwind表格构建阶段排序优化,系列patch已经合入主线。
优化后基本削减了这部分耗时:
[ 0.037253] clocksource: refined-jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1910969940391419 ns
[ 0.040714] random: get_random_bytes called from start_kernel+0x8b/0x563 with crng_init=0
4.7 Cloud-init优化
Alinux 2采用systemd启动用户态系统服务进程,以实现最大化并行启动。在到达启动完成的ssh登陆状态前,依赖一系列必要的系统服务,其中cloud-init是关键链路中一个耗时热点,启动画像中可以看出cloud-init几乎占整个系统启动时间的一半,因此优化cloud-init能够获得较大的启动性能收益。
在cloud-init服务中,一个耗时的配置任务是用户密码配置,需要从metadata服务器获取账号密码,完成配置。Alinux 2 LTS 内核开启了 Qemu firmware configuration
功能,能够通过qemu透传一些诸如账号密码的配置到虚拟机内部,使得cloud-init能够本地读取配置信息,加快cloud-init配置动作。感谢阿里云镜像团队跟阿里云虚拟化团队共同努力,即将推出InnerPasswd功能,加速Alinux 2 LTS 实例的cloud-init配置,提升实例启动时间,敬请期待!
4.8 其它优化
另外,启动阶段的console输出也是一个相对耗时的动作,因为串行口的波特率是固定的,大量是输出会形成阻塞导致console enabled延时较大。例如:
开启console output,console耗时2.6s!:
配置内核参数quiet
,关闭console output:
5. 下一步工作
虽然Alinux 2 LTS在启动优化已经取得了不错的效果,启动性能得到进一步提升,但仍然还有进一步挖掘的空间。特别是内存初始化这块,仍然是大规格实例启动热点。即便已经开启的deferred page init特性,但内存初始仍然限于node间并行,而node内并行初始化值得进一步挖掘,特别对当前ECS实例大都为单node实例(NUMA关闭)的场景下,理论上有更大的收益。
据了解社区已经有贡献者在着手进行相关工作,值得期待。