Large-Scale Changes#

本章由 Hyrum Wright 撰寫,探討 Google 如何在超大型程式碼庫中進行大規模變更(Large-Scale Change, LSC)。

想像一下你自己的程式碼庫——你能在單次提交中可靠地更新多少個檔案?限制這個數字的因素有哪些?你曾嘗試過那麼大的提交嗎?能在緊急情況下合理的時間內完成嗎?你最大的提交規模與程式碼庫的實際規模相比如何?你會如何測試這樣的變更?需要多少人審查?如果提交出了問題,你能回滾嗎?

在 Google,他們很早就放棄了以大型原子變更(atomic change)橫掃程式碼庫的想法。他們觀察到,隨著程式碼庫和工程師人數增長,可能的最大原子變更規模反而會減少——執行所有受影響的 presubmit 檢查和測試變得困難,更不用說確保變更中的每個檔案在提交前都是最新的。

因此,Google 發展出一套結合社會規範與技術工具的 LSC 流程,使基礎設施團隊能持續改善底層系統,而不會被程式碼庫的規模所束縛。雖然你的程式碼庫可能不像 Google 的,但理解這些原則並在本地加以調適,將有助於你的開發組織在擴展的同時仍能對程式碼庫進行廣泛的變更。

什麼是大規模變更?#

大規模變更(LSC)是指一組邏輯上相關、但無法以單一原子單元提交的變更集合。無法原子提交的原因可能包括:

  • 變更觸及太多檔案,底層工具無法一次提交
  • 變更規模太大,始終會產生合併衝突(merge conflict)
  • 組織使用分散式或聯合式儲存庫(federated repositories),跨儲存庫的原子變更在技術上根本不可行

在很多情況下,LSC 的定義是由你的儲存庫拓撲(repository topology)所決定的:如果組織使用一組分散式或聯合式儲存庫,跨儲存庫進行原子變更在技術上可能根本不可行。

Google 的 LSC 幾乎全部透過自動化工具產生,常見類別包括:

  • 利用程式碼庫分析工具清理常見反模式(antipattern)
  • 替換已棄用的函式庫功能(deprecated library features)
  • 推動底層基礎設施升級,例如編譯器更新
  • 將使用者從舊系統遷移至新系統

觸發 LSC 的更廣泛原因也有很多:新的語言標準可能引入更高效的慣用法、內部函式庫介面可能改變、新的編譯器版本可能要求修正既有問題。

絕大多數 LSC 對功能的影響趨近於零——它們傾向於是廣泛的文字更新,目的在提升清晰度、最佳化或確保未來相容性。但 LSC 在理論上不限於這類行為保留/重構性質的變更。

在 Google 規模的程式碼庫中,基礎設施團隊可能需要變更數十萬甚至數百萬個引用。在最大的案例中,他們曾觸及數百萬個引用,而且預期這個流程會繼續良好地擴展。

Google 發現,及早且頻繁地投資 LSC 工具對所有進行基礎設施工作的團隊都是有利的。高效的工具不只幫助變更數千個檔案的工程師,同樣的工具在縮小規模到數十個檔案時也運作得很好。

誰負責 LSC?#

建立與管理底層系統的基礎設施團隊承擔了大部分 LSC 工作,不過工具和資源在全公司都可使用。有人可能會問:為什麼不直接引入新的類別或函式,然後要求所有人自行遷移到新版本?雖然這看似簡單,但在實踐中無法良好擴展,原因有三。

領域知識集中化#

基礎設施團隊對底層系統最為熟悉,擁有修正數十萬個引用所需的領域知識。消費端團隊不太可能有處理這些遷移的背景知識,期望他們各自重新學習基礎設施團隊已有的專業知識,在全局層面極度低效。

集中化也讓面對錯誤時的修復更快速——錯誤通常落入少數幾個類別,執行遷移的團隊可以準備對應的處置手冊(playbook),無論是正式的還是非正式的。

