零代价修复服务器内核缺欠 UCloud内核热补丁技巧揭秘

零代价修复服务器内核缺陷 UCloud内核热补丁技术揭秘

7月18日,由InfoQ主办的ArchSummit全球架构师峰会在深圳拉开帷幕,此次会议重点选择了6个当前最受关注的领域,包括:游戏、电商、移动互联网等等。UCloud作为国内专注服务上述垂直领域的云服务商,受邀参加了本次大会。会上,UCloud资深工程师邱模炯还以《UCloud云平台的内核实践》为主题,给大家揭开了UCloud云平台内核技术的神秘面纱。其中,“UCloud内核热补丁技术”更是引发了全场架构师们的极大关注。

如何零代价修复海量服务器的Linux内核缺陷?

对于一个拥有成千上万台服务器的公司,Linux内核缺陷导致的死机屡见不鲜。让工程师们纠结的是,到底要不要通过给服务器升级内核来修复缺陷?升级意味者服务器重启、业务中断以及繁重的准备工作;不升级则担心服务器死机,同样造成业务中断和繁重的善后工作。

而在今天的云计算时代,一台宿主机往往运行多个云主机,每一次重启不管是主动升级还是被动死机,都意味着中断其上运行的所有云主机。因此,宿主机内核缺陷的修复更加棘手。

而作为一个支撑着上万家企业用户IT基础架构的云服务商,UCloud云平台上的海量宿主机又是如何修复内核缺陷的呢?

邱模炯透露,如果按照传统的重启方式来修复,那么无论是对于UCloud或是用户,都意味着繁重的运维和业务中断。但是,UCloud通过“内核热补丁技术”——即给运行中的内核打上二进制补丁,UCloud已经做到了零代价免重启修复海量服务器的内核缺陷!目前为止,UCloud对所发现的上游内核10+个缺陷全以热补丁方式修复,累计数万台次,无一例失败且无任何副作用;理论上避免了相应次数的宿主机重启及所隐含的云主机业务中断。这项技术在UCloud已经成熟。

UCloud 内核热补丁技术揭秘

UCloud的热补丁技术基于多年前的开源ksplice加以定制优化而来,通过加载一个特殊准备的热补丁模块来修复内核。其过程如下图所示:

图片 1

热补丁模块由ksplice程序编译生成,包含有缺陷的二进制指令和修复后的二进制指令(这些二进制按函数级别组织);模块加载后,自动定位到内核的缺陷处并以修复指令动态替换缺陷指令。

除了免重启修复,热补丁还用于内核开发过程的性能分析和故障定位。比如,加上性能统计代码生成热补丁,就可以在线分析感兴趣的性能问题;加入额外调试代码捕捉运行中内核的异常。这些非常有用,更是海量服务器里捕捉不可重现内核异常的不二法宝。由于热补丁不需要重启服务器,既可打入也可撤销,所以不会有副作用。

UCloud对开源Ksplice的优化主要在以下三个方面:

支持高版本内核

热补丁技术与内核紧密耦合。不同版本的内核在指令结构体,符合表结构体和一些特性上(比如早期内核没有ftrace)有所不同,直接影响热补丁成败。UCloud研究了各版本内核的区别,使得同一份ksplice支持各个版本的Linux内核。值得一提的是,解决了ftrace与ksplice不兼容的问题。

允许热修复频繁调用的函数

不管什么样的热补丁技术,两种类型的内核函数难以热补丁:频繁使用的内核函数如schedule,
hrtimer;经常处于线程栈内核部分顶部的函数,如sys_poll,
sys_read。UCloud更改了ksplice相关内核代码和用户态工具,成功解除了这些限制,比如UCloud现网服务器已打入了三个hrtimer热补丁。

减少业务中断时间

ksplice是在stop_machine后替换二进制指令的。虽然单次stop_machine对业务造成的中断在一毫秒左右,但有些频繁使用的内核函数需要大量重试才能碰到合适的热补丁时机,于是会造成最长达上百毫秒的中断。UCloud在此做过一点优化,使得业务中断时间控制在十毫秒级别。

