愛伊米

高併發儲存最佳化篇:諸多策略,快取為王

本文內容概述

快取是什麼

1。1。 儲存宕機的致命代價

1。2。 資料庫效能為什麼會下降

1。3。 快取的型別

一線研發最頭疼的快取問題

2。1。 快取穿透

2。2。 快取擊穿

2。3。 快取雪崩

2。4。 資料漂移

2。5。 快取踩踏

2。6。 快取汙染

2。7。 熱點key

頂級快取架構一覽

3。1。 微博快取架構演進

3。2。 知乎首頁已讀過濾快取設計

總結

Part1快取是什麼

1。1

儲存宕機的致命代價[1]

2015年5月28號,攜程網站和APP全面癱瘓持續12小時,資料庫被物理刪除的訊息在朋友圈風傳。

按上季度財報估算,此次宕機直接影響攜程營收大約1200w美元,攜程股價也大跌11%。這還只是發生在網際網路剛剛普及的2015年。如果發生在現在。。。據公司公告是由於員工操作失誤導致。

雖然這不在我們想討論的效能原因導致異常的範圍內,但不妨礙我們得出結論,資料庫宕機對一個系統的影響是災難性的。

1。2

結構化資料庫效能為什麼會下降

以Mysql為例,我們知道,為了調和CPU和磁碟的速度不匹配,MySQL 用buffer pool來載入磁碟資料到一段連續的記憶體中,供讀寫使用。一般情況下,如果緩衝池足夠大,能夠放下所有資料頁,那mysql操作基本不會產生讀IO,而寫IO是非同步的,不會影響讀寫操作。

Buffer pool 不夠大,資料頁不在裡面該怎麼辦?

去磁碟中讀取,將磁碟檔案中的資料頁載入到buffer pool中,那麼就需要等待物理IO的同步讀操作完成,如果此時IO沒有及時響應,則會被堵塞。因為讀寫操作需要資料頁在buffer中才能進行,所以必須等待作業系統完成IO,否則該執行緒無法繼續後續的步驟。

熱點資料,當新的會話執行緒也需要去訪問相同的資料頁怎麼辦?

會等待上面的執行緒將這個資料頁讀入到快取中buffer pool。如果第一個請求該資料頁的執行緒因為磁碟IO瓶頸,遲遲沒有將物理資料頁讀入buffer pool, 這個時間區間拖得越長,則造成等待該資料塊的使用者執行緒就越多。對高併發的系統來說,將造成大量的等待。

高併發,大量請求的訪問行為被阻塞,會造成什麼後果?

對於服務來說,大量超時會使伺服器處於不可用的狀態。該臺機器會觸發熔斷。熔斷觸發後,該機器的流量會打到其他機器,其他機器發生類似的情況的可能性會提高,極端情況會引起所有服務宕機,曲線掉底。

上面是由於磁碟IO導致服務異常的分析邏輯,也是我們生產中最常遇到的一種資料庫效能異常的場景。除此之外,還有鎖競爭快取命中率等異常場景也會導致服務異常。

如果單庫單表的極限存在,分庫分表等最佳化策略也只能緩解,不會根除

為了避免上述情況,快取的使用就非常有必要了。

1。3

快取的型別

快取的存在,是為了調和差異。

差異有多種,比如處理器和儲存之間的速度差異、使用者對產品的使用體驗和服務處理效率的差異等等。

1.3.1 客戶端快取

離使用者最近的web頁面快取&app快取。web頁面因為技術成熟所以問題不是太多,但app因為裝置的限制,在使用快取時要多加註意。

之前經歷的某個業務,因為客戶端快取出現問題,發生兩次請求訂單號串單,導致業務異常。串單吶,猜是因為快取發生了混亂,至今比較奇怪會發生這種情況,需要對客戶端相關加深認識了。

1.3.2 單機快取

CPU快取

[2]。為了調和CPU和記憶體之間巨大的速度差異,設定了L1/L2/L3三級快取,離CPU越近,速度越快。後面章節中介紹的知乎首頁已讀過濾的快取架構,其靈感就是來源於此。

高併發儲存最佳化篇:諸多策略,快取為王

L1快取行示例

Ehcache

[3]。是最流行了Java快取框架之一。因為其開源屬性,在spring/Hibernate等框架上被廣泛使用。支援磁碟持久化和堆外記憶體。快取功能齊全。

高併發儲存最佳化篇:諸多策略,快取為王

Ehcache架構圖

值得一說的是ehcache具備堆外快取的能力,因為堆外快取不受JVM限制,所以不會引發更多的GC停頓,對某些場景下的GC停頓調優有不小的意義。但是需要注意的是堆外記憶體需要用byte來操作,要實現序列化和反序列化,並且在速度上,也要比堆記憶體要慢不少,所以,如果不是GC停頓有較大問題,且對業務影響較大,沒必要非用不可。

