首頁>技術>

一位七牛的資深架構師曾經說過這樣一句話:“Nginx+業務邏輯層+資料庫+快取層+訊息佇列,這種模型幾乎能適配絕大部分的業務場景。

圖片來自 Pexels

這麼多年過去了,這句話或深或淺地影響了我的技術選擇,以至於後來我花了很多時間去重點學習快取相關的技術。

我在 10 年前開始使用快取,從本地快取、到分散式快取、再到多級快取,踩過很多坑。下面我結合自己使用快取的歷程,談談我對快取的認識。

本地快取

頁面級快取

我使用快取的時間很早,2010 年左右使用過 OSCache,當時主要用在 JSP 頁面中用於實現頁面級快取。

虛擬碼類似這樣:

<cache:cache key="foobar" scope="session">        some jsp content  </cache:cache> 

中間的那段 JSP 程式碼將會以 key="foobar" 快取在 session 中,這樣其他頁面就能共享這段快取內容。

在使用 JSP 這種遠古技術的場景下,透過引入 OSCache 之後 ,頁面的載入速度確實提升很快。

但隨著前後端分離以及分散式快取的興起,服務端的頁面級快取已經很少使用了。但是在前端領域,頁面級快取仍然很流行。

物件快取

2011 年左右,開源中國的紅薯哥寫了很多篇關於快取的文章。他提到:開源中國每天百萬的動態請求,只用 1 臺 4 Core 8G 的伺服器就扛住了,得益於快取框架 Ehcache。

這讓我非常神往,一個簡單的框架竟能將單機效能做到如此這般,讓我欲欲躍試。

於是,我參考紅薯哥的示例程式碼,在公司的餘額提現服務上第一次使用了 Ehcache。

邏輯也很簡單,就是將成功或者失敗狀態的訂單快取起來,這樣下次查詢的時候,不用再查詢支付寶服務了。

虛擬碼類似這樣:

新增快取之後,最佳化的效果很明顯 , 任務耗時從原來的 40 分鐘減少到了 5~10 分鐘。

上面這個示例就是典型的「物件快取」,它是本地快取最常見的應用場景。相比頁面快取,它的粒度更細、更靈活,常用來快取很少變化的資料,比如:全域性配置、狀態已完結的訂單等,用於提升整體的查詢速度。

重新整理策略

2018 年,我和我的小夥伴自研了配置中心,為了讓客戶端以最快的速度讀取配置, 本地快取使用了 Guava。

整體架構如下圖所示:

那本地快取是如何更新的呢?有兩種機制:

客戶端啟動定時任務,從配置中心拉取資料。當配置中心有資料變化時,主動推送給客戶端。這裡我並沒有使用 websocket,而是使用了 RocketMQ Remoting 通訊框架。

後來我閱讀了 Soul 閘道器的原始碼,它的本地快取更新機制如下圖所示,共支援 3 種策略:

Zookeeper Watch 機制:soul-admin 在啟動的時候,會將資料全量寫入 Zookeeper,後續資料發生變更時,會增量更新 Zookeeper 的節點。

與此同時,soul-web 會監聽配置資訊的節點,一旦有資訊變更時,會更新本地快取。

Websocket 機制:Websocket 和 Zookeeper 機制有點類似,當閘道器與 admin 首次建立好 websocket 連線時,admin 會推送一次全量資料,後續如果配置資料發生變更,則將增量資料透過 websocket 主動推送給 soul-web。

Http 長輪詢機制:Http 請求到達服務端後,並不是馬上響應,而是利用 Servlet 3.0 的非同步機制響應資料。

當配置發生變化時,服務端會挨個移除佇列中的長輪詢請求,告知是哪個 Group 的資料發生了變更,閘道器收到響應後,再次請求該 Group 的配置資料。

不知道大家發現了沒?

pull 模式必不可少增量推送大同小異

長輪詢是一個有意思的話題 , 這種模式在 RocketMQ 的消費者模型也同樣被使用,接近準實時,並且可以減少服務端的壓力。

分散式快取

關於分散式快取, Memcached 和 Redis 應該是最常用的技術選型。相信程式設計師朋友都非常熟悉了,我這裡分享兩個案例。

合理控制物件大小及讀取策略

2013 年,我服務一家彩票公司,我們的比分直播模組也用到了分散式快取。當時,遇到了一個 Young GC 頻繁的線上問題,透過 jstat 工具排查後,發現新生代每隔兩秒就被佔滿了。

進一步定位分析,原來是某些 key 快取的 value 太大了,平均在 300K 左右,最大的達到了 500K。這樣在高併發下,就很容易導致 GC 頻繁。

找到了根本原因後,具體怎麼改呢?我當時也沒有清晰的思路。於是,我去同行的網站上研究他們是怎麼實現相同功能的,包括:360 彩票,澳客網。

我發現了兩點:

資料格式非常精簡,只返回給前端必要的資料,部分資料透過陣列的方式返回。使用 Websocket,進入頁面後推送全量資料,資料發生變化推送增量資料。

再回到我的問題上,最終是用什麼方案解決的呢?當時,我們的比分直播模組快取格式是 JSON 陣列,每個陣列元素包含 20 多個鍵值對, 下面的 JSON 示例我僅僅列了其中 4 個屬性。

[{      "playId":"2399",      "guestTeamName":"小牛",      "hostTeamName":"湖人",      "europe":"123"  }] 

這種資料結構,一般情況下沒有什麼問題。但是當欄位數多達 20 多個,而且每天的比賽場次非常多時,在高併發的請求下其實很容易引發問題。

