天天看點

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

作者:架構思考
本文将系統介紹緩存的相關知識,通過用23張圖檔及5W1H分析法為你詳細闡述,希望能幫你系統掌握緩存。

來,先上文章的目錄,讓大家可以對緩存這塊知識先建立一個系統性的認知,然後我會按點逐個擊破,讀者們也可以按需閱讀哈!

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

一、 什麼是緩存(what)

維基百科對緩存的定義是:

In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.

簡而言之,緩存就是存儲資料副本或計算結果的元件,以便後續可以更快地通路。

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

在計算中,緩存是一個高速資料存儲層,其中存儲了資料的子集,且通常是短暫性存儲,這樣日後再次請求該資料時,直接讀緩存會比重新計算結果或讀資料存儲更快。通過緩存,你可以高效地重用之前檢索或計算的資料。

二、為什麼要使用緩存(why)

從定義上可以看出所謂緩存其實是其他資料的副本,使用緩存是為了更快地檢索或計算資料。

(一)硬體層面:如CPU中的高速緩存

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

從上圖可以看出CPU(藍色)和記憶體(粉紅色)之間存在巨大的性能差距。在CPU通路資料和指令遵循計算機系統的局部性原理:

  • 時間局部性:CPU通常使用的許多資料會被多次使用。
  • 空間局部性:CPU使用的許多資料通常在實體上接近以前使用的資料。

使用高速緩存可以彌補CPU和記憶體之間的性能差異,減少CPU浪費計算時間等待記憶體資料。

(二)軟體層面

  • 為緩解CPU壓力而做緩存:比如把方法運作結果存儲起來、把原本要實時計算的内容提前算好、把一些公用的資料進行複用,這可以節省CPU算力,順帶提升響應性能。
  • 為緩解I/O壓力而做緩存:比如把原本對網絡、磁盤等較慢媒體的讀寫通路變為對記憶體等較快媒體的通路,将原本對單點部件(如資料庫)的讀寫通路變為到可擴縮元件(如緩存中間件)的通路,順帶提升響應性能。

(三)産品層面

是否可以解決使用者的痛點問題決定着使用者會不會一開始嘗試使用某款産品,是否有極緻的使用者體驗影響使用者會不會持續使用某款産品。在業務中使用緩存的目的就是通過擴大系統吞吐量、減少時延和響應時間來優化使用者體驗,适當的性能優化可以提升體驗,增強使用者粘性。

在現有的網際網路應用中,緩存的使用是一種能夠提升服務快速響應的關鍵技術,也是産品經理無暇顧及的非功能需求,需要在設計技術方案時對業務場景,具有一定的前瞻性評估後,決定在技術架構中是否需要引入緩存解決這種這種非功能需求。

三、 什麼時候使用緩存(when)

緩存不是架構設計的必選項,也不是業務開發中的必要功能點,隻有在業務出現性能瓶頸,進行優化性能的時候才需要考慮使用緩存來提升系統性能。并非所有的業務場景都适合用緩存,讀多寫少、不要求一緻性、時效要求越低、通路頻率越高、對最終一緻性和資料丢失有一定程度的容忍的場景才适合使用緩存,緩存并不能解決所有的性能問題,倘若濫用緩存會帶來額外的維護成本,使得系統架構更複雜更難以維護。

雖然緩存适用于各種各樣的案例,但要充分利用緩存,需要進行一定的規劃。是以在決定是否緩存一段資料時,請考慮以下問題:

  • 使用緩存值是否安全?相同的資料在不同的上下文中可能有不同的一緻性要求。例如電商系統中,線上結賬期間,必須知道商品的确切價格,是以不适合使用緩存,但在其他頁面上,價格晚幾分鐘更新不會給使用者帶來負面影響。
  • 對于該資料而言,緩存是否高效?某些應用程式會生成不适合緩存的通路模式;例如,掃描頻繁變化的大型資料集的鍵空間。在這種情況下,保持緩存更新可能會抵消緩存帶來的所有優勢。
  • 資料結構是否适合緩存?例如:以單條資料庫記錄形式緩存資料通常足以提供顯著的性能優勢。但有些時候,資料最好以多條記錄組合在一起的格式進行緩存。緩存以簡單的鍵值形式存儲,是以您可能還需要以多種不同格式緩存資料記錄,以便按記錄中的不同屬性進行通路。

另外把緩存當做存儲來使用是一件極其緻命的做法,這種錯誤的認識,将緩存引入系統的那一刻起就意味着已經讓系統走上了危險的局面,隻有對緩存的使用邊界有深刻的了解,才能盡可能減少引入緩存帶來的副作用。

四、 誰會使用緩存(who)

其實緩存的思想随處可見,日常生活中人們都會有意無意地用到。比如你會把常看的書放到書桌上,這樣你可以更快地拿到它們,而受限于桌面空間,不常看的書就要放在空間更大的書櫃裡了,等到要看的時候再從書櫃裡拿出來放到書桌上。這裡的書桌其實就是一種緩存媒體了。

對于程式員來說,緩存更是家常便飯了。有哪些程式員會用到緩存技術呢?

硬體開發工程師:比如CPU緩存、GPU緩存和數字信号處理器(DSP)緩存等。

軟體開發工程師:

用戶端開發:比如第4章節講到的頁面、浏覽器、APP緩存技術都和用戶端息息相關。

後端開發:比如服務端本地緩存、redis和memcached充當資料庫緩存、靜态頁面緩存等。

分布式開發:現在分布式大行其道,分布式緩存必然是分布式開發中繞不開的一環,6.10節的分布式緩存也是我們要重點講解的。

作業系統開發:作業系統核心負責管理磁盤緩存,比較典型的就有主存中的頁面緩存技術。

五、 哪些地方會使用緩存(where)

(一)緩存分類

按照不同的次元,可以對緩存分門别類,如下圖所示。

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

下面我着重按照 緩存所處鍊路節點的位置 來系統梳理出不同類型的緩存應用。

  • 用戶端緩存

HTTP協定的無狀态性決定了它必須依靠用戶端緩存來解決網絡傳輸效率上的缺陷。由于每次請求都是獨立的,服務端不儲存此前請求的狀态和資源,是以也不可避免地導緻其攜帶有重複的資料,造成網絡性能降低。HTTP協定對此問題的解決方案便是用戶端緩存。

常見的用戶端緩存有如下幾種:

  • 頁面緩存

頁面緩存是指将靜态頁面擷取頁面中的部分元素緩存到本地,以便下次請求不需要重複資源檔案,h5很好的支援的離線緩存的功能,具體實作可通過頁面指定manifest檔案,當浏覽器通路一個帶有manifest屬性的檔案時,會先從應用緩存中擷取加載頁面的資源檔案,并通過檢查機制處理緩存更新的問題。

  • APP緩存

APP可以将内容緩存到記憶體或者本地資料庫中,例如在一些開源的圖檔庫中都具備緩存的技術特性,當圖檔等資源檔案從遠端伺服器擷取後會進行緩存,以便下一次不再進行重複請求,并可以減少使用者的流量費用。

用戶端緩存是前端性能優化的一個重要方向,畢竟用戶端是距離“使用者”最近的地方,是一個可以充分挖掘優化潛力的地方。

  • 浏覽器緩存

浏覽器緩存通常會專門開辟記憶體空間以存儲資源副本,當使用者後退或者傳回上一步操作時可以通過浏覽器緩存快速的擷取資料,減少頁面加載時間和帶寬使用。在HTTP從1.0到1.1,再到2.0版本的每次演進中,逐漸形成了現在被稱為“狀态緩存”、“強制緩存”(許多資料中簡稱為“強緩存”)和“協商緩存”的HTTP緩存機制。在HTTP 1.1中通過引入e-tag标簽并結合expire、cache-control兩個特性能夠很好的支援浏覽器緩存。

下面我們用思維導圖了解強制緩存和協商緩存,請注意體會梳理思路!

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

