題目與適用情境
請設計一個支援關注關係的個人化動態消息系統。使用者可以發布文字或圖片貼文、關注或取消關注作者, 並瀏覽按相關性排序的首頁動態。刪除貼文、改為私人、封鎖作者和取消關注應及時影響後續讀取;按讚、 留言和推薦模型只作為排序訊號,本題不展開媒體轉碼、留言樹、廣告競價或模型訓練。
本題假設有 5000 萬 DAU,每位活躍使用者每天讀取 20 頁,每頁 20 則;每天產生 1000 萬則貼文。 動態消息讀取 p99 小於 200 毫秒,普通作者發文到活躍粉絲可見的 p99 小於 5 秒。普通作者平均有 200 名符合投遞條件的粉絲,頭部作者最多有 5000 萬粉絲。數字與時限是面試約束,不代表任何現有 產品的實際規模或承諾。
這道題適合中高階後端、基礎設施和系統設計面試。公開面試記錄會直接追問寫擴散、讀擴散、高粉絲 作者、游標分頁、快取和一致性;第一手工程資料還顯示,真實動態消息會把候選召回、聚合、過濾和多階段 排序拆開,並保留可降級的已物化內容。因此回答重點是如何根據成本與正確性組合這些機制,而非背一張 固定架構圖。
面試官考察什麼
第一項是能否先量化讀寫不對稱。5000 萬 DAU 乘每天 20 頁等於 10 億次讀取,平均約 1.16 萬 QPS; 若尖峰為平均值的 5 倍,就是約 5.8 萬 QPS。1000 萬則貼文平均約 116 次建立每秒,但普通作者的 寫擴散會把它放大到每天約 20 億則時間線參照,即平均約 2.31 萬次插入每秒。只比較原始發文 QPS 會 低估真正的寫入壓力。
第二項是能否說明推、拉與混合策略的適用邊界。純寫擴散讓讀取便宜,卻可能因一位 5000 萬粉絲作者 產生 5000 萬次插入;純讀擴散讓發文便宜,卻要求每次讀取合併大量關注對象。高品質回答會預先計算 普通作者的候選參照,對高扇出作者在讀取時拉取,並讓門檻由活躍粉絲數、發文率、佇列預算和延遲目標 共同決定。
第三項是能否把貼文真源、候選集合與最終展示分開。時間線只儲存輕量參照,不複製正文。聚合器先合併 預先計算收件匣、頭部作者近期貼文和推薦候選,再做權限、刪除、封鎖與去重,最後分層排序和補全。排序 模型的結果不能繞過可見性規則。
第四項是能否處理動態排序下的分頁與一致性。分數會隨互動和模型版本改變,單獨使用 offset 或分數 會造成重複與漏項。答案要給出短期凍結的 feed session 或帶排序版本的複合游標,並明確新貼文、刪除、 取消關注和權限變化在翻頁期間如何表現。
最後是可復原性和可證偽性。事件匯流排、快取和扇出工作程序都會失敗或重複執行。候選人需要用冪等 參照、積壓監控、回源降級、墓碑過濾和修復掃描讓系統收斂,並用高粉絲作者突發、亂序事件、取消關注 競態、刪除洩漏和快取故障驗證設計。
回答前要確認的問題
- 動態消息來源是什麼? 本題以關注作者的貼文為主,可混入少量推薦候選;廣告、群組和話題流不展開。
- 排序契約是什麼? 預設相關性排序,要求內容新鮮且可解釋地混合來源;不承諾嚴格時間順序。
- 誰能看到貼文? 貼文有公開、僅粉絲和私人狀態;封鎖、刪除和權限收緊必須優先於快取命中與排名。
- 發文後誰需要讀到自己的內容? 作者應立即讀到自己的貼文;活躍普通粉絲目標為 5 秒內可見,
離線粉絲可在下次造訪時補齊。
- 分頁需要多穩定? 一次連續捲動工作階段中盡量無重複、無漏項;新貼文可在重新整理或新工作階段
出現,刪除和權限收緊必須在目前工作階段立即生效。
- 關注關係規模如何分布? 分布高度傾斜,普通作者平均 200 名符合投遞條件的粉絲,頭部作者可達
5000 萬;不能用平均值掩蓋熱點。
- 資料保留多久? 每位活躍使用者預先計算最近 500 則候選參照,貼文正文按產品政策保留;更舊
內容可在讀取時從作者索引和推薦儲存補充。
- 多地區要求是什麼? 讀取就近服務,貼文在作者主要地區提交後非同步複寫;跨地區新鮮度允許秒級,
可見性收緊走高優先級失效通道。
30 秒回答框架
「我會把貼文真源、關注圖和每位使用者候選收件匣分開。發文交易寫貼文與 outbox;普通作者的事件由 扇出工作程序冪等寫入活躍粉絲收件匣,頭部作者只寫作者近期索引,讀取時再拉取。Feed 聚合器合併 預先計算、頭部作者和推薦候選,先做權限、刪除、封鎖和去重,再用輕量召回分數與精排生成短期 feed session,游標按 session 和位置翻頁。正文只在最後批次補全。刪除和權限收緊寫權威墓碑並走高優先級 失效,讀取始終複核。佇列積壓時保留舊物化流並對近期關注作者做有界回源,監控發文到可見延遲、 重複率、刪除洩漏和各階段 p99。」
分步深入分析
第一步:用容量推導架構預算
每日讀取頁數為:
50,000,000 DAU × 20 pages/day = 1,000,000,000 feed reads/day
1,000,000,000 / 86,400 ≈ 11,574 average read QPS
11,574 × 5 peak factor ≈ 57,870 peak read QPS每日貼文建立平均約 10,000,000 / 86,400 ≈ 116 QPS。若所有普通作者平均向 200 名活躍粉絲 投遞,則每天產生約 20 億則候選參照,平均 2,000,000,000 / 86,400 ≈ 23,148 次插入每秒。 單一 5000 萬粉絲作者發一則就會超過普通流量的許多秒預算,因此必須繞開同步全量扇出。
每位 5000 萬活躍使用者保留 500 則候選,共 250 億則參照。若每則參照把 postid、authorid、 初始分數、時間和旗標粗略按 64 位元組計算,邏輯下限約 1.6 TB,尚未包含索引、儲存引擎開銷和副本。 這說明時間線應只儲存參照、限制長度,並按活躍度分層儲存;複製正文會迅速放大空間和失效成本。
第二步:定義所有權、API 與最小資料模型
貼文服務擁有正文、作者、建立時間、可見性、版本和刪除墓碑;社交圖服務擁有 following 與 followers 鄰接索引;Feed 服務擁有候選參照、工作階段和展示記錄。物件儲存與 CDN 承載圖片, 時間線只儲存圖片參照。核心介面可以是:
POST /v1/posts create a post with Idempotency-Key
DELETE /v1/posts/{post_id} write a deletion tombstone
PUT /v1/users/{id}/following/{author} follow an author
DELETE /v1/users/{id}/following/{author} unfollow an author
GET /v1/feed?cursor=...&limit=20 read one ranked page
GET /v1/posts/{post_id} hydrate one visible postposts 按 postid 儲存權威狀態,並有 (authorid, createdat, postid) 近期索引。follows 至少能按關注者列出關注對象,也能按作者分片列出粉絲。feedinbox 按 viewerid 分區,排序鍵包含 粗排分數、建立時間和 postid;feedsessions 短期儲存排序版本、候選 ID 序列與過期時間。展示 事件和互動事件進入獨立日誌,供排序特徵與實驗使用,不在讀取請求中同步回寫所有候選。
發文請求使用呼叫端冪等鍵。貼文與 outbox 事件在同一資料庫交易提交,成功後作者可直接從貼文真源 看到內容。事件匯流排按至少一次傳遞,(viewerid, postid) 唯一或條件覆寫讓重複扇出收斂;事件 攜帶貼文版本,舊事件不能復活已刪除或已收緊權限的貼文。
第三步:為普通作者和頭部作者使用混合扇出
普通作者發文後,扇出服務讀取符合條件的活躍粉絲分片,批次把輕量參照寫入各自收件匣。工作程序使用 分片游標和事件 ID 記錄進度;逾時重試同一批,不能因重複訊息生成兩則候選。離線或長期不活躍使用者 不必立即物化,可在其下次造訪時按關注圖補建近期候選。
頭部作者的貼文只進入 (authorid, createdat) 近期索引和熱點快取。讀取者關注這些作者時,聚合器 並行拉取每位頭部作者的有限則近期內容,與收件匣合併。門檻不應硬編碼成一個永久粉絲數:應估算 eligibleactivefollowers × postsperwindow 的寫入成本,並與預計讀取合併成本、佇列餘量和 5 秒 新鮮度預算比較。作者突然爆紅或連續發文時,控制平面可以把其切到拉取模式;已進佇列的批次允許冪等 完成或取消,不能同時無界執行兩種路徑。
純拉取仍會在使用者關注大量頭部作者時產生讀扇出。聚合器要限制每個來源的候選數、並行與截止時間, 快取作者近期索引,並在某個來源逾時時回傳其他合格候選。頭部內容還可按地區或關注群組預先聚合,但 只有測量表明合併成本是瓶頸時才增加這層複雜度。
第四步:把候選生成、過濾、排序和補全拆開
讀取路徑分四段:
- 從使用者收件匣取一批普通作者候選,並從頭部作者索引和推薦服務取有界候選;
- 按
post_id去重,檢查關注關係、封鎖、貼文墓碑、目前可見性和地區合規; - 先用便宜分數縮小候選,再用較重模型計算互動機率、時效、來源品質和負面回饋,最後套用多樣性與頻控;
- 對最終 20 則批次讀取貼文正文、作者摘要和聚合計數,生成回應並非同步記錄展示。
Meta 的公開工程說明把 Feed 聚合器描述為收集候選、物件和特徵後再排序,並使用多階段模型縮小計算; Pinterest 的公開設計也把未展示候選池、內容生成和已物化動態分開。面試中不需要複刻具體實作,但這 證明「先複製完整貼文,再在快取裡排序」缺少必要邊界。
安全與產品規則在排序前後都要守住。前置過濾減少無效計算,補全時再次檢查版本可以擋住排序期間發生的 刪除或權限收緊。模型分數只是排序輸入,不能把不可見內容重新加入結果。互動計數允許最終一致;作者、 正文、可見性和墓碑來自權威貼文版本。
第五步:用 feed session 穩定動態排序分頁
offset 會在新內容插入或分數變化時重複和跳過。單純以 (score, post_id) 做 keyset 也不夠,因為 精排分數在下一頁請求前可能變化。首屏請求可以生成最多 500 個排序後的候選 ID,並寫入 30 分鐘的 feedsessionid;不透明游標包含 session、下一位置、查詢指紋和簽章。後續頁面按位置讀取,再對 候選做即時可見性檢查和補位。
新貼文在使用者重新整理或開始新 session 時出現,不插入目前清單中間。刪除、封鎖和權限收緊立即過濾, 所以目前 session 可能少一則;聚合器從 session 後續候選補足頁面。若 session 快取遺失或過期, 伺服器回傳可識別的游標過期結果,讓用戶端保留已展示 ID 並重新整理,不能默默把舊位置套到新排名上。
如果 500 則凍結序列的儲存成本不可接受,可儲存排名 epoch 和複合 keyset,並讓用戶端提交已展示摘要; 這種方案儲存較省,但重複、模型切換和刪除補位更難。應根據連續捲動時長、允許的重複率和工作階段儲存 預算選擇,不把游標編碼格式當成核心答案。
第六步:讓關注、刪除和權限變化正確收斂
關注成功後,使用者下一次讀取可直接拉取新作者的近期貼文,同時非同步回填有限候選。取消關注與封鎖 先更新權威社交圖,讀取路徑立即過濾該作者,再非同步清理收件匣;這樣無需等待數百萬則物理刪除才能 阻止展示。關注事件與發文事件亂序時,候選中攜帶的關係版本只能用於最佳化,最終可見性仍以讀取時 權威規則為準。
刪除或改為私人時,貼文服務在提交新版本與墓碑的同一交易寫高優先級 outbox。快取失效、搜尋清理和 收件匣清理都可以非同步,但 Feed 補全必須批次讀取目前版本並過濾。熱墓碑快取縮短檢查延遲,持久 貼文記錄防止快取遺失後舊內容重現。背景掃描負責刪除過期參照;物理清理不承擔授權職責。
按讚和留言引起的分數變化不應重新寫入所有粉絲收件匣。互動事件更新聚合計數和特徵,活躍使用者在 下次請求時重排候選;極熱內容可更新共享特徵快取。這樣接受短暫排序陳舊,避免每次互動再次產生全網 扇出。
第七步:設計降級、復原與多地區邊界
扇出佇列積壓時,先保護貼文寫入與已物化流讀取。Feed 可以回傳上一份合格候選,並對使用者最近活躍的 關注作者做有界回源;頁面可容許新鮮度下降,權限檢查不能降級。若某作者造成佇列熱點,立即切換為拉取 模式並限制其未完成批次。復原後按事件時間和版本冪等追趕,監控最舊未處理事件,避免只看佇列長度。
候選快取故障時,可從持久收件匣讀取較少候選或回傳已儲存 session;排序服務逾時則使用確定性的時效 分數和多樣性規則。貼文或社交圖權威儲存不可用時,系統可以回傳已驗證且仍在安全 TTL 內的內容,超過 權限快取期限就應失敗或縮小結果,不能以可用性為由展示可能已撤權的內容。
多地區下,貼文由作者主要地區接受寫入並產生全域 ID,事件非同步複寫到讀取地區。普通內容允許秒級 新鮮度,作者本人透過主要地區讀後寫保證立即可見。刪除、封鎖和權限收緊使用更高優先級複寫,並在 全域墓碑服務確認前限制舊副本展示。跨地區主動—主動寫關注圖會引入關係衝突;只有明確要求後再增加 衝突解決規則。
第八步:用指標與故障注入證明邊界
核心指標包括發文成功率、發文到首位/95% 活躍粉絲可見延遲、按分片的扇出吞吐與最舊積壓、頭部作者 拉取耗時、候選數、去重率、過濾率、粗排和精排 p99、補全批次大小、快取命中、游標過期率、頁面重複 率、空頁率,以及刪除或撤權內容洩漏數。最後一項應始終為零,並有合成探針主動驗證。
故障注入至少覆蓋:同一發文事件重複和亂序;5000 萬粉絲作者連續發文;作者在推送中途從普通模式切到 頭部模式;關注、取消關注與發文並行;翻頁期間刪除或改私人;墓碑快取遺失;一個粉絲分片變慢;事件 匯流排停機後追趕;排序服務逾時;feed session 節點遺失;地區複寫延遲。每個情境都斷言頁面無越權 內容、候選參照可收斂、延遲降級可觀測,且復原不會重複放大寫入。
高品質示範回答
「我先按 5000 萬 DAU、每人每天 20 頁計算,平均約 1.16 萬次 feed 讀取每秒,5 倍尖峰約 5.8 萬。 1000 萬則日發文只有約 116 QPS,但普通作者平均 200 名活躍粉絲會放大為平均約 2.31 萬則候選插入 每秒;5000 萬粉絲作者的一則貼文不能同步全推。
我會把貼文真源、社交圖和候選收件匣分開。發文交易寫貼文和 outbox。普通作者的事件按粉絲分片非同步 寫入每位使用者收件匣,viewerid + postid 冪等;頭部作者只寫作者近期索引,Feed 讀取時拉取並 合併。推拉門檻按活躍粉絲、發文率、佇列預算和讀取成本動態決定。時間線只存參照,正文和圖片在最後 批次補全。
讀取時,聚合器從收件匣、頭部作者和推薦來源取有界候選,先按目前關注、封鎖、墓碑和可見性過濾、去重, 再做便宜召回、精排、多樣性與頻控。首屏把最多 500 個候選凍結成 30 分鐘 feed session,游標攜帶 session 和位置;新貼文等重新整理出現,刪除和權限收緊在每頁補全時立即過濾並補位。
刪除與改私人提交權威版本和高優先級失效事件,快取和收件匣可以稍後清理,但每次展示必須複核。佇列 積壓時回傳舊物化流,並對近期作者做有界回源;排序逾時降級到時效分數。權限檢查永不降級。最後用 發文到可見延遲、最舊積壓、各排序階段 p99、重複率和撤權洩漏監控系統,並注入高粉絲作者突發、 重複事件、取消關注競態、刪除、快取與地區故障驗證復原。」
常見錯誤
- 只比較原始讀寫 QPS。 發文只有約 116 QPS,粉絲扇出才是主要寫入量;應把寫放大和頭尾分布
一起計算。
- 純寫擴散到所有粉絲。 一位 5000 萬粉絲作者會製造 5000 萬次插入;應對高扇出作者讀時拉取。
- 把「高粉絲作者門檻」寫成永久常數。 粉絲活躍度、發文頻率和佇列餘量都會變化;應基於成本與
SLO 動態分類並保留切換保護。
- 在時間線複製完整正文。 編輯、刪除和權限變化會產生巨大的失效面;收件匣只儲存輕量參照,
展示前補全並複核版本。
- 用
offset分頁動態排名。 插入與重排會造成重複和漏項;使用短期 feed session 或帶版本的
複合游標。
- 只在非同步任務中過濾取消關注和刪除。 清理延遲會洩漏內容;權威關係、墓碑和可見性必須在
讀取時檢查。
- 聲稱事件匯流排提供端到端 exactly-once。 工作程序、儲存和重試仍可能重複;用事件 ID、貼文
版本與候選唯一鍵實現冪等收斂。
- 降級時跳過權限檢查。 陳舊排序和較少候選可以接受,越權展示不可接受;安全過濾有獨立預算和
失敗策略。
- 只測正常流量。 設計的關鍵風險來自頭部作者、積壓、亂序和權限競態;故障注入必須覆蓋這些邊界。
追問及應對
追問一:普通作者突然獲得 5000 萬粉絲並連續發文,怎樣避免兩種路徑重複?
控制平面按版本把作者切換到拉取模式。扇出任務讀取作者模式版本,只允許開始前已認領的有限批次完成; 候選以 (viewerid, postid) 冪等,因此已推送與讀取拉取合併後只保留一則。佇列對單一作者設配額, 切換期間的發文直接進入頭部作者索引。
追問二:使用者取消關注後,舊 feed session 裡還有該作者貼文怎麼辦?
session 只凍結候選順序,不凍結授權。每頁補全都批次檢查目前關注與封鎖關係;已取消關注作者被過濾, 並從後續候選補位。非同步清理收件匣用於節省空間,不能承擔即時權限語義。
追問三:怎樣支援嚴格按時間排序?
重用同一候選來源和權限過濾,把排序鍵改為 (createdat, postid),使用帶上界時間的 keyset 游標。 目前工作階段固定 snapshot_time,新貼文在重新整理後出現。頭部作者與普通收件匣仍需歸併,但無需 精排 session。
追問四:排序模型發布後重複率突然升高,如何定位?
先按模型版本比較候選去重前後數量、session 生成 ID、游標位置和已展示記錄,確認重複來自候選來源、 排序輸出還是工作階段遺失。若新模型給同一貼文不同來源別名,統一到權威 post_id;若 session 快取 抖動,回復模型也不會修復,應恢復工作階段儲存並讓用戶端明確重新整理。
追問五:如何判斷混合扇出門檻是否合適?
離線重播真實關注度和活躍度分布,分別估算每位作者的預先計算寫入、佇列等待、讀取合併與快取命中成本; 線上逐步調整門檻,觀察發文到可見 p99、Feed 讀取 p99、總儲存寫入和降級率。門檻應帶遲滯,避免作者 在兩種模式間頻繁抖動。