想想看:你花多少時間去做一系列你不理解的半機械式變更中的第一個?你可能要花時間閱讀變更的動機和性質,找到一個簡單的範例,嘗試遵循建議,然後試著將其應用到你自己的程式碼。對組織中的每個團隊重複這個過程,會大幅增加整體執行成本。讓少數集中化的團隊負責 LSC,Google 既內部化了這些成本,又透過提高效率來降低了它們。

避免無資金支持的強制命令#

所謂「無資金支持的強制命令」(unfunded mandate),指的是外部實體施加的額外要求,卻沒有相應的補償——有點像執行長宣布每週五是「正裝日」,卻不給你加薪來購買正裝。

即使新系統在各方面都更優秀,其好處往往分散在整個組織中,個別團隊不太可能有足夠動機主動遷移。如果新系統值得遷移,遷移成本終究會在組織中的某處被承擔。集中化遷移並統一核算成本,幾乎總是比依賴各團隊自行遷移更快速且便宜。

激勵機制對齊#

由擁有需要 LSC 的系統的團隊來負責遷移,能確保變更確實完成。在經驗中,自發性遷移(organic migration)不太可能完全成功,部分原因是工程師撰寫新程式碼時傾向參考現有程式碼作為範例。有一支對移除舊系統有切身利益的團隊負責遷移工作,有助於確保遷移真正被完成。

雖然資助和配備一支團隊來執行這類遷移看似額外成本,但實際上只是將無資金命令所產生的外部性內部化,並附帶規模經濟的額外好處。

案例:填補坑洞(Filling Potholes)

LSC 系統不只用於高優先級遷移。Google 發現,光是讓這些工具可用,就開啟了許多小型修復的機會。就像交通基礎設施的工作既包括修建新路也包括修補舊路一樣,Google 的基礎設施團隊花大量時間修復既有程式碼。

例如,Google Template Library 中有兩個標頭檔分別命名為 stl_util.hmap-util.h(注意不同的分隔符)。這不僅讓一致性至上的人抓狂,也降低了生產力——工程師必須記住哪個檔案用哪種分隔符,錯了才會在漫長的編譯週期後發現。

利用 LSC 工具,僅花費數週的背景作業時間便統一了命名,量化地減少了此類建置失敗次數。隨著在整個程式碼庫進行變更的能力提升,變更的多樣性也擴大了——有時候,花點力氣填補幾個坑洞是值得的。

原子變更的障礙#

在理想世界中,所有邏輯變更都能打包為單一原子提交,獨立地被測試、審查和提交。但隨著儲存庫與工程師人數增長,這個理想越來越不可行。在使用分散式或聯合式儲存庫的情況下,即使在小規模也可能完全不可行。

技術限制#

大多數版本控制系統(VCS)的操作成本與變更規模呈線性關係。系統可能處理數十個檔案的小型提交毫無問題,但缺乏足夠的記憶體或處理能力來一次性原子提交數千個檔案。在集中式 VCS 中,提交在處理過程中可能阻塞其他寫入者(在較舊的系統中甚至阻塞讀取者)。

簡言之,大型原子變更可能不只是「困難」或「不明智」——在特定基礎設施下可能根本不可能。將大型變更拆分為較小的獨立區塊(chunk)可以繞過這些限制,雖然會讓執行過程更為複雜。

合併衝突#

變更規模越大,合併衝突的可能性也越高。每個已知的版本控制系統都要求更新與合併——若中央儲存庫中存在檔案的新版本,可能需要手動解決衝突。當變更中的檔案數量增加,遇到衝突的機率相應增長,再加上同時在儲存庫中工作的工程師人數,衝突被進一步放大。

如果公司規模小,或許可以趁週末沒人開發時偷偷提交觸及所有檔案的變更,或者透過傳遞虛擬(甚至實體!)令牌來取得全域儲存庫鎖。但在 Google 這樣的大型全球公司,這些做法不可行:永遠有人在對儲存庫進行變更。

