題幹與適用場景
一張位於物件儲存上的 Apache Iceberg 事件表有 20 TiB 活躍資料。串流任務以一分鐘微批寫入,每天新增約 200 GiB,卻產生約 80,000 個 Parquet 資料檔案,檔案中位數只有 3 MiB。最近查詢 p95 從 20 秒升至 95 秒,其中規劃階段從 4 秒升至 32 秒。業務要求繼續近即時入湖、保留 7 天時間旅行,並且禁止直接刪除物件儲存中的表檔案。
請說明怎樣證明效能退化確實由小檔案主導,找出產生小檔案的寫入和分區原因,阻止問題繼續增長,再安全壓實存量檔案。還要給出併發控制、資源預算、回復與驗收方案。
以上容量、檔案數、延遲和吞吐均為面試題設,不是通用基準。題目適合資料工程、分析平台、湖倉基礎設施和資料平台 SRE 職位。核心能力是資料佈局與表維護,因此歸為 data,而非只考 Spark 參數記憶或物件儲存維運。
面試官考察點
第一,能否先建立證據鏈。檔案多不代表根因成立;要把活躍檔案數量、大小分佈、分區傾斜、manifest 讀取時間、任務啟動與檔案開啟開銷,和查詢規劃、掃描階段分別關聯。
第二,能否區分治標與治本。壓實只能處理存量;微批頻率、寫入任務數量、資料分佈和過細分區不改,檔案會很快重新碎片化。
第三,能否尊重表格式事務邊界。Iceberg 的壓實是一次資料檔案重寫並提交新快照。舊快照仍可能引用舊檔案,所以不能繞過目錄中繼資料直接刪除物件。
第四,能否做有邊界的取捨。bin-pack 主要調整檔案大小;sort 或 Z-order 還改變資料聚簇,可能提高過濾效率,但會增加 shuffle、排序和暫存空間成本。
第五,能否量化維運方案。強回答會估算每日重寫位元組、目標檔案數、作業窗口、併發衝突機率,並用資料正確性和效能指標共同決定是否擴大範圍。
回答前需要澄清的問題
- 「小」以什麼目標判斷? 先讀取表的
write.target-file-size-bytes,再結合查詢選擇性、分區日增量和引擎測試定門檻;不能把某個固定大小當成所有表的真理。 - 80,000 是目前快照的活躍檔案,還是歷史快照中的全部檔案? 診斷查詢路徑應先看
files;容量與保留成本還要看all_files和快照引用。 - 延遲花在規劃還是掃描? 若規劃佔比上升,要查 manifest 和檔案任務數量;若掃描吞吐下降,還要查資料傾斜、刪除檔案、壓縮、欄統計與下游資源。
- 分區規則和寫入分佈是什麼? 按高基數欄位或過細時間粒度分區,會讓單一分區永遠累積不到目標檔案;大量平行 writer 也會各自提交未填滿的檔案。
- 表採用 copy-on-write 還是 merge-on-read? 後者可能同時累積 position/equality delete 檔案,只合併資料檔案還不夠。
- 哪些分區仍接收遲到資料? 優先壓實已關閉的冷分區;熱分區需要更小的 file group、受控併發與衝突重試。
- 7 天保留指什麼? 要確認是快照可查詢時間、分支/標籤保留,還是物件儲存生命週期;三者不能用一次目錄刪除替代。
30 秒回答框架
「我先查 Iceberg 目前快照的檔案數、大小分位數和分區分佈,並拆開規劃與掃描耗時,確認小檔案是主因。題設每天 200 GiB 卻有 80,000 個檔案;若候選目標為 512 MiB,理想量級約 400 個,需優先檢查微批、writer 和分區粒度。
治理時先降低新碎片:擴大批次、按分區鍵分佈並控制 writer;再對一個冷分區做受限併發的 bin-pack,只有過濾收益明確才排序。壓實以新快照提交,舊檔案按 7 天保留過期,絕不直接刪除物件。最後用資料對帳、檔案分位數、規劃與查詢 p95、積壓和衝突率決定是否擴量。」
分步驟深入解答
第一步:用目前快照建立檔案畫像
先查 Iceberg 中繼資料,而非遞迴列舉物件儲存目錄。目錄可能包含歷史快照或孤兒物件,不能代表目前查詢實際會讀取的檔案。以下 SQL 是 Spark + Iceberg 目錄的示意,目錄名和分位數函式需按實際引擎調整:
SELECT
partition,
COUNT(*) AS active_files,
SUM(file_size_in_bytes) AS active_bytes,
percentile_approx(file_size_in_bytes, array(0.5, 0.9, 0.99)) AS size_percentiles
FROM lakehouse.analytics.events.files
GROUP BY partition
ORDER BY active_files DESC;同時記錄目前快照 ID、資料檔案和刪除檔案數量、manifest 數量、每個分區的檔案分位數,以及小於候選門檻的檔案比例。平均值會掩蓋長尾:一個分區可能有少量大檔案和數萬個 1 至 3 MiB 檔案,所以至少看 p50、p90、p99 與直方圖。
再把查詢 p95 拆解為 catalog/manifest 解析、檔案規劃、任務調度、首位元組和掃描時間。若檔案數與規劃時間同步增長,合併相同總位元組的測試分區後規劃顯著回落,證據才接近閉環。若規劃穩定而掃描變慢,應繼續查選擇性、欄統計、刪除檔案、傾斜和計算資源。
第二步:從寫入路徑找出再生原因
題設每天 1,440 個一分鐘批次。80,000 個檔案相當於平均每批約 56 個檔案;每個 writer 或分區組合資料不足時,設定 512 MiB 目標也不能把 3 MiB 輸出憑空放大。write.target-file-size-bytes 是目標,不是「每個檔案必定達到該值」的保證。
根因通常是組合問題:微批過密;上游平行度遠高於每批資料量;寫入前沒有按表分區鍵聚集;按小時、租戶或使用者等高基數組合過度分區;熱點鍵造成傾斜;重試產生額外提交;merge-on-read 更新又累積刪除檔案。
應把「檔案小」按分區解釋。某個每天只有 40 MiB 的合法低流量分區,無論怎樣等待都無法形成 512 MiB 檔案;這時應接受較小目標、合併分區粒度,或使用 bucket/隱藏分區,而非無限增加壓實頻率。
第三步:先讓寫入側不再製造同規模碎片
寫入側從最小改動開始小範圍驗證:在新鮮度 SLA 內將一分鐘提交合併為更大的觸發批次;讓資料按 Iceberg 分區鍵做 hash 或 range 分佈;按每批位元組而非叢集最大平行度決定 writer 數;避免把高基數欄位直接變成分區。
目標檔案大小要透過實際壓縮率、列寬、查詢選擇性和單一分區日增量測試。題設先用 512 MiB(536,870,912 位元組)作為候選,是因為 Iceberg 預設設定可提供這個起點,並不排除 128、256 MiB 或更大值更合適。若單一分區資料遠小於目標,優先演進分區規則;若資料足夠但 writer 各自只取得少量列,修正分佈和平行度。
先選擇一個流量相近的分區做 A/B:比較新舊寫入在相同資料量下的檔案數、大小分佈、提交延遲、串流處理積壓和失敗恢復。只有新檔案生成速率明顯下降,存量壓實才不會變成永久追趕。
第四步:按收益和衝突劃定壓實範圍
第一輪選擇遲到資料和業務修正窗口都已關閉的一個冷分區,至少避開目前正在寫入的小時。分區是否還會更新與快照保留 7 天是兩條獨立時間線。預設先用 bin-pack,因為目標是減少中繼資料與檔案開啟開銷;只有常用過濾欄的範圍重疊嚴重,且基準測試證明額外 shuffle 值得,才升級為 sort 或 Z-order。
CALL lakehouse.system.rewrite_data_files(
table => 'analytics.events',
strategy => 'binpack',
options => map(
'target-file-size-bytes', '536870912',
'min-input-files', '5',
'max-concurrent-file-group-rewrites', '3',
'partial-progress.enabled', 'true'
),
where => 'event_date = DATE ''2026-07-10'''
);where 選擇的是「可能包含匹配資料的檔案」,所以述詞應與分區邊界一致並在執行前查看候選位元組。file group 限制單次工作量;受控併發避免物件儲存、shuffle 與查詢叢集同時飽和。部分進度允許分組提交,衝突時減少整輪重做,但代價是產生多個快照,監控和回復必須按組處理。
熱分區無法完全避開併發寫入時,縮小時間範圍和 file group,保證冪等調度,區分資料檔案衝突與可重試的中繼資料提交衝突。不要同時執行兩個範圍重疊的壓實作業。
第五步:估算目標檔案數、I/O 和窗口
200 GiB 除以 512 MiB,理論上約為 400 個目標檔案;分區邊界、壓縮率和尾部餘量會讓實際數略高。這個估算用於發現數量級錯誤,不是承諾精確產出 400 個檔案。
一輪全量日分區壓實至少讀取約 200 GiB 並寫出約 200 GiB,資料 I/O 約 400 GiB,另有 shuffle、暫存空間、中繼資料與失敗重試。若基準測試得到按輸入位元組計的端到端持續吞吐 100 MiB/s,理想時間為:
200 GiB * 1024 MiB/GiB / 100 MiB/s = 2,048 s ≈ 34.1 min生產計畫應加入傾斜、併發查詢和重試餘量,例如給 60 至 90 分鐘窗口,並設定最大候選位元組、最大併發 file group、物件儲存請求率與暫存磁碟告警。若壓實每天只能處理 150 GiB,而新增 200 GiB,積壓必然擴大;要先提升可持續吞吐或進一步減少新檔案。
第六步:把快照保留與實體清理解耦
壓實成功後,新快照引用大檔案,正在讀取舊快照的查詢仍可完成,7 天時間旅行也仍需舊檔案。此時直接刪除原 Parquet 檔案會破壞快照和併發讀取。
先驗證新快照並觀察一個完整業務週期,再按策略執行 expiresnapshots,同時保留 7 天窗口、必要分支/標籤和最少快照數。該操作只刪除不再被任何保留快照需要的檔案。孤兒檔案是沒有被任何表中繼資料引用的另一類物件,應單獨執行 removeorphanfiles,先 dryrun,使用保守的 older_than,核對路徑 scheme/authority 和在途寫入最長持續時間後再刪除。
快照過期不是壓實本身,孤兒清理也不是回收舊快照引用檔案的替代品。三者的調度、權限和稽核記錄應獨立。
第七步:分階段驗收並定義停止條件
先鎖定壓實前快照與候選分區,記錄列數、去重鍵計數、關鍵金額/事件聚合、最小最大時間和 null 統計。壓實後對同一邏輯範圍重算;僅比較總列數會漏掉「少一列又多一列」的抵消,關鍵表可增加分桶 checksum 或按業務鍵抽樣對帳。
效能側比較活躍檔案數、大小 p50/p90/p99、manifest 數、規劃 p50/p95、端到端查詢 p95、掃描位元組、壓實重寫位元組與資源成本。維運側追蹤小檔案生成率、壓實積壓、失敗 file group、提交衝突、快照數量和可回收位元組。
若正確性不一致、規劃時間沒有改善、查詢成本轉移到寫入 SLA,或壓實吞吐長期低於新碎片生成速度,就停止擴大範圍並回到根因分析。表快照可用於將中繼資料指標回復到先前版本,但已執行的快照過期和實體刪除不能靠同一動作恢復。
高品質示範回答
「我不會先跑壓實,而會先證明瓶頸。物件儲存目錄混有歷史和孤兒檔案,我會從 Iceberg 目前快照的 files 中繼資料表按分區統計活躍檔案數、總位元組和大小 p50/p90/p99,再把查詢的 catalog、manifest 解析、任務規劃、檔案開啟和掃描階段拆開。題設每天 200 GiB 卻產生 80,000 個檔案,中位數 3 MiB;若候選目標是 512 MiB,理想數量級約 400 個檔案,所以寫入佈局值得優先調查,但仍需用相同資料量的合併分區實驗確認規劃時間是否回落。
然後我會修再生原因。一天有 1,440 個一分鐘微批,平均每批約 56 個檔案。檢查 writer 平行度、分區基數、寫前分佈、傾斜、重試和 merge-on-read 刪除檔案後,我會在新鮮度 SLA 內適度增大提交批次,讓資料按分區鍵 hash 或 range 分佈,並按每批位元組控制 writer。512 MiB 只是起點;低流量分區永遠累積不到時,要調整分區粒度或單獨降低目標。
存量先用一個冷分區做小範圍驗證,使用 rewritedatafiles 的 bin-pack、分區述詞、受限 file group 併發和最大候選位元組。只有常用過濾欄測試顯示明顯收益時才用 sort 或 Z-order,因為排序會增加 shuffle 和暫存空間。熱分區若必須處理,就縮小 file group、啟用部分進度並做衝突重試;不執行重疊壓實。
容量上,200 GiB 到 512 MiB 的理論目標約 400 個檔案。一輪約讀 200 GiB、寫 200 GiB;若按輸入位元組計的端到端實測吞吐是 100 MiB/s,理想約 34.1 分鐘,我會給 60 至 90 分鐘窗口並限制併發,確認日處理能力高於日增量。
壓實以新快照原子提交,舊查詢和 7 天時間旅行繼續引用舊檔案,所以絕不直接刪除物件。新快照完成列數、業務聚合、分桶 checksum 和查詢 p95 驗收後,按 7 天策略過期快照;孤兒檔案另做 dry run 和保守延遲清理。最終看正確性、小檔案生成率、檔案分位數、規劃 p95、壓實積壓、衝突率和每 GiB 收益,任一核心指標惡化就停止擴量。」
常見錯誤
- 看到檔案多就直接歸因 → 歷史檔案數不等於目前查詢檔案數 → 從目前快照建立分區檔案畫像並拆解規劃與掃描時間。
- 只跑一次壓實 → 微批、writer 和過細分區仍會持續生成小檔案 → 先降低新碎片生成率,再治理存量。
- 把目標大小當成強保證 → writer 只能寫出自己收到的資料 → 把目標與每批位元組、分佈、分區日增量一起調整。
- 對整張 20 TiB 表無謂重寫 → 成本與衝突範圍過大 → 按冷分區、候選大小和收益分批處理。
- 預設使用 sort 或 Z-order → 它們增加 shuffle 與暫存空間 → 先用 bin-pack,按查詢過濾收益決定是否聚簇。
- 直接刪除舊 Parquet 檔案 → 保留快照或併發查詢仍可能引用它們 → 透過快照過期回收,孤兒檔案單獨 dry run。
- 只驗證總列數 → 錯刪和重複可能互相抵消 → 增加業務聚合、鍵計數、分桶 checksum 與抽樣對帳。
- 只算計算時間,不算可持續吞吐 → 日處理量低於日新增會讓積壓擴大 → 同時預算讀取、寫出、shuffle、重試和壓實 lag。
- 用
coalesce(1)解決所有小檔案 → 單一 writer 會破壞平行度並製造超大瓶頸 → 按分區和目標位元組計算合理 writer 數。
追問及應對
追問一:為什麼把檔案目標設成 512 MiB,而不是 128 MiB?
512 MiB(536,870,912 位元組)是 Iceberg 預設目標,可作為題設中大表的實驗起點,不是普適最佳值。應比較 128、256、512 MiB 等候選在查詢選擇性、規劃開銷、任務平行度、壓縮率和單一分區日增量下的表現。選擇性很高的查詢可能偏好較小檔案,吞吐掃描和巨大分區可能偏好較大檔案。
追問二:為什麼修改目標大小後,檔案仍然只有幾 MiB?
目標控制 writer 希望接近的輸出上限,不能合併不同任務手中的資料。如果一分鐘微批被拆給數十個 writer,或一個 writer 同時寫許多低流量分區,每個檔案都可能提前隨任務提交關閉。要調整批次、寫入分佈、平行度和分區設計,而非繼續把目標值調大。
追問三:bin-pack、sort 和 Z-order 怎麼選?
只想減少檔案數和開啟開銷時先選 bin-pack。查詢常按一個或有層級關係的欄位過濾,並且欄統計能有效裁剪時,可測試 sort。過濾經常跨多個維度且組合變化較大時才評估 Z-order。後兩者必須把額外 shuffle、暫存空間、寫入放大和後續維護成本計入基準。
追問四:壓實與串流寫入衝突怎麼辦?
優先過濾掉仍活躍的分區。無法避開時,把工作拆成更小 file group,限制併發,啟用部分進度並對提交衝突做有界重試;調度器保證同一表範圍互斥。部分進度減少單組衝突導致整輪重做的代價,但會增加快照和部分成功狀態,必須記錄每組結果。
追問五:為什麼壓實後物件儲存容量沒有立刻下降?
壓實只提交引用新檔案的快照,舊檔案仍被歷史快照、分支或標籤引用,以支援併發讀取和時間旅行。滿足 7 天保留後執行快照過期,未被保留快照需要的檔案才可回收。完全不在表中繼資料中的孤兒檔案還需獨立清理。
追問六:如何證明收益來自小檔案減少,而不是快取或更多資源?
用相同引擎設定、關閉或預熱到一致快取狀態,對相同快照邏輯範圍執行可重複查詢;記錄壓實前後的規劃時間、任務數、檔案開啟數、掃描位元組和執行時間。保留未壓實的相似分區作對照,並多次執行取分位數。只有檔案佈局改變且規劃/開啟開銷隨之穩定下降,因果證據才更強。