查看原文
其他

历时1年,上百万行代码!首次揭秘手淘全链路性能优化(上)

淘宝技术 淘系技术 2020-02-20

作者|手淘用户体验提升项目组

出品|阿里巴巴新零售淘系技术部


导读:自阿里在11年提出 All in 无线之后,手淘慢慢成长为承载业务最多,体量巨大的航母级移动端应用。与之相应的,手淘离轻量,快速,敏捷这些关键词却越来越远,启动慢,使用卡逐步成为用户使用过程中的主要体验问题。为此,手淘的技术团队启动了极速版项目,其目标是还给用户一个更加流畅的淘宝。整个项目历时近1年,横跨几十个团队,经历了数百次的数据实验,涉及代码上百万行,最终使得手淘的性能有一个质的飞跃。


下面,我们一起来看手淘团队在性能优化过程中的一些思考和实践。


启动框架的思考


▐   启动框架在手淘的意义


启动性能,是用户在使用APP 过程中的第一感观,可见是相当重要的。相信很多同学都能说出一些常规的手段,比如只加载必要的模块,延迟加载等。从大的策略上说,是没有问题的,也是手淘做启动性能优化的一个方向,也得了一些效果,但仍存在一些问题。


前面提到,手淘承载的业务非常多,为了更好支撑业务,使用了动态化技术及一些非常复杂的策略,就首页本身依赖的模块和任务就非常多,相互关系也复杂,只加载必要任务,仍然是一笔不小的开销。于是,为了更加极致的优化,我们不得不继续思考性能优化的本质。


通常我们为了更快的达到目标,把与目标无关的事情,提到完成目标之后,通过减少执行代码从而减少执行时间的方式,叫着软优化。相对的,对于提升系统的吞吐效率,对于相同的代码用更少的执行时间完成,叫着硬优化。硬优化是面向硬件资源,包括CPU,内存,网络,磁盘 IO等的调度,减少等待时间,最大化利用硬件资源,保持系统负载在合理范围内。


这次优化我们有一个大的原则,要求基本不能影响业务需求,也就是要在不减任何业务代码的情况下进行优化。


对手淘而言,因为启动包含很多基础 SDK,SDK 的初始化有着一定的先后顺序;业务 SDK 又是围绕着多个基础 SDK 建立的。


那么如何保证这些 SDK 在正确的阶段、按照正确的依赖顺序、高效地初始化?怎么合理调度任务,才不至于让系统负载过高?如何最大化利用设备的性能,承接越来越多的业务?


其实启动框架就是一个任务调度系统,是手淘启动的“大管家”。各个业务模块我们称之为启动任务,管家要做的事情就是把它们的关系梳理得明明白白,有条不紊,合理安排位置、调度时间,同时提升硬件资源的利用率。


▐  启动框架的思路


总结下来无非就是两点:一是 如何保证时序 ;二是 怎么控制拥塞,提高吞吐,充实不瞎忙。我们先看一组实验数据,在并发下面的 IO 性能。


启动任务高并发 IO耗时低并发 IO 耗时
InitPH331ms

139ms

InitCM315ms

179ms

InitSM270ms

182ms

InitPA245ms

66ms

InitAV159ms

64ms


由表上的数据可以看到降低 IO 的并发,整体的执行时间大幅降低。


我们借鉴了很多任务调度系统。比如谷歌新出的 WorkManager,再比如 Spark 的 DAGScheduler。


从 Spark 的 DAGScheduler 中领悟到它的核心思想,面向阶段调度(Stage-Oriented Scheduler):把应用划分成一个个的阶段(Stage),再把任务(Task)安排到各个阶段中去,任务的编排则是通过构建 有向无环图(DAG),把任务依赖通过图的方式梳理得 井井有条。因为它分阶段执行,先集中资源把阶段一搞定,再齐心协力去执行阶段二,这样即能控制拥塞,又能保证时序,还能并发执行,让设备性能尽可能得到发挥,岂不美哉:



▐  阶段划分


什么阶段做什么事情,前面打基础,只有夯实了基础,后期才能顺理成章。我们把手淘的启动阶段做了以下细分:


启动流程如下:


可以看到:整个流程很清晰,分阶段、多任务并发执行,不存在老框架下几条初始化链路交错在一起的情况,首页那一块位置不受干扰。


▐  任务编排


无锁化,得益于“有向无环图”,通过构建任务间的依赖,启动框架严格按照图的顺序执行各项 SDK 的初始化,真正做到时序可预期,原本需要靠锁来保证状态同步的,现在转变成了“无锁化”。


开箱即用,对一项启动任务而言,极致的体验应该是:无论我身处何处,所依赖的基础库、中间件们都应该“开箱即用”。


