日請求量過億,談陌陌的 Feed 服務優化之路

e21er2131 8年前發布 | 34K 次閱讀 Redis Feed 分布式/云計算/大數據

摘要: 先從產品層?面介紹一下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

關于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,最后做分頁查詢。

wx4g9相鄰的8個 geohash :wx4gb,wx4gc,wx4gf,wx4g8,wx4gd,wx4g2,wx4g3,wx4g6。

歸納為四個步驟: ExtendSearch ->  MergeAndSort ->  DistanceFilter ->  Skip

但是這種方式查詢效率比較低,作為讀遠遠大于寫的場景,換了一種思路,在 更新Feed空間索引 時,將Feed寫入相鄰的8個矩形,這樣每個矩形還包含了相鄰矩形的Feed,查詢省去了

ExtendSearch和MergeAndSort兩個步驟。

通過數據冗余的方式,換取了更高的查詢效率。將復雜的geo查詢,簡化為redis的zrange操作,性能 提高了一個數量級 ,平均耗時降到3ms。空間索引通過geohash分片到redis節點,具有 數據分布均勻方便擴容 的優勢。

總結

陌陌的Feed服務大規模使用Redis作為緩存和存儲,Redis的性能非常高,了解它的特性,并且正確使用可以解決很多大規模請求的性能問題。通常內存的故障率遠低于硬盤的故障率,生產環境Redis的穩定性是非常高的。通過合理的持久化策略和一主多從的部署結構,可以確保數據丟失的風險降到最低。

另外,陌陌的Feed服務構建在許多內部技術框架和基礎組件之上,本文偏重于業務方面,沒有深入展開,后續有機會可以再做介紹。

互動問答

問題:MongoDB采用什么集群方式部署的,如果數據量太大,采用什么方式來提高查詢性能?

我們通過在mongo客戶端按id做hash的方式分片。當時MongoDB版本比較低,復制集(repl-set)還不太成熟,沒有在生產環境使用。除了建索引以外,還可以通過把mongo數據文件掛載在內存盤(tmpfs)上提高查詢性能,不過有重啟丟數據的風險。

問題:用戶的關系是怎么存儲的呢 還有就是獲取好友動態時每條feed的用戶信息是動態從Redis或者其他地方讀取呢?

陌陌的用戶關系使用Redis存儲的。獲取好友動態是的用戶信息是通過feed的owner,再去另外一個用戶資料服務(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是否可用。為了防止誤切,切換會有一定延遲,多次檢測失敗才會切換。如果主庫不可用會有數據丟失,重要數據的寫入,在業務上有重試機制

來自: https://www.sdk.cn/news/3344

 本文由用戶 e21er2131 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!