1. 程式人生 > >指數級增長背後,滴滴出行業務系統的架構升級

指數級增長背後,滴滴出行業務系統的架構升級

滴滴出行業務系統

成立四年,估值已超260億美元,公司指數級發展、業務爆炸式增長,在此背景下,滴滴出行業務系統的架構升級是怎樣進行的?本文根據滴滴出行平臺產品中心技術總監——杜歡在2016ArchSummit全球架構師(深圳)峰會上的演講整理而成。

老司機簡介

杜歡,滴滴平臺產品中心技術總監。2015年加入滴滴,負責公司公共業務、客戶端/前端架構和新業務孵化,致力於用技術手段解決業務痛點和提升研發效率,曾作為技術負責人主導公司技術架構升級以支撐公司業務快速迭代的需求。在加入滴滴前有長達五年的創業經歷,具有豐富的團隊管理經驗,熟悉移動網際網路應用的整個技術棧。

今天給大家主要介紹的是去年滴滴內部做的一次重大架構升級,滴滴快速發展的過程中,系統的迭代速度和其他方面的設計遇到了很多困難,這次升級就是為了解決這些困難。

業務大綱

去年我們做了一次非常大的重構。上面圖中是今天要講的大綱,我會從問題本身出發,回顧一下整個過程,包括如何發現問題、分析問題和解決方案。最後,我也會提出一些想法,如何規避重蹈這樣的覆轍。

挑戰在哪裡?

公司規模指數級增長

首先,我們看一下挑戰在哪裡。滴滴在出行領域是非常獨特的公司,它的獨特不在於業務模式多複雜,而在於它的發展非常快。滴滴的成立時間是2012年的6月,到現在為止才經過了四年的時間。

滴滴的成長速度十分驚人,到今天它的估值已經超過260億美元,融資輪次非常多。如果不是因為競爭非常惡劣,滴滴也不會一直用融資的方法為自己開路。在這樣的壓力之下,滴滴所有的動作可能都會走形,所有的想法可能因為現在一些短期利益不得不進行一些權衡。

業務數量

同時,公司的業務也在爆炸式地增長。如果滴滴只做一個業務,原本可以做得非常深入。滴滴從2014年開始加入了專車業務,2015年業務數量增加到七條,2016年已經超過十條。業務急速發展之中大家會思考,到底怎麼做才能使這些還不穩定或者還沒有想清楚的業務很好地迭代起來。

想到最簡單的方法是,如果新業務跟某個舊業務非常類似但又不完全一樣,我們就把舊業務的舊程式碼複製並修改,這樣新業務就做出來了。之前,這種情況經常發生,就造成了很大的問題。

滴滴出行

在2015年上半年,滴滴整個系統已經積累了很多問題,分佈在乘客App、服務端、Web App之中。特別值得一提的是,服務端的問題並不是效能,而是在於巨大的耦合導致資料紊亂和迭代速度越來越慢。

滴滴的獨特性迫使我們獨立思考這些問題,所有的解法都要針對滴滴現狀,而不是看哪個大公司是怎麼做的,然後直接複製過來。

現狀是什麼?

系統架構

在解決問題之前,我們需要了解現狀是怎樣的。如圖所示,在2015年下半年,滴滴的系統架構分為四層。最頂層是使用者應用,每一個使用者應用就是一個端,也就是使用者所能看到的入口。然後是接入層,這是非常傳統的結構,我們用了Nginx,還專門做了TCP接入層。

在業務層,Web是非常大的叢集,有非常大的程式碼量,我們只對業務做了分割,有策略引擎、司機排程。在資料層,有KV叢集、MySQL叢集、任務佇列、特徵儲存。這是任何一個初創公司應該有的架構,我們對這個架構並沒有做特殊的策劃,僅僅在這個技術體系裡面把業務邏輯實現出來。

乘客App結構