多任务并发,早期任务少、业务简单,基础尚未成型,单流水线作业就够了;但随着业务日益膨胀,基础只会越来越厚,就必须多流水线齐头并进,协同作业,提高吞吐率。


无锁化的好处:


  • 代码执行效率高,SDK 的初始化基本上都需要考虑多线程安全问题,如果从时序上能保证顺序,也即不存在竞争,等同于“无锁”;

  • 减少 ANR,降低卡顿故障,比如我们之前查的网络库在 vivo y85a 上启动长时卡顿达 1s 以上的问题,如果我们能正确梳理各项 SDK 之间的依赖,类似的问题就可以避免了;

任务编排是重中之重,是决定成败的重要因素:依赖梳理不当,执行效率上不去。


▐  任务调度


要支持多任务并发,那肯定绕不开线程池,既然要用到线程池,那线程池大小需要一个比较合理的设置。


★ 核心思想

阶段(Stage)+ 线程池(ThreadPool Executor)


★ 线程池大小

因为我们的 SDK 大多涉及到 so 的加载、文件的读写,线程等待时间占比比较高,所以我采用了一个通用的估算方法:2N + 1,N 是 CPU 个数。


★ 线程优先级

把先于首页(落地页)的阶段的线程优先级都调高一些,以求得到优先调度,尽快执行;进入 idle 阶段后,性质原因,慢任务居多,调整线程池大小,同时把优先级调低,做到尽量不干扰 UI 主线程,在后台慢慢跑。


实际运行的 DAG 图


优化效果:



启动环境是应用中最为复杂环节,任务多,负载重,资源争抢下,不管是 CPU ,内存,网络,IO都有可能成为瓶颈,启动框架的引入,让我们在面对这些挑战时,有了一个明确的方向,给出一个稍微系统化的解。当然,系统资源调度优化是个非常深刻的课题,加上手机各种硬件配置多样性,我们在这个领域仍然面临更大的挑战,当前只是一个开始。


网络的链路优化


▐  问题定义


可以看到,手淘首次安装冷启动30s内,网络请求数达到 400上下。非首次冷启动30s内,请求数相比首装冷启减少,但依然在 100+。启动场景下,存在着以下几个问题:


  • 请求过多:重复请求、请求滥发情况严重。

  • 数据量过大:资源文件的下载占流量的80%以上。

  • 业务方请求时机不合理:非首页&启动必要请求需延后。


过量的请求集中在启动阶段导致原本就有限的网络带宽和端上处理能力更加严峻。


▐  深入剖析


更深入些来看,我们尝试以一个请求的全链路出发来看,探寻每个请求真正耗时的点在哪里。

为何是全链路请求分析?


一直以来,性能埋点方案均为独立的模块,更多针对各个SDK关注自身的请求性能。但是从一个数据或图片请求链路上来看,一个完整的请求往往跨越多个核心SDK。特定场景内(启动),每个环节的耗时都会牵一发而动全身影响请求的性能,剥离完整请求和特定场景单纯从某个中间模块看整体性能往往不能发现最根本的问题。就现状而言,独立SDK的埋点方案显然不能够把一个请求串联起来,以一个场景切入做更精准的分析。因此,亟需从特定场景下请求的完整链路角度来分析,以揪出各个阶段的耗时请求。

对于请求整个链路,我们把请求的关键耗时阶段抽象为以下几点。


  • 发送处理:本地处理耗时,包含数据或图片库处理,网络库处理耗时

  • 网络库耗时:纯网络传输时间

  • 返回处理:包括网络库响应处理回调和上层图片库的处理(json解析/图片解码)操作

  • 回调消息-回调执行:任务dispatch到主线程并开始消费的耗时,反映主线程的流畅程度

  • 回调执行-回调返回:业务在回调内部执行处理的耗时


从首次安装冷启动的场景切入,我们线下针对启动30s内的请求在图片库、网络库内部进行了日志打点统计,以获取请求全链路各个关键阶段的耗时情况。如下图:



分析启动请求耗时阶段,针对每个阶段得出以下结论和优化点:


  • 发送处理阶段:网络库bindService影响前x个请求,图片并发限制图片库线程排队。

  • 网络耗时:部分请求响应size大,包括 SO文件,Cache资源,图片原图大尺寸等。

  • 返回处理:个别数据网关请求json串复杂解析严重耗时(3s),且历史线程排队设计不合适。

  • 上屏阻塞:回调UI线程被阻,反映主线程卡顿严重。高端机达1s,低端机恶化达3s以上。

  • 回调阻塞:部分业务回调执行耗时,阻塞主线程或回调线程。


