天天看點

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

作者|手淘使用者體驗提升項目組

出品|阿裡巴巴新零售淘系技術部

導讀:自阿裡在11年提出 All in 無線之後,手淘慢慢成長為承載業務最多,體量巨大的航母級移動端應用。與之相應的,手淘離輕量,快速,靈活這些關鍵詞卻越來越遠,啟動慢,使用卡逐漸成為使用者使用過程中的主要體驗問題。為此,手淘的技術團隊啟動了極速版項目,其目标是還給使用者一個更加流暢的淘寶。整個項目曆時近1年,橫跨幾十個團隊,經曆了數百次的資料實驗,涉及代碼上百萬行,最終使得手淘的性能有一個質的飛躍。
下面,我們一起來看手淘團隊在性能優化過程中的一些思考和實踐。

啟動架構的思考

▐ 啟動架構在手淘的意義

啟動性能,是使用者在使用APP 過程中的第一感觀,可見是相當重要的。相信很多同學都能說出一些正常的手段,比如隻加載必要的子產品,延遲加載等。從大的政策上說,是沒有問題的,也是手淘做啟動性能優化的一個方向,也得了一些效果,但仍存在一些問題。

前面提到,手淘承載的業務非常多,為了更好支撐業務,使用了動态化技術及一些非常複雜的政策,就首頁本身依賴的子產品和任務就非常多,互相關系也複雜,隻加載必要任務,仍然是一筆不小的開銷。于是,為了更加極緻的優化,我們不得不繼續思考性能優化的本質。

通常我們為了更快的達到目标,把與目标無關的事情,提到完成目标之後,通過減少執行代碼進而減少執行時間的方式,叫着軟優化。相對的,對于提升系統的吞吐效率,對于相同的代碼用更少的執行時間完成,叫着硬優化。硬優化是面向硬體資源,包括CPU,記憶體,網絡,磁盤 IO等的排程,減少等待時間,最大化利用硬體資源,保持系統負載在合理範圍内。

這次優化我們有一個大的原則,要求基本不能影響業務需求,也就是要在不減任何業務代碼的情況下進行優化。

對手淘而言,因為啟動包含很多基礎 SDK,SDK 的初始化有着一定的先後順序;業務 SDK 又是圍繞着多個基礎 SDK 建立的。

那麼如何保證這些 SDK 在正确的階段、按照正确的依賴順序、高效地初始化?怎麼合理排程任務,才不至于讓系統負載過高?如何最大化利用裝置的性能,承接越來越多的業務?

其實啟動架構就是一個任務排程系統,是手淘啟動的“大管家”。各個業務子產品我們稱之為啟動任務,管家要做的事情就是把它們的關系梳理得明明白白,有條不紊,合理安排位置、排程時間,同時提升硬體資源的使用率。

▐ 啟動架構的思路

總結下來無非就是兩點:一是 如何保證時序 ;二是 怎麼控制擁塞,提高吞吐,充實不瞎忙。我們先看一組實驗資料,在并發下面的 IO 性能。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

由表上的資料可以看到降低 IO 的并發,整體的執行時間大幅降低。

我們借鑒了很多任務排程系統。比如谷歌新出的 WorkManager,再比如 Spark 的 DAGScheduler。

從 Spark 的 DAGScheduler 中領悟到它的核心思想,面向階段排程(Stage-Oriented Scheduler):把應用劃分成一個個的階段(Stage),再把任務(Task)安排到各個階段中去,任務的編排則是通過建構 有向無環圖(DAG),把任務依賴通過圖的方式梳理得 井井有條。因為它分階段執行,先集中資源把階段一搞定,再齊心協力去執行階段二,這樣即能控制擁塞,又能保證時序,還能并發執行,讓裝置性能盡可能得到發揮,豈不美哉:

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

▐ 階段劃分

什麼階段做什麼事情,前面打基礎,隻有夯實了基礎,後期才能順理成章。我們把手淘的啟動階段做了以下細分:

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

啟動流程如下:

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

可以看到:整個流程很清晰,分階段、多任務并發執行,不存在老架構下幾條初始化鍊路交錯在一起的情況,首頁那一塊位置不受幹擾。

▐ 任務編排

