2019年6月20日下午,GMTC 北京2019全球大前端技术大会「多端提效与质量优化实践」技术专场,来自贝壳找房的四位技术专家分别就“极限前端性能优化”、“贝壳找房 Node 服务稳定性建设”、“贝壳移动端监控建设实践”以及“ Flutter 在贝壳的接入实践”主题进行分享。以下是对本次专场的精华内容做了部分梳理和总结。
极限前端性能优化
移动互联网时代,应用性能作为影响用户体验最重要的因素,在开发过程中显得尤为重要。性能优化是开发中老生常谈的话题,也是一名开发者从入门向资深进阶的必经阶段。在实践中,开发者们如何进行性能优化?
贝壳找房资深工程师嘻老师(企业代号名),在专场活动中就向大家介绍了贝壳找房使用的性能优化工具,对比传统性能优化与极限性能的优化差异,并通过真实案例让大家了解性能优化的价值与收益,提升开发者在性能优化技术上更深一步的进阶。
性能优化需经历无阶段意识和有阶段意识两大阶段,以及无优化、通用方案、指标、匠心四小阶段。
在项目启动初期阶段,用户少,压力小,问题大多数是从单个个体用户的使用场景来看。慢慢地,开始注意性能优化的问题,寻找常规优化方法。逐步到有意识阶段,优化方式出现针对性和策略性,开始关注用户感官优化,力求在多个细节做到极致,更多以数据为基础导向。
性能优化本身是需要数据来支撑的。贝壳找房的数据平台叫 fee,如下图所示。最上层业务应用层,包含贝壳的APP、链家APP、经纪人APP、小程序,以及腾讯的九宫格等。数据下载后,直接到内部的服务层,服务层包含贝壳API、DIG服务器以及贝壳网关。然后通过Kafka,将数据打入队列,在一些指定系统进行回滚,检查数据问题。左右两侧一侧是权限系统。另一侧是监控报警以及任务调度,他们共同组成贝壳的一整套的监控系统。
当有了数据监控平台,接下来就该关注性能优化所需的技术本身。
在前端优化中,内容优化是最根本的,90%的网站涉及文本和图片。据HTTP Archive 统计,2016到2019年PC端的文本大概有295.8K,到2019年上升到396K,文本上升34%,图片上升30%。同时移动端文本上升50%,图片上升100%。那么,在网络不给力的情况下,该如何做文本压缩以及图片压缩呢?
常规情况下使用GZIP对文本资源进行压缩。GZIP原理依赖两种算法,一种是LZ77,另一种是Huffman。
LZ77 是顺序数据压缩的一个通用算法,如果文件中有两块内容相同的话,那么只要知道前一块的位置和大小,就可以确定后一块的内容。所以可以用(两者之间的距离,相同内容的长度)这样一对信息,来替换后一块内容。由于(两者之间的距离,相同内容的长度)这一对信息的大小,小于被替换内容的大小,所以文件得到了压缩。
Huffman 算法是把文件中一定位长的值看作为符号,比如把8位长的256种值,也就是字节的256种值看作是符号。根据这些符号在文件中出现的频率,对这些符号重新编码。对于出现次数非常多的,用较少的位来表示,对于出现次数非常少的,用较多的位来表示。这样一来,文件的一些部分位数变少了,一些部分位数变多了,由于变小的部分比变大的部分多,所以整个文件的大小还是会减小,所以文件得到了压缩 。
另外,由于图片压缩算法一般是余弦变换和小波算法,所以使用GZIP仅仅了压缩6.3%。因此建议对于图片的压缩可以使用消除和替换图像、对矢量图和光栅图进行优化,或者使用有损压缩和无损压缩等形式进行优化。
大会上嘻老师(企业代号名)还通过一个跨国项目案例介绍了在极限前端性能优化的使用场景,与传统的性能优化大不相同,经过几次方案渐进迭代。
1、第一次方案,方案svg。
2、第二次方案,无损压缩。
3、第三次方案,抽离像素通道。
4、第四次方案,图片转行样式表。
5、第五次方案
(1)无损压缩图片。
(2)手动刷新运营商缓存,强制缓存图片。
(3)投放大量广告。
最终确定组合方案从而满足优化需求。
第二个案例从一个不易复现的性能问题,使用贝壳架构团队正在孵化的开源项目【时光机】解决用户操作完整路径、数据回放的问题,惊艳全场。
最后,嘻老师(企业代号名)建议『前端工程师不仅要在浏览器里做事情,也要跳出浏览器看看外面的世界。』
贝壳找房Node服务稳定性探索
贝壳找房资深工程师信玄(企业代号名)老师则为大家带来了贝壳找房在Node服务稳定性方面的探索及实践经验。Node.JS 采用事件驱动、异步编程,为网络服务而设计,其非阻塞模式的 I/O 处理可带来在相对低系统资源耗用下的高性能与出众的负载能力,适合用作依赖其它 I/O 资源的中间层服务。
贝壳为什么要用Node?
原因有三点:一是用于SEO,虽然现在的搜索引擎爬虫已经可以抓取客户端渲染的内容,但贝壳找房专门的SEO团队,还是建议我们采用服务端渲染的方式;二是为了实现前后端同构,提高开发效率;三是为了加快首屏速度,不需要依赖JS类库做渲染,优化性能。
有这些想法之后,基于公司内部 Bucky 方案并结合 React ,贝壳做了一套React服务渲染方案,如下图所示。最底端是存储端,这一端 Node 不会涉及,最多涉及到 Redis,中间最底层调用 API 提供业务数据。再上一层是Node,主要是做数据拼接和渲染,上层是客户端,中间红色主要为同构的部分组件和类库。
有了以上的基础架构,贝壳又是如何将小事做到极致解决稳定性问题呢?
首先需要预防问题。在一些项目上线之前,如何能够尽量考虑线下的一般情况,根据这些情况做出一些相应应对措施,避免上线之后出现问题。预防问题包括压力估算和压测、CodeReview 两部分。
压力估算和压测:一是预测线上的压力情况,根据日常的一些数据,或者根据过去数据的一些测算逻辑,估计未来的压力情况是什么样。二是设置压测目标,希望使用多少的机器资源实现抗住压力的目的。三是要切实执行一个线下压力测试。最后一步,根据预估的压力情况和压测得到的结果,计算出需要多少台机器来部署服务。
CodeReview:主要关注三类问题。 第一变量问题,变量是否为空,或者变量的类型。第二性能问题,例如使用async await导致接口串行请求的情况。第三关于硬编码,更多是体现在配置中,由于一些操作失误改变了环境变量。
其次是发现问题。发现问题可能两种思路,第一种是主动发现问题,考虑触发边缘的case接口会不会挂。但是由于目前没有很好的平台实现这样的诉求,更多的还是被动发现问题。
被动的方案需要有一个值班机制,要求必须有人响应,不然后面所有报警监控都是无济于事,如果没有人响应你是不可能知道问题的。监控部分,有两类异常监控,一是服务器本身的异常监控,是否服务当中有代码出错了或网关出错了。还有就是服务器资源监控,判断服务器资源是否够用。
服务异常监控贝壳主要监控几种类型的日志,第一是NGINX日志,这里用NGINX网关,一个是499,这个是NGINX特有的日志,意思是客户端主动断开连接。第二是404,页面中没有找到。另外还有服务本身一些日志,比如说这是503的日志,这种情况体现业务本身的日志。如果用日志的方式实现异常监控,不要使用try catch的方式影响错误日志的输出,保证能够监控到相应的错误的场景。
发现问题之后,要进行的就是解决问题。处理问题主要有几点:一是如果上线瞬间引发了问题,想到第一个方案就是快速回滚。如果是在一些业务稳定运行的时间内,又发生了问题,需要对问题做快速的定位。如果与服务本身没有关系,那么可能跟服务的资源有关系。如有大量的流量来临,或者内存泄露的情况,导致内存一直没有释放则考虑重启或者扩容的方式。
贝壳移动端监控建设实践
针对移动端上的Crash、自定义事件/错误、网络等痛点,多维度监控和报警功能十分有必要。贝壳找房移动端架构负责人,B端APP开发负责人刘伯温(企业代号名)老师在演讲中详细介绍了如何构建一个完整的监控体系,如何进行异常上报、分析、处理及报警,分享Native、Flutter 和 JS 等不同场景数据收集方案,从而实现线上问题主动发现、主动预警、聚合统计、全方位还原现场等。
首先看Crash监控功能需求,一般的 Fabric 包括调用栈、设备信息、系统信息等,贝壳在此基础上增加了业务线、系统日志、操作路径、报警及网络数据等功能。
在Crash捕获方面,可以通过Thread.setDefaultUncaughtExceptionHandler来设置一个自定义的UncaughtExceptionHandler,当Crash发生时,要先保存Crash信息,然后立即上传到后端,避免数据丢失。
而 ANR 捕获有两种方式,一种是发生ANR时写一个Traces文件,只需监听此文件即可。但是此版本会面临理解性文件性能问题。另一种方式是 WatchDOG 方式,当主线程收到消息时变量加 1 ,判断主线程是否堵塞,贝壳更多采用的是两种方案结合。
当 Crash 收集完之后是数据分发。此时 Dig 通道将消息推送至队列,动态负载持续检测资源状态,然后动态分配消费任务。
在Crash解析方面,当移动端收到崩溃消息时,通过调入栈传到后端,并将宿主和插件打包传到解析平台,而后堆栈、聚合。
在Crash报警上,第一个要考虑的事情是制定 Crash 的严重等级,达到什么样的才是严重的Crash。贝壳有以下几个评估为度:一个是多少次Crash,一个是多少用户Crash,另外还有占比比例。
那么报警策略怎么报警呢?贝壳将某一天中最多的 Crash*1.5,超过这个阈值就报警。另外还将报警分几个等级,最低等级是1.5倍。比如一天发生100次,认为这个不需要关注,但是发生150次就会认为需要报警了。报警的策略可分为单系统版本报警和单设备类型报警。
触发报警的形式是企业微信+邮件,报警详情包含数量、比例、业务线、时间段等内容,这样大家不用进去看,直接看报警消息即可。
通过这个强大的Crash监控,贝壳的B端Android App,Crash率降低到了0.007%的水平。
后面又分享了自定义错误监控,网络监控,及监控后端的技术干货。
Flutter 在贝壳的接入实践
贝壳找房移动端资深工程师逍遥风(企业代号名)老师从2019年Flutter发布正式版后开始调研将 Flutter 接入到当前的贝壳 APP 中,进行 Flutter 在贝壳的接入方案和平台化工作,在这一过程中,积累了丰富的经验。在演讲中,他介绍了贝壳 Flutter 接入在业务解耦、研发效率和集成自动化方面的探索,为 Flutter 在原生 APP中 接入提供了宝贵的参考。
Flutter 是 Fuchsia 的开发框架,是一套移动UI框架,可以快速在iOS、Android以及Fuchsia上构建高质量的原生用户界面。目前Flutter是完全免费、开源的。其官方编程语言为Dart,也是一门全新的语言。但是dart的上手成本并不高,语言以及框架的思想也结合了很多前端设计思想,可以认为是一种大前端通用设计理念。
Flutter工程中,通常有以下几种工程类型:
1. Flutter Application
标准的Flutter App工程,包含标准的Flutter Dart层与Native平台层
2. Flutter Module
Flutter组件工程,仅包含Dart层实现,Native平台层子工程为通过Flutter自动生成的隐藏工程
3. Flutter Plugin
Flutter平台插件工程,包含Dart层与Native平台层的实现
4. Flutter Package
Flutter纯Dart插件工程,仅包含Dart层的实现,往往定义一些公共Widget
日常flutter开发的最常见的场景是在已有的原生工程中接入Flutter,同时还不影响既有原生的配置和功能。最常用的实现方式是这样,把Flutter生成对应平台两个产物,在对应的原生安卓工程或者IOS工程进行依赖,不用配置任何Flutter东西就可以和原生App进行很好的结合。
通常的原生接入Flutter方案是利用FlutterModule功能,把Flutter作为子Module的形式,放到原生的Andriod或者IOS;或者将已有的原生Android和iOS工程作为Flutter的平台子工程。但是会出现原生App与Flutter耦合度较高、原生开发感知到flutter,关联flutter module时需要配置Flutter环境、无法满足已有的插件化或组件化业务工程分离的模式、Flutter代码所有业务耦合在同一个Module中,以及运行构建成本较高需要完整构建原生应用等问题。
基于此,贝壳在接入选择Flutter接入原生的时候,有几个考虑的点:
下图为整个接入方案的整体图,主要分成两个空间,就是Flutter的空间和原生空间。Flutter空间主要作用是在集成模式下生成一个产物,发布在远端;同时flutter空间对业务进行了工程分离解耦。原生工程就是通过直接依赖,这样对于原生开发者的影响比较小。
深入来看,右边是很标准的原生App结构,就是现在在国内比较通用的插件化和组件化方案,下面基本是基础通用的库,上层是一个主壳。同样我们在flutter空间也进行了类似的分层和设计,在基础库的上面是二手、新房等分离解耦的业务组件,flutter的portal主工程对所有的业务组件和基础库进行聚合生成flutter产物,平时Flutter开发过程业务只需要关注对应业务线的具体业务逻辑,无需关注flutter的平台特性,更不用关心嵌入的原生应用。
总之,贝壳Flutter接入方案优点可总结为以下几点:
业务分离:各个业务独立代码仓库,只需关注自己业务代码与其他业务解耦
平台无关:只关注业务核心逻辑,无需关注iOS/Android环境配置,我们帮助封装平台特性
研发效率:开发时只构建flutter,同时支持在业务package工程中热重载(hotreload)
集成无感:持续集成在Android实现无感知,QA在构建过程中无需关注flutter相关事宜