2 架構核心技術之分佈式緩存

要自己努力去爭取的。

學習自:架構師的 36 項修煉

1 開場白

緩存是架構設計中一個重要的手段,因爲其技術比較簡單,同時對性能提升的效果又很顯著。使用緩存需要注意幾個關鍵指標:緩存鍵集合大小、緩存空間的大小以及緩存的使用壽命。這三個指標決定了緩存的有效性、緩存的使用效率、緩存實現的效果。緩存的類型主要有代理緩存、反向代理緩存、 CDN 緩存和對象緩存幾種。

不是所有的數據都適合使用緩存:

  • 數據頻繁修改,這類數據使用緩存效果比較差。

  • 數據沒有熱點,這類數據緩存的命中率比較差。

  • 數據不一致,因爲緩存的數據和數據庫的數據是不同步的,可能存在數據不一致的情況,如果業務場景對數據一致性要求非常高,這個時候使用緩存也要注意。

  • 緩存雪崩,當緩存崩潰的時候,可能會導致整個系統的崩潰,這也是使用緩存中要注意的一個事項。

在架構中使用最多的是分佈式緩存。分佈式緩存最重要的幾個技術點是:分佈式對象緩存的架構、分佈式對象緩存的訪問模型,以及分佈式緩存中一個重要的算法——一致性哈希算法

2 緩存的特點

緩存的主要特點:

  • 技術簡單

  • 性能提升顯著

  • 應用場景多

在計算機的整個體系結構中緩存幾乎是無處不在的,比如,CPU 中就有緩存,在 CPU 固件裏就有 Cache,當 CPU 進行計算的時候,它並不總是每次都去內存中讀取數據,而是預加載一部分指令和數據到 Cache 裏面,也就是 CPU 的緩存裏面,CPU 核心計算取的數據其實大多數是 CPU 緩存中的數據。

                                           

操作系統的文件緩存。操作系統對磁盤進行操作的時候,它也會對數據進行緩存,以加快操作系統訪問磁盤文件數據的速度。

數據庫的查詢緩存,數據庫本身也會對一些數據表進行緩存。比如對索引的結構 B+ 樹進行緩存,對一些熱點的數據記錄也要進行緩存,以加快應用程序的訪問速度。

在外部應用系統中,比較常用的有 DNS 客戶端緩存、HTTP 瀏覽器緩存、HTTP 代理和反向代理緩存、CDN 緩存,以及各種類型的對象緩存,對象緩存常用的比如 Redis、Memcached 等等。

3 緩存數據存儲(Hash 表)

數據結構檢索(查找)之入土攻略(二)

緩存是存儲在內存中的,那麼如何從內存中快速獲取一個數據呢?

緩存使用的數據結構主要是哈希(Hash)表。哈希表最終的存儲形式通常是一個順序表,也就是一個數組結構。數組結構的特點是在內存中連續存儲分配。那麼,當我們要在哈希表中存儲一個數據的時候,哈希表通常是以 key、value 這樣的數據結構進行存儲的。

哈希表真正的物理存儲是一個數組,通過哈希表可以使整個數據存儲或檢索效率時間複雜都是 O(1)。

4 緩存的關鍵指標——命中率

緩存的主要特點是一次寫入多次讀出,通過這種手段減少對數據庫的使用,儘快從緩存中讀取數據,提高性能。所以緩存是否有效,主要就是看它一次寫進去的緩存能不能夠多次去讀出來響應業務的請求,這個判斷指標就叫作緩存的命中率。

緩存命中率怎麼算呢?查詢得到正確緩存結果除以總的查詢次數,得到的比值就是緩存命中率,比如說十次查詢九次都能夠得到緩存的正確結果,命中率就是 90%。

影響緩存命中率的主要因素有三個,分別是:

  • 緩存鍵集合大小

  • 內存空間大小

  • 緩存的壽命

4.1 緩存鍵集合大小

緩存中的每個對象都是通過緩存鍵進行識別的。剛纔 abc hello 這個例子裏 abc 就是一個緩存的鍵,鍵是緩存中唯一的識別符,定位一個對象的唯一方式就是對緩存鍵進行精確的匹配。

比如說我們想緩存每個商品的在線商品信息,就需要使用商品 ID 作爲緩存鍵。應用生成的唯一鍵越多,重用的機會越小。比如說根據 IP 地址緩存天氣數據,可能需要 40 多億個鍵。但是如果基於國家緩存天氣數據,那麼只需要幾百個緩存鍵就夠了,全世界也不過就幾百個國家。

