1. 程式人生 > >從優化效能到應對峰值流量:微博快取服務化的設計與實踐

從優化效能到應對峰值流量:微博快取服務化的設計與實踐

導讀:高可用架構 8 月 20 日在深圳舉辦了『網際網路架構:從 1 到 100』為主題的閉門私董會研討及技術沙龍,本文是陳波分享的微博快取服務的演進歷程。

陳波

陳波,08 年加入新浪,參與 IM 系統的後端研發。09 年之後從事新浪微博的系統研發及架構工作,在海量資料儲存、峰值訪問、規模化快取服務及開放平臺等方面參與技術架構改進,當前主要負責微博平臺的基礎設施、中介軟體的研發及架構優化工作,經歷新浪微博從起步到成為數億使用者的大型網際網路系統的技術演進過程。

在所有介紹微博架構演進的使用場景都離不開快取,今天上午騰訊分享的 CKV 也同樣提到了快取服務在騰訊社交產品的重要性。快取的設計為什麼重要,我們先介紹其使用場景。

1、微博的快取業務場景

微博幾乎所有的介面都是實時組裝的,使用者請求最終轉化到資源後端可能會存在 1 – 2 個數量級的讀放大,即一個使用者請求可能需要獲取幾十上百個以上的資源資料進行動態組裝。

比如大家刷微博的時候,會觸發一個 friends_timeline 的介面請求,然後服務端會聚合並組裝最新若干條(比如 15 條)微博給使用者,那這個過程後端服務需要到資源層拿哪些資料來組裝?

  • 首先是後端服務會從資源層去獲取使用者的關注列表;

  • 然後根據關注列表獲取每一個被關注者的最新微博 ID 列表 以及 使用者自己收到的微博 ID 列表(即 inbox);

  • 通過對這些 ID 列表進行聚合、排序和分頁等處理後,拿到需要展現的微博 ID 列表;

  • 再根據這些 ID 獲取對應的微博內容;

  • 如果是轉發微博還要獲取源微博的內容;

  • 然後需要獲取使用者設定的過濾詞並進行過濾。

  • 此後還需要獲取微博作者、包括源微博作者的 user 資訊進行組裝;

  • 還需要獲取這個使用者對這些微博是不是有收藏、是否贊,

  • 最後還需要獲取這些微博的轉發、評論、讚的計數等進行組裝。

微博快取服務化

從以上過程可以看到,使用者的一個首頁請求,最終後端 server 可能需要從資源層獲取幾百甚至幾千個資料進行組裝才能得到返回的資料。

微博線上業務的很多核心介面響應需要在毫秒級,可用性要求達到 4 個 9。因此,為了保證資源資料的獲取效能和可用性,微博內部大量使用快取,而且對快取是重度依賴的,不少核心業務的單埠快取訪問 QPS 已經達到了百萬級以上。

微博使用的快取主要是 Memcache 和 Redis,因為 Memcache 的使用場景、容量更大,而且目前推的快取服務化也是優先基於 Memcache,然後再擴充套件到 Redis、 ssdcache 等其他快取,所以今天的快取服務討論也是以 Memcache 為儲存標的來展開的。我們最早使用的快取架構就是直接利用開源版本的 Memcache 執行在物理機上,我們稱之為裸資源。

2、快取的裸資源架構演進

首先看一下微博 Memcache 快取的裸資源架構的演進過程。微博上線之初,我們就對核心業務資料進行分池、分埠的,把 size 接近的資料放在相同的池子裡面。業務方通過 Hash 演算法訪問快取池裡的節點。同時,每個 IDC 部署使用獨立的快取資源,為了加速,業務前端也會在本地啟用 local-Cache。

上線幾個月之後,隨著業務量和使用者量的急聚增加,快取節點數很快增加到數百個。這段時間,時常會因為網路異常、機器故障等,導致一些快取節點不可用,從而導致快取 miss。這些 miss 的請求最終會穿透到 DB 中。

如果某個時間點,核心業務的多個快取節點不可用,大量請求穿透會給 DB 帶來巨大的壓力,極端情況會導致雪崩場景。於是我們引入 Main-HA 雙層架構。

