NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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

  • 2019-09-25
  • 本文字数:6016 字

    阅读完需:约 20 分钟

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

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


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

从头到尾使用 Debian 软件包?

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


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


第三方依赖项的更新:使用少量 Debian 软件包,这容易让人觉得只需要为自己的软件负责,而不需要为一体机中运行的其他软件负责。如果你只使用了自己的 datto.deb,但此时 Apache、Samba、libc 甚至 PHP 的更新管理工作其实同样重要。鉴于我们作为 Datto,本身所销售的就是完整的一体机,当然也就需要负责管理整个栈,尤其是第三方软件的安全补丁等内容。


服务重启动/重引导:对于一些需要重启动服务甚至重引导计算机的大型更新,依赖项问题也会变得异常棘手。当然,Debian 软件包自己就应该能处理服务重启动问题,但实际上并非所有软件包都能妥善搞定。并且一旦需要重引导(例如需要升级系统内核),还需要确保不会打断重要的设备任务(例如备份、虚拟化……),此外还要保证设备最终能引导成功(这事情并不像你想的那么容易,下文将会详细介绍!)。


发行版升级:如果整个操作系统的版本需要升级,这才是最麻烦的地方。举例来说,如果只使用 apt-get dist-upgrade 命令以及 reboot 命令将 Ubuntu 10.04 升级到 16.04,整个过程将变得漫长无比,并且很多时候可能会升级失败(只要你用过 usedapt-get dist-upgrade,那么肯定会明白)。


数千个版本和状态:在 Debian 的升级模型中,“设备”的实际行为其实和普通计算机无异:刚刚创建好镜像并部署后,一切都是崭新的,一切都可以正常运转。但随着镜像越来越老,操作系统退化的问题就变得越严峻,导致不同设备的状态产生巨大差异。能严重到什么程度?我们的设备(在切换到 KVM 前)曾经使用了 40 个不同版本的 VirtualBox、25 个不同的 ZFS 版本,以及超过 80 个不同的 Linux 内核!


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

使用镜像,而非软件包!

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

前提要求

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


所有设备沿用相同的升级路径,并且只存在一个升级路径。


所有设备均可通过这种方式升级(哪怕操作系统盘较小的老设备)。


从一个版本切换到另一个版本的过程必需满足原子性要求(或尽可能满足这种要求)。


(如果升级失败)能够回滚到上一个版本。


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


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

那么镜像到底是什么?

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


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


Ubuntu 基础操作系统(内核、系统库……)


必需的第三方工具和库(Apache、KVM、ZFS……)


Datto 设备软件(我们的营销团队将其称之为 IRIS)


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



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

基于镜像的升级

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

构建镜像

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


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


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



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

分发镜像

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


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


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



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


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


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


引导镜像

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

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

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


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


ZFS-on-root和 A/B 数据集:我们的镜像备份操作中大量使用了 ZFS,因此一开始很自然就觉得也可以将 ZFS 用作一体机的根文件系统。为此可以将 BCDR 一体机的镜像作为一个 ZFS 数据集(而非上文提到的 zvol)来进行分发,对其进行克隆并直接引导至 ZFS 的克隆副本。由于Grub的新版本已经可以支持读取 ZFS,此外还提供了 ZFS initramfs 模块,ZFS-on-root 绝对是可行的。如果要从一个镜像升级到下一个(例如从一个 ZFS 数据集升级到下一个),只需要更新 Grub 的配置并重引导就行。这种方式可以正常起效,但因为引导至 ZFS,这是一种比较新的做法,我们认为其成熟度还不足以满足我们产品的需求。不予考虑。


简单的 A/B 分区:有些一体机和手机会使用两个分区,其中一个包含当前系统,另一个包含下一个系统。这种思路也很简单:下载新镜像,将其 Rsync 到不活跃分区,更新 Grub,然后重引导。然而这种做法的问题在于,我们的有些设备不具备额外创建一个分区所需的存储空间(或者至少需要重建分区)。我们在实验中尝试过在首次重引导过程中,从 initramfs 内部将活跃根分区拆分为两个并且成功了(挺酷的对吧),但考虑到这将要用于我们的主要产品,该方法风险太大。不予考虑。