所以要儘可能減少緩存鍵的數量,鍵的數量越少,緩存的效率越高。設計緩存的時候要關注緩存鍵是如何進行設計的,它的整個的集合範圍,限定在一個既能夠高效使用,又可以減少它的數量,這個時候緩存的性能是最好的。

4.2 緩存內存空間大小

緩存通常是存儲在內存中的,緩存對象可用的內存空間相對來說比較昂貴,而且受到嚴格限制。如果想緩存更多的對象,就需要先刪除老的對象,再添加新的對象。而這些老的對象被刪除掉,就會影響到緩存的命中率。所以物理上緩存的空間越大,緩存的對象越多,緩存的命中率也就越高。

4.3 緩存對象生存時間(緩存壽命)

 TTL:Time To Live

對象緩存的時間越長,被重用的可能性就越高。使緩存失效的方法有兩種:一種是超時失效;一種是清除失效,也就是實時清除。

(1) 超時失效

寫緩存的時候,每個緩存對象都設置一個超時時間,在超時之前訪問緩存就會返回緩存的數據,而一旦超時,緩存就失效了,這時候再訪問緩存,就會返回空。

(2) 實時清除

當有緩存對象更新的時候,直接通知緩存將已經被更新了的數據進行清除。清除了以後,應用程序下一次訪問這個緩存對象鍵的時候,就不得不到數據庫中去查找讀取,這個時候就會得到最新的數據,因爲更新總是更新在數據庫裏的。

還有一種,雖然時間上還沒有失效但是新的對象要寫入緩存,而內存空間不夠了,這個時候就需要將一些老的緩存對象清理掉,爲新的緩存對象騰出空間。

內存空間清除主要使用的算法是 LRU(Least Recently Used)算法,LRU 算法就是最近最久未用算法,也就是說清除那些最近最久沒有被訪問過的對象。這個算法使用鏈表結構實現的,所有的緩存對象都放在同一個鏈表上。當一個對象被訪問的時候,就把這個對象移到整個鏈表的頭部。當需要通過 LRU 算法清除那些最近最久未用對象的時候,只需要從隊列的尾部進行查找,越是在隊列尾部的,越是最近最久沒有被訪問過的,也就是優先清除的,騰出的內存空間讓新對象加入進來。

5 緩存的主要類型

5.1 代理緩存

代理緩存是存在客戶端一端的緩存,代理客戶端訪問互聯網。它的主要作用是互聯網訪問代理,代理了所有的客戶端 HTTP 請求,可以進行頁面緩存。如果有一些其他的客戶端已經訪問過這個網頁,那麼當新的客戶端連接的時候,就可以通過代理緩存中的數據直接返回,避免對數據中心的訪問。

5.2 反向代理緩存

反向代理代理數據中心輸出,存在於系統數據中心裏,它是數據中心的統一入口,代理整個數據中心其他服務器的應用處理。

用戶通過互聯網連接到數據中心的時候,連接的通常是一個反向代理服務器,反向代理服務器根據用戶的請求,在本地的反向代理緩存中查找是否有用戶請求的數據,如果有就直接返回這個數據,如果沒有再把這個請求向下繼續轉發,請求後面的應用服務器去處理生成數據。

反向代理緩存可以多層反向代理緩存的形式出現。因爲我們的應用服務器也是經過分層的,在處理的前端通常是一個前端服務器,後面有 Web 服務器,之後有應用服務器,再後還有其他的各類服務器。在這樣一個分層的服務器結構裏,我們可以對每一層的服務器都進行反向代理緩存。

如下圖所示,前端 Web 服務器和 Web 服務器分爲兩層,用戶請求接入的時候,先接入前端 Web 服務器,其上可以加一層反向代理服務器來代理前端 Web 服務器的 HTTP 請求。如果用戶請求的數據已經包含在這個反向代理服務器中,就可以直接返回;如果沒有,就再把 HTTP 請求提交給前端 Web 服務器,前端 Web 服務器會把請求發給後面的 Web 服務器。在 Web 服務器和前端 Web 服務器之間還可以再加一層反向代理服務器。如果前端 Web 服務器的請求在這一層的反向代理服務器中存在,那麼這一層反向代理服務器可以直接將數據返還;如果不存在,再將請求下發給 Web 服務器。

5.3 內容分發網絡 CDN 緩存

