雲音樂 iOS 跨端緩存庫 - NEMichelinCache

語言: CN / TW / HK

本文作者:繹推

背景

在雲音樂全面轉跨端的時代,H5 / RN 緩存模塊是非常重要的組成部分,對頁面的穩定性,頁面性能等都有非常大影響,目前雲音樂使用的緩存庫已經“歷史悠久”,沒法在現有的基礎上來支撐日益龐大的跨端需求,面臨着當前架構沒法修復的問題:

  1. 後台 wake up問題與後台頻繁 I / O 操作導致的崩潰 - 據統計,最高50%以上的後台崩潰是老緩存庫導致
  2. 主線程偶現卡死問題 - 線程管理問題
  3. RN / H5 頁面偶現空白問題 - 數據不一致導致
  4. Fatal Exception,Bundler Error等降級錯誤率高
  5. RN unregister module 錯誤高
  6. 大量細散重複日誌,浪費網絡資源
  7. 沒有整體日誌監控,難以定位問題

因此我們基於緩存庫的可擴展架構,從問題出發,重新設計了一套新的跨端緩存庫 - NEMichelinCache,全文以 RN 緩存的角度來描述

緩存

首先我們要知道緩存的目的是什麼?目的是以空間換時間。説起緩存,很多人會想到操作系統的緩存設計以及緩存中的直寫與回寫模式(Write Through and Write Back)。

直寫模式

zhixie

CPU 將數據同時更新到 Cache 和 Memory 中

優點

  • 有助於數據恢復(在停電或系統故障的情況下)
  • Cache 和 Memory 數據始終保持一致
  • 直接 I / O 訪問,可以獲取到最新數據

缺點

  • 寫操作多

回寫模式

huixie

CPU 將數據更新到 Cache 時,對 Cache 做一個標記,但不同步更新到 Memory 中(異步更新)

優點

  • 速度快
  • 寫操作少

缺點

  • 容易造成 Cache 和 Memory 數據不一致
  • 直接 I / O 訪問,不能獲取到最新數據

思考

對於一個跨端緩存庫方案,主要考慮以下幾個方面:

  • 如何解決目前面臨的問題:收集緩存庫相關問題,從問題出發設計解決方案
  • 如何提高緩存的穩定性:需要綜合緩存的優缺點,在數據一致性,讀寫速度等方面考慮方案
  • 錯誤快速定位能力:針對各個階段的錯誤,設計錯誤上報模塊,需要做到不多報、不誤報、不漏報
  • 完善的日誌模塊:以本地回撈日誌(儲存於客户端,需要時通過指令上報的debug日誌)為主,減少服務端壓力,儘量保證日誌的信息量足
  • 緩存庫新老切換成本:AB 切換成本,新老緩存遷移成本,各指標定義等
  • 業務拓展性:針對數據源,緩存類型等,給業務提供拓展點
  • 業務接入成本:內置通用方案,降低接入成本

通過各方調研,跨端緩存方案有些類似回寫模式,但是需要着重關注回寫的缺點。

問題解決方案

從緩存的回寫模式缺點出發

  1. 保證數據一致性:保證內存緩存、引擎、磁盤緩存數據一致性
  2. 不提供任何 I / O 直接訪問緩存的方法給業務方

因緩存庫導致的後台崩潰 / 主線程卡死問題

  • 線程模塊設計 - 設計線程池,保證 I / O 操作/耗時操作都在次線程完成
  • 下載更新模塊 - 以保證數據一致性為核心,責任鏈模塊設計,各節點功能原子化,保證耗時操作在次線程完成
  • 數據庫模塊設計 - 統一管理,FMDB Queue

降級錯誤 / 加載失敗 / 頁面空白 / 卡片模塊消失空白 / unregister module等引擎錯誤

  • 同步數據庫時機 - 完全成功後同步,保證磁盤緩存必是可用的
  • 數據庫模塊 - 支持事務,可 Fallback,保證出錯時可回退
  • 緩存多版本並存 - 保證本地 Bundle 緩存互不干擾
  • 引用計數模塊 - 用於清空緩存,保證使用中的緩存不被提前清空
  • 接口修改
    • 刪除對外提供清空緩存的接口 - 避免業務方隨意刪除緩存
    • 刪除對外提供直接讀取本地磁盤的接口 - 避免業務方隨意讀取緩存
  • 責任鏈 Runner - 優先級隊列,優先保證正在加載的頁面加載速度
  • 數據庫/文件遷移 - 保證新版本兼容老版本數據,避免重複下載
  • 接口 CDN 遷移
  • 網絡模塊強行使用 https ,防攔截

有效快速定位問題

  • 日誌模塊設計
  • 整體監控錯誤日誌 - 自定義 Domain ,方便區分各個階段,方便歸因
  • 刪除宂餘日誌
  • 結合加載流程做到異常信息細化,形成閉環

方案設計

fangan

業務接口層

對業務方而言,主要是面向業務接口層開發,設計的初衷為了減少接入的難度,使接口可控,不讓業務方隨意訪問磁盤等,如何設計這一層非常關鍵,對業務方來説,他們只要知道他們需要做什麼,以及能夠得到什麼,我們的想法是這一層應該具備以下幾點:

  • 初始化參數 CacheConfig :緩存名,緩存根目錄,其他自定義參數

