應用多級快取模式支撐海量讀服務

發表於2016-08-17

快取技術是一個老生常談的問題,但是它也是解決效能問題的利器,一把瑞士軍刀;而且在各種面試過程中或多或少會被問及一些快取相關的問題,如快取演算法、熱點資料與更新快取、更新快取與原子性、快取崩潰與快速恢復等各種與快取相關的問題。而這些問題中有些問題又是與場景相關,因此如何合理應用快取來解決問題也是一個選擇題。本文所有內容是跟讀服務快取相關,不會涉及寫服務資料的快取。本文也不考慮內容型應用前置的CDN架構。本文也不會涉及快取資料結構優化、快取空間利用率跟業務資料相關的細節問題,主要從架構和提升命中率等層面來探討快取方案。本文將基於多級快取模式來介紹下應用快取時需要注意的問題和一些解決方案,其中一些方案已經實現,而有一些也是想使用來解決痛點問題。

1、多級快取介紹

所謂多級快取,即在整個系統架構的不同系統層級進行資料快取,以提升訪問效率,這也是應用最廣的方案之一。我們應用的整體架構如下圖所示:

整體流程如上圖所示:

1、首先接入Nginx將請求負載均衡到應用Nginx,此處常用的負載均衡演算法是輪詢或者一致性雜湊,輪詢可以使伺服器的請求更加均衡,而一致性雜湊可以提升應用Nginx的快取命中率;後續負載均衡和快取演算法部分我們再細聊;

2、接著應用Nginx讀取本地快取(本地快取可以使用Lua Shared Dict、Nginx Proxy Cache(磁碟/記憶體)、Local Redis實現),如果本地快取命中則直接返回,使用應用Nginx本地快取可以提升整體的吞吐量,降低後端的壓力,尤其應對熱點問題非常有效;為什麼要使用應用Nginx本地快取我們將在熱點資料與快取失效部分細聊;

3、如果Nginx本地快取沒命中,則會讀取相應的分散式快取(如Redis快取,另外可以考慮使用主從架構來提升效能和吞吐量),如果分散式快取命中則直接返回相應資料(並回寫到Nginx本地快取);

4、如果分散式快取也沒有命中,則會回源到Tomcat叢集,在回源到Tomcat叢集時也可以使用輪詢和一致性雜湊作為負載均衡演算法;

5、在Tomcat應用中,首先讀取本地堆快取,如果有則直接返回(並會寫到主Redis叢集),為什麼要加一層本地堆快取將在快取崩潰與快速修復部分細聊;

6、作為可選部分,如果步驟4沒有命中可以再嘗試一次讀主Redis叢集操作,目的是防止當從有問題時的流量衝擊;

7、如果所有快取都沒有命中只能查詢DB或相關服務獲取相關資料並返回;

8、步驟7返回的資料非同步寫到主Redis叢集,此處可能多個Tomcat例項同時寫主Redis叢集,可能造成資料錯亂,如何解決該問題將在更新快取與原子性部分細聊。

整體分了三部分快取:應用Nginx本地快取、分散式快取、Tomcat堆快取,每一層快取都用來解決相關的問題,如應用Nginx本地快取用來解決熱點快取問題,分散式快取用來減少訪問回源率、Tomcat堆快取用於防止相關快取失效/崩潰之後的衝擊。

雖然就是加快取,但是怎麼加,怎麼用細想下來還是有很多問題需要權衡和考量的,接下來部分我們就詳細來討論一些快取相關的問題。

2、如何快取資料

2.1、過期與不過期

對於快取的資料我們可以考慮不過期快取和帶過期時間快取;什麼場景應該選擇哪種模式需要根據業務和資料量等因素來決定。

不過期快取場景一般思路如下圖所示:

如上圖所示,首先寫資料庫,如果成功則寫快取。這種機制存在一些問題:

1、事務在提交時失敗則寫快取是不會回滾的造成DB和快取資料不一致;

2、假設多個人併發寫快取可能出現髒資料的;

3、同步寫對效能有一定的影響,非同步寫存在丟資料的風險。

如果對快取資料一致性要求不是那麼高,資料量也不是很大,可以考慮定期全量同步快取。

為解決以上問題可以考慮使用訊息機制,如下圖所示:

1、把寫快取改成寫訊息,通過訊息通知資料變更;

2、同步快取系統會訂閱訊息,並根據訊息進行更新快取;

3、資料一致性可以採用:訊息體只包括ID、然後查庫獲取最新版本資料;通過時間戳和內容摘要機制(MD5)進行快取更新;

4、如上方法也不能保證訊息不丟失,可以採用:應用在本地記錄更新日誌,當訊息丟失了回放更新日誌;或者採用資料庫binlog,採用如canal訂閱binlog進行快取更新。

對於長尾訪問的資料、大多數資料訪問頻率都很高的場景、快取空間足夠都可以考慮不過期快取,比如使用者、分類、商品、價格、訂單等,當快取滿了可以考慮LRU機制驅逐老的快取資料。

過期快取機制,即採用懶載入,一般用於快取別的系統的資料(無法訂閱變更訊息、或者成本很高)、快取空間有限、低頻熱點快取等場景;常見步驟是:首先讀取快取如果不命中則查詢資料,然後非同步寫入快取並設定過期時間,下次讀取將命中快取。熱點資料經常使用過期快取,即在應用系統上快取比較短的時間。這種快取可能存在一段時間的資料不一致情況,需要根據場景來決定如何設定過期時間。如庫存資料可以在前端應用上快取幾秒鐘,短時間的不一致時可以忍受的。

2.2、維度化快取與增量快取

對於電商系統,一個商品可能拆成如:基礎屬性、圖片列表、上下架、規格引數、商品介紹等;如果商品變更了要把這些資料都更新一遍那麼整個更新成本很高:介面呼叫量和頻寬;因此最好將資料進行維度化並增量更新(只更新變的部分)。尤其如上下架這種只是一個狀態變更,但是每天頻繁呼叫的,維度化後能減少服務很大的壓力。

3、分散式快取與應用負載均衡

3.1、快取分散式

此處說的分散式快取一般採用分片實現,即將資料分散到多個例項或多臺伺服器。演算法一般採用取模和一致性雜湊。如之前說的做不過期快取機制可以考慮取模機制,擴容時一般是新建一個叢集;而對於可以丟失的快取資料可以考慮一致性雜湊,即使其中一個例項出問題只是丟一小部分,對於分片實現可以考慮客戶端實現,或者使用如Twemproxy中介軟體進行代理(分片對客戶端是透明的)。如果使用Redis可以考慮使用redis-cluster分散式叢集方案。

3.2、應用負載均衡

應用負載均衡一般採用輪詢和一致性雜湊,一致性雜湊可以根據應用請求的URL或者URL引數將相同的請求轉發到同一個節點;而輪詢即將請求均勻的轉發到每個伺服器;如下圖所示:

整體流程:

1、首先請求進入接入層Nginx;

2、根據負載均衡演算法將請求轉發給應用Nginx;

3、如果應用Nginx本地快取命中,則直接返回資料,否則讀取分散式快取或者回源到Tomcat。

輪詢的優點:到應用Nginx的請求更加均勻,使得每個伺服器的負載基本均衡;輪詢的缺點:隨著應用Nginx伺服器的增加,快取的命中率會下降,比如原來10臺伺服器命中率為90%,再加10臺伺服器將可能降低到45%;而這種方式不會因為熱點問題導致其中某一臺伺服器負載過重。

一致性雜湊的優點:相同請求都會轉發到同一臺伺服器,命中率不會因為增加伺服器而降低;一致性雜湊的缺點:因為相同的請求會轉發到同一臺伺服器,因此可能造成某臺伺服器負載過重,甚至因為請求太多導致服務出現問題。

解決辦法是根據實際情況動態選擇使用哪種演算法:

1、負載較低時使用一致性雜湊;

2、熱點請求降級一致性雜湊為輪詢;

3、將熱點資料推送到接入層Nginx,直接響應給使用者。

4、熱點資料與更新快取

熱點資料會造成伺服器壓力過大,導致伺服器效能、吞吐量、頻寬達到極限,出現響應慢或者拒絕服務的情況,這肯定是不允許的。可以從如下幾個方案去解決。

4.1、單機全量快取+主從