變更中的檔案越少,合併衝突的機率越低,也更有可能順利提交。這個性質同樣適用於後續討論的其他領域。

鬧鬼墓地(No Haunted Graveyards)#

Google SRE 有一條信條:「不要有鬧鬼墓地。」鬧鬼墓地指的是那些古老、晦澀或複雜到無人敢進入的系統。它們往往是業務關鍵系統,因為任何變更都可能造成不可理解的故障,付出真金白銀的代價,所以被凍結在時間中。它們構成真正的存亡風險,可能消耗不成比例的資源。

鬧鬼墓地不僅存在於生產系統,也存在於程式碼庫中。許多組織都有一些舊的、無人維護的軟體——由早已離開團隊的人撰寫,位於某個重要營收功能的關鍵路徑上。這些系統也被凍結在時間中,層層官僚機制被建立起來以防止可能造成不穩定的變更。沒有人想當那個翻錯位元的網路支援工程師!

從 LSC 的角度來看,這些部分阻礙了各種有意義的進展:大型遷移無法完成、它們所依賴的系統無法退役、它們使用的編譯器和函式庫無法升級。

Google 的應對之道是扎實的測試。當軟體被充分測試後,無論系統多古老或複雜,都能自信地對其進行任意變更並確認這些變更是否造成破壞。撰寫這些測試需要大量努力,但它讓程式碼庫能在長時間內持續演化,使「鬧鬼軟體墓地」的概念本身也被送進墓地。

異質性(Heterogeneity)#

LSC 只有在大部分工作能由電腦而非人類完成時才真正有效。人類善於處理模糊性,但電腦依賴一致的環境來將正確的程式碼轉換應用到正確的位置。如果組織內存在多種不同的 VCS、持續整合(CI)系統、專案專屬工具或格式化規範,就很難在整個程式碼庫進行全面變更。簡化環境以增加一致性,對需要在其中移動的人類和進行自動化轉換的機器人都有幫助。

例如,Google 的許多專案配置了 presubmit 測試,範圍從檢查新依賴是否在白名單中,到執行測試,到確保變更關聯了一個 bug。這些檢查中有很多與撰寫新功能的團隊相關,但對 LSC 來說只是增加了不相關的複雜性。

Google 選擇擁抱部分複雜性(例如標準化的 presubmit 測試),同時建議團隊在 LSC 觸及其專案時省略特殊檢查。大多數團隊樂於配合,因為這類變更對其專案有益。

第八章提到的一致性對人類的諸多好處,同樣適用於自動化工具。

測試#

每個變更都應被測試,但變更越大,適當測試的難度越高。Google 的 CI 系統不只執行直接受影響的測試,還會遞移地執行所有依賴變更檔案的測試。這意味著變更能獲得廣泛覆蓋,但 Google 也觀察到,在依賴圖中離受影響檔案越遠的測試,其失敗由該變更本身造成的可能性越低。

小型、獨立的變更更容易驗證——每個變更影響的測試集較小,測試失敗也更容易診斷和修復。在 25 個檔案的變更中找到測試失敗的根因相當直接;但在 10,000 個檔案的變更中找到一個,就像大海撈針。

這個決策的取捨在於,較小的變更會導致相同的測試被多次執行,特別是那些依賴程式碼庫大部分內容的測試。但由於工程師追蹤測試失敗所花費的時間遠比執行這些額外測試的運算時間昂貴,Google 有意識地做出了這個取捨。這個取捨不一定適用於所有組織,但值得檢視什麼是適合你的組織的平衡點。

案例:測試 LSC(Testing LSCs)

如今,一個專案中 10% 到 20% 的變更可能來自 LSC,意味著大量程式碼被與該專案全職工作無關的人所變更。沒有好的測試,這種工作不可能進行,Google 的程式碼庫會在自身的重量下迅速萎縮。LSC 使 Google 能系統性地將整個程式碼庫遷移至更新的 API、棄用舊的 API、更改語言版本,並移除流行但危險的實踐。