引导至 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),这样就可以照常使用 linux 和 initrd 命令找到内核和 RAM 磁盘。例如我们在设备 grub.cfg 文件中(通过/etc/grub.d 中的钩子)生成的菜单项范例如下所示:


menuentry 'Datto OS (v415.0)' {  search --set=root --no-floppy --fs-uuid 8c43bf01-046c-401c-8cb8-97cb658ef698  loopback loop /images/415.0.img  linux (loop)/vmlinuz root=UUID=8c43bf01-046c-401c-8cb8-97cb658ef698 rw loop=/images/415.0.img ...  initrd (loop)/initrd.img}
复制代码


在这个例子中,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 中设置而来,指向了一个镜像文件:



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


镜像间的升级

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


清理老镜像,下载新镜像,导入到池,导出到镜像文件。


将配置从当前镜像迁移到下一个镜像。


更新 Grub 以指向新镜像。


重引导。


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



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 中文站翻译并分享


原文链接:


80k operating system upgrades every two weeks


2019-09-25 18:022744
用户头像

发布了 283 篇内容, 共 102.1 次阅读, 收获喜欢 61 次。

关注

评论

发布
暂无评论
发现更多内容

Elasticsearch文档版本冲突原理与解决

Skysper

elasticsearch 乐观锁 悲观锁

翻译: Effective Go (2)

申屠鹏会

翻译 Go 语言

精纯还是混乱?职场十二箴言——重读“成为乔布斯”的思考(二)

石君

创业 乔布斯 成为乔布斯

【SpringBoot】掌握这两个属性,你的测试类可以启动的更快些

遇见

Java Spring Boot Unit Test

如何做一名失败的安全架构师

石君

架构 安全架构师 安全评估

删掉最后一句话

池建强

心理学 情绪控制

《小狗钱钱》——财富离我们并不遥远

Yin

读书笔记 投资 成长 思维方式

我的第一个千万阅读量

彭宏豪95

创作 生活 写作

简单到不可能失败 —— 《微习惯》

零和幺

读书笔记

如何解决 Kubernetes 的 DNS 延迟问题

倪朋飞

Kubernetes 微服务 云原生

走出舒适区最好办法别走了,扩大它

乐少

小议RPA

一品凡心

人工智能 RPA 自动化

任正非管理哲学中的三个常识和三种科学

霍太稳@极客邦科技

创业 团队管理 华为

做产品的同理心

孙苏勇

产品 产品经理 产品设计

一篇文章搞定 java 中的 path 和 classpath

shengjk1

Java classpath vs path classpath path

做小池塘里的大鱼,还是大池塘里的小鱼?这是个问题。

霍太稳@极客邦科技

创业 团队管理 目标管理

特别评论:甲骨文的傲气

张晓楠

云计算 互联网巨头 企业文化

Kubernetes中的CI/CD

倪朋飞

Kubernetes DevOps 微服务

GitHub知错就改,是个好同志

遇见

GitHub

是时候要说再见了,春风十里,不如邮你!

乐少

精纯还是混乱?职场十二箴言——重读“成为乔布斯”的思考(一)

石君

职场 乔布斯 成功学

分布式数据库是无用的屠龙术吗?

海边的Ivan

企业架构 分布式数据库 业务中台

探究vscode debug流程,解决无法运行go程序的问题

simpleapples

vscode Go 语言

dubbo-go 中如何实现远程配置管理

joe

Apache 开源 微服务 dubbo Go 语言

【深度】为您解读东西方艺术教育的专业设置差异对比~

默聲

用你喜欢的 emoji 作为页面的 favicon 吧 🎉

遇见

CSS html favicon emoji

我如何用 Python 给 Github 的 README.md 做一个访客统计功能

遇见

Python GitHub 开源 badge open-source

HTTP Methods和RESTful API的设计

孙苏勇

架构 系统设计 RESTful 接口

喔,明白了,成功也是一种苦难

霍太稳@极客邦科技

创业 身心健康 企业文化 个人成长 心理

无代码开发

Fenng

Kubernetes 容器运行时演进

倪朋飞

Kubernetes 容器 云原生

数千台设备的软件和操作系统,每两周升级一次,这是怎么做到的_架构_Philipp Heckel_InfoQ精选文章