眾所周知,Google 使用單一程式碼庫來共享所有 20 億行程式碼,並且使用主幹(trunk-based)開發模式。
對於公司之外的眾多開發者來說,儘管這點令人驚訝,並且感覺違反常理,但它實際上運作得很好。(上面的文章已經給出了很好的例子,這裡我就不重複了。)
Google 的程式碼庫為世界各地數十個辦事處,共超過 2.5 萬名 Google 軟體開發者提供共享服務。在一個普通的工作日內,他們能對程式碼庫進行 1.6 萬次修改。(具體內容見此)
本文是關於如何構建一個開源 Web 框架(AngularDart)的一些細節。
僅有單一版本
當你在單一程式碼庫中使用主幹開發模式時,你擁有的一切都是單一版本的。從字面上看,這很明顯,但仍需要特別指出,它的意思是,Google 的 FooBar 不會有 AngularDart 2.2.1 和 2.3.0 兩個版本,一定只會存在單一版本,而且是最新的版本。
這就是為什麼 Google 的員工有時會說,他們所有的軟體都處於行業前端,使用了最新的技術。
如果這時你突然想大叫“危險!”,這是可以理解的。誠然,僅僅依靠生產程式碼庫中的主幹(也就是 git 中的“master”),這聽起來確實很危險。但別急,前方還有一個劇情轉折點。
每個提交前的 7.4 萬次測試
AngularDart 定義了 1601 個測試(看這裡)。但是,當你在 Google 程式碼庫中修改了 AngularDart 程式碼時,它也會為那些使用此框架的 Google 員工進行測試。目前,一次提交大約會進行 7.4 萬次測試(取決於更改的程度,而系統會啟發性地跳過不受影響的測試)。
當然,測試越多越好。
例如,我做了一個小更動(在這條 if 語句中新增了 && random.nextDouble() > .05),讓它只顯示了 5% 的時間,以模擬更改檢測及驗證演算法中的競態條件。執行後,它並沒有開始 1601 個測試,卻開始了一堆客戶端測試。
它真正的價值在於,測試的是真實應用程式。它們不僅數量眾多,還反映了開發者如何使用框架(而不僅侷限於框架作者)。重要的一點是:框架作者並不是總能夠正確地估計框架的使用情況。
這也能幫助生產中的應用程式獲得每月數十億美元的流水。框架作者在業餘時間完成的演示應用程式,與數十或數百人投入數年產生的實際應用程式之間,仍然存在著很大差異。如果未來的網路是息息相關的,我們需要更好地支援後者的發展。
那麼,如果框架破壞了其中一些應用程式,會發生什麼事呢?
誰破壞,誰修復
如果 AngularDart 的框架作者引入了一種破壞性的更改,他們必須為使用者去修復它。由於 Google 使用了單一的程式碼庫,更容易發現破壞者,而且可以馬上對其修復。
對 AngularDart 的任何破壞性的更改,也包括對所有依賴於 Google 的應用程式的修復。因此,破壞和修復,在由相關方面審查程式碼後,會同時進入程式碼庫。
讓我們舉一個具體的例子:當 AngularDart 團隊的某個人做出一些更改,影響到 AdWords 應用程式的程式碼時,他們會審查該應用程式的原始碼並修復它。修復過程中,他們可以執行 AdWords 現有的測試,還可以新增新的測試。然後,他們會把所有這一切放到變更列表中,並要求進行程式碼審查。由於變更列表涉及到 AngularDart 和 AdWords 兩者的程式碼,系統會自動要求這兩個團隊進行程式碼審查。只有兩邊都通過,才能提交更改。
這很明顯能夠阻止框架開發過程中,毫無督促自由發揮這一情況的發生。AngularDart 框架的開發者們可以訪問自己平臺構建的數百萬行程式碼,而且由於經常接觸這些程式碼,他們不需要設想別人如何使用他們的框架。(值得一提的是,他們只能看到了 Google 內部使用此框架者的程式碼,而不是世界上所有使用此框架者的程式碼。)
升級使用者的程式碼也會降低開發速度,影響不會有你想的那麼大,(你可以看看 10 月以來 AngularDart 的進展),但總歸會讓開發進度降低一點。最終結果的好壞取決於你想從框架中得到什麼。等會兒我們再回到這個問題上來。
那麼,下一次,你就能理解,為什麼 Google 的某人會說,某個庫的 Alpha 版本是穩定的,並可以應用到生產環境了。
大規模改動
如果 AngularDart 需要做一個重大改動(比方說,版本從 2.x 升級到 3.0),會影響到 7.4 萬個測試嗎?團隊要修復所有的問題嗎?他們是否要對數千個原始檔進行修改,甚至其中的大部分都不是他們編寫的?
答案是 Yes。
類安全系統(sound type system)能夠讓一切變得更加高效、簡單。例如,在類安全系統 Dart 中,工具可以是某個變數的特定型別。對其中一個變數的修改,能夠自動化應用到所有相同型別,無需開發者手動更改。
當類 Foo 上的一個方法從 bar() 變更為 baz() 時,你可以建立一個工具,來遍歷整個單一 Google 程式碼庫,查詢 Foo 類及其子類的所有例項,並將所有的 bar() 變更為 baz()。藉助類安全系統 Dart,你可以確保不影響其他部分。沒有類安全系統,即便是一個簡單的改動也是麻煩多多。
dart_style(Dart 的預設格式化程式)是另一個有助於大規模改動的利器,Google 所有的 Dart 程式碼都是通過此工具進行格式化的。當你的程式碼推送到審查者面前時,它已經自動應用了 dart_style 進行格式化,所以不再有“新行是不是要放這裡”這樣的問題。同時,它也適用於大規模的程式碼重構。
效能指標
如上所述,AngularDart 能從子產品的測試中受益,而又不僅僅侷限於測試。Google 非常嚴格地衡量其應用程式的效能,因此大多數(也可能是所有)產品應用程式都有基準套件。
當 AngularDart 團隊引入了一個改動,使 AdWords 的載入速度降低了 1%,在改動上線前 Google 內部就會知道。團隊在今年 10 月表示,自 8 月份以來,AngularDart 應用程式的規模縮小了 40%,但速度卻提高了 10%。他們談論的不是像 TodoMVC 這樣的小型應用程式,而是真實世界中那些處理關鍵任務、有數百萬使用者基礎和上兆位元組的業務邏輯程式碼。
旁註:封閉的構建工具
讀者可能會好奇:你怎麼知道 AngularDart 引入了這個脆弱的 Bug 後,要在巨大的內部儲存庫中進行哪些測試哪?當然不能手動挑選 74 萬個測試,也肯定不會在 Google 內執行所有的測試。答案是一個叫做 Bazel 的東西。
在這種程式碼規模下,你不可能構寫一系列的 shell 指令碼來進行編譯。首先這樣很脆弱,其次它非常非常慢。你需要的是一個封閉的構建工具。
“封閉”這個詞與函式中的“pure”非常相似。你的構建步驟不能有副作用(例如臨時檔案、更改 PATH 路徑等等),而且它們必須是確定的(即相同的輸入一定得是相同的輸出)。在這種情況下,你可以在任何時間,在任何機器上執行構建和測試,以保證輸出的一致性。你不需要 make clean
。因此,你可以傳送編譯/測試來構建伺服器,並將它們並行化。
Google 花了數年時間開發這個構建工具,而且去年將它作為 Bazel 開源了。
有了這一基礎架構,內部測試工具可以自行決定構建/測試的影響範圍,並在適當的時候開始運作。
這一切意味著什麼哪?
AngularDart 的目標明確,就是在開發大型 Web 應用程式時,達到最佳的生產力、效能和可靠性。本文說的就是最後一部分——可靠性,以及為什麼 Google 的重量級應用程式,如 AdWords 和 AdSense,都在使用這個框架。不是團隊吹噓他們的使用者,如上所述,擁有龐大的內部使用者使得 AngularDart 不太可能引入一些表面上的變化,而這使得框架更加可靠。
如果你正在尋找一個隔幾月會有一次大改或者重大功能升級的框架,AngularDart 絕對不適合你。即使團隊希望以這樣的方式構建框架,本文也明確得告訴你,這一點做不到。但我們真誠地相信,一個不那麼符合潮流,但格外可靠的框架,還是有其生存空間的。
在我看來,一個開源技術棧是否會有長期維護和支援,最佳的預測方法是看它是否是主要維護人員公司業務的重要組成部分。以 Android、dagger、MySQL 或 git 為例。這就是為什麼我很高興 Dart 終於有了一個首選的 Web 框架(AngularDart)、一個首選的元件庫(AngularDart Components)和一個首選的移動框架(Flutter),所有這些都用來構建 Google 關鍵業務的應用程式。