對于Etag的補充說明:

HTTP伺服器可以根據自己的意願來選擇如何生成這個辨別,比如Apache 伺服器的Etag值預設是對檔案的索引節點(INode),大小和最後修改時間進行哈希計算後得到的。

Etag是HTTP中一緻性最強的緩存機制,比如,Last-Modified标注的最後修改隻能精确到秒級,如果某些檔案在1秒鐘以内,被修改多次的話,它将不能準确标注檔案的修改時間;又或者如果某些檔案會被定期生成,可能内容并沒有任何變化,但Last-Modified卻改變了,導緻檔案無法有效使用緩存,這些情況Last-Modified都有可能産生資源一緻性問題,隻能使用Etag解決。

Etag卻又是HTTP中性能最差的緩存機制,展現在每次請求時,服務端都必須對資源進行哈希計算,這比起簡單擷取一下修改時間,開銷要大了很多。Etag和Last-Modified是允許一起使用的,伺服器會優先驗證Etag,在 Etag一緻的情況下,再去對比Last-Modified,這是為了防止有一些HTTP 伺服器未将檔案修改日期納入哈希範圍内。

擴充知識:内容協商機制

到這裡為止,HTTP的協商緩存機制已經能很好地處理通過URL擷取單個資源的場景,為什麼要強調“單個資源”呢?在HTTP協定的設計中,一個 URL 位址是有可能能夠提供多份不同版本的資源,比如,一段文字的不同語言版本,一個檔案的不同編碼格式版本,一份資料的不同壓縮方式版本,等等。是以針對請求的緩存機制,也必須能夠提供對應的支援。為此,HTTP協定設計了以Accept(Accept、Accept-Language、Accept-Charset、Accept-Encoding)開頭的一套請求Header和對應的以Content-(Content-Language、Content-Type、Content-Encoding)開頭的響應Header,這些Headers被稱為HTTP的内容協商機制。與之對應的,對于一個URL能夠擷取多個資源的場景中,緩存也同樣也需要有明确的辨別來獲知根據什麼内容來對同一個URL傳回給使用者正确的資源。這個就是Vary Header的作用,Vary後面應該跟随一組其他Header的名字,比如:

HTTP/1.1 200 OK
Vary: Accept, User-Agent           

以上響應的含義是應該根據MIME類型和浏覽器類型來緩存資源,擷取資源時也需要根據請求Header中對應的字段來篩選出适合的資源版本。

(二)網絡緩存

網絡緩存位于用戶端以及服務端中間,通過代理的方式解決資料請求的響應,降低資料請求的回源率。回源率又分為以下兩種:

  • 回源流量比:回源流量是代理伺服器節點請求源伺服器資源時産生流量。回源流量比=回源流量/(回源流量+使用者請求通路的流量),比值越低,性能越好。
  • 回源請求數比:指代理伺服器節點對于沒有緩存、緩存過期(可緩存)和不可緩存的請求占全部請求記錄的比例。

網絡緩存常見的代理形式分為兩種:web代理緩存、邊緣緩存。

在介紹網絡緩存之前我們先了解下前置知識----兩種伺服器代理方式:正向代理、反向代理。

正向代理其實就是:用戶端通過代理伺服器與源伺服器進行非直接連接配接。用戶端可以感覺到代理伺服器的存在,對源伺服器透明(源伺服器感覺不到用戶端的存在),如下圖所示:

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

通信時用戶端和代理伺服器要設定好代理協定,比如Socks協定或者是HTTP協定(可以設定第四章第一節浏覽器緩存中的HTTP Header)。

那正向代理有什麼作用呢?

  • 提高通路速度:通常代理伺服器都設定一個較大的緩沖區,當有外界的資訊通過時,同時也将其儲存到緩沖區中,當其他使用者再通路相同的資訊時, 則直接由緩沖區中取出資訊,傳給使用者,以提高通路速度。
  • 控制對内部資源的通路:如某大學FTP(前提是該代理位址在該資源的允許通路範圍之内)使用教育網内位址段免費代理伺服器,就可以用于對教育網開放的各類FTP下載下傳上傳,以及各類資料查詢共享等服務。
  • 過濾、調整内容:例如限制對特定計算機的通路、壓縮請求包、改變請求包的語言格式等。
  • 隐藏真實IP:通過代理伺服器隐藏自己的IP,但更安全的方法是利用特定的工具建立代理鍊(如:Tor)。
  • 突破網站的區域限制:通過代理伺服器通路一些被限制的網站。

反向代理就是:用戶端通過代理伺服器與源伺服器進行非直接連接配接。用戶端隻會得知反向代理的IP位址,而不知道在代理伺服器後面的伺服器叢集的存在。如下圖所示:

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

反向代理有什麼作用呢?

  • 對于靜态内容及短時間内有大量通路請求的動态内容提供緩存服務。
  • 對用戶端隐藏伺服器(叢集)的IP位址。
  • 安全:作為應用層防火牆,為伺服器提供基于Web的攻擊行為(例如DoS/DDoS)的防護,更容易排查惡意軟體等。
  • 為後端伺服器(叢集)統一提供加密和SSL加速(如SSL終端代理)。
  • 負載均衡:若伺服器叢集中有機器負荷較高,反向代理通過URL重寫,把請求轉移到低負荷機器擷取與所需相同的資源。深入了解負載均衡強烈推薦看這篇幹貨:《老生常談的負載均衡,你真的懂了嗎?》。
  • 對一些内容進行壓縮,以節約帶寬或為帶寬不佳的網絡提供正常服務。
  • 提供HTTP通路認證。

介紹完2種伺服器代理形式後,我們來說下兩種網絡緩存形式。

web代理緩存:web代理緩存通常是指正向代理,會将資源檔案和熱點資料放在代理伺服器上,當新的請求到來時,如果在代理伺服器上能擷取資料,則不需要重複請求到應用伺服器上。

邊緣緩存:和正向代理一樣,反向代理同樣可以用于緩存,例如nginx就提供了緩存的功能。進一步,如果這些反向代理伺服器能夠做到和使用者請求來自同一個網絡,那麼擷取資源的速度進一步提升,這類的反向代理伺服器可以稱之為邊緣緩存。常見的邊緣緩存就是内容分發網絡(Content Delivery Network),簡稱CDN。可以将圖檔等靜态資源檔案放到CDN上。

那什麼CDN呢?

如果把某個網際網路系統比喻為一家跨國企業,那内容分發網絡就是它遍布世界各地的分銷機構,如果現在有客戶要買一塊CPU,那訂機票飛到美國加州英特爾總部肯定是不合适的,到本地電腦城找個裝機店鋪才是普遍的做法,在此場景中,内容分發網絡就相當于電腦城裡的本地經銷商。

那CDN的主要工作過程有哪些呢?主要包括路由解析、内容分發、負載均衡、CDN的利用場景。

  • 路由解析

一次沒有内容分發網絡參與的DNS域名解析過程如下:

