[关闭]
@liuhui0803 2019-09-23T15:57:30.000000Z 字数 6704 阅读 1134

数千台设备的软件和操作系统,每两周升级一次,这是怎么做到的

Bcdr Grub ZFS Debian


任何曾经管理过几十上百台物理服务器的人都知道:确保所有服务器始终安装最新安全更新,或者保证所有服务器的配置和状态相一致,这始终是一件很难完成的任务。为了解决这个问题,系统管理员通常会使用PuppetSalt等工具,或将应用程序部署到容器中。如果整个环境都能由你控制,这些当然都是很棒的方法,但如果你使用了类似BCDR一体机之类的设备(或者任何未部署在自己基础架构内的一体机/服务器设施),这些方法往往就不怎么实用了。除此之外,替换系统内核、安装大型系统升级,或安装其他需要重启的大型补丁,此时也无法适用这些方法。

当我们使用的BCDR设备面临这些问题后,我们开始寻找其他更可行的方法,并且真的有所收获。近两年来,我们为超过80,000台设备使用了这种方法,效果一直很稳定。本文我将谈谈我们是如何通过镜像、回环设备(Loop device)以及大量和Grub有关的“魔法”解决这个问题的。如果对此话题感兴趣,欢迎继续阅读下去。

从头到尾使用Debian软件包?

我们的BCDR一体机始终运行了Ubuntu,因此在更新软件时,最自然的方法就是使用Debian软件包。过去很长时间以来都是这样做的:每两周,我们会为Ubuntu 10.04/12.04(没错,我知道你有疑问,请继续读下去!)构建所需的发布,经过全面测试后将其正式部署出去。

很长时间以来这样做完全没问题,但这种做法有一些很明显的不足之处:

其实,实际遇到的问题远比上面列出的更多,不过这里就不拿更多问题来给大家添堵了。快速开始介绍最有趣的内容:如何解决!

使用镜像,而非软件包!

鉴于会遇到这么多问题,很明显,我们需要用更好的解决方案来管理设备状态和配置。产品中不同的设备配置/软件包/版本数量不仅要降至最低,并且在每次升级时必需能保证能够升级整个栈:不仅要能升级我们自己的软件,还要能升级第三方软件,甚至诸如Libc或系统内核等系统库。

前提要求

随后我们开始确定这个解决方案的前提要求,其实这些要求并不多:

而这些要求还暗含了一个最重要的前提条件:不能继续使用基于软件包的升级方法了,并且(从字里行间也能体会到)在升级过程中重引导一体机,这是可以接受的。

这些都是很大胆的念头。我们确实做出了一个重大决定!

那么镜像到底是什么?

为了减少配置的数量,我们决定不再将我们的软件及其所有依赖项看作不同个体,而是将所有这一切组合成一个统一的可交付物:镜像。

那么镜像到底是什么?镜像(在我们的环境中)是指一种EXT4文件系统,其中包含了引导和运行BCDR一体机所需的一切,例如:

下图就显示了一个这种镜像所包含的内容:

01.png-34kB

我们对这种想法非常激动,因为通过使用镜像,只需要一个数字,也就是镜像的版本号(例如上图中的“415”)就可以定义所安装的每个软件的具体版本。再也不用针对多种ZFS版本测试我们的软件,更不用暗自祈祷我们的软件能兼容所有KVM版本。太棒了!

基于镜像的升级(Image based upgrade,IBU)

做出所有这些重要决定后,我们依然需要通过某种方法来构建、分发,并在设备上引导这些镜像。具体怎么做呢?

构建镜像

通常来说,每次标记了一个新的发布(或发布候选)后,我们会自动构建镜像:每次在Git中推送标签后,一个CI工作进程会开始构建镜像。构建过程本身也挺有趣,不过已经超出了本文的范围,但为了不吊大家胃口,下文将简单介绍这个过程:

我们首先会为自己的软件构建Debian软件包,并将其发布至一个Debian仓库。随后使用aptly(参阅“Datto packages”一图)为这个Debian仓库创建快照,同时还会定期对一个上游Ubuntu仓库(“Upstream packages”)执行类似操作。随后使用debootstrap创建一个Ubuntu基准系统,并将我们的所有软件及其依赖项安装到一个Chroot中。一旦完成这些操作,会对其创建Tar归档并Rsync到我们自己的镜像服务器。在镜像服务器上,我们会提取出Tarball并Rsync给最新镜像,这个最新镜像位于一个格式化为EXT4文件系统的ZFS卷(zvol)中。在将所有未使用的EXT4块归零后,会对包含该文件系统的zvol创建最终快照。

因此在镜像服务器上可以看到类似下图所示的内容:

02.png-44.8kB

上述zvol包含了我们BCDR一体机的EXT4文件系统。这就是一个镜像,也是我们唯一需要交付的东西。它可以作为一个整体进行测试,一旦通过了QA流程,就可以分发到客户的BCDR设备中了。

