[譯] 通過後臺資料預獲取技術實現效能提升

再也不悍跳的阿哲發表於2018-02-01

通過後臺資料預獲取技術實現效能提升

Instagram 社群變得比從前要更加龐大和多樣化。每月有 8 億人訪問 Instagram,其中有 80% 的訪問來自美國之外的地方。隨著社群的不斷擴大,面對多樣複雜的網路狀況、種類繁多的裝置和非傳統的使用模式,我們的 app 是否能依舊錶現出色,這個問題變得越來越重要。來自紐約的客戶端效能優化團隊正在致力於使 Instagram 變得更流暢,效能更強大,讓來自任何地區的任意使用者都能有較好的體驗。

具體而言,我們團隊的工作重點是在不浪費任何網路和硬碟資源的情況下,做即時的內容分發。最近我們決定著重研發高效的後臺資料預獲取技術,通過這項技術,可以讓 Instagram 在網路不可用或使用者流量套餐受限的情況下依然能正常使用。

遇到的問題

網路可用性

[譯] 通過後臺資料預獲取技術實現效能提升

世界上大部分地區的網路質量都不容樂觀。我們的資料科學家 Michael Midling 繪製出了上方這張地圖來表示世界上不同國家地區使用 Instagram 的平均網路頻寬。深綠色的區域,比如加拿大,有約 4+Mbps 的頻寬。相比較而言,像印度這種淺綠色的區域,就只有 1Mbps 的平均頻寬。

當使用者開啟 Instagram,開始檢視分享內容或者滑動瀏覽 feed 時,我們不能假設這些媒體資源都是可用的。如果你想在印度開發一款流暢的媒體應用,由於他們的網路不夠發達且網路傳輸延遲可能高達 2 秒以上,所以你需要使用不一樣的資料載入策略而不是實時載入資料。如果我們希望每個人都能暢通無阻地訪問 Instagram,瀏覽來自親密的朋友和興趣列表中的視訊和圖片,那麼我們必須能夠應付不同的網路頻寬速率。創造能適應所有這些網路情況的應用是一種挑戰。

手機網路敏感

我們的一個解決方案是將使用者的網路型別寫到我們的日誌系統中。這樣,我們便可以觀察不同網路型別使用者的使用情況,來幫助我們適配。我們努力做到適配每個人的資料流量套餐並最大化免費網路連線的資料傳輸。

[譯] 通過後臺資料預獲取技術實現效能提升

上圖展示了全球的使用者是通過什麼網路來訪問我們的應用。比如在印尼,當人們的流量套餐快要用完時,他們會切換 SIM 卡,然後主要使用蜂窩網訪問。但是,在巴西,人們大部分時間都是通過 wifi 來訪問我們的應用的。

網路連線失敗

[譯] 通過後臺資料預獲取技術實現效能提升

如果網路連線全部都失敗了會怎麼樣?之前,我們會將未獲取到的圖片、視訊顯示為灰色的方塊,希望使用者在網路情況變好時回來重試。但是這樣體驗不好。

[譯] 通過後臺資料預獲取技術實現效能提升

分散的網路連線和蜂窩網路擁塞都是我們關心的問題。當使用者處於上方地圖中頻寬較低的淺綠色區域時,我們需要想出一個辦法來減小或消除使用者的載入等待時間。

我們的目標是讓使用者對於網路連線斷開無感知,但是對於此並沒有找到一個通用的解決方案。為了滿足不同網路條件和使用場景下的離線體驗,我們提出瞭如下幾種解決方案。

解決方案

我們提出了一系列的解決策略。首先,我們將重點放在構建離線模式的使用者體驗。從中,我們實現了從磁碟獲取資料進行內容分發的技術,使得資料就好像是從網路獲取的一樣。其次,利用這個快取架構,我們建立了一箇中心化的後臺資料預獲取框架,用預獲取的未展現資料來填充該快取。

離線體驗的原則

