天天看點

網際網路公司通用緩存架構

<b></b>

在網際網路高速發展的今天,緩存技術被廣泛地應用。無論業内還是業外,隻要是提到性能問題,大家都會脫口而出“用緩存解決”。

這種說法帶有片面性,甚至是一知半解,但是作為專業人士的我們,需要對緩存有更深、更廣的了解。

緩存技術存在于應用場景的方方面面。從浏覽器請求,到反向代理伺服器,從程序内緩存到分布式緩存。其中緩存政策,算法也是層出不窮,今天就帶大家走進緩存。

緩存對于每個開發者來說是相當熟悉了,為了提高程式的性能我們會去加緩存,但是在什麼地方加緩存,如何加緩存呢?

假設一個網站,需要提高性能,緩存可以放在浏覽器,可以放在反向代理伺服器,還可以放在應用程式程序内,同時可以放在分布式緩存系統中。

網際網路公司通用緩存架構

從使用者請求資料到資料傳回,資料經過了浏覽器,cdn,代理伺服器,應用伺服器,以及資料庫各個環節。每個環節都可以運用緩存技術。

從浏覽器/用戶端開始請求資料,通過 http 配合 cdn 擷取資料的變更情況,到達代理伺服器(nginx)可以通過反向代理擷取靜态資源。

再往下來到應用伺服器可以通過程序内(堆内)緩存,分布式緩存等遞進的方式擷取資料。如果以上所有緩存都沒有命中資料,才會回源到資料庫。

緩存的請求順序是:使用者請求 → http 緩存 → cdn 緩存 → 代理伺服器緩存 → 程序内緩存 → 分布式緩存 → 資料庫。

看來在技術的架構每個環節都可以加入緩存,看看每個環節是如何應用緩存技術的。

一、 http緩存

當使用者通過浏覽器請求伺服器的時候,會發起 http 請求,如果對每次 http 請求進行緩存,那麼可以減少應用伺服器的壓力。

當第一次請求的時候,浏覽器本地緩存庫沒有緩存資料,會從伺服器取資料,并且放到浏覽器的緩存庫中,下次再進行請求的時候會根據緩存的政策來讀取本地或者服務的資訊。

網際網路公司通用緩存架構

一般資訊的傳遞通過 http 請求頭 header 來傳遞。目前比較常見的緩存方式有兩種,分别是:強制緩存和對比緩存

當浏覽器本地緩存庫儲存了緩存資訊,在緩存資料未失效的情況下,可以直接使用緩存資料。否則就需要重新擷取資料。

這種緩存機制看上去比較直接,那麼如何判斷緩存資料是否失效呢?這裡需要關注 http header 中的兩個字段 expires 和 cache-control。

expires 為服務端傳回的過期時間,用戶端第一次請求伺服器,伺服器會傳回資源的過期時間。如果用戶端再次請求伺服器,會把請求時間與過期時間做比較。

如果請求時間小于過期時間,那麼說明緩存沒有過期,則可以直接使用本地緩存庫的資訊。

反之,說明資料已經過期,必須從伺服器重新擷取資訊,擷取完畢又會更新最新的過期時間。

這種方式在 http 1.0 用的比較多,到了 http 1.1 會使用 cache-control 替代。

cache-control 中有個 max-age 屬性,機關是秒,用來表示緩存内容在用戶端的過期時間。

例如:max-age 是 60 秒,目前緩存沒有資料,用戶端第一次請求完後,将資料放入本地緩存。

那麼在 60 秒以内用戶端再發送請求,都不會請求應用伺服器,而是從本地緩存中直接傳回資料。如果兩次請求相隔時間超過了 60 秒,那麼就需要通過伺服器擷取資料。

需要對比前後兩次的緩存标志來判斷是否使用緩存。浏覽器第一次請求時,伺服器會将緩存辨別與資料一起傳回,浏覽器将二者備份至本地緩存庫中。浏覽器再次請求時,将備份的緩存辨別發送給伺服器。

伺服器根據緩存辨別進行判斷,如果判斷資料沒有發生變化,把判斷成功的 304 狀态碼發給浏覽器。

這時浏覽器就可以使用緩存的資料來。伺服器傳回的就隻是 header,不包含 body。

下面介紹兩種辨別規則:

在用戶端第一次請求的時候,伺服器會傳回資源最後的修改時間,記作 last-modified。用戶端将這個字段連同資源緩存起來。

last-modified 被儲存以後,在下次請求時會以 last-modified-since 字段被發送。

"&gt;

當用戶端再次請求伺服器時,會把 last-modified 連同請求的資源一起發給伺服器,這時 last-modified 會被命名為 if-modified-since,存放的内容都是一樣的。

伺服器收到請求,會把 if-modified-since 字段與伺服器上儲存的 last-modified 字段作比較:

若伺服器上的 last-modified 最後修改時間大于請求的 if-modified-since,說明資源被改動過,就會把資源(包括 header+body)重新傳回給浏覽器,同時傳回狀态碼 200。

若資源的最後修改時間小于或等于 if-modified-since,說明資源沒有改動過,隻會傳回 header,并且傳回狀态碼 304。浏覽器接受到這個消息就可以使用本地緩存庫的資料。

注意:last-modified 和 if-modified-since 指的是同一個值,隻是在用戶端和伺服器端的叫法不同。

用戶端第一次請求的時候,伺服器會給每個資源生成一個 etag 标記。這個 etag 是根據每個資源生成的唯一 hash 串,資源如何發生變化 etag 随之更改,之後将這個 etag 傳回給用戶端,用戶端把請求的資源和 etag 都緩存到本地。

etag 被儲存以後,在下次請求時會當作 if-none-match 字段被發送出去。

在浏覽器第二次請求伺服器相同資源時,會把資源對應的 etag 一并發送給伺服器。在請求時 etag 轉化成 if-none-match,但其内容不變。

伺服器收到請求後,會把 if-none-match 與伺服器上資源的 etag 進行比較:

如果不一緻,說明資源被改動過,則傳回資源(header+body),傳回狀态碼 200。

如果一緻,說明資源沒有被改過,則傳回 header,傳回狀态碼 304。浏覽器接受到這個消息就可以使用本地緩存庫的資料。

注意:etag 和 if-none-match 指的是同一個值,隻是在用戶端和伺服器端的叫法不同。

二、cdn 緩存http 緩存主要是對靜态資料進行緩存,把從伺服器拿到的資料緩存到用戶端/浏覽器。

如果在用戶端和伺服器之間再加上一層 cdn,可以讓 cdn 為應用伺服器提供緩存,如果在 cdn 上緩存,就不用再請求應用伺服器了。并且 http 緩存提到的兩種政策同樣可以在 cdn 伺服器執行。

cdn 的全稱是 content delivery network,即内容分發網絡。

讓我們來看看它是如何工作的吧:

用戶端發送 url 給 dns 伺服器。

dns 通過域名解析,把請求指向 cdn 網絡中的 dns 負載均衡器。

dns 負載均衡器将最近 cdn 節點的 ip 告訴 dns,dns 告之用戶端最新 cdn 節點的 ip。

用戶端請求最近的 cdn 節點。

cdn 節點從應用伺服器擷取資源傳回給用戶端,同時将靜态資訊緩存。注意:用戶端下次互動的對象就是 cdn 緩存了,cdn 可以和應用伺服器同步緩存資訊。

cdn 接受用戶端的請求,它就是離用戶端最近的伺服器,它後面會連結多台伺服器,起到了緩存和負載均衡的作用。

三、 負載均衡緩存說完用戶端(http)緩存和 cdn 緩存,我們離應用服務越來越近了,在到達應用服務之前,請求還要經過負載均衡器。

雖說它的主要工作是對應用伺服器進行負載均衡,但是它也可以作緩存。可以把一些修改頻率不高的資料緩存在這裡,例如:使用者資訊,配置資訊。通過服務定期重新整理這個緩存就行了。

網際網路公司通用緩存架構

以 nginx 為例,我們看看它是如何工作的:

使用者請求在達到應用伺服器之前,會先通路 nginx 負載均衡器,如果發現有緩存資訊,直接傳回給使用者。

如果沒有發現緩存資訊,nginx 回源到應用伺服器擷取資訊。

另外,有一個緩存更新服務,定期把應用伺服器中相對穩定的資訊更新到 nginx 本地緩存中。

四、程序内緩存通過了用戶端,cdn,負載均衡器,我們終于來到了應用伺服器。應用伺服器上部署着一個個應用,這些應用以程序的方式運作着,那麼在程序中的緩存是怎樣的呢?

程序内緩存又叫托管堆緩存,以apc為例,同時會受到托管堆回收算法的影響。

由于其運作在記憶體中,對資料的響應速度很快,通常我們會把熱點資料放在這裡。

在程序内緩存沒有命中的時候,我們會去搜尋程序外的緩存或者分布式緩存。這種緩存的好處是沒有序列化和反序列化,是最快的緩存。缺點是緩存的空間不能太大,對垃圾回收器的性能有影響。

這裡我們需要關注幾個緩存的回收政策,具體的實作架構的回收政策會有所不同,但大緻的思路都是一緻的:

fifo(first in first out):先進先出算法,最先放入緩存的資料最先被移除。

lru(least recently used):最近最少使用算法,把最久沒有使用過的資料移除緩存。

lfu(least frequently used):最不常用算法,在一段時間内使用頻率最小的資料被移除緩存。

在分布式架構的今天,多應用中如果采用程序内緩存會存在資料一緻性的問題。

這裡推薦兩個方案:

消息隊列修改方案

timer 修改方案

應用在修改完自身緩存資料和資料庫資料之後,給消息隊列發送資料變化通知,其他應用訂閱了消息通知,在收到通知的時候修改緩存資料。

網際網路公司通用緩存架構

為了避免耦合,降低複雜性,對“實時一緻性”不敏感的情況下。每個應用都會啟動一個 timer,定時從資料庫拉取最新的資料,更新緩存。

不過在有的應用更新資料庫後,其他節點通過 timer 擷取資料之間,會讀到髒資料。這裡需要控制好 timer 的頻率,以及應用與對實時性要求不高的場景。

網際網路公司通用緩存架構

程序内緩存有哪些使用場景呢?

場景一:隻讀資料,可以考慮在程序啟動時加載到記憶體。當然,把資料加載到類似 redis 這樣的程序外緩存服務也能解決這類問題。

場景二:高并發,可以考慮使用程序内緩存,例如:秒殺。

五、分布式緩存說完程序内緩存,自然就過度到程序外緩存了。與程序内緩存不同,程序外緩存在應用運作的程序之外,它擁有更大的緩存容量,并且可以部署到不同的實體節點,通常會用分布式緩存的方式實作。

分布式緩存是與應用分離的緩存服務,最大的特點是,自身是一個獨立的應用/服務,與本地應用隔離,多個應用可直接共享一個或者多個緩存應用/服務。

網際網路公司通用緩存架構

既然是分布式緩存,緩存的資料會分布到不同的緩存節點上,每個緩存節點緩存的資料大小通常也是有限制的。

資料被緩存到不同的節點,為了能友善的通路這些節點,需要引入緩存代理,類似 twemproxy。他會幫助請求找到對應的緩存節點。

同時如果緩存節點增加了,這個代理也會隻能識别并且把新的緩存資料分片到新的節點,做橫向的擴充。

為了提高緩存的可用性,會在原有的緩存節點上加入 master/slave 的設計。當緩存資料寫入 master 節點的時候,會同時同步一份到 slave 節點。

一旦 master 節點失效,可以通過代理直接切換到 slave 節點,這時 slave 節點就變成了 master 節點,保證緩存的正常工作。

每個緩存節點還會提供緩存過期的機制,并且會把緩存内容定期以快照的方式儲存到檔案上,友善緩存崩潰之後啟動預熱加載。

當緩存做成分布式的時候,資料會根據一定的規律配置設定到每個緩存應用/服務上。

如果我們把這些緩存應用/服務叫做緩存節點,每個節點一般都可以緩存一定容量的資料,例如:redis 一個節點可以緩存 2g 的資料。

如果需要緩存的資料量比較大就需要擴充多個緩存節點來實作,這麼多的緩存節點,用戶端的請求不知道通路哪個節點怎麼辦?緩存的資料又如何放到這些節點上?

緩存代理服務已經幫我們解決這些問題了,例如:twemproxy 不但可以幫助緩存路由,同時可以管理緩存節點。

這裡有介紹三種緩存資料分片的算法,有了這些算法緩存代理就可以友善的找到分片的資料了。

hash 表是最常見的資料結構,實作方式是,對資料記錄的關鍵值進行 hash,然後再對需要分片的緩存節點個數進行取模得到的餘數進行資料配置設定。

例如:有三條記錄資料分别是 r1,r2,r3。他們的 id 分别是 01,02,03,假設對這三個記錄的 id 作為關鍵值進行 hash 算法之後的結果依舊是 01,02,03。

我們想把這三條資料放到三個緩存節點中,可以把這個結果分别對 3 這個數字取模得到餘數,這個餘數就是這三條記錄分别放置的緩存節點。

hash 算法是某種程度上的平均放置,政策比較簡單,如果要增加緩存節點,對已經存在的資料會有較大的變動。

一緻性 hash 是将資料按照特征值映射到一個首尾相接的 hash 環上,同時也将緩存節點映射到這個環上。

如果要緩存資料,通過資料的關鍵值(key)在環上找到自己存放的位置。這些資料按照自身的 id 取 hash 之後得到的值按照順序在環上排列。

網際網路公司通用緩存架構

如果這個時候要插入一條新的資料其 id 是 115,那麼就應該插入到如下圖的位置。

網際網路公司通用緩存架構

同理如果要增加一個緩存節點 n4 150,也可以放到如下圖的位置。

網際網路公司通用緩存架構

這種算法對于增加緩存資料,和緩存節點的開銷相對比較小。

這種方式是按照關鍵值(例如 id)将資料劃分成不同的區間,每個緩存節點負責一個或者多個區間。跟一緻性哈希有點像。

例如:存在三個緩存節點分别是 n1,n2,n3。他們用來存放資料的區間分别是,n1(0, 100], n2(100, 200], n3(300, 400]。

那麼資料根據自己 id 作為關鍵字做 hash 以後的結果就會分别對應放到這幾個區域裡面了。