對後端的快取訪問時,會先訪問 Main 層,如果 miss 繼續訪問 HA 層,從而在獲得更高的命中率的同時,即便部分 Main 節點不可用,也可以保證快取的命中率,並減少 DB 壓力。

這一階段我們對業務資源進一步的分拆,每一種核心資料都分拆到獨立的埠。同時,根據不同的訪問頻率、容量進行快取搭配部署,對 Memcache 資源的埠進行統一規劃,確保快取層的效能和可用性。同時我們發現,在各種海量業務資料的沖刷下,前端使用 local-Cache,命中率不高,效能提升不明顯,所以我們把 local Cache 層去掉了。

隨著業務訪問量進一步增加,特別是一些突發事件爆發式的出現並傳播, Main-HA 結構也出現了一些問題,主要是很多快取節點的頻寬被打滿, Memcache 的 CPU 比較高, Memcache 響應變慢。

通過分析,我們發現主要是大量熱資料的集中訪問導致的服務過載,單個埠不能承載熱資料的訪問(比如明星發的微博所在的埠),於是我們引入了 L1 結構。

通過部署 3 – 4 組以上的小容量 L1 快取,每個 L1 組等價儲存熱資料,來滿足業務要求。

微博快取服務化

總結一下微博的快取架構演進過程:

  1. 在直接使用裸快取資源的過程中,我們通過 Main-HA 雙層結構,消除了單點問題;

  2. 通過熱資料的多 L1 副本,可以用較低的成本即可應對高峰、突發流量;

  3. L1s-M-H 三層快取結構消除了快取層出現的頻寬和 CPU 過載的情況,使整個系統的讀取性都、可用性得了很大的提高。

在以上 3 階段的演進過程中,我們較好的解決了訪問效能與訪問峰值的壓力,不過在服務的可管理性方面依然存在可管理空間。不同業務之間只有經驗可以複用,在快取的實現方面經常需要各種重複的勞動。我們需要把快取的使用服務化才能把可管理性帶到一個新的階段。

3、快取服務的設計與實踐

直接使用裸快取資源也存在一系列問題:

  1. 首先,隨著業務的發展,微博快取的訪問量、容量都非常大。線上有數千個快取節點,都需要在業務前端要去配置,導致快取配置檔案很大也很複雜。

  2. 同時,如果發生快取節點擴容或切換,需要運維通知業務方,由業務方對配置做修改,然後進行業務重啟,這個過程比較長,而且會影響服務的穩定性。

  3. 另外,微博平臺主要採用 Java 語言開發,我們定製了 Java Memcache 快取層來訪問三層快取結構,內建了不少訪問策略。這時候,如果公司其他部門也想使用,但由於用的是其他開發語言如 PHP,就沒法簡單推廣了。

  4. 最後,資源的可運維性也不足,基於 IP、埠運維複雜性比較高。比如一個線上機器宕機,在這個機器上部署了哪些埠、對應了哪些業務呼叫,沒有簡單直觀的查詢、管理入口。

於是我們開始考慮快取的服務化,主要的過程及策略如下:

  1. 首先是對 Memcache 快取引入了一個 proxy 層,基於 Twitter 的 twemproxy 進行改造。

  2. 引入 cluster,並內嵌了 Memcache Cluster 訪問策略,包括三層的一些更新、讀取,以及 miss 後的穿透、回寫等。

  3. 我們通過單程序單埠來對多個業務進行訪問,不同業務通過 namespace Prefix 進行區分。

  4. 在 Cache-proxy 也引入了 LRU,在某些業務場景減少熱點資料的穿透。

通過 cacheProxy,簡化了業務前端的配置,簡化了開發,業務方只需要知道 cacheProxy 的 IP 和埠,即可實現對後端各種業務的多層快取進行訪問。

我們對快取服務的服務治理也做了不少工作。

接入配置中心

