題幹與適用場景
設計一個供多個團隊使用的集中式日誌系統。10 萬個服務實例、容器和批次工作在穩態下每秒產生 100 萬筆日誌,平均每筆原始日誌 600 位元組。一次大規模故障可能讓流量持續 15 分鐘升到每秒 300 萬筆,因為失敗請求、重試和例外堆疊會同時增加。最近日誌從產生到可搜尋的 p99 目標為 30 秒; 帶租戶、時間、服務、環境和等級過濾的最近 15 分鐘常見查詢,p95 目標小於 2 秒。
使用者需要按時間、服務、主機、等級和 traceId 查詢,也需要受限的全文檢索、下載和告警。不同日誌類別 有不同保留與可靠性要求:除錯日誌可以短期保存並在緊急情況下取樣;執行錯誤日誌需要可靠留存;安全稽核 日誌不得被普通降級策略丟棄。系統還要支援租戶配額、欄位遮蔽、存取稽核、生命週期管理和歷史恢復。
這些規模、延遲、保留期和可靠性目標都是面試輸入,不代表任何日誌產品的公開承諾。範圍不包括實作一個 完整搜尋引擎、在面試中重寫訊息佇列,也不要求複製某家公司的內部架構。2026 年的公開系統設計題仍直接 要求候選人設計支援突發寫入、檢索、保留和多租戶隔離的集中式日誌平台。OpenTelemetry 的穩定日誌資料 模型、Collector 的佇列故障說明,以及 Elastic 對資料分層和 mapping explosion 的文件,都能為關鍵 工程邊界提供第一手依據。
面試官考察點
第一,看候選人是否把「可靠接收」與「立即可搜尋」分開。閘道把日誌寫入複製的持久訊息流後,就可以確認 中央系統接收成功;搜尋索引可以非同步落後數十秒。如果只有寫入搜尋叢集才算成功,索引維護或擴容會直接 反壓全部生產者。
第二,看候選人能否正視事故期間的相關性故障。最需要日誌時,應用程式錯誤、重試風暴和搜尋叢集壓力往往 同時出現。答案必須包含本機磁碟緩衝、中央持久佇列、背壓、有限容量、分級丟棄和可觀測的遺失計數,不能 只說「增加 Kafka 和 Elasticsearch」。
第三,看索引策略是否有成本與基數意識。任意 JSON 屬性全部動態建立索引,會讓高基數欄位製造大量映射、 字典和小分片。合理設計只對穩定、常查、型別明確的欄位建立索引;其他屬性仍可保存,但預設不參與索引。
第四,看多租戶隔離是否貫穿寫入與查詢。租戶身分必須來自認證憑證,不能相信日誌內容中的 tenantId。 一個高流量租戶、寬泛正規表示式查詢或欄位爆炸,不能耗盡共享佇列、索引執行緒、快取和查詢併發。
最後,看容量、交付語意、安全和驗證能否閉環。候選人要計算原始吞吐與突發積壓,明確 at-least-once 會產生重複,說明稽核日誌與除錯日誌為何需要不同策略,並能透過端到端 canary 證明 「沒有靜默遺失」。
回答前需要澄清的問題
- 哪些日誌必須不遺失? 稽核與安全事件要求可靠留存;除錯日誌允許在明確降級策略下取樣或丟棄。
- 確認成功的邊界在哪裡? 中央閘道將事件提交到跨可用區複製的持久訊息流後確認,不等待搜尋索引刷新。
- 生產者能被日誌系統阻塞嗎? 普通應用日誌不能同步阻塞業務請求;節點代理使用非同步批次和有限本機磁碟。
- 查詢模式是什麼? 主要是帶租戶和時間範圍的結構化過濾、
traceId精確查找,以及有限視窗的全文檢索。 - 是否要求全域順序? 不要求。只盡量保持單一來源實例內的順序,跨來源用時間戳記和觀察時間輔助排序。
- 保留多久? 假設除錯日誌熱索引 24 小時、原始歸檔 7 天;執行日誌熱索引 7 天、歸檔 90 天;
稽核日誌可搜尋 30 天並不可變歸檔 365 天。
- 需要跨區域主動寫入嗎? 主設計是每個區域獨立採集並寫本區域管線,控制平面統一;災備查詢可以讀歸檔。
- 是否索引所有欄位? 不。穩定的頂層欄位與白名單屬性可索引,任意高基數屬性預設只儲存。
- 敏感資料怎麼處理? SDK 和節點代理優先阻止或遮蔽,中央處理器再次檢查;金鑰和權杖不應進入日誌。
- 允許多大查詢範圍? 預設強制時間範圍、分頁游標、掃描預算、併發限制和取消;大範圍匯出走非同步工作。
30 秒回答框架
「我會讓應用程式寫標準輸出或非同步 SDK,由每台節點代理負責批次、遮蔽和有限磁碟緩衝。區域閘道完成 認證、租戶配額和格式檢查,再把日誌寫入跨可用區複製的持久訊息流;成功確認以這裡為邊界。處理器從訊息流 讀取,規範化欄位,並把原始記錄寫入物件儲存,把允許檢索的欄位和部分正文寫入熱搜尋索引。查詢服務強制 租戶與時間範圍,並按時間視窗路由熱、溫和歸檔資料。穩態原始流量約 600 MB/s、51.84 TB/日;15 分鐘 3 倍突發相對穩態會多積壓約 1.08 TB。系統採用 at-least-once,使用 eventId 去重;佇列或索引故障時 優先保留稽核和錯誤日誌,先取樣除錯日誌,並把每次丟棄變成可告警指標。」
分步驟深入解答
先定義六個不變量:
- 日誌呼叫不能讓普通業務請求同步依賴遠端日誌平台。
- 中央系統只有在事件進入複製的持久緩衝後才確認接收成功。
- 租戶身分由認證上下文確定,寫入、儲存、查詢和匯出都不能越過租戶邊界。
- 稽核日誌不能使用除錯日誌的取樣與丟棄策略。
- 任意使用者屬性預設不動態建立索引,高基數欄位不能無限擴展 schema。
- 每個丟棄、延遲、解析失敗和遮蔽失敗都必須可計量,不能靜默發生。
第一步:定義採集路徑和確認邊界。
應用程式 stdout / 非同步 SDK
-> 節點代理或 sidecar:批次、壓縮、遮蔽、本機磁碟 spool
-> 區域採集閘道:認證、tenant 綁定、配額、強制遮蔽、格式與大小限制
-> 跨可用區持久訊息流
-> 規範化與路由處理器
-> 物件儲存中的原始歸檔
-> 熱搜尋索引
-> 告警與串流訂閱
-> 查詢協調器 -> 熱 / 溫 / 歸檔讀取應用程式優先寫標準輸出,讓節點代理與業務行程解耦;有明確結構化事件需求時可以用非同步 SDK,但 SDK 也只寫記憶體佇列或本機代理。代理按位元組和時間批次傳送,限制單筆大小,保存目前位置,並在網路故障時 寫有限磁碟 spool。磁碟滿後必須執行按類別定義的策略,不能無限阻塞或無限占用磁碟。
區域閘道從 mTLS 身分、工作負載身分或短期憑證解析租戶、服務和環境,覆蓋日誌內容中偽造的同名欄位。 它做限流、壓縮解碼、基本 schema 檢查、最大事件限制,並在進入中央持久緩衝前執行必要的金鑰與權杖 遮蔽。事件被複製到至少跨可用區的持久訊息流後返回確認。搜尋索引、物件歸檔和告警都是下游消費者, 因此某個消費者故障不會讓生產者直接依賴它。
第二步:設計統一但有邊界的資料模型。
LogEvent(
eventId, timestamp, observedTimestamp,
tenantId, service, environment, instance,
severityNumber, severityText, body,
traceId, spanId, schemaVersion,
attributes, sensitivityClass
)timestamp 是事件發生時間,observedTimestamp 是採集系統首次觀察到事件的時間。機器時鐘偏差時,查詢 仍可看到採集順序和延遲。traceId 與 spanId 讓日誌關聯分散式追蹤。severityNumber 支援統一比較, severityText 保留來源表達。schemaVersion 允許處理器按版本升級解析規則。
穩定頂層欄位使用明確型別並建立索引。attributes 保存額外結構化資訊,但只有註冊過的白名單欄位可進入 索引映射;未知欄位可以放入扁平物件、鍵值欄或原始正文。若同一欄位一會兒是數字、一會兒是物件,處理器 把它隔離為解析失敗或版本化欄位,不能讓一次錯誤部署破壞整個共享索引。
eventId 可由代理按來源實例、啟動紀元和本機序號產生。系統採用 at-least-once,因此代理未收到確認會 重送,消費者和索引寫入以 eventId 冪等。去重視窗有限,歸檔中也可能保留重複;查詢和統計必須知道這一 語意,不能宣稱昂貴且脆弱的端到端 exactly-once。
第三步:規劃分區與多租戶隔離。
訊息流按虛擬分區擴展。路由鍵可由 (tenantId, sourceInstance) 雜湊產生,以維持單一來源的大致順序, 同時把一個租戶分散到多個分區。只按 tenantId 分區會讓大租戶形成熱點;只按隨機鍵又會失去來源內順序。 大型租戶可以分配獨立分區池,小租戶共享池,並透過控制平面動態調整,而不改變事件格式。
每個租戶有寫入位元組率、事件率、突發桶、本機與中央 backlog、索引欄位數、熱儲存量、查詢併發、掃描 位元組和匯出工作配額。配額拒絕和降級按租戶記錄。系統級過載時,優先級可以是:
- 保留稽核和安全事件。
- 保留錯誤和關鍵執行事件。
- 對重複告警、資訊和除錯日誌按已宣告策略取樣。
- 拒絕新的低優先級大範圍查詢和匯出。
共享熱索引適合小租戶,但必須在每個文件、快取鍵和查詢計畫中帶可信 tenantId。高合規或超大租戶可進入 獨立索引與加密邊界。不能為每個小租戶每天建立大量空分片,也不能把所有租戶塞進沒有配額的單一索引。
第四步:把原始歸檔與搜尋索引分開。
物件儲存保存規範化後的原始事件,按區域、租戶、日期、小時和可選服務進行大檔分區,並使用欄式格式與 壓縮。它是低成本長期真相,可用於合規匯出、歷史查詢以及重建損壞的熱索引。物件寫入先聚合成合適大小的 檔案,避免每筆日誌一個物件。
熱搜尋索引只保存近期和允許檢索的資料。穩定欄位使用倒排或欄式索引;正文全文索引可以按類別啟用。 高基數的請求 ID、使用者 ID 或任意標籤如果並不常查,就保留為儲存欄位,不預設建立聚合結構。traceId 雖然高基數,但有明確的精確查找價值,可以用專用精確欄位和有限保留期,而不是允許任意動態欄位複製它。
生命週期控制器按日誌類別把資料從 hot 移到 warm、cold 或 frozen/歸檔層。熱層為寫入和低延遲查詢配置 更多運算與副本;較舊資料降低成本並接受更慢存取。保留刪除要同時覆蓋索引、歸檔、快取、匯出和法律保留, 不能只刪一個搜尋索引就宣稱完成。
第五步:設計受控查詢路徑。
查詢 API 預設要求 tenantId 來自工作階段,並強制 startTime、endTime、服務或其他選擇性條件。 查詢協調器先看時間範圍和日誌類別,再把請求路由到熱索引、溫層或非同步歸檔掃描。常見介面可以是:
POST /logs/search
{ startTime, endTime, services, severities, traceId, query, cursor, limit }
-> { events[], nextCursor, partial, scannedBytes }
POST /logs/exports
{ startTime, endTime, filters }
-> { jobId }結果使用穩定排序鍵 (timestamp, eventId) 和游標分頁,避免深 offset。互動查詢設定掃描位元組、返回筆數、 執行時間和併發上限;到達上限時返回明確的 partial 標記,而不是悄悄漏資料。寬泛正規表示式、90 天全文 搜尋和大型匯出進入非同步佇列,使用者可以取消。查詢服務還要對熱點條件快取中繼資料,但不能跨租戶重用 未隔離結果。
「最近 15 分鐘過濾查詢 p95 小於 2 秒」只適用於選擇性過濾和健康叢集。沒有時間範圍的全庫全文檢索不能 沿用同一 SLO。面試中把不同查詢類別寫清楚,比承諾所有查詢都快更可信。
第六步:完成容量估算。
穩態原始吞吐為:
1,000,000 events/s × 600 bytes = 600 MB/s
600 MB/s × 86,400 s = 51.84 TB/day15 分鐘內流量從每秒 100 萬升到 300 萬。如果下游只能維持穩態處理能力,需要吸收的額外積壓是:
(3,000,000 - 1,000,000) × 600 bytes × 900 s = 1.08 TB這 15 分鐘總共進入 1.62 TB,但 0.54 TB 屬於同時被穩態能力處理的基線。實際緩衝還要考慮副本、批次 開銷、故障恢復和安全餘量,因此不能把 1.08 TB 直接當作磁碟採購數。
若只有 20% 日誌進入全文或結構化熱索引,並保留 7 天,索引前原始輸入仍約為:
51.84 TB/day × 20% × 7 = 72.576 TB真實索引容量取決於欄位、壓縮、分片和副本,需要壓測衡量。90 天原始歸檔約為 51.84 TB × 90 = 4.6656 PB,壓縮前約 4.67 PB。這個數量說明分層、保留類別、壓縮和減少無價值日誌 比單純擴搜尋叢集更重要。
分區數從每分區可持續位元組率、事件率和故障時重放速度反推。除了穩態吞吐,還要驗證失去一個可用區、 消費者追趕和 3 倍突發同時發生時,最舊訊息年齡仍能恢復。不能只用訊息筆數,因為例外堆疊會顯著改變 平均大小。
第七步:處理背壓、日誌風暴和下游故障。
每一層都有有限容量:SDK 記憶體佇列、代理磁碟、閘道連線、訊息流保留、處理器併發、索引寫入佇列和 查詢執行緒。系統透過 retry-after、批次縮小、消費者暫停和優先級佇列逐級傳遞壓力。代理磁碟接近上限 時,先按策略取樣除錯日誌,並發出包含丟棄類別與數量的本機計數;稽核事件進入獨立保留池或觸發明確 業務失敗策略。
OpenTelemetry Collector 的故障排查文件指出,目標不可用或 Collector 配置不足都可能造成丟棄;傳送 佇列和重試能覆蓋暫時故障,但過大的佇列也會造成記憶體壓力。這表示「開啟重試」並不等於可靠:必須同時 監控佇列使用量、最舊事件、拒絕、永久失敗和行程記憶體,並在容量耗盡前告警。
索引叢集不可用時,訊息流繼續接收,原始歸檔消費者與索引消費者獨立推進。恢復後索引消費者按租戶公平 追趕,並限制重放速度,避免把剛恢復的叢集再次壓垮。若索引落後,UI 顯示搜尋新鮮度和缺失時間窗;不能 讓使用者誤以為「無搜尋結果」等於「沒有日誌」。
第八步:關閉安全、隱私和刪除環。
最有效的敏感資訊控制是在產生端不記錄。SDK 提供結構化欄位白名單,代理遮蔽常見權杖、密碼、Cookie 和 個人資訊模式;閘道在寫入中央持久流前執行強制規則,後續處理器再按 schema 做語意遮蔽,並把失敗事件送 隔離佇列。原始正文也不能因為「只進歸檔」就繞過安全控制。
傳輸使用 mTLS 或短期工作負載身分,靜態資料加密,金鑰按環境或合規租戶隔離。RBAC 至少限制租戶、環境、 服務、欄位和時間範圍;查詢、匯出、保留變更和法律保留都寫不可變存取稽核。高敏欄位可以做欄位級加密或 完全刪除,不能依靠 UI 隱藏。
刪除流程用可追蹤工作流覆蓋熱索引、溫層、物件分區、快取和匯出副本。稽核保留與隱私刪除發生衝突時, 產品和法務必須定義優先級、例外和法律保留;系統設計只能執行明確政策,不能用「日誌不可變」迴避刪除責任。
第九步:用指標與故障注入證明系統。
核心指標包括產生到閘道、閘道到持久流、持久流到歸檔、持久流到可搜尋的 p50/p95/p99;每層佇列使用量、 最舊事件年齡、重試和丟棄;按租戶的位元組率、限流、欄位基數與成本;索引拒絕、mapping 增長、查詢 掃描量、逾時、取消和快取命中;遮蔽失敗、越權拒絕和歸檔恢復成功率。
端到端 canary 每分鐘從各區域寫入帶唯一 ID 的結構化日誌,並分別驗證持久接收、搜尋可見、物件歸檔和 到期刪除。計數對帳比較代理傳送、閘道接受、訊息流提交、歸檔寫入和索引成功數量,允許解釋過的重複, 不允許無法歸因的差值。
故障測試至少覆蓋:持續 15 分鐘的 3 倍日誌風暴、搜尋叢集不可用 30 分鐘、物件儲存限流、一個可用區 故障、熱點租戶、高基數欄位攻擊、錯誤 schema、機器時鐘偏差、敏感資料注入、代理磁碟耗盡、重複批次、 訊息重放和從歸檔重建熱索引。關鍵斷言是:稽核事件在策略要求內可恢復;普通日誌的任何丟棄都有租戶、 類別、時間與數量證據;搜尋新鮮度退化時使用者和告警系統都能知道。
高品質示範回答
「我先把可靠接收與搜尋可見分開。應用程式寫標準輸出或非同步 SDK,節點代理負責批次、遮蔽和有限磁碟 緩衝,因此日誌平台故障不會同步阻塞普通業務請求。區域閘道從工作負載身分確定租戶,執行配額和格式檢查, 再把批次寫入跨可用區複製的持久訊息流。這個提交點就是中央接收成功;搜尋索引和物件歸檔非同步消費。
事件採用穩定頂層欄位:事件時間、觀察時間、租戶、服務、環境、實例、等級、正文、traceId、spanId、 schema 版本和敏感等級。任意 attributes 預設只儲存,只有型別穩定、確實常查的白名單欄位才建立索引, 避免高基數欄位造成 mapping 和索引放大。系統是 at-least-once,代理用穩定 eventId 重試,索引寫入 冪等,但歸檔可能保留可識別的重複。
物件儲存保存可重放的長期原始記錄,熱搜尋層只保留近期和允許檢索的資料。查詢必須帶可信租戶和時間範圍, 按熱、溫、歸檔層路由,並限制掃描位元組、併發和返回量。寬泛歷史全文檢索與匯出進入非同步工作。小租戶 共享索引,大租戶可獨立隔離,但寫入、backlog、欄位和查詢配額始終按租戶執行。
容量上,穩態是 600 MB/s、51.84 TB/日。15 分鐘 3 倍突發在下游只處理穩態流量時會多積壓約 1.08 TB。 若只索引 20% 並保留 7 天,索引前仍有約 72.6 TB;90 天原始歸檔壓縮前約 4.67 PB,所以必須做選擇性 索引、分類保留和冷熱分層,不能把所有欄位永久放在熱搜尋叢集。
故障時,訊息流吸收短期積壓,索引消費者與歸檔消費者獨立恢復。每層容量都有上限:優先保留稽核和錯誤 日誌,先取樣除錯日誌,並把丟棄計數、最舊訊息年齡和搜尋新鮮度暴露給監控與 UI。最後我會用唯一 canary 貫穿生產、持久流、索引和歸檔,並注入 3 倍日誌風暴、下游停機、高基數欄位和磁碟耗盡,證明沒有靜默 遺失,也能從歸檔重建索引。」
常見錯誤
- 應用程式同步呼叫遠端日誌 API → 日誌故障拖垮業務請求 → 寫本機非同步代理,並使用有限緩衝與明確降級。
- 寫進搜尋叢集才確認成功 → 索引維護會反壓全部生產者 → 在複製的持久訊息流提交後確認。
- 所有 JSON 欄位自動建立索引 → 高基數欄位造成 mapping explosion 和成本失控 → 只索引穩定白名單欄位。
- 只按租戶 ID 建一個訊息分區 → 大租戶成為單分區熱點 → 使用租戶與來源的虛擬分區並提供獨立分區池。
- 隨機分區後宣稱全域順序 → 跨來源時鐘和並行處理無法保證 → 只維護來源內順序,並保存觀察時間。
- 用 exactly-once 掩蓋重複 → 回應遺失和重放仍會發生 → at-least-once、穩定
eventId和冪等寫入。 - 佇列無限大就不會遺失日誌 → 磁碟、記憶體和保留最終都會耗盡 → 容量上限、最舊年齡、優先級和丟棄證據。
- 日誌風暴時統一取樣 → 稽核證據也被丟棄 → 按日誌類別分別定義可靠性與降級策略。
- 所有查詢都承諾 2 秒 → 90 天全文掃描會破壞互動叢集 → 區分選擇性熱查詢和非同步歷史掃描。
- UI 沒結果就當作沒有日誌 → 索引延遲隱藏事故證據 → 展示搜尋水位、partial 標記和缺失時間窗。
- 只刪除熱索引 → 歸檔、快取和匯出仍保留敏感資訊 → 使用覆蓋所有副本的可稽核刪除工作流。
- 只監控 Collector 是否存活 → 行程健康也可能持續丟資料 → 對帳每階段數量並監控佇列、拒絕與永久失敗。
追問及應對
追問一:為什麼不讓代理直接寫搜尋叢集?
小規模內部工具可以這麼做,元件少、搜尋新鮮度也更直接。但在本題規模下,搜尋叢集的分片調整、mapping 問題和寫入拒絕會直接傳到 10 萬個來源。持久訊息流提供明確確認邊界、突發吸收、消費者獨立恢復和重放; 代價是額外維運、重複事件和數十秒非同步延遲。若實測規模很小,應選擇直接路徑,而不是為架構圖增加佇列。
追問二:如何處理一個欄位突然產生數百萬種值?
schema 註冊表記錄可索引欄位、型別、擁有團隊和基數預算。處理器用近似去重計數監控欄位基數;超過閾值 時停止為該欄位建立新索引結構,把值保留為未索引屬性,並通知租戶。已經發生 mapping explosion 時, 先阻止新欄位、修正上游格式,再把健康欄位重建到新索引;只拆分更多索引不會消除無界欄位增長。
追問三:訊息流和本機 spool 都滿了怎麼辦?
先承認系統無法用有限資源保證無限突發。代理按日誌類別保留獨立預算:稽核事件進入更強的獨立通道,錯誤 優先於資訊,除錯日誌先取樣或丟棄。每次丟棄寫本機計數並在恢復後上報。若業務規定某項稽核必須可靠, 相關操作可以明確失敗或進入本機交易 outbox;普通 debug 呼叫不能因此無限阻塞整個應用程式。
追問四:如何從物件儲存重建搜尋索引?
歸檔物件保存 schema 版本、時間分區、租戶、校驗和和物件清單。重建工作選擇租戶與時間窗,讀取清單並 校驗,按目前映射轉換,使用 eventId 冪等寫入新索引,再比較事件數、時間邊界和 canary。完成後透過 別名或路由原子切換。重建流量有獨立配額,不能搶占即時索引寫入。
追問五:如何避免一個租戶的查詢拖垮所有人?
查詢排程器按租戶維護併發、CPU 時間、掃描位元組和返回量令牌桶,並使用加權公平佇列。互動查詢優先於 匯出,昂貴查詢可取消並轉非同步。共享快取鍵包含租戶與權限摘要。大型或高合規租戶可以使用獨立索引池, 但隔離前先衡量瓶頸,避免為每個小租戶製造大量小分片。
追問六:稽核日誌為什麼需要單獨策略?
稽核日誌的價值是證明誰在何時做了什麼,因此取樣、可變內容和普通管理員刪除都會破壞用途。它需要更嚴格 schema、來源身分、完整性校驗、不可變歸檔、受限查詢和存取稽核。仍要遵守隱私刪除與法律保留政策,所以 「不可變」表示普通路徑不能修改,不表示永遠不執行受授權的合規工作流。
追問七:跨區域如何設計?
每個區域先把本機日誌寫入本機持久流和歸檔,避免業務請求跨洲等待。事件攜帶區域和全域租戶 ID,控制平面 下發 schema、配額與保留策略。全域查詢協調器按時間和區域扇出,並對部分區域不可用返回 partial。若監管 要求資料駐留,原始日誌不跨區,只同步允許的索引或聚合。區域災難後可從本機或合規複製的物件歸檔恢復。
追問八:如何證明系統沒有靜默丟日誌?
每個批次記錄來源事件數和校驗和,各階段輸出接受、重複、拒絕、永久失敗和寫入成功計數。唯一 canary 持續穿過代理、閘道、訊息流、索引和歸檔。對帳允許由 eventId 解釋的重複,以及由明確配額策略解釋的 丟棄;任何無法歸因的差值都觸發事故。再停止索引 30 分鐘並恢復,驗證訊息年齡回落、canary 補齊和 UI 新鮮度恢復,而不只檢查行程是否重新啟動。