無鎖化,得益于“有向無環圖”,通過建構任務間的依賴,啟動架構嚴格按照圖的順序執行各項 SDK 的初始化,真正做到時序可預期,原本需要靠鎖來保證狀态同步的,現在轉變成了“無鎖化”。

開箱即用,對一項啟動任務而言,極緻的體驗應該是:無論我身處何處,所依賴的基礎庫、中間件們都應該“開箱即用”。

多任務并發,早期任務少、業務簡單,基礎尚未成型,單流水線作業就夠了;但随着業務日益膨脹,基礎隻會越來越厚,就必須多流水線齊頭并進,協同作業,提高吞吐率。

無鎖化的好處:

代碼執行效率高,SDK 的初始化基本上都需要考慮多線程安全問題,如果從時序上能保證順序,也即不存在競争,等同于“無鎖”;

減少 ANR,降低卡頓故障,比如我們之前查的網絡庫在 vivo y85a 上啟動長時卡頓達 1s 以上的問題,如果我們能正确梳理各項 SDK 之間的依賴,類似的問題就可以避免了;

任務編排是重中之重,是決定成敗的重要因素:依賴梳理不當,執行效率上不去。

▐ 任務排程

要支援多任務并發,那肯定繞不開線程池,既然要用到線程池,那線程池大小需要一個比較合理的設定。

核心思想

階段(Stage)+ 線程池(ThreadPool Executor)

線程池大小

因為我們的 SDK 大多涉及到 so 的加載、檔案的讀寫,線程等待時間占比比較高,是以我采用了一個通用的估算方法:2N + 1,N 是 CPU 個數。

線程優先級

把先于首頁(落地頁)的階段的線程優先級都調高一些,以求得到優先排程,盡快執行;進入 idle 階段後,性質原因,慢任務居多,調整線程池大小,同時把優先級調低,做到盡量不幹擾 UI 主線程,在背景慢慢跑。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

實際運作的 DAG 圖

優化效果:

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

啟動環境是應用中最為複雜環節,任務多,負載重,資源争搶下,不管是 CPU ,記憶體,網絡,IO都有可能成為瓶頸,啟動架構的引入,讓我們在面對這些挑戰時,有了一個明确的方向,給出一個稍微系統化的解。當然,系統資源排程優化是個非常深刻的課題,加上手機各種硬體配置多樣性,我們在這個領域仍然面臨更大的挑戰,目前隻是一個開始。

網絡的鍊路優化

▐ 問題定義

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)
曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

可以看到,手淘首次安裝冷啟動30s内,網絡請求數達到 400上下。非首次冷啟動30s内,請求數相比首裝冷啟減少,但依然在 100+。啟動場景下,存在着以下幾個問題:

  • 請求過多:重複請求、請求濫發情況嚴重。
  • 資料量過大:資源檔案的下載下傳占流量的80%以上。
  • 業務方請求時機不合理:非首頁&啟動必要請求需延後。

過量的請求集中在啟動階段導緻原本就有限的網絡帶寬和端上處理能力更加嚴峻。

▐ 深入剖析

更深入些來看,我們嘗試以一個請求的全鍊路出發來看,探尋每個請求真正耗時的點在哪裡。

為何是全鍊路請求分析?

一直以來,性能埋點方案均為獨立的子產品,更多針對各個SDK關注自身的請求性能。但是從一個資料或圖檔請求鍊路上來看,一個完整的請求往往跨越多個核心SDK。特定場景内(啟動),每個環節的耗時都會牽一發而動全身影響請求的性能,剝離完整請求和特定場景單純從某個中間子產品看整體性能往往不能發現最根本的問題。就現狀而言,獨立SDK的埋點方案顯然不能夠把一個請求串聯起來,以一個場景切入做更精準的分析。是以,亟需從特定場景下請求的完整鍊路角度來分析,以揪出各個階段的耗時請求。

對于請求整個鍊路,我們把請求的關鍵耗時階段抽象為以下幾點。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)
  • 發送處理:本地處理耗時,包含資料或圖檔庫處理,網絡庫處理耗時
  • 網絡庫耗時:純網絡傳輸時間
  • 傳回處理:包括網絡庫響應處理回調和上層圖檔庫的處理(json解析/圖檔解碼)操作
  • 回調消息-回調執行:任務dispatch到主線程并開始消費的耗時,反映主線程的流暢程度
  • 回調執行-回調傳回:業務在回調内部執行處理的耗時

