译 | 缓存穿透问题导致Facebook史上最严重事故之一
點(diǎn)擊關(guān)注公眾號,Java干貨及時送達(dá)
How a Cache Stampede Caused One of Facebook’s Biggest Outages
2010年9月23,這個世界上最大的社交平臺項目facebook,遭遇了最嚴(yán)重宕機(jī)故障之一,以至于facebook網(wǎng)站4個小時后才恢復(fù)運(yùn)行。而且這次事故非常極端,工程師不得不先讓facebook下線,才能恢復(fù)。雖然10年前的facebook遠(yuǎn)沒有現(xiàn)在這么大,不過仍然有超過10億用戶,人們?nèi)witter上抱怨或者取笑這次故障。
那么,導(dǎo)致是什么原因?qū)е逻@次facebook宕機(jī)呢?
Today we made a change to the persistent copy of a configuration value that was interpreted as invalid. This meant that every single client saw the invalid value and attempted to fix it. Because the fix involves making a query to a cluster of databases, that cluster was quickly overwhelmed by hundreds of thousands of queries a second.
一個錯誤的配置變更,導(dǎo)致大量的請求擊穿緩存,直達(dá)數(shù)據(jù)庫。我們把這種現(xiàn)象稱之為cache stampede,wiki地址:https://en.wikipedia.org/wiki/Cache_stampede。這在技術(shù)行業(yè)是一個非常普遍的問題,很多公司都出現(xiàn)過類似的事故,無數(shù)工程師為了不讓自己的項目遭遇這樣的問題做了大量的工作。
1、什么是緩存踩踏
cache stampede是指很多線程嘗試并行訪問緩存,如果緩存中不存在要訪問的數(shù)據(jù),那么這時候,線程一般會請求數(shù)據(jù)庫獲取它們需要的數(shù)據(jù)(所以cache stampede可以翻譯成緩存踩踏。和緩存穿透有點(diǎn)不一樣,Cache Stampede的重點(diǎn)是很多的線程穿透緩存)。
緩存踩踏破壞性這么大的主要原因是,它可能會導(dǎo)致故障雪崩,也就是說一個故障接著一個故障:
-
大量線程并發(fā)請求沒有從緩存中獲取到數(shù)據(jù),導(dǎo)致這些請求都會落到數(shù)據(jù)庫上。
-
數(shù)據(jù)庫由于恐怖的CPU毛刺而宕機(jī),從而導(dǎo)致大量的超時錯誤。
-
請求線程接收到超時后,又不斷重試請求,從而又導(dǎo)致新一輪的災(zāi)難。
-
反反復(fù)復(fù),無窮無盡。
需要說明的是,即使你沒有 Facebook 那樣的規(guī)模,也會遇到這個問題,因為它與規(guī)模無關(guān)。這個問題一直困擾著初創(chuàng)公司和科技巨頭。
2、如何阻止緩存踩踏
這是個很好的問題,在這篇文章中,我們將探索不同的策略來緩解甚至阻止緩存踩踏的出現(xiàn)。畢竟,你也不想等到你自己的服務(wù)出現(xiàn)問題后,才想到要學(xué)習(xí)如何預(yù)防。
2.1 增加更多的緩存
一個很簡單的方法就是增加更多的緩存,它的原理有點(diǎn)類似操作系統(tǒng)的多級緩存。操作系統(tǒng)使用了一個緩存層次結(jié)構(gòu)(L1、L2、L3),為了更快速的訪問。參考操作系統(tǒng),你也能在你的應(yīng)用中引入多級緩存。比如本地內(nèi)存緩存叫做L1緩存(例如Guava Cache,Caffeine),遠(yuǎn)程緩存叫做L2緩存(例如Redis,memcached):
這個策略對那些頻繁訪問的數(shù)據(jù)來說是非常有用的。即使L2緩存中的Key失效了,L1緩存中仍然有值,能夠擋住大量請求不會打到數(shù)據(jù)庫上。
然后,這種方法需要做一些取舍,在應(yīng)用服務(wù)器本地緩存中緩存數(shù)據(jù)可能會導(dǎo)致OOM。在使用本地緩存的時候要非常小心,尤其當(dāng)你會緩存一些大量數(shù)據(jù)的時候。
另外,這個策略在接下來我要說的這種情況下仍然沒有作用。例如,當(dāng)一個有很多粉絲的大V上傳了一個新的照片或者視頻到他們的社交賬號上,這時候大量粉絲被提醒大V有新的內(nèi)容發(fā)布,這時候粉絲會集中在相同的時間點(diǎn)上登陸社交平臺查看新的內(nèi)容。但是可能大V發(fā)送的新內(nèi)容數(shù)據(jù)還沒有加載到緩存中,這就會導(dǎo)致可怕的緩存踩踏。那么,我們還能做什么呢?
2.2 鎖和Promise
緩存踩踏的核心問題是競態(tài)條件(race condition),即很多的線程爭奪共享資源。只不過這里爭奪的共享資源是緩存。
通常在高并發(fā)的系統(tǒng)中,一種阻止共享資源競態(tài)的方法是加鎖。一般來講,鎖是用在相同機(jī)器上的不同線程,不過也可以使用分布式鎖來應(yīng)對不同機(jī)器對共享資源的競爭(參考redis分布式鎖:http://redis.cn/topics/distlock.html)。
通過給緩存KEY加鎖,就會在同一時間只有一個調(diào)用者能訪問爭奪的緩存。如果KEY不存在或者已經(jīng)過期,調(diào)用者就會拿到鎖。這時候其他爭奪的處理線程必須等待直到這個鎖被釋放。
用鎖來解決這個問題,它也會引入另一個問題:系統(tǒng)如何處理所有正在等待鎖釋放的那些線程?
你想嘗試自旋鎖(spinlock),讓這些線程持續(xù)不斷的輪詢?nèi)カ@取鎖?這就會導(dǎo)致出現(xiàn)非常busy的場景,消耗大量的CPU?;蛘咦尵€程在檢查鎖是否可用之前隨機(jī)等待一段時間?這樣的話,你又會碰到驚群效應(yīng)問題(thundering herd problem)。
引入退避和抖動機(jī)制來防止驚群效應(yīng)?這可能行得通,但還有另外一個問題。持有鎖的線程必須重新計算值,并在釋放鎖之前更新緩存鍵。這個過程可能需要耗費(fèi)一點(diǎn)時間,特別是當(dāng)計算成本很高或存在網(wǎng)絡(luò)問題時,如果因為計算緩存而耗盡了可用的連接池,仍然可能導(dǎo)致宕機(jī)。
backoff-and-jitter
幸運(yùn)的是,一些大公司也碰到過這樣的問題,他們使用promises來解決這樣的問題。
2.3 Promises如何防止自旋
引用Instagram工程師博客(Thundering Herds & Promises)中的內(nèi)容:
在Instagram, 當(dāng)我們啟用一個新的集群,并且因為集群中的緩存是空的,我們就會碰到緩存stampede問題。這時候,我們就會用promises來解決這個問題。它的核心思想是:不緩存實際的值,而是緩存一個promise,這個promise最終會提供我們需要的值。當(dāng)我們使用緩存時,如果碰到一個不存在的KEY,我們不立即去數(shù)據(jù)庫中查詢,而是創(chuàng)建一個promise然后放到緩存中,這個緩存中的promise會去查詢數(shù)據(jù)庫,其他的并發(fā)請求發(fā)現(xiàn)這個promise就不會把請求打到數(shù)據(jù)庫上,它們都會等待第一個線程放進(jìn)去的promise去數(shù)據(jù)庫中查詢結(jié)果。
通過緩存promise而不是實際的值,就不會自旋鎖了。第一個線程發(fā)現(xiàn)緩存中沒有數(shù)據(jù),就會用原子性的操作創(chuàng)建并緩存一個異步的promise,所有后續(xù)的請求都能立即返回這個promise:
你仍然需要使用鎖來防止多個線程訪問緩存KEY,假設(shè)創(chuàng)建 Promise 是一個近乎即時的操作,那么線程停留在自旋鎖中的時間長度就可以忽略不計了。但是,如果重新計算緩存數(shù)據(jù)需要相當(dāng)長的時間,那該怎么辦?即使線程能夠立即獲取到緩存的 Promise,它們?nèi)匀恍枰却惒竭M(jìn)程完成后才能將數(shù)據(jù)返回。雖然這種場景不一定會導(dǎo)致宕機(jī),但仍然會導(dǎo)致尾部延遲和影響整體用戶體驗。如果保持較低的尾部延遲對于應(yīng)用程序來說很重要,那么就需要考慮另外一種策略。
2.4 預(yù)先重新計算
預(yù)先重新計算(也被稱為提前過期)原理很簡單:在緩存KEY失效發(fā)生前,重新計算緩存的值然后延長失效時間,這就能確保緩存總是最新的,緩存缺失的問題也永遠(yuǎn)不會發(fā)生。
最簡單的實現(xiàn)方式就是開啟一個后臺處理線程,或者一個定時任務(wù)。例如。假設(shè)緩存KEY過期時間時一個小時,它需要花兩分鐘來計算值。那么,定時任務(wù)可以在過期時間到來之前的5分鐘運(yùn)行,更新緩存的值并延長失效時間一個小時。
雖然原理非常簡單,但是有一個明顯的缺點(diǎn),除非你很清楚哪個緩存KEY會被使用,否則你需要重新計算緩存中每個KEY的值,這將是一個非常耗時的過程。而且如果考慮到高可用,某個節(jié)點(diǎn)上計算任務(wù)失敗了,還需要轉(zhuǎn)移到另一個可用的節(jié)點(diǎn)上繼續(xù)計算。
基于這個原因,生產(chǎn)環(huán)境上很少有這么做的。當(dāng)然,也有一個例外。
2.5 概率性重新計算
在2015年,一組研究員發(fā)布了一份白皮書 Optimal Probabilistic Cache Stampede Prevention,即最優(yōu)概率性預(yù)防緩存踩踏。在這份白皮書中,他們描述了一個算法來預(yù)測在緩存失效之前,什么時候需要重新計算緩存的值。這里涉及到很多數(shù)學(xué)理論,但是可以做一個簡單的總結(jié):
currentTime - ( timeToCompute * beta * log(rand()) ) > expiry
這個公式中各變量的含義如下所示:
-
currentTime?表示當(dāng)前時間; -
timeToCompute?表示重新計算緩存值需要的時間; -
beta是一個大于0的非負(fù)數(shù),默認(rèn)為1,可配置; -
rand()?一個返回0~1之間隨機(jī)數(shù)的方法; -
expiry?下一次需要設(shè)置的失效時間戳;
它的思想是,每次線程從緩存中獲取數(shù)據(jù)時,它都需要運(yùn)行這個算法,如果返回true,那么這個線程將主動去重新計算緩存值。而且離失效時間越近,這個算法返回true的概率就越大。
這個策略不是很好理解,但是實現(xiàn)非常簡單,不需要考慮失敗轉(zhuǎn)移,也不需要到重新計算緩存中每一個KEY的值。當(dāng)然,預(yù)先重計算假設(shè)有一個值需要重新計算,它本身并不能防止其他線程引起緩存踩踏問題。為此,你需要將其與鎖和 Promise 結(jié)合起來使用。
3、如何停止正在發(fā)生的緩存踩踏
facebook緩存踩踏之所以如此嚴(yán)重的原因之一是,即使當(dāng)工程師找到了解決方案,他們并不能通過部署來解決。因為踩踏仍在繼續(xù)。事后診斷報告提到:
更糟糕的是,每次客戶端接收到數(shù)據(jù)庫查詢錯誤時,都會把它當(dāng)作一個無效的值,然后就會刪除緩存中相關(guān)的KEY,這就意味著即使原來的問題被修復(fù)了,但是查詢還在繼續(xù)。一旦數(shù)據(jù)庫無法正確響應(yīng)某一部分請求,那么就會導(dǎo)致緩存KEY被刪除,從而引起更多的請求打到數(shù)據(jù)庫上。
所幸的是,有一種已知的模型能處理這個問題。
熔斷器
這個想法不是很新的事情,2007年Michael Nygard發(fā)布了?Release It!后就慢慢流行了。熔斷器(Circuit breaking)的原理非常簡單,我們會在熔斷器中封裝一個方法,當(dāng)監(jiān)測到失敗時進(jìn)行計數(shù),并且一旦失敗達(dá)到一定閾值時,調(diào)用就會收到熔斷器直接返回的錯誤碼,而不會調(diào)用到受到熔斷器保護(hù)的地方,例如數(shù)據(jù)庫等。如下圖所示,第一次supplier能正常服務(wù),但是第二次、第三次訪問都是超時。達(dá)到熔斷器閾值后,第四次直接返回錯誤碼,而不會將請求直接打給supplier:熔斷器是響應(yīng)式的,所以它不能阻止宕機(jī)。不過它可以防止連鎖故障的發(fā)生。而且它提供了一個終止開關(guān),當(dāng)事態(tài)已經(jīng)徹底失控時可以開啟。如果 Facebook 使用了熔斷機(jī)制,就可以避免讓整個網(wǎng)站癱瘓下線。2010年的時候熔斷器還不是很流行,不過今天已經(jīng)有很多熔斷的開源組件,例如:Resilience4j, Istio和 Envoy。
4、學(xué)到了什么
這篇文章中談?wù)摿撕芏鄳?yīng)對緩存踩踏問題的策略,以及其他的科技公司是如何使用這些策略的。那么facebook呢?他們從這次事故中學(xué)到了什么?以及他們采取了什么措施來防止事故再次發(fā)生?他們的工程師寫了一篇文章:Under the hood: Broadcasting live video to millions,討論了他們對架構(gòu)所做的改進(jìn)。和本文我們提到的一樣,比如二級緩存。當(dāng)然,也提到了一些新的方法,比如 HTTP請求合并??傊?#xff0c;這篇文章非常值得一讀
5、寫在最后
我相信理解緩存踩踏對系統(tǒng)的破壞性是非常有必要的,當(dāng)然,并不是說每個團(tuán)隊必須馬上把這些策略用到他們的系統(tǒng)中。因為,選擇何種策略要應(yīng)對緩存踩踏并不是一件容易的事情,它依賴你的實際用戶場景,架構(gòu),以及流量負(fù)載情況。但是了解緩存踩踏以及對可能的解決方案對您將來有所幫助,當(dāng)你以后面對類型問題時,能從容應(yīng)對。
原文地址:https://betterprogramming.pub/how-a-cache-stampede-caused-one-of-facebooks-biggest-outages-dbb964ffc8ed
熱門內(nèi)容:說實話,DataGrip真得牛逼,只是你不會用而已~8年開發(fā),連登陸接口都寫這么爛...
字符串拼接還在用StringBuilder?快試試Java8中的StringJoiner吧,真香!編寫 if 時不帶 else,你的代碼會更好!最近面試BAT,整理一份面試資料《Java面試BAT通關(guān)手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。獲取方式:點(diǎn)“在看”,關(guān)注公眾號并回復(fù)?666?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
明天見(。・ω・。)ノ?
總結(jié)
以上是生活随笔為你收集整理的译 | 缓存穿透问题导致Facebook史上最严重事故之一的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中国计算机学会CCF推荐国际学术会议和期
- 下一篇: MCS-51单片机实验开发系统实验箱,Q