Guava cache

。靈感來源於ConcurrentHashMap,但具有更豐富的元素失效策略,功能沒有ehcache齊全,如只支援jvm記憶體,但比較輕量簡潔。之前曾用guava cache來快取閘道器的一些配置資訊,定時過期自動載入的功能還比較方便。

1.3.3 資料庫快取

Query cache即將查詢的結果快取起來,開啟後生效。其可以降低查詢的執行時間,對需要消耗大量資源的查詢效果明顯。

高併發儲存最佳化篇:諸多策略,快取為王

Query cache 的合理性檢驗[4]

1.3.4 分散式快取

memcached。[5]memcached是一個高效的分散式記憶體cache,搭建與操作使用都比較簡單,整個快取都是基於記憶體的,因此響應時間很快,但是沒有持久化的能力。

高併發儲存最佳化篇:諸多策略,快取為王

memcached儲存核心

Redis。Redis以優秀的效能和豐富的資料結構,以及穩定性和資料一致性的支援,被業內越來越普遍的使用。

高併發儲存最佳化篇:諸多策略,快取為王

Redis核心物件示意

在使用redis的都有誰?

redis官網羅列的redis使用者

看到了那個熟悉的公司——微博。微博算是redis的重度使用者,相傳redis的新特性好多都是為了微博定製的。有關微博的儲存架構在後面章節另做詳述。

本文後續的大部分內容也會基於Redis來敘述。

1.3.5 網路快取

高併發儲存最佳化篇:諸多策略,快取為王

一個簡單請求中的各快取位置示意

CDN伺服器是建立在網路上的內容分發網路。佈置在各地的邊緣伺服器,使用者可以經過中央渠道的負載平衡、內容分發、排程等功用模組獲取附近所需的內容,減少網路擁塞,提高響應速度和命中率。

Nginx基於Proxy Store實現,使用Nginx的http_proxy模組可以實現類似於squid的快取功能。當啟用快取時,Nginx會將相應資料儲存在磁碟快取中,只要快取資料尚未過期,就會使用快取資料來響應客戶端的請求。

Part2一線研發最頭疼的快取問題

下面這些問題其實大家在很多地方都應該見過了,不過為了內容的完整,還是羅列說明一下。

2。1

快取穿透

查詢的是資料庫中不存在的資料,沒有命中快取而資料庫查詢為空,也不會更新快取。導致每次都查庫,如果不加處理,遇到惡意攻擊,會導致資料庫承受巨大壓力,直至崩潰。

解決方案

有兩種:一種是遇到查詢為空的,就快取一個空值到快取,不至於每次都進資料庫。二是布隆過濾器,提前判斷是否是資料庫中存在的資料,若不在則攔截。

高併發儲存最佳化篇:諸多策略,快取為王

布隆過濾器利用多個hash函式標識資料是否存在,該方法讓較小的空間容納較多的資料,且衝突可控。其工作原則是,過濾器判斷不存在的資料則一定不存在。

高併發儲存最佳化篇:諸多策略,快取為王

我是動圖,請等一秒——-布隆過濾器原理原理

如上圖,左側為新增元素時的hash槽變化,右邊為判斷某資料是否存在時校驗的hash槽,可以看到,添加了1、2 後hash槽位某些被佔用,判斷2 、3 是否存在時,校驗對應hash槽即可。

2。2

快取擊穿

從字面意思看,快取起初時起作用的。發生的場景是某些熱點key的快取失效導致大量熱點請求打到資料庫,導致資料庫壓力陡增,甚至宕機。

解決方案

有兩種:

一種是熱點key不過期。有的同學在這裡提出了邏輯過期的方案,即物理上不設定過期時間,將期望的過期時間存在value中,在查詢到value時,透過非同步執行緒進行快取重建。

第二種是從執行邏輯上進行限制,比如,起一個單一執行緒的執行緒池讓熱點key排隊訪問底層儲存,以損失系統吞吐量的代價來維護系統穩定。

2。3

快取雪崩

鑑於快取的作用,一般在資料存入時,會設定一個失效時間,如果插入操作是和使用者操作同步進行,則該問題出現的可能性不大,因為使用者的操作天然就是雜湊均勻的。

而另一些例如快取預熱的情況,依賴離線任務,定時批次的進行資料更新或儲存,過期時間問題則要特別關注。

因為離線任務會在短時間內將大批資料操作完成,如果過期時間設定的一樣,會在同一時間過期失效,後果則是上游請求會在同一時間將大量失效請求打到下游資料庫,從而造成底層儲存壓力。同樣的情況還發生在快取宕機的時候。

解決方案