首先,把 Cache 層接入了配置中心 configServer(內部叫 vintage)。實現了 Memcache 快取、 cacheProxy 的動態註冊和訂閱,運維把 Memcache 資源的 IP 埠、 Memcache 訪問的 hash 方式、分散式策略等也以配置的形式註冊在配置中心, cacheProxy 啟動後通過到配置中心訂閱這些資源 IP 及訪問方式,從而正確連線並訪問後端 Memcache 快取資源。而且 cacheProxy 在啟動後,也動態的註冊到配置中心, client 端即可到配置中心訂閱這些 cacheProxy 列表,然後選擇最佳的 cacheProxy 節點訪問 Memcache 資源。同時,運維也可以線上管理 Memcache 資源,在網路中斷、 Memcache 宕機,或業務需要進行擴容時,運維啟動新的 Memcache 節點,同時通知配置中心修改資源配置,就可以使新資源快速生效,實現快取資源管理的 API 化、指令碼化。

監控體系

其次,把 cacheProxy、後端 Memcache 資源也納入到了 Graphite 體系,通過 logtailer 工具將快取的訪問日誌、內部狀態推送到 Graphite 系統,用 dashboard 直接展現或者按需聚合後展現。

Web 化管理

同時,我們也開發了快取層管理元件 clusterManager(內部也叫 captain),把之前的 API 化、指令碼化管理進一步的升級為介面化管理。運維可以通過 clusterManager,介面化管理快取的整個生命週期,包括業務快取的申請、稽核,快取資源的變更、擴縮容、上下線等。

監控與告警

ClusterManager 同時對快取資源、 cacheProxy 等進行狀態探測及聚合分析,監控快取資源的 SLA,必要時進行監控報警。

我們也準備將 clusterManager 整合公司內部的 jpool(編排釋出系統)、 DSP(混合雲管理平臺) 等系統,實現了對 cacheProxy、 Memcache 節點的一鍵部署和升級。

開發工具

對於 client 端,我們基於 Motan(微博已開源的 RPC 框架)擴充套件了 Memcache 協議,使 client 的配置、獲取服務列表、訪問策略更加簡潔。方便開發者實現面向服務程式設計,比如開發者在和運維確定好快取的 SLA 之後,通過 spring 配置 <weibo:cs namespace=“ unread-feed” registry=”vintage” /> ,即可訪問 unread-feed 業務對應的 Memcache 資源,後續的擴容、節點切換等都不需要開發者介入,也不需要重啟。

部署方式

對於 cacheProxy 的部署,目前有兩種方式,一種是本地化部署,就是跟業務前端部署在一起的,在對 cacheProxy 構建 Docker 映象後,然後利用 jpool 管理系統進行動態部署。另外一種是集中化部署,即 cacheProxy 在獨立的機器上部署,由相同的業務資料獲取方進行共享訪問。

Cache 服務化後的業務處理流程如圖。

首先運維通過 captain 把 Memcache 資源的相關配置註冊到 configServer, cacheProxy 啟動後通過 configServer 獲取 Memcache 資源配置並預建連線; cacheProxy 在啟動準備完畢後將自己也註冊到 configServer,業務方 client 通過到 configServer 獲取 cacheProxy 列表,並選擇最佳的 cacheProxy 傳送請求指令, cacheProxy 收到請求後,根據 namespace 選擇快取的 cluster,並按照配置中的 hash 及分佈策略進行請求的路由、穿透、回寫。 Captain 同時主動探測 cacheProxy、 Memcache 快取資源,同時到 Graphite 獲取歷史資料進行展現和分析,發現異常後進行報警。

微博快取服務化

在業務執行中,由於各個業務的訪問量的不斷變化、熱點事件的應對,需要根據需要對快取資源進行擴縮,有兩種擴縮方式:叢集內的擴縮 和 叢集的增減。

對於叢集內的擴縮,線上操作最多的是增減 L1 組或擴容 main 層。

對於 L1,通常直接進行上下線資源,並通過 captain 對配置中心的配置做變更即可生效。而 main 層擴縮有兩種方式,一是通過 L1、 Main 的切換,即新的 main 層先做為 L1 上線,命中率達到要求後,再變更一次配置,去掉老的 main,使用新的 main 層 ; 另外一種方式是使用 main-elapse 策略,直接上線 main,把老的 main 改為 main-elapse, main 層 miss 後先訪問 main_elapse 並回種, set 時對 main-elapse 做刪除操作。