無論是使用浏覽器抑或是在程式代碼中通路某個網址域名,比如以www.wallbig.club.cn為例,如果沒有緩存的話,都會先經過DNS伺服器的解析翻譯,找到域名對應的IP位址才能開始通信,這項操作是作業系統自動完成的,一般不需要使用者程式的介入。不過,DNS伺服器并不是一次性地将“www.wallbig.club.cn”直接解析成IP位址,需要經曆一個遞歸的過程。首先DNS會将域名還原為“www.wallbig.club.cn.”,注意最後多了一個點“.”,它是“.root”的含義。早期的域名必須帶有這個點才能被DNS正确解析,如今幾乎所有的作業系統、DNS伺服器都可以自動補上結尾的點号,然後開始如下解析步驟:

  1. 用戶端先檢查本地的DNS緩存,檢視是否存在并且是存活着的該域名的位址記錄。DNS是以存活時間(Time to Live,TTL)來衡量緩存的有效情況的,是以,如果某個域名改變了IP位址,DNS伺服器并沒有任何機制去通知緩存了該位址的機器去更新或者失效掉緩存,隻能依靠TTL超期後的重新擷取來保證一緻性。後續每一級DNS查詢的過程都會有類似的緩存查詢操作。
  2. 用戶端将位址發送給本機作業系統中配置的本地DNS(Local DNS),這個本地DNS伺服器可以由使用者手工設定,也可以在DHCP配置設定時或者在撥号時從PPP伺服器中自動擷取到。
  3. 本地DNS收到查詢請求後,會按照“是否有www.wallbig.club.cn的權威伺服器”→“是否有wallbig.club.cn的權威伺服器”→“是否有club.cn的權威伺服器”→“是否有cn的權威伺服器”的順序,依次查詢自己的位址記錄,如果都沒有查詢到,就會一直找到最後點号代表的根域名伺服器為止。這個步驟裡涉及了兩個重要名詞:權威域名伺服器(Authoritative DNS)是指負責翻譯特定域名的DNS 伺服器,“權威”意味着這個域名應該翻譯出怎樣的結果是由它來決定的。DNS 翻譯域名時無需像查電話本一樣刻闆地一對一翻譯,根據來訪機器、網絡鍊路、服務内容等各種資訊,可以玩出很多花樣,權威DNS的也有很多靈活應用,後面也會講到。根域名伺服器(Root DNS)是指固定的、無需查詢的頂級域名(Top-Level Domain)伺服器,可以預設為它們已内置在作業系統代碼之中。全世界一共有13組根域名伺服器(注意并不是13台,每一組根域名都通過任播的方式建立了一大群鏡像,根據維基百科的資料,迄今已經超過1000台根域名伺服器的鏡像了)。13這個數字是由于DNS主要采用UDP傳輸協定(在需要穩定性保證的時候也可以采用TCP)來進行資料交換,未分片的UDP資料包在IPv4下最大有效值為512位元組,最多可以存放13組位址記錄,由此而來的限制。
  4. 現在假設本地DNS是全新的,上面不存在任何域名的權威伺服器記錄,是以當DNS查詢請求按步驟3的順序一直查到根域名伺服器之後,它将會得到“cn的權威伺服器”的位址記錄,然後通過“cn的權威伺服器”,得到“club.cn的權威伺服器”的位址記錄,以此類推,最後找到能夠解釋www.wallbig.club.cn的權威伺服器位址。
  5. 通過“www.wallbig.club.cn的權威伺服器”,查詢www.wallbig.club.cn的位址記錄,位址記錄并不一定就是指IP位址,在RFC規範中有定義的位址記錄類型已經多達數十種,比如IPv4下的IP位址為A記錄,IPv6下的AAAA記錄、主機名稱CNAME記錄,等等。

下面畫個圖,友善大家了解這個解析過程:

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

前面提到過,每種記錄類型中還可以包括多條記錄,以一個域名下配置多條不同的A記錄為例,此時權威伺服器可以根據自己的政策來進行選擇,典型的應用是智能線路:根據通路者所處的不同地區(比如華北、華南、東北)、不同服務商(比如電信、聯通、移動)等因素來确定傳回最合适的 A 記錄,将通路者路由到最合适的資料中心,達到智能加速的目的。

那如果有CDN參與的話,路由解析的具體工作過程又是哪樣的呢?

  1. 架設好“wallbig.club”的伺服器後,将伺服器的IP位址在你的CDN服務商上注冊為“源站”,注冊後你會得到一個CNAME,即本例中的“wallbig.club.cdn.dnsv1.com.”。
  2. 将得到的CNAME在你購買域名的DNS服務商上注冊為一條CNAME記錄。
  3. 當第一位使用者來訪你的站點時,将首先發生一次未命中緩存的DNS查詢,域名服務商解析出CNAME後,傳回給本地DNS,至此之後鍊路解析的主導權就開始由内容分發網絡的排程服務接管了。
  4. 本地DNS查詢CNAME時,由于能解析該CNAME的權威伺服器隻有CDN服務商所架設的權威DNS,這個DNS服務将根據一定的均衡政策和參數,如拓撲結構、容量、時延等,在全國各地能提供服務的CD緩存節點中挑選一個最适合的,将它的IP代替源站的IP位址,傳回給本地DNS。
  5. 浏覽器從本地DNS拿到IP位址,将該IP當作源站伺服器來進行通路,此時該IP的CDN節點上可能有,也可能沒有緩存過源站的資源,這點将在下面的“内容分發”小節讨論。
  6. 經過内容分發後的CDN節點,就有能力代替源站向使用者提供所請求的資源。

為了讀者更生動地了解以上步驟,我畫個時序圖,建議和上面的圖對比來看:

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

DNS系統多級分流的設計使得DNS系統能夠經受住全球網絡流量不間斷的沖擊,但也并非全無缺點。典型的問題是響應速度,當極端情況(各級伺服器均無緩存)下的域名解析可能導緻每個域名都必須遞歸多次才能查詢到結果,顯著影響傳輸的響應速度。

專門有一種被稱為“DNS 預取”(DNS Prefetching)的前端優化手段用來避免這類問題:如果網站後續要使用來自于其他域的資源,那就在網頁加載時生成一個link請求,促使浏覽器提前對該域名進行預解釋,比如下面代碼所示:

<link rel="dns-prefetch" href="//domain.not-wallbig.club">           

而另一種可能更嚴重的缺陷是DNS的分級查詢意味着每一級都有可能受到中間人攻擊的威脅,産生被劫持的風險。要攻陷位于遞歸鍊條頂層的(比如根域名伺服器,cn權威伺服器)伺服器和鍊路是非常困難的,它們都有很專業的安全防護措施。但很多位于遞歸鍊底層或者來自本地營運商的Local DNS伺服器的安全防護則相對松懈,甚至不少地區的營運商自己就會主動進行劫持,專門傳回一個錯的IP,通過在這個IP上代理使用者請求,以便給特定類型的資源(主要是HTML)注入廣告,以此牟利。

為此,最近幾年出現了另一種新的DNS工作模式:HTTPDNS(也稱為 DNS over HTTPS,DoH)。它将原本的DNS解析服務開放為一個基于 HTTPS 協定的查詢服務,替代基于UDP傳輸協定的DNS域名解析,通過程式代替作業系統直接從權威DNS或者可靠的Local DNS擷取解析資料,進而繞過傳統Local DNS。這種做法的好處是完全免去了“中間商賺差價”的環節,不再懼怕底層的域名劫持,能夠有效避免Local DNS不可靠導緻的域名生效緩慢、來源IP不準确、産生的智能線路切換錯誤等問題。

  • 内容分發

在DNS伺服器的協助下,無論是對使用者還是伺服器,内容分發網絡都可以是完全透明的,在兩者都不知情的情況下,由CDN的緩存節點接管了使用者向伺服器發出的資源請求。後面随之而來的問題是緩存節點中必須有使用者想要請求的資源副本,才可能代替源站來響應使用者請求。這裡面又包括了兩個子問題:“如何擷取源站資源”和“如何管理(更新)資源”。

CDN擷取源站資源的過程被稱為“内容分發”,“内容分發網絡”的名字正是由此而來,可見這是CDN的核心價值。目前主要有以下兩種主流的内容分發方式:

主動分發(Push):分發由源站主動發起,将内容從源站或者其他資源庫推送到使用者邊緣的各個CDN緩存節點上。這個推送的操作沒有什麼業界标準可循,可以采用任何傳輸方式(HTTP、FTP、P2P等等)、任何推送政策(滿足特定條件、定時、人工等等)、任何推送時間,隻要與後面說的更新政策相比對即可。由于主動分發通常需要源站、CDN服務雙方提供程式API接口層面的配合,是以它對源站并不是透明的,隻對使用者一側單向透明。主動分發一般用于網站要預載大量資源的場景。比如雙十一之前一段時間内,淘寶、京東等各個網絡商城就會開始把未來活動中所需用到的資源推送到CDN緩存節點中,特别常用的資源甚至會直接緩存到你的手機APP的存儲空間或者浏覽器的localStorage上。

