1. 程式人生 > >[原創]分散式系統之快取的微觀應用經驗談(四) 【互動場景篇】

[原創]分散式系統之快取的微觀應用經驗談(四) 【互動場景篇】

分散式系統之快取的微觀應用經驗談(四) 【互動場景篇】 

前言   近幾個月一直在忙些瑣事,幾乎年後都沒怎麼閒過。忙忙碌碌中就進入了2018年的秋天了,不得不感嘆時間總是如白駒過隙,也不知道收穫了什麼和失去了什麼。最近稍微休息,買了兩本與技術無關的書,其一是 Yann Martel 寫的《The High Mountains of Portugal》(葡萄牙的高山),發現閱讀此書是需要一些耐心的,對人生暗喻很深,也有足夠的留白,有興趣的朋友可以細品下。好了,下面迴歸正題,嘗試寫寫工作中快取技術相關的一些實戰經驗和思考。 正文   在分散式Web程式設計中,解決高併發以及內部解耦的關鍵技術離不開快取和佇列,而快取角色類似計算機硬體中CPU的各級快取。如今的業務規模稍大的網際網路專案,即使在最初beta版的開發上,都會進行預留設計。但是在諸多應用場景裡,也帶來了某些高成本的技術問題,需要細緻權衡。本系列主要圍繞分散式系統中服務端快取相關技術,也會結合朋友間的探討提及自己的思考細節。文中若有不妥之處,懇請指正。

  為了方便獨立成文,原諒在內容排版上的一點點個人強迫症。   第四篇打算作為系列最後一篇,這裡嘗試談談快取的一些併發互動場景,包括與資料庫(特指 RDBMS)互動,和一些獨立的高併發場景相關補充處理方案(若涉及具體應用同樣將主要以Redis舉例)。   另見:分散式系統之快取的微觀應用經驗談(三)(資料分片和叢集篇)     (https://yq.aliyun.com/u/autumnbing)     (https://www.cnblogs.com/bsfz/)   一、簡單談下快取和資料庫的互動流程     為了便於後面的相關討論,這裡約定文中的資料庫(Database)均指傳統的 RDBMS,使用DB標識,同時需區別於快取(Cache)裡的DB劃分空間。
    我在早前一篇快取設計細節的文章裡,有闡述關於 Cache 自身 CURD 時的一些具體細節,而這裡將結合DB,就 DB 和 Cache 之間的並行 CURD 操作進行一些討論。當然,這裡面在互動層面上是一定會涉及到分散式事務(Distributed Transaction)相關的一致性話題,但為了避免表述出現模糊和不必要的邊界放大,這裡我儘可能剝離開來,專注在基於 Cache 的處理上。     預先抽象這樣一個基礎場景:DB中存在一張資金關聯表(FT),這裡 FT 裡儲存的都是熱點條目(屬於極高頻訪問資料),在系統設計時,FT裡的資料將與對應的 Cache 服務 C1 進行關聯儲存(這裡僅指一級快取),以達到提升一定的併發查詢效能。

    1.1 向 FT 中新增(Create)一條資料       通過 SQL 向 FT中插入一條資料:如果插入失敗,則不需要對 C1有任何操作;如果插入成功,則此時需要判斷,考慮是否在 C1中同步插入。       這種情景一般比較簡單,如果沒有特別的情況,此刻不需對 C1 做主動插入,而是後續被動插入(後面會提到)。但是如果插入 FT 中的資料往後操作只有刪除這個動作,並且 FT的資料經常被批量操作,那麼個人建議同步執行對 C1的插入操作。       (PS:這裡也順便申明下,如果需要往C1插入,但插入失敗,請根據業務場景加入重試機制,後面對Cache的操作均包含這個潛在的動作。至於重試處理失敗的情況,如往C1插入一條資料,個人建議是不再過度處理,最終預設是整體操作成功,並進行對應狀態返回。這裡注意不要與分散式事務的一致性進行混合類比,後面不再贅述。)

    1.2 準備更新(Update)一條資料       當需要更新 FT 中的一條資料時,意味著之前 C1 中的資料已經無效,而在一個高併發環境中這裡無法做到統一的直接更新 C1。首先就需要考慮的是 C1 的資料是主動更新還是被動更新,主動更新即更新完 FT後,同時將資料覆蓋進 C1,而被動更新指的是更新完 FT 後,立即淘汰 C1 中的資料,並等待下次查詢時重新寫入C1。       只要上述請求動作出現了任何併發,比如兩個相同動作,動作1和動作2同時發生請求,那麼會出現一個不一致的問題:動作1先操作 FT,動作2後操作 FT,然後動作2先操作了C1,動作1後操作了C1。       這樣存在不止一個執行緒併發的更新 FT 資料時,無法確認更新 FT 的順序和最終更新 C1 的順序是否保持一致,結果是一定會出現大量 FT 和 C1 中資料出現幻讀,而這個在存在主從Cache的情況下這種概率會大大提升(可參見上一章主從複製的部分)。推薦的方式是,如果不考慮Cache 多次需要重寫的損耗,在沒有其他特殊要求下,可以直接淘汰 C1 中的資料,也額外照顧到了Cache在合適的時候完全命中(Hit)。       其實到這裡還沒結束,當決定是淘汰 C1 的資料,那麼就要選擇一個淘汰時機:一種是先更新 FT,然後對C1 執行淘汰;一種則是,先對 C1 執行淘汰,然後才更新FT。       雖然兩種方式都有合適的場景,但這裡需要權衡一種概率性問題:當對C1執行淘汰時,又併發了一個對C1的查詢操作,此時,C1會從DB拉取資料重新寫入,那麼C1中即為髒資料,當併發越大,存在資料一直“髒”下去的概率更大。所以,這裡更推薦的做法是選擇前者。       (注意,這裡還有一些去討論的細節並不打算在此話題延伸,比如關於 C1和FT之間的原子性問題,是否可以採用二階段/三階段提交等模擬事務方式和對業務造成的影響。)

    1.3 開始讀取(Read)一條資料       這裡就沒有太多特別,畢竟應用Cache 的目的就已經說明了讀取資料時,只需要遵循“先讀Cache再讀DB”。即先從C1裡拿取資料,如果C1裡不存在該資料,則從FT中搜索,搜尋完成如果依然不存在該資料,則直接返回Empty狀態。如果存在,則同時將該資料儲存進C1中,並返回對應狀態。       順帶提一下,可能有人會說,在某些場景下,即使 C1中有資料,也要先從 FT裡優先獲取。我贊同,沒錯,但注意這裡不要混淆討論的主題了,這本質是屬於基於一種業務結果的導向,就類似在傳統 RDBMS 讀寫分離情況下,在關鍵資料的驗證處,直接請求主庫獲取並操作。所以上面說的其實並沒有矛盾,我們討論時要明確清晰,不要混淆。

    1.4 從FT 中刪除(Delete)一條資料       與Create相反的操作,通過 SQL 向 FT中移除一條資料:如果移除失敗,則不需要對 C1 有任何操作,如刪除成功,則將對應C1中資料移除(另外請類比1.2中的一些細節)。

  二、談談快取的穿透雪崩等相關問題

    在專案發展到後期,一些業務場景整體都處於高併發狀態,大量QPS對整體業務的負載要求很高,為了避免很多時候脫離架構優化的初衷,還需要在專案中做到很多預先性的規避和細節把控。

    2.1 優化防止快取擊穿       當請求發來的查詢 Key 在 Cache 中存在,但某一時刻資料過期了,並且此時出現了大量併發請求,那麼這裡因為 Cache 中 Miss,就會統一去 DB 中搜索,直接造成在很短的時間內,DB 的 QPS 壓力會陡增。       對於這種問題的預防和優化,往往從兩方面入手:一是程式中加小粒度的鎖/訊號(去年有寫過一篇關於商城系統裡庫存併發管控雜記,裡面有具體話題的細節擴充套件,詳見:https://www.cnblogs.com/bsfz/ );二是將 DB的讀取延遲 和 Cache的寫入時間儘可能拉到最低;三是對其中過於熱點的資料採取一個較大的過期時間並做一定的隨機性(這裡非必要,可自行權衡)。其實還有一點,少數情況下,可根據場景是否限制,可以增加適當的到期自動重新整理的策略,這裡也可以考慮在程式中開啟固定的執行緒通知維護。

    2.2 預防大量快取穿透       當請求發來的查詢 Key 在 Cache 中 Miss,自然就會去 DB 裡搜尋,這裡本身沒問題,但是假如查詢的 Key 在 DB 中也不存在,那麼意味著每次請求實際上都是實打實落在了 DB 上。這種問題比較常見,並且即使併發不是很大的時候 DB 的連線數也輕鬆達到上限,而且本身也不符合我們設計為了提高QPS的初衷。       對於這種漏洞性問題的解決方式,同樣可以從兩方面入手:一是程式可以在第一次從DB搜尋資料為 NULL 的時候,直接將 NULL 或者一個識別符號 Sign 快取起來,同時個人建議儘量設定一個小範圍的隨機過期時間,避免不必要的長期記憶體佔用;二是程式裡限制過濾一些不可能存在的資料KEY,如借鑑 Bloom filter 思想,特別是在前端請求到後端的這裡,儘量進行一次中間判斷處理(如有時對不合法KEY直接返回NULL)。

    2.3 控制快取雪崩       這裡會有某些細節和上面類似,但不完全。當Cache出現不可用,再或者大量資料同一場景裡同一時刻失效,批量請求直接訪問DB,並且此刻也等同於沒有任何Cache措施了。       為了規避這種偏極端的問題,主要可以考慮從三個方面入手:一是增加完善Cache 的高可用機制,並最好有單獨的運維監控預警;二是類似上面針對Cache的時間再次作隨機,特別是包含預熱和批量的場景裡。(ps:你看很多地方都有類似設計來降低一定概率,個人在設計時,即使是專案初期階段的簡化版本里也會包含進去。);三是,在部分場景增加多級Cache,但是在很多時候會增加其他的問題(如多級之前的同步問題),所以個人推薦優先增加到二級即可,然後稍微調整下時間儘量不高於一級Cache。

結語   由於個人能力和經驗均有限,自己也在持續學習和實踐,文中若有不妥之處,懇請指正。 本系列告一段落,正好也要去忙一些事情,暫時可能不寫相關的東西了。   個人目前備用地址:     社群1:https://yq.aliyun.com/u/autumnbing     社群2:https://www.cnblogs.com/bsfz/

End.