所謂的 CDN 是指在用戶請求的前端(儘量前的前端)爲用戶提供數據服務。CDN 並不存在於我們的數據中心,也不存在於用戶的訪問系統一端,它介於兩者之間,作爲網絡服務商的緩存服務。用戶進行互聯網訪問的時候,需要通過互聯網網絡服務商提供的網絡鏈接才能夠連接到數據中心,那麼網絡服務商就可以在自己提供的網絡服務的機房裏進行一次緩存操作,提供一次緩存服務。如下圖所示。

客戶端第一次訪問 example.com 的時候,訪問數據中心,數據中心返回 HTML 頁面以後,客戶端解析 HTML,HTML 裏面還各種 js 文件、css 文件、圖片等,這些靜態資源訪問的就是 CDN 服務器。CDN 服務器檢查自己是否有需要的靜態資源,如果有,就立即返回給客戶端;如果沒有,就自己訪問數據中心,獲得需要的靜態資源後,緩存在 CDN 服務器上後,再返回客戶端。

所以 CDN 緩存也叫作網絡訪問的“第一跳”,用戶請求先到達的是互聯網網絡服務商的機房。在機房裏面部署 CDN 服務器,提供緩存服務。如果 CDN 中存在用戶請求的 Web 響應內容,那麼就可以直接通過 CDN 進行返回;如果 CDN 中不存在,那麼 CDN 會把這個請求通過後面的網絡連接,把它發到系統的數據中心去。數據中心返回的結果依然是先通過 CDN 服務器,CDN 服務器就可以把數據緩存在自己的本地,供後面的用戶請求操作響應。 

5.4 通讀緩存

上面講到的代理緩存、反向代理緩存、CDN 緩存,都是通讀緩存。它代理了用戶的請求,也就是說用戶在訪問數據的時候,總是要通過通讀緩存。

當通讀緩存中有需要訪問的數據的時候,直接就把這個數據返回;如果沒有,再由通讀緩存向真正的數據提供者發出請求。其中重要的一點是客戶端連接的是通讀緩存,而不是生成響應的原始服務器,客戶端並不知道真正的原始服務器在哪裏,不會直接連接原始服務器,而是由通讀緩存進行代理。

5.5 旁路緩存

和通讀緩存相對應的叫作旁路緩存。前面提到的 key、value 這樣的對象緩存就屬於旁路緩存。旁路緩存和通讀緩存不同。旁路緩存是客戶端先訪問旁路緩存中是否有自己想要的數據,如果旁路緩存中沒有需要的數據,那麼由客戶端自己去訪問真正的數據服務提供者,獲取數據。客戶端獲得數據以後,會自己把這個數據寫入到旁路緩存中,這樣下一次或者其他客戶端去讀取旁路緩存的時候就可以獲得想要的數據了。

在這裏插入講解一下各種介質數據訪問的延遲,以便對數據的存儲、緩存的特性以及數據的訪問延遲有一個感性的認識。      

如上圖所示,訪問本地內存大概需要 100ns 的時間;使用 SSD 磁盤進行搜索,大概需要 10萬ns 時間;數據包在同一個數據中心來回一次的時間,也就是在同一個路由環境裏進行一次訪問,大概需要 50萬ns 時間,也就是 0.5ms;使用非 SSD 磁盤進行一次搜索,大概需要 1000萬ns,也就是 10ms 的時間;按順序從網絡中讀取 1MB 的數據也是需要 10ms 的時間;按順序從傳統的機械磁盤,即非 SSD 磁盤,讀取 1MB 數據,大概需要 30ms 的時間;跨越大西洋進行一次網絡數據傳輸,一個來回大概需要 150ms 的時間。其中,1s 等於 1000ms,等於 10億ns。

6 合理使用緩存

6.1 注意頻繁修改的數據

緩存數據是爲一次寫入多次讀取準備的,但是如果寫入的數據很快就被修改掉了,數據還沒來得及讀取就已經失效或者更新了,系統的負擔就會很重,使用緩存也就沒有太多的意義。一般說來,數據的讀寫比例至少在 2:1 以上,緩存纔有意義。

6.2 注意沒有熱點的訪問數據

寫入的數據並不會被多次讀取,也就是所謂的沒有熱點,這時候使用緩存也是沒有意義的。

在淘寶中,那些熱門的商品可能會被幾百萬幾千萬次的訪問,那些冷門的商品可能一次訪問都沒有,熱門商品數據就是有熱點的,就需要緩存。在微博中也是,那些微博大V們的微博會被幾百萬幾千萬的粉絲訪問,他們的微博數據也是有熱點的,而那些沒有幾個粉絲的博主的微博,幾乎不會被訪問,這些數據是沒有熱點的。所以緩存存儲的就是淘寶上那些熱門的商品,微博上那些大V的微博,它們都是有熱點的緩存,這些數據都能實現一次寫入多次甚至非常多次的讀取,這種緩存就有效果。但還是有一些業務場景數據是沒有熱點的,那麼這一類業務場景數據就不需要使用緩存。