被動回源(Pull):被動回源由使用者通路所觸發全自動、雙向透明的資源緩存過程。當某個資源首次被使用者請求的時候,CDN緩存節點發現自己沒有該資源,就會實時從源站中擷取,這時資源的響應時間可粗略認為是資源從源站到CDN緩存節點的時間,再加上資源從CDN發送到使用者的時間之和。是以,被動回源的首次通路通常是比較慢的(但由于CDN的網絡條件一般遠高于普通使用者,并不一定就會比使用者直接通路源站更慢),不适合應用于資料量較大的資源。被動回源的優點是可以做到完全的雙向透明,不需要源站在程式上做任何的配合,使用起來非常友善。這種分發方式是小型站點使用CDN服務的主流選擇,如果不是自建CDN,而是購買阿裡雲、騰訊雲的CDN服務的站點,多數采用的就是這種方式。

對于“CDN如何管理(更新)資源”這個問題,同樣沒有統一的标準可言,盡管在HTTP協定中,關于緩存的Header定義中确實是有對CDN這類共享緩存的一些指引性參數,比如“浏覽器”小節HTTP header參數Cache-Control的s-maxage,但是否要遵循,完全取決于CDN本身的實作政策。

現在,最常見的做法是逾時被動失效與手工主動失效相結合。逾時失效是指給予緩存資源一定的生存期,超過了生存期就在下次請求時重新被動回源一次。而手工失效是指CDN服務商一般會提供給程式調用來失效緩存的接口,在網站更新時,由持續內建的流水線自動調用該接口來實作緩存更新。

  • 負載均衡

負載均衡就是以統一的接口對外提供服務,但建構和排程服務叢集對使用者保持透明。深入了解負載均衡強烈推薦看這篇幹貨:《老生常談的負載均衡,你真的懂了嗎?》

  • CDN的應用場景

加速靜态資源:這是CDN最普遍的應用場景。

安全防禦:CDN在廣義上可以視作網站的堡壘機,源站隻對CDN提供服務,由CDN來對外界其他使用者服務,這樣惡意攻擊者就不容易直接威脅源站。CDN對某些攻擊手段的防禦,如對DDoS攻擊的防禦尤其有效。但需注意,将安全都寄托在CDN上本身是不安全的,一旦源站真實IP被洩漏,就會面臨很高的風險。

協定更新:不少CDN提供商都同時對接(代售CA的)SSL證書服務,可以實作源站是HTTP協定的,而對外開放的網站是基于HTTPS的。同理,可以實作源站到CDN是HTTP/1.x協定,CDN提供的外部服務是HTTP/2或 HTTP/3協定、實作源站是基于IPv4網絡的,CDN提供的外部服務支援IPv6網絡,等等。

狀态緩存:CDN不僅可以緩存源站的資源,還可以緩存源站的狀态,比如源站的301/302轉向就可以緩存起來讓用戶端直接跳轉、還可以通過CDN開啟HSTS、可以通過CDN進行OCSP裝訂加速SSL證書通路等。有一些情況下甚至可以配置CDN對任意狀态碼(比如404)進行一定時間的緩存,以減輕源站壓力,但這個操作應當慎重,在網站狀态發生改變時去及時重新整理緩存。

修改資源:CDN可以在傳回資源給使用者的時候修改它的任何内容,以實作不同的目的。比如,可以對源站未壓縮的資源自動壓縮并修改Content-Encoding,以節省使用者的網絡帶寬消耗、可以對源站未啟用用戶端緩存的内容加上緩存Header,自動啟用用戶端緩存,可以修改CORS的相關 Header,将源站不支援跨域的資源提供跨域能力等。

通路控制:CDN可以實作IP黑/白名單功能,根據不同的來訪IP提供不同的響應結果,根據IP的通路流量來實作QoS控制、根據HTTP的Referer 來實作防盜鍊等。

注入功能:CDN可以在不修改源站代碼的前提下,為源站注入各種功能。

(三)服務端緩存

服務端緩存主要是為了減少CPU/IO、資料庫以及下遊服務接口的壓力,這也是實際程式設計中最常用的手段。

除減少資料庫的壓力外,緩存傳回資料的響應速度比資料庫要快。另外,盡可能不調用外部接口,因為外部接口無論WebSocket、WebService,還是HTTP,其響應速度都是不可控的。如果外部接口響應時間過長,也會影響自身性能。

服務端緩存大緻分為以下幾種:

  • 容器緩存,如Nginx、Tomcat等。
  • 中間件緩存,如MongoDB、Elasticsearch、Redis、ZooKeeper、Kafka等。
  • 頁面靜态化緩存,如FreeMaker、Thymeleaf等。
  • 檔案管理,如FastDFS等。

六、緩存必知必會

(一)緩存擊穿

緩存擊穿是一個失效的熱點Key被并發集中通路,導緻請求全部打在資料庫上。如下圖所示:

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

解決方案:

  • 熱點key緩存不失效:對熱點key可以設定永不過期。
  • 使用互斥鎖或堵塞隊列:這樣可以控制資料庫的線程通路數,減小資料庫的壓力,但也會讓系統吞吐率有所下降。實作流程如下:
  1. 阻塞目前Key的請求。
  2. 從後端存儲恢複資料。
  3. 在Key對應的Value還未恢複的過程中, 如果有其他請求繼續擷取該Key,同樣阻塞該請求。
  4. 當Key從後端恢複後,依次喚醒該Key對應阻塞的請求。
  • 用堵塞隊列來實作的話可以參考Golang官方庫singleflight。

熱點資料由代碼來手動管理,緩存擊穿是僅針對熱點資料被自動失效才引發的問題,對于這類資料,可以直接由開發者通過代碼來有計劃地完成更新、失效,避免由緩存政策來自動管理。

(二)緩存雪崩

某個時刻熱點資料出現大規模的緩存失效,大量的請求全部打到資料庫,導緻資料庫瞬時壓力過載進而拒絕服務甚至是當機。

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

分析:造成緩存雪崩的關鍵在于在同一時間大規模的key失效。誘因可能是

  • 大量熱點資料設定了相同或相近的過期時間。
  • 緩存元件不可用,比如redis當機了。

解決方案:

  • 打散緩存失效時間:主要是通過對key的TTL增加随機數去盡量規避,過期時間則需要根據業務場景去設定。
  • 使用多級緩存:這裡有個github開源庫:hybridcache可以借鑒下。
  • 兜底邏輯使用熔斷機制。防止過多請求同時打到DB。

在觸發熔斷機制後傳回預先配置好的兜底資料,減少過多請求壓倒DB導緻服務不可用。

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)
  • 元件高可用:
  • 對于redis這樣的緩存元件,可以搭建Redis叢集(叢集模式或哨兵模式),提高Redis的可用性,盡量規避單點故障導緻緩存雪崩。
「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

Redis基于一個Master主節點多Slave從節點的模式和Redis持久化機制,将一份資料保持在多個執行個體中,進而實作增加副本備援量,又使用哨兵機制實作主備切換,在master故障時,自動檢測,将某個slave切換為master,最終實作Redis高可用。

  • 提高資料庫的容災能力,可以使用分庫分表,讀寫分離的政策。

(三)緩存穿透

緩存穿透是指使用者查詢資料庫沒有的資料,緩存中自然也不會有。那先查緩存再查資料相當于進行了兩次無效操作。大量的無效請求将給資料庫帶來極大的通路壓力,甚至導緻其過載拒絕服務。下圖中紅色箭頭标出了每一次使用者請求到來都會經過的兩次無效查詢。

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