通過資料分析和使用者調研,我們提出了一些能代表主要痛點和地區的改進原則:

  1. 離線是一種狀態,而不是錯誤
  2. 離線體驗應該是無感知的
  3. 通過明確的溝通來建立信任

你可以看到上述原則是如何在下方視訊中體現的:

應用是否可用與網路無關

利用儲存的請求資料以及圖片、視訊的快取,當網路不可用時,我們依舊可以將內容呈現到使用者的螢幕上,相當於模仿了一次成功的網路請求。

我們有 3 個主要元件:裝置螢幕、組成 HttpRequests 的裝置網路層和負責向伺服器端傳送網路請求的裝置網路引擎。

實現了從磁碟中獲取資料的技術後,在高速增長的市場下,我們發現使用者使用 Instagram 的體驗得到了提升。我們認為與其讓使用者看到白屏和灰色方塊,不如讓使用者能看到之前的內容,這樣的體驗應該會更好,基於此才決定將網路請求資料快取到本地。但是,最理想的方案仍然是展現最新的內容。這就是後臺資料預獲取技術的由來。

架構

在 Instagram,有一句工程師口號是“從最簡單的做起”,所以第一步並不是做一個完美的後臺資料預獲取框架。而是,當 app 在後臺執行且僅在手機連著 wifi 時去做資料預獲取。BackgroundPrefetcher 程式迴圈遍歷任務列表並依次執行。

這樣的首個原型程式可以做到:

  1. 迴圈預取各種內容資料
  2. 從使用者體驗的角度去分析向使用者呈現最新的媒體內容快取的實際效果
  3. 以此作為基準去評測最終框架(的穩定性)
public void registerJob(Runnable job) {
  mBackgroundJobs.add(job);
}
@Override
public void onAppBackgrounded() {
  if (NetworkUtil.isConnectedWifi(AppContext.getContext())) {
    while (!mBackgroundJobs.isEmpty()){   
    mSerialExecutor.execute(mBackgroundJobs.poll());
  }
 }
}
複製程式碼

現實情況是,apps 是很複雜的,同時使用者也是多種多樣的!你必須很仔細地分析使用者的使用習慣,才能決定到底去預獲取什麼型別的媒體內容。比如,一些使用者會比其他人更加頻繁地使用某個功能。

我們的主頁包含繽紛多彩的內容,從熱門分享到個人分享應有盡有。我們也可以預獲取 feed 中的圖片和視訊、待處理的訊息、搜尋的內容以及最近的通知。就我們的情況而言,我們決定從簡單的開始,只去提前獲取你搜尋的內容、熱門分享和首頁 feed。

構建一個可以靈活地適應不同使用場景的中心化架構有利於保持高效且方便擴容。

除了做到在 app 後臺執行時,通過我們的框架去排程任務自動預獲取資料之外,我們還在 app 的頂部新增了額外的邏輯。將資料預獲取邏輯集中到一個點上,讓我們能夠對其設定規則並驗證其是否滿足某些條件,比如:

  • 控制網路連線型別 -> 不計費的
  • 任務停止 -> 如果條件變化或 app 在前臺執行,我們要能夠停止正在進行的後臺資料預獲取任務
  • 合併請求,在 2 次會話之間找到最佳時間僅做一次資料預獲取
  • 指標收集 -> 所有的任務完成需要花費多長時間?排程執行後臺資料預獲取任務的成功率是多少?

工作流

[譯] 通過後臺資料預獲取技術實現效能提升

讓我們來看看後臺資料預獲取策略在安卓系統上的工作流程:

  • 當 main activity 啟動時(即 app 在前臺執行),我們將 BackgroundWifiPrefetcherScheduler 例項化,同時啟用即將被執行的一類任務。
  • 這個物件將它自身註冊為一個 BackgroundDetectorListener。在上下文中,我們實現了這樣的程式碼結構,每當 app 進入後臺執行時都會傳送通知,這樣我們便可以在 app 程式被殺死前做一些事情(比如將分析資料傳送到伺服器)。
  • 當 BackgroundWifiPrefetcherScheduler 收到通知時,它會呼叫我們自己寫的 AndroidJobScheduler 來對後臺資料預獲取任務進行排程。JobInfo 引數會被傳入,該引數包含了這些資訊:需要啟動哪些服務以及啟動這個任務需要滿足哪些條件。

