架构专家高磊:缓存为王——无线缓存架构优化
高磊?
微軟架構專家
讀完需要
12
分鐘速讀僅需 4 分鐘
高磊,一線架構師,具有多年架構和研發(fā)經(jīng)驗;曾就職于阿里、華為等公司,專注于云計算、微服務體系等領域。
1
? ?
無線緩存的定義、限制條件及影響
本章僅討論運行在移動設備上的 App 所涉及的緩存。移動端緩存的作用是使 App 盡量少地通過網(wǎng)絡通信訪問應用服務器,以降低網(wǎng)絡及服務器的負載,同時提高移動端的性能。
但是,由于移動端應用的宿主是移動設備,移動設備是屬于內存受限型的,無法水平擴展,所以它不可能像服務器那樣創(chuàng)造出分布式緩存一類的緩存類型,所以它無法緩存那么多內容,即使有緩存的內容,也不可能被存放太久(需要讓出空間給其他需要緩存的內容)。移動端應用的緩存被禁錮在本機上,好處是其實現(xiàn)的復雜度大大小于在服務器上進行緩存。此外,移動設備處于整個系統(tǒng)的前端,其緩存體現(xiàn)出了前端緩存應該有的特點,它不僅僅緩存服務器返回的數(shù)據(jù),還要緩存提交的請求及響應內容,或緩存類似圖片這樣的資源。
2
? ?
無線緩存要從全局考慮
2.1
? ?
服務器架構對無線緩存的影響
無論移動端與服務端通信的協(xié)議如何,移動端的網(wǎng)絡層必須能夠屏蔽其協(xié)議上的差異并能夠將數(shù)據(jù)轉換為統(tǒng)一的格式,這樣做不僅簡化了移動端的實現(xiàn)復雜性,更重要的是隔離了對緩存的影響,使緩存不必關心協(xié)議及數(shù)據(jù)格式的差異,比如網(wǎng)絡層把數(shù)據(jù)全部轉換成 Java 對象,這樣緩存只需要保存 Java 對象即可,如圖 19.1 所示。
圖 19.1
但是我們要警惕服務器對移動端緩存的侵入。因為緩存組件緩存的是服務器上的內容, 一旦服務器發(fā)生變更就必須要有一種機制可以通知移動端對應的緩存項失效,否則會有數(shù)據(jù)不一致等情況發(fā)生。比如移動端上首頁展示的商品價格在服務端有變動,如果不使原價格失效,那么極有可能使用戶按原來的價格進行交易。另外,本地也有一些輔助的失效策略,比如超時、LRU 及主體相關性緩存失效策略等。
2.2
? ?
無線緩存對服務器架構的影響
服務器通知移動端緩存失效的辦法一般是通知系統(tǒng)分離失效事件和失效推送,這是因為在移動端數(shù)量巨大的情況下,失效通知的發(fā)布是一項體量巨大的工作,不可能由應用服務器自己承擔,所以應用服務器只是激發(fā)一個失效事件,將事件傳遞給一個有能力推動給移動端的大規(guī)模分布式系統(tǒng),并由它負責推送給各個移動端,
圖 19.2
如圖 19.2 所示。在服務器集群中一般會部署無線網(wǎng)關服務,移動端的所有業(yè)務都將與其通信,要使移動端能夠找到它,就需要網(wǎng)關的定位提供一種機制,在網(wǎng)絡上提供一個給移動端訪問的網(wǎng)關注冊管理中心,為移動端提供透明化的位置服務和名字服務,移動端只需要知道服務名稱而不需要知道具體的 IP 地址,當服務器上的服務遷移或者變更時也不會影響移動端對服務的可訪問性。服務器要確保訪問它的是合法的移動端(是自己組織開發(fā)的 App 或者經(jīng)過自己組織認證可以接入的 App),所以往往也需要提供認證機制。另外,還需要提供一整套安全防御機制
2.3
? ?
大流量下無線緩存作用的劣化
上面提到了無線緩存的作用,一方面是提高移動端的性能,另一方面是降低服務器負載。但是第二個作用會隨著移動端數(shù)量的增加而逐漸變得沒那么明顯,這是由移動端資源限制導致的。因為內存小,能夠承載的緩存內容較少,所以有不少對資源的請求會落在服務器上。隨著用戶規(guī)模的不斷增大,這種對服務器的請求負載會越來越高,所以無線緩存最主要的作用是優(yōu)化移動端的性能,而降低服務器負載的作用是處于次要地位的。有了這樣的認識,架構師在設計系統(tǒng)時就必須在服務器上下功夫,使這種逐漸增加的負載不會對服務器的穩(wěn)定性產(chǎn)生很大的影響。
2.4
? ?
無線緩存與本機移動端組件的關系
無線緩存在移動端內作為一種資源層而存在,移動端本身可能會以多種方式訪問它, 其中會時常使用多線程,所以緩存本身的實現(xiàn)必須是線程安全的。
2.5
? ?
無線緩存存儲介質的選擇
無線緩存存儲介質一般會采用內存,但是移動設備的操作系統(tǒng)對移動端進程所能使用的內存有所限制,比如 Android 中對每個進程的限制為 16MB(但是這不是必然的數(shù)值,在各個廠商從 Google 原版 Android 代碼移植到自己的硬件上時,可能會對此限制做出改動),所以為了增加容量,也存在這樣的架構設計——使用 Sdcard 作為二級緩存,理由是 Sdcard 的讀寫速度和內存很接近。這是一個好的決策,可以使由于移動端規(guī)模增大而造成的對服務器負載的影響進一步減小。
3
? ?
數(shù)據(jù)、資源緩存及失效策略
3.1
? ?
架構詳論
架構實例
無線緩存是本地集合對象所作的內存緩存,為了簡化對它的管理,可以將其與網(wǎng)絡框架融合起來,這樣可以起到一種緩存透明化的作用,使得不需要過多考慮移動端的緩存如何實現(xiàn)和管理,更能集中精力把功能實現(xiàn)好。
以圖 19.3 所示的 Volley 架構為例,其實這里并不想給予一個十分有傾向性的架構定義, 因為業(yè)務場景千差萬別,只想通過一個具體存在的實例給予一定的設計思路,使你在決定采用開源產(chǎn)品時有所依據(jù),或者在決定自己“創(chuàng)造一個新的輪子”時有相應的參考。我們采用開源的 Volley 來說明思路,希望起到拋磚引玉的作用。
圖 19.3
Volley 主要通過兩種 Diapatch Thread 不斷從 RequestQueue 中取出請求,而請求本身被抽象成幾種數(shù)據(jù)獲取接口。以便從服務器中獲取不同的數(shù)據(jù)或者資源,根據(jù)是否已緩存調用 Cache 或 Network 這兩類數(shù)據(jù)獲取接口之一,從緩存(內存緩存或者 Sdcard 緩存)或是服務器中取得請求的數(shù)據(jù),然后交由 ResponseDelivery 去做結果分發(fā)及回調處理。它的構成包含:
● Volley:Volley 對外暴露的 API 供調用方使用,通過 newRequestQueue(…)函數(shù)新建并啟動一個請求隊列 RequestQueue。
● Request:表示一個請求的抽象類,StringRequest、JsonRequest、ImageRequest 都是它的子類,表示某種類型的請求,指示從服務器獲取不同的資源。
● RequestQueue:表示請求隊列,里面包含一個 CacheDispatcher(用于處理緩存請求的調度線程)、NetworkDispatcher 數(shù)組(用于處理走網(wǎng)絡請求的調度線程)、一個 ResponseDelivery (返回結果分發(fā)接口 ),通 過 start() 函數(shù)啟動時會啟動 CacheDispatcher 和 NetworkDispatchers。
● CacheDispatcher:一個線程,用于調度處理緩存中的請求。啟動后會不斷地從緩存請求隊列中取出請求處理,隊列為空則等待, 請求處理結束則將結果傳遞給 ResponseDelivery 去執(zhí)行后續(xù)處理。當結果未緩存過、緩存失效或緩存需要刷新時, 該請求都需要重新進入 NetworkDispatcher 去調度處理,這種機制叫作失敗緩存重發(fā)。
● NetworkDispatcher:一個線程,用于調度處理走網(wǎng)絡的請求。啟動后會不斷從網(wǎng)絡請求隊列中取出請求處理,隊列為空則等待, 請求處理結束則將結果傳遞給 ResponseDelivery 去執(zhí)行后續(xù)處理,并判斷結果是否要進行緩存。
● ResponseDelivery:返回結果分發(fā)接口,目前只有基于 ExecutorDelivery 的在入?yún)?handler 對應線程內進行分發(fā)。
● HttpStack:處理 HTTP 請求,返回請求結果。目前 Volley 中有基于 HttpURLConnection 的 HurlStack 和基于 Apache HttpClient 的 HttpClientStack。
● Network:調用 HttpStack 處理請求,并將結果轉換為可被 ResponseDelivery 處理的 NetworkResponse。
● Cache:緩存請求結果。Volley 默認使用的是基于內存的緩存,也可以指定基于 Sdcard 的 DiskBasedCache。NetworkDispatcher 得到請求結果后判斷是否需要存儲在 Cache 中,CacheDispatcher 會從 Cache 中取出緩存結果。
本地緩存
圖 19.4
如圖 19.4 所示,App 進程是緩存組件的宿主和 App 共享內存,而進程所能夠使用的內存多是被限制的,所以緩存組件必然侵吞 App 使用的內存,目前有很多 App 還在采用這種方式,不過也有使用 Sdcard 方案緩解以上問題的。
服務式緩存
圖 19.5
為了克服本地緩存的缺點,可以把緩存作為服務獨立地運行在另外一個進程中,如圖 19.5 所示,比如 Android 的 Service 就可以作為緩存的宿主,App 通過 IPC 與之通信,緩存可用的內存會大大增加,而且使用 Sdcard 可以進一步增加可用的存儲空間。
3.2
? ?
實現(xiàn)失效策略
服務器主動失效策略
訂單生成后需要同步移動端緩存中的用戶 profile 數(shù)據(jù),后臺管理端(比如運營平臺) 修改了一個商品的價格后也需要同步移動端緩存中的商品數(shù)據(jù)以便用戶可以觀察到最新的價格,此類種種操作都與功能、業(yè)務觸達有關,很難將其抽取到平臺上,但是服務器可以提供推送同步數(shù)據(jù)請求的機制而非具體的策略。
由移動端根據(jù)業(yè)務需要定義緩存的 Key,將其注冊到服務器上并與關心的數(shù)據(jù)源綁定, 一旦數(shù)據(jù)源發(fā)生變化就發(fā)出數(shù)據(jù) Key 失效事件,凡是訂閱了此事件的推送服務集群都會將此變更數(shù)據(jù)(連同 Key 和最新的數(shù)據(jù))再推送給所有訂閱了此 Key 的移動端事件接收者, 令其緩存項失效并同步最新的數(shù)據(jù)。這樣做的好處是,沒有必要只讓移動端接收事件而后再到服務端去取最新的數(shù)據(jù),從而導致瞬間加大應用服務器的負載;另外應用服務器只是發(fā)出一個失效事件,由推送集群發(fā)送到移動端可以避免失效推送對應用服務器性能的影響。
這種機制也被封裝在移動端的網(wǎng)絡層,服務器也有相對應的實現(xiàn)機制,不過就是把定義和業(yè)務相關的 Key 等這樣的事情交給了移動端實現(xiàn),這樣就簡化了失效策略的實現(xiàn)方式, 也最大限度地防止了因實現(xiàn)方對緩存失效策略的理解不同而造成的錯誤使用。
本地失效策略
1. 超時
超時可以作為一種輔助手段,而不是主要手段,這是因為超時的大小會引起一些麻煩, 比如超時過長且服務器推送失效事件的機制沒有打開時,客戶端總是看到舊的數(shù)據(jù),這就會引起一些業(yè)務數(shù)據(jù)處理不一致的情況;而如果設定超時過短,服務器的負載就會過高;再者,如果我們設定的時間整齊劃一,也就是會同時失效,那么就會出現(xiàn)雪崩效應。所以, 在使用超時機制時需要謹慎一些。為防止以上問題,可采取如下措施。
設定合理的超時時間值。
時間設定之間要加入一定的散列值,防止整齊劃一的設定。
服務器的失效推送機制需要設定為啟動。
2. LRU
LRU(Least Recently Used,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù),其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的概率更高”,如圖 19.6 所示。最常見的實現(xiàn)是使用一個鏈表保存緩存數(shù)據(jù),詳細算法實現(xiàn)如下:
將新數(shù)據(jù)插入鏈表頭部。
每當出現(xiàn)緩存命中(即緩存數(shù)據(jù)被訪問)時,便將數(shù)據(jù)移到鏈表頭部。
當鏈表已滿時,將鏈表尾部的數(shù)據(jù)丟棄。這樣的實現(xiàn)固然簡單,但也有固有的缺點。當存在熱點數(shù)據(jù)時,LRU 的效率很好,但偶發(fā)性的、周期性的批量操作會導致 LRU 命中率急劇下降,緩存污染情況比較嚴重。命中時需要遍歷鏈表,找到命中的數(shù)據(jù)塊索引,然后將數(shù)據(jù)移到頭部,因此性能比較差。
圖 19.6
另一種比較科學的實現(xiàn)是 LRU-K。LRU-K 中的 K 代表最近使用的次數(shù),因此 LRU 可以認為是 LRU-1。LRU-K 的主要目的是解決 LRU 算法“緩存污染”的問題,其核心思想是將“最近使用過 1 次”的判斷標準擴展為“最近使用過 K 次”,如圖 19.7 所示。和 LRU 相比,LRU-K 需要多維護一個隊列,用于記錄所有緩存數(shù)據(jù)被訪問的歷史。只有當數(shù)據(jù)的訪問次數(shù)達到 K 次時,才將數(shù)據(jù)放入緩存。當需要淘汰數(shù)據(jù)時,LRU-K 會淘汰第 K 次訪問時當前時間間距最大的數(shù)據(jù)。詳細實現(xiàn)如下:
數(shù)據(jù)第一次被訪問,加入訪問歷史列表。
如果數(shù)據(jù)在訪問歷史列表中沒有達到 K 次訪問,則按照一定規(guī)則(FIFO①,LRU)淘汰。
當訪問歷史隊列中的數(shù)據(jù)訪問次數(shù)達到 K 次后,將數(shù)據(jù)索引從歷史隊列中刪除, 并將數(shù)據(jù)移到緩存隊列中,緩存此數(shù)據(jù),緩存隊列重新按照時間排序。
緩存數(shù)據(jù)隊列中被再次訪問后,重新排序。
需要淘汰數(shù)據(jù)時,淘汰緩存隊列中排在末尾的數(shù)據(jù),即淘汰“倒數(shù)第 K 次訪問離現(xiàn)在最久”的數(shù)據(jù)。
LRU-K 具有 LRU 的優(yōu)點,同時能夠避免 LRU 的缺點。在實際的應用中,LRU-2 是綜合各種因素后最優(yōu)的選擇,LRU-3 或者擁有更大 K 值的 LRU 策略的命中率會更高,但適應性差,需要大量的數(shù)據(jù)訪問才能將歷史訪問記錄清除掉。LRU-K 雖然降低了“緩存污染” 帶來的問題而且命中率比 LRU 要高,但是也有一定的代價。由于 LRU-K 需要記錄那些被訪問過但還沒有被放入緩存的對象,因此內存消耗會比 LRU 多,當數(shù)據(jù)量很大的時候,內存消耗會比較可觀。LRU-K 需要基于時間進行排序(可以在需要淘汰時再排序,也可以即時排序),CPU 消耗比 LRU 要高。一些其他的算法實現(xiàn),比如 Two queues(Q2)、Multi Queue(MQ)等,此處不做詳解,留給讀者自己分析。
圖 19.7
3. 主體相關性的失效
移動端經(jīng)常采用 MVC、MVVM、MVP 等架構模式,其中網(wǎng)絡層經(jīng)常被定位為 Model 層的一部分,當諸如 Android 的 Activity(可以看作控制器)所控制的 View 關閉后,也許會過一段時間才會被再次激活。所以,該策略的思想把一個 Activity 中所緩存的東西和宿主 Activity 的生命周期綁定在一起,當 Activity 關閉時也就會把相關的緩存項清空,以便達到節(jié)約內存使用的目的。
4
? ?
總結
天下沒有完美的架構,能夠支持演進的需要、滿足目前需求的架構就是好架構。恰到好處是我們追求的目標,靈活使用無線緩存并深知它的限制和優(yōu)勢對移動端的設計是非常有好處的。另外,這也使架構師能夠將移動端與服務端作為一個整體去考慮問題,而不再從單純的角度(設計服務器就是單獨設計服務器,設計移動端就是單獨設計移動端)去考慮,使得架構的演進方向更加科學和健康。
本文節(jié)選自《架構寶典》
總結
以上是生活随笔為你收集整理的架构专家高磊:缓存为王——无线缓存架构优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中台实践:新汽车行业的业务、技术和平台转
- 下一篇: 一些Chrome 调试小技巧汇总