分析:造成緩存穿透的原因可能是

  • 空資料查詢(黑客攻擊),空資料查詢通常指攻擊者僞造大量不存在的資料進行通路(比如不存在的商品資訊、使用者資訊)。
  • 緩存污染(網絡爬蟲),緩存污染通常指在周遊資料等情況下冷資料把熱資料驅逐出記憶體,導緻緩存了大量冷資料而熱資料被驅逐。

解決方案:

  • 對于空資料查詢
  • 使用布隆過濾器高效判斷key是否存在,流程圖如下:
「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

雖然不能完全避免資料穿透的現象,但已經可以将99%的穿透查詢給屏蔽在Redis層了,極大的降低了底層資料庫的壓力,減少了資源浪費(布隆過濾器用bitmap實作)。如果想擁有更高的空間使用率和更小的誤判,替代的方案是使用布谷鳥過濾器。如果想了解這兩種過濾器的相關細節,強烈推薦看這篇文章:《作為一名背景開發,你必須知道的兩種過濾器》。

由于布隆過濾器存在“誤報”和“漏報”,而且實作也比較複雜,對于空資料查詢其實還有一種簡單粗暴的政策:

  • 緩存空值,流程圖如下:
「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

當碰到查詢結果為空的key時,放一個空值到緩存中,下次再通路就知道此key是無效的,避免無效查詢資料庫。但這樣花費額外的空間來存儲空值。

  • 對于緩存污染:關鍵點是能識别出隻通路一次或者通路次數很少的資料。然後使用淘汰政策去删除冷資料,下面以redis為例:
「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)
  • noeviction政策:不會淘汰資料,解決不了。
  • volatile-ttl政策:給資料設定合理的過期時間。當緩存寫滿時,會淘汰剩餘存活時間最短的資料,避免滞留在緩存中進而造成污染。
  • volatile-random政策:随機選擇資料,無法把不再通路的資料篩選出來,會造成緩存污染。
  • volatile-lru政策:LRU政策隻考慮資料的通路時效,對隻通路一次的資料,不能很快篩選出來。
  • volatile-lfu政策:LFU政策在LRU政策基礎上進行了優化,篩選資料時優先篩選并淘汰通路次數少的資料。
  • allkeys-random政策:随機選擇資料,無法把不再通路的資料篩選出來,會造成緩存污染。
  • allkeys-lru政策:LRU政策隻考慮資料的通路時效,對隻通路一次的資料,不能很快篩選出來。
  • allkeys-lfu政策:LFU政策在LRU政策基礎上進行了優化,篩選資料時優先篩選并淘汰通路次數少的資料。

(四)緩存預熱

緩存預熱是指系統上線後,提前将熱點資料加載到緩存系統。避免在使用者請求的時候,先查詢資料庫,然後再将資料緩存的問題,線上上高并發通路時可以提高資料通路速度和減小資料庫壓力。

解決方案:

  • 對于單點緩存
  • 寫個緩存重新整理頁面,上線時手工操作。
  • 資料量不大時,可以在程式啟動時加載。
  • 用定時器定時重新整理緩存,或者模拟使用者觸發。
  • 對于分布式緩存系統,如Redis
  • 寫程式或腳本往緩存中加載熱點資料。
  • 使用緩存預熱架構。

(五)緩存降級

緩存降級是指當通路量劇增、服務出現問題(如響應時間慢或不響應)或非核心服務影響到核心流程的性能時,即使是有損部分其他服務,仍然需要保證主服務可用。可以将其他次要服務的資料進行緩存降級,進而提升主服務的穩定性。降級的目的是保證核心服務可用,即使是有損的。而且有些服務是無法降級的(如加入購物車、結算)。

以參考日志級别設定預案:

  • 一般:比如有些服務偶爾因為網絡抖動或者服務正在上線而逾時,可以自動降級。
  • 警告:有些服務在一段時間内成功率有波動(如在95~100%之間),可以自動降級或人工降級, 并發送告警。
  • 錯誤:比如可用率低于90%,或者資料庫連接配接池被打爆了,或者通路量突然猛增到系統能承受的最大閥值,此時可以根據情況自動降級或者人工降級;
  • 嚴重錯誤:比如因為特殊原因資料錯誤了,此時需要緊急人工降級。

服務降級的目的,是為了防止Redis服務故障,導緻資料庫跟着一起發生雪崩問題。是以,對于不重要的緩存資料,可以采取服務降級政策,例如一個比較常見的做法就是,Redis出現問題,不去資料庫查詢,而是直接傳回預設值給使用者。

(六)緩存更新模式

  • Cache-Aside模式

Cache-Aside是最常用的一種緩存更新模式,其邏輯如下:

  • 失效:應用程式先從cache取資料,沒有得到,則從資料庫中取資料,成功後,放到緩存中。(圖1)
  • 命中:應用程式從cache中取資料,取到後傳回。(圖1)
  • 更新:先把資料存到資料庫中,成功後,再讓緩存失效。(圖2)
「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

注意,Cache-Aside更新操作是先更新資料庫,成功後,讓緩存失效。

如果是先删除緩存,然後再更新資料庫,會有什麼影響呢?

我們可以思考一下,假設有兩個并發操作,一個是更新操作,另一個是查詢操作,更新操作删除緩存後,查詢操作沒有命中緩存,先把老資料讀出來後放到緩存中,然後更新操作更新了資料庫。于是,在緩存中的資料還是老的資料,導緻緩存中的資料是髒的,而且還一直這樣髒下去了。

那麼Cache-Aside是否會出現上面提到的問題呢?

我們可以腦補一下,假如有兩個并發操作:查詢和更新,首先,沒有了删除cache資料的操作了,而是先更新了資料庫中的資料,此時,緩存依然有效,是以,并發的查詢操作拿的是沒有更新的資料,但是,更新操作馬上讓緩存的失效了,後續的查詢操作再把資料從資料庫中拉出來。就不會出現後續的查詢操作一直都在取老的資料的問題。

這是标準的design pattern,包括Facebook的論文《Scaling Memcache at Facebook》也使用了這個政策。為什麼不是寫完資料庫後更新緩存?你可以看一下Quora上的這個問答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕兩個并發的寫操作導緻髒資料。

那麼,是不是Cache Aside這個就不會有并發問題了?不是的,比如,一個是讀操作,但是沒有命中緩存,然後就到資料庫中取資料,此時來了一個寫操作,寫完資料庫後,讓緩存失效,然後,之前的那個讀操作再把老的資料放進去,是以,會造成髒資料。

這種case理論上會出現,不過,實際上出現的機率可能非常低,因為這個條件需要發生在讀緩存時緩存失效,而且并發着有一個寫操作。而實際上資料庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必需在寫操作前進入資料庫操作,而又要晚于寫操作更新緩存,所有的這些條件都具備的機率基本并不大。

是以,這也就是Quora上的那個答案裡說的,要麼通過2PC或是Paxos協定保證一緻性,要麼就是拼命的降低并發時髒資料的機率,而Facebook使用了這個降低機率的玩法,因為2PC太慢,而Paxos太複雜。當然,最好還是為緩存設定上過期時間。

  • Read/Write Through模式

在上面的Cache Aside中,應用代碼需要維護兩個資料存儲,一個是緩存(Cache),一個是資料庫(Repository)。是以,應用程式代碼比較複雜。而Read/Write Through是把更新資料庫(Repository)的操作由緩存服務自己代理,對于應用層來說,就簡單很多了。可以了解為,應用認為後端就是一個單一的存儲,而存儲自己維護自己的Cache。

Read-Through模式

Read-Through政策是當緩存失效時(過期或LRU換出),緩存服務自己從資料庫加載丢失的資料,填充緩存并将其傳回給應用程式。

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

Cache-Aside和Read-Through政策都是延遲加載資料,也就是說,隻有在第一次讀取時才加載資料。