海量服务器环境下热补丁技术可用来零代价且无副作用地修复内核缺陷,而且内核开发也因热补丁能走得更远更好。以前因为缺乏辅助分析手段和惧怕内核BUG,即使适合在内核实现的特性也被告诫移到用户态实现,然而有了热补丁,相关观念也可以适当调整,内核开发也可以更加大胆和跳脱。

UCloud内核热补丁技术揭秘
7月18日,由InfoQ主办的ArchSummit全球架构师峰会在深圳拉开帷幕,此次会议重点选择了…

如果发布了一个release包,但是里面出现了严重的事故级别(某些等不及下一版本迭代就得修复)的bug,那么站在公司角度,这边QA,开发,客服,甚至PR等部门都得加班赶忙发包重现覆盖,耗费的资源代价相当的大,而出问题的或许只是一两行代码;站在用户User角度,刚刚下载了一个包,准备尝鲜,发现又让更新,是不是很烦,如果新包打开就崩溃了,很多就会去市场评论,“垃圾,更新之后闪退”云云,抑或“垃圾,天天要更新”。在这个基础上热更新技术出现了,即使发出了release包,如果出现某些等不及下一版本迭代就得修复的bug,向用户下发Patch,在用户无感知的情况下,修复了bug或问题。

花了几天查阅一些资料,去了解一些目前的热更新方案,主要有以下两大类型:

不重启不当机!Linux内核热补丁的四种技术

图片 2

供图: Shutterstock

有多种技术在竞争成为实现Linux内核热补丁的最优方案。

没人喜欢重启机器,尤其是涉及到一个内核问题的最新补丁程序。

为达到不重启的目的,目前有3个项目在朝这方面努力,将为大家提供内核升级时打热补丁的机制,这样就可以做到完全不重启机器。

参考1
参考2
参考3
参考4

参考文章:理解 Android Hook 技术以及简单实战

Ksplice项目

首先要介绍的项目是Ksplice,它是热补丁技术的创始者,并于2008年建立了与项目同名的公司。Ksplice在替换新内核时,不需要预先修改;只需要一个diff文件,列出内核即将接受的修改即可。Ksplice公司免费提供软件,但技术支持是需要收费的,目前能够支持大部分常用的Linux发行版本。

但在2011年Oracle收购了这家公司后,情况发生了变化。
这项功能被合入到Oracle自己的Linux发行版本中,只对Oralcle自己提供技术更新。
这就导致,其他内核hacker们开始寻找替代Ksplice的方法,以避免缴纳Oracle税。

一:热修复相关

热修复概念:
以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载。
PathClassloader和DexClassLoader:
(1)PathClassloader作为其系统类和应用类的加载器,只能去加载已经安装到Android系统中的apk文件。
(2)DexClassLoader可以用来从.jar和.apk类型的文件内部加载classes.dex文件。可以用来执行非安装的程序代码。
(3)Android使用PathClassLoader作为其类加载器,DexClassLoader可以从.jar和.apk类型的文件内部加载classes.dex文件。
热修复原理:
PathClassLoader和DexClassLoader都继承自BaseDexClassLoader
在BaseDexClassLoader中有如下源码:

#BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class clazz = pathList.findClass(name);

    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }

    return clazz;
}

#DexPathList
public Class findClass(String name) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext);
            if (clazz != null) {
                return clazz;
            }
        }
    }

    return null;
}

#DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
    return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);1  n j                                                                                                                         
n 

1
BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的集合dexElements,而对于类加载呢,就是遍历这个集合,通过DexFile去寻找。
2
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
3
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:

图1

4
把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图:

图2

Kgraft项目