▐  请求治理


对于应用启动,尽快地完成启动展现可交互页面给用户是第一要务。有限的网络带宽和端上处理能力,意味着过多的请求势必会导致资源争抢更加严重。首页无关&不合理请求很大程度上回阻塞启动主链路请求的响应耗时。


针对启动阶段请求,我们开展了请求治理行动,每个请求责任到人,横向推动业务方评估请求的必要性。主要从以下几个方面展开:


  • 多次重复的请求,业务方务必收敛请求次数,减少非必须请求。

  • 数据大的请求如资源文件、so文件,非启动必须统一延后或取消。

  • 业务方回调执行阻塞主线程耗时过长整改。我们知道,肉眼可见流畅运行,需要运行60帧/秒, 意味着每帧的处理时间不超过16ms。针对主线程执行回调超过16ms的业务方,推动主线程执行优化。

  • 协议json串过于复杂导致解析耗时严重,网络并发线程数有限,解析耗时过长意味着请求长时间占用MTOP线程影响其他关键请求执行。推动业务方handler注入使用自己的线程解析或简化json串。


▐  结果


优化后的数据看,首装冷启动请求减少300+,请求数优化至30个左右,整体减少60%,带宽流量减少75%。


请求数的减少带来的是更快的首页展现。 首屏图片渲染耗时中高端机上从4.2s减少至2.1s,低端机上从12.7s减少至7.8s。


效果评估

所有的优化结果,都应该是客观的,稳定的,可重复的。为此,我们专门搭建了一套优化效果的自动化评估方案。当然,我们首先要定义,我们的结果数据怎么体现。


▐  数据指标定义


第一个维度-指标:定义合适的数据指标,结合业务场景,多方位评估启动和页面的用户体感性能。



1、数据指标:经过手淘用户体验提升项目组的讨论,定义了如下指标来衡量用户的体感数据, 之前大部分的响应时长只规定了渲染完成时长, 可以反映应用的部分性能情况,但是渲染完成后用户多久可以对应用进行操作, 是否有卡顿,无法通过该指标观察到。因此新增了两个指标,可交互时长和可流畅交互时长,可以比较直观的反映用户最早可以对应用进行交互的时间。


  • 渲染时长:点击进入页面,页面80%以上内容渲染完毕。


渲染中

渲染完毕

 

 


  • 可交互时长:页面渲染完毕后立即开始滑屏操作,页面能响应滑屏事件那一刻即为可交互时长。


渲染完毕

可交互


  • 可流畅交互时长:页面进入可交互状态后,匀速连续上下滑动屏幕,直至屏幕上下滚动跟手势同步次数超过3次以上即可判断为可流畅交互。


可交互

可流畅交互

 

 


2、业务场景:不论是应用启动还是在应用中打开页面都会有不同的业务场景。只有从多个不同的场景下对应用进行多角度评估, 获得的数据才能够全面反映用户在不同情况下的真实感受。


  • 启动:可按照不同的安装方式、启动方式、启动发起方分为不同的启动业务场景。


  • 页面打开:可按照不同的页面进入方式氛围不同的页面响应时间业务场景。


第二个维度-自动化:自动化手段可以支撑实现体感数据的高效采集和3个用户体感数据的准确计算。


第三个维度-流程:通过指标定义, 以及对应指标数据的自动化采集,我们可以在发布前、发布中、发布后的全研发流程中对应用的用户体感性能进行评估。



▐  自动化测试方案


人工测试方案,虽然能达到了我们的目标,可以较为准确的反应应用的用户体感性能,但是存在两个问题:效率较低,产出数据需要时间比较长 ;不同人的操作可能不一致,造成数据采集标准不一致;因此我们需要把人工测试方案转化为自动化方案,达到高效、稳定产出数据的目的。


▐  关键点识别


主要思路是从视频中找出来渲染完成、可交互完成、可流畅交互完成几个节点的关键特征,通过程序算法去进行识别。

考虑过的几种识别算法:


  • 算法1:  相邻两帧变化趋于平稳,无变化时,认为渲染完成, 经过实验后发现对于存在动画的页面,该算法的结果会比实际情况要长。

  • 算法2:  使用参考帧概念,将业务页面渲染完成的图片作为参考, 比较每一帧与该参照图的相似度, 当相似度>=门限时,认为启动完成。该算法的缺点是对于一些变化频繁的页面, 比如首页更换了banner图或氛围,变了投放元素,原来的参考图就无效了,需要进行更换且更换成本较高。

  • 算法3: 检测关键特征,如8个icon, 5个tab都出现认为启动完成。这个算法的难点在于不同页面的特征提取,需要比较多的调整工作,而且在不同分辨率的手机上特征出现情况可能不一样, 还需要根据屏幕适配。

  • 算法4:通过OCR提取图片中的文字信息作为关键特征。该算法的优势:1. 在于应用页面上基本都是有文字的, OCR也可以识别到图片上的文字, 文字出现则图片加载完成, 和用户体感是一致的;2. 文字作为特征,过滤掉了很多图片特征可能带来的噪声, 减少了算法调试的工作量;另外阿里集团内有非常成熟和优秀的OCR服务——读光,文档识别率超过99.7%, 使用水滴平台封装的OCR服务,可以快速接入和使用。最终的识别方案就是基于OCR识别来进行的,以下介绍下基于OCR的识别方案的改进过程。