6.3 注意數據不一致和髒讀

緩存中的數據有可能和主存儲數據庫中的數據不一致。這個問題主要是通過失效時間來解決的,也就是說這個業務能夠容忍的失效時間之內,保持緩存中的數據和數據庫中的數據不一致,比如說淘寶的商品數據,如果賣家在對商品的數據進行了編輯,這個時候可能買家是看不到這些被更新過的數據的,可能需要幾分鐘的時間,比如是 3 分鐘,那麼 3 分鐘之內,賣家編輯的數據買家是看不到的,但這種延遲通常是可以接受的。

如果某些業務場景對更新非常敏感,必須要實時看到,這個時候就不能夠使用失效時間進行緩存過期處理了,可能需要進行失效通知。當數據進行更新的時候,立即清除緩存中的數據,下次訪問這個數據的時候,緩存必須要重新從主數據庫中去加載,才能夠得到最新的數據。

6.4 注意緩存雪崩

因爲熱點數據主要是從緩存中去讀取的,而熱點數據是數據訪問壓力最大的一類數據。這些數據都從緩存中讀取,極大地降低了數據庫的訪問壓力。

而數據庫整個系統也是在有緩存的情況下進行設計的,數據庫的處理能力是強依賴緩存的。如果緩存忽然崩潰了,那麼所有的訪問壓力就都會傳遞到數據庫上去。數據庫不能夠承受這樣的訪問壓力,可能也會崩潰。數據庫崩潰了以後,應用程序訪問不到數據庫,請求不斷超時,負載壓力不斷升高,應用程序服務器也會崩潰,最後導致整個網站所有服務器崩潰。這就是緩存雪崩。這種情況下系統甚至無法啓動,因爲系統啓動後,新的訪問壓力又過來,依然是那麼大,還是會崩潰。

這時候重啓緩存也是沒有用的,因爲重啓的話緩存中是沒有數據的。我們剛纔也講到,對象緩存是通過加載數據庫中的數據並寫入到緩存中才有數據的。重新啓動的緩存沒有數據,它就不能夠承擔提供數據讀操作的能力。所以,對緩存有重點依賴的系統,需要特別關注緩存的可用性。緩存用的部分數據丟失可以到數據庫中加載,但是如果全部的緩存數據都丟失了,可能導致整個系統都會崩潰,特別需要注意。

7 分佈式緩存

分佈式對象緩存:對象緩存以一個分佈式集羣的方式對外提供服務,多個應用系統使用同一個分佈式對象緩存提供的緩存服務。這裏的緩存服務器是由多臺服務器組成的,這些服務器共同構成了一個集羣對外提供服務。

如何找到正確的緩存服務器進行讀寫操作?

如果第一次寫入數據的時候寫入的是 A 服務器,但是數據進行緩存讀操作的時候訪問的是 B 服務器,就不能夠正確地查找到數據,緩存也就沒有了效果。

7.1 Memcached 服務器集羣

當需要進行分佈式緩存訪問的時候,依然是以 Key、value 這樣的數據結構進行訪問。如上圖所示的例子中就是 BEIJING 作爲 Key,一個 DATA 數據作爲它的 value。當需要進行分佈式對象訪問的時候,應用程序需要使用分佈式對象緩存的客戶端 SDK。比如說 Memcached 提供的一個客戶端 API 程序進行訪問,客戶端 API 程序會使用自己的路由算法進行路由選擇,選擇其中的某一臺服務器,找到這臺服務器的 IP 地址和端口以後,通過通訊模塊和相對應的服務器進行通信。

因爲進行路由選擇的時候,就是使用緩存對象的 key 進行計算。下一次使用相同的 key 使用相同路由算法進行計算的時候,算出來的服務器依然還是前面計算出來的這個服務器。所以通過這種方法可以訪問到正確的服務器進行數據讀寫。服務器越多,提供的緩存空間就越大,實現的緩存效果也就越好。通過集羣的方式,提供了更多的緩存空間。

路由算法又是如何進行服務器路由選擇的?