即使是簡單的單行簽名變更,在一千個不同地方、跨越數百個不同產品和服務時也會變得複雜。變更寫完後,需要協調數十個團隊的 code review。審查通過後,還需要執行盡可能多的測試以確保變更安全。

TAP Train 策略:核心洞見是 LSC 很少彼此互動,且大多數受影響的測試對大多數 LSC 都會通過。因此可以同時測試多個變更,減少總測試執行次數。列車每三小時啟動一次:

  1. 對列車上的每個變更,執行 1,000 個隨機選取的測試
  2. 收集所有通過 1,000 個測試的變更,合併為一個超級變更(即「列車」)
  3. 執行所有受群組變更直接影響的測試聯集。對於夠大或夠底層的 LSC,這可能意味著執行 Google 儲存庫中的每一個測試,可能需要超過六小時
  4. 對每個失敗的非不穩定測試,逐一對每個變更重新執行,以確定哪個變更造成了失敗
  5. 為列車上的每個變更產生報告,描述所有通過和失敗的目標,可作為 LSC 安全提交的證據

Code Review#

如同第九章所述,所有變更在提交前都需要審查,這項政策同樣適用於 LSC。審查大型提交既繁瑣、費力且容易出錯,尤其是手動產生的變更(一種應盡量避免的做法)。將 LSC 拆分為獨立的分片(shard)使審查變得更為可行。

案例:scoped_ptr 到 std::unique_ptr

Google 的 C++ 程式碼庫從早期就使用名為 scoped_ptr 的自毀智慧指標,來包裝堆分配的物件並確保它們在指標離開作用域時被銷毀。這個型別在程式碼庫中被廣泛使用以適當管理物件生命週期。雖然不完美,但在當時的 C++ 標準(C++98)限制下,它讓程式更安全。

C++11 引入了 std::unique_ptr,功能嚴格優於 scoped_ptr,還能防止語言現在可以偵測的其他類別的 bug。然而程式碼庫中有超過 50 萬個引用散布在數百萬個原始檔中。

在數個月內,多位工程師並行作業。利用 Google 的大規模遷移基礎設施,他們將 scoped_ptr 的引用轉換為 std::unique_ptr,同時逐步調整 scoped_ptr 的行為使其更接近 std::unique_ptr。高峰期每天穩定產生、測試和提交超過 700 個獨立變更,觸及超過 15,000 個檔案。如今吞吐量有時是當時的十倍。

如同幾乎所有 LSC,這次遷移有一條很長的尾巴——追蹤各種細微的行為依賴(Hyrum’s Law 的又一體現)、與其他工程師的競爭條件、以及自動化工具無法偵測的生成式程式碼中的用法。scoped_ptr 也被用作某些廣泛使用的 API 的參數型別,使小型獨立變更更加困難。

最終策略是先將 scoped_ptr 設為 std::unique_ptr 的型別別名(type alias),然後執行文字替換,最後移除舊的別名。如今 Google 的程式碼庫得以使用與 C++ 生態系統其餘部分相同的標準型別,這只有因為他們擁有 LSC 的技術和工具才得以實現。

LSC 基礎設施#

Google 投入了大量基礎設施來支援 LSC,涵蓋變更產生、變更管理、Code Review 和測試。然而,最重要的支撐或許是圍繞 LSC 演化出的文化規範與監督機制。雖然每個組織的技術和社會工具組合可能不同,但一般原則應是相同的。

政策與文化#

如第十六章所述,Google 將大部分原始碼儲存在單一的 monorepo 中,每位工程師對幾乎所有程式碼都有可見性。這種高度開放性意味著任何工程師都可以編輯任何檔案,並將編輯提交給有審批權限的人審查。然而,每次編輯都有產生成本和審查成本。