2014年2月,SUSE提供了一个很好的解决方案:Kgraft,该内核更新技术以GPLv2/GPLv3混合许可证发布,且Suse不会将其作为一个专有发明封闭起来。Kgraft被提交到Linux内核主线,很有可能被内核主线采用。目前Suse已经把此技术集成到Suse
Linux Enterprise Server 12。

Kgraft和Ksplice在工作原理上很相似,都是使用一组diff文件来计算内核中需要修改的部分。但与Ksplice不同的是,Kgraft在做替换时,不需要完全停止内核。
在打补丁时,正在运行的函数可以先使用老版本或新内核中对应的部分,当补丁打完后就可以完全切换新的版本。

二:阻止相关类打上CLASS_ISPREVERIFIED标志

dex校验: 如果两个相关联的类在不同的dex中就会报错,例如ClassA
引用了ClassB,但是发现这这两个类所在的dex不在一起,其中:

  1. ClassA 在classes.dex中
  2. ClassB 在patch.dex中
    结果发生了错误。

dex校验的前提:
如果引用者这个类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验。

相关类打上CLASS_ISPREVERIFIED标志的发生场景:
在虚拟机启动的时候,当verify选项被打开的时候,如果static方法、private方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么这个类就会被打上CLASS_ISPREVERIFIED
下图是class A 打上CLASS_ISPREVERIFIED标志

图三

下图是class A 没有打上CLASS_ISPREVERIFIED标志

图四

其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。

在class文件中插入代码来阻止相关类打上CLASS_ISPREVERIFIED标志:在dx工具执行之前,将LoadBugClass.class文件呢,进行修改,再其构造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后继续打包的流程。
原始代码

package dodola.hackdex;
public class AntilazyLoad
{

}

package dodola.hotfix;
public class BugClass
{
    public String bug()
    {
        return "bug class";
    }
}

package dodola.hotfix;
public class LoadBugClass
{
    public String getBugString()
    {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}

无需重启Application、无需启动Activity即可更新Java方法 安卓代码要达到真正“热”更新的效果,也只有基于AOP这种技术,就是在方法级别这个粒度做替换。

Kpatch项目

Red Hat也提出了他们的内核热补丁技术。同样是在2014年初 —
与Suse在这方面的工作差不多 — Kpatch的工作原理也和Kgraft相似。

主要的区别点在于,正如Red Hat的Josh
Poimboeuf总结的那样,Kpatch并不将内核调用重定向到老版本。相反,它会等待所有函数调用都停止时,再切换到新内核。Red
Hat的工程师认为这种方法更为安全,且更容易维护,缺点就是在打补丁的过程中会带来更大的延迟。

和Kgraft一样,Kpatch不仅仅可以在Red
Hat的发行版本上使用,同时也被提交到了内核主线,作为一个可能的候选。
坏消息是Red Hat还未将此技术集成到产品中。 它只是被合入到了Red Hat
Enterprise Linux 7的技术预览版中。

三:插入jar

插入代码

System.out.println(dodola.hackdex.AntilazyLoad.class)

在构造函数中插入操作代码(javassist)

package test;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

public class InjectHack
{
    public static void main(String[] args)
    {
        try
        {
            String path = "/Users/zhy/develop_work/eclipse_android/imooc/JavassistTest/";
            ClassPool classes = ClassPool.getDefault();
            classes.appendClassPath(path + "bin");//项目的bin目录即可
            CtClass c = classes.get("dodola.hotfix.LoadBugClass");
            CtConstructor ctConstructor = c.getConstructors()[0];
            ctConstructor
                    .insertAfter("System.out.println(dodola.hackdex.AntilazyLoad.class);");
            c.writeFile(path + "/output");
        } catch (Exception e)
        {
            e.printStackTrace();
        }

    }
}

把AntilazyLoad.class打包成jar包,然后写入App的私有目录,最后把该jar对应的dexElements文件插入到数组的最前面。

public class HotfixApplication extends Application
{