分发镜像

在成功构建镜像后,又该如何将其从我们的数据中心发送给超过8万台设备?很简单,我们使用了ZFS send/recv

我们的所有设备都具备ZFS池,其中存储了设备的镜像备份,并且之前我们就在大量使用ZFS send/recv为这些备份提供离场保存能力。而此时只不过是换种方向使用这种技术。

我们是这样做的:需要升级时,会让一部设备通过HTTPS下载ZFS sendfile diff(之前曾经尝试过直接通过SSH使用ZFS send/recv,但这种方式无法进行缓存):

03.png-377.6kB

从上图中可以看到,通常并不需要下载完整镜像,因为设备以前就升级过,已经在本地池中保存了镜像的一个版本。这就很棒了:通过这种技术,我们可以进行差异化的操作系统升级,也就是说,设备只需要下载镜像中有变化的块。

这是一种双赢的结果,因为不会过多占用客户网络带宽,而我们自己的数据中心也可以节约一笔带宽费用。

下载好的镜像会被导入本地ZFS池。这对于下一次升级很必要(可以确保只需要下载有变化的内容):

04.png-218.4kB

引导镜像

拿到镜像后,如何引导至这个新的文件系统?如果我们构建的每个镜像版本都是全新操作系统,又该如何从一个版本引导至下一个版本?

ZFS-on-root、A/B分区和A/B文件夹

毫无疑问,这些问题的答案并不只有一种。我们可以通过多种方法使用镜像生成可引导的系统,因此需要多次实验找出一种最佳方法。

这个过程也很有趣,因此我准备简要介绍每种方法,以及最终未选择这些方法的原因:

  1. ZFS-on-root和A/B数据集:我们的镜像备份操作中大量使用了ZFS,因此一开始很自然就觉得也可以将ZFS用作一体机的根文件系统。为此可以将BCDR一体机的镜像作为一个ZFS数据集(而非上文提到的zvol)来进行分发,对其进行克隆并直接引导至ZFS的克隆副本。由于Grub的新版本已经可以支持读取ZFS,此外还提供了ZFS initramfs模块,ZFS-on-root绝对是可行的。如果要从一个镜像升级到下一个(例如从一个ZFS数据集升级到下一个),只需要更新Grub的配置并重引导就行。这种方式可以正常起效,但因为引导至ZFS,这是一种比较新的做法,我们认为其成熟度还不足以满足我们产品的需求。不予考虑。
  2. 简单的A/B分区:有些一体机和手机会使用两个分区,其中一个包含当前系统,另一个包含下一个系统。这种思路也很简单:下载新镜像,将其Rsync到不活跃分区,更新Grub,然后重引导。然而这种做法的问题在于,我们的有些设备不具备额外创建一个分区所需的存储空间(或者至少需要重建分区)。我们在实验中尝试过在首次重引导过程中,从initramfs内部将活跃根分区拆分为两个并且成功了(挺酷的对吧),但考虑到这将要用于我们的主要产品,该方法风险太大。不予考虑。
  3. 引导至A/B目录:由于一些设备缺乏备用分区,我们还实验过将镜像的两个副本保存到根分区中的两个文件夹中(例如一个/images/412和一个/images/415),随后修改initramfs引导至/images/415,而非引导至/。不管你信不信,虽然听起来挺疯狂,但这样做竟然也成功了,并且整个方法也超级简单,只要对initramfs进行少量修改:mount --bind /images/415 /root改成这样就行。一切都可以正常运转,不过很多Linux工具(df、mount……)会因为根目录不是/而遇到一些问题,所以这个方法也不予考虑。

循环往复,这就够了!

在尝试过用多种方法引导镜像后,我们最终采取的做法似乎感觉有些无趣。不过无趣也是好事对吧!

我们发现,如果要引导一个镜像,最简单可靠的方法是利用Grub的回环引导(Loopback booting)机制,并配合initramfs对Loop的支持(请参阅loop=...参数):

众所周知,Grub是种引导加载器(Boot loader)。它的责任是加载初始的RAM磁盘和内核。为此,Grub内置了对很多文件系统的读取能力,并能通过loopback命令支持稍后将要提到的“文件系统中的文件系统”。loopback命令可在根分区找到镜像文件并对其进行环回(Loop),这样就可以照常使用linuxinitrd命令找到内核和RAM磁盘。例如我们在设备grub.cfg文件中(通过/etc/grub.d中的钩子)生成的菜单项范例如下所示:

  1. menuentry 'Datto OS (v415.0)' {
  2. search --set=root --no-floppy --fs-uuid 8c43bf01-046c-401c-8cb8-97cb658ef698
  3. loopback loop /images/415.0.img
  4. linux (loop)/vmlinuz root=UUID=8c43bf01-046c-401c-8cb8-97cb658ef698 rw loop=/images/415.0.img ...
  5. initrd (loop)/initrd.img
  6. }