過去,這些成本大致對稱,限制了單一工程師或團隊能產生的變更範圍。隨著 LSC 工具的改進,產生大量變更變得非常廉價,但這也讓單一工程師能輕易對大量審查者造成負擔。Google 希望鼓勵對程式碼庫的廣泛改善,但也要確保背後有監督和深思熟慮,而非隨意的微調(例如,他們不希望工具被用來爭論註解中「gray」和「grey」的正確拼寫)。

最終結果是一套輕量級審批流程

  • 由一群熟悉各語言細微差異的資深工程師組成的委員會進行監督,並邀請相關領域專家
  • 目標不是禁止 LSC,而是幫助作者產出最佳品質的變更,充分利用 Google 的技術和人力資本
  • 偶爾委員會會建議某個清理不值得做——例如修正常見拼寫錯誤卻沒有防止再次發生的方法
  • 委員會也作為 LSC 相關疑慮或衝突的升級裁決機構——不同意變更的當地擁有者可以向此群組申訴,由他們仲裁任何衝突

與這些政策相關的是圍繞 LSC 的文化轉變。雖然程式碼擁有者對其軟體有責任感很重要,但他們也需要認識到 LSC 是 Google 擴展軟體工程實踐的重要部分。產品團隊最了解自己的軟體,但函式庫基礎設施團隊了解基礎設施的細微之處。讓產品團隊信任這種領域專業知識,是社會接受 LSC 的重要步驟。

當地擁有者偶爾會質疑作為更廣泛 LSC 一部分的特定提交的目的,變更作者會像回應其他審查評論一樣回應這些問題。社會層面上,程式碼擁有者理解其軟體的變更很重要,但他們也已經認識到自己不對更廣泛的 LSC 持有否決權。經過時間積累,良好的 FAQ 和穩固的改善歷史記錄已在 Google 內部產生了對 LSC 的廣泛認同。

程式碼庫洞察(Codebase Insight)#

進行 LSC 需要對程式碼庫進行大規模分析的能力,包括文字層面和語意層面:

  • 語意索引:Google 使用 Kythe 等語意索引工具,提供程式碼庫各部分之間的完整連結圖,能回答「這個函式的呼叫者在哪裡?」或「哪些類別繼承自這個類別?」等問題。Kythe 和類似工具也提供程式化的資料存取,使其能被整合到重構工具中(更多範例見第十七和二十章)
  • AST 分析與轉換:使用基於編譯器索引的工具,如 ClangMR、JavacFlume、Refaster 等,以高度平行化的方式執行抽象語法樹(AST)分析與轉換。這些工具依賴語意洞察作為其功能的一部分
  • 小型變更工具:對於較小的變更,作者可以使用專門的自訂工具、perl、sed、正規表達式匹配,甚至簡單的 shell 腳本

無論使用何種工具產生變更,人力投入應與程式碼庫規模呈次線性(sublinear)關係——無論儲存庫多大,產生所有必要變更所需的人力時間應大致相同。變更產生工具也應全面覆蓋程式碼庫,讓作者能確信其變更涵蓋了所有需要修正的案例。經驗法則:若變更需要超過 500 次編輯,學習並使用自動化工具通常比手動執行更有效率。對於經驗豐富的「程式碼清潔工」(code janitor),這個門檻通常更低。

如同本書其他領域,在工具上的早期投資通常能在短期到中期內獲得回報。

變更管理#

可以說 LSC 基礎設施中最重要的一環是將主要變更(master change)拆分為較小分片、並管理其測試、寄送、審查和提交流程的工具集。在 Google,這個工具叫做 Rosie。在很多方面,Rosie 不只是一個工具,而是在 Google 規模下進行 LSC 的完整平台。它提供了將由工具產生的大規模綜合變更拆分為更小分片的能力,每個分片可以獨立地被測試、審查和提交。

測試(基礎設施層面)#

如同第十一章所討論的,測試是我們驗證軟體行為符合預期的重要方式之一。這在應用非人類撰寫的變更時尤其重要。穩健的測試文化和基礎設施意味著其他工具能有信心地確認這些變更不會有意外效果。