上面這張圖可能會比較有趣。右邊這個紅色的球,代表的是重構之前App依賴的關係。當時我很想梳理一下App在模組之間是如何進行依賴的,然後我就寫了一個指令碼運行了一下,得到的結果讓我很驚訝。我用藍色的線表示正常的依賴,就是模組A依賴於模組B,A是B的上一層,B不會反過來依賴A,用紅色的線表示異常的依賴,即A依賴B、B通過各種手段反過來依賴A,最後發現基本上都是紅色的。

做任何模組的拆分,發現不得不面臨這樣的問題:把任何一個模組取出來就等於把所有模組都取出來,實際上沒有做拆分。所以,關鍵是需要解耦模組結果。這是iOS的情況。安卓的情況更糟糕。

Web App結構

對於Web App來講,最大的問題在於耦合性。以前滴滴只有計程車這個業務,最開始的Web App只有計程車,後來專車上線了,就在出租車裡面加了專車入口,只是業務名不同介面會有小區別,後來加入了快車、代駕,都跟出租車差不多,沒遇到太大問題。

再後來有了順風車,順風車跟其他功能不一樣,整體介面是預約型的,有乘客和車主兩種模式。如果在老首頁裡面開發順風車成本太大了,需要和計程車業務線的人一起開發業務模組,如果未來做迭代,這種開發模式將非常痛苦。老首頁的模組也沒有做拆分,程式碼散落各地,只是通過打包工具拼接在一起,沒有做模組化,所以整體情況也比較糟糕。

API結構

相比端,API稍好一點的是,API至少在業務維度上是分開的,計程車與專車、快車是分開的兩個系統,放在兩個倉庫裡面。不過API也有一個很大的問題,業務程式碼沒有做服務化拆分,沒有model 封裝,業務所有的API和後臺MIS都在一個倉庫裡,這對系統來說是非常大的一個隱患。

該如何入手?

現狀看上去很糟糕,要仔細思考才能入手。最基本的思路是把所有事情分類,就像整理自己家裡一樣,無論多亂,我們要做的事情就是將東西分門別類放好。因此,最關鍵的是要了解到底哪些東西應該放在一起,我們用顏色來比喻模組或者程式碼的歸屬,核心問題就變成這些模組到底是什麼顏色。重構思路我們的思路是,先從前面,也就是從使用者入口進行拆分,要先保證所有的模組是足夠內聚的,由統一的團隊負責。比如,計程車業務線可以完全控制自己的程式碼,能夠寫自己的客戶端,也能夠寫自己的Web App,最終只是通過一些工程構建手段將多個業務整合起來變成一個完整的端。

做到這一點之後,所有的業務迭代問題就迎刃而解了,因為業務間已經沒有依賴和耦合了。這一步完成之後做的就是重新梳理業務,讓業務根據自己模型特點進行一些重構。

程式碼

最開始的時候,我們考慮的是怎麼做程式碼治理和模組下沉。程式碼治理本質上就是把各種模組進行染色、再把它們歸類的過程。程式碼治理最難的事情在於消除錯綜複雜的依賴。到底怎麼做才對呢?

  • 首先,一定要把不同模組的程式碼放在不同倉庫裡面,使得模組能夠物理上隔離。特別是Java、Obj-C這些靜態編譯的語言,一旦把程式碼倉庫隔離就完全沒有辦法直接對其他模組產生依賴,至少絕對不會再出現迴圈依賴。
  • 再者,就看如何把迴圈依賴通過一些間接層隔離開,比如通過抽象介面隔離開,一點一點把程式碼拆到不同倉庫。
  • 最後,有了這樣一個簡單的拆分之後,就需要考慮怎麼讓模組能獨立的開發、測試、上線。獨立的流程一旦獨立起來,就意味著拆分基本上成功了。

模組下沉與程式碼治理息息相關。如果只是要求把所有程式碼拆分,而沒有合適的拆分方法,這件事情是無法推進下去的。對於程式設計師來說,他們內心總有一種衝動想做有意思的事情,比如封裝一個很有意思的模組給更多程式設計師用。大家並非不想做封裝,只是如果封裝並共享出來的代價太大,就會影響大家的熱情。