Read-Though和Cache-Aside的差別:

  • Cache-Aside政策是應用程式負責從資料庫中擷取資料并填充到緩存。此邏輯在Read-Though中通常由庫或獨立緩存服務提供支援
  • 與Cache-Aside不同,Read-Through cache中的資料模型不能與資料庫中的資料模型不同。

Write-Through模式

Write Through套路和Read Through相仿,不過是在更新資料時發生。當有資料更新的時候,如果沒有命中緩存,直接更新資料庫,然後傳回。如果命中了緩存,則更新緩存,然後再由Cache自己更新資料庫(這是一個同步操作)。

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

就其本身而言,Write-Through似乎沒有太大作用,而且它會引入額外的寫入延遲,因為資料首先寫入緩存,然後寫入主資料庫。它一般和Read-Though配對使用,這樣可以獲得資料一緻性保證而且免于應用程式和緩存失效、更新、叢集管理等複雜的問題。

DynamoDB Accelerator (DAX) 是通讀/直寫緩存的一個很好的例子。它連接配接DynamoDB和你的應用程式。對DynamoDB的讀取和寫入可以通過DAX完成。

  • Write Behind Caching模式

Write Behind也叫Write Back。其實Linux檔案系統的Page Cache算法用的也是這個,是以說底層的東西很多時候是相通。

Write Behind的思想是:在更新資料時,隻更新緩存,不更新資料庫,而我們的緩存會異步地批量更新資料庫。

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

Write Behind的優勢有:

  • 直接操作記憶體,資料的I/O操作速度極快。
  • 更新資料庫時可以異步更新,可以合并對同一資料的多次操作,性能強悍。

事物總有兩面性,Write Behind的不足有:

  • 資料不能保證強一直性,而且可能會丢失。比如在Linux/Unix系統下突然關機會導緻資料丢失。
  • 實作邏輯比較複雜。因為需要監視哪些資料發生被更新了,并且在适當的時機持久化這些資料。比如作業系統的Write Behind會在僅當這個cache需要失效的時候,才會被真正持久起來,比如,記憶體不夠了,或是程序退出了等情況,這又叫lazy write。

沒有完美的方案,隻有合适的方案。我們基本上不可能做出一個沒有缺陷的設計,軟體設計從來都是取舍Trade-Off。比如算法設計中的時間換空間、空間換時間,有時候軟體的強一緻性和高性能不可兼得,高可用和高性能會有沖突等。

緩存更新這個章節,我們都沒有考慮緩存(Cache)和持久層(Repository)的整體事務的問題。比如,更新Cache成功,更新資料庫失敗了怎麼嗎?對于事務、分布式事務這個龐大的話題,大家如果有興趣的話歡迎私信我,後續我再出幾篇文章來分析、總結和歸納下。

(七)緩存資料庫更新一緻性問題

  • 概念介紹

雙寫一緻性:更新資料庫後保證redis緩存同步更新

  • 讀操作執行過程
  • 命中:讀操作先查詢cache,命中則直接傳回緩存資料。
  • 失效:讀操作先查詢cache,cache失效或過期導緻未命中,則從資料庫中讀,寫到緩存後傳回。
  • 寫操作執行過程
  • 寫操作更新緩存(更新緩存:先更新資料庫還是緩存?)
  • 先更新資料庫後更新緩存

A線程更新資料庫後還沒更新緩存時,B線程覆寫掉A的資料庫值,然後A後寫入緩存覆寫掉B的緩存值,導緻資料不一緻。

  • 先更新緩存後更新資料庫

更新緩存成功,但是更新資料庫失敗導緻資料不一緻

  • 寫操作删除緩存(删除緩存:先删還是後删緩存?)
  • 先寫資料庫後删緩存(facebook的緩存)

寫入資料庫未删緩存之前的緩存命中會是舊值(資料不一緻時間很短)

A線程的緩存未命中讀取資料庫舊值後B線程寫入資料庫并删除緩存(這裡沒有删除掉緩存,因為緩存不存在,如果存在A線程不可能緩存未命中),之後A線程才把舊值寫入緩存導緻資料不一緻(機率小:讀操作必需在寫操作前進入資料庫操作,而又要晚于寫操作更新緩存,但是讀操作時間比寫操作時間快很多)

  • 先删緩存後寫資料庫

A線程删掉緩存後未寫入資料庫之前,B線程讀取資料舊值,A線程寫入資料後,B更新緩存,導緻資料不一緻性。

解決辦法:延時雙删,延時雙删政策會在寫庫前後删除緩存中的資料,并且給緩存資料設定合理的過期時間,進而可以保證最終一緻性。寫流程具體如下:

  1. 先删除緩存,再寫資料庫。
  2. 休眠一段時間(比如500ms)
  3. 再次删除緩存。
「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

這裡為什麼會休眠一段時間呢?這裡主要是防止:在寫請求删除緩存但還未成功寫入資料庫後,讀請求可能将舊值加載到緩存。

讀流程:先讀緩存,當緩存未命中,再從資料庫中讀取,然後再寫入緩存。

延時雙删雖然解決了上述讨論的緩存對資料庫不一緻的問題,但這種政策存在以下問題:

  • 休眠一段時間可能會對性能造成影響。
  • 在第二次緩存删除失敗後,會導緻資料不一緻,需要業務方實作重試機制。

休眠時間需要業務方設定(難點在時間怎麼設定),并且要大于一次讀操作時間,才能在舊值加載到就緩存之後二次順利删掉緩存。

(八)緩存過期

緩存過期是個很複雜的問題。沒有解決這一問題的“靈丹妙藥”,但有幾個簡單的政策可供參考:

始終對所有緩存鍵設定生存時間(TTL),但Write-Through緩存更新政策(第6章第6節中有講到)除外。可以将生存時間設定成很長的時間,例如數小時,甚至數天。這種方法能夠捕獲應用程式錯誤,例如在更新底層資料庫時,忘記更新或删除給定的緩存鍵。最終,緩存鍵會自動過期并重新整理。

對于頻繁更改的資料,隻需設定較短的TTL (幾秒鐘) 即可。例如評論、排行榜、活動流等,不要添加Write-Through緩存或複雜的過期邏輯。如果某條資料庫查詢在生産環境中被大量通路,隻需改動幾行代碼就能為此查詢添加TTL為5秒的緩存鍵。在你評估更優雅的解決方案時讓你的應用程式保持正常運作。

Ruby on Rails團隊研究出了一種更新的模式 - 俄羅斯套娃緩存。在這種模式下,嵌套記錄通過其自有緩存鍵進行管理,頂層資源就是這些緩存鍵的集合。假設您有一個包含使用者、文章和評論的新聞網頁。在這種方法中,他們中的每個都是自己的緩存鍵,頁面則分别查詢每個鍵。

若不确定緩存鍵是否會在資料庫更新時受到影響,隻需删除此緩存鍵。如果你使用的是延遲更新政策(Cache-Aside、Write-Through)會在需要時重新整理此鍵。

有關緩存過期和俄羅斯套娃緩存的詳細介紹,請參閱 Basecamp Signal vs Noise部落格文章“俄羅斯套娃”緩存的性能影響。

還可以使用另一種模式以在下遊服務不可用時一定程度提高服務的彈性,也就是使用兩個TTL:一個軟TTL和一個硬TTL。用戶端将嘗試根據軟TTL重新整理緩存項,但如果下遊服務不可用或因其他原因未響應請求,則将繼續使用現有的緩存資料,直至達到硬TTL。

(九)緩存淘汰

緩存回收政策可以分為:

  • 基于時間:當某緩存超過生存時間時,則進行緩存回收。或者當某緩存最後被通路後超過某時間仍然沒有被通路,則進行緩存回收。
  • 基于空間:當緩存超過某大小時,則進行緩存回收。
  • 基于容量:當緩存超過某存儲條數時,則進行緩存回收。

緩存淘汰算法有:

