1. 程式人生 > >[聊聊架構] 日請求量過億,談陌陌的 Feed 服務優化之路

[聊聊架構] 日請求量過億,談陌陌的 Feed 服務優化之路

場景和架構介紹     先從產品層⾯面介紹一下Feed業務。Feed本⾝身就是一段簡短文字加一張圖片,帶有位置資訊,釋出之後可以被好友和附近的人看到,通過點贊評論的方式互動。類似微博和朋友圈。     陌陌上季度的MAU為6980萬,Feed作為主要的社交業務,從2013年上線到現在,日請求量超過億,總資料量超過百億。下面是Feed系統的整體架構圖:     
        * 資源層主要使用Redis、MongoDB、HBase等NoSQL型別資料庫。         * 儲存層是內部RPC服務,根據業務場景和儲存特性,組合各種資料庫資源。         * 業務層呼叫儲存層讀寫資料,實現產品邏輯,直接面向用戶使用。 內容儲存(Feed Content)
    首先介紹Feed內容海量資料儲存優化。Feed內容是json格式,對schema沒有做嚴格限制,可以根據業務靈活擴充套件,下面是一個基礎的結構:         {"content":"今天天⽓氣不錯啊","id":"88888888","time":1460198781,"owner":"25927525"}     最初使用MongoDB做持久化儲存,MongoDB本身對JSON⽀支援非常好,這樣從前端API的輸出格式到底層資料儲存都是統一的,非常簡潔。         MongoDB另外一個優勢是不需要預先定義結構,靈活的增減欄位,支援複雜查詢,非常適合創業階段快速迭代開發。     Feed內容儲存是整個系統訪問量最大的部分,QPS達到幾十萬,MongoDB的查詢效能不能滿足線上要求,在前端使用Redis叢集做LRU快取,快取id和對應的content。
    移動社交產品的熱點資料大部分是最近產生的,LRU快取可以扛住99.5%的線上請求通過監控miss(每秒穿透)和evict(每秒逐出)兩個指標,用來評估快取容量,適時擴容。     這裡特別說一下,為了快速大規模擴容,Redis的LRU快取叢集沒有采用一致性hash,而是最簡單Mod取餘的hash方式通過節點資料複製,快速的翻倍擴容,省去了快取預熱的過程。     隨著資料量的增長,MongoDB需要不斷擴容,單個MongoDB例項佔用空間接近硬碟的上限。而且讀效能太低成為瓶頸。最終將持久化遷移到Hbase,廢棄掉了MongoDB在線上的使用。 好友動態( Feed timeline)
    接下來介紹好友動態(timeline)實現和優化過程。好友動態(timeline)通過好友關係聚合內容,按時間排序,類似微信的朋友圈。     Timeline使用Redis的zset結構做儲存,天然有序,支援原子的增/刪/查詢操作。和早期SNS系統MySQL+Memcached相比,實現簡單很多,大部分業務一行程式碼搞定:         1. ZADD timeline 1460198781 88888888     //插入一條feed_id為88888888的Feed,插入時間為1460198781         2. ZREVRANGE timeline 0 100                     //檢視最近的100條Feed, Redis Zrevrange 命令返回有序集中,指定區間內的成員。 其中成員的位置按分數值遞減(從大到小)來排列。     關於Feed系統的推(push)模式和拉(pull)模式有很多討論。     陌陌最初使用的是推的模式,也就是釋出Feed後,非同步插入到每個好友的timeline。這種方式讀取效率高,可以看作O(1)的操作。但是寫操作開銷大,每秒1000條Feed,每人N個好友,會產生1000*N的OPS,而且一個feed_id重複儲存N次,產生大量冗餘資料。     隨著使用者產生資料的積累,長尾效應明顯,冷資料佔比會越來越高。而且redis對小zset採用ziplist的方式緊湊儲存,列表增長會轉換為skiplist,記憶體利用率下降。儲存timeline的Redis叢集近百臺伺服器,成本太高,推動改造為拉的模式。     通過timeline聚合層,根據使用者的好友關係和個人Feed列表,找到上次訪問之後產生的新Feed,增量實時聚合新內容。大致步驟:         1. 遍歷我的好友,找到最近發表過Feed的人         2. 遍歷最近發表過Feed的人,得到id和time         3. 合併到我的timeline     聚合過程採用多執行緒並行執行,總體聚合時間平均20ms以下,對查詢效能影響很小。     改為拉模式後,timeline從儲存變為快取,冷資料可以被淘汰刪除,timeline不存在的則觸發全量聚合,效能上也可以接受。redis叢集只快取最近的熱點資料,解決了儲存成本高的問題,伺服器規模下降了一個數量級。 附近動態 (Nearby Feed)     最後介紹LBS的附近動態空間查詢效能優化,也是有特色的地方。     陌陌上每一條Feed都帶有經緯度資訊,附近動態是基於位置的timeline,可以看到附近5公里範圍內最新的Feed。技術上的難點在於每個人的位置都不一樣,每個人看到內容也不同,需要實時計算無法快取。     第一個版本用mongo的2D索引實現空間查詢:         feeds.find({location : {"$near" : [39.9937,116.4361]}}).sort({time:-1});     由於mongo的2D查詢不能建立聯合索引,按時間排序的話,效能比較低,超過100ms。通過資料檔案掛載在記憶體盤上和按地理位置partition的方法,做了一些優化,效果還是不理想。     第二個版本,採用geohash演算法實現了更高效的空間查詢。     首先介紹geohash。geohash將二維的經緯度轉換成字串,例如經緯度39.9937,116.4361對應的geohash為wx4g9。每個geohash對應一個矩形區域,矩形範圍內的經緯度的geohash是相同的。     根據Feed的經緯度,計算geohash,空間索引使用Redis的zset結構,將geohash作為空間索引的key,feed_id作為member,時間作為score。 查詢時根據使用者當前經緯度,計算geohash,就能找到他附近的Feed。但存在邊界問題,附近的Feed不一定在同一個矩形區域內。如下圖:     
    解決這個問題可以在查詢時擴大範圍,除了查詢使用者所在的矩形外,還擴散搜尋相鄰的8個矩形,將9個矩形合併(如下圖),按時間排序,過濾掉超出距離範圍的Feed,最後做分頁查詢。     歸納為四個步驟:ExtendSearch -> MergeAndSort -> DistanceFilter -> Skip。     但是這種方式查詢效率比較低,作為讀遠遠大於寫的場景,換了一種思路,在更新Feed空間索引時,將Feed寫入相鄰的8個矩形,這樣每個矩形還包含了相鄰矩形的Feed,查詢省去了ExtendSearch和MergeAndSort兩個步驟通過資料冗餘的方式,換取了更高的查詢效率。     (通過GEOHash)將複雜的geo查詢,簡化為redis的zrange操作,效能提高了一個數量級,平均耗時降到3ms。空間索引通過geohash分片到redis節點具有資料分佈均勻、方便擴容的優勢。 總結     陌陌的Feed服務大規模使用Redis作為快取和儲存,Redis的效能非常高,瞭解它的特性,並且正確使用可以解決很多大規模請求的效能問題。通常記憶體的故障率遠低於硬碟的故障率,生產環境Redis的穩定性是非常高的。通過合理的持久化策略和一主多從的部署結構,可以確保資料丟失的風險降到最低。     另外,陌陌的Feed服務構建在許多內部技術框架和基礎元件之上,本文偏重於業務方面,沒有深入展開,後續有機會可以再做介紹。 互動問答 問題:MongoDB採用什麼叢集方式部署的,如果資料量太大,採用什麼方式來提高查詢效能?     我們通過在mongo客戶端按id做hash的方式分片。當時MongoDB版本比較低,複製集(repl-set)還不太成熟,沒有在生產環境使用。除了建索引以外,還可以通過把mongo資料檔案掛載在記憶體盤(tmpfs)上提高查詢效能,不過有重啟丟資料的風險。 問題:使用者的關係是怎麼儲存的呢 還有就是獲取好友動態時每條feed的使用者資訊是動態從Redis或者其他地方讀取呢?     陌陌的使用者關係使用Redis儲存的。獲取好友動態是的使用者資訊是通過feed的ownerId,再去另外一個使用者資料服務(profile服務)讀取的,使用者資料服務是陌陌請求量最大的服務,QPS超過50W。 問題:具體實現用到Redis解決效能問題,那Redis的可用性是如何保證的?萬一某臺旦旦機資料怎麼保證不丟失的?     Redis通過一主一從或者多從的方式部署,一臺機器宕機會切換到備用的例項。另外Redis的資料會定時持久化到rdb檔案,如果一主多從都掛了,可以恢復到上一次rdb的資料,會有少量資料丟失。 問題:Redis這麼高效能是否有在應用伺服器上做本地儲存,如果有是如何做Redis叢集與本地資料同步的?     沒有在本地部署Redis,應用伺服器部署的都是無狀態的RPC服務問題:Redis一個叢集大概有多少個點? 主從之間同步用的什麼機制? 直接mod問題多嗎?     一個Redis叢集幾個節點到上百個節點都有。大的叢集通過分號段再mod的方式hash。Redis 3.0的cluster模式還沒在生產環節使用。使用的Redis自帶的主從同步機制。 問題:文中提到Redis使用mod方式分片,新增機器時進行資料複製,複製的過程需要停機麼,如果不停資料在動態變化,如何處理?     主從同步的方式複製資料不需要停機,擴容的過程中一直保持資料同步,從庫和主庫資料一致,擴容完成之後從庫提升為主庫,關閉主從同步。 問題:Redis宕機後的主從切換是通過的哨兵機制嗎?在主從切換的時候,是有切換延時的,這段時間的寫入主的資料是否會丟失,如果沒丟,怎麼保證的?     通過內部開發的Sentinel系統,檢測Reids是否可用。為了防止誤切,切換會有一定延遲,多次檢測失敗才會切換。如果主庫不可用會有資料丟失,重要資料的寫入,在業務上有重試機制。 陌陌Feed讀後總結 Redis 採用Mod取餘Hash的方式. 通過節點資料複製,快速的翻倍擴容,省去了快取預熱的過程。 原文這部分沒有細說.
假如原來有三個redis例項, 並且各自帶了Slave. 通過 id mod 3 取餘,判斷存放位置. 擴容的時候,僅僅需要把Slave Readonly去掉, 前端通過 id mod 6 取餘, 判斷存放位置即可,同時斷掉Redis 主從複製. 這時候備機就成了寫入機。 同時,將這三個原來三個主機標號成為 4,5,6。 因為 id mod 6 == 0 的情況 必定 id mod 3 == 0。比如 10 mod 3 = 1 ,現在改為 10 mod 6 = 4 正好落在原來的備機上. 待完成之後,需要清理每個例項上一半冗餘的資料.不過一般設定了過期時間,可以等待他自然過期. 這樣的擴容方式,每次需要擴一倍。