Google 的 LSC 測試策略與一般變更略有不同,但仍使用相同的底層 CI 基礎設施。測試 LSC 不只是確保大型主要變更不會導致失敗,還要確保每個分片可以安全且獨立地被提交。因為每個分片可以包含任意檔案,所以不使用標準的專案級 presubmit 測試,而是對每個分片執行其可能影響的所有測試的遞移閉包(transitive closure)。

Cattle Versus Pets(牛群與寵物)

在分散式運算環境中,我們常用「牛群與寵物」的比喻來指個別機器,但同樣的原則也適用於程式碼庫中的變更。

在 Google(如同大多數組織),對程式碼庫的典型變更是由個別工程師針對特定功能或 bug 修復手工打造的。工程師可能花費數天甚至數週來打造、測試和審查單一變更,對其瞭若指掌,在它最終被提交到主要儲存庫時感到自豪。這種變更的創建就像擁有和養育一隻心愛的寵物。

相比之下,有效處理 LSC 需要高度自動化,並產生大量個別變更。在這種環境中,將特定變更視為牛群是有用的:無名無面的提交,隨時可能被回滾或拒絕,成本極低——除非整群牛都受影響。這通常是因為測試未捕捉到的意外問題,或甚至只是簡單的合併衝突。面對「寵物」提交被拒絕,很難不覺得是人身攻擊;但在處理大量 LSC 分片時,這只是工作的本質。擁有自動化意味著工具可以快速更新並以極低成本重新產生變更,偶爾失去幾頭牛並不是問題。

語言支援#

LSC 通常按語言進行,不同語言的支援難度差異很大:

  • 型別別名與轉發函式(type aliasing, forwarding function)對於非原子遷移至關重要,允許現有使用者在新系統引入和使用者遷移期間繼續運作。對於缺乏這些特性的語言,增量遷移往往特別困難
  • 靜態型別語言比動態型別語言更容易進行大規模自動化變更。基於編譯器的工具搭配強大的靜態分析能提供大量資訊,用於建構 LSC 工具並在測試階段之前就排除無效轉換。這意味著 Python、Ruby、JavaScript 等動態型別語言對維護者來說格外困難
  • 語言選擇與程式碼壽命的關聯:傾向於重視開發者生產力的語言,往往更難維護。雖然這不是內在的設計需求,但這是當前技術水準的現況
  • 自動格式化工具是 LSC 基礎設施的關鍵部分。因為 Google 以可讀性為程式碼最佳化目標,他們希望確保自動化工具產生的變更對即時審查者和未來的程式碼讀者都是清晰易懂的。所有 LSC 生成工具都會執行適用於被變更語言的自動格式化工具(如 google-java-formatclang-format)作為獨立步驟,使變更特定的工具無需關心格式化細節。沒有自動格式化,大規模自動化變更永遠不會成為 Google 被接受的常態

案例:Operation RoseHub

LSC 已成為 Google 內部文化的重要部分,但也開始對更廣泛的世界產生影響。2017 年初,Apache Commons 函式庫的漏洞允許任何在遞移 classpath 中包含受影響版本的 Java 應用程式受到遠端執行攻擊。這個 bug 被稱為 Mad Gadget。其中它被用來加密舊金山市交通局的系統並中斷其營運。因為漏洞的唯一要求是在 classpath 中某處有錯誤的函式庫,任何依賴 GitHub 上眾多開源專案之一的東西都是脆弱的。

一些有進取心的 Google 員工發起了自己版本的 LSC 流程。利用 BigQuery 等工具,志願者辨識受影響的專案並發送了超過 2,600 個修補程式來將其 Commons 函式庫版本升級到已修正 Mad Gadget 的版本。與自動化工具管理流程不同,超過 50 位人類讓這次 LSC 得以實現。

LSC 流程#

有了上述基礎設施,LSC 的實際執行大致分為四個階段(階段之間的邊界很模糊):

  1. 授權(Authorization)
  2. 變更產生(Change creation)
  3. 分片管理(Shard management)
  4. 清理(Cleanup)

