【系統設計】如何設計 Twitter 時間線和搜尋?
阿新 • • 發佈:2022-04-19
如何設計 Twitter 時間線和搜尋?
1.業務場景
業務場景如下:
-
使用者釋出推文
- 服務將推文推送給關注者,傳送推送通知和電子郵件
- 使用者檢視使用者時間線(來自使用者的活動)
- 使用者檢視主頁時間線(使用者關注的人的活動)
- 使用者搜尋關鍵字
- 服務具有高可用性
其他場景:
- 服務將推文推送到 Twitter Firehose 和其他流
- 服務根據使用者的可見性設定刪除推文
- 如果使用者沒有關注被回覆的人,則隱藏回覆
- 尊重“隱藏轉發”設定
- 分析
2.業務要求
假設如下:
- 流量分佈不均
- 釋出推文應該很快
- 向所有關注者傳送推文應該很快,除非你有數百萬關注者
- 1億活躍使用者
- 每天 5 億條推文或每月 150 億條推文
- 每條推文平均扇出 10 次交付
- 每天通過扇出發送 50 億條推文
- 每月通過扇出發送 1500 億條推文
- 每月 2500 億次讀取請求
- 每月 100 億次搜尋
時間線
- 檢視時間線應該很快
- Twitter 閱讀量大於寫入量
- 優化推文的快速閱讀
- 攝取推文寫得很重
搜尋
- 搜尋應該很快
- 搜尋是重讀
簡單的對業務要求進行計算,轉換成業務指標
- 每條推文的大小:
-
tweet_id
- 8 個位元組 -
user_id
- 32 位元組 -
text
- 140 位元組 -
media
- 平均 10 KB - 總計:~10 KB
-
- 每月 150 TB 的新推文內容
- 每條推文 10 KB * 每天 5 億條推文 * 每月 30 天
- 3 年內 5.4 PB 的新推文內容
- 每秒 10 萬個讀取請求
- 每月 2500 億次讀取請求 *(每秒 400 次請求 / 每月 10 億次請求)
- 每秒 6,000 條推文
- 每月 150 億條推文 *(每秒 400 條請求 / 每月 10 億條請求)
- 每秒扇出 6 萬條推文
- 每月通過扇出發送 1500 億條推文 *(每秒 400 個請求 / 每月 10 億個請求)
- 每秒 4,000 個搜尋請求
- 每月 100 億次搜尋 *(每秒 400 次請求 / 每月 10 億次請求)
方便的轉換指南:
- 每月 250 萬秒
- 每秒 1 個請求 = 每月 250 萬個請求
- 每秒 40 個請求 = 每月 1 億個請求
- 每秒 400 個請求 = 每月 10 億個請求
3.系統設計
1.系統設計
我們必須進行必要的服務拆分
-
Timeline Service : 時間線服務,獲取儲存在Memory Cache中的時間線資料,包含使用者ID和推文ID
- TWeet Info Service: 推文資訊服務,獲取有關推文ID的附加資訊
- User Info Service : 使用者資訊服務,獲取有關UserID的附加資訊
-
Fan Out Service:扇出服務,A釋出推文後,通知關注了A的所有使用者,A發了新推文
-
User Graph Service : 使用者關係服務,提供使用者之間的關係圖,比如A使用者關注了哪些使用者
-
Search Service : 關鍵字搜尋服務,全文檢索(搜尋叢集,Lucene)
-
Notification Service: 通知服務,向某使用者傳送推文通知(你關注的使用者xx發了新推文)
-
2.用例實現
用例1:使用者釋出推文
我們可以將使用者自己的推文儲存在關係資料庫中以填充使用者時間線(來自使用者的活動)。
我們可以將照片和視訊等儲存在 Object Store
- Client將推文釋出到Web Server,作為反向代理執行
- Web Server將請求轉發到Write API Server
- Write API Server將推文儲存在SQL 資料庫上的使用者時間軸中
-
Write API Server 聯絡 Fan Out 服務,該服務執行以下操作:
- 查詢 User Graph 服務,查詢 記憶體快取中儲存的使用者關注者
- 將推文儲存在記憶體快取中使用者關注者的主頁時間線中
- O(n) 操作:1,000 個關注者 = 1,000 次查詢和插入
- 將推文儲存在Search Service中以實現快速搜尋
- 在Object Store中儲存媒體資料
- 使用Notification Service 服務向關注者傳送推送通知:
- 使用佇列(未圖示)非同步傳送通知
記憶體快取如果使用redis,可以使用如下結構的redis列表
tweet n+2 tweet n+1 tweet n
| 8 bytes 8 bytes 1 byte | 8 bytes 8 bytes 1 byte | 8 bytes 8 bytes 1 byte |
| tweet_id user_id meta | tweet_id user_id meta | tweet_id user_id meta |
新的推文也會被放在redis中,該快取會填充使用者的主頁時間線(來自使用者關注人的活動)
$ curl -X POST --data '{ "user_id": "123", "auth_token": "ABC123", \
"status": "hello world!", "media_ids": "ABC987" }' \
https://twitter.com/api/v1/tweet
響應
{
"created_at": "Wed Sep 05 00:37:15 +0000 2012",
"status": "hello world!",
"tweet_id": "987",
"user_id": "123",
...
}
內部通訊,可以用grpc
用例2:使用者檢視主頁時間線
- Client向Web Server釋出主時間線請求
- Web Server將請求轉發到Read API Server
- Read API Server 與 Timeline Service聯絡,後者執行以下操作:
- 獲取儲存在記憶體快取中的時間線資料,包含推文 ID 和使用者 ID - O(1)
- 使用multiget查詢Tweet Info Service以獲取有關推文 ID 的附加資訊 - O(n)
- 使用 multiget查詢User Info Service以獲取有關使用者 ID 的附加資訊 - O(n)
$ curl https://twitter.com/api/v1/home_timeline?user_id=123
響應:
{
"user_id": "456",
"tweet_id": "123",
"status": "foo"
},
{
"user_id": "789",
"tweet_id": "456",
"status": "bar"
},
{
"user_id": "789",
"tweet_id": "579",
"status": "baz"
},
用例3:使用者檢視使用者自己的時間線
- Client向Web Server釋出使用者時間線請求
- Web Server將請求轉發到Read API Server
- Read API Server 從SQL 資料庫中檢索使用者時間線
類似於用例2的檢視主頁時間線,除了所有推文都來自使用者自己而不是使用者關注的人。
用例4:使用者搜尋關鍵字
- Client向Web Server傳送搜尋請求
- Web Server將請求轉發到Search API Server
-
Search API Server 聯絡Search Service,它執行以下操作 :
- 解析/標記輸入查詢,確定需要搜尋的內容
- 刪除標記
- 將文字分解為術語
- 修正錯別字
- 規範大寫
- 將查詢轉換為使用布林運算
- 查詢搜尋叢集(即Lucene)以獲取結果:
- Scatter 收集叢集中的每個伺服器以確定是否有任何查詢結果
- 合併、排名、排序並返回結果
- 解析/標記輸入查詢,確定需要搜尋的內容
$ curl https://twitter.com/api/v1/search?query=hello+world
除了與給定查詢匹配的推文外,響應將類似於主時間線的響應。
4.系統優化
優化要點:
- DNS
- CDN
- Load Balancer:負載均衡
- SQL Read Relicas :讀多副本
- SQL Write Master-Slave :寫主從模式
關於扇出服務的效能瓶頸:一個幾百萬的使用者A發推文,可能需要幾分鐘,才能通知到關注了A的使用者,A傳送了新的推文:
當用戶A關注人數到達一定閾值的時候,可以讓Client主動搜我關注的A有沒有新發推文
其他優化:
- 在記憶體快取中只保留每個家庭時間線的數百條推文
-
僅在記憶體快取中保留活動使用者的主頁時間線資訊
- 如果使用者在過去 30 天內未處於活動狀態,我們可以從SQL 資料庫重建時間線
- 查詢User Graph以確定使用者正在關注誰
- 從SQL 資料庫中獲取推文並將它們新增到記憶體快取中
- 如果使用者在過去 30 天內未處於活動狀態,我們可以從SQL 資料庫重建時間線
- Tweet Info Service中僅儲存一個月的推文
- 僅在User Info Service中儲存活動使用者
- 搜尋叢集可能需要將推文儲存在記憶體中以保持低延遲