從首次安裝冷啟動的場景切入,我們線下針對啟動30s内的請求在圖檔庫、網絡庫内部進行了日志打點統計,以擷取請求全鍊路各個關鍵階段的耗時情況。如下圖:

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

分析啟動請求耗時階段,針對每個階段得出以下結論和優化點:

  • 發送處理階段:網絡庫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年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

1、資料名額:經過手淘使用者體驗提升項目組的讨論,定義了如下名額來衡量使用者的體感資料, 之前大部分的響應時長隻規定了渲染完成時長, 可以反映應用的部分性能情況,但是渲染完成後使用者多久可以對應用進行操作, 是否有卡頓,無法通過該名額觀察到。是以新增了兩個名額,可互動時長和可流暢互動時長,可以比較直覺的反映使用者最早可以對應用進行互動的時間。

  • 渲染時長:點選進入頁面,頁面80%以上内容渲染完畢。
曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)
  • 可互動時長:頁面渲染完畢後立即開始滑屏操作,頁面能響應滑屏事件那一刻即為可互動時長。
曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

可流暢互動時長:頁面進入可互動狀态後,勻速連續上下滑動螢幕,直至螢幕上下滾動跟手勢同步次數超過3次以上即可判斷為可流暢互動。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

2、業務場景:不論是應用啟動還是在應用中打開頁面都會有不同的業務場景。隻有從多個不同的場景下對應用進行多角度評估, 獲得的資料才能夠全面反映使用者在不同情況下的真實感受。

啟動:可按照不同的安裝方式、啟動方式、啟動發起方分為不同的啟動業務場景。

頁面打開:可按照不同的頁面進入方式氛圍不同的頁面響應時間業務場景。

第二個次元-自動化:自動化手段可以支撐實作體感資料的高效采集和3個使用者體感資料的準确計算。

第三個次元-流程:通過名額定義, 以及對應名額資料的自動化采集,我們可以在釋出前、釋出中、釋出後的全研發流程中對應用的使用者體感性能進行評估。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

▐ 自動化測試方案

人工測試方案,雖然能達到了我們的目标,可以較為準确的反應應用的使用者體感性能,但是存在兩個問題:效率較低,産出資料需要時間比較長 ;不同人的操作可能不一緻,造成資料采集标準不一緻;是以我們需要把人工測試方案轉化為自動化方案,達到高效、穩定産出資料的目的。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

▐ 關鍵點識别

主要思路是從視訊中找出來渲染完成、可互動完成、可流暢互動完成幾個節點的關鍵特征,通過程式算法去進行識别。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

考慮過的幾種識别算法:

算法1: 相鄰兩幀變化趨于平穩,無變化時,認為渲染完成, 經過實驗後發現對于存在動畫的頁面,該算法的結果會比實際情況要長。

算法2: 使用參考幀概念,将業務頁面渲染完成的圖檔作為參考, 比較每一幀與該參照圖的相似度, 當相似度>=門限時,認為啟動完成。該算法的缺點是對于一些變化頻繁的頁面, 比如首頁更換了banner圖或氛圍,變了投放元素,原來的參考圖就無效了,需要進行更換且更換成本較高。

算法3: 檢測關鍵特征,如8個icon, 5個tab都出現認為啟動完成。這個算法的難點在于不同頁面的特征提取,需要比較多的調整工作,而且在不同分辨率的手機上特征出現情況可能不一樣, 還需要根據螢幕适配。

算法4:通過OCR提取圖檔中的文字資訊作為關鍵特征。該算法的優勢:1. 在于應用頁面上基本都是有文字的, OCR也可以識别到圖檔上的文字, 文字出現則圖檔加載完成, 和使用者體感是一緻的;2. 文字作為特征,過濾掉了很多圖檔特征可能帶來的噪聲, 減少了算法調試的工作量;另外阿裡集團内有非常成熟和優秀的OCR服務——讀光,文檔識别率超過99.7%, 使用水滴平台封裝的OCR服務,可以快速接入和使用。最終的識别方案就是基于OCR識别來進行的,以下介紹下基于OCR的識别方案的改進過程。