通常這些步驟發生在新系統、類別或函式被撰寫之後,但在設計新系統時就應牢記這些步驟。在 Google,他們的目標是設計後繼系統時,從一開始就考慮從舊系統遷移的路徑,使系統維護者能自動地將其使用者遷移到新系統。

1. 授權(Authorization)#

潛在作者需填寫一份簡短文件,說明:

  • 提議變更的原因
  • 估計對程式碼庫的影響範圍(大型變更會產生多少較小的分片)
  • 針對潛在審查者可能提出問題的回答
  • FAQ 和提議的變更描述

作者也從被重構 API 之擁有者那裡取得「領域審查」(domain review)。

此提議會被轉交給約十二人的監督委員會進行討論。委員會最常見的調整之一是將 LSC 的所有 code review 導向單一的「全域審批者(global approver)」——許多首次進行 LSC 的作者傾向於認為當地專案擁有者應審查所有東西,但對於大多數機械式 LSC,讓單一專家理解變更的性質並建立自動化審查流程更為經濟。

變更被核准後,作者可以推進提交。委員會歷來對審批相當寬鬆,通常不僅批准特定變更,還會批准一系列相關變更。委員會徹底拒絕的變更種類非常有限——只有被認為危險的(如將所有 NULL 實例轉換為 nullptr)或極低價值的(如將英式英語拼寫改為美式英語,或反之)。隨著經驗增長和 LSC 成本下降,審批門檻也隨之降低。

委員會成員可以自行決定快速通過明顯的變更,無需完整討論。此流程的目的是提供監督和升級路徑,但不至於對 LSC 作者造成過重的負擔。

2. 變更產生(Change Creation)#

獲得核准後,作者開始產生實際的程式碼編輯。有時這些編輯可以被全面地產生為一個單一的大型全域變更,隨後被拆分為許多較小的獨立片段。通常,由於底層版本控制系統的技術限制,變更規模太大而無法放入單一全域變更中。

變更產生過程應盡可能自動化,以便在使用者回退到舊用法(原因可能是複製貼上既有範例、提交已開發一段時間的變更、或單純是舊習慣)或發生文字合併衝突時能更新父變更。在極少數技術工具無法產生全域變更的情況下,可以將變更產生工作分散給人類(如 Operation RoseHub 案例)。雖然比自動產生變更更為勞動密集,但這讓全域變更能在時間敏感的場景下更快速地完成。

無論使用何種工具,Google 針對人類可讀性最佳化程式碼,因此產生的變更應盡可能像人類撰寫的程式碼——這是需要風格指南和自動格式化工具的原因之一。

3. 分片與提交(Sharding and Submitting)#

全域變更產生後,作者開始執行 Rosie。Rosie 基於專案邊界和所有權規則將大型變更拆分為可原子提交的分片,然後讓每個分片獨立通過測試-寄送-提交管線。Rosie 可能是 Google 開發者基礎設施的重度使用者,因此它會限制任何 LSC 的未完成分片數量、以較低優先級執行,並與其餘基礎設施溝通在共享測試基礎設施上可接受的負載量。