模組下沉是一種機制,一方面我們應該鼓勵,另一方面還應該讓大家發現這是一件不得不做的事情。如果僅僅對內公開模組列表讓大家自由選擇,達不到模組下沉的目的。因為人都很懶,不想思考太多,只想儘快把事情完成,大家往往傾向於複製貼上,也不願意額外花時間做下沉。

怎麼辦呢?我們會給所有業務提供一個統一的SDK,裡面包含所有能用的元件,大家必須使用它進行開發。如果業務模組穩定了並且比較通用,我們有工具和相應的簡單機制把業務模組下沉下來,變成SDK的一部分,長期下去SDK會越來越大,只要SDK裡做好分類和規劃,上層就會越來越輕,我們可以真正專注於業務邏輯開發。

除了上面這些,最核心的一點在於,一定要把所有業務都做到“無狀態”和“非同步化”。

“無狀態”這個概念在服務端比較容易理解。一般我們傾向於把各種業務做到無狀態,這樣容易做水平擴充套件。在客戶端也是一個道理,也要考慮橫向擴充套件性。一個簡單的框架往往提供一些最基礎的控制元件,比如按紐、列表,這些都不會耦合任何業務邏輯,所以很容易使用。

但是當業務做起來,大家習慣將一些狀態放到業務控制元件裡面,這在一定程度上方便了,但是一旦需要將業務進行重構或者進行模組化下沉的時候,就造成了非常大的困難。例如,一個模組如果大量通過全域性變數或單例跟上下游耦合,那麼這個模組就很難複用和重構,這些全域性變數或單例就是狀態。

所以,我們在客戶端也提出使用“無狀態”的方式,把儲存的資訊都放到外面。後面我會提到到底應該怎麼樣去做。

“非同步化”也是解耦的方式。服務端的RPC類似於函式呼叫,如果引數變了,實現和呼叫的雙方都要做改變,這很不透明,也不能夠漸進式上線。我們用訂閱/釋出的模式對 RPC進行解耦,要求所有介面都要非同步返回。

在客戶端也是這樣,比如做資料的快取,想優化網路,我們不能夠期待這個函式是一個同步函式,一定用回撥的方式接受所有引數。所以做設計的時候,只要是有可能發生網路請求或者訪問磁碟,在客戶端也儘量非同步請求資料。

業務模型

剛剛講的都是相對比較抽象的內容,接下來會說一下滴滴的業務形態本身。

滴滴是一個出行的平臺,涵蓋的是整個出行領域所有的出行需求。大家出行到底想要什麼?就是到達自己想去的地方。實際上,我們的模型可以做得非常抽象和簡單。比如,我想要打快車去機場,我就是一個需求方,我的需求會發到很多服務者那裡去,服務者會根據特徵進行一些匹配。

最基本的特徵是服務能力,如果服務者能夠開快車並通過了能力驗證,這個需求就有可能發給他。如果開出租車的也有能力開快車,但是他還沒有在平臺上驗證這個能力,就只能開出租車。一個人可以驗證很多服務,白天可以開快車,晚上可以做代駕,做不同的事。

服務和需求的匹配是通過計價模型和匹配策略來實現的。傳送需求的時候需要選擇計價模型和車的型別。快車和專車服務過程大同小異,但是價格差別很明顯,專車價格會貴很多。通過匹配策略可以實現各種需求的匹配。

例如,選擇了拼車,這個需求會盡量匹配已經有拼友和順路的車。如果選擇專車,可以要求這輛車在指定時間來接人,這時候匹配策略會優化傾向這種方式。

滴滴所有的業務基本上都是以這種模式運轉的,所有功能都是核心主幹或者旁路,只要把業務模型抽象出來,基本上就能夠滿足大部分的業務了。

高度可配置的出行工具

基於這樣的想法,我們就思考如何設計真正高度抽象的工具。簡單起見,我們把滴滴出行的過程抽象成一個框架(見上圖),這並不是完整的框架。有顏色的地方表示出租車、快車、專車、代駕共同的流程,只要組合各種流程就可以實現整個業務形態的能力。在這個框架裡可以定製所有業務形態的車標、提示語、匹配的模型、計價模型等功能。

當時梳理這個抽象的時候,我們感覺非常興奮,因為這意味著在這個基礎之上就可以簡易擴展出滴滴未來的業務形態。只要滴滴還是在做需求和服務的匹配,基本上就離不開這樣一種套路。

客戶端怎麼拆?

然後我們開始落實到具體該怎麼拆的問題。

首先就是客戶端,最重要的是需要將業務拆出來。以前所有業務放在同一個倉庫裡,如果不小心提交了一段錯誤程式碼就會帶來災難性的後果,所有業務工作可能都會受到影響。以前編譯速度也很糟糕,大家可以想象,每次下載程式碼都會有幾個標頭檔案發生改變,由於迴圈依賴的緣故幾乎所有檔案都要重編,二三十分鐘後才能重新除錯,這個過程讓人極度崩潰。

對於iOS,我們用cocoapods把業務拆到不同的pod裡面;對於安卓,我們把業務拆分打包並用Maven管理起來。我們拆分方法如下圖所示,其中虛線框部分展示的是公共框架,最開始沒有很細緻分割,只是把它放在一個獨立倉庫裡,保證依賴關係充分清楚,後面就可以隨時把程式碼獨立出來,使其變成單獨的模組。

乘客App方案

同時,我們也在開發構建系統。原生的構建系統使用起來會有很多問題,它並不支援多人並行開發,如果要實現一個舒適的工作流就需要定製。我們還做了網路和日誌的封裝,將其放在下層。還有一個業務整合的基礎框架,包括滴滴出行的App介面框架、首頁導航欄,各種業務可以註冊自己的入口,並在導航欄裡進行切換。

業務之間沒有任何程式碼耦合,比如計程車和專車業務沒有關聯性,那麼程式碼也沒有任何相關的地方,這意味著開發出租車業務的時候,完全沒有必要實時更新專車程式碼,整合的時候也不會因為專車程式碼而造成問題。

最頂層的One Travel可以通過簡單的配置分業務包,比如可以輸出只有計程車業務的包,在這上面開發測試速度比較快,整體也會比較靈活。One Travel裡面只有極少的程式碼,未來會改成沒有程式碼、通過指令碼就可以生成的專案。

乘客App跨頁面解耦

怎麼做頁面的解耦?上圖中是一種類似資料庫快取的設計。從客戶端角度來看,如果把伺服器當做一個數據庫,最終狀態儲存在伺服器,而客戶端裡存著的是跟伺服器同步過的最新狀態的快取。客戶端不太可能做到精確的資料同步,一定是每隔一段時間同步一次,或者是在關鍵節點上靠伺服器推送得到訂單狀態變化。

客戶端的業務程式碼其實不關心究竟是如何同步狀態的,所以我們專門寫了一個快取伺服器狀態的Store層,它是熱資料。如果不需要最新狀態的資料,業務讀取Store時可以讀到上次同步的資料,假設此時Store從未同步過狀態就會自動讀取最新狀態;如果業務一定要最新狀態的資料,那麼就顯示要求快取失效,這樣Store就會再讀取一次獲取最新的資訊。

Store還可以自動設定失效時間長度,這個機制跟跟做資料庫快取是一樣的,為了效能的平衡,要保證讀出準確的資料,同時效能也要最優。同時,Store也有責任負責資料更新,當客戶端變化可能會讓伺服器狀態變化時,Store可以自動讓相關狀態失效,這也是管理快取的一般做法。

做了這樣一些解耦之後,令人驚喜的是,我們發現所有介面是可以隨意跳轉的,雖然沒有從發單直接跳到評價的必要性,但實際上只要有這個架構,就可以從介面A跳到介面B,不會有任何問題。

如果跳到另外一個介面,沒有發現必要的資料,就從伺服器讀取,它自己也會報錯,整個邏輯非常清晰。如果需要在流程A和流程B之間再增加一個流程C,我們可以把流程C直接加進去,流程C沒有破壞A和B之間的依賴,因為原本A和B之間也沒有什麼依賴。

乘客App元件化

我們也做一些App的元件化,把從服務端API到客戶端邏輯打包在一起,引用客戶端元件就可以實現完整功能。實際封裝方法略微有點複雜(注:可以閱讀另外一篇文章支撐滴滴高速發展的引擎:滴滴的元件化實踐與優化)。

圖中所示是做平滑移動元件,地圖上有很多車在移動,這些車就是地圖上的額外資訊,把這些車掛在地圖上。如果這個控制元件不存在,地圖上就沒有車,控制元件存在,地圖上就有車,只要在上面啟動控制元件就好了。

App整合也採用了非同步和無障礙的做法,每個業務只需要在倉庫裡面測試完之後直接打tag,之後就能自動生成整個所有業務的ipa/apk包。

Web App怎麼拆?

Web App方案

接下來講Web App的拆解,這實際上是純工程的解耦。

首先,我們需要實現一個簡單的公共框架,這跟業務是無關的。我們使用scrat和webpack來實現工程化,將首頁拆分成了許多元件,所有的業務可以根據不同配置選擇使用哪些元件,同時也保證頁面風格的統一、功能的穩定。

如果網路比較糟糕,我們會做一系列的降級,首先出來的會是一些統一的控制元件,比如上車地點、目的地、廣告等,之後會根據定位的結果得到當前開通的業務線列表,並載入業務程式碼,然後預設選擇當前業務線的邏輯。

如果業務線程式碼載入好了就開始渲染,如果業務加載出錯或程式碼執行出錯,業務就會被隱藏。業務線之間也是完全解耦的,大家可以通過公共框架提供的事件機制來通訊,但不允許業務之間直接通訊。線上的Web App就是如上圖所看到的,每個業務線都有一段獨立js程式碼,第一次載入相對較慢,會看到很多請求,如果業務線程式碼沒有更新,下次開啟就完全不走網路請求。

Web App 方案

我們也做了很多控制元件,這是內網釋出的一些控制元件(見上圖),每個業務只要關注自己的業務邏輯即可,公共的功能都可以使用控制元件。特別是選擇地址的控制元件,它把前端介面互動和後端API都打包在一起,和客戶端一樣,只要引用它,就可以直接在Web App使用,無需任何服務端的開發。

伺服器API怎麼拆?

伺服器API拆分

關於伺服器API的拆分,我們最開始希望一次性實現理想方案,但是這個理想方案遇到一些問題。

我先來談談理想方案是什麼。首先,滴滴業務一般都是基於訂單流轉推動各種業務動作。為什麼會發生訂單流轉?是因為對乘客和司機做了一些操作,如果想象成一個客戶端系統,就有點類似於觸發各種使用者事件。客戶端動作根本上決定了資訊該如何流轉,所有事情都應該在客戶端觸發,觸發之後來到了元件這一層,所有動作進行消費,然後進行下一步操作。

比如,使用者提出一個需求,發單對需求進行過濾,判斷是哪種需求,然後進行一些檢查。快車有拼車和不拼車兩種,發單的時候就可以知道是拼車還是不拼車,對於統一訂單系統來說這就是個標誌。無論拼不拼,這個單對使用者都一樣,無非就是消耗多少人民幣、消耗幾個座位還是消耗整輛車的問題。

之後分單系統會進行訂單的匹配。一旦匹配成功,客戶端有很多動作,司機確認接單,乘客可以看到確認。如果直接做成訊息,客戶端和服務端用一條匯流排連線,問題就解決了。

這裡有一個很大的優點——可拼接,所有東西都元件化了。但是最大的問題在於抽象程度非常高。這是函式式的思想,要求所有的Worker都是純函式,純函式是非常高的要求,上下文狀態必須要通過引數才行。我們發現很難做到這一點,因為所有系統必須有狀態,一旦這樣這個純函式就不是純函數了,要依賴外部的變數。

與面向物件設計的思路差異非常大,做函式式設計時很容易陷入一些抉擇當中,如何定義輸入、輸出,如何劃分流程。有一些流程劃分成三段式,中間的流程非同步調出去,又非同步調回來繼續後續流程,這種設計讓人很糾結。

函式很依賴非同步化,非同步化會讓資料流變得複雜。我們思考資料流的流向,以及每次資料流在流轉的時候都需要設定的輸入、輸出。最終,這個方案並沒有實施,雖然我們開發了接近半年的時間。

2016年,我們又重新思考了這個問題,這次是比較簡單和現實的方法。首先我們進行了一些程式碼的隔離,把程式碼分開,之後對系統按照剛才講的模組進行面向物件的抽象,比如發單就是單獨的系統,訂單也是一個單獨的系統,支付的收銀體系是一個系統,評價體系是一個系統。每一個系統變得很簡單,互相之間用RPC呼叫關聯起來。

這會有什麼缺點呢?長期來講缺點還是比較明顯的,就是不容易擴充套件。現在我們設計的模型是來源於當前業務現狀,如果業務發生改變,比如多了一種車型,就會遇到該如何擴充套件的抉擇:應該提供更多API介面滿足新的業務功能,還是在原有API修改上提供更多引數。

兩種方法看起來都可以,但是本質上我認為無論用哪種方案都會使模組本身變得越來越臃腫,其實都是把很多種東西融合在一起,並不是很理想。當一個服務臃腫到一定程度之後又會出現以前的問題,又要再次做拆分和重構,甚至整個RPC呼叫流程都會發生很大震動。

從專案整體實施效果上來講,這次重構最主要是解決了開發迭代的問題,能夠讓迭代速度更快。讓我們比較意外的情況是,重構前客戶端crash率非常高,重構中我們對程式碼進行了非常多的修改,同時還在使用者體驗上做了很多優化,但最終crash率反而大幅下降,從以前1%降低到0.3%。

重構後各個業務團隊的開發模式發生了根本的變化,以前是各個業務各耦合在一起進行開發,現在各個業務都能獨立開發,互不干擾,同時平臺還會不斷產出更多的公共元件。

如何避免重蹈覆轍?

滴滴出行

最後提一下如何重蹈覆轍。我認為,所有的設計應該是自上而下,先從產品層面上規劃核心業務的模式,然後考慮如何讓產品技術實現它。如果把業務模式描述成如圖所示的核心迴圈,會非常清楚。我們不僅要考慮現在,還要考慮未來。如果讓整個架構保持健康,就要考慮什麼功能是真正緊密相關的。

比如在服務端,直覺上感覺各種不同的發單應該是在一起的,但實際上並不是這樣。不同車型的發單介面互相之間並沒有什麼聯絡,每一種發單都會有獨特的個性化定製,這些定製才是真正應該跟發單緊耦合的東西。

所以我們應該從產品角度上考慮,把一種發單所呼叫的所有相關API放在一起,服務端發生變化,呼叫的元件也會發生變化,做到發單閉環。剛剛提到的今年服務端的重構的方法,實際上並沒有讓各個子系統打通,這是一件很遺憾的事。未來如果開發一些新需求,肯定還會涉及多個模組、團隊,避免不了一些溝通成本。

Feature Team

另外給大家介紹一下,我們專門做了一個元件平臺,叫做魔方元件庫,是客戶端到服務端的庫,我們會繼續沉澱更多的客戶端到服務端打通的元件,讓業務開發更快更輕鬆。

文章出處:InfoQ