在这个例子中,Grub首先会通过search以及UUID寻找根分区(就像对常规安装的Ubuntu做的那样)。随后会发现根分区中的镜像文件/images/415.0.img,最后找到镜像中的内核((loop)/vmlinuz)和RAM磁盘((loop)/initrd.img)。

整个过程异常简单,但同时却非常酷:引导加载器竟然能这样做,这一点让我大为惊奇。

当Grub找到内核和初始RAM磁盘后,会将RAM磁盘载入内存(震惊!),随后挂载根文件系统,最后将控制权转交给init进程。

在Ubuntu中,initramfs-tools软件包提供了创建和修改初始RAM磁盘的工具。幸亏该软件包已经可以支持回环引导机制,因此一般来说除了需要在内核行传递loop=参数,其他什么都不用做。如果设置了该参数,initramfs会用回环的方式,使用mount -o loop(参阅源代码)将根文件系统加载至镜像。考虑到代码中有一条相当吓人的FIXME消息(# FIXME This has no error checking),我们认为最好能提高它的弹性,为其增加错误处理和fsck能力。不过大部分情况下,使用initramfs都可以顺利引导并且不显示任何信息。

就是这样,一个简单的解决方案,洋洋洒洒写了这么多。

这种方法在实践中用起来是这样的。如图所示,该设备的根文件系统位于/dev/loop0,该回环设备在initramfs中设置而来,指向了一个镜像文件:

05.png-105.8kB

本例中,镜像是位于根分区(如/dev/sda1)下的/images/412.0.img。请注意,如果镜像中存在空的/host文件夹,initramfs会将根分区挂载在这里:

06.png-109.4kB

镜像间的升级

我们已经可以构建、分发并引导镜像。如果将这一切结合在一起就会发现,从一个镜像到下一个镜像的升级其实一点也不难:

  1. 清理老镜像,下载新镜像,导入到池,导出到镜像文件。
  2. 将配置从当前镜像迁移到下一个镜像。
  3. 更新Grub以指向新镜像。
  4. 重引导。

我们所做的就是这样。为此还开发了一个名为upgradectl的工具:

07.png-1262.9kB

upgradectl通常可由我们的签入进程远程触发:在设备正常运转的过程中,它可以下载并导出镜像(第1步),借此在后台为升级过程做准备。需要进行升级时(通常是夜间的设备闲置时段),实际的升级过程将非常快速地完成,因为只需要迁移配置,更新Grub并重引导(第2-4步)即可。一般来说,升级过程中的设备停机时间约为5-10分钟,并且这主要取决于重引导所需的时间(大型设备可能需要更久,因为需要IPMI/BMC初始化)。

当然,这一过程中也有数不胜数的问题和边缘案例需要考虑:听起来确实简单,但想要做对其实并不容易,尤其是考虑到我们现有的8万台一体机中,有些在生产环境中连续运转已经有超过7年时间了。

但这也造就了一些有趣的挑战:我们已经将数千台设备从Ubuntu 12.04(甚至10.04)直接升级至Ubuntu 16.04。如果升级过程因为某些原因失败,会通过一些逻辑来处理老镜像的回滚。我们处理了完整的操作系统盘、有故障的硬件(磁盘、IPMI、RAM……)、配置为RAID的操作系统盘以及Grub无法向其中写入的问题,当然还有ZFS池出错、Linux进程挂起(D状态)、重引导挂起等各种问题。

但是你猜怎样:这一切都是值得的。这就好像结束了一场为期7年的寒冬之后进行的春季大扫除。我们让这些设备重新焕发了生机,并且这样的工作还将继续,每两周进行一次!

总结

本文介绍了如何将BCDR一体机的部署流程由基于Debian软件包的方法改为基于镜像的方法。此外还介绍了构建、分发镜像的方法,以及如何使用Grub的loopback机制引导镜像的做法。

虽然这种基于镜像的升级方法的诞生有我的全程参与,但这其中最让人激动的一点在于:借助这种机制,我们甚至可以在不同内核,以及不同的操作系统大版本之间切换。每次发布升级后,我们都可以有效地引导至一个全新操作系统,这意味着系统不会随着时间的延长而退化,所有手工改动都会被消除,甚至从技术上来看,还可以在愿意的情况下切换使用不同的Linux发行版。

并且这一切都是在后台进行的,完全无需用户介入,对用户来说完全透明:每两周对8万个操作系统进行升级,这该有多酷啊!

本文最初发布于Datto Engineering博客,原作者Philipp Heckel,经原作者授权由InfoQ中文站翻译并分享。点击阅读英文原文:How we upgrade the software and operating system of thousands of appliances every two weeks

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注