大規模變更與所有權 (Large-Scale Changes & Ownership)

★★★★★Draft

FromSoftware Engineering at Google

📚 From the Books

大規模變更與所有權 (Large-Scale Changes & Ownership)Code Review

前面幾頁講的都是「小顆 CL、一位 Reviewer、一天內回饋」的常規審查——那套流程建立在一個前提上:變更小到一個人審得完。但當一筆變更要在整個 monorepo 裡同步觸及數十萬、甚至數百萬個引用時,這個前提就崩了。大規模變更(Large-Scale Change, LSC) 是一組邏輯相關、卻無法以單一原子單元提交的變更;要讓它們安全落地,靠的不是「更用力地審一顆巨大 CL」,而是把它拆片、自動化、並用 OWNERS 結構重組「誰該核准」

🧠 為什麼常規審查撐不起大規模變更

常規流程(見 為什麼需要 Code Review審查實務準則Critique 實戰流程)把「小顆變更 + 一位 Reviewer」當成地基。LSC 恰恰打破這個地基:一筆變更可能觸及數百萬個原始檔中的引用,沒有任何一位 Reviewer 有能力、也沒有任何一套 VCS 有記憶體,去一次吞下它。

Google 很早就放棄了「用大型原子變更橫掃程式碼庫」的想法。原因反直覺卻關鍵——隨著程式碼庫與工程師人數增長,可能的最大原子變更規模反而會縮小:要讓變更中的每個檔案在提交前都是最新的、要跑完所有受影響的 presubmit 與測試,都變得越來越難。於是問題不再是「怎麼把巨大變更審好」,而是「怎麼讓巨大變更根本不必以巨大的形式存在」。

NOTE

LSC 的定義往往由儲存庫拓撲決定。若組織用的是分散式或聯合式儲存庫(federated repositories),跨儲存庫的原子變更在技術上可能根本不可行——這時即使變更規模不大,也天生是一筆 LSC。

⚖️ 巨大變更為何無法走常規審查——四道障礙

一筆理想的邏輯變更本該打包成單一原子提交,獨立地被測試、審查、提交。以下四道障礙解釋了為什麼在規模化下這條路走不通,也解釋了為什麼答案是「拆片」。

① 技術限制 + 合併衝突:原子提交可能根本不可能
  • 大多數 VCS 的操作成本與變更規模呈線性:處理數十個檔案的提交毫無問題,但缺乏記憶體或算力去一次原子提交數千個檔案;集中式 VCS 在處理期間甚至可能阻塞其他寫入者。
  • 變更越大,撞上合併衝突的機率越高。在 Google 這種全球公司,永遠有人正在改動 repo——沒辦法「趁週末沒人開發時偷偷提交觸及所有檔案的變更」。
  • 把大型變更拆成較小的獨立區塊(chunk),就能繞過這兩道限制;代價是執行過程更複雜。
// 想像一個要在 50 萬處把 scoped_ptr 換成 std::unique_ptr 的變更。
// ❌ 一顆巨大原子 CL:VCS 吞不下、幾乎必然合併衝突、沒人審得完。
// ✅ 拆成上千個各自獨立、可原子提交的分片(shard),逐片跑管線。
② 測試與審查:巨大 diff 讓「找根因」和「審得完」都失效
  • Google 的 CI 不只跑直接受影響的測試,還會遞移地跑所有依賴被改檔案的測試。在 25 個檔案的變更裡找到失敗根因很直接;在 10,000 個檔案的變更裡找一個,像大海撈針。
  • 小型獨立的變更,影響的測試集較小、失敗更好診斷——這正是拆片的核心動機之一。取捨是相同測試會被多次重跑,但工程師追蹤失敗的時間遠比多跑測試的算力昂貴,Google 有意識地選了後者。
  • 所有變更提交前都需審查,這條政策同樣適用於 LSC。但手動審一顆大型提交既繁瑣又易錯。把 LSC 拆分為獨立分片(shard),才讓審查重新變得可行。
③ 由誰負責:基礎設施團隊,而非「叫大家自己遷移」

為什麼不直接發布新 API,然後要求所有人自行遷移?因為那無法擴展,理由有三:

  • 領域知識集中化:基礎設施團隊最懂底層系統,也最有能力備好處置手冊(playbook)快速修錯;讓每個消費端團隊各自重學一遍,全局極度低效。
  • 避免無資金支持的強制命令(unfunded mandate):好處分散在整個組織、個別團隊缺乏動機主動遷移。集中遷移並統一核算成本,幾乎總是更快更便宜。
  • 激勵機制對齊:由「有切身利益移除舊系統」的團隊來負責,才能確保遷移真的被做完——自發性遷移(organic migration)很少能完全成功。