``` @interface NEMichelinCacheConfig : NSObject

  • (instancetype)initWithAppName:(NSString )appName cacheRootPath:(NSString )cacheRootPath xxx @end ```

  • DataProvider協議 - 業務方只需要實現一個接口即可正常使用緩存功能

- (void)fetchBundleCacheResWithLocalApps:(NSArray<NEMCAppInfo *> *)apps completionHandler:(void (^)(NSArray<NEMichelinResVersionInfo * > *infoList, NSError *error))completionHandler;

  • 緩存更新接口

- (void)updateResourceOfAppInfo:(id<NEMCAppInfo *>)appInfo priority:(NEMichelinSerialChainPriority)priority completeBlock:(void (^)(NSError *error, NSDictionary *result))completeBlock;

  • 自定義緩存 / 數據協議 - 只有在特殊自定義緩存時,需要特殊實現

除了業務需要關心的以上接口外,此層中處理了:新老緩存庫 AB 切換,內部協議定義,其他自定義接口預留等

責任鏈模塊

zerenlian

  • 拆分前置判斷下載MD5 校驗zip / gz 解壓合併tar 解壓更新緩存節點等,顆粒度細化
  • 自定義鏈路能力
    • 可刪除,增加節點
  • 支持暫停 pause,繼續 resume 能力
  • 全局 Context 傳遞
  • 失敗異常拋出能力 - 節點執行失敗後,中斷執行,用於收集異常
  • 生命週期監聽能力 - 支持各個節點開始與結束生命週期監聽
  • 節點職責單一(只負責自己模塊,誰創建,誰釋放(包括本地臨時文件))
  • 邏輯內聚,只依賴數據 :節點自行判斷 Context 數據,節點間不相互依賴。

責任鏈模塊的設計,為後續日誌模塊,錯誤模塊設計打下了良好基礎,可以方便在這個設計下收集各個模塊的日誌,以及刪除宂餘日誌,錯誤也可以及時拋出,不會出現重複拋出的情況,也為後面跨端APM數據收集打下了基礎,可以方便的在各個節點間插樁,減少了APM建設的工作量。最重要的收益是提升了穩定性,降低的出錯可能性,各個節點完全掌控自己的臨時變量,不會出現漏刪文件,變量等情況。

責任鏈 Runner

  • 優先級隊列能力
  • 支持一個key對應多個責任鏈
  • 責任鏈緩存能力

主要為了支持優先級隊列的能力,可以讓優先級高的鏈插隊,有效提升緩存速度。

解壓/合併模塊

jieya

  • 抽離 zip,tar,gz 解壓,壓縮,合併能力
  • 可自定義配置 zip,tar,gz 壓縮包解壓庫能力
  • 減少對三方庫的依賴,可任意替換三方庫

數據庫

shujuku

  • FMDB 替換 sqlite3
  • 使用事務
  • 數據遷移
  • 數據校驗
  • 出錯回滾

多版本並存

  • AppInfo:相當於緩存描述,裏面有 Bundle 文件路徑
  • Bundle文件:RN 讀取的 JS Bundle文件

為什麼要做多版本並存?

duobanben 根據上圖可以看出,AppInfo 讀取時機跟引擎加載本地 Bundle 文件的時機是不一致的,所以有可能讀取的 AppInfo 中的本地緩存路徑已經被更改,從而導致不可預估的問題。

多版本

duobanb 為了保證數據的一致性,就出現了多版本共存的情況,簡單理解是同一個版本,在使用期間,數據庫、內存、文件都不會被刪除,也不會被覆蓋。這樣操作不就會導致磁盤緩存無限放大麼?所以我們就想到了通過引用計數的方式刪除宂餘緩存。

引用計數 - 本地 Bundle 緩存清理時機

yinyongjishu

  • Bridge 創建時,Bridge 對應的本地緩存會被引用持有
  • 直到所有的 Bridge 被釋放時,就會做本地緩存清理操作
  • 本地緩存清理操作是悲觀操作,也會校驗是否是最新緩存,是否在使用

總結

數據統計

| | CCCandyWebCache(老緩存庫) | NEMichelinCache | 結論 | | ---- | ---- | ---- | ---- | | md5 校驗成功率 | 97% |100%|上升3% | | 降級錯誤 | W | W |下降94%| | xcode 獲取 wakeup 導致的 crash | 22年6月份:最高到近 70% 的量;去年一年:Top10中佔了3個 | 0| 下降 100% | | ANR | 抽樣卡死 104 次,影響 94 用户 | 暫未發現 | 下降 100% | | 卡頓 | 抽樣卡頓 46061 次,影響 2519 用户 |卡頓事件 3 個|下降 99% | | CPU 異常 | 抽樣數量 100+ |暫未找到|下降 100% | | OOM | 抽樣錯誤量 236 ,影響用户 67 | 暫未找到|下降 100% | | 引擎錯誤 | 9000+ | 481 |下降 94% | | 24 小時升級率 | Vip(96.43%),Square(75.25%) | Vip(98.21%), Square(96.78%) | 升級率上升 2% 到 20% 不等|

除了以上模塊,我們對錯誤通過 Domain 定義進行了詳細分類,日誌模塊以雲音樂自研的 Corona 平台,本地回撈等手段進行了詳細監控,網絡模塊以網絡庫作為基礎,支持了斷點續傳等能力。目前新庫已在雲音樂 RN 模塊全量使用,錯誤率下降非常明顯,後面將持續替換H5緩存,DSL 模版緩存等。

參考資料

本文發佈自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!