通过观察视频,我们可以发现这样一个规律, 中转页, 开始进入页面, 页面渲染完成,页面可滑动这几种状态下, OCR字符串长度是不一样的,并且由于操作的固定性(进入页面,来回滑动)这个曲线存在一定的模式,基本可以分为两种, 一种是可滑动后滑动到的页面字数比渲染完成要多, 另一种是可滑动后滑动到的页面字数比渲染完成要少。



进入页面前的中转页面,这个页面总是字数较少的。


页面渲染完成, 页面元素比较丰富,字数也比较多。



页面可交互,字数相对渲染完成时的情况要多一点,或者少一点,存在两种不同的曲线模式。


识别到上面的模式之后,我们的识别算法也基本确定下来。


基于上面的自动化方案,从自动化驱动到自动识别渲染、可交互、可流畅交互时长,几乎不需要人的参与。


  • 可以动态的适应不同业务场景,对手淘主要业务场景进行一次评测从1天1人力减少到了2小时0人力,并且可以自动产出版本报表。

  • 在研发进行性能优化的阶段, 每日自动产出各业务用户体验时长数据, 为优化提供决策参考。某个优化是否要集成,集成之前也需要先产出下数据,评估其价值, 如果提升不明显风险较高, 则放弃该优化。


小结


性能优化是老生常谈的问题,说简单也不简单,需要一个系统化的视角来分析和解决。找问题,不仅仅是要看到某段区间慢了,更要去深入分析,为什么慢了。trace 上一段方法执行时间过长,有可能是本身逻辑复杂,或是有 IO 等耗时操作,也有可能是因为 CPU 调度,IO 竞争等原因,因此,在分析上一定要能系统化进行全局思考。

工具是性能优化利器,除了使用像 trace 及 systrace,过渡绘制等常见的工具,还用到一些 linux 命令,直观的观察系统内各进程及线程的运行情况,当前系统负载情况等,当然,原生工具还是有一些局限性,特别是像IO 的读写分析这样特别领域,还是显得有些力不从心,为此在优化过程中我们也沉淀了不少的工具,比如细粒度方法级耗时监控,及IO 读写的监控,有了合适的工具,能极大的提高效率。


整个优化过程中,发现问题不难,难的是对解决方案技术决策。这次优化过程中,我们发现比较大的一个问题是代码规模迅速膨胀,功能堆砌式累积,启动整个系统运行时的效率偏低,当前手淘的架构不能满足对极致体验的要求。因此我们的主要手段是对启动框架重新定义,包括前面提到的对任务进行按序编排,对网络资源的合理使用,减少排队情况,以此提升系统的吞吐率。优化过程除了拼智力,还得拼体力。手淘的业务规模十分复杂,上百个启动任务需要重新 reivew,梳理特性编排顺序,还有数百个网络请求的清理,用阿里的土话说,脑力,心力,体力,缺一不可。

一般说在缓存的使用场景上,通常是借助于 LRU 算法或是其变种,提升 cache 的命中率。智能化预加载,是我们在优化过程进一步尝试,希望在命中率与下载缓存数上寻找到一个最适合的奇点。这次针对 H5 的缓存优化,我们尝试使用了机器学习的方式,通过统计用户的使用习惯及 H5 的访问频次来设计H5的缓存下载,在大辐降低下载缓存数量的同时,又保证了命中率的基本稳定。


无人化验证优化数据,在整个性能优化过程是非常重要的一环,能够快速验证优化是否有效。除了性能本身的收益之外,我们更需要关注优化对业务的影响。对于手淘来说,要在前进中,重构架构无疑是相当于飞行中更换引擎,任何不经意一句代码,都可能对业务造成严重的影响。而 AB 实验,在优化过程中扮演着非常关键的决策的角色,我们的优化项是否能真正上线,一切以 AB 实验的结果为依据。借助于AB 实验,隔离掉无关因素,认真核对实验中的数据是否存在不可预期的变化,及时控制其中风险。





    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存