【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

应用程序热补丁(三):完整的设计与实现

  • 2017-04-20
  • 本文字数:4126 字

    阅读完需:约 14 分钟

前言

在前两篇文章介绍了应用程序热补丁的关键技术:

  • 修复运行时进程的函数
  • 加载热补丁到进程中
  • 自动生成热补丁等等

这些是组成应用程序热补丁技术框架的关键部分,但是在生产环境中使用热补丁技术还需要考虑适应现代软件的属性、热补丁的安全性、以及在运营中对热补丁的管理等等。

通过介绍 UCloud 应用程序热补丁框架的设计理念和框架中各个组件,我们会解决以下实践中遇到的问题:

  • 热补丁的管理(加载、卸载、激活、回滚热补丁等)
  • 打入热补丁时的安全检查(简单说是什么时候打入热补丁是安全的)
  • 热补丁对多线程的支持等等。

应用程序热补丁的意义介绍 UCloud 应用程序热补丁框架之前,首先介绍一下我们为什么研发和使用热补丁技术。

目前主流的热补丁技术,例如 Ksplice、kpatch、kGraft、以及后来的 livepatch 等都是特别针对 Linux 内核的热补丁技术,可以在不重启系统的情况下,修复内核缺陷。我们一般称为内核热补丁。

UCloud 使用了内核热补丁修复了若干内核问题,避免了重启系统导致的服务中断,保证了操作系统本身的可用性。

在此基础上,我们的下一个目标提高是核心组件中的单点的可用性。例如虚拟化的核心组件 QEMU,虽然作为单点程序运行,但是可用性的要求和内核是一致的。虽然 QEMU 本身支持在线迁移,可以迁移客户的虚拟机到新版本的 QEMU 上,但是迁移本身比较笨重。在迁移过程中会牵扯多个模块,例如网络、存储等,同时迁移时间和虚拟机的 downtime、break time 在运营上也会带来挑战。

对比 QEMU 通过在线迁移升级,使用热补丁修复极快,并且对虚拟机周边环境没有依赖,可以对用户的虚拟机做到静默升级。由于热补丁本身的天然属性,热补丁更适用于代码改动较小的修复(例如安全漏洞),而在线迁移升级比较适用于大版本的升级。

在 UCloud 我们通过热补丁修复了若干次 QEMU 的缺陷和安全漏洞,极大提高了可用性和安全性。因此我们认为对于代码改动较小的问题时,热补丁是一个完美的解决方案。

为什么要自研应用程序热补丁技术?答案也很简单,我们无法找到一个实用并且易用的应用程序热补丁技术,同时也由于我们已经在内核热补丁领域的具有一定的积累,所以决定敢为人先、自研应用程序热补丁技术。

设计理念提出需求

介绍设计理念之前,首先应该提出应用程序热补丁在 UCloud 云平台的需求:

  • 应用程序热补丁的适用场景和内核热补丁是一致的,目的是修复缺陷,而不是增加功能和升级版本。所以应用程序热补丁必须允许函数级别上的修改(不论是本地函数还是全局函数)。
  • 应用程序热补丁必须是安全的,也就是打入热补丁的前后进程的状态必须一致,热补丁只会操作修改的函数,不会影响进程的正常运行。
  • 应用程序热补丁必须支持云平台环境中现代软件具有的特性,比如说 Linux x86_64、多线程等等。
  • 热补丁必须由工具来构建,也必须要由工具来管理(加载卸载等)。
  • 热补丁必须同时支持回滚,同时支持一个进程多次热补丁修复。
  • 降低热补丁运营的难度。

我们针对这些需求,设计出如下的应用程序热补丁框架。

设计思路

  • 支持修复函数级别的、并且可以自动化生成热补丁的工具。
  • 支持多线程、热补丁安全检查、多热补丁状态管理的热补丁加载工具。
  • 运行中的应用程序应记录热补丁的信息和状态,可供外部工具查询。

或者简单来说,我们要做到,拿到源码和 patch 就能通过工具自动生成热补丁,热补丁可以安全的打入运行的多线程应用程序中(不会引起程序的错乱和崩溃),并且支持打入多个热补丁。打入后的热补丁可以被回滚取消,可以查询当前应用程序中热补丁的状态和信息。

框架组件

这个框架的设计我们通过以下组件实现:

Creator

负责通过 patch 和源码自动化生成热补丁。

支持函数级别修复,不论本地函数还是全局函数。

Loader

负责加载热补丁到目标进程中,也负责管理热补丁(类似于客户端程序)。

目标进程支持 Linux x86_64、多线程等。

支持热补丁安全检查。

支持对热补丁状态的操作(例如加载、卸载、激活、回滚查询等等)。

Core Runtime

负责记录多个热补丁的状态和信息,同时提供热补丁通用操作。

作为热补丁模块的通用运行时框架被 Loader 加载到目标进程中。

Loader 对热补丁状态的操作最终由 Core Runtime 在目标进程空间中执行。

