新浪微博關(guān)系服務(wù)與Redis的故事
新浪微博的工程師們曾經(jīng)在多個公開場合都講到過,微博平臺當(dāng)前在使用并維護著可能是世界上***的Redis集群,其中***的一個業(yè)務(wù),單個業(yè)務(wù)使用了超過 10T 的內(nèi)存,這里說的就是微博關(guān)系服務(wù)。
風(fēng)起
2009年微博剛剛上線的時候,微博關(guān)系服務(wù)使用的是最傳統(tǒng)的 Memcache+Mysql 的方案。Mysql 按 uid hash 進行了分庫分表,表結(jié)構(gòu)非常簡單:
業(yè)務(wù)方存在兩種查詢:
- 查詢用戶的關(guān)注列表:select touid from table where fromuid=?order by addTime desc
- 查詢用戶的粉絲列表:select fromuid from table where touid=?order by addTime desc
兩種查詢的業(yè)務(wù)需求與分庫分表的架構(gòu)設(shè)計存在矛盾,最終導(dǎo)致了冗余存儲:以 fromuid 為hash key存一份,以 touid 為hash key再存一份。memcache key 為 fromuid.suffix ,使用不同的 suffix 來區(qū)分是關(guān)注列表還是粉絲列表,cache value 則為 PHP Serialize 后的 Array。后來為了優(yōu)化性能,將 value 換成了自己拼裝的 byte 數(shù)組。
云涌
2011年微博進行平臺化改造過程中,業(yè)務(wù)提出了新的需求:在核心接口中增加了“判斷兩個用戶的關(guān)系”的步驟,并增加了“雙向關(guān)注”的概念。因此兩個用戶的關(guān)系存在四種狀態(tài):關(guān)注,粉絲,雙向關(guān)注和無任何關(guān)系。為了高效的實現(xiàn)這個需求,平臺引入了 Redis 來存儲關(guān)系。平臺使用 Redis 的 hash 來存儲關(guān)系:key 依然是 uid.suffix,關(guān)注列表,粉絲列表及雙向關(guān)注列表各自有一個不同的 suffix,value 是一個hash,field 是 touid,value 是 addTime。order by addTime 的功能則由 Service 內(nèi)部 sort 實現(xiàn)。部分大V的粉絲列表可能很長,與產(chǎn)品人員的溝通協(xié)商后,將存儲限定為“***的5000個粉絲列表”。
微博關(guān)系存儲Redis結(jié)構(gòu)
需求實現(xiàn):
- 查詢用戶關(guān)注列表:hgetAll uid.following ,then sort
- 查詢用戶粉絲列表:hgetAll uid.follower,then sort
- 查詢用戶雙向關(guān)注列表:hgetAll uid.bifollow,then sort
- 判斷兩個用戶關(guān)系:hget uidA.following uidB && hget uidB.following uidA
后來又增加了幾個更復(fù)雜的需求:“我與他的共同關(guān)注列表”、“我關(guān)注的人里誰關(guān)注了他”等等,就不展開來講了。
平臺在剛引入 Redis 的一段時間里踩了不少坑,舉幾個例子:
1、運維工具和流程從零開始做,運維成熟的速度趕不上業(yè)務(wù)增長的速度:在還沒來得及安排性能調(diào)優(yōu)的工作,fd 已經(jīng)達到默認配置的上限了,***我們只能趁凌晨業(yè)務(wù)低峰期重啟 Redis 集群,以便設(shè)置新的 ulimit 參數(shù);
2、平臺最開始使用的 Redis 版本是 2.0,因為 Redis 代碼足夠簡單,從引入到微博起,我們就開始對其進行了定制化開發(fā),從主從復(fù)制,到寫磁盤限速,再到內(nèi)存管理,都進行了定制。導(dǎo)致的結(jié)果是,有一段時間,微博的線上存在超過5種不同的 Redis 修改版,對于運維,bugfix,升級都帶來了巨大的麻煩。后來由田風(fēng)軍 @果爸果爸 為內(nèi)部 Redis 版本提供了不停機升級功能后,才慢慢好轉(zhuǎn)。
3、平臺有一個業(yè)務(wù)曾經(jīng)使用了非默認 db ,后來費了好大力氣去做遷移
4、平臺還有一個業(yè)務(wù)需要定期對數(shù)據(jù)進行 flush db ,以騰出空間存儲***數(shù)據(jù)。為了避免在 flush db 階段影響線上業(yè)務(wù),我們從 client 到 server 都做了大量的修改。
5、平臺每年長假前都會做一些線上業(yè)務(wù)排查,和故障模擬(2013年甚至做了一個名叫 Touchstone 的容災(zāi)壓測系統(tǒng))。2011年十一假前,我們用 iptables 將 Redis 端口的所有包都 drop 掉,結(jié)果 client 端等了 120 秒才返回。于是我們在放假前熬夜加班給 client 添加超時檢測功能,但真正上線還是等到了假期回來后。
破繭
對于微博關(guān)系服務(wù),***的挑戰(zhàn)還是容量和訪問量的快速增長,這給我們的 Redis 方案帶來了不少的麻煩:
***個碰到的麻煩是 Redis 的 hgetAll 在 hash size 較大的場景下慢請求比例較高。我們調(diào)整了 hash-max-zip-size,節(jié)約了1/3的內(nèi)存,但對業(yè)務(wù)整體性能的提升有限。***,我們不得不在 Redis 前面又擋了一層 memcache,用來抗 hgetAll 讀的問題。
第二個麻煩是新上的需求:“我關(guān)注的人里誰關(guān)注了他”,由于用戶的粉絲列表可能不全,在這種情況下就不能用關(guān)注列表與粉絲列表求交集的方式來計算結(jié)果,只能降級到需求的字面描述步驟:取我的關(guān)注人列表,然后逐個判斷這些人里誰關(guān)注了他。client 端分批并行發(fā)起請求,還好 Redis 的單個關(guān)系判斷非???。
第三個麻煩,也是***的麻煩,就是容量增長的問題了。最初的設(shè)計方案,按 uid hash 成 16 個端口,每臺 64G 內(nèi)存的機器上部署 2 個端口,每個業(yè)務(wù) IDC 機房部署一套。后來,每臺機器上就只部署一個端口了。再后來,128G 內(nèi)存的機器還沒有進入公司采購目錄,64G 內(nèi)存就即將 OOM 了,所以我們不得不做了一次端口擴容:16端口拆64端口,依然是每臺 64G 內(nèi)存機器上部署 2 個端口。再后來,又只部署一個端口。再后來,升級到 128G 內(nèi)存機器。再后來,128G 機器上出現(xiàn) OOM 了!現(xiàn)在怎么辦?
化蝶
為了從根本上解決容量的問題,我們開始尋找一種本質(zhì)的解決方案。最初選擇引入 Redis 作為一個 storage,是因為用戶關(guān)系判斷功能請求的數(shù)據(jù)熱點不是很集中,長尾效果明顯,cache miss 可能會影響核心接口性能,而保證一個可接受的 cache 命中率,耗費的內(nèi)存與 storage 差別不大。但微博經(jīng)過了 3 年的演化,最初作為選擇依據(jù)的那些假設(shè)前提,數(shù)據(jù)指標(biāo)都已經(jīng)發(fā)生了變化:隨著用戶基數(shù)的增大,冷用戶的絕對數(shù)量也在增大;Redis 作為存儲,為了數(shù)據(jù)可靠性必須開啟 rdb 和 aof,而這會導(dǎo)致業(yè)務(wù)只能使用一半的機器內(nèi)存;Redis hash 存儲效率太低,特別是與內(nèi)部極度優(yōu)化過的 RedisCounter 對比。種種因素加在一起,最終確定下來的方向就是:將 Redis 在這里的 storage 角色降低為 cache 角色。
前面提到的微博關(guān)系服務(wù)當(dāng)前的業(yè)務(wù)場景,可以歸納為兩類:一類是取列表,一類是判斷元素在集合中是否存在,而且是批量的。即使是 Redis 作為 storage 的時代,取列表都要依賴前面的 memcache 幫忙抗,那么作為 cache 方案,取列表就全部由 memcache 代勞了。批量判斷元素在集合中是否存在,redis hash 依然是***的數(shù)據(jù)結(jié)構(gòu),但存在兩個問題:cache miss 的時候,從 db 中獲取數(shù)據(jù)后,set cache 性能太差:對于那些關(guān)注了 3000 人的微博會員們,set cache 偶爾耗時可達到 10ms 左右,這對于單線程的 Redis 來說是致命的,意味著這 10ms 內(nèi),這個端口無法提供任何其它的服務(wù)。另一個問題是 Redis hash 的內(nèi)存使用效率太低,對于目標(biāo)的 cache 命中率來說,需要的 cache 容量還是太大。于是,我們又祭出 “Redis定制化”的法寶:將 redis hash 替換成一個“固定長度開放hash尋址數(shù)組”,在 Redis 看來就是一個 byte 數(shù)組,set cache 只需要一次 redis set。通過精心選擇的 hash 算法及數(shù)組填充率,能做到批量判斷元素是否存在的性能與原生的 redis hash 相當(dāng)。
通過微博關(guān)系服務(wù) Redis storage 的 cache 化改造,我們將這里的 Redis 內(nèi)存占用降低了一個數(shù)量級。它可能會失去“***的單個業(yè)務(wù)Redis集群”的頭銜,但我們比以前更有成就感,更快樂了。
【作者簡介】唐福林(@唐福林),微博技術(shù)委員會成員,微博平臺資深架 構(gòu)師,致力于高性能高可用互聯(lián)網(wǎng)服務(wù)開發(fā),及高效率團隊建設(shè)。從2010年開始深度參與微博平臺的建設(shè),目前工作重心為微博服務(wù)在無線環(huán)境下 的端到端全鏈路優(yōu)化。業(yè)余時間他是一個一歲女孩的爸爸,最擅長以45°涼開水沖泡奶粉。
感謝張龍對本文的審校。