基於工期以及風險考慮,最終我們採用了比較保守的最佳化方案:

①修改新生代大小,從原來的 2G 修改成 4G。

②將快取資料的格式由 JSON 改成陣列,如下所示:

[["2399","小牛","湖人","123"]] 

修改完成之後,快取的大小從平均 300K 左右降為 80K 左右,YGC 頻率下降很明顯,同時頁面響應也變快了很多。

但過了一會,CPU Load 會在瞬間波動得比較高。可見,雖然我們減少了快取大小,但是讀取大物件依然對系統資源是極大的損耗,導致 Full GC 的頻率也不低。

我們把快取拆成兩個部分,第一部分是全量資料,第二部分是增量資料(資料量很小)。

頁面第一次請求拉取全量資料,當比分有變化的時候,透過 Websocket 推送增量資料。

第 3 步完成後,頁面的訪問速度極快,伺服器的資源使用也很少,最佳化的效果非常優異。

經過這次最佳化,我理解到: 快取雖然可以提升整體速度,但是在高併發場景下,快取物件大小依然是需要關注的點,稍不留神就會產生事故。

另外我們也需要合理地控制讀取策略,最大程度減少 GC 的頻率 , 從而提升整體效能。

分頁列表查詢

列表如何快取是我非常渴望和大家分享的技能點。這個知識點也是我 2012 年從開源中國上學到的,下面我以「查詢部落格列表」的場景為例。

我們先說第 1 種方案:對分頁內容進行整體快取。這種方案會按照頁碼和每頁大小組合成一個快取 key,快取值就是部落格資訊列表。

下面我介紹下第 2 種方案:僅對部落格進行快取。

流程大致如下:

①先從資料庫查詢當前頁的部落格 id 列表,sql 類似:

select id from blogs limit 0,10  

②批次從快取中獲取部落格 id 列表對應的快取資料 ,並記錄沒有命中的部落格 id,若沒有命中的 id 列表大於 0,再次從資料庫中查詢一次,並放入快取,sql 類似:

④返回部落格物件列表。

理論上,要是快取都預熱的情況下,一次簡單的資料庫查詢,一次快取批次獲取,即可返回所有的資料。

另外,關於快取批次獲取,如何實現?

本地快取:效能極高,for 迴圈即可。Memcached:使用 mget 命令。Redis:若快取物件結構簡單,使用 mget 、hmget命令;若結構複雜,可以考慮使用 pipleline,lua 指令碼模式。

第 1 種方案適用於資料極少發生變化的場景,比如排行榜,首頁新聞資訊等。

第 2 種方案適用於大部分的分頁場景,而且能和其他資源整合在一起。

舉例:在搜尋系統裡,我們可以透過篩選條件查詢出部落格 id 列表,然後透過如上的方式,快速獲取部落格列表。

多級快取

首先要明確為什麼要使用多級快取?本地快取速度極快,但是容量有限,而且無法共享記憶體。

分散式快取容量可擴充套件,但在高併發場景下,如果所有資料都必須從遠端快取種獲取,很容易導致頻寬跑滿,吞吐量下降。

有句話說得好,快取離使用者越近越高效!

使用多級快取的好處在於:高併發場景下, 能提升整個系統的吞吐量,減少分散式快取的壓力。

2018 年,我服務的一家電商公司需要進行 App 首頁介面的效能最佳化。我花了大概兩天的時間完成了整個方案,採取的是兩級快取模式,同時利用了 Guava 的惰性載入機制。

整體架構如下圖所示:

快取讀取流程如下:

①業務閘道器剛啟動時,本地快取沒有資料,讀取 Redis 快取,如果 Redis 快取也沒資料,則透過 RPC 呼叫導購服務讀取資料,然後再將資料寫入本地快取和 Redis 中;若 Redis 快取不為空,則將快取資料寫入本地快取中。

②由於步驟 1 已經對本地快取預熱,後續請求直接讀取本地快取,返回給使用者端。

最佳化後,效能表現很好,平均耗時在 5ms 左右。最開始我以為出現問題的機率很小,可是有一天晚上,突然發現 App 端首頁顯示的資料時而相同,時而不同。

也就是說:雖然 LoadingCache 執行緒一直在呼叫介面更新快取資訊,但是各個伺服器本地快取中的資料並非完成一致。

說明了兩個很重要的點:

惰性載入仍然可能造成多臺機器的資料不一致。LoadingCache 執行緒池數量配置的不太合理, 導致了執行緒堆積。

最終,我們的解決方案是:

惰性載入結合訊息機制來更新快取資料,也就是:當導購服務的配置發生變化時,通知業務閘道器重新拉取資料,更新快取。適當調大 LoadigCache 的執行緒池引數,並在執行緒池埋點,監控執行緒池的使用情況,當執行緒繁忙時能發出告警,然後動態修改執行緒池引數。

寫在最後

快取是非常重要的一個技術手段。如果能從原理到實踐,不斷深入地去掌握它,這應該是技術人員最享受的事情。

這篇文章屬於快取系列的開篇,更多是把我 10 多年工作中遇到的典型問題娓娓道來,並沒有非常深入地去探討原理性的知識。

我想我更應該和朋友交流的是:‍‍‍‍‍如何體系化的學習一門新技術。

簡介:現任科大訊飛高階架構師。11 年後端經驗,曾就職於同程藝龍、神州優車等公司。樂於分享、熱衷透過自己的實踐經驗平鋪對技術的理解。

17
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 牛批!一張6000w資料表的查詢最佳化到0.023s