每一件事都要用多方面的角度来看它。
6 月 20 日下午,GMTC 北京 2019 全球大前端技术大会「多端提效与质量优化实践」技术专场,来自贝壳找房的四位技术专家分别就“极限前端性能优化”、“贝壳找房 Node 服务稳定性建设”、“贝壳移动端监控建设实践”以及“ Flutter 在贝壳的接入实践”主题进行分享。InfoQ 对本专场的精华内容做了部分梳理和总结。
极限前端性能优化
移动互联网时代,应用性能作为影响用户体验最重要的因素,在开发过程中显得尤为重要。性能优化是开发中老生常谈的话题,也是一名开发者从入门向资深进阶的必经阶段。在实践中,开发者们如何进行性能优化?
贝壳找房前端架构委员会专家嘻老师(花名),在专场活动中就向大家介绍了贝壳找房使用的性能优化工具,对比传统性能优化与极限性能的优化差异,并通过真实案例让大家了解性能优化的价值与收益,提升开发者在性能优化技术上更深一步的进阶。
性能优化需经历无阶段意识和有阶段意识两大阶段,以及无优化、通用方案、指标、匠心四小阶段。
在项目启动初期阶段,用户少,压力小,问题大多数是从单个个体用户的使用场景来看。慢慢地,开始注意性能优化的问题,寻找常规优化方法。逐步到有意识阶段,优化方式出现针对性和策略性,开始关注用户感官优化,力求在多个细节做到极致,更多以数据为基础导向。
性能优化本身是需要数据来支撑的。贝壳找房的数据平台叫 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 两部分。
其次是发现问题。发现问题可能两种思路,第一种是主动发现问题,考虑触发边缘的 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 接入方案优点可总结为以下几点: