上图是外卖App引入MRN后的架构全景图,接下来我们会从下到上、从左到右逐步介绍:
-
最下层是Android/iOS系统服务层,因为MRN是跨端的,所以需要引入这一层。相对单一平台来说,由于MRN的引入,整个App的架构不可避免地需要考虑Android和iOS平台本身的差异性。
-
倒数第二层是平台服务层,这一层相对与单一平台来说,并没有太大区别。
-
再往上一层是MRN基建层,这一层的工作主要是:(1)尽可能地屏蔽Android和iOS系统的差异性;(2)打通已有的平台基建能力,让上层业务不能感知到差异。
-
再上一层是业务组件层,这一层相对于单一平台来说,区别不大,主要是增加了Android和iOS的RN容器,同时业务组件是可以被RN调用的。
-
继续往上是MRN接口层,该层的主要任务是尽可能地屏蔽Android和iOS组件之间的差异,让上层页面使用的RN接口保持一致。
-
最后是业务层,这一层是用户可直接接触到的页面,页面的实现可以是Android/iOS/RN。
-
左上角是研发支撑,主要包括代码规范、代码检查工具、Debug插件、准入规范、准入检查工具、代码模板插件等。这块相对于单一平台来说,主要的差异体现在:由于编译器和语言不同,使用的工具有所区别,但工具要做的事情基本是一致的。
-
左下角是测试支撑,主要包括UI自动化测试、自测覆盖率检查、AppMock工具、业务自测小助手、性能测试、云测平台等。这块相对于单一平台来说,基本也是一致的,主要的差异同研发支撑,主要是语言不同,使用的工具有所区别。
-
右上角是发布支撑,主要包括打包Bundle和APK、打包检查、发布检查、发布Bundle和APK等。这块相对于单一平台来说,保持了打包发布平台的一致性,区别在于:需在原有的基础上,增加MRN的打包发布环节。
-
右下角是运维支撑,主要包括基建成功率监控、业务成功率监控、线上问题追踪、网络降级等。这块相对于单一平台来说,保持了一致性,区别在于:需在原有的基础上,增加MRN的监控运维。
研发测试支撑
外卖业务MRN组件架构
RN官方对双端只提供了30多个常用组件,与成熟的Native开发相比,天壤之别。所以我们在开发的过程中面临的一个很重要问题就是组件的缺失。于是,MRN团队基于RN组件进行了丰富,引入了一些优秀的开源组件,但是源于外卖业务的特殊性,一方面需要业务定制,另一方面部分组件依然缺失。所以为了减少重复代码,提升外卖客户端MRN的研发效率,建设外卖组件库就变得非常有必要。
上图是我们外卖组件库的架构图,最底层依赖Android和iOS的原生服务;然后是MRN基建层,用于抹平Android和iOS系统之间的差异;再上一层则是外卖组件库及其依赖,如平台组件库和打包服务,组件库分为两类:纯JS组件和包含JS和Native的复合组件。再上一层则是Android和iOS的MRN容器,它提供了上层Bundle的运行环境。整个组件的架构思路,是利用中间层来屏蔽平台的差异,尽可能地使用JS组件,减少对原生组件的依赖。这样可以有效地减少上层业务开发时对平台的理解。接下来,我们主要讲一下WM-RN组件库:
如上图所示,WM-RN组件库主要包含三部分:RN interface、RN Native组件、外卖RN JS组件。RN Interface主要包括Native组件的Bridge部分和Native组件在JS侧的封装,封装一层的好处是方便调用Native暴露出的接口,也可以用来抹平Android和iOS系统间的差异;RN Native组件分为Android和iOS两端,依赖各自的业务模块,为RN提供外卖Native的业务能力,如购物车服务、广告服务;外卖RN JS组件则是纯JS实现,内部兼容外卖App与美团外卖频道间的差异、Android和iOS平台间的差异,依赖现有的MRN组件库和外卖开源Beeshell组件库,减少组件的开发成本;从工程的物理结构来看,建议将Native组件、RN Interface放在一个仓库进行管理,主要是因为Native与JS侧的很多通信都是通过字符串来匹配的,放在一起方便双端与JS侧的接口统一对齐,发布时也会更加方便。目前,外卖组件库已经扩展了几十个业务组件,支持了线上近百个MRN页面。
Native/MRN/H5选型标准
目前,美团外卖App存在三种技术栈:Native、MRN、H5,面对业务持续增长和安装包不断变大的压力,选择合适的技术栈显得尤为重要。H5在性能和用户体验方面相比Native和基于Native渲染的RN相对弱一些,所以目前大部分H5页面只是用来承载需求变更频繁、需要即时上线的活动页面。那么MRN和Native的界限是什么呢?当有一个新的页面产生时,我们应该如何做取舍?通过实践,我们逐渐摸索了一套选型规则,如下:
-
Native选型规则,强交互(同时存在2种及以上手势操作),无法用二元函数描述的复杂动效,对用户体验要求极致的页面,类似首页、点菜页、提单页等。
-
对于强交互或强动画,MRN技术栈支持效果不理想,不建议使用。其他情况下,建议使用MRN。
-
H5适用于需要外链展示的轻展示页面,比如向外投放活动的运营页面等等。
具体选型细节可参考下表:
发布运维支撑
发布运维是一个成熟的软件项目中非常核心的部分,它保证了整个项目能够高效且稳定地运转。建立一个稳定可靠的发布运维体系是我们建设整个外卖MRN技术体系的重要目标。但发布运维的建设上下游牵扯了众多基建:拥有一个合理的工程结构对发布运维来说至关重要。如果工程结构臃肿且混乱,将会引起的一系列的权限问题、管理维护问题,这样会严重制约整个发布运维体系的效率。所以MRN的工程架构演进优化也是发布运维体系建设的重要组成部分。
MRN分库 & 工程结构演进
业务分库
任何一个大型、长期的前端技术项目,良好的工程结构都是研发发布支撑中非常核心的部分。从2018年10月份,外卖正式启动MRN项目以来,面临涉及近百个MRN和几十人参与的大规模MRN应用计划。从项目初期,我们就开始寻找一个非常适合开发维护的工程结构。
在最开始的时候,我们的目标是快速验证及落地,使用了一个Git库与一个Talos项目(美团自研发布系统)去承接所有页面的开发及发布工作,同时对权限进行了收缩,保证初期阶段的安全发布。然而随着页面的增多,每个版本的发布压力逐渐增大。发布SOP上的三大关键节点权限:Git库操作权限、Talos的发布权限、美团自研的线上降级系统Horn权限,互不相关,负责人也各异,导致发布时常因各个节点的权限审批问题,严重阻塞效率。
随着项目的大规模铺开,我们的页面数量、合并上线次数与初期已不可同日而语。为了解决逐渐臃肿的代码仓库问题及发布效率问题,我们将庞大
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
而臃肿的RN库根据业务维度和维护团队拆分成了4个业务库,分别是订单业务、流量业务、商家业务、营销业务,并确认各库的主R,建立对应的Talos项目,而主R也是对应Talos项目的负责人。同时所有的主R都有MRN灰度脚本的管控权限。这样一来,MRN的工程结构和Native的工程结构完全对齐,每个责任人都非常明确自己的职责,不会来回地穿插在不同的业务之间,同时业务库任意页面的发布权限都进行了集中,RD只需要了解业务的负责人,即可找到对应的主R完成这个业务的所有相关工作。
工程结构
在项目初期,对于每个库的工程结构,美团内部比较流行的工程结构有两种:一个是适合小型业务开发的单工程多Bundle方案,另一个是相对更适合中大型业务开发的多工程多Bundle方案。
单工程单Bundle方案
顾名思义,单工程单Bundle方案的意思就是一个前端工程承载所有的业务代码,最终的产物也只有一个RN Bundle。通过入参决定具体加载哪个页面。
对于业务不多,参与人不多的团队,使用单工程单Bundle的方式即可快速完成开发、发布。因为通过一次发布就可以完成整个发布的工作,但是带来的弊端也是不可接受的:因为所有业务都耦合在一起,每次更新都会“牵一发而动全身”,增大了问题的隐患。如果多个业务需求同时提测的时候,在团队配合上也是一个极大的挑战,因为新版本号会覆盖旧版本号,导致两个需求提测时会出现相互覆盖的情况。所以我们在立项之初就排除了这种方案。
多工程多Bundle方案
多工程多Bundle方案的意思就是一个Git库中存放了多个页面文件夹,各个文件夹是完全独立的关系,各自是一个完整的前端工程。拥有自己独立的MRN配置信息、package.json、组件、Lint配置等(如下图所示)。每个页面文件夹都输出一个独立的RN Bundle。
相比于单工程单Bundle方案,多工程多Bundle方案将页面进行解耦,使之基本可以满足中大型MRN项目的需求。在外卖MRN项目初期,一直都使用着这样的工程结构进行开发。但是我们也为之付出了相应的代价,即每个页面的依赖都需要对应RD去维护升级,依赖碎片化的问题日趋严重。同时在工程级别的管控,如统一Lint规则、Git Hook等也变得更加复杂。
多工程多Bundle方案 => 单工程多Bundle方案
随着外卖MRN页面规模以及参与人规模的进一步增大,多工程多Bundle方案的缺点日益凸显。特别对于那些前端技术底子相对薄弱的团队来说,依赖管理问题会变得很头疼。在这种情况下,单工程多Bundle的方案就应运而生了。
核心思路也很简单:观察一下单工程单Bundle方案和多工程多Bundle方案的优缺点可知,单工程单Bundle依赖管理方便的优点主要来自于“单工程”,而多工程多Bundle的业务解耦的优点主要来自于“多Bundle”。所以结合这两种工程方案的核心优点,就可以设计一种新方案:单工程多Bundle。即用一个工程去承接所有的页面代码,但是又可以让每个页面输出独立的RN Bundle来保证互不影响。其实,这种方式类似于Native一个静态库的管理,如下图所示:
通过分析MRN的打包原理可知,MRN通过一个配置文件配置了一个Bundle的所有业务信息以及mrn-pack2的打包入口。所以我们只需要让配置文件支持多份Bundle信息的配置,通过打包命令与参数选择正确的mrn-pack2打包入口,即可打出我们最终所需要的业务Bundle。如下图所示:
核心优势:
-
整个工程采用一个package.json,管理业务库中所有的依赖。这样可以有效地解决各自页面去管理自己依赖时,必然产生的依赖版本碎片化问题,避免同一依赖库因为版本不一样,而导致页面表现不一样的问题。
-
从依赖角度去规范各自页面的使用工具规范,如A页面使用某一种三方库来实现某种功能,B页面使用另一种三方库也实现了同一种功能,单一依赖管理就可以从库依赖的角度强制做技术选型,减少各个页面的实现差异,从而降低维护成本。
-
让业务同学可以更加专心地开发业务代码,不用关心复杂的依赖问题,大大提升了开发效率。
-
实现了工程级别的管控,如Pre-Commit,脚手架方案管理将变得更加便捷。
这种工程组织形式也成为了MRN工程结构的最佳实践,而且美团内部也有多个团队采用了这种解决方案。目前已支撑超过几百个页面的开发和维护工作。
外卖发布运维体系
下图展示了我们的发布运维全景,共覆盖了开发交付、线上发布、线上监控、有效应对、复盘改进等五大模块。接下来我们会逐一进行介绍。
(1)开发交付
开发阶段,需求RD完成开发,提交到Git库的发布分支。对应的业务库主R角色(通常由RN经验较丰富的工程师来承担)进行CodeReview,确认无误之后会执行代码的合并操作。顺便说一下,这也是外卖RN质量保障长征路的第一步。
(2)线上发布
合入发布分支之后,就可以正式启动一次RN Bundle发布。这里我们借助了美团内部的Talos完成整个发布过程,Talos的发布模板与插件流水线规范了一次发布需要的所有操作,核心步骤包括发布准备(Git拉代码、环境参数确认、本次发布说明填写)、发布自检(依赖问题检查、Lint、单元测试)、正式打包(Build、版本号自更新)、产物上传测试环境(测试/线上环境隔离、测试环境进行测试),双重确认(QA、Leader确认发布)、产物上传线上环境等等。
产物上传线上环境,实际上是上传到了美团内部的CD平台–Eva。在Eva上,我们可以借助RN Bundle的发布配置去约束发布App的版本号、SDK版本等,以及具体的发布比例及地区,去满足我们不同的发布需求。最终执行发布操作,将RN Bundle上传到CDN服务器,供用户下载,完成整个发布流程。
(3)运维监控
发布之后,运维是重中之重。首先我们的运维难点在于我们的业务横跨两个平台——美团App与外卖App。由于它们在基建、扩展、网络部分都存在差异,所以我们选取指标的维度不仅要从业务出发,还要增加全局的维度,来确保外卖平台MRN的正常运转。基于这个层面的思考,我们选取了一系列RN核心指标(在下面的章节会详细列举),进行了全方位的监控。目前外卖客户端,已经做到分钟级监控、小时级监控和日级别监控等三档监控。
在监控手段上,首先我们使用了美团开源的Cat告警平台(这部分已经通过Talos插件完全自动化配置),确保当核心指标在线上出现波动、异常的时候,相关RD、QA以及业务负责人可以及时接受到报警,并由对应的RD主R负责,快速进入到“有效应对”的环节。同时为了能够分阶段、更好地处理问题,我们将核心指标报警分为【P1】与【P0】两个级别,分别代表“提高警觉,确认问题”与“大事不好,马上处理”。保障了一个问题出现之后能够及时被发现并快速进行处理。
除了监控报警手段之外,我们还会借鉴客户端高可用性保障的经验。用一些日常运维的手段去发现问题。比如使用灰度小助手、数据日报等手段从宏观角度主动去发现存在隐患的指标,及时治理,避免问题。
(4)有效应对
根据“墨菲定律”:如果事情有变坏的可能,不管这种可能性有多小,它总会发生。即便我们在发布管控和线上监控上做的再充分,线上问题最终还是无法避免的。所以当通过线上告、客诉等手段发现线上问题之后,我们需要及时的应对问题、解决问题,把问题带来的影响降低到最小,并以最快的速度恢复对用户的服务。
在有效应对的方面,我们主要靠两种手段。第一种是存在B方案兜底的情况,使用Horn灰度配置,关掉MRN开关,短时间内恢复成Native页面或者H5页面继续为用户提供服务,同时通知相关RD和QA快速定位问题,及时修复,验证并上线。第二种是无兜底方案的情况,CDN服务器(Eva)上撤掉问题Bundle,实现版本回滚,接下来的问题定位过程跟手段保持一致。
这两种备案保障了外卖MRN业务的整体高可用性。
(5)复盘改进
在以上四个大环节中,问题可能会出现在任意一个环节。除了及时发现问题与解决问题,我们还需要尽力避免问题。这一点主要是靠我们内部的例会、复盘会,对典型问题进行Review,将问题进行归类,包括复盘流程规范问题、操作失误问题、框架Bug等,并力图通过规范流程、系统优化来尽力地避免问题。
在外卖MRN项目实施过程中,我们共推动了二十多项规范流程、系统优化等措施,大大保障了整体服务的稳定性。
最后,我们用一张图对外卖监控运维体系做一个总结,帮助大家有一个全局的认知。
混合式架构流程
针对混合式架构的流程,目前外卖技术团队采用的是正常双周版本迭代流程+周迭代上线流程。MRN页面既可以跟版迭代,也可以不跟版迭代,这样可以有效地减少流程的复杂度和降低QA的测试成本,而周迭代流程可以有效地利用MRN动态发版的灵活性。混合式开发和原生开发应尽量保持时间节点和已有流程的一致。这种设计的好处在于,一方面随着动态化的比例越来越高,版本迭代将可以无限拉长,另一方面从双周迭代逐渐演变成周迭代的切换成本也得到大幅的降低。详细可分为下面几个阶段:
评审阶段
业务评审阶段在原有的流程上,增加了技术选型阶段。在技术选型时,明确是否会存在需要使用MRN页面的情况,如果页面可以完全不涉及到Native部分即可完成,就可以进入周迭代的发版流程。如果需求用MRN实现,但是又涉及到Native部分,仍然走周迭代的上线流程。除正常开发需求的时间外,RD需综合考虑到双端上的适配成本。
开发阶段
客户端以周维度进行开发,每周确定下周可提测的内容,根据提测内容是否为动态化的业务、下周是否在版本迭代周期内,决定跟版发布或周发布。
提测阶段
提测前,为了保证MRN页面的提测质量,RD首先需要按照QA提供的测试用例提前发现适配问题。提测时需要在提测邮件中注明:(1)提测的Bundle名称和对应的版本号 ;(2)标明哪些组件涉及Native模块 ;(3)依赖变更情况,如是否升级了基础库,升级后的影响范围;(4) 重点测试点的建议。
上线阶段
MRN由于其可动态发布的特性可以跟版发布,也可不跟版发布,但上线时间和灰度时间节点都保持了一致。不过版本还是动态发版,都默认周二上线,周四全量。
-
跟版发布:默认只对当前版本生效,需在双周迭代三轮提测节点,周二当天将Bundle上线服务器,MRN的灰度开关全量打开。通过周四App的发版灰度比例来控制MRN的灰度比例,上线时需配置报警和灰度助手监控,实时掌握MRN的线上数据。
-
不跟版发布:也同样以周四作为全量发布窗口,Bundle需在周二时上线指定线上版本,指定QA白名单。测试通过后,在周三按照比例逐步灰度,周四正式全量,和跟版发布一样,上线时需要配置报警和监控。
架构总结
引入MRN后,相对单平台而言,架构层级上,我们增加了2个MRN中间层去屏蔽Android和iOS平台、原生组件之间的差异。这样做的目的是为了让上层业务开发者可以很快地使用框架进行业务开发,完全不用关心平台和组件间的差异。通过引入MRN技术栈,带来的好处很明显:
(1)使用MRN实现的页面理论上可以实现一套代码,部署到不同平台上,开发效率得到大幅度提升。 (2)采用MRN框架,无论是加载性能还是页面滑动性的用户体验上,都会比原来H5的方式要好。 (3)部分页面具备了快速编译、快速发布的能力。
但一个硬币总有两面,混合式架构增加了架构的复杂度,使得原本只要考虑一个平台的事情,逐渐转变成需要考虑三个平台,另外Android本身具备碎片化的问题,这使得混合式架构的适配问题较为突出。当出现问题时,我们的第一反应由“这是什么问题”变成“它是否存在于两个平台,还是只在一个平台上?”、“如果仅在一个平台上,是在原生代码还是React Native代码出了问题?”、“历史版本的MRN是否存在问题,是否需要修复”、“修复的效果在Android和iOS上的表现是否一样”,这些问题增加了定位和修复工作的复杂性。另外,MRN的适应场景也是有限的,并非所有的业务和页面都适合改造成MRN,如何做选择也需要进行有效的判断,从而增加了决策成本。
针对上述问题,我们的建议是:
(1)减少分歧: - 在研发、测试、发布和运维环节,MRN的页面尽可能对齐Native原有的环节,减少团队理解的成本。 - 在Debug开发环境下,利用页面浮层提示技术栈使用情况;Release环境下,利用工具、MRN自动化报表,及时的让开发同学明确知道是Native页面还是MRN页面,减少确认。 - MRN页面尽可能地避免原生组件的使用,而使用纯JS代码实现,供MRN页面使用的原生组件的需要高质量的提供,减少下层组件的问题。 - 默认只修复当前的版本,出现严重问题时才考虑修复历史版本,减少多版本带来的复杂度提升。
(2)技术栈明确边界
- 做好Native和MRN技术栈使用的边界,尽可能用简单的选型标准,让合适的场景选用合适的技术栈,从而保证业务整体的可用性,让用户体验依然如初。
(3)单技术栈转向多技术栈团队
- 培养全栈工程师,当团队的同学都具备iOS、Android和MRN多个技术栈能力时,将会有效地提升开发的效率,短期内可选择iOS、Android和MRN工程师结伴编程的策略。
正如在“监控运维”章节中所讲到的那样,线上运维是我们工作的重中之重。这个章节我们就讲一下我们对于监控指标的选取。鉴于外卖业务的特殊性,除了美团的外卖频道之外,外卖业务还需要运行在独立的外卖App上。如下图所示:
外卖App经过多年的发展,目前已逐渐成为一个平台级应用,承接了C端、闪购、跑腿等多个业务。与美团App相比,它们之间在很多基础建设、扩展、网络部分都存在差异。所以在监控核心指标的选取上,我们除了保证C端MRN业务在美团以及外卖两端的高可用性,还需要保证外卖App平台本身基建的稳定性,从而保证运转在外卖App上所有MRN业务的高可用性。
而从监控的大分类上来讲,我们分为了【可用性指标】以及【性能指标】,它们分别关注业务本身的可用性,以及页面的性能与用户体验。接下来,我们就依次进行讲解。
MRN可用性指标
可用性指标也是我们关注的关键指标,它直接决定了我们的MRN页面是否能够正确、稳定地为用户提供服务。通过MRN Bundle加载全景,我们可以确定整个包加载的几个关键节点。可以说,MRN业务的可用性就是取决于这些关键节点的成功率。
下载链路
MRN是一个动态化的框架,所有的MRN Bundle都是从CDN节点上远程下载。所以下载成功是MRN业务可用的先决条件。有些普通的业务方是不需要关注这个指标的,而外卖App可能会因为网络库基建,出现启动下载线程拥堵、DNS劫持等问题,所以我们把下载成功率作为外卖App监控的全局指标。目前,外卖App的下载成功率长期稳定在99.9%左右。
加载链路
加载链路可以细分为初始化引擎部分以及业务Bundle加载部分。前者跟基建有关,代表从引擎创建到加载完Common包加载成功这段的成功率。这部分主要依赖MRN SDK的稳定性,从我们的日报上看,稳定性基本保持在99.99%以上。
而业务Bundle加载成功率(MRN PageLoad Success),是MRN页面创建到业务视图内容渲染过程中,没有发生错误的比例。它与跟拉包时网络情况、MRN框架稳定性和业务JS代码都有关系。这也是我们关注的核心指标,因为它直接决定了我们某个页面是否可以渲染成功,所以我们把这个指标同时列为了外卖App监控告警的全局指标与单Bundle告警的指标。目前,整个外卖业务的Bundle加载成功率稳定在99.9%以上。
使用链路
Bundle加载成功之后,页面成功被渲染。但是在使用的过程中,可能会因为JS代码,Native代码的Bug出现JS Error、Native Crash等问题,这样给用户带来的直观反馈就是应用闪退、页面白屏等,造成了服务的不可用。所以在使用链路上出现问题率,基本也可以直观反映出一个RN页面的质量以及它当前的运行状况。
在使用链路上,我们主要关注的是JS Error率、JS Error个数以及页面退出成功率(MRN PageExit Success)等。
JS Error很好理解,由于RN是由JS驱动的框架,所以一个页面的JS Error率基本上可以综合反映出一个页面的可用性、稳定性或者基建的稳定性,故我们同样把这个指标同时列为了外卖App监控告警的全局指标与单Bundle告警的指标。我们用上报上来的JS Error数量做分子,该页面的PV做分母,计算一个页面的JS错误率,当JS Error个数短时间内极速升高或者JS Error率有大幅上升时,就会触发我们的JS Error告警。目前外卖大盘的JS Error率保持在万分之一左右,略低于Native Crash率。
页面退出成功率(MRN PageExit Success),理解起来不如前面的指标那么简单,因为它表示的是用户在退出MRN页面时,业务视图内容已成功渲染的比例。它会包含所有已知和未知的异常,但是用户进入页面后快速退出的场景,也会被错误的统计在其中,因为用户退出时可能页面尚在加载中。相比于JS Error,它是一个更加综合的指标,基本上涵盖了加载失败、渲染白屏、使用时出现错误等多个异常场景,基本上可以反映出一次MRN业务的单次可用性,相比于之前的指标会更加严格。我们把这个指标同时列为了外卖App监控告警的全局指标与单Bundle告警的指标。我们希望它永远能保持在99.9%以上,否则就会触发告警。目前外卖大盘的MRN PageExit Success基本稳定在万分之三左右,我们最终的目标是希望稳定在万分之一左右。
最后,我们希望通过两个“脑图”快速回顾一下外卖全局监控与单业务监控关注的核心指标。