對於叢集的增減,我們增加了一個新元件 updateServer,然後通過複製來實現,目前還在內部開發測試狀態。為什麼會有叢集增減,因為微博的訪問存在時間上的規律性,比如晚上 9 點到 0 點的高峰期、節假日、奧運等熱點出現,流量可能會有 30-50% 以上 變化,原有叢集可能撐不住這麼大的量,我們可能需要新建一個前端 + 資源叢集,來滿足業務需要,這時可以提前 1-2 個小時在公有云部署資源服務並加熱,供新叢集的業務方使用,待峰值過去後,再做下線處理,在提供更好地服務的同時,也可以降低成本。

如何 updateServer 進行叢集間複製?可以結合下面這張圖來看。

快取叢集分為 master 叢集、 slave 叢集。 Master 叢集的 cacheProxy 收到 client 的請求後,對於讀請求直接訪問 L1-m-h 三層結構,但對寫請求會發往本地的 updateServer ; slave 叢集的 cacheProxy 除了做 master 叢集的相同的動作,還會同時將寫請求路由到 master 叢集的 updateServer。只要 cacheProxy 更新 master、 local 叢集中任何一個 updateServer 成功則返回成功,否則返回失敗。 updateServer 收到寫請求,在路由到後端快取資源的同時,會日誌記錄到 aof 檔案, slave 叢集的快取即通過 updateServer 進行同步。為什麼引入 updateServer 這個角色,主要是更好的應對前端本地部署,由於本地部署方式 cacheProxy 節點特別多,前端機器配置較差,更重要的原因前端 Docker 映象隨時可能會被下線清理,所以需要把寫請求傳送到獨立部署的 updateServer 進行更新。而對於集中化部署, Cache proxy 和 updateServer 的角色也可以合二為一,變為一個程序。

微博快取服務化

快取服務化的推進,效能也是業務方考慮的一個重要因素。

  • 我們對原來的 pipeline 請求中的讀取類請求,進行了請求合併,通過 merge req 機制提高效能;

  • 把單程序升級為多程序(這一塊也在內部開發中);

  • 對於 LRU 我們升級為 LS4LRU,線上資料分析發現,相同容量及過期時間, LS4LRU 總體命中率能進一步提高 5% – 7%

LS4LRU 簡介

這裡對 LS4LRU 做個簡單地介紹。首先介紹 S4LRU,它是分成四個子 LRU: LRU0-LRU3。 Key miss 或新寫入一個 key 時,把這個 key 放在第一層 LRU0,如果後來被命中則移到 LRU1 ;如果在 LRU1 又一次被命中則移到 LRU2,依此類推,一直升級到 LU3。如果它四次以上命中,就會一直把它放在 LU3。如果發現 LU3 的資料量太多需要 evict,我們先把待 evict 的 key 降級到 LU2 上,如此類推。同時每個 kv 有過期時間,如果發現它過期就清理。

而 LS4LRU 是在 S4LRU 的基礎上增加一個分級的過期時間,每個 KV 有兩個過期時間 exp1 和 exp2。比如說某業務, exp1 是一秒, xep2 是三秒, LS4LRU 被命中的時候,如果發現它是在一秒內的資料,則直接反給客戶端的,如果是在 1 秒到 3 秒的時候,則會首先返回到客戶端,然後再從非同步獲取最新的資料並更新。如果是 3 秒以上的,就直接去清理,走 key miss 流程。

微博快取服務化

服務化的總結

我們再看一下服務化的其他一些方面的實踐總結。

  • 對於容災, Memcache 部分節點故障,我們有多級 Cache 解決;

  • 對於 proxy/Memcache 較多節點異常,我們通過重新部署新節點,並通過 captain 線上通知配置中心,進而使新節點快速生效;

  • 對於配置中心的故障,可以訪問端的 snapshot 機制,利用之前的 snapshot 資訊來訪問 Cache proxy 或後端快取資源。

  • 對於運維,我們可以通過 Graphite、 captain,實現標準化運維;對於節點故障、擴縮容按標準流程進行介面操作即可。運維在處理資源變更時,不再依賴開發修改配置和業務重啟,可以直接在後端部署及服務註冊。對於是否可以在故障時直接部署並進行配置變更,實現自動化運維,這個我們也還在探索中。