一是考慮熱點資料不過期獲取用上一節提到的邏輯過期。

二是讓過期時間離散化,如,在固定的過期時間上額外增加一個隨機數,這樣會讓快取失效的時間分散在不同時間點,底層儲存不至於瞬間飆升。

三是用叢集主從的方式,保障快取服務的高可用。防止全面崩潰。當然也要有相應的熔斷和限流機制來應對可能的快取宕機。

2。4

資料漂移

資料漂移多發生在分散式快取使用一致性hash叢集模式下,當某一節點宕機,原本路由在此節點的資料,將被對映到下一個節點。

高併發儲存最佳化篇:諸多策略,快取為王

圖片來源:知乎使用者Java架構師

但是,當宕機的節點恢復之後,剛才原本從新hash到下一個節點的資料,就全部失效,因為hash路由已經恢復到了此節點上,所以,下一個節點的資料變成冗餘資料,且,請求當前節點發現資料不存在,則會增加底層儲存呼叫。

這個問題,是我們使用一致性hash來保證快取叢集機器宕機時不會造成快取大量失效方案帶來的一些附加問題。因此需要保證一致性hash儘量的均勻(一致性hash虛擬節點的運用),防止資料傾斜的節點的宕機和恢復對其他節點造成衝擊。

2。5

快取踩踏[6]

快取踩踏其實只是一種快取失效場景的提法,底層原因是快取為空或還未生效。關鍵是因為上游呼叫超時後喚起重試,引發惡性迴圈。

比如,當某一名人新發布了圖片,而他們粉絲都會收到通知,大量的粉絲爭先搶後的想去看釋出了什麼,但是,因為是新發布的圖片,服務端還沒有進行快取,就會發生大量請求被打到底層儲存,超過服務處理能力導致超時後,粉絲又會不停的重新整理,造成惡性迴圈。

解決方案

:鎖 和 Promise。

發生這種踩踏的底層原因是對快取這類公共資源拼搶,那麼,就把公共資源加鎖,消除併發拼搶。

但是,加鎖在解決公共資源拼搶的同時,引發了另一個問題,即沒有搶佔到鎖的執行緒會阻塞等待喚醒,當鎖被釋放時,所有執行緒被一同喚醒,大量執行緒的阻塞和喚醒是對伺服器資源極大的消耗和浪費,即驚群效應。

高併發儲存最佳化篇:諸多策略,快取為王

promise的工作原理

promise的原理其實是一種代理模式,實際的快取值被promise代替,所有的執行緒獲取promise 並等待promise返回給他們結果 , 而promise負責去底層儲存獲取資料,透過非同步通知方式,最終將結果返回給各工作執行緒。

這樣,就不會發生大量併發請求同時操作底層儲存的情況。

2。6

快取汙染

快取汙染的主要表現是,正常的快取資料總是被其他非主線操作影響,導致被替換失效,之前的一篇敘述訊息佇列的文章《BAT實際案例看訊息中介軟體的妙用》中對kafka的快取汙染及其解決方案做了詳述,有興趣的可以看下。

解決快取汙染的基本出發點,是要拆解不同消費速度的任務(實時消費/定時消費)、或不同的資料生產來源(主流程/follower),分而治之的思路避免相互間快取的影響。

2。7

熱點key熱點key的處理邏輯示意圖

熱點key的影響不再敘述,而解決熱點key的方法,主要在熱點key的發現和應對上:

可以透過監控nginx日誌對使用者請求進行時間窗計數、建立多級快取、伺服器本地利用LRU快取熱點key、根據業務預估熱點key提前預熱等等;

可以透過分散儲存來降低單個快取節點應對熱點的壓力。

Part3頂級快取架構一覽

3。1

微博快取架構演進

微博有100T+儲存,1000+臺物理機,10000+Redis例項,那他的快取方案是怎麼演變發展到可以抗N個明星同時離婚的呢?

快取的架構演進[7]

高併發儲存最佳化篇:諸多策略,快取為王

高併發儲存最佳化篇:諸多策略,快取為王

高併發儲存最佳化篇:諸多策略,快取為王

高併發儲存最佳化篇:諸多策略,快取為王

從上面的幾張快取演進的架構圖中可以看到,微博的快取架構其實大部分都是在應對熱點資料,比如,用HA層而不用一致性hash,是因為微博有典型的跟隨者踩踏效應,一致性hash在踩踏效應下某節點的宕機,會引發下游一系列節點的異常。在比如L1快取的引入,則是因為微博的流量在時間上存在一些衰減規律,越新的一段越熱,所以,用小的熱點分片來擋住發生的少但流量大的情況。

只是上面這些還不夠,一些系統化的問題不容忽視:

某組資源請求量過大導致需要過多的節點

Cache 的伸縮容和節點的替換動靜太大

