微博緩存服務化的設計與實踐

導讀:高可用架構 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 配置 ,即可訪問 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 進行下線,然後通過重啓解決,也可以通過我們的熱升級機制解決。

【轉自】:http://h2ex.com/1331

發佈了14 篇原創文章 · 獲贊 8 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章