Google 是如何開發 Web 框架的
Google 是如何開發 Web 框架的
眾所周知, Google 使用單一倉庫來共享所有代碼 ——20億行代碼,并且這個倉庫是采用基于 trunk 的開發方式。
(這毫無疑問是世界上最大的代碼倉庫。)
對于 Google 公司以外的開發者來說,這是令人驚訝的,也是反直覺的,但是它確實運作良好。(所給的鏈接 文章 中已經給了很好的例子,這里就不累述了)
Google 的代碼庫在 Google 在全世界幾十個辦事處,共 25000 多名軟件工程師之間共享。在一個普通的工作日里,就有 16000 多次代碼提交。
這篇文章主要講述構建一個開源 Web 框架( AngularDart )的開發過程。
(“Human users” 意味著谷歌的軟件工程師提交代碼)
只有一個版本
當你在一個單倉庫項目中使用基于 trunk 的開發模式時,你的所有東西都只有一個版本。例如, Google 不會發生一個叫做 FooBar 的 App 使用 AngularDart 2.2.1 版本,而另一個叫做 BarFoo 的 App 使用 2.3.0 版本。所有的 App 都用同一個版本——而且是最新版本。
這就是為什么 Google 的工程師說所有的 Google 軟件都活在“失血邊緣”。
如果現在你的心里在大喊“太危險了!”,那就對了。在你的生產環境代碼里面依賴另外一個庫的 trunk(相當于 git 里面的 master)分支上面的代碼聽起來確實很危險,但是你要知道,故事在這之前還有別的情節。
每次 commit 前的 74000 次測試
AngularDart 定義了 1601 個測試。但是當你要提交一個改動到 Google 倉庫中的 AngularDart 源代碼時,所有依賴于 AngularDart 的測試都會被跑一遍。這時,大概有 74000 個測試(這依賴于你的改動有多大——系統會知道你影響了哪些測試,并且會啟發式地跳過這些測試)。
測試越多越好。
例如,我僅僅是做了一個小改動,用來在變化檢測插入驗證算法中做一個模擬某種場景的競爭情況(我添加了 && random.nectDouble() > .05 到 這條 if 語句 中)。當我運行它的時候(運行一次),AngularDart 本身的1601個測試并不需要跑,但是這個改動卻命中了一堆的客戶端測試。
這里面體現出真正的價值是,這些測試都是 真實產品 的測試。這些測試不僅數量龐大,同時它們也反映了開發者是如何使用的這個框架(不僅僅是框架的作者使用)。這樣做的意義是:框架的作者并不總是能夠正確地預估別人是怎么使用他的框架。
對正在生產環境運行的 App 也是有好處的,畢竟上面每個月有數十億美元的流水。開發者不是在一個業余時間拿來玩玩的 Demo App 上使用框架,而是一個成千上萬人投入在上面的線上產品使用。如果 Web 是和未來息息相關的,那么作為 Web 框架開發者,我們更應該更好地支持后者的開發。
所以,如果一個框架導致依賴于它的一些 App 崩潰,會發生什么?
誰搞砸,誰修復
如果 AngularDart 的某位成員引入了一個引起崩潰的改動,那么 他必須要為他們的用戶修復它 。因為大家都使用單一的倉庫,因此 bug 很容易被發現,并且可以立馬修復它。
因為 bug fix 的時候可能帶來新的 bug,因此 bug 和 fix 有可能是同時入代碼庫。當然,在入庫之前它們都會被做代碼審查。
我們來給一個具體的例子。當 AngularDart 團隊里的某個成員修改了代碼影響了 AdWords 程序的運行,那么他們就要跑到 AdWords 去修復它們。在修復代碼過程中,他們會跑 AdWords 已有的測試,也可以新建測試。然后他們會把測試和 bug fix 都放入變更列表,并且申請代碼檢查。因為這個變更列表中包含了 AngularDart 代碼和 AdWords 的代碼,因此系統會自動地將代碼發送到兩個團隊進行審查,只有兩邊都通過了才能被提交入庫。
這是一個很好的防止閉門造車的方法。AngularDart 框架的開發者們可以訪問到數以百萬計行的代碼,這些代碼都依賴于該框架。他們不需要去假想其他人會怎么使用這個框架。(當然他們只能訪問到 Google 的代碼,而訪問不到世界上其他依賴于 AngularDart 的代碼。)
別人用你的框架,但是你要升級別人的代碼,這樣聽起來會使得開發變慢。但是也沒想象中那么慢(可以看一下 AngularDart 10月份的進度),但是也確實讓開發進度慢了一些。這是一把雙刃劍,怎么來看待這個問題取決于你想要從這個框架中獲得什么。等下我們再來討論這個問題。
所以,如果有個 Google 的家伙跟你說,他們 Alpha 版本的庫已經穩定了并且可以應用到生產環境了,你就知道是什么回事了。
大規模改動
那么,如果 AngularDart 要做一個大規模的變動(如版本從 2.x 升級到 3.0)并且會命中 74000 個測試該怎么辦呢?團隊要修復所有的問題嗎?難道他們要去更改上千個跟根本就不是他們寫的源文件嗎?
是的。
一個好的 類型安全系統 會使你的工作更有效率。例如,在一個類型安全的 Dart 中,類型安全可以保證所有變量都有一個確定的類型。那么你進行重構的時候很多東西都可以做到自動化,而不需要開發者手工確認。
當類 Foo 中的一個方法 bar() 變成了 baz() ,你可以創建一個工具,它可以遍歷整個 Google 倉庫,找到所有 Foo 類的實例以及它子類的實例,并且把所有的 bar() 改成 baz() 。有了類型安全,你可以確定這個改動不會使任何地方崩潰。如果沒有類型安全,甚至這么一個簡單的修改都可能變得非常麻煩。
(根據 Dart 代碼規范,一鍵格式化代碼)
另外一個對于大規模改動很有幫助的是 dart_style , Dart 的默認格式器。所有 Google 的 Dart 代碼都是通過這個工具進行格式化的。在你的代碼被 push 到代碼審查人員之前,就通過 dart_style 進行格式化了,所以不存在像要不要把新的一行放在這或者放在那這樣的問題。這也被應用到大規模的代碼重構。
性能指標
正如我上面所說的,AngularDart 得益于依賴于它的產品的測試。但僅僅只是測試還不夠,Google 對于 App 的性能要求的很嚴格,幾乎所有的產品都有基準測試套件。
所以,如果 AngularDart 引入了一個導致 AdWords 加載慢了 1% 的改動,上線這個改動之前他們都會知道。如果十月份 AngularDart 團隊宣布他們自從八月份以來 AngularDart App 比原來體積小了40%,速度快了10%的時候,他們并不是在討論什么 TodoMVC 這樣的小事。他們談論的是真實世界中里面上百萬級別用戶的產品和上兆字節的業務邏輯代碼。
順帶提一下:封閉式編譯工具
你可能會好奇,在 AngularDart 引入了一個 bug 后,在這么龐大的內部倉庫里面,應該去跑哪些測試呢?當然不會手動的在 74000 個測試去挑測試來跑,當然也不會把 Google 所有的測試都跑一邊。答案在于一個叫做 Bazel 的玩意兒。
在這種代碼規模下,你不可能寫 Shell 腳本來編譯所有文件,這樣做會很不靠譜而且會慢的令人發指。這時候你需要的是一個封閉式編譯工具。
“封閉式”(Hermetic)的意思有點類似于函數式編程里面純函數的”純“。也就是說,你的編譯過程不能有副作用(如臨時文件,改變環境變量 PATH 等),他們也必須是確定性的(相同的輸入必須得到同樣的輸出)。這樣的話不管你什么時候在你的機器上編譯和運行測試,你會得到一致的輸出結果。你不需要 make clean 。因此你可以發送你的編譯/測試到編譯服務器上并行編譯。
Google 花費了數年的時間開發這個編譯工具。近些年將其開源了,就是 Bazel 。
多虧了有這樣的基礎設施,內部測試工具才能夠自己來決定編譯和測試哪些受影響的部分,并且在恰當的時候運行他們。
以上種種意味著什么?
AngularDart 的目標非常明確,就是要在構建大型 Web 應用領域,做到一流的效率、性能和可靠性。這篇文章所講述的就是后一個部分——可靠性。同時也解釋了為什么 Google 的關鍵產品,如 AdWords 和 AdSense 都使用這個框架。不是 AngularDart 自己吹噓自己有多么厲害的用戶,就正如上面所說的——就是因為有這么多的內部用戶,AngularDart 幾乎不可能引入一些隨意的變更。這也使得這個框架更加的可靠。
如果你想要一個每幾個月就有一次大的改版或者重大功能升級的框架,很明顯 AngularDart 并不是你要找的。即使我們想這么干,你看完這篇文章就只知道,這么嚴格的開發流程是不可能開發出這樣的框架的。但我們真心相信,一個不是很流行但是卻很穩定的框架肯定會它有發展和生存的空間。
在我看來,如何判斷一個開源技術會不會得到長期的維護和支持的,那么你就看看這個技術是不是維護者的公司業務的重要組成部分。可以拿 Android,dagger,MySQL 或者 git 作為例子。這就是我為什么我非常高興地看到 Dart 有一個首選它的 Web 框架(AngularDart)、一個首選它的組件庫( AngularDart Components )、一個首選它的移動框架( Flutter )——所有的這些都被用 Google 的商業產品中。
來自:https://segmentfault.com/a/1190000008427217