热补丁(补丁本身)

负责提供修复后的替换代码和额外信息。

被 Loader 加载到目标进程中,注册自己的信息到 Core Runtime

在激活后使用自身包含的替换代码代替有问题的函数。

组件之间协作如下图所示,Creator 工具根据程序源码和 patch 生成热补丁模块,然后 Loader 将热补丁模块加载到目标进程 Process 的地址空间里,最后热补丁和通用运行时 Core Runtime 一起完成热修复。

实现方法

接下来分别讲各个组件的实现:

Creator

基于对多种内核热补丁技术的理解,我们认为应用程序的热补丁也是可以通过工具自动生成的。虽然相比内核,应用程序的格式更加复杂、编译链接的过程也更不固定,但是自动生成热补丁应该是可行的。

我们知道,编译源代码之后会生成目标文件,将单个或多个目标文件链接可以生成可执行文件。目标文件和可执行文件都是 ELF 格式(Executable and Linkable Format)。ELF 是一种标准且通用的文件格式,Linux 上的可执行文件、目标文件、库、core dump 都是 ELF。

Creator 工具根据 ELF 标准的格式,解析修复前后的目标文件,找到前后不同的函数,提取出差异(包括改变和新增的函数),连同差异本身的属性信息,生成一个动态链接库格式的热补丁。如下图所示:

之前的文章介绍过二进制比较生成热补丁替换代码,这里不再赘述。

Loader

Loader 工具作为一个客户端程序,操作目标进程 Process,包括热补丁的加载、激活、回滚、卸载、查看等。如下所示:

Loader 利用了 ptrace 调用。Loader 通过 ptrace 可以对 Process 的内存、寄存器进行读写,改变 Process 的运行状态,也可以捕获 Process 的信号。这样 Loader 可以停止 Process 的运行,并根据 AMD64 ABI 对内存和寄存器进行修改,使 Process 执行 dlopen 等函数,加载热补丁到 Process 的地址空间中,或者执行其他热补丁的操作。热补丁被加载之后,在 /proc/pid/maps 文件中可以看到。

Loader 如何利用 ptrace 加载热补丁在之前的文章中有详细描述,不再赘述。

Loader 随后会停止 Process 所有的线程,准备激活热补丁,此时 Loader 需要进行一致性检查,也就是查看热补丁的激活对当前的所有线程来讲是否安全。需要检查的是热补丁的需要替换的函数是否在线程当前函数调用栈上,如果在调用栈上,说明现在是不安全的,激活热补丁不能保证一致性。

在停止所有线程的时候,首先需要得到全部的线程信息,可以通过 libthread_db 库与进程中 libc 进行交互得到线程的信息,也可以通过 /proc/pid/tasks/ 目录从内核中直接得到线程的信息。在停止所有线程之后,需要再次获取所有线程信息,查看是否有新增线程,如果有需要停止新增线程。重复以上动作直到没有新线程出现。

Core Runtime

在一个进程的生命周期中,可能需要多次热补丁修复,同时多个热补丁也会使用一些通用的功能,因此需要一个通用的核心模块来提供通用功能,并且记录进程中每个热补丁的信息。这个通用模块作为一个动态链接库,我们叫做 Core Runtime。

Loader 在加载热补丁时,首先需要加载 Core Runtime 到进程的地址空间,对进程而言,Core Runtime 只需要加载一次。

在热补丁被加载到进程的地址空间后,通过构造函数,首先向 Core Runtime 提供自己的信息,注册到 Core Runtime 里,然后将热补丁中差异函数的需要重定向的部分手动计算重定向。在激活热补丁的时候,Core Runtime 会根据热补丁注册时得到的信息,保存旧函数,并把旧函数入口位置替换成跳转到新的函数的机器码,完成热修复。如下所示:

在回滚热补丁的时候,Core Runtime 把旧函数入口位置恢复,完成回滚。

Core Runtime 同时可以管理多个热补丁,以热补丁的名字作为 ID,区分不同的热补丁,记录必要的信息。

如下所示:

热补丁(补丁本身)

这里的热补丁指的是狭义上的作为动态链接库被 Loader 加载到目标进程 Process 中的热补丁。

热补丁由 Creator 产生,包含了替换代码和一些动态信息(比如新旧函数的地址、大小、重定向信息等)。热补丁被加载后,包含的函数和变量就存在于目标进程的地址空间中。热补丁激活以后,所有对老函数的访问,都会重定向到热补丁地址范围内的新函数。

如下所示:

总结

Creator、Loader、Core Runtime、热补丁这四者构成了 UCloud 热补丁技术框架,这四个组件相辅相成,互相协作完成热补丁。

Creator 负责生成热补丁,Loader 负责热补丁的进程外管理(包括加载、卸载、激活、回滚热补丁等),Core Runtime 负责热补丁的进程内管理(记录热补丁、备份旧函数、恢复旧函数等)。虽然是四个组件,但是都必须遵守同一个热补丁规格标准,这样才能共同完成热补丁的工作。