    @Override
    public void onCreate()
    {
        super.onCreate();
       //创建jar对应的文件
        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
       //将asset文件中的jar写到App的私有目录下面。
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
       // 把jar对应的dexElements插入到dex数组最前面。
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try
        {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }

    }
}

创建jar对应的文件

public class Utils {
    private static final int BUF_SIZE = 2048;

    public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {
        BufferedInputStream bis = null;
        OutputStream dexWriter = null;
        bis = new BufferedInputStream(context.getAssets().open(dex_file));
        dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
        byte[] buf = new byte[BUF_SIZE];
        int len;
        while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
            dexWriter.write(buf, 0, len);
        }
        dexWriter.close();
        bis.close();
        return true;

}

找相应的ClassLoader进行操作

public final class HotFix
{
    public static void patch(Context context, String patchDexFile, String patchClassName)
    {
        if (patchDexFile != null && new File(patchDexFile).exists())
        {
            try
            {
                if (hasLexClassLoader())
                {
                    injectInAliyunOs(context, patchDexFile, patchClassName);
                } else if (hasDexClassLoader())
                {
                    injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
                } else
                {

                    injectBelowApiLevel14(context, patchDexFile, patchClassName);

                }
            } catch (Throwable th)
            {
            }
        }
    }
 }

Combine(合并)App的DexElements和AntilazyLoad.class的DexElements

 private static boolean hasDexClassLoader()
{
    try
    {
        Class.forName("dalvik.system.BaseDexClassLoader");
        return true;
    } catch (ClassNotFoundException e)
    {
        return false;
    }
}


