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