過多資源帶來的運維問題

Cache的易用性問題

CacheService快取服務[8]

為了解決上述問題資源微博對快取進行了服務化,提供一個分散式的 CacheService 架構,簡化業務開發方的使用,實現系統的動態伸縮容、容災、多層 Cache 等相關功能。

高併發儲存最佳化篇:諸多策略,快取為王

可以看到,在cache池上層,被封裝了一層proxy邏輯,包括非同步事件處理器用來管理資料連線、接收資料請求,processer用來進行資料解析,Adapter用來適配底層儲存協議,Router用來路由請求到對應的資源分片,LRU_cache用來最佳化效能、緩解proxy效能損耗,Timer用來進行健康狀態探測。

某次機緣巧合和微博架構組的總監簡單聊了幾句瞭解到,現在的整個cacheService服務的易用性已經非常高,伺服器節點的彈性伸縮依賴檢測體系全部自動進行,極大的減少了運維和維護成本,可能微博同學們曾經哪些加班吃瓜的歡樂日子已經一去不復返了。

Redis在微博的極致運用[9][10]

從2010年引入redis,至今已有十多個年頭。有非常多的使用經驗和定製化需求,不然也不會被redis官網列在使用者名單前三的位置。

$ 單執行緒下bgsave重操作卡頓問題

bgsave因為是非常重的操作,發生時會出現明顯的卡頓,造成業務波動;在故障宕機後恢復時主從速度慢,經常出現頻寬洪峰

從主執行緒中獨立出來Bio thread,專門執行Bgsave等操作,避免干擾;

在Redis中內建Cronsave功能,控制備份時間;

放棄bgaofrewrite。

$ redis完全替代mysql實現儲存落地

在Redis替代MySQL儲存落地的過程中,微博對Redis也進行很多定製化改造:

修改了AOF機制,增加原本不存在的POS位;

修改了Replication機制,實現基於AOF+POS位置的資料同步

修改落地機制,改為RDB+AOF滾動機制,保障資料持久化儲存。

$ longset定製化資料結構

針對千億級別的關係類儲存,為了減少成本,放棄了原生的Hash結構(比較佔記憶體),記憶體降為原來的1/10。

高併發儲存最佳化篇:諸多策略,快取為王

$ 計數功能最佳化

為了方便計數,將redis的KV改成了定長的KV ,透過預先分配記憶體,知道了總數,會極大的降低計數的操作開銷。

10年的深度依賴,微博在redis的使用上積累了大量的經驗和技巧,值得我們學習參考。

3。2

知乎首頁已讀過濾快取設計[11]

知乎社群擁有2。2億使用者、38萬的話題量、2800萬問題、1。3億回答,而個性化的首頁,需要過濾已讀並長期儲存以展示豐富的內容,對系統的效能和穩定性有著極高的要求。

$ 早期方案

高併發儲存最佳化篇:諸多策略,快取為王

高併發儲存最佳化篇:諸多策略,快取為王

$ 最佳化方案

高併發儲存最佳化篇:諸多策略,快取為王

大家有沒有發現這個架構思路很熟,是的,就是CPU的多級快取架構。透過快取攔截、副本擴充套件、壓縮降壓的方式,其實基本都是對前面章節敘述的快取問題的整體應對,以達到低延遲且穩定的快取服務效果。

Part4總結

本篇文章,透過底層儲存的極限理論,論證了快取存在的必要性;對快取場景的一些典型問題做了分析了闡述,最後,用微博和知乎兩個頂級的快取架構例項,對上面的內容進行了呼應。原創不易,如有感覺有所幫助,歡迎讀者朋友的幫助轉發分享,畢竟,匯仁牌腎寶,大家好才是真的好~

參考資料

[1]

環球旅訊:https://www。traveldaily。cn/article/92559

[2]

cpu快取:https://manybutfinite。com/post/intel-cpu-caches/

[3]

ehcache官網:https://www。ehcache。org/

[4]

深入分散式快取:機械工業出版社

[5]

memcached官網:https://memcached。org/

[6]

Facebook 史上最嚴重的宕機事件分析:https://www。infoq。cn/article/Bb2YC0yHVSz4qVwdgZmO

[7]

百億級日訪問量的應用如何做快取架構設計:https://my。oschina。net/JKOPERA/blog/1921089

[8]

微博 CacheService 架構淺析:https://www。infoq。cn/article/weibo-cacheservice-architecture

[9]

萬億級日訪問量下Redis在微博的9年最佳化歷程:https://cloud。tencent。com/developer/news/462944

[10]

微博Redis定製化之路:https://developer。aliyun。com/article/62598

[11]

知乎首頁已讀資料萬億規模下的查詢系統架構設計:Qcon大會分享‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

「在看」嗎?