日訪問量百億級的應用如何做快取架構設計
微博日活躍使用者1.6億+,每日訪問量達百億級,面對龐大使用者群的海量訪問,良好架構且不斷改進的快取體系具有非常重要的支撐作用。
4月21日,中生代技術走進盒子科技的現場技術交流活動上,新浪微博技術專家陳波為大家講解了微博Cache架構的設計實踐過程。
刷微博嗎?跟我們一起聽聽那些龐大的資料是如何呈現的吧!
資料挑戰
Feed平臺系統架構
總共分為五層,最上層是端層,比如web端,客戶端,大家用的ios或安卓的一些客戶端,還有一些開放平臺,第三方接入的一些介面。下面是平臺接入層,不同的池子,主要是為了把好的資源集中調配給重要的核心介面,這樣突發流量的時候,有更好的彈性來服務,提高服務穩定性。再下面是平臺服務層,主要是Feed演算法,關係等等。接下來是中間層,通過各種中間介質提供一些服務。最下面一層就是儲存層,平臺架構大概是這樣。
Feed timeline
大家日常刷微博的時候,比如在主站或客戶端點一下重新整理,最新獲得了十到十五條微博,它這個是怎麼構建出來的呢?重新整理之後,首先會獲得使用者的關注關係,比如她有一千個關注,會把這一千個ID拿到,根據這一千個UID,拿到每個使用者發表的一些微博,同時會獲取這個使用者的Inbox,就是她收到的特殊的一些訊息,比如分組的一些微博,群的微博,下面她的關注關係,她關注人的微博列表,拿到這一系列微博列表之後進行集合、排序,拿到所需要的那些ID,再對這些ID去取每一條微博ID對應的微博內容,如果這些微博是轉發過來的,它還有一個原微博,會進一步取原微博內容,通過原微博取使用者資訊,進一步根據使用者的過濾詞,對這些微博進行過濾,過濾掉使用者不想看到的微博,留下這些微博後,再進一步來看,使用者對這些微博有沒有收藏、贊,做一些flag設定,最後還會對這些微博各種計數,轉發、評論、贊數進行組裝,最後才把這十幾條微博返回給使用者的各種端。這樣看,使用者一次請求,最終得到十幾條記錄,後端伺服器大概要對幾百甚至幾千條資料進行實時組裝,再返回給使用者,整個過程對Cache體系強度依賴。所以Cache架構設計優劣直接會影響到微博體系表現的好壞。
Feed Cache架構
然後我們看一下Cache架構,它主要分為6層,首先是Inbox,主要是分組的一些微博,然後直接對群主的一些微博,Inbox比較少,主要是推的方式。然後對於Outbox,每個使用者都會發常規的微博,都會在它Outbox裡面去,根據存的ID的數量,實際上分成多個Cache,普通的大概是200多,如果是長的大概是2000條。第三組就是一些關係,它的關注、粉絲、使用者。第四個就是內容,每一條微博一些內容存在這裡。下面就是一些存在性判斷,比如微博裡面,這條微博有沒有贊過,之前有一些明星就說我沒有點贊這條微博怎麼顯示我點讚了,引發一些新聞,這種就是記錄,實際上她在某個時候點贊忘記了。最下面還有比較大的一塊——計數。一條微博評論轉發等計數,對使用者來說,她的關注數粉絲數這些資料。
Cache架構及演進
1.簡單KV資料型別
接下來我們著重講一些微博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分之一,六分之一、八分之一、十分之一這樣一個記憶體量,根據請求量我會增加4到8個L1,這樣所有的請求來了之後首先會訪問L1,L1命中的話就會直接訪,如果沒有命中再來訪問Main-HA層,這樣在一些突發流量的時候,可以由L1來抗住大部分熱的請求。對微博本身來說,新的資料就會越熱,只用增加很少一部分記憶體就會抗住更大的量。
簡單總結一下,通過簡單KV資料型別的儲存,我們實際上以MC為主的,層內HASH節點不漂移,Miss穿透到下一層去讀取。通過多組L1讀取效能提升,對峰值、突發流量能夠抗住,而且成本會大大降低。對讀寫策略,採取多寫,讀的話採用逐層穿透,如果Miss的話就進行回寫,對存在裡面的資料,我們最初採用Json/xml,12年之後就直接採用Protocol| Buffer格式,對一些比較大的用QuickL進行壓縮。
集合類資料
剛才講到簡單的QA資料,對於複雜的集合類資料怎麼來處理,比如我關注了2000人,新增一個人,這就涉及到部分修改。有一種方式把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大概要十幾分鍾以上,現在是毫秒級升級。對於AOF,我們採用滾動的AOF,每個AOF是帶一個ID的,達到一定的量再滾動到下一個AOF裡面去。對RDB落地的時候,我們會記錄構建這個RDB時,AOF檔案以及它所在的位置,通過新的RDB、AOF擴充套件模式,實現全增量複製。
其他資料型別-計數
接下來還有一些其他的資料型別,比如一個計數,實際上計數在每個網際網路公司都可能會遇到,對一些中小型的業務來說,實際上MC和Redis足夠用的,但在微博裡面計數出現了一些特點,單條Key有多條計數,比如一條微博,有轉發數、評論數、還有點贊,一個使用者有粉絲數、關注數等各種各樣的數字,因為是計數,它的Value size是比較小的,根據它的各種業務場景,大概就是2-8個位元組,一般4個位元組為多,然後每日新增的微博大概十億條記錄,總記錄就更可觀了,然後一次請求,可能幾百條計數要返回去。
計數器-Counter Service
最初是可以採取Memcached,但它有個問題,如果計數超過它內容容量的時候,它會導致一些計數的剔除,宕機或重啟後計數就沒有了。另外可能有很多計數它是為零,那這個時候怎麼存,要不要存,存的話就佔很多記憶體。微博每天上十億的計數,光存0都要佔大量的記憶體,如果不存又會導致穿透到DB裡面去,對服務的可溶性就會存在影響。2010年之後我們又採用Redis訪問,隨著資料量越來越大之後,發現Redis記憶體有效負荷還是比較低的,它一條KV大概需要至少65個位元組,但實際上我們一個計數需要8個位元組,然後Value大概4個位元組,實際上有效只有12個位元組,其他還有四十多個位元組都是被浪費掉的,這還只是單個KV,如果一條Key有多個計數的情況下,它就浪費得更多了,比如說四個計數,一個Key8個位元組,四個計數每個計數是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方式進行復制。
其他資料型別-存在性判斷
然後除了計數的話,微博還有一些業務,一些存在性判斷,比如一條微博展現的,有沒有點贊、閱讀、推薦,如果這個使用者已經讀過這個微博了,就不要再顯示給他,這種有個很大的特點,它檢查是否存在,每條記錄非常小,比如Value1個bit就可以了,但總資料量巨大。比如微博每天新發表微博1億左右,讀的可能有上百億、上千億這種總的資料需要判斷,怎麼來儲存是個很大的問題,而且這裡面很多存在性就是0,還是前面說的,0要不要存,如果存了,每天就存上千億的記錄,如果不存,那大量的請求最終會穿透Cache層到DB層,任何DB都沒有辦法抗住那麼大的流量。
我們也進行了一些選型,首先直接考慮我們能不能用Redis,單條KV65個位元組,一個KV可以8個位元組的話,Value只有1個bit,這樣算下來我每日新增記憶體有效率是非常低的。第二種我們新開發的Counter Service,單條KV Value1個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叢集內高可用、它的擴充套件性,包括它的效能,還有一個特別重要就是儲存成本,還有一些我們沒有關注到,比如21運維性如何,微博現在已經有幾千差不多上萬臺伺服器等等。
進一步優化
服務化
採取的方案首先就是對整個Cache進行服務化管理,對配置進行服務化管理,避免頻繁重啟,另外如果配置發生變更,直接用一個指令碼修改一下。
服務化還引入Cluster Manager,實現對外部的管理,通過一個介面來進行管理,可以進行服務校驗。服務治理方面,可以做到擴容、縮容,SLA也可以得到很好保障。另外對於開發來說,現在就可以遮蔽Cache資源。
總結與展望
最後簡單總結一下,對於微博Cache架構來說,從它資料架構、效能、儲存成本、服務化不同方面進行優化增強。