通過觀察視訊,我們可以發現這樣一個規律, 中轉頁, 開始進入頁面, 頁面渲染完成,頁面可滑動這幾種狀态下, OCR字元串長度是不一樣的,并且由于操作的固定性(進入頁面,來回滑動)這個曲線存在一定的模式,基本可以分為兩種, 一種是可滑動後滑動到的頁面字數比渲染完成要多, 另一種是可滑動後滑動到的頁面字數比渲染完成要少。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

進入頁面前的中轉頁面,這個頁面總是字數較少的。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

頁面渲染完成, 頁面元素比較豐富,字數也比較多。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

頁面可互動,字數相對渲染完成時的情況要多一點,或者少一點,存在兩種不同的曲線模式。

曆時1年,上百萬行代碼!首次揭秘手淘全鍊路性能優化(上)

識别到上面的模式之後,我們的識别算法也基本确定下來。

基于上面的自動化方案,從自動化驅動到自動識别渲染、可互動、可流暢互動時長,幾乎不需要人的參與。

可以動态的适應不同業務場景,對手淘主要業務場景進行一次評測從1天1人力減少到了2小時0人力,并且可以自動産出版本報表。

在研發進行性能優化的階段, 每日自動産出各業務使用者體驗時長資料, 為優化提供決策參考。某個優化是否要內建,內建之前也需要先産出下資料,評估其價值, 如果提升不明顯風險較高, 則放棄該優化。

小結

性能優化是老生常談的問題,說簡單也不簡單,需要一個系統化的視角來分析和解決。找問題,不僅僅是要看到某段區間慢了,更要去深入分析,為什麼慢了。trace 上一段方法執行時間過長,有可能是本身邏輯複雜,或是有 IO 等耗時操作,也有可能是因為 CPU 排程,IO 競争等原因,是以,在分析上一定要能系統化進行全局思考。

工具是性能優化利器,除了使用像 trace 及 systrace,過渡繪制等常見的工具,還用到一些 linux 指令,直覺的觀察系統内各程序及線程的運作情況,目前系統負載情況等,當然,原生工具還是有一些局限性,特别是像IO 的讀寫分析這樣特别領域,還是顯得有些力不從心,為此在優化過程中我們也沉澱了不少的工具,比如細粒度方法級耗時監控,及IO 讀寫的監控,有了合适的工具,能極大的提高效率。

整個優化過程中,發現問題不難,難的是對解決方案技術決策。這次優化過程中,我們發現比較大的一個問題是代碼規模迅速膨脹,功能堆砌式累積,啟動整個系統運作時的效率偏低,目前手淘的架構不能滿足對極緻體驗的要求。是以我們的主要手段是對啟動架構重新定義,包括前面提到的對任務進行按序編排,對網絡資源的合理使用,減少排隊情況,以此提升系統的吞吐率。優化過程除了拼智力,還得拼體力。手淘的業務規模十分複雜,上百個啟動任務需要重新 reivew,梳理特性編排順序,還有數百個網絡請求的清理,用阿裡的土話說,腦力,心力,體力,缺一不可。

一般說在緩存的使用場景上,通常是借助于 LRU 算法或是其變種,提升 cache 的命中率。智能化預加載,是我們在優化過程進一步嘗試,希望在命中率與下載下傳緩存數上尋找到一個最适合的奇點。這次針對 H5 的緩存優化,我們嘗試使用了機器學習的方式,通過統計使用者的使用習慣及 H5 的通路頻次來設計H5的緩存下載下傳,在大輻降低下載下傳緩存數量的同時,又保證了命中率的基本穩定。

無人化驗證優化資料,在整個性能優化過程是非常重要的一環,能夠快速驗證優化是否有效。除了性能本身的收益之外,我們更需要關注優化對業務的影響。對于手淘來說,要在前進中,重構架構無疑是相當于飛行中更換引擎,任何不經意一句代碼,都可能對業務造成嚴重的影響。而 AB 實驗,在優化過程中扮演着非常關鍵的決策的角色,我們的優化項是否能真正上線,一切以 AB 實驗的結果為依據。借助于AB 實驗,隔離掉無關因素,認真核對實驗中的資料是否存在不可預期的變化,及時控制其中風險。