歷年的演進經驗可以看到,快取服務化的道路還是很長,未來還需要進一步的對各 Cache 元件進行打磨和升級,我們也會在這條路上不斷前行。大家對於快取的設計有各種建議的,歡迎在文後留言進行探討。

Q&A

提問: L1 和 main 是如何協作的,什麼時候可以把資料升級到了 L1,什麼時候淘汰?為什麼要使用這樣的機制, L1 和 main 的訪問速度應該差不了很多吧?為什麼要另外再加一個熱點資料放在 L1 裡面? L1 跟 main 怎麼做資料同步?

陳波:首先 L1 的容量比 Main 小很多,同時 L1 會有很多組,線上核心也有一般在 4-6 組以上,每組 L1 的資料基本上是熱資料。如果部署了 L1,所有的寫請求、 L1 的讀 miss 後的回寫,都會把資料寫入 L1,淘汰方式是 L1 組在容量滿了之後由 Memcache 自動剔除。

對於為什麼需要 L1,因為對於微博業務來說,它是一個冷熱非常明顯的業務場景,一般來講,新發的微博請求量大,之前發的微博請求量小,另外在峰值期請求量會特別大,在高峰訪問期間、節假日時,核心業務單埠的訪問 QPS 會有百萬級,這時單層或雙層 main-ha 結構的 Memcache 快取效能上無法滿足要求,主要表現就是頻寬被打滿、 CPU 過高、請求耗時增加。另外在突發事件爆發時,比如最近的寶寶事件,如果對部分熱 key 有數十萬級以上的併發訪問,再加上其他不同 key 的請求,雙層快取結構是完全無法滿足效能要求的,快取節點過載,讀取效能下降,超時會???量出現。因此我們增加 L1 層,通過多個 L1 組,把這些熱資料分散到不到 L1 組來訪問,從而避免 Cache 層過載。這樣 L1 層就分擔了 Main 層對熱資料的大部分訪問,一些溫熱的資料訪問才會落到 Main 和 slave 層,為了保持 main 層資料的熱度,實際線上執行中,我們也會把 main 層作為一個 L1 組來分擔部分熱資料的訪問,只是這種情況下, key miss 後會直接訪問 slave。

資料同步是通過多寫和穿透回寫的方式進行。在更新資料的時候,直接對所有的 L1、 Main、 slave 層進行更新,從而保證各層的資料是最新的。另外,進行資料讀取的時候,存在 L1-main-ha、 DB 四層的穿透回寫機制,如果前面讀取的快取層 miss 了,後面快取層、 DB 層命中了,然後就可以進行原路回寫,從而對前面的快取層都寫入相同的 kv。

提問:什麼樣的資料放在 L1 裡面?

陳波:最熱的資料存在 L1 中,它通過 Memcache 層的淘汰機制進行的。因為 L1 容量比 main 小很多,最熱的資料、訪問頻率最高的資料基本都在 L1 裡面,而稍冷的資料會很快的從 L1 裡面踢走。所以直觀上,你可以認為最熱的、當前訪問量最大資料就在 L1 層。比如說可以認為姚晨、寶強的最新資料都在 L1 層,我們普通使用者的資料大多靠 Main 層命中。

提問:你們線上 Redis 的記憶體碎片情況如何?

陳波:我們去年和前年對部分業務的 Redis 有做過分析,一般有效記憶體負荷在 85% 到 90% 以上,也就是碎片率小於 1.1-1.2,很多是 1.0x,有些跑了半年或者一年以上的部分例項可能會稍微高一點。

提問: Redis 碎片率過高的話你們是怎麼來優化的?

陳波:如果發現碎片率比較高,比如 master,我們會切換一個新 maste,然後把老的 maste 進行下線,然後通過重啟解決,也可以通過我們的熱升級機制解決。

本文及本次沙龍相關 PPT 連結如下

https://pan.baidu.com/s/1geTJtZX

文章出處:高可用架構

高可用架構