导读: 目前携程 75% 以上订单来自移动端,App 几乎承载了整个集团的所有业务形态。那么无线服务端和客户端底层架构如何支撑如此复杂灵活多样多变的业务,并顺利接入整个集团十几个不同研发团队开发的代码,让这么多团队协同开发,无缝集成在同一个 App 内,还能确保其质量和性能?这对移动端架构提出了非常严峻的挑战。
从2013年开始,我们先后进行了不同路径的多样性架构探索,在实践过程中也经历了各种曲折与压力,最终实现了2015年的这个全新架构,实现了无线服务端基于API Gateway的架构框架、客户端的模块化开发、测试与部署,支持运行期间的模块实时加载、按需Lazyloding、Remote加载,从而实现模块级动态升级以及代码级热修复,并
且逐步推动数百人的客户端研发团队由不堪重负、效率低下的大版本大火车开发模式向模块间独立迭代、发布轻量级的开发方向演进。
同时在架构探索期间,携程做了App相关的很多性能优化,比如底层网络通道治理的优化、应用层插件容器加载启动速度以及存的优化、业务中间件Hybrid的优化等等,逐步保证随着业务的不断的迭代,能保证用户的比较好的优化体验。
早期App服务端架构使用了传统的PC无线开发架构,即在PC Web应用基础上增加一些无线端的REST接口直接供给App访问,没有考虑架构的扩展性、 灵活性、安全型等因素。
如图1所示,服务端系统一方面以Web应用的方式提供给PC端浏览器访问,另一方面为支持移动,在Web应用基础上增加一些REST接口直接供App访问。相应地,无线接口和Web应用作为同一工程开发,作为同一个应用部署,这种架构设计思路是很直接和自然的,可以快速把PC端功能复制到App上,其思想设计是在现有Web应用上打补丁,体现的是PC思维无线化,把App简单作为PC端应用的翻版,并把两者物理上捆绑在一起,在早期也能满足当时的业务需求,但是随着平台化的发展,以及业务越来越复杂和多样性,这种架构设计带来的一些列的问题逐步暴露出来,其中最突出的急需解决的有三个问题:耦合、重复造轮子、系统稳定性,具体如下所示:
无线接口和Web应用紧耦合,Web端的修改会影响无线接口,Web端的发布导致无线接口被动连带发布,Web端的Bug影响无线接口的可用性,反过来也一样,无线接口的任何变化会影响Web应用。
此外其中酒店无线接口和机票的无线接口,或者其他BU无线的接口,也存在着较为严重的耦合问题,这种耦合带来的问题,最严重最明显的就是这个BU的接口调整或者修改Bug,有可能会影响其他BU接口的稳定型,从而带来每次发布,要带来更多的测试回归工作。
无线接口除了给App提供业务数据,还需要考虑一系列非功能性因素的接口功能验证,如通讯协议和数据格式封装、安全控制、日志记录,性能监控等,这些对每个无线接口都适用。如果App和后端系统直连,意味着每个后端系统都需要单独支持这些通用功能,导致重复开发。一旦这些通用需求有变化(如对数据传输进行加密增强),所有后端系统都要强制同步修改和上线,给项目管理和产品发布带来很大挑战。
App和多个后端系统直连,只要一个系统出问题,就会影响App的可用性,比如酒店服务出了问题比如变慢或者耗用CPU过多资源,其机票服务或者其他服务会受到一定影响,其典型的弊端就是缺乏故障隔离机制,缺少负载均衡、缺少监控、缺少熔断等影响后端稳定性的问题,导致App的健壮性很差,非常脆弱。
基于架构V1.0三个比较严重的缺点,于是我们开始尝试使用一种新的无线架构V2:基于API Gateway的无线服务端架构。
基于如图2所示的无线API Gateway架构,具备如下功能特点。
App实际上和PC端浏览器是对等的,PC端应用有服务端,App也需要自己独立的服务端,两个服务端都需要针对自身的特点,独立开发,独立部署,同时实现逻辑和物理层面的解耦,从架构层面彻底摆脱PC思维无线化。
核心逻辑从Web应用剥离出来,进行服务化改造,服务实现时不区分PC和无线,App和Web应用都依赖于这些服务,一套接口,多方调用。
提供统一的无线网关,所有App调用指向此网关,网关包括通用层、接口路由层、适配层。通用层包括通讯协议适配、数据封装、安全、监控、日志、隔离、熔断、限流、反爬这些系统级功能,每个接口调用都需要同样逻辑,这些功能统一由网关前置处理,避免重复开发。具体实现时,每个通用处理逻辑封装成拦截器,遵循统一的过滤接口,并且做到可配置,网关依次调用这些拦截器,这样可以支持通用逻辑的灵活扩展。
无线API Gateway应该目前很多公司都有自己的实现,目前市场上也提供了很多开源项目Zuul、Archaius、Hystrix、Eureka等帮助我们去实现自己的Gatway。
携程基于Netflix的开源项目Zuul开发了无线APIGateway架构如上图2所示,其Gateway的职能是负责接收来自无线端的所有API请求,并将他们路由到正确的目标应用服务器,并且提供限流、隔离、熔断等功能,保证了无线服务的长期稳定运行,拥有的弹性容错机制也减少了日常运维工作。同时该Gateway提供了多维度的监控数据,并与报警系统对接,实时监控线上情况,达到运维自动化。其API Gateway具有的几个核心职能:路由、隔离、限流、熔断、反爬、监控报警,具体如下所示:
Gateway支持集中管控的同时,也带来单点问题。假设后台某个服务接口,由于某种原因,性能有严重问题,对应Adapter处理很慢,那么网关所在服务器的线程很快被耗尽,导致单个接口拖垮整个系统。这种问题,单纯通过增加机器,水平扩展网关数量是解决不了的,实践中,我们引入了智能升降级机制来快速隔离单个接口的影响,从而实现了接口的自动隔离熔断机制,其实现原理如图3所示。
针对特定一个接口,如果在一定时间间隔内(比如5分钟),它的超时失败率到了一定比例(比如5%),网关会对该接口做降级处理,随机抛弃部分流量,比如只允许50%流量通过。下一个5分钟再评估,如果失败率还没有改善,允许通过的流量降到25%,以此类推。如果成功率好转,网关对该接口做升级处理,提升通过的流量比例,为了快速恢复,一般提升到原流量4倍,然后在下一个时间段再评估是否触发升降级。
整个过程全自动智能处理(为防止误判,可支持人工干预),这样单个接口出问题,不会影响整个网关的处理能力。
携程App服务端架构通过一系列的拆分和整合,既优化了公司整体应用架构,又为App做大做强奠定良好基础,其带来的好处是全方面的,增加了架构的可扩展性、健壮性、稳定性、灵活性,并且提高了团队的开发效率和团队长远的收益,其具体表现在:
携程App的第一个版本在2011发布,那时候App架构很简单,基本上就是在传统的MVC的架构基础上封装了一个数据服务层即代理数据层,如图4所示。
在携程业务发展的早期,移动App经历从无到有的阶段,为了快速上线抢占市场,其移动App开发的MVC架构成了“短平快”思路的首选。
在如上图4所示的MVC的体系架构中,业务控制层负责整个App中主要逻辑功能的实现;业务逻辑Model层则负责数据结构的描述以及数据持久化的功能;数据服务层作为数据的代理媒介层,主要负责与Control层进行数据通信,包括实现基础框架数据通信,序列化和反序列的机制等;而移动界面UI View层作为展现层负责渲染整个App的UI。这种架构分工清晰,简洁明了,并且这种系统架构在语言框架层就得到了Android和iOS的支持,所以非常适用于App的startup开发。
但是这种架构在开发的后期会由于其超高耦和性,从而造就庞大Controller层,而这也是一直被人所诟病。最终的MVC都从Model-View-Controller走向了Massive-View-Controller的终点,其最严重的结果就是Control层的代码越来越多,在携程内部很多类,早期都超过了2000行,同时Control层和View层之间存在一些较高的耦合。其对应的App工程结构架构如图5所示:当时无论iOS和Android工程,都只有一个工程结构CtripWireless。
单个工程去实现一个App的好处就是各个业务线的接口通信方便,调用简单随意,可以随意使用工程中的任何公共和业务组件,并且接入学习成本低。但是随着业务越来越复杂,以及各BU业务通信交互的需求越来越多,其各个BU的业务耦合越来越严重,这个直接为后期插件化Bundle架构埋下了伏笔。
基于携程业务不断快速发展,后来活跃用户已经超过1亿,日活用户千万,很快触及到了当时Android虚拟机机制的设计缺陷,即移动端在Android上面临了两个比较严重的问题,这两个问题导致的严重后果就是在2.3的系统里面,用户直接都不能安装和使用。
一是单dex 65535方法数限制,二是线性内存分配器(LinearAlloc)限制。今天的Android开发者看到这两个限制都不会陌生。前者是因为Android的早
期设计中,对dex文件中方法id用16位整型标记,单个dex文件中的方法数无法超过65535,eclipse环境中生成不了未做过proguard的deBug apk。
后者则是dalvik虚拟机用来加载类的堆内存大小被硬编码了,2.3以下是5M,2.3以上是8M,致使App无法安装的原因就是因为这个堆内存被耗尽导致dexopt失败。
现在来看肯定大家都觉得不是问题,因为Google已经给出了一些可靠的解决方案,辅以更加先进的gradle + Android Studio,开发者们可能根本不会再遇到这两个经典问题,官方的MultiDex分dex机制解决了方法数限制的问题,其中main dex最小化原则,结合dalvik LinearAlloc heap size调整(修改
到了16M),使得dexopt的失败几率大幅下降。而ART的出现彻底不再存在LinearAlloc这样的限制。
但是我们回过来再看,那个在用户Android 2.3还占50%的时代里,是如何通过软件架构调整解决这个问题的,其中的经验有我们值得借鉴和学习的地方。
基于上述我们遇到的问题,我们在原来的传统架构上又做了重新调整和优化,提出了移动端架构V2.0,其主要设计思路就是:
在业务快速发展过程当中,发展到5.0的时候App上已经承载了很多业务功能,但其中一些功能用户使用频率比较低,并且之前快速试错被证明效果不佳的一些功能也大量存留在现有版本中。这些不常使用的功能不应该始终占用程序资源,所以从架构上进行纵向分离,保证主要重要场景的体验,是这一时期的主要设计思路,这时期的架构设计图如图6所示。
要实现这个架构,第一步就是进行各个BU业务线的功能解耦,这个工作花费了整个团队大概3个月时间3个App大版本的周期去进行。
进行功能解耦的重要思想,就是实行轻重分离,主次分明的思想;在代码模块的组织架构上进行重要的调整,保证主要重要的App功能快速迭代和性能稳定,将附属的使用频率不高的新功能,使用H5容器进行动态加载,所以在V2.0的架构上,携程App就是个典型的Hybrid App ,可以看到刚开始就核心模块酒店和机票采用Native 进行开发,其他模块基本是采用H5去实现。
V2.0架构基础上,做了一系列的工作就是将App中比较鸡肋的功能比如客户价值和转化率低的功能转成H5实现。这样做的好处就是集中精力去优化Native业务体验,同时也能减小Android因为方法数超标的限制压力。
在V2.0这个阶段还做了一件事情去解决dex 65535的问题,即将工程项目里面出现的不再使用的类和不再使用的方法进行了集中清理,这样的好处是代码也整理干净了,如果方法数超出的不是太多的话通过清理就可以让方法数减少到65536以下,同时还清理了不使用的jar包、重复引入的jar包以及对第三方jar包进行瘦身,一般来说jar里面的方法数最好,清除一两个无用的jar包就能大大的减少方法数。
同时这个阶段还定义了一个原则,一些信息说明展示或者活动优惠页面,非用户主流程的页面都是采用H5去实现,一方面减少开发成本,同时也是为了应对方法数增多的压力。
上面三种方法都是从传统的技术防守的角度即防止引入更多的方法和类,以及在原有工程角度上去瘦身,但是这两个方法都不能本质上去解决单dex 65535方法数限制App不能安装的问题,要想根本解决这个问题,就必须减少单个Dex的大小,使用新的技术进攻的手段去一劳永逸的去解决这个问题。
所以接下来做了比较重大的决定就是各个BU进行解耦,每个BU单独独立一个工程,每个独立插件有独立的UI界面逻辑和资源、存储及网络通信数据处理逻辑,通过共用统一的基础库接口访问网络服务、图片库、定位库等。V2.0架构对应的App工程结构如图7所示。
在当时为了彻底解决方法数溢出的问题,基于上面解耦的基础上采用了多Dex分包方案,当时携程的做法是借鉴Facebook提供的方案去动态分包,将一个apk中的dex文件分割成多个,然后动态加载dex文件。首先简单描述下Facebook的思路:
携程与Facebook的dex形式完全一致,这是因为我们也是使用Facebook开源工具buck编译的。
Facebook将加载Dex的逻辑放于单独的nodex进程,这是一个非常简单、轻量级的进程。它没有任何的ContentProvider,只有有限的几个Activity、Service。
android:name="com.facebook.nodex.startup.splashscreen.NodexSplashActivity">
所以依赖集为Application、NodexSplashActivity的间接依赖集即可,而且这部分逻辑应该相对稳定,我们无须做动态扫描。这就实现了一个非常轻量级的依赖集方案。
加载Dex逻辑也非常简单,由于NodexSplashActivity的intent-f ilter指定为Main与LAUNCHER。首先拉起nodex进程,然后初始化NodexSplashActivityActivity
,若此时Dex已经初始化过,即直接跳转到主页面。
Facebook加载Dex的方案,其加载流程图如图8所示。
这种方式好处在于依赖集非常简单,同时首次加载Dex时也不会卡死。但是它的缺点也很明显,即每次启动主进程时,都需先额外启动一个nodex进程。尽管nodex进程逻辑非常简单,但是也需要加载时间100ms以上。但是携程对这个启动时间非常敏感,当时推动产品很难会去采用这个方案。
基于这个方案的缺点,我们在其基础上进行了优化方案,即能不能主进程直接加载Dex方案,具体定的方案策略如下。
Dex形式并不是重点,假定我们使用当前的Dex形式,即assets/secondary-program-dex-jars/secondary-N.dex.jar。
主Dex应该保证简单,即类似Facebook,只需要少量与Dex加载相关的类即可,并且这部分代码是相对稳定。我也无须去更改任何非加载相关的代码。
这个是重点,我们应该通过什么加载方案去实现这样的分包规则。首先大家明确若是点击图标,的确无须再起一个进程是可行的方案,但是问题就在于在Application初始化时,或是在attachBaseContext时,我们无法确保即将进入的是主界面Activity。可能系统要起的是某一个Service或Receiver或者Notification,这种跳转方式是不行的。
如图9所示,有两个关键问题需要解决:
关于问题1,进程同步可以使用pthread_mutex_xxx、 pthread_cond_xxx,但是mutex或cond要放于共享内存中,这种实现方式较为复杂,所以我最后实现时采用的是一个最简单的方法即每隔95ms去检测TempFile是否存在,如果存在则直接进入主程序,同时在加载dex的工作线程中去判断,如果加载dex成功,则创建TempFile。
关于问题2,在挂起主进程的同时,去启动一个工作线程去加载dex,也就是这个线程是非UI主线程,不会造成阻塞UI主线程的情况,经过多次测试,也确实没发生ANR现象,这个通过分析ANR现象的本质就能得出这个结论。
基于Facebook的基础上我们优化实现了动态加载Dex的方案,比较完美彻底地解决了因为方法数超标而无法安装的问题,同时也不用担心随着业务发展,代码中方法越来越多的问题。
同时在这个阶段,也就是2015年初的时候,携程开始全面由Eclipse工具迁移到Android studio + Gradle的构建方式,同时由于Google支持了MutilDex方
案,所以后来就直接使用了官方提供的方案。
V2.0架构解耦之后,不同BU工程的依赖是解除了,良好的解决了以前各个不同BU相互依赖的问题,同时也可以支持多个团队进行并行开发。但是这个阶段的阶段架构存在以下两个明显严重的问题:
即会存在如果其他BU的工程修改了,如果没及时通知对方人员,全全局报错,整个工程编译都无法通过,影响到其他BU的正常开发工作。
打包不可配置,构建编译速度慢,因为携程BU很多,业务也很全而复杂,大概解耦成有10几个工程,因为不可选择所以需全量编译,所以造成一次构建速度最慢的时候差不多30分钟,一般10分钟以上,所以整个开发效率比较低,开发人员的体验感也比较差。
基于上述缺点,我们在V2.0的架构基础上又进行了优化,提出了V3.0的架构,具体的架构图如图10所示。V3.0架构在V2.0的工程解耦升级的基础上去完成了,V3.0架构是基于Bundle的动态加载插件化架构,即几乎工程中的任何组织形态都可以看成Bundle, 而最终携程App 由一系列的Bundle组合而成,运行在可以容纳加载的Bundle容器DynamlicLoader中。
如图10所示,应用层的酒店、机票、火车票等都是一个个独立的APK,它们之间独立开发,互相不受影响。最终统一以插件的方式集成到统一的携程APK里面。酒店和机票之间通迅方式采取两种方式,BUS数据总线跳转 和 URL Scheme跳转。
V3.0架构对应的工程结构图如图11所示。
如图11所示,现有的工程结构,有超过30个Bundle(apk),并且随着未来业务的发展,其Bundle是越来越多。为了解决Bundle过多造成编译速度过慢的问题,我们采用配置文件去动态灵活配置,各个BU需要使用什么Bundle,通过简单的一句配置,将其加到工程中即可,同时其他不需要打进来的Bundle支持aar(.a)和源码依赖,按需添加依赖即可。
为了一劳永逸解决我们V2.0遇到的Dex方法数超标的问题,我们内部基于目前携程App的现状研发实现了一个动态加载的插件化框架DynamicLoader,支持即时加载,按需加载,远程加载三种方式。即时加载,即刚开始就直接加载进来,按需加载是使用的时候才去加载,远程加载即刚开始没有这个工程,然后用户通过远程安
装就可以直接使用这个功能。这种机制同时也支持了我们后续使用到了Hotfix机制。在这里首先简单总结下目前市场上出现了比较著名的开源的插件化框架如表1所示。
表1 市场主流插件化技术对比
如表1所示,携程在2015上半年开始着手研究自己的插件化框架,同时也对当时市场上的插件化技术做了调研,最终得出结果,当时市场上的主流框架都不能满足携程当时工程结构的现状和当时插件化的需求,也就是接入其插件化之后,携程的各个BU团队需要很多额外的开发成本去实现整体迁移,同时还不能有效保证后续的插件化稳定性,基于此背景下,携程的插件化应运而生,其实现原理是通过系统的ClassLoader动态加载类,通过系统的AssetManager去动态加载插件的资源,同时通过修改aapt的源码去替换系统的Appt解决各BU资源之间冲突的问题。关键是各BU原有的代码和现有的开发模式都不需要额外的去改动从而增加额外的开发成本,插件化的思想即一切皆Bundle组件的思想,每个Bundle有自己的版本号,通过BundleManager 去管理Bundle的升级。
在V3.0架构推进阶段,为了需要支持按需加载的时候,其Bundle加载的速度,我们约定了一个规则:即每个Bundle加载的时间不需要超过500ms。所以需要对大Bundle进行拆分,比如酒店和机票内部又拆分了自己的6个Bundle。
V3.0架构就比较适合中到大型团队,并且解耦之后,可以支持多个团队的并行开发,也可以满足多个版本的同时开发和发布。每个BU团队所做的工作就是在发布之前提供一个Bundle即可,然后到发布集成阶段,将其集成到携程的统一APK里面。
进入到2015年后,携程在软件架构上逐渐趋于平稳。在V2.0原有插件加载基础上,研究了更多行业内Android应用的技术架构,并且也结合官方MultiDex的实现。
V3.0在V2.0解耦的基础上,自己实现了动态加载插件化框架,并且在此基础上增加动态热补丁功能,通过携程内部的Hotfix发布平台,实现了携程客户端补丁版本更新直接覆盖,用户无需安装新版本就可以将严重的Bug修复掉。类似阿里的AndFix热修复技术框架。
V3.0架构已经可以支持多个团队的快速高效并行开发,但是技术永远在前进,所以未来的V4.x架构我们还在进一步推进探索中,比如我们做Native App能否像Web网站一样随时部署,即用即取,能否做到跨平台的体验良好的Native App开发,能否实现数十个工程秒级部署编译,从而大大提高开发效率,这些问题是我们Native开发人员一直在探索追求的话题。
目前携程正在推进和已经进行的技术架构:
架构是非常值得分享和讨论的,好的技术架构能够持续支持伟大的商业梦想。但是无论什么优秀的可扩展性好的技术架构,都不能脱离于业务而存在,最终都会随着业务的不断发展,而同时其架构也在进行不同程度的演进与优化。一个好的架构首先是必须是能解决公司遇到的现实技术问题和符合满足公司目前架构技术现状,其次能带来技术性的革新从而引领业务的发展。
其次做架构之前,要想清楚这样设计的目的是什么,通过架构设计使程序模块化,做到模块内部的高聚合和模块之间的低耦合,做到基本符合迪米特、依赖倒置、里氏替换、接口隔离等原则。这样做的好处是使得程序在开发的过程中,开发人员只需要专注于一点,提高程序开发的效率,并且更容易进行后续的测试以及定位问题。但设计不能违背目的,对于不同量级的工程,具体架构的实现方式必然是不同的,切忌犯为了设计而设计,为了架构而架构的毛病。
文章来源:http://geek.csdn.net/news/detail/108167