使用餘數哈希。比如說,我們這裏緩存服務器集羣中有 3 臺服務器,key 的哈希值對 3 取模得到的餘數一定在 0、1、2 三個數據之間,那麼每一個數字都對應着一臺服務器,根據這個數字查找對應的服務器 IP 地址就可以了。使用餘數取模這種方式進行路由計算非常簡單,但這種算法也有一個問題,就是當服務器進行擴容的時候,比如說我們當前的服務器集羣有 3 臺服務器,如果我們 3 臺服務器不夠用了,需要添加 1 臺服務器,這個時候對 3 取模就會變成對 4 去取模,導致的後果就是以前對 3 取模的時候寫入的數據,對 4 取模的時候可能就查找不到了。

上面也講過緩存雪崩的情況,實際上如果使用取模算法進行服務器添加,因爲除數的變化會導致和緩存雪崩一樣的後果,也就是說前面寫入緩存服務器集羣中的緩存數據,添加了 1 臺服務器後很多數據都找不到了,類似於雪崩,最後會導致整個服務器集羣都崩潰。

我們添加服務器的主要目的是提高它的處理能力,但是不正確的操作可能會導致整個集羣都失效。解決這個問題的主要手段是使用一致性哈希算法。

7.2 一致性哈希算法

一致性哈希和餘數哈希不同,一致性哈希首先是構建一個一致性哈希環的結構。一致性哈希環的大小是 0~2 的 32 次方減 1,實際上就是我們計算機中無符號整型值的取值範圍,這個取值範圍的 0 和最後一個值 2 的 32 次方減 1 首尾相連,就構成了一個一致性哈希環,如下圖所示。

對每個服務器的節點取模,求它的哈希值並把這個哈希值放到環上,所有的服務器都取哈希值放到環上,每一次進行服務器查找路由計算的時候,把 key 也取它的哈希值,取到哈希值以後把 key 放到環上,順時針查找距離它最近的服務器的節點是哪一個,它的路由節點就是哪一個。通過這種方式也可以實現,key 不變的情況下找到的總是相同的服務器。這種一致性哈希算法除了可以實現像餘數哈希一樣的路由效果以外,對服務器的集羣擴容效果也非常好。

在一致性哈希環上進行服務器擴容的時候,新增加一個節點不需要改動前面取模算法裏的除數,導致最後的取值結果全部混亂,它只需要在哈希環里根據新的服務器節點的名稱計算它的哈希值,把哈希值放到這個環上就可以了。放到環上後,它不會影響到原先節點的哈希值,也不會影響到原先服務器在哈希環上的分佈,它只會影響到離它最近的服務器,比如上圖中 NODE3 是新加入的服務器,那麼它只會影響到 NODE1,原先訪問 NODE1 的 key 會訪問到 NODE3 上,也就是說對緩存的影響是比較小的,它只會影響到緩存裏面的一小段。如果緩存中一小部分數據受到了影響,不能夠正確的命中,那麼可以去數據庫中讀取,而數據庫的壓力只要在它的負載能力之內,也不會崩潰,系統就可以正常運行。所以通過一致性哈希算法可以實現緩存服務器的順利伸縮擴容。

但是一致性哈希算法有着致命的缺陷。我們知道哈希值其實是一個隨機值,把一個隨機值放到一個環上以後,可能是不均衡的,也就是說某兩個服務器可能距離很近,而和其它的服務器距離很遠,這個時候就會導致有些服務器的負載壓力特別大,有些服務器的負載壓力非常小。同時在進行擴容的時候,比如說加入一個節點 3,它影響的只是節點 1,而我們實際上希望加入一個服務器節點的時候,它能夠分攤其它所有服務器的訪問壓力和數據衝突。

所以對這個算法需要進行一些改進,改進辦法就是使用虛擬節點。也就是說我們這一個服務器節點放入到一致性哈希環上的時候,並不是把真實的服務器的哈希值放到環上,而是將一個服務器虛擬成若干個虛擬節點,把這些虛擬節點的 hash 值放到環上去。在實踐中通常是把一個服務器節點虛擬成 200 個虛擬節點,然後把 200 個虛擬節點放到環上。key 依然是順時針的查找距離它最近的虛擬節點,找到虛擬節點以後,根據映射關係找到真正的物理節點。

第一,可以解決我們剛纔提到的負載不均衡的問題,因爲有更多的虛擬節點在環上,所以它們之間的距離總體來說大致是相近的。第二,在加入一個新節點的時候,是加入多個虛擬節點的,比如 200 個虛擬節點,那麼加入進來以後環上的每個節點都可能會受到影響,從而分攤原先每個服務器的一部分負載。

發佈了180 篇原創文章 · 獲贊 642 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章