如何設計 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中存儲活動用戶
- 搜索集羣可能需要將推文保存在內存中以保持低延遲