【重磅】一文看懂uber如何在高速擴張中重寫應用程式的噩夢般經歷

作者 | McLaren Stanley

譯者 | 王者

策劃 | 萬佳

本文講述了數年前,在高速擴張的背景下,Uber 工程團隊為解決技術問題而重寫應用程式的“噩夢般”經歷。

1

高速擴張的隱憂

2016 年,特朗普還沒當上美國總統,“刪除 Uber”運動還未爆發,Travis Kalanick 還是 Uber CEO。那時,Uber 還處在國際化擴充套件的高速增長期,業務蒸蒸日上,公眾情緒非常積極。

但是,高速擴張不可能一直風平浪靜,Uber App 開始出現一些問題。那個時候,Uber 工程團隊的規模幾乎每年都在翻倍增長。當一家公司以如此快的速度增長,最終要面臨的是令人難以置信的技術性爆炸問題。

再加上團隊提倡的“讓開發者放手去幹”的理念,我們的應用架構變得既複雜又脆弱。Uber 當時非常注重客戶端邏輯,所以應用程式會出現很多問題。我們一直在做熱修復,不斷髮布版本,設計的擴充套件性也變得很差。

2

噩夢開始:重寫應用程式

因為這些問題的出現,公司各個層面開始出現一種運動,主要的想法是“從頭開始重寫應用程式”。人們普遍認為,我們的架構正在拖累我們,只有重新開始才會讓我們走得更快。因此,Uber 成立了一個團隊,為新 App 構建全新的移動架構。這個團隊的目標是構建一個能夠“在未來 5 年內支撐 Uber 移動開發”的架構。

我們要同時支援兩個平臺,產品和設計也重新來過。在 iOS 平臺方面,這次重寫為採用 Swift(當時 Swift 的版本是 2。x)帶來了機會。Uber 之前也嘗試過 Swift,但早期使用過它的人都知道,它存在的問題比較多,所以在重寫之前就被禁止了。

不過,架構團隊的總體感覺是,當時 Swift 的大多數問題都集中在與 Objective-C 的互操作性上,所以如果我們開發的是一個純 Swift 應用,就能規避這些問題。

架構團隊希望在 Android 和 iOS 這兩個平臺上使用相同的架構模式。Android 團隊都是 RxJava 的忠實粉絲,而 Swift 也有一個支援函數語言程式設計的 RxSwift 庫。於是,這個由設計、產品和架構組成的核心團隊在一個房間裡工作了幾個月,使用新的函式式和反應式模式、新的程式語言開發新的應用程式,一切都進行得很順利。

這個架構高度依賴了 Swift 的高階語言特性。新的 UI 設計為 Uber 不斷增長的產品提供支援,函數語言程式設計非常強大(雖然學習曲線有一定的坡度),新的架構以我們的新實時流網路協議為基礎。

幾個月後,透過一系列演示,這種勢頭逐漸形成。這個專案看起來很成功。他們在很短的時間內與少數工程師一起創造了令人驚歎的體驗,核心產品的大部分功能都已經完成。

於是,在全公司範圍內的推廣開始了。各個團隊開始將更多的功能引入到新 App 中。最初,新應用帶來的興奮感激發了他們的積極性和生產力。新架構的主要特點是功能隔離,可以讓團隊快速開發新功能。

3

問題不斷:開發速度變慢、App 啟動時間變長……

但是,使用 Swift 的工程師數量一旦超過 10 個,開發速度就會慢下來。當時,Swift 編譯器仍然比 Objective-C 慢得多,因此構建時間大大增加,甚至幾乎無法進行除錯。

有一個 Uber 工程師在 Xcode 中輸入了一行程式碼,等了 45 秒之後,字母才慢慢地、一個接一個地出現在編輯器中。

隨後,我們又遇到動態連結器問題。那個時候,我們只能動態地連結 Swift 庫,而連結器的執行時間是多項式時間,蘋果建議單個二進位制檔案的最大連結庫數量是 6,而我們有 92 個,而且還在不斷增加。因此,在點選應用圖示後,需要 8 秒到 12 秒才開始呼叫主函式。新 App 的啟動速度比老款還要慢。

緊接著的是 App 的檔案大小問題。

當這些問題開始出現時,我們已經走過了可以回頭的臨界點。

此時,整個公司都將精力傾注在新 App 上。數千人參與其中,花費數百萬美元(我不能告訴你確切的數字,但肯定比你想象的多),管理層已經完全相信一切儘早掌握之中。

4

搞定各種難題

我私下裡和主管提過“我們必須停下來”的話題。但他告訴我說,如果這個計劃失敗,他就要捲鋪蓋走人。他的老闆,老闆的老闆,一直到副總裁,都要走人。沒有回頭路了!

所以我們擼起袖子,讓最優秀的人負責處理每一個棘手的問題(動態連結、二進位制檔案大小)。

我們很快發現,將所有程式碼放到主檔案中就可以解決 App 啟動時的連結問題。但我們都知道,Swift 的名稱空間與框架是混合在一起的,如果要這麼做,就需要修改大量的程式碼,包括檢查名稱空間。

這時,聰明的 Richard Howell 發現,在讀取 Xcode 的構建輸出時,可以在構建完成後用自定義指令碼將所有中間目標檔案重新連結到主檔案。由於 Swift 在編譯時將物件名稱空間轉換為符號名稱,這意味著他可以安全地保留名稱空間。於是我們可以靜態連結庫,並將之前的時間從 10 秒減少到 0。

下一個是 App 大小問題。當時,我們計劃將新 App 包含在舊 App 包中,並一步一步“安全”地釋出出去。為節省空間,我們做的第一件事就是移除舊 App。我們將這種策略稱為“Yolo”,由當時的 CEO 做的決定。

我們還用類替換了 Swift 的結構體。由於物件扁平化以及複製和自動初始化需要額外的機器程式碼,值型別通常需要大量的開銷,所以替換掉結構體為我們節省了一些空間。

但隨著 App 的不斷髮展,很快就達到了二進位制檔案(iOS 8 和更早的版本)的下載限制 (100MB),這意味著有大量使用者無法註冊。

此時距離公開發布日期只有幾周時間。我們得到一家公司的幫助,但他們不能解決我們的問題。我們唯一能做的就是為 Objective-C 重新生成所有的模型程式碼(佔總程式碼總量的 25%)或放棄支援 iOS 8。iOS 9 引入了新架構,可以把大小降到原來的一半。因為留給我們的時間只有一週了,所以我們決定放棄支援 iOS 8。

我們的普遍想法是,iOS 9 版本的二進位制檔案大小減小了一半,所以我們仍然擁有足夠的空間,可以在重寫完成後,在未來的某個時間解決問題。不幸的是,我們完全想錯了。

在 App 釋出後,我們舉辦了一個盛大的派對。新 App 受到了媒體的好評,它快速、時髦、設計新穎。

一群人得到了升職。我們都鬆了一口氣。在連續奮戰了 90 個禮拜之後,我們消停了幾個星期。

5

更糟糕的事情發生了

隨後,公眾的情緒開始發生轉變。新 App 的設計核心是讓使用者先進入到目的地,這樣他們就可以預先知道打車價格。如果不手動選擇位置,就會以最後接收到的 GPS 位置為準。但這個非常不準確(尤其是在高樓林立的城市),司機可能會走錯街區。這是一種很糟糕的使用者體驗。

為了改進位置獲取功能,我們修改了位置許可權,在後臺收集位置資訊,這樣就可以把司機派到使用者當前的位置。但人們被這個做法驚到了。我的一些 Twitter 舊同事建議我離開這家會追蹤使用者位置的“壞”公司。受到“驚嚇”的人們關閉了手機的位置許可權,但新 App 並沒有相應的解決辦法。

我們趕緊想辦法討論對策。我們想過關閉後臺位置收集,但這樣會破壞使用者體驗。

在特朗普入主白宮後(這是在新 App 釋出三個月後),這個問題引發了連鎖反應,導致“刪除 Uber”運動的爆發。

在這段時間裡,Swift 程式碼量一直在快速增長。問題的持續存在和緩慢的開發環境在 Uber 的 iOS 工程師中形成了兩個敵對派別,我稱它們為“Swift 狂熱派”和“Objective-C 頑固派”。外部的壓力和內部的派系鬥爭讓氣氛變得高度緊張。Swift 狂熱派否認 Swift 所造成的問題。這些壞脾氣的人抱怨著一切,卻不怎麼提供解決方案。

正是在這個時候,我們遇到了 App 大小的問題。我做好隨時待命的準備,而釋出團隊在提交 App 時遇到了麻煩。事實證明,我們針對動態連結問題提出的解決方案建立的主檔案對於某些平臺來說太大了。在臨時解決了這個問題後,我們做了一些調查,發現編譯的程式碼大小以每週 1。3 MB 的速度增長。如果我們不採取行動,在 3 周內就會達到手機下載的上限。

但因為內部鬥爭太過激烈,我們被“無視”了。一位技術負責人寫了兩頁的材料,試圖證明手機下載限制並不是個問題。

我們的一名資料科學家設計了一個測試,人為地將架構的一部分推到限制閾值,並觀察對業務指標的影響。在接下來的一個星期,我們把之前的部分下架,再把另一個部分推到限制閾值。

結果是災難性的,這種做法對業務的負面影響比 Swift 重寫的成本要大幾個數量級。事實證明,很多人在第一次下載 Uber App 時就使用了手機網路。

我們組建了另一支突擊團隊。我們開始反編譯目標檔案,並逐行檢查,看看為什麼 Swift 程式碼生成的檔案體積會這麼大。我們刪除了一些沒有被使用的特性,並把 watchOS 應用重新改回了 Objective-C。

我們幾乎達到了極限,精疲力盡,但每個人都努力打起精神。這是真正優秀的工程師開始散發光芒的時刻。阿姆斯特丹的一名開發人員想到了重新最佳化編譯器 pass。關於編譯器的 pass,我需要解釋一下。

現代編譯器會對程式碼進行大量的 pass,例如 pass 行內函數,或者用值來替換常量表達式。根據執行順序的不同,可能會得到更小體積的機器碼。

如果行內函數碰到一個常量,編譯器就會知道,並進行替換。於是,如果先進行內聯,

就會變成常量 7,這樣生成的機器碼就更少。

如果內聯是後進行的,就無法推斷函式體,會生成更多的機器碼。當然,這完全取決於你所寫的程式碼是什麼樣的,因此很難對 pass 的順序進行通用的最佳化。

阿姆斯特丹的這位工程師在構建過程中使用退火演算法來重新排序編譯器最佳化,最小化生成的機器碼。這減少了 11MB 的機器碼,為我們提供了足夠的空間繼續開發功能。

但這卻嚇壞了 Swift 編譯器工程師,他們擔心未經測試的編譯器最佳化命令會導致未經測試的 bug(即使每個 pass 都被認為是安全的,但很難推斷出可能出現的組合)。不過,我們並沒有遇到什麼大問題。

我們也嘗試了一些其他的解決方案,並按照開發週數來測算它們給我們帶來的好處。但我們發現,真正的問題是增長曲線,它總是讓我們的努力“功虧一簣”。

最終,我們讓蘋果將手機下載限制提高到 150MB,他們還添加了一些編譯器選項 (-Osize),幫我們進行檔案大小最佳化。Swift 團隊也承認,Swift 編譯器不可能像 Objective-C 編譯器那樣將檔案編譯到很小。

但到了 2020 年,他們將 Swift 編譯生成的機器碼大小降至 Objective-C 的 1。5 倍,並將下載限制提升至 200MB 的可選上限。這足夠讓我們再撐好幾年了。

如果不是因為蘋果提高了上限,我們將被迫重新回到 Objective-C。最終,我們也解決了其他問題。聰明的 Alan Zeino 和他的團隊讓 Uber 的 BUCK 構建系統支援 Swift,極大地加快了構建速度。

一路下來,我們的很多同事都感到精疲力竭。Uber 花了一大筆錢,也吸取了慘痛的教訓,但直到今天,大多數人仍然堅持認為

重寫

是值得的。新加入的工程師喜歡新架構的一致性,但他們並不知道我們為了實現這一目標經歷了怎樣的痛苦。

社群也從我們的經歷中受益。Ellie 做了一個很棒的演示,並透過巡迴演講來分享我們的經驗。我用我的經驗去教其他團隊如何做出更好的決策。

6

寫在最後

我認為,計算機科學當中的一切東西都存在一種權衡,不存在所謂的通用的高階語言。無論你做什麼,都要明白你為什麼要這麼做,不要讓它演變成各派固執己見的政治鬥爭。

設立好故障點。如果你意識到自己犯了一個錯誤,你要弄清楚如何做出權衡,並給自己一條出路。你陷在錯誤決策中的時間越長,成本就越高。不要做一個對解決問題沒有貢獻的壞脾氣的人,不要做一個給別人製造更大問題的狂熱者。與我共事過的那些優秀的工程師們都很善於避免落入這兩個陷阱。

https://threadreaderapp。com/thread/1336890442768547845。html

TAG: SwiftAPP我們Uber編譯器