通过这个框架,极大降低了我们制作热补丁、打入热补丁和运营热补丁的难度。

例如,一个 QEMU 安全漏洞修复的流程可以简化为:

  1. Creator 通过 QEMU 源码和漏洞修复 patch 生成热补丁。
  2. 热补丁被 Loader 打入正在运行的应用程序中(加载并且激活)。
  3. (可选)对运行中的应用程序查询热补丁的状态和信息。
  4. (可选)对已经打入的热补丁进行回滚和卸载。

值得指出的是,目前不是全部 patch 都可以自动生成热补丁,原因是极少部分由于程序修改复杂,但是可以通过手动修改 patch 简化代码或者简化逻辑做到可以自动生成热补丁。大约 90% 的 patch 在无需修改的情况下都能自动生成热补丁。

在一些特定场景下,我们通过第一篇文章(《应用程序热补丁(一):几行代码构造免重启修复补丁》)中介绍的热补丁技术手动编写热补丁即可,无需使用复杂的自动生成热补丁技术。

另外,目前 UCloud 应用程序热补丁技术支持 Linux C 语言程序,但对于其他编译型语言解决思路基本一致(例如 C++ 等)。

在 UCloud,我们利用应用程序热补丁修复了若干紧急安全漏洞和缺陷,在关键时刻迅速解决问题,相比于传统的软件升级方式,解决问题更加及时。

希望通过一系列的文章填补目前应用程序热补丁的空白部分,使更多人了解热补丁的技术原理,让热补丁技术给更多人带来更多的价值。


感谢孟夕对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-04-20 19:002910

评论

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

命令行操作Java程序的那些事~

Bob

Java 命令行 8月日更

oeasy教您玩转vim - 14 - # 行头行尾

o

金融级IT架构:网商银行是如何进行数字化落地的

博文视点Broadview

Linux之netstat命令

入门小站

Linux

Android开发:获取安卓App版本号的方法步骤

三掌柜

8月日更

手撸二叉树之将有序数组转换为二叉搜索树

HelloWorld杰少

数据结构与算法 8月日更

small-spring 代码贡献者3个月,敢说精通Spring了,分享我的总结!

小傅哥

spring 小傅哥 cglib aware BeanPost

Mybatis自定义拦截器与插件开发

码农参上

8月日更

Web 框架 Gin | Gin 介绍

xcbeyond

Go 语言 gin 8月日更

如果面试官问你 JVM,额外回答逃逸分析技术会让你加分!

陈皮的JavaLib

Java 面试 JVM 逃逸分析 8月日更

Go语言那些事儿之管道的关闭

Regan Yue

Go 语言 8月日更 管道

LeetCode题解:781. 森林中的兔子,贪心,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

从0开始的TypeScriptの五:webpack打包typescript

空城机

JavaScript typescript 大前端 8月日更

全球增长最快的对象存储开源系统MinIO

liuzhen007

8月日更

JavaScript Array 方法详解

程序员海军

JavaScript 方法 大前端 array 引航计划

Django 做个小后台,细节在完善一点点,滚雪球学 Python 第三阶段

梦想橡皮擦

8月日更

netty系列之:自动重连

程序那些事

Java Netty 程序那些事 响应式系统

Vue进阶(二十七):Vuex 之 getters, mapGetters, ...mapGetters详解

No Silver Bullet

Vue vuex 8月日更

七夕赶上服务器架构升级,女朋友的约会怎么办

华为云开发者联盟

华为云 FunctionGraph DevStar Serverless架构 服务器架构

Prometheus监控的4个黄金指标

Rubble

Prometheus 8月日更

Android开发:引入重复包报错Error:Execution failed for task ‘:app:transform...’解决方法

三掌柜

8月日更 8月

在线年龄计算器

入门小站

工具

一文带你了解 TreeMap ,LinkedHashMap 的主要特点

4ye

Java 后端 hashmap LinkedHashMap 8月日更

Rust从0到1-模式-相关语法

rust 语法 模式 Patterns Syntax

【LeetCode】从上到下打印二叉树Java题解

Albert

算法 LeetCode 8月日更

在openEuler上做开发?这个大赛拿出30万寻找开源的yyds

华为云开发者联盟

开源 操作系统 服务器 openEuler 鲲鹏

Ipfs靠谱吗?ipfs中国授权公司都有哪些?

分布式存储 区块链+ IPFS fil

【设计模式】代理模式

Andy阿辉

C# 后端 设计模式 8月日更

【Flutter 专题】70 图解自定义 ACEStepper 步进器

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 8月日更

MinIO Client 使用(一)

耳东@Erdong

Minio 8月日更 mc minio client

失败的小项目-外卖cps

箭上有毒

8月日更

应用程序热补丁(三):完整的设计与实现_语言 & 开发_王超_InfoQ精选文章