 private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
            throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException
{
    PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
    Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
            getDexElements(getPathList(
                    new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
    Object a2 = getPathList(pathClassLoader);
    setField(a2, a2.getClass(), "dexElements", a);
    pathClassLoader.loadClass(str2);
}

将Patch.jar补丁插入到APP中,过程和插入AntilazyLoad.class一样

public class HotfixApplication extends Application
{

    @Override
    public void onCreate()
    {
        super.onCreate();
        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hack_dex.jar");
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try
        {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }

        dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar");
        Utils.prepareDex(this.getApplicationContext(), dexPath, "path_dex.jar");
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hotfix.BugClass");

    }
}
  • ### ClassLoader(originally from qq空间团队 –原始)

    在它之后和他有相同原理的其他热门开源库有:
    • ###### Nuwa

    • ###### HotFix

    • ###### DroidFix

    • ###### Tinker

    需要重启Activity或重启Application达到更新效果。

  • ### Robust

…也许 Kgraft + Kpatch更合适?

Red Hat的工程师Seth
Jennings在2014年11月初,提出了第四种解决方案。将Kgraft和Kpatch结合起来,
补丁包用这两种方式都可以。在新的方法中,Jennings提出,“热补丁核心为其他内核模块提供了一个热补丁的注册接口”,
通过这种方法,打补丁的过程 — 更准确的说,如何处理运行时内核调用
–可以被更加有序的组织起来。

这项新建议也意味着两个方案都还需要更长的时间,才能被linux内核正式采纳。尽管Suse步子迈得更快,并把Kgraft应用到了最新的enterprise版本中。让我们也关注一下Red
Hat和Canonical近期是否会跟进。

原文:
作者: Serdar Yegulalp
译文: LCTT 译者: coloka

本文永久更新链接地址:

供图:
Shutterstock 有多种技术在竞争成为实现Linux内核热补丁的最优方案。
没人喜欢重启机器,尤…

四:总结

(1)因为我们的Patch是以独立的jar包,插入到APP的DexElements中,
所以如果APP中中的类引用了Patch中的类,就会在校验时报错。因为当进行dex校验时,如果两个相关联的类在不同的dex中就会报错。(LoadBugClass引用BugClass)
(2)
为了防止上述错误就要阻止Dex校验,阻止Dex校验的方法是阻止相关类打上CLASS_ISPREVERIFIED标志。
(3)
阻止相关类打上CLASS_ISPREVERIFIED标志的做法是:在相关引用的类(LoadBugClass.class)的构造方法中,引用另外一个jar中类AntilazyLoad.class
(4)因为AntilazyLoad.class在另一个jar中,所以需要把该jar对应的dex插入到App中。并且在Application中的onCreate()方法中将该类加载进来。
(5)把Patch.jar对应的dexElement加载进App中。因为Patch.jar放在dex数组的第一个位置,所以首先被加载。即:如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。

前两个都会存在一些兼容性问题,为此美团借鉴了Instant Run原理,推出Android热更新方案Robust

  • ##### 基于native hook的方案:需要针对dalvik虚拟机和art虚拟机做适配,需要考虑指令集的兼容问题,需要native代码支持,兼容性上会有一定的影响;但是无需重启Application、无需启动Activity即可更新Java方法。

  • ###### 基于Multidex的方案,需要反射更改DexElements,改变Dex的加载顺序,这使得patch需要在下次启动时才能生效,实时性就受到了影响,同时这种方案在android N [speed-profile]编译模式下可能会有问题,可以参考Android N混合编译与对热补丁影响解析

  • ###### 各大热补丁方案分析和比较

基于exposed的AOP框架,方法级粒度,可以进行AOP编程、插桩、热补丁、SDK hook等功能。是真正程度上的热更新。无需重启Application、无需启动Activity即可更新Java方法。基于AOP这种技术,就是在方法级别这个粒度做替换。

我们知道,应用启动的时候,都会fork
zygote进程,装载class和invoke各种初始化方法,Xposed就是在这个过程中,替换了app_process,hook了各种入口级方法(比如handleBindApplication、ServerThread、ActivityThread、ApplicationPackageManager的getResourcesForApplication等),加载XposedBridge.jar提供动态hook基础。方法级的替换是指,可以在方法前、方法后插入代码,或者直接替换方法。只能针对java方法做拦截,不支持C的方法。

1.不支持AndroidRuntime2.如果线上release版本进行了混淆,那写补丁也是一件很痛苦的事情,需要反射写混淆后的代码,粒度太细,要替换的方法多的话,工作量会比较大。反射+内部类,可能还有包名和内部类的名字冲突,总而言之就是写得很痛苦。

同样是方法的hook,AndFix不像Dexposed从Method入手,而是以Field为切入点。

其他相同原理开源库链接:Nuwa, HotFix,DroidFix,Tinker。
Classloader的原理就和上面两个不一样了,参考文章安卓App热补丁动态修复技术介绍,以及Android dex分包方案,所以这俩篇务必要看。这里就不对三个框架做过多对比了,因为原理都一致,实现的代码可能差异并不是特别大。
总结一下就是:
  1. 一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

  2. 把多个dex放进app的classloader之中,从而使得所有dex的类都能被找到。而实际上findClass的过程中,如果出现了重复的类,改变类加载的实现,是会使用第一个找到的类的。

  3. 如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。所以我们可以把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面。

  4. 只要把有问题的类修复后,放到一个单独的dex,通过反射插入到dexElements数组的最前面,就可以让虚拟机加载到打完补丁的class了。

  5. 但在实践中,会发现运行加载类的时候报preverified错误,原来在DexPrepare.cpp,将dex转化成odex的过程中,会在DexVerify.cpp进行校验,验证如果直接引用到的类和clazz是否在同一个dex,如果是,则会打上CLASS_ISPREVERIFIED标志。通过在所有类(Application除外,当时还没加载自定义类的代码)的构造函数插入一个对在单独的dex的类的引用,就可以解决这个问题。空间使用了javaassist进行编译时字节码插入。总结起来就是两点,第一是动态改变BaseDexClassLoader对象间接引用的dexElements,第二是在app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志。

关于这种方式想了解更多的可以参考文章:HongYang大神–Android 热补丁动态修复框架小结
Robust开源GitHub
Android热更新方案之美团Robust

相关文章