「 微博日活躍用戶 1.6 億+,每日訪問量達百億級,面對龐大用戶羣的海量訪問,良好的架構且不斷改進的緩存體系具備很是重要的支撐做用。算法
本文由新浪微博技術專家陳波老師,分爲以下四個部分跟你們詳細講解那些龐大的數據都是如何呈現的:後端
微博在運行過程當中的數據挑戰數組
Feed 平臺系統架構緩存
Cache 架構及演進服務器
總結與展望網絡
Feed 平臺系統架構總共分爲五層:數據結構
最上面是端層,好比 Web 端、客戶端、你們用的 iOS 或安卓的一些客戶端,還有一些開放平臺、第三方接入的一些接口。架構
下一層是平臺接入層,不一樣的池子,主要是爲了把好的資源集中調配給重要的核心接口,這樣遇到突發流量的時候,就有更好的彈性來服務,提升服務穩定性。運維
再下面是平臺服務層,主要是 Feed 算法、關係等等。性能
接下來是中間層,經過各類中間介質提供一些服務。
最下面一層就是存儲層。
你們平常刷微博的時候,好比在主站或客戶端點一下刷新,最新得到了十到十五條微博,這是怎麼構建出來的呢?
刷新以後,首先會得到用戶的關注關係。好比他有一千個關注,會把這一千個 ID 拿到,再根據這一千個 UID,拿到每一個用戶發表的一些微博。
同時會獲取這個用戶的 Inbox,就是他收到的特殊的一些消息,好比分組的一些微博、羣的微博、下面的關注關係、關注人的微博列表。
拿到這一系列微博列表以後進行集合、排序,拿到所須要的那些 ID,再對這些 ID 去取每一條微博 ID 對應的微博內容。
若是這些微博是轉發過來的,它還有一個原微博,會進一步取原微博內容。經過原微博取用戶信息,進一步根據用戶的過濾詞對這些微博進行過濾,過濾掉用戶不想看到的微博。
根據以上步驟留下的微博,會再進一步來看,用戶對這些微博有沒有收藏、點贊,作一些 Flag 設置,還會對這些微博各類計數,轉發、評論、贊數進行組裝,最後才把這十幾條微博返回給用戶的各類端。
這樣看來,用戶一次請求獲得的十幾條記錄,後端服務器大概要對幾百甚至幾千條數據進行實時組裝,再返回給用戶。
整個過程對 Cache 體系強度依賴,因此 Cache 架構設計優劣會直接影響到微博體系表現的好壞。
接下來咱們看一下 Cache 架構,它主要分爲六層:
第一層是 Inbox,主要是分組的一些微博,而後直接對羣主的一些微博。Inbox 比較少,主要是推的方式。
第二層是 Outbox,每一個用戶都會發常規的微博,都會到它的 Outbox 裏面去。根據存的 ID 數量,實際上分紅多個 Cache,普通的大概是 200 多條,若是長的大概是 2000 條。
第三層是一些關係,它的關注、粉絲、用戶。
第四層是內容,每一條微博一些內容存在這裏。
第五層就是一些存在性判斷,好比某條微博我有沒有贊過。以前有一些明星就說我沒有點贊這條微博怎麼顯示我點讚了,引起了一些新聞。而這種就是記錄,實際上她有在某個時候點贊過但可能忘記了。
最下面還有比較大的一層——計數,每條微博的評論、轉發等計數,還有用戶的關注數、粉絲數這些數據。
接下來咱們着重講一下微博的 Cache 架構演進過程。最開始微博上線時,咱們是把它做爲一個簡單的 KV 數據類型來存儲。
咱們主要採起哈希分片存儲在 MC 池子裏,上線幾個月以後發現一些問題:有一些節點機器宕機或是其餘緣由,大量的請求會穿透 Cache 層達到 DB 上去,致使整個請求變慢,甚至 DB 僵死。
因而咱們很快進行了改造,增長了一個 HA 層,這樣即使 Main 層出現某些節點宕機狀況或者掛掉以後,這些請求會進一步穿透到 HA 層,不會穿透到 DB 層。
這樣能夠保證在任何狀況下,整個系統命中率不會下降,系統服務穩定性有了比較大的提高。
對於這種作法,如今業界用得比較多,而後不少人說我直接用哈希,但這裏面也有一些坑。
好比我有一個節點,節點 3 宕機了,Main 把它給摘掉,節點 3 的一些 QA 分給其餘幾個節點,這個業務量還不是很大,穿透 DB,DB 還能夠抗住。
但若是這個節點 3 恢復了,它又加進來以後,節點 3 的訪問就會回來,稍後節點 3 由於網絡緣由或者機器自己的緣由,它又宕機了,一些節點 3 的請求又會分給其餘節點。
這個時候就會出現問題,以前分散給其餘節點寫回來的數據已經沒有人更新了,若是它沒有被剔除掉就會出現混插數據。
實際上微博是一個廣場型的業務,好比突發事件,某明星找個女友,瞬間流量就 30% 了。
突發事件後,大量的請求會出如今某一些節點,會致使這些節點很是熱,即使是 MC 也沒辦法知足這麼大的請求量。這時 MC 就會變成瓶頸,致使整個系統變慢。
基於這個緣由,咱們引入了 L1 層,仍是一個 Main 關係池,每個 L1 大概是 Main 層的 N 分之一,六分之1、八分之1、十分之一這樣一個內存量,根據請求量我會增長 4 到 8 個 L1,這樣全部請求來了以後首先會訪問 L1。
L1 命中的話就會直接訪問,若是沒有命中再來訪問 Main-HA 層,這樣在一些突發流量的時候,能夠由 L1 來抗住大部分熱的請求。
對微博自己來講,新的數據就會越熱,只要增長不多一部份內存就會抗住更大的量。
簡單總結一下:經過簡單 KV 數據類型的存儲,咱們其實是以 MC 爲主的,層內 Hash 節點不漂移,Miss 穿透到下一層去讀取。
經過多組 L1 讀取性能提高,可以抗住峯值、突發流量,並且成本會大大下降。
對讀寫策略,採起多寫,讀的話採用逐層穿透,若是 Miss 的話就進行回寫。對存在裏面的數據,咱們最初採用 Json/xml,2012 年以後就直接採用 Protocol Buffer 格式,對一些比較大的用 QuickL 進行壓縮。
剛纔講到簡單的 QA 數據,那對於複雜的集合類數據怎麼來處理?
好比我關注了 2000 人,新增 1 我的,就涉及到部分修改。有一種方式是把 2000 個 ID 所有拿下來進行修改,但這種對帶寬、機器壓力會很大。
還有一些分頁獲取,我存了 2000 個,只須要取其中的第幾頁,好比第二頁,也就是第十到第二十個,能不能不要全量把全部數據取回去。
還有一些資源的聯動計算,會計算到我關注的某些人裏面 ABC 也關注了用戶 D。這種涉及到部分數據的修改、獲取,包括計算,對 MC 來講其實是不太擅長的。
各類關注關係都存在 Redis 裏面取,經過 Hash 分佈、儲存,一主多從的方式來進行讀寫分離。如今 Redis 的內存大概有 30 個 T,天天都有 2-3 萬億的請求。
在使用 Redis 的過程當中,實際上仍是遇到其餘一些問題。好比從關注關係,我關注了 2000 個 UID,有一種方式是全量存儲。
但微博有大量的用戶,有些用戶登陸得比較少,有些用戶特別活躍,這樣所有放在內存裏成本開銷是比較大的。
因此咱們就把 Redis 使用改爲 Cache,好比只存活躍的用戶,若是你最近一段時間沒有活躍,會把你從 Redis 裏踢掉,再次有訪問的時候再把你加進來。
這時存在一個問題,由於 Redis 工做機制是單線程模式,若是它加某一個 UV,關注 2000 個用戶,可能擴展到兩萬個 UID,兩萬個 UID 塞回去基本上 Redis 就卡住了,沒辦法提供其餘服務。
因此咱們擴展一種新的數據結構,兩萬個 UID 直接開了端,寫的時候直接依次把它寫到 Redis 裏面去,讀寫的整個效率就會很是高。
它的實現是一個 long 型的開放數組,經過 Double Hash 進行尋址。
咱們對 Redis 進行了一些其餘的擴展,你們可能也在網上看到過咱們以前的一些分享,把數據放到公共變量裏面。
整個升級過程,咱們測試 1G 的話加載要 10 分鐘,10G 大概要 10 分鐘以上,如今是毫秒級升級。
對於 AOF,咱們採用滾動的 AOF,每一個 AOF 是帶一個 ID 的,達到必定的量再滾動到下一個 AOF 裏去。
對 RDB 落地的時候,咱們會記錄構建這個 RDB 時,AOF 文件以及它所在的位置,經過新的 RDB、AOF 擴展模式,實現全增量複製。
接下來還有一些其餘的數據類型,好比一個計數,實際上計數在每一個互聯網公司均可能會遇到,對一些中小型的業務來講,實際上 MC 和 Redis 足夠用的。
但在微博裏計數出現了一些特色:單條 Key 有多條計數,好比一條微博,有轉發數、評論數,還有點贊;一個用戶有粉絲數、關注數等各類各樣的數字。
由於是計數,它的 Value size 是比較小的,根據它的各類業務場景,大概就是 2-8 個字節,通常 4 個字節爲多。
而後每日新增的微博大概十億條記錄,總記錄就更可觀了,而後一次請求,可能幾百條計數要返回去。
最初是能夠採起 Memcached,但它有個問題,若是計數超過它內容容量時,會致使一些計數的剔除,宕機或重啓後計數就沒有了。
另外可能有不少計數它爲零,那這個時候怎麼存,要不要存,存的話就佔不少內存。
微博天天上十億的計數,光存 0 都要佔大量的內存,若是不存又會致使穿透到 DB 裏去,對服務的可溶性會存在影響。
2010 年以後咱們又採用 Redis 訪問,隨着數據量愈來愈大以後,發現 Redis 內存有效負荷仍是比較低的,它一條 KV 大概須要至少 65 個字節。
但實際上咱們一個計數須要 8 個字節,而後 Value 大概 4 個字節,因此有效只有 12 個字節,還有四十多個字節都是被浪費掉的。
這還只是單個 KV,若是在一條 Key 有多個計數的狀況下,它就浪費得更多了。
好比說四個計數,一個 Key 8 個字節,四個計數每一個計數是 4 個字節,16 個字節大概須要 26 個字節就好了,可是用 Redis 存大概須要 200 多個字節。
後來咱們經過本身研發的 Counter Service,內存降至 Redis 的五分之一到十五分之一如下,並且進行冷熱分離,熱數據存在內存裏,冷數據若是從新變熱,就把它放到 LRU 裏去。
落地 RDB、AOF,實現全增量複製,經過這種方式,熱數據單機能夠存百億級,冷數據能夠存千億級。
整個存儲架構大概是上圖這樣,上面是內存,下面是 SSD,在內存裏是預先把它分紅 N 個 Table,每一個 Table 根據 ID 的指針序列,劃出必定範圍。
任何一個 ID 過來先找到它所在的 Table,若是有直接對它增增減減,有新的計數過來,發現內存不夠的時候,就會把一個小的 Table Dump((內存信息)轉儲,轉存 ) 到 SSD 裏去,留着新的位置放在最上面供新的 ID 來使用。
有些人疑問說,若是在某個範圍內,個人 ID 原本設的計數是 4 個字節,可是微博特別熱,超過了 4 個字節,變成很大的一個計數怎麼處理?
對於超過限制的,咱們把它放在 Aux dict 進行存放,對於落在 SSD 裏的 Table,咱們有專門的 IndAux 進行訪問,經過 RDB 方式進行復制。
除了計數,微博還有一些業務,一些存在性判斷。好比一條微博展示的,有沒有點贊、閱讀、推薦,若是這個用戶已經讀過這個微博了,就不要再顯示給他。
這種有一個很大的特色,它檢查是否存在,每條記錄很是小,好比 Value 1 個 bit 就能夠了,但總數據量巨大。
好比微博天天新發表微博 1 億左右,讀的可能有上百億、上千億這種總的數據須要判斷。
怎麼來存儲是個很大的問題,並且這裏面不少存在性就是 0。仍是前面說的,0 要不要存?
若是存了,天天就存上千億的記錄;若是不存,那大量的請求最終會穿透 Cache 層到 DB 層,任何 DB 都沒辦法抗住那麼大的流量。
咱們也進行了一些選型:首先直接考慮能不能用 Redis。單條 KV 65 個字節,一個 KV 能夠 8 個字節的話,Value 只有 1 個 bit,這樣算下來每日新增內存有效率是很是低的。
第二種咱們新開發的 Counter Service,單條 KV Value 1 個 bit,我就存 1 個 byt,總共 9 個 byt 就能夠了。
這樣每日新增內存 900G,存的話可能就只能存最新若干天的,存個三天差很少快 3 個 T 了,壓力也挺大,但比 Redis 已經好不少。
咱們最終方案是本身開發 Phantom,先採用把共享內存分段分配,最終使用的內存只用 120G 就能夠。
算法很簡單,對每一個 Key 能夠進行 N 次哈希,若是哈希的某一個位它是 1,那麼進行 3 次哈希,三個數字把它設爲 1。
把 X2 也進行三次哈希,後面來判斷 X1 是否存在的時候,從進行三次哈希來看,若是都爲 1 就認爲它是存在的;若是某一個哈希 X3,它的位算出來是 0,那就百分百確定是不存在的。
它的實現架構比較簡單,把共享內存預先拆分到不一樣 Table 裏,在裏面進行開方式計算,而後讀寫,落地的話採用 AOF+RDB 的方式進行處理。
整個過程由於放在共享內存裏面,進程要升級重啓數據也不會丟失。對外訪問的時候,建 Redis 協議,它直接擴展新的協議就能夠訪問咱們這個服務了。
小結一下:到目前爲止,咱們關注了 Cache 集羣內的高可用、擴展性、組件高性能,還有一個特別重要就是存儲成本,還有一些咱們沒有關注到的,好比運維性如何,微博如今已經有幾千差很少上萬臺服務器等。
採起的方案首先就是對整個 Cache 進行服務化管理,對配置進行服務化管理,避免頻繁重啓,另外若是配置發生變動,直接用一個腳本修改一下。
服務化還引入 Cluster Manager,實現對外部的管理,經過一個界面來進行管理,能夠進行服務校驗。
服務治理方面,能夠作到擴容、縮容,SLA 也能夠獲得很好的保障。另外,對於開發來講,如今就能夠屏蔽 Cache 資源。
最後簡單總結一下,對於微博 Cache 架構來講,咱們從它的數據架構、性能、儲存成本、服務化等不一樣方面進行了優化加強。