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