每個分片通過以下步驟:

  1. 測試:透過 TAP(CI 框架)遞移地執行所有依賴該分片中檔案的測試。這聽起來可能在運算上很昂貴,但實際上絕大多數分片影響的測試少於一千個(在整個程式碼庫數百萬個測試中)。對於影響更多測試的分片,可以分組處理:先執行所有分片受影響測試的聯集,再對各個分片僅執行與首輪失敗測試的交集。大多數這些聯集幾乎會導致程式碼庫中的每個測試都被執行,因此向該批次加入額外分片幾乎是免費的

  2. 寄送審查者:在 Google 這樣有數千名工程師的公司中,審查者發現本身就是一個挑戰性的問題。儲存庫中的程式碼以 OWNERS 檔案組織,列出對特定子樹有審批權限的使用者。Rosie 使用一個理解這些 OWNERS 檔案的所有者偵測服務,依據預期審查能力加權選擇審查者。若特定擁有者無回應,Rosie 自動新增額外審查者以確保變更能及時被審查。寄送過程中也會執行各專案的 precommit 工具,並選擇性停用某些檢查(如非標準的變更描述格式檢查)——這些檢查對個別專案的個別變更有用,但作為程式碼庫的異質性來源,會對 LSC 流程增加顯著摩擦

  3. 審查:實務上,當地擁有者往往不會以與一般變更同等的嚴謹程度審查 LSC——他們過度信任 LSC 工程師。理想情況下這些變更應像其他變更一樣被審查,但實際上當地專案擁有者已經對基礎設施團隊產生了高度信任。Google 因此僅在需要上下文(而非僅需審批權限)時才將變更發送給當地擁有者;其他變更可以交給「全域審批者」。全域審批者通常擁有他們正在審查的語言和/或函式庫的特定知識,與 LSC 作者合作以了解預期的變更類型和可能的失敗模式。他們不逐一審查每個變更,而是使用獨立的模式比對工具,自動批准符合預期的變更,僅手動檢查因合併衝突或工具故障而異常的少數變更,使流程能非常好地擴展

  4. 提交:如同寄送步驟一樣,確保通過各專案的 precommit 檢查後正式提交到儲存庫

Rosie 積極忽略在變更之前就已存在的 presubmit 檢查失敗。在處理個別專案時,工程師很容易修正這些既有失敗並繼續原本的工作,但在跨 Google 程式碼庫進行 LSC 時,這種技巧無法擴展。當地程式碼擁有者有責任維護自己程式碼庫中不存在既有的失敗——這是他們與基礎設施團隊之間的社會契約。

透過 Rosie,Google 能夠每天有效地產生、測試、審查和提交數千個變更,讓團隊有能力有效地遷移其使用者。過去被視為終局的技術決策——如廣泛使用的符號名稱或熱門類別在程式碼庫中的位置——不再需要是終局。

4. 清理(Cleanup)#

不同 LSC 對「完成」的定義不同,從完全移除舊系統到僅遷移高價值引用、讓舊引用自然消失皆有。遺憾的是,我們最希望自然分解的系統往往是最頑固的——它們是程式碼生態系統中的「塑膠六環組」。

但在幾乎所有情況下,建立一套系統來防止重新引入已被 LSC 辛苦移除的符號或系統至關重要。Google 使用 Tricorder 框架(在第十九和二十章中提及)在 code review 時自動標記工程師引入新的已棄用物件使用,這已被證明是防止回退(backsliding)的有效方法。

完整的棄用流程在第十五章有更詳細的討論。

沒有防止回退的機制,LSC 的成果會隨著時間逐漸被侵蝕——工程師會無意間重新引入已被移除的模式,使得遷移工作功虧一簣。

結論#

LSC 是 Google 軟體工程生態系統的重要組成部分。它在多個層面上發揮作用:

  • 設計階段:開啟更多可能性,因為某些設計決策不再像過去那樣不可逆轉
  • 維護階段:讓核心基礎設施的維護者能將大量程式碼從舊系統、語言版本和函式庫慣用法遷移至新的,保持程式碼庫在空間和時間上的一致性
  • 規模化:所有這些都僅靠數十位工程師支援數萬名其他工程師來完成

無論組織規模如何,都值得思考如何在你的原始碼集合中進行這類全面變更。無論是出於選擇還是必要,擁有這種能力將在組織擴展的同時提供更大的靈活性,同時讓原始碼隨著時間保持可塑性。

TL;DRs#

  • LSC 流程使得重新審視某些技術決策的不可變性成為可能
  • 傳統的重構模型在大規模下會崩潰
  • 進行 LSC 意味著養成進行 LSC 的習慣——投資工具與流程,使其成為組織文化的一部分