我們的主要條件是延遲和不計費的網路連線。針對 Android 系統,其他一些條件也要被考慮進來,比如省電模式是否開啟。我們已經測試了不同程度的延遲,並仍在努力提供個性化的服務體驗。現階段,我們在 2 次會話之間僅做一次後臺預獲取資料。當 app 進入後臺執行時,到底應該在什麼時刻執行這個任務,為了找到這個最佳時間,我們計算了使用者每次開啟會話的平均間隔時間(使用者訪問 Instagram 的頻率是多少?),並使用標準差去除異常值(比如,一個頻繁使用 Instagram 的使用者的睡眠時間不應該被計算在內)。我們的目標是恰好在平均時間之前開始資料預獲取。

  • 在這個時間點後,程式會檢查網路連線是否滿足條件(不計費/wifi)。如果滿足,BackgroundPrefetcherJobService 將會啟動。如果不滿足,BackgroundPrefetcherJobService 的啟動會被掛起直到條件滿足。(且當手機未處於省電模式下)
  • BackgroundService 將建立一個 serialExecutor 通過序列方式執行每一個後臺任務。當然,在收到了 http 請求響應後,我們會以非同步的方式去預獲取媒體資料。
  • 在所有任務完成後,我們會向作業系統傳送一個通知,告知其我們的程式可以被殺死了,這樣一來可以延長記憶體/電池的使用壽命。在 Android 系統中,殺死這些正在執行的服務是很重要的,使得那些不會再被使用的記憶體資源得以釋放。

所有這些工作都是使用者級別的。程式要能夠處理使用者登出或切換身份的情況。如果使用者登出了,我們會停止排程的任務以避免它們做一些沒必要的服務啟動工作。

IgJobScheduler

針對安卓,我們具體做了如下幾點:

  1. 尋找一種高效的後臺任務排程方法,讓我們能夠在會話中將資料持久化並指定網路連線需求。
  2. 在 Lollipop(安卓在 2014 年釋出的作業系統)之前,Android 的 API 還不支援 JobScheduler 介面,所以我們分析了有多少使用者的安卓手機系統是低於此版本的。這是一個我們無法繞過的問題......對於這些使用者,我們需要開發一個相容的版本。
  3. 尋找一個適用於低版本安卓系統的現有開源解決方案去排程任務。儘管我們找到了很多優秀的第三方庫,但是它們都不適用於我們的場景,因為它們會依賴 Google Play Service。根據現有情況,我們認為 APK 的大小是 Instagram 能維持排行榜第一的主要因素。
  4. 最後,我們為 Android JobScheduler APIs 創造了一套可定製的高效能相容解決方案。

評測

在 Instagram,我們是資料驅動的,對於我們研發的每個系統,都會嚴格地評估它的影響。這也就是為什麼我們在設計後臺資料預獲取框架的同時,也會思考應該收集什麼樣的指標才能得到正確的反饋。

事實是,中心化的架構也有利於收集更高層次的指標。能夠準確地評估利弊,知道有多少預獲取的資料位元組被浪費了或者能分析是什麼造成了全域性 CPU 的波動,這些都是很重要的。

[譯] 通過後臺資料預獲取技術實現效能提升

我們通過網路請求策略給每個網路請求打上標籤來標識其行為和型別,這十分有用。它已經被內建到 app 中了,但是我們利用它來切分我們的指標。我們將請求策略關聯到發出的 http 請求上,並標出它是否是預獲取資料的請求。另外,請求策略還會給每個網路請求打上型別標籤。請求的型別可以為圖片、視訊、API 和分析資料等等。這可以幫助我們:

  • 設定請求優先順序
  • 通過全域性 CPU 使用率曲線、資料使用情況和快取利用率等這些維度,更好地對系統做分析和權衡
/**
 * 網路請求策略中的 Behavior 列舉類描述了被標記的請求返回資料是否需要渲染到螢幕上(比如預獲取資料的請求就不需要立刻渲染)
 */
public enum Behavior {
 Undefined(-1),
 OffScreen(0),
 OnScreen(1),
 ;
 int weight;
 Behavior(int weight) {
  this.weight = weight;
 }
}
複製程式碼

上方展示了 Android 原始碼中 requestPolicy 類的一部分程式碼片段。我們將一個請求標註為“on screen”,就意味著使用者正在等待該請求的返回資料。有約大於 1% 的 offScreen 的請求返回的資料不會與使用者互動。

高效快取日誌

我們希望知道有多少預獲取的位元組被真正使用到了,所以我們對快取中的資料的使用情況做了調研。我們構建了整個快取日誌系統,它滿足以下幾點:

  • 系統可擴充套件。能支援通過 API 新增快取例項。
  • 系統是健壯的,可以支援容錯。能夠忍受快取失效(沒有日誌記錄)或者某些時刻資料不一致的情況。
  • 系統是可靠的。在會話之間可以持久化資料。
  • 寫日誌時,使用最小的磁碟空間和最低的延遲。快取的讀/寫經常發生,所以我們需要將其開銷限制到最小。快取讀/寫時的日誌記錄可能會帶來更多的崩潰和更高的延遲。

[譯] 通過後臺資料預獲取技術實現效能提升

我們也想知道,當新增一個後臺資料預獲取請求時,到底有多少資料被使用了。我們在手機上有一個分層的基礎網路引擎,正如之前所說的,每個網路請求都被附加了一個 requestPolicy。這讓我們能夠很輕鬆地追蹤 app 中的資料使用情況以及觀察下載圖片、視訊和 JSON 資料等到底消耗了多少流量。

同時,我們也想分析對比在 wifi 和蜂窩網下的資料使用分佈情況。這使得我們有可能針對不同網路連線嘗試不同的資料預獲取模式。

其他好處

後臺資料預獲取技術除了可以消除對網路可用性的依賴以及降低蜂窩網的流量使用,還有什麼其他的好處麼?如果我們減少了大量的請求,那麼我們便降低了整體的網路阻塞。通過合併未來的請求,我們可以節省開銷和延長電池壽命。

防止 CPU 曲線波動

在研發後臺資料預處理程式之前,我們就考慮到了它可能造成全域性 CPU 使用率升高這一潛在風險。

CPU 使用率怎麼會升高呢?請看下面這個例子。假設有一個獲取 Instagram 熱門 feed 的介面。每次使用者 A 開啟 Instagram 獲取第一頁的最新 feed 時,他的裝置便會請求一次該介面。這個介面會做一些 CPU 密集型操作,比如排序和根據使用者選擇的條件進行內容分類。如果在每次使用者開啟 Instagram 的時候去做後臺資料預獲取,便會增加 CPU 負載,沒錯吧?

為了最小化服務端的 CPU 使用率波動,在第一版的後臺資料預獲取系統中,內容推薦團隊的工程師 Fei Huang 為我們新開了一個介面地址。這個介面只獲取前20條沒有被顯示的新分享內容。

結論

這是我們構建系統時的工作流程。我們小組不會將 API 開放給其他工程師直到我們能確保框架的質量且使用者能從中受益。

隨著越來越多的人加入 Instagram,這項工作只會變得越來越重要。我們期望能不斷提升 Instagram 的效率和效能,從而使世界各地的人都能暢快無阻地使用 Instagram。

Lola Priego 是一位來自 Instagram 紐約地區客戶端效能優化團隊的工程師。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章