如上圖所示,所有快取都儲存在應用本機,回源之後會把資料更新到主Redis叢集,然後通過主從複製到其他從Redis叢集。快取的更新可以採用懶載入或者訂閱訊息進行同步。

4.2、分散式快取+應用本地熱點

對於分散式快取,我們需要在Nginx+Lua應用中進行應用快取來減少Redis叢集的訪問衝擊;即首先查詢應用本地快取,如果命中則直接快取,如果沒有命中則接著查詢Redis叢集、回源到Tomcat;然後將資料快取到應用本地。

此處到應用Nginx的負載機制採用:正常情況採用一致性雜湊,如果某個請求型別訪問量突破了一定的閥值,則自動降級為輪詢機制。另外對於一些秒殺活動之類的熱點我們是可以提前知道的,可以把相關資料預先推送到應用Nginx並將負載均衡機制降級為輪詢。

另外可以考慮建立實時熱點發現系統來發現熱點:

1、接入Nginx將請求轉發給應用Nginx;

2、應用Nginx首先讀取本地快取;如果命中直接返回,不命中會讀取分散式快取、回源到Tomcat進行處理;

3、應用Nginx會將請求上報給實時熱點發現系統,如使用UDP直接上報請求、或者將請求寫到本地kafka、或者使用flume訂閱本地nginx日誌;上報給實時熱點發現系統後,它將進行統計熱點(可以考慮storm實時計算);

4、根據設定的閥值將熱點資料推送到應用Nginx本地快取。

因為做了本地快取,因此對於資料一致性需要我們去考慮,即何時失效或更新快取:

1、如果可以訂閱資料變更訊息,那麼可以訂閱變更訊息進行快取更新;

2、如果無法訂閱訊息或者訂閱訊息成本比較高,並且對短暫的資料一致性要求不嚴格(比如在商品詳情頁看到的庫存,可以短暫的不一致,只要保證下單時一致即可),那麼可以設定合理的過期時間,過期後再查詢新的資料;

3、如果是秒殺之類的,可以訂閱活動開啟訊息,將相關資料提前推送到前端應用,並將負載均衡機制降級為輪詢;

4、建立實時熱點發現系統來對熱點進行統一推送和更新。

5、更新快取與原子性

正如之前說的如果多個應用同時操作一份資料很可能造成快取資料是髒資料,解決辦法:

1.1、更新資料時使用更新時間戳或者版本對比,如果使用Redis可以利用其單執行緒機制進行原子化更新;

1.2、使用如canal訂閱資料庫binlog;

2.1、將更新請求按照相應的規則分散到多個佇列,然後每個佇列的進行單執行緒更新,更新時拉取最新的資料儲存;

2.2、分散式鎖,更新之前獲取相關的鎖。

6、快取崩潰與快速修復

6.1、取模

對於取模機制如果其中一個例項壞了,如果摘除此例項將導致大量快取不命中,瞬間大流量可能導致後端DB/服務出現問題。對於這種情況可以採用主從機制來避免例項壞了的問題,即其中一個例項壞了可以那從/主頂上來。但是取模機制下如果增加一個節點將導致大量快取不命中,一般是建立另一個叢集,然後把資料遷移到新叢集,然後把流量遷移過去。

6.2、一致性雜湊

對於一致性雜湊機制如果其中一個例項壞了,如果摘除此例項將隻影響一致性雜湊環上的部分快取不命中,不會導致瞬間大量回源到後端DB/服務,但是也會產生一些影響。

另外也可能因為一些誤操作導致整個快取叢集出現了問題,如何快速恢復呢?

6.3、快速恢復

如果出現之前說到的一些問題,可以考慮如下方案:

1、主從機制,做好冗餘,即其中一部分不可用,將對等的部分補上去;

2、如果因為快取導致應用可用性已經下降可以考慮:1、部分使用者降級,然後慢慢減少降級量;2、後臺通過Worker預熱快取資料。

也就是如果整個快取叢集壞了,而且沒有備份,那麼只能去慢慢將快取重建;為了讓部分使用者還是可用的,可以根據系統承受能力,通過降級方案讓一部分使用者先用起來,將這些使用者相關的快取重建;另外通過後臺Worker進行快取資料的預熱。

相關文章