FIFO(First In First Out):優先淘汰最早進入被緩存的資料。FIFO 實作十分簡單,但一般來說它并不是優秀的淘汰政策,越是頻繁被用到的資料,往往會越早被存入緩存之中。如果采用這種淘汰政策,很可能會大幅降低緩存的命中率。

LRU(Least Recent Used):優先淘汰最久未被使用通路過的資料。LRU通常會采用HashMap加雙端連結清單的雙重結構來實作(如 LinkedHashMap),以HashMap來提供通路接口,保證常量時間複雜度的讀取性能,以LinkedList的連結清單元素順序來表示資料的時間順序,每次緩存命中時把傳回對象調整到LinkedList開頭,每次緩存淘汰時從連結清單末端開始清理資料。對大多數的緩存場景來說,LRU都明顯要比FIFO政策合理,尤其适合用來處理短時間内頻繁通路的熱點對象。但相反,它的問題是如果一些熱點資料在系統中經常被頻繁通路,但最近一段時間因為某種原因未被通路過,此時這些熱點資料依然要面臨淘汰的命運,LRU依然可能錯誤淘汰價值更高的資料。

LFU(Least Frequently Used):優先淘汰最不經常使用的資料。LFU會給每個資料添加一個通路計數器,每通路一次就加1,需要淘汰時就清理計數器數值最小的那批資料。LFU可以解決上面LRU中熱點資料間隔一段時間不通路就被淘汰的問題,但同時它又引入了兩個新的問題,首先是需要對每個緩存的資料專門去維護一個計數器,每次通路都要更新,多線程并發更新要加鎖就會帶來高昂的開銷;另一個問題是不便于處理随時間變化的熱度變化,譬如某個曾經頻繁通路的資料現在不需要了,它也很難自動被清理出緩存。

緩存淘汰政策直接影響緩存的命中率,沒有一種政策是完美的、能夠滿足全部系統所需的。不過,随着淘汰算法的發展,近年來的确出現了許多相對性能要更好的,也更為複雜的新算法。以LFU分支為例,針對它存在的兩個問題,近年來提出的TinyLFU和W-TinyLFU算法就往往會有更好的效果。

TinyLFU(Tiny Least Frequently Used):TinyLFU是LFU的改進版本。為了緩解LFU每次通路都要修改計數器所帶來的性能負擔,TinyLFU 會首先采用Sketch對通路資料進行分析,所謂Sketch是統計學上的概念,指用少量的樣本資料來估計全體資料的特征,這種做法顯然犧牲了一定程度的準确性,但是隻要樣本資料與全體資料具有相同的機率分布,Sketch得出的結論仍不失為一種高效與準确之間權衡的有效結論。借助Count–Min Sketch算法(可視為布隆過濾器的一種等價變種結構),TinyLFU可以用相對小得多的記錄頻率和空間來近似地找出緩存中的低價值資料。為了解決 LFU 不便于處理随時間變化的熱度變化問題,TinyLFU采用了基于“滑動時間窗”的熱度衰減算法,簡單了解就是每隔一段時間,便會把計數器的數值減半,以此解決“舊熱點”資料難以清除的問題。

W-TinyLFU(Windows-TinyLFU):W-TinyLFU又是TinyLFU的改進版本。TinyLFU在實作減少計數器維護頻率的同時,也帶來了無法很好地應對稀疏突發通路的問題,所謂稀疏突發通路是指有一些絕對頻率較小,但突發通路頻率很高的資料,譬如某些運維性質的任務,也許一天、一周隻會在特定時間運作一次,其餘時間都不會用到,此時 TinyLFU就很難讓這類元素通過Sketch的過濾,因為它們無法在運作期間積累到足夠高的頻率。應對短時間的突發通路是LRU的強項,W-TinyLFU就結合了LRU和LFU兩者的優點,從整體上看是它是LFU政策,從局部實作上看又是LRU政策。具體做法是将新記錄暫時放入一個名為Window Cache的前端LRU緩存裡面,讓這些對象可以在Window Cache中累積熱度,如果能通過TinyLFU的過濾器,再進入名為Main Cache的主緩存中存儲,主緩存根據資料的通路頻繁程度分為不同的段(LFU政策,實際上W-TinyLFU隻分了兩段),但單獨某一段局部來看又是基于LRU政策去實作的(稱為Segmented LRU)。每目前一段緩存滿了之後,會将低價值資料淘汰到後一段中去存儲,直至最後一段也滿了之後,該資料就徹底清理出緩存。

另外還有兩種進階淘汰政策ARC(Adaptive Replacement Cache)、LIRS(Low Inter-Reference Recency Set),大家有興趣可以再深入閱讀,對其他緩存淘汰政策感興趣的讀者也可以參考維基百科中對Cache Replacement Policies的介紹。

(十)分布式緩存

相比起緩存資料在程序記憶體中讀寫的速度,一旦涉及網絡通路,由網絡傳輸、資料複制、序列化和反序列化等操作所導緻的延遲要比記憶體通路高得多,是以對分布式緩存來說,處理與網絡有相關的操作是對吞吐量影響更大的因素,往往也是比淘汰政策、擴充功能更重要的關注點,這決定了盡管也有Ehcache、Infinispan這類能同時支援分布式部署和程序内嵌部署的緩存方案,但通常程序内緩存和分布式緩存選型時會有完全不同的候選對象及考察點。我們決定使用哪種分布式緩存前,首先必須确認自己需求是什麼?

從通路的角度來說,如果是頻繁更新但甚少讀取的資料,通常是不會有人把它拿去做緩存的,因為這樣做沒有收益。對于甚少更新但頻繁讀取的資料,理論上更适合做複制式緩存;對于更新和讀取都較為頻繁的資料,理論上就更适合做集中式緩存。筆者簡要介紹這兩種分布式緩存形式的差别與代表性産品:

複制式緩存:複制式緩存可以看作是“能夠支援分布式的程序内緩存”,它的工作原理與Session複制類似。緩存中所有資料在分布式叢集的每個節點裡面都存在有一份副本,讀取資料時無須網絡通路,直接從目前節點的程序記憶體中傳回,理論上可以做到與程序内緩存一樣高的讀取性能;當資料發生變化時,就必須遵循複制協定,将變更同步到叢集的每個節點中,複制性能随着節點的增加呈現平方級下降,變更資料的代價十分高昂。複制式緩存的代表是JBossCache,這是JBoss針對企業級叢集設計的緩存方案,支援JTA 事務,依靠JGroup進行叢集節點間資料同步。以JBossCache為典型的複制式緩存曾有一段短暫的興盛期,但今天基本上已經很難再見到使用這種緩存形式的大型資訊系統了,JBossCache被淘汰的主要原因是寫入性能實在差到不堪入目的程度,它在小規模叢集中同步資料尚算差強人意,但在大規模叢集下,很容易就因網絡同步的速度跟不上寫入速度,進而導緻在記憶體中累計大量待重發對象,最終引發OutOfMemory崩潰。如果對JBossCache沒有足夠了解的話,稍有不慎就要被埋進坑裡。為了緩解複制式同步的寫入效率問題,JBossCache的繼任者Infinispan提供了另一種分布式同步模式(這種同步模式的名字就叫做“分布式”),允許使用者配置資料需要複制的副本數量,譬如叢集中有八個節點,可以要求每個資料隻儲存四份副本,此時,緩存的總容量相當于是傳統複制模式的一倍,如果要通路的資料在本地緩存中沒有存儲,Infinispan完全有能力感覺網絡的拓撲結構,知道應該到哪些節點中尋找資料。