④ LSC 的四階段流程

有了基礎設施後,LSC 大致分四個階段(邊界模糊):

  1. 授權(Authorization):作者填一份簡短文件,說明變更原因、預估影響範圍(會拆出多少分片)、FAQ 與變更描述,並向被重構 API 的擁有者取得「領域審查」。提案交給約十二人的監督委員會——一群熟悉各語言細節的資深工程師。委員會的目標不是禁止 LSC,而是幫作者產出最高品質的變更,並作為衝突的升級裁決機構。真正被徹底拒絕的變更很少,只有危險的(如把所有 NULL 轉成 nullptr)或極低價值的(如英式↔美式拼寫互換)。
  2. 變更產生(Change Creation):盡可能自動化地產生實際編輯,好在使用者回退到舊用法、或發生文字合併衝突時能重新產生。產出的變更應盡量像人寫的——這是需要風格指南與自動格式化工具的原因。
  3. 分片與提交(Sharding & Submitting):見下一折頁。
  4. 清理(Cleanup):建立機制防止已被移除的符號被重新引入。Google 用 Tricorder 在 code review 時自動標記工程師引入新的已棄用物件。
⑤ 分片審查:Rosie、TAP、與「全域審批者」

分片與提交是 LSC 基礎設施最核心的一環。Google 的工具叫 Rosie:它把一筆巨大的綜合變更,依專案邊界與所有權規則拆成可原子提交的分片,讓每片獨立走「測試—寄送—審查—提交」管線。Rosie 會限制未完成分片數量、以較低優先級執行,避免壓垮共享的測試基礎設施。

每個分片經過:

  • 測試:透過 TAP 遞移地跑所有依賴該片檔案的測試;絕大多數分片影響的測試少於一千個。
  • 寄送審查者:Rosie 用一個理解 OWNERS 檔案的所有者偵測服務,依審查能力加權挑選 Reviewer;若某擁有者無回應,自動補上額外審查者。
  • 審查:實務上,當地擁有者常過度信任 LSC 工程師、不會用審一般變更的嚴謹度來審。Google 因此只在需要上下文(而非僅需審批權限)時才發給當地擁有者,其餘交給全域審批者(global approver)——他們通常精通該語言/函式庫,不逐一審每筆變更,而是用獨立的模式比對工具自動批准符合預期的變更,只手動檢查因合併衝突或工具故障而異常的少數,讓流程能極好地擴展。
  • 提交:通過各專案 precommit 檢查後正式提交。
// 全域審批者的心智模型(示意):
// 不是逐一讀每筆 diff,而是驗證「這筆分片是否符合預期模式」。
function autoApprove(shard, expectedPattern) {
  if (matches(shard.diff, expectedPattern)) return "LGTM"; // 自動批准
  return "manual-review"; // 只有異常分片才進人工
}

透過 Rosie,Google 能每天有效地產生、測試、審查、提交數千個變更。

⑥ OWNERS 檔案:規模化下「誰能核准」的結構

常規審查裡,一筆變更要提交需要三種面向的核准:同儕 LGTM(正確性與可理解性)、Code Owner 核准(適不適合進這塊 codebase)、以及 Readability 核准(符不符合語言風格)。分片審查改寫的,主要是中間那一項的「找誰」機制。

Google 的 codebase 是階層式目錄結構,每個目錄可放一個 OWNERS 檔案,列出對該目錄及其子目錄有 stewardship 的人。運作方式:

  • 關係是階層累加的:一個檔案的 Owner,通常是其所有上層目錄 OWNERS 成員的聯集
  • OWNERS 檔案可引用其他 OWNERS 或外部存取控制清單,但最終都解析為個別人員名單。
  • 根目錄的 OWNERS 可作為大規模變更的「全域核准者」——這正是 LSC 得以繞過各地方團隊、把審查導向單一專家的制度基礎。
  • 它同時充當一種文件:人和工具都能藉由往上走訪目錄樹來找到負責人(Rosie 的所有者偵測服務就靠它)。
  • 新專案不需向中央註冊,一個新的 OWNERS 檔案就夠了

IMPORTANT

許多首次做 LSC 的作者,直覺認為「當地專案擁有者應該審每一樣東西」。但對大多數機械式 LSC,讓單一全域審批者理解變更性質、建立自動化審查,遠比騷擾數十個團隊更經濟。委員會最常見的調整之一,就是把 LSC 的所有 code review 導向這位全域審批者。

WARNING

沒有防止**回退(backsliding)**的機制,LSC 的成果會隨時間被侵蝕——工程師會無意間重新引入已被移除的模式,讓辛苦的遷移功虧一簣。清理階段的 Tricorder 標記正是為此而存在。

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading