當前位置: 妍妍網 > 碼農

微服務 + 多級緩存,效能起飛!

2024-05-17碼農

點選關註公眾號,Java幹貨 及時送達 👇

今天我們來聊聊緩存這個話題,看看在微服務環境下如何設計有效的多級緩存架構。主要涉及三方面內容:

  • Web 套用的客戶端緩存;

  • 套用層靜態資源緩存;

  • 服務層多級緩存。

  • 首先,咱們先講解微服務架構的多級緩存設計。

    微服務架構中的多級緩存設計

    提到緩存,想必每一位軟體工程師都不陌生,它是目前架構設計中提高效能最直接的方式。這裏我們舉個例子:

    Redis 緩存

    假設應用程式將原始數據儲存在 MySQL 資料庫中。眾所周知 MySQL 資料庫會將數據儲存在硬碟以防止掉電遺失,但是受制於硬碟的物理設計,即便是目前效能最好的企業級 SSD 硬碟,也比記憶體的這種高速裝置 IO 層面差一個數量級,而以淘寶、京東這種電商為代表的互聯網套用,都是典型的 「讀多寫少」 的場景,因此我們需要在設計上進行數據的讀寫分離,在數據寫入時直接落盤處理,而占比超過 90% 的數據讀取操作時則從以 Redis 為代表的記憶體 NoSQL 資料庫提取數據,利用記憶體的高吞吐瞬間完成數據提取,這裏 Redis 的作用就是我們常說的緩存。

    當然,緩存可不只有用記憶體替代硬碟這一種形式,在分布式架構下緩存在每一層都有自己的設計,下面咱們透過這個微服務的多級緩存架構圖為主線進行講解。

    X 緩存多級緩存架構縱覽

    這張圖從上到下包含四層,分別為: 客戶端、套用層、服務層以及數據層。

    客戶端緩存

    X 商城客戶端為瀏覽器,在瀏覽器層面我們主要是對 HTML 中的圖片、CSS、JS、字型這些靜態資源進行緩存。

    瀏覽器緩存

    我們以百度 Logo 圖片為例,百度在 HTTP 透過 Expires 響應頭控制靜態圖片的有效期。Expires 代表過期時間。當前百度 Logo 的過期時間為 2031 年 2 月 8 日 9 時 26 分 31 秒。在這個時間段內,瀏覽器會將圖片以檔形式緩存在本地,再次存取時會看到「from disk cache」的提示,此時瀏覽器不再產生與伺服器的實際請求,會從本地直接讀取緩存圖片。透過在瀏覽器端設定 Expires 可以在很大程度減少重復請求靜態資源帶來的頻寬損耗,這在高並行 Web 套用中是基礎而重要的設定。

    套用層緩存

    那 Expires 到底在哪裏進行設定呢?對於瀏覽器來說它只是客戶端,只負責讀取Expires響應頭,對於 Expires 要在套用層,也就是 CDN 與 Nginx 中進行設定。

    CDN 內容分發網路

    CDN 全稱是 Content Delivery Network,即內容分發網路,是互聯網靜態資源分發的主要技術手段。

    CDN 內容分發網路

    中國幅員遼闊,從北京到上海就有上千公裏,如果大量的上海使用者同時要存取千裏之外的北京伺服器的資源,這麽長的通訊必然帶來高延遲與更多不可控因素影響數據傳輸,如果有某種機制允許將北京的靜態檔緩存到上海的伺服器,上海使用者自動就近存取伺服器獲取資源,這樣便可很大程度降低網路延遲,進而提高系統的可用性。而剛才提到的分布式緩存技術就是我們常提到的CDN(內容分發網路)。

    對於廣域的互聯網套用,CDN 幾乎是必需的基礎設施,它有效解決了頻寬集中占用以及數據分發的問題。像 Web 頁面中的圖片、音視訊、CSS、JS 這些靜態資源,都可以透過 CDN 伺服器就近獲取。

    CDN 技術的核心是「智慧 DNS」,智慧 DNS 會根據使用者的 IP 地址自動確定就近存取 CDN 節點,咱們以下圖為例:

    CDN 執行流程

    以某上海使用者的瀏覽器要存取商城首頁廣告區的 banner.jpg 檔,瀏覽器透過服務商提供的智慧 DNS 服務,將請求自動轉發到商城在上海地區準備的 CDN 伺服器,上海 CDN 收到請求後首先檢查本機是否已緩存過 banner.jpg,如果檔已存在便直接將圖片數據返回給客戶端;如果沒有緩存過,則回源到北京的源數據節點,將 banner.jpg 檔抽取並緩存到上海伺服器,最後上海 CDN 節點再將本機的 banner.jpg 返回給客戶端。對於 banner.jpg 來說,第一次存取後上海 CDN 節點已緩存該檔,則之後的緩存有效期內所有後續存取由上海 CDN 直接提供。與之類似的,商城套用可以在重要城市搭建 CDN 節點,這樣原本集中被發往北京伺服器的請求就被分攤到 CDN 節點,這也直接降低了北京機房的頻寬壓力。

    在互聯網套用中,因為 CDN 涉及多地域多節點組網,前期投入成本較高,更多的中小型軟體公司通常會選擇阿裏雲、騰訊雲等大廠提供的 CDN 服務,透過按需付費的方式降低硬體成本。而這些服務商又會為 CDN 賦予額外的能力,比如阿裏雲、騰訊雲 CDN 除了緩存檔之外,還提供了管理後台能為響應賦予額外的響應頭。如下所示在阿裏雲 CDN 後台,就額外設定了 Cache-Control 響應頭代表緩存有效期為 1 小時。這裏我們額外提一下 Expires 與的 Cache-Control 的區別,Expires 是指定具體某個時間點緩存到期,而 Cache-Control 則代表緩存的有效期是多長時間。Expires 設定時間,Cache-Control 設定時長,根據業務場景不同可以使用不同的響應頭。

    阿裏雲自訂響應頭

    Nginx 緩存管理

    說完 CDN,下面再來聊一下 Nginx。Nginx 是一款開源的、跨平台的高效能 Web 伺服器,它有著高效能,穩定性好,配置簡單,模組結構化,資源消耗低的優點。同時支持反向代理、負載均衡、緩存的功能。Nginx 是 Web 套用架構中的常客,例如後端 Tomcat 集群便可透過增加 Nginx 前置做軟負載均衡,為套用提供高可用特性。

    Nginx 代理伺服器

    在互聯網套用中,使用者分布在全國各地,對資源的響應速度與頻寬要求較高,因此部署 CDN 是十分有必要的。但在更多的企業套用中,其實大部份的企業使用者都分布在指定的辦公區域或者相對固定的場所,再加上並行使用者相對較少,其實並不需要額外部署 CDN 這種重量級解決方案。在架構中只需要部署 Nginx 伺服器,利用 Nginx 內建的靜態資源緩存與壓縮功能便可勝任大多數企業套用場景。

    在 Nginx 中內建將後端套用中圖片、CSS、JS 等靜態資源緩存功能,我們只需在 Nginx 的核心配置 nginx.conf 中增加下面的片段,便可對後端的靜態資源進行緩存,關鍵配置我已做好註釋,同學們可以直接使用。

    # 設定緩存目錄
    # levels代表采用1:2也就是兩級目錄的形式保存緩存檔(靜態資源css、js)
    # keys_zone定義緩存的名稱及記憶體的使用,名稱為babytun-cache ,在記憶體中開始100m交換空間
    # inactive=7d 如果某個緩存檔超過7天沒有被存取,則刪除
    # max_size=20g;代表設定資料夾最大不能超過20g,超過後會自動將存取頻度(命中率)最低的緩存檔刪除
    proxy_cache_path d:/nginx-cache levels=1:2 keys_zone=babytun-cache:100m inactive=7d max_size=20g;
    #配置xmall後端伺服器的權重負載均衡策略
    upstream xmall {
    server 192.168.31.181 weight=5 max_fails=1 fail_timeout=3s;
    server 192.168.31.182 weight=2;
    server 192.168.31.183 weight=1;
    server 192.168.31.184 weight=2;
    }
    server {
    #nginx透過80埠提供Web服務
     listen 80;
    # 開啟靜態資源緩存
    # 利用正規表式匹配URL,匹配成功的則執行內部邏輯
    # ~* 代表URL匹配不區分大小寫
     location ~* \.(gif|jpg|css|png|js|woff|html)(.*){
    # 配置代理轉發規則
    proxy_pass http://xmall;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_cache xmall-cache;
    #如果靜態資源響應狀態碼為200(成功) 302(暫時性重新導向)時 緩存檔有效期1天
    proxy_cache_valid 200 302 24h;
    #301(永久性重新導向)緩存保存5天
    proxy_cache_valid 301 5d;
    #其他情況
    proxy_cache_valid any 5m;
    #設定瀏覽器端緩存過期時間90天
    expires 90d;
     }
    #使用xmall伺服器池進行後端處理
     location /{
    proxy_pass http://xmall; 
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     }
    }


    增加上面配置後,每一次透過 Nginx 存取套用中新的靜態檔時,在 Nginx 服務的緩存目錄便會生成緩存檔,在緩存有效期內該靜態資源的請求便不再送到後端伺服器,而直接由 Nginx 讀取本地緩存並返回。

    Nginx 本地緩存

    服務層緩存

    在前面無論是 CDN 還是 Nginx,都是對 Web 套用中的靜態資原始檔進行緩存。但後端套用與服務更多的是存取介面與數據,對於這些物件我們如何利用緩存技術進行效能最佳化呢?對於後端套用與服務的緩存可以按部署方式分為 行程內緩存 分布式緩存 服務。

    行程內緩存

    所謂行程內緩存,就是在套用中開辟的一塊記憶體空間,數據在執行時被載入這塊記憶體,透過本地記憶體的低延遲、高吞吐的特性提高程式的存取速度。行程內緩存在眾多 Java 框架內都有廣泛套用,例如 Hibernate、Mybatis 框架的一二級緩存、Spring MVC 的頁面緩存都是行程內緩存的經典套用場景,這些行程內緩存在 Java 中也有著非常多優秀的開源實作,如 EhCache、Caffeine 都是代表性產品。

    分布式緩存服務

    與行程內相對的,就是需要獨立部署的分布式緩存服務。最常用的是基於 Redis 這種記憶體型 NoSQL 資料庫,對整體架構中的套用數據進行集中緩存。

    圖片

    在架構設計時,很多新架構師一聽到緩存,下意識認為增加 Redis 分布式緩存伺服器就夠了,其實這是片面的做法。在緩存架構設計時,一定要按照由近到遠、由快到慢的順序進行逐級存取。假設在電商進行商品秒殺活動時,如果沒有本地緩存,所有商品、訂單、物流的熱點數據都保存在 Redis 伺服器中,每完成一筆訂單,都要額外增加若幹次網路通訊,網路通訊本身就可能由於各種原因存在通訊失敗的問題。即便是你能保證網路 100% 可用,但 Redis 集群承擔了來自所有外部套用的存取壓力,一旦突發流量超過 Redis 的負載上限,整體架構便面臨崩潰的風險。

    Redis 緩存服務集群

    因此在 Java 的套用端也要設計多級緩存,我們將行程內緩存與分布式緩存服務結合,有效分攤套用壓力。在 Java 套用層面,只有 EhCache 的緩存不存在時,再去 Redis 分布式緩存獲取,如果 Redis 也沒有此數據再去資料庫查詢,數據查詢成功後對 Redis 與 EhCahce 同時進行雙寫更新。這樣 Java 套用下一次再查詢相同數據時便直接從本地 EhCache 緩存提取,不再產生新的網路通訊,套用查詢效能得到顯著提高。

    多級緩存設計


    保障緩存一致性

    但事無完美,當引入多級緩存後,我們又會遇到緩存數據一致性的挑戰,以下圖為例:

    緩存一致性問題

    我們都知道作為資料庫寫操作,是不透過緩存的。假設商品服務例項 1 將 1 號商品價格調整為 80 元,這會衍生一個新問題:如何主動向應用程式推播數據變更的訊息來保證它們也能同步更新緩存呢?

    相信此時你已經有了答案。沒錯,我們需要在當前架構中引入 MQ 訊息佇列,利用 RocketMQ 的主動推播功能來向其他服務例項以及 Redis 緩存服務發起變更通知。

    透過 RocketMQ 解決保證訊息一致性

    如上圖所示,在商品服務例項 1 對商品調價後,主動向 RocketMQ Broker 發送變更訊息,Broker 將變更資訊推播至其他例項與 Redis 集群,這些服務例項在收到變更訊息後,在緩存中先刪除過期緩存,再建立新的數據,以此保證各例項數據一致。

    看到這裏你會發現,對於緩存來說,並沒有終極的解決方案。雖然多級緩存設計帶來了更好的套用效能,但也為了緩存一致性必須引入 MQ 增加了架構的復雜度。那到底多級緩存設計該如何取舍呢?在我看來,有三種情況特別適合引入多級緩存。

    第一種情況,緩存的數據是穩定的。例如郵政編碼、地域區塊、歸檔的歷史數據這些資訊適合透過多級緩存減小 Redis 與資料庫的壓力。

    第二種情況,瞬時可能會產生極高並行的場景。例如春運購票、雙 11 零點秒殺、股市開盤交易等,瞬間的流量洪峰可能擊穿 Redis 緩存,產生流量雪崩。這時利用預熱的行程內緩存分攤流量,減少後端壓力是非常有必要的。

    第三種情況,一定程度上允許數據不一致。例如某部落格平台中你修改了自我介紹這樣的非關鍵資訊,此時在套用集群中其他節點緩存不一致也並不會帶來嚴重影響,對於這種情況我們采用T+1的方式在日終處理時保證緩存最終一致就可以了。

    以上是我總結的三種適合服務層做多級緩存的場景。當然如果你們的套用並行量不大,在未來的1~2 年內利用 Redis 分布式緩存集群完全可以勝任套用效能要求,那自然就沒有必要設計多級緩存,我們要根據業務特點靈活調整架構。

    小結

    今天咱們介紹了在套用微服務架構下從客戶端到服務層,各層的緩存設計以及解決方案,講解了從瀏覽器的 Expires 響應頭到 CDN、Nginx 的靜態資源緩存,再到服務層針對數據的多級緩存,使你對微服務架構的緩存有了總體的了解。


    END


    看完本文有收獲?請轉發分享給更多人

    關註「Java編程鴨」,提升Java技能

    關註Java編程鴨微信公眾號,後台回復:碼農大禮包可以獲取最新整理的技術資料一份。涵蓋Java 框架學習、架構師學習等!

    文章有幫助的話,在看,轉發吧。

    謝謝支持喲 (*^__^*)