集中式緩存:集中式緩存是目前分布式緩存的主流形式,集中式緩存的讀、寫都需要網絡通路,其好處是不會随着叢集節點數量的增加而産生額外的負擔,其壞處自然是讀、寫都不再可能達到程序内緩存那樣的高性能。集中式緩存還有一個必須提到的關鍵特點,它與使用緩存的應用分處在獨立的程序空間中,其好處是它能夠為異構語言提供服務,譬如用C語言編寫的Memcached完全可以毫無障礙地為Java語言編寫的應用提供緩存服務;但其壞處是如果要緩存對象等複雜類型的話,基本上就隻能靠序列化來支撐具體語言的類型系統(支援Hash類型的緩存,可以部分模拟對象類型),不僅有序列化的成本,還很容易導緻傳輸成本也顯著增加。舉個例子,假設某個有100個字段的大對象變更了其中1個字段的值,通常緩存也不得不把整個對象所有内容重新序列化傳輸出去才能實作更新,是以,一般集中式緩存更提倡直接緩存原始資料類型而不是對象。相比之下,JBossCache通過它的位元組碼自審(Introspection)功能和樹狀存儲結構(TreeCache),做到了自動跟蹤、處理對象的部分變動,使用者修改了對象中哪些字段的資料,緩存就隻會同步對象中真正變更那部分資料。如今Redis廣為流行,基本上已經打敗了Memcached及其他集中式緩存架構,成為集中式緩存的首選,甚至可以說成為了分布式緩存的實質上的首選,幾乎到了不必管讀取、寫入哪種操作更頻繁,都可以無腦上Redis的程度。也因如此,之前說到哪些資料适合用複制式緩存、哪些資料适合集中式緩存時,筆者都在開頭加了個拗口的“理論上”。盡管Redis最初設計的本意是NoSQL資料庫而不是專門用來做緩存的,可今天它确實已經成為許多分布式系統中無可或缺的基礎設施,廣泛用作緩存的實作方案。

從資料一緻性角度說,緩存本身也有叢集部署的需求,理論上你應該認真考慮一下是否能接受不同節點取到的緩存資料有可能存在差異。譬如剛剛放入緩存中的資料,另外一個節點馬上通路發現未能讀到;剛剛更新緩存中的資料,另外一個節點通路在短時間内讀取到的仍是舊的資料,等等。根據分布式緩存叢集是否能保證資料一緻性,可以将它分為AP和CP兩種類型。此處又一次出現了“理論上”,是因為我們實際開發中通常不太會把追求強一緻性的資料使用緩存來處理,可以這樣做,但是沒必要(可類比MESI等緩存一緻性協定)。譬如,Redis叢集就是典型的AP式,有着高性能高可用等特點,卻并不保證強一緻性。而能夠保證強一緻性的ZooKeeper、Doozerd、Etcd等分布式協調架構,通常不會有人将它們當為“緩存架構”來使用,這些分布式協調架構的吞吐量相對Redis來說是非常有限的。不過ZooKeeper、Doozerd、Etcd倒是常與Redis和其他分布式緩存搭配工作,用來實作其中的通知、協調、隊列、分布式鎖等功能。

分布式緩存與程序内緩存各有所長,也有各有局限,它們是互補而非競争的關系,如有需要,完全可以同時把程序内緩存和分布式緩存互相搭配,構成透明多級緩存(Transparent Multilevel Cache,TMC),如下圖所示。先不考慮“透明”的話,多級緩存是很好了解的,使用程序内緩存做一級緩存,分布式緩存做二級緩存,如果能在一級緩存中查詢到結果就直接傳回,否則便到二級緩存中去查詢,再将二級緩存中的結果回填到一級緩存,以後再通路該資料就沒有網絡請求了。如果二級緩存也查詢不到,就發起對最終資料源的查詢,将結果回填到一、二級緩存中去。

「計算機基礎」兩萬字詳解幫你系統掌握緩存!(圖文并茂)

盡管多級緩存結合了程序内緩存和分布式緩存的優點,但它的代碼侵入性較大,需要由開發者承擔多次查詢、多次回填的工作,也不便于管理,如逾時、重新整理等政策都要設定多遍,資料更新更是麻煩,很容易會出現各個節點的一級緩存、以及二級緩存裡資料互相不一緻的問題。必須“透明”地解決以上問題,多級緩存才具有實用的價值。一種常見的設計原則是變更以分布式緩存中的資料為準,通路以程序内緩存的資料優先。大緻做法是當資料發生變動時,在叢集内發送推送通知(簡單點的話可采用Redis的PUB/SUB,求嚴謹的話引入ZooKeeper或Etcd來處理),讓各個節點的一級緩存自動失效掉相應資料。當通路緩存時,提供統一封裝好的一、二級緩存聯合查詢接口,接口外部是隻查詢一次,接口内部自動實作優先查詢一級緩存,未擷取到資料再自動查詢二級緩存的邏輯。

附加解釋下CAP定理:CAP是分布式計算領域所公認的著名定理。其描述了一個分布式的系統中,涉及共享資料問題時,以下三個特性最多隻能同時滿足其中兩個:

  • 一緻性(Consistency):代表資料在任何時刻、任何分布式節點中所看到的都是符合預期的。
  • 可用性(Availability):代表系統不間斷地提供服務的能力,了解可用性要先了解與其密切相關兩個名額:可靠性(Reliability)和可維護性(Serviceability)。可靠性使用平均無故障時間(Mean Time Between Failure,MTBF)來度量;可維護性使用平均可修複時間(Mean Time To Repair,MTTR)來度量。可用性衡量系統可以正常使用的時間與總時間之比,其表征為:A=MTBF/(MTBF+MTTR),即可用性是由可靠性和可維護性計算得出的比例值,譬如99.9999%可用,即代表平均年故障修複時間為32秒。
  • 分區容忍性(Partition Tolerance):代表分布式環境中部分節點因網絡原因而彼此失聯後,即與其他節點形成“網絡分區”時,系統仍能正确地提供服務的能力。

七、如何設計緩存(how)

在進行緩存結構設計的時候,需要考慮的點有很多:

對緩存帶來的價值持懷疑态度:從成本、延遲和可用性等方面來評估引入緩存的合理性,并仔細評估引入緩存帶來的額外風險和收益。

業務流量量級評估:對于低并發低流量的應用而言,引入緩存并不會帶來性能的顯著提升,反而會帶來應用的複雜度以及極高的運維成本。也不是任何資料都需要使用緩存,比如圖檔視訊等檔案使用分布式檔案系統更合适而不是緩存。是以,在引入緩存前,需要對目前業務的流量進行評估,在高并發大流量的業務場景中引入緩存相對而言收益會更高;

緩存元件選型:緩存應用有很多如Redis、Memcached以及tair等等,針對每一種分布式緩存應用的優缺點以及适用範圍、記憶體效率、運維成本甚至團隊開發人員的知識結構都需要了解,才能做好技術選型;

緩存相關名額評估:在引入緩存前,需要着重評估value大小、峰值QPS、緩存記憶體空間、緩存命中率、過期時間、過期淘汰政策、讀寫更新政策、key值分布路由政策、資料一緻性方案等多個因素,要做到心中有數;

緩存高可用架構:分布式緩存要高可用,比如緩存的叢集設計、主從同步方案的設計等等,隻有緩存足夠可靠,才能服務于業務系統,為業務帶來價值;

完善的監控平台:當緩存投入生産環境後,需要有一套監控系統能夠顯式的觀測緩存系統的運作情況,才能更早的發現問題,同時對于預估不足的非預期熱點資料,也需要熱點發現系統去解決非預期的熱點資料緩存問題。

緩存最近通路原則:将緩存資料放在離使用者最近的地方,無疑會極大的提升響應的速度,這也是多級緩存設計的核心思想。

考慮緩存資料的安全問題。包括加密、與外部緩存隊列通信時的傳輸安全性以及緩存投毒攻擊和側信道攻擊的影響。

文章來源:梁其用_騰訊背景開發工程師_https://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&mid=2247534557&idx=1&sn=576bc35c36024e8e2b35e0fb28acb3ca&chksm=eaa85d8ddddfd49b641b76a9f5bdb5c40869f3e7d621c2687fb143ec6f014231bcc75368d441&scene=21#wechat_redirect

繼續閱讀