阿里巴巴开源移动容器化框架Atlas的技术演进之路
qq115
7年前
<p>摘要:在2017云栖大会深圳峰会开源专场上,阿里巴巴手淘技术部资深技术专家倪生华(玄黎)做了题为《Atlas-容器化演进之路》的精彩演讲,玄黎从Atlas的发展、特性、技术原理以及开源运作等四个方面为大家分享了手淘的移动容器化框架Atlas的技术演进之路。面对All in手淘的航母战略,如何实现组件化?本文不容错过。</p> <p>以下内容根据嘉宾演讲视频以及PPT整理而成。</p> <p><strong>本次分享将主要分为以下四个部分:</strong></p> <p>一、Atlas的发展</p> <p>二、Atlas的特性</p> <p>三、Atlas的技术原理</p> <p>四、Atlas的开源运作</p> <h2>一、Atlas的发展</h2> <p><strong>Atlas开发动力</strong></p> <p>当大家打开手机淘宝时,可能就会发现阿里巴巴的手淘的业务其实很大,基本上可以在手淘中看到阿里巴巴所有的业务,比如聚划算、天猫等等。特别是在2013年的时候,整个阿里集团开始all in手淘,手淘成为了阿里巴巴集团的一艘航母,基本上所有的业务都需要能够在手淘上运行。</p> <p><img src="https://simg.open-open.com/show/a93fe2deb6be53af969347211611e26f.png"></p> <p><strong>Atlas发展历程</strong></p> <p>但是突然发现手淘自己还没有准备好,那时候手淘基本上一个月发布一个新的版本属于比较正常的情况,但是当突然多出了100多个业务,而这些业务可能几天就需要更新一个版本,这样就出现了平台的发展速度赶不上业务发展速度的情况。除此之外,另一个变化是之前只有一个团队进行开发,但是all in手淘战略提出之后,手机淘宝包含了100多个业务,团队之间的协作成为了非常大的问题。所以在这样的需求变得越来越强烈的情况下,手淘非常需要一套框架来支持整个阿里事业部几百个团队的协作以及各自业务的快速迭代。所以从2012年开始,手淘技术团队就开始尝试构建移动容器化框架,那个时候的Atlas框架还是基于插件化的设计,进程之间相互隔离,插件彼此独立,框架完全是一个提供服务的空壳。 <img src="https://simg.open-open.com/show/886841dc8b5ade0a01372467209e9777.png"></p> <p><strong>插件化架构</strong></p> <p>插件化的框架策略使用了两年,后来就发现这样的策略存在着很多的问题,所以在2014年,手淘技术团队痛定思痛,完完全全地重写了一遍Atlas,所以现在大家所看到的开源的Atlas其实就是第二版的Atlas,也就是从原来多进程的方式转化到单一进程的方式,但是同时也实现了多进程的遍历系统,支持业务的独立开发、独立部署、独立运行。到2015年的时候,基于业务的需求重新做了按需加载,实现了对于远程组件的支持,并且对于容器进行了升级。所以从2015年到2016年,Atlas在阿里巴巴内部稳定运行了两年,而在207年初的时候之前正式开源了Atlas这个项目。</p> <p><img src="https://simg.open-open.com/show/9bad71661c8962a58da782aba6fe0898.png"></p> <p><strong>插件化所带来的问题</strong></p> <p>接下来首先介绍一下Atlas在2012年时的插件化架构设计。如下图所示的就是当时Atlas插件化的架构,最底层是容器进程:ContainerActivity与ContainerService,在这之上会存在各种各样独立的插件进程,比方说天猫、聚划算等等。这些插件进程分别都有各自的生命周期,独立地进行管理。为了实现这样的架构就需要在系统层实现很多的事情,也就是基本上系统所要做的进程管理,Atlas 也都要完成。当时插件化的架构设计所带来的好处就是插件之间的隔离,包括内存调度等都进行了完全的隔离;除此之外就是不需要再次开发,有新的业务进来可以部署并且直接运行。这样的架构设计大概运行了两年,其实也才真正完成了一年多时间就发现了一些非常严重的问题。第一个问题就是因为形成了整个物理的隔离,所有的APK都是隔离的,但是手机却不是隔离的,用户会将这些APK都放在同一个包里面。一个简单的例子就是天猫引进了一个库,聚划算也引进了这个库,在两个APK包里面,并且可能有很多代码属于这个库的,而这种情况可能出现的比较多,所以代码的可用性就会非常非常差,这样就会对于后面的统一调度以及管控上面造成非常不利的情况。第二方面,因为Atlas 插件化的架构设计是基于进程式的,大家都知道进程与线程相比更加重量级,所以使用插件式的方案要比原生的不采用插件式的方案的内存消耗要更大。第三部分是因为要在系统之上隔一层,所以整体的插件适配情况会比较差,插件每发布一个版本甚至每个厂家发布一个版本都需要去重新进行识别。</p> <p><img src="https://simg.open-open.com/show/78da7fb3b182ca85f6b3b5a6a68e38cb.png"></p> <p><strong>组件化的思考</strong></p> <p>这样的插件化架构运行了两年的时间,后来团队发现简直痛不欲生,所以在2014年初的时候整个团队就开始重新思考手淘到底需要什么样的架构。基于之前整个插件化的方案,在经过了深入的思考后认为:</p> <ul> <li>首先,手淘需要高复用性,手淘不是一个大杂烩,需要进行统一地管控,对于所有的中间件、所有的服务都需要有一方进行统一管控。</li> <li>第二方面就是要做到低侵入性,对于开发者而言,他们不需要感知容器的需求,只需要进行编码就可以了,这就是低侵入性,也就是开发者不需要对于系统由特别深刻的了解就可以运用大量的黑科技,比如说开发者并不需要了解安卓系统底层设计,而只需要在安卓容器上使用Java层的API就可以完成自己想要实现的功能。</li> <li>第三个方面就是高可用性,高可用性也好,性能也好,要保证使用了插件不能对于应用的性能产生太大的损耗。</li> <li>最后一个方面也是业务插件方提出的请求,就是希望业务和业务之间使用插件隔离开来,独立地进行开发和调试。业务今天可以跟着手淘的航空母舰一起跑,未来也可以自己独立地运行。</li> </ul> <p>在2014年初的时候,手淘技术团队在想清楚这四条原则之后,大概花费了一个月的时间构建出了新版的Atlas框架体系。</p> <p><img src="https://simg.open-open.com/show/8fcb0fc939e7fcc21b371ef15dfae7b0.png"></p> <p><strong>Atlas体系</strong></p> <p>如下图所示就是现在的Atlas体系结构,在整个容器中没有进行混淆之前不会超过100个类。像图中最下面的一层,称之为Hack层,包括OS Hack toolkit & verifier,这里其实主要是为了对系统能力做一些扩展,然后做一些安全校验。上面的一层是Bundle Franework,就是我们的容器基础框架,这一层仿照整个OSG2层容器的概念定义了一层所有容器插件化的生命周期提供Bundle管理、加载、生命周期、安全等一些最基本的能力。再之上的一层是运行期管理层,主要在容器的运行时,对于所有Bundle的版本控制进行管理;以及清单,会把所有的Bundle和它们的能力列在一个清单上,在调用时方便查找,这样就可以知道到底有多少个容器;这一层还存在对于类加载器以及资源的代理,这里就是和业界一些插件化框架机制类似的地方,Atlas会代理系统的运行环境,让Bundle运行在自己的容器框架上。这一层最右边的则是整个运行期间的监控器,可以监控运行的情况,也方便了工程期开发调试。最上面一层就是业务,这一层是面向开发者的,开发者只需要知道存在AtlasBridgeApplication就可完全去使用容器的能力,除此之外还有Tools,可以用来进行校验和判断。这样带来情况是使得组件开发与在后台的开发是一模一样的,经过一个代理的Resource和Classloader进行操作。而这些东西对于组件开发而言,完全不需要深入了解,这部分内容会在之后的章节会继续展开进行分享。</p> <p><img src="https://simg.open-open.com/show/e7e4d68ba469631b6845634b085a0aeb.png"></p> <p><strong>Atlas业务分层</strong></p> <p>Atlas的整体业务分层如下图所示。底层称之为服务层或者是宿主APP,大家都知道很多应用会引入第三方的中间件或者组件,将这些组件放在基础服务层,也就是上层的业务统一会使用的业务能力层,而这一层的业务能力不会直接去调用应用模块。在服务层之上就是各自的业务模块,这些业务模块可以和技术服务层进行交互,也可以和其他业务模块通过协议的模式进行交互。但是如果业务模块需要使用到一些公共的业务模块,就可以抽离出一层偏业务的服务。</p> <p><img src="https://simg.open-open.com/show/5613713bd7e18c7d26f931aa8641eede.png"></p> <p>基于整个Atlas的容器,手淘做到了什么样的程度呢?大概是从2013年开始,手淘开始开发Atlas容器框架,到2014年正式成立。下图中左边的这张图就显示了手淘Android端的发布次数,从2013年发布了42次增长到2016年发布了600多次,这里的版本是指的真实交付到用户客户端上面的。右面这张图是从手淘团队从2013年开始到2016年为止,发布一个新版本所需要的时间情况,大家可以看到在2013年发布一个新的版本需要平均10天以上的时间才能交付给用户,而到了2016年几乎一两天就可以交付给用户一个新的版本,可以认为手淘的业务每天都在发生着更新。在这样的快速迭代之上,目前整个手淘大概会有70多个备案明确的业务;对于人员而言,手机淘宝自己的部门大概会有400多个工程师,而整个阿里系里面还有20多个BU在参与其中。这些在Android方面的基础都是基于Atlas来做的,基于Atlas才能实现对于综合业务的快速支持。</p> <p><strong>Atlas带来的价值</strong></p> <p><img src="https://simg.open-open.com/show/ff4da9926355b56372c5237be5b0f170.png"></p> <h2>二、Atlas的特性</h2> <p>在第一个章节介绍了Atlas的整体发展历程,Atlas的发展其实是基于整个业务的快速迭代和分工协作进行的,接下来为大家分享Atlas的一些特性。从2015年开始Atlas进行了组件化,整体进行了更新,拥有了很多新的特性。</p> <p>对于Atlas的特性而言,可以大致如下图所示。首先,Atlas对于开发者而言是无侵入性的,没有任何冲突,在开发过程中不需要做任何感知,开发者原来是怎样开发的现在依然是怎样开发。第二个特性就是组件是独立进行开发和调试的,组件的开发可以不依赖于其他模块。第三个特性就是高兼容性和高稳定性,Atlas从开始运行到今年位置在阿里实际运行了4年多,组件化设计从2015年开始到现在已经运行了2年多,扛过了两年的双11。同时它具备动态更新能力,所谓动态更新能力就是目前Atlas可以支持组件独立开发,Atlas兼容从Android 4.X一直到Android 8,而且对于市面上所知道的ROM都是兼容的,而且目前没有发现任何一个ROM在手机上面会出现问题。目前手淘已经稳定运行了2年多,整体手淘的Crash率一直维持在万分之五左右,因为容器导致的crash占比小于百分之一,而手淘Atlas的性能损耗在5%之内,也就是使用容器和不使用容器加载方式来点击的性能损耗在5%以内。</p> <p><img src="https://simg.open-open.com/show/d052564a9e290210e2e6a145f95bf063.png"></p> <p>对于Atlas的特性而言,在另一个角度上将其分为两部分。第一方面Atlas可以做到工程师独立,在所有的开发过程中都可以做到独立开发、独立调试、独立集成以及独立发布,也就是说业务开发可以完全脱离手淘,可以认为没有这个大的IP存在,就像一个Web应用一样将其放上去,甚至不用去找应用市场进行信息发布。这就是在整个工程化体系上面每个Bundle独立开发以及独立发布。第二个方面是在整个运行期间,之前提到了本质上是基于OS G2去做的,因为Atlas实现了资源的隔离和aapt分段,运行期中在Bundle和Bundle之间也实现了资源的隔离。所以总体而言,Atlas可以实现透明,灵活,稳定,敏捷,这同样也是在最开始构建Atlas时所提出的四个要求。</p> <p><img src="https://simg.open-open.com/show/41baef4b9a95a8f03e8fe81acccd6f77.png"></p> <p><strong>Atlas与其他组件化方案的对比</strong></p> <p>在下图中将市面上所有组件化解决方案进行了罗列。对于组件化方案可以进行对比去看,不一定有好坏之分,只是适不适合某个特定的场景,比如对于需要对组件进行独立支持和独立加载的APK适合什么样的组件化方案,Atlas就不支持这样加载独立的APK,如果需要支持加载Web APK或者第三方的APK可能选择Atlas并不合适。第二点,Atlas支持动态更新所谓的插件宿主,也就是宿主容器中间件这个层面也是支持动态更新的。第三点就是对于四大组件的动态启动,现在对于四大组件的动态增加Atlas是部分支持的。对于首次插件启动的性能而言,如果插件引用了容器化或者组件化的方案,那么在插件首次启动时,性能都会有一定的影响,这是因为引入这样的方案会将资源和类的加载过程向后推。在Atlas中对于插件首次启动的性能进行了优化,所以用户可以感知到插件启动是非常快速的,这个优化是基于系统层面的,并且是基于一个进程的,所以物理隔离是没有做的,如果相关组件挂掉了,那么这个插件也会挂掉。而且Atlas的hook的程度其实是非常轻的。对于代码运行隔离而言,其实因为Atlas的整体运行机制基本上可以做到组件和组件之间的隔离,目前非侵入式这些Atlas都是可以做到的。对于插件的懒加载,Atlas采用的策略是只有在用到插件时才会进行加载,一个像手淘这样的应用可能会有70多个模块,但是用户可能并不会全部使用到,所以没有必要在一开始全部加载进来,只需要在用户要使用的时候把模块加载进来就可以,比如用户在手淘中点击了天猫或者聚划算,才会将这些插件启动起来,不使用时不会启动。其实下图中标红的部分也就是与其他的组件化方案相比,Atlas的一些特点,如果这些特点正好符合应用的需求,就可以尝试使用Atlas,如果这些特点与需求相差甚远,可能Atlas就并不适合这样的场景。</p> <p><img src="https://simg.open-open.com/show/9f5fa58757198dde5e48b4f9a5affcbc.png"></p> <p><strong>Atlas动态能力</strong></p> <p>以上是在组件化的方案上进行对比,而现在比较火的就是动态更新能力。其实在阿里运行的最多的还是基于Atlas这一套的东西,接下来就为大家分享一下Atlas的动态能力。这部分从六个方面来看:</p> <ol> <li>支持类型,目前Atlas支持Class文件,SO和资源文件的增删改操作,可以说是全类型的动态能力。</li> <li>兼容性,Atlas适配Android 4.x - 7.x版本,而且已经在线上稳定运行了2年多的时间。</li> <li>高性能,通过去Verify等手段,达到极小的性能损耗。</li> <li>补丁大小,Atlas自己有一套Cache代码的git,包括OS、资源文件的利用,通过精细化Diff的方法,达到非常小的Patch包,手淘在增加业务时包的大小也不会也太大增加。</li> <li>成功率,目前成功率还是非常高的,与业界相比,具有较高的部署成功率和效率,目前影响成功的最大因素就是用户的磁盘空间。</li> <li>开发透明,这一点也就是对于开发者友好。如果开发者需要去开发动态部署的能力或者动态更新的能力,其实不需要去关心应用是不是动态更新的,只需要在业务代码里提交一个新的版本,Atlas会帮助开发者自动生成Diff包。</li> </ol> <p><img src="https://simg.open-open.com/show/5e0c5e238f31d3d8ea1d1fb4310b2363.png"></p> <p>目前存在的局限也可能是所有动态更新中都会存在的局限,就是暂时不支持Manifest中的权限等内容,如果涉及到Manifest里面的权限就需要做系统的注册,所以这些Atlas还是不支持的;而且Atlas对于Activity,Service的新增也是有限支持的。</p> <h2>三、Atlas的技术原理</h2> <p>上面就主要为大家介绍了Atlas的两个特性:组件化和周边化。接下来会为大家简单地介绍一些Atlas的整个技术原理。</p> <p><strong>类加载机制</strong></p> <p>组件化也好,动态部署也好,都需要进行类加载来完成这样的事情。大家都知道所有的类加载都是基于ClassLoader来执行的,Atlas的核心就是在每个Bundle下面去设置一层BundleClassLoader,Bundle类加载器的逻辑则是在Bundle进行查找的时候首先会去查找自己的Bundle里面有没有这个类,如果有的话就不会再去寻找,如果没有就会去找Host,也就是整个宿主类的Classloader里面有没有这个类,如果还没有就会去系统或者其所依赖的关系中去找寻这个类。对于下面整张图而言,其实原理非常简单,还是Classloader双亲委托这样的一件事情,Atlas只是在Android和Java的基础之上设计了两层的Classloader,通过自己定义的Classloader的加载规则来实现对于整个宿主的隔离,从这个意义上讲是比较简单的。</p> <p><img src="https://simg.open-open.com/show/069369f59d3962f2ca5d997cc9d29d16.png"></p> <p><strong>资源处理机制</strong></p> <p>除了类之外,开发过程中比较关心的就是资源问题。那么Atlas的资源处理机制时怎样做的呢?其实也非常简单,但是还是会与ClassLoader有所区别的。在Android某些系统上面将资源进行隔离时会存在一定的兼容性问题,所以Atlas采用了如下图中右图所示的另外一种做法,把所有的Bundle资源在编译期进行了隔离,每个Bundle的资源段都会通过AP去进行修改,给出一个统一的资源字段,通过在加载的时候将每个Bundle的资源放到AssetsManager里面。Atlas没有做运行时的隔离,而是通过编译期的资源分段模式实现了资源隔离的方案,所以在Bundle里面可以使用到Bundle自身的资源也可以使用到宿主的资源。</p> <p><img src="https://simg.open-open.com/show/aaacfbdde2e4f06b7c0e743543a2c2ca.png"></p> <p><strong>动态更新能力</strong></p> <p>目前Atlas动态更新支持三种模式:</p> <ul> <li>第一种模式指的是组件远程下载。当拿到这个需求的时候,大家可能都会比较慌,有一个Bundle特别大可能无法装入到APK里面,而在Atlas里面其实做的事情也很简单,它只是编译的过程中让这个Bundle参与编译,但是在最后发布过程中将这个Bundle去除掉,在需要它运行的时候从远端把这个Bundle下载下来。这种方式使用的比较多的地方就是手淘的预装包,一般现在的APK都会非常大,通过这种方式可以将主要功能放入到UPDATE APK里面去,一些次要的功能可以通过远程包配置,当这个Bundle启动的时候再去下载远程包,这就可以认为是从无到有。当然这并不是严格意义上的从无到有,因为在编译的过程中已经将所有的Manifest都已经打包进去了,次要的功能通过远程下载的方式进行配置。</li> <li>第二种动态加载的模式是业务组件动态更新。因为整个宿主类或者中间层的服务类本身的变动一般而言都是比较少的,基本上这些类不会每天都发生更新变化,应对这样的需求可以采用业务组件动态更新,因为可以认为是完全独立的ClassLoader进程,可以认为是剥离在整个Android系统的进程之外的,可以实现什么时候需要就去加载什么样的资源。业务组件的动态更新其实是通过组件Diff和Merge的过程进行更新的,并且这样使直接在组件的生命周期内做,兼容性极好。因为不会涉及到系统操作,只是一个资源虚化,原来是直接加载资源,现在是Patch下来之后直接生成一个新的资源,这种方式的兼容性会非常好。</li> <li>第三纵动态加载模式是如果真的需要修改宿主类时就回归到常见的通过类、资源、SO文件等的diff算法,通过merge算法来实现更新,有比较好的兼容性。</li> </ul> <p><img src="https://simg.open-open.com/show/b643f6253ae96eb2eb49a4354fcf8d57.png"></p> <p><strong>Bundle的部署</strong></p> <p>下图是现在Bundle部署的diff过程。在进行Bundle打包的时候可能会打一份源代码,发布哪个就会在仓库里面存储一个版本,第二次发布的打了一个新的包,并对每个资源进行diff,比如class与class之间通过dex格式形成一个新的classes.dex。Resource与Resource之间可能会去基于MD5进行diff。而对于research和assert会基于Dex merge的模式进行diff。对于资源文件也是一样,基于手机上的文件和Patch文件生成一个新的资源合集,基于所有的Patch会生成新的运营期的APK文件的容器模型。今天可能发布一个Patch明天还会发布一个Patch,为了支持Patch最小化就需要使用增量发布。增量发布能够使得用户只要更新了一次,下次再进行更新只要发生变化的资源和类就可以了,不需要将所有的从源文件到类都更新,这样就可以保证更新量比较小,速度也比较快。</p> <p><img src="https://simg.open-open.com/show/7d041685be2d958bcf9209b7a06e914f.png"></p> <p><strong>目录结构</strong></p> <p>下图是Atlas在手机上面的目录结构。其实在每个Bundle下都会各自有一个目录结构,第一次发布完成之后会合成一个version 1,在后面patch目录中会合成一个patch 1,当发布了第二个版本时,会在运行的时候去取Version 2这个版本。由于基于目录结构来做的,所以回滚操作也会非常简单。所以如果基于动态部署来实现,更新版本后如果发现有问题,只需要在服务端下发一个命令就可以删掉或者回滚,而不需要用户进行操作。在服务端也存在清单的概念,所以可以精确感知到哪几个版本或者模块是存在问题的。所以基于这套机制,客户端的发布和之前服务端的发布是一模一样的,开发者可以去随时发布或者随时回滚,大大地提升了发布和回滚的效率,一个命令下去可能几秒钟之内就能够回滚到上一版本或者之前的版本上去。</p> <p><img src="https://simg.open-open.com/show/d7d711a3a9a50d8976264f56269816f1.png"></p> <p><strong>中间件代码部署</strong></p> <p>下图展示的是Atlas为了规避性能损失的一个措施,其实在整个容器这部分,因为Android的限制需要根据classLoader来更新,会使用插装的方式和Verify机制。而现在发现基于系统的dex机制可以不使用插装的机制,所以现在Atlas的做法是用户手机上面设置多个dex,如果新的dex和原来是重复的,就会将重复的dex删除掉,自动合并到新的dex里面去,这样做的好处是这个机制完全符合谷歌的Manifest规则体系的,其次因为取消了Verify机制,整体的性能会非常快。</p> <p><img src="https://simg.open-open.com/show/e2d361214540eb8f110b5f0f52dcabd3.png"></p> <p><strong>宿主资源部署</strong></p> <p>因为安卓本身有一个限制,所有的资源必须得在base包里,新增一个资源是不生效的。所以一个做法是在打包的时候预留很多空资源。另外更新已有的资源则通过资源覆盖来完成。在宿主中会预留一些空资源段,当新增的时候就可以适应这些主dex预留的资源段。也就是如下图中显示的会有很长一段null的资源,如果增加了资源就需要对于这些标记为null的资源段进行更新。</p> <p><img src="https://simg.open-open.com/show/949c58750cb9318d297689148cfcfd70.png"></p> <h2>四、Atlas的开源运作</h2> <p>到目前为止,手淘技术团队有一个专职的开发团队去维护Atlas。目前整个阿里系能够看得到的使用了Atlas的应用有很多,而且目前阿里巴巴对内和对外的Atlas是一套代码,所以大家不必担心Atlas开源之后不会进行维护了。</p> <p><img src="https://simg.open-open.com/show/1d29473eee8e656e40656933e2bae05f.png"></p> <p>下图展示的就是Atlas未来的一些版本计划。接下来首先还需要继续提升Atlas的自身稳定性以及兼容性。其次,因为开源了Atlas,所以也需要降低接入的成本。大概6月份,希望能够提供出IDE快速调试开发插件,来完善Atlas工具链。并希望新增支持动态Service/适配Android O。大概在9月份Atlas会提供极简接入工具,并对于动态部署扩展(小patch方式)/dexmerge的算法进行改进。</p> <p><img src="https://simg.open-open.com/show/038b0619587b194ae2f9b2506aeef9b6.png"></p> <p> </p> <p>来自:https://zhuanlan.zhihu.com/p/27080871</p> <p> </p>