【共享】滴滴出行業務系統的架構升級

11月1日起,備受關注的《網路預約出租汽車經營服務管理暫行辦法》正式施行,以滴滴為代表的網約車將何去何從?如果你更關心滴滴背後的技術,歡迎閱讀此文章。

本文根據杜歡在2016ArchSummit全球架構師(深圳)峰會上的演講整理而成。

ArchSummit即將在2018年7月6日深圳華僑城洲際酒店開幕,更多分享內容請瀏覽:鏈接

講師介紹:

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

-----------

滴滴出行業務系統的架構升級 - 騰訊視頻 http://v.qq.com/x/page/j00217ybt8f.html

杜歡:先自我介紹一下,我是杜歡,2015年5月加入滴滴,在此之前我有五年的創業經歷,創業前曾在微軟和百度任職。我非常喜歡思考一些比較抽象和有趣的東西,滴滴這樣一個平台能夠讓我找到樂趣和興奮點。滴滴是中國出行領域的縮影,業務非常獨特,也是一個高速成長的公司。去年我考慮加入滴滴的時候,我發現有這樣一個機會能夠從頭重新思考整個滴滴的業務形態,而且能夠對它的業務產生影響,這件事情讓我非常興奮。

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

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

一、挑戰在哪裡

首先,我們看一下挑戰在哪裡。滴滴在出行領域是非常獨特的公司,它的獨特不在於業務模式多複雜,而在於它的發展非常快。滴滴的成立時間是2012年的6月,到現在為止才經過了四年的時間。滴滴的成長速度十分驚人,到今天它的估值已經超過260億美元,融資輪次非常多。如果不是因為競爭非常惡劣,滴滴也不會一直用融資的方法為自己開路。在這樣的壓力之下,滴滴所有的動作可能都會走形,所有的想法可能因為現在一些短期利益不得不進行一些權衡

同時,公司的業務也在爆炸式地增長。如果滴滴只做一個業務,原本可以做得非常深入。滴滴從2014年開始加入了專車業務,2015年業務數量增加到七條,2016年已經超過十條。業務急速發展之中大家會思考,到底怎麼做才能使這些還不穩定或者還沒有想清楚的業務很好地迭代起來。想到最簡單的方法是,如果新業務跟某箇舊業務非常類似但又不完全一樣,我們就把舊業務的舊代碼複製並修改,這樣新業務就做出來了。之前,這種情況經常發生,就造成了很大的問題。

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

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

二、現狀是什麼

在解決問題之前,我們需要了解現狀是怎樣的。如下圖所示,在2015年下半年,滴滴的系統架構分為四層。最頂層是用戶應用,每一個用戶應用就是一個端,也就是用戶所能看到的入口。然後是接入層,這是非常傳統的結構,我們用了Nginx,還專門做了TCP接入層。在業務層,Web是非常大的集群,有非常大的代碼量,我們只對業務做了分割,有策略引擎、司機調度。在數據層,有KV集群、MySQL集群、任務隊列、特徵存儲。這是任何一個初創公司應該有的架構,我們對這個架構並沒有做特殊的策劃,僅僅在這個技術體系裡面把業務邏輯實現出來

下面這張圖可能會比較有趣。右邊這個紅色的球,代表的是重構之前App依賴的關係。當時我很想梳理一下App在模塊之間是如何進行依賴的,然後我就寫了一個腳本運行了一下,得到的結果讓我很驚訝。我用藍色的線表示正常的依賴,就是模塊A依賴於模塊B,A是B的上一層,B不會反過來依賴A,用紅色的線表示異常的依賴,即A依賴B、B通過各種手段反過來依賴A,最後發現基本上都是紅色的。做任何模塊的拆分,發現不得不面臨這樣的問題:把任何一個模塊取出來就等於把所有模塊都取出來,實際上沒有做拆分。所以,關鍵是需要解耦模塊結果。這是iOS的情況,Android的情況更糟糕。

對於Web App來講,最大的問題在於耦合性。以前滴滴只有計程車這個業務,最開始的Web App只有計程車,後來專車上線了,就在計程車裡面加了專車入口,只是業務名不同界面會有小區別,後來加入了快車、代駕,都跟計程車差不多,沒遇到太大問題。再後來有了順風車,順風車跟其他功能不一樣,整體界面是預約型的,有乘客和車主兩種模式。如果在老首頁裡面開發順風車成本太大了,需要和計程車業務線的人一起開發業務模塊,如果未來做迭代,這種開發模式將非常痛苦。老首頁的模塊也沒有做拆分,代碼散落各地,只是通過打包工具拼接在一起,沒有做模塊化,所以整體情況也比較糟糕。

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

三、該如何入手

現狀看上去很糟糕,要仔細思考才能入手。最基本的思路是把所有事情分類,就像整理自己家裡一樣,無論多亂,我們要做的事情就是將東西分門別類放好。因此,最關鍵的是要了解到底哪些東西應該放在一起,我們用顏色來比喻模塊或者代碼的歸屬,核心問題就變成這些模塊到底是什麼顏色。

我們的思路是,先從前面,也就是從用戶入口進行拆分,要先保證所有的模塊是足夠內聚的,由統一的團隊負責。比如,計程車業務線可以完全控制自己的代碼,能夠寫自己的客戶端,也能夠寫自己的Web App,最終只是通過一些工程構建手段將多個業務整合起來變成一個完整的端。做到這一點之後,所有的業務迭代問題就迎刃而解了,因為業務間已經沒有依賴和耦合了。這一步完成之後做的就是重新梳理業務,讓業務根據自己模型特點進行一些重構。

最開始的時候,我們考慮的是怎麼做代碼治理模塊下沉。代碼治理本質上就是把各種模塊進行染色、再把它們歸類的過程。代碼治理最難的事情在於消除錯綜複雜的依賴。到底怎麼做才對呢?首先,一定要把不同模塊的代碼放在不同倉庫裡面,使得模塊能夠物理上隔離。特別是Java、Obj-C這些靜態編譯的語言,一旦把代碼倉庫隔離就完全沒有辦法直接對其他模塊產生依賴,至少絕對不會再出現循環依賴。再者,就看如何把循環依賴通過一些間接層隔離開,比如通過抽象介面隔離開,一點一點把代碼拆到不同倉庫。最後,有了這樣一個簡單的拆分之後,就需要考慮怎麼讓模塊能獨立的開發、測試、上線。獨立的流程一旦獨立起來,就意味著拆分基本上成功了。

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

模塊下沉是一種機制,一方面我們應該鼓勵,另一方面還應該讓大家發現這是一件不得不做的事情。如果僅僅對內公開模塊列表讓大家自由選擇,達不到模塊下沉的目的。因為人都很懶,不想思考太多,只想儘快把事情完成,大家往往傾向於複製粘貼,也不願意額外花時間做下沉。怎麼辦呢?我們會給所有業務提供一個統一的SDK,裡面包含所有能用的組件,大家必須使用它進行開發。如果業務模塊穩定了並且比較通用,我們有工具和相應的簡單機制把業務模塊下沉下來,變成SDK的一部分,長期下去SDK會越來越大,只要SDK里做好分類和規劃,上層就會越來越輕,我們可以真正專註於業務邏輯開發。

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

「無狀態」這個概念在服務端比較容易理解。一般我們傾向於把各種業務做到無狀態,這樣容易做水平擴展。在客戶端也是一個道理,也要考慮橫向擴展性。一個簡單的框架往往提供一些最基礎的控制項,比如按紐、列表,這些都不會耦合任何業務邏輯,所以很容易使用。但是當業務做起來,大家習慣將一些狀態放到業務控制項裡面,這在一定程度上方便了,但是一旦需要將業務進行重構或者進行模塊化下沉的時候,就造成了非常大的困難。例如,一個模塊如果大量通過全局變數或單例跟上下游耦合,那麼這個模塊就很難復用和重構,這些全局變數或單例就是狀態。所以,我們在客戶端也提出使用「無狀態」的方式,把存儲的信息都放到外面。後面我會提到到底應該怎麼樣去做。

「非同步化」也是解耦的方式。服務端的RPC類似於函數調用,如果參數變了,實現和調用的雙方都要做改變,這很不透明,也不能夠漸進式上線。我們用訂閱/發布的模式對 RPC進行解耦,要求所有介面都要非同步返回。在客戶端也是這樣,比如做數據的緩存,想優化網路,我們不能夠期待這個函數是一個同步函數,一定用回調的方式接受所有參數。所以做設計的時候,只要是有可能發生網路請求或者訪問磁碟,在客戶端也盡量非同步請求數據。

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

滴滴是一個出行的平台,涵蓋的是整個出行領域所有的出行需求。大家出行到底想要什麼?就是到達自己想去的地方。實際上,我們的模型可以做得非常抽象和簡單。比如,我想要打快車去機場,我就是一個需求方,我的需求會發到很多服務者那裡去,服務者會根據特徵進行一些匹配。最基本的特徵是服務能力,如果服務者能夠開快車並通過了能力驗證,這個需求就有可能發給他。如果開計程車的也有能力開快車,但是他還沒有在平台上驗證這個能力,就只能開計程車。一個人可以驗證很多服務,白天可以開快車,晚上可以做代駕,做不同的事。

服務和需求的匹配是通過計價模型和匹配策略來實現的。發送需求的時候需要選擇計價模型和車的類型。快車和專車服務過程大同小異,但是價格差別很明顯,專車價格會貴很多。通過匹配策略可以實現各種需求的匹配。例如,選擇了拼車,這個需求會盡量匹配已經有拼友和順路的車。如果選擇專車,可以要求這輛車在指定時間來接人,這時候匹配策略會優化傾向這種方式。

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

基於這樣的想法,我們就思考如何設計真正高度抽象的工具。簡單起見,我們把滴滴出行的過程抽象成一個框架(見下圖),這並不是完整的框架。有顏色的地方表示計程車、快車、專車、代駕共同的流程,只要組合各種流程就可以實現整個業務形態的能力。在這個框架里可以定製所有業務形態的車標、提示語、匹配的模型、計價模型等功能。當時梳理這個抽象的時候,我們感覺非常興奮,因為這意味著在這個基礎之上就可以簡易擴展出滴滴未來的業務形態。只要滴滴還是在做需求和服務的匹配,基本上就離不開這樣一種套路。

四、客戶端怎麼拆

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

首先就是客戶端,最重要的是需要將業務拆出來。以前所有業務放在同一個倉庫里,如果不小心提交了一段錯誤代碼就會帶來災難性的後果,所有業務工作可能都會受到影響。以前編譯速度也很糟糕,大家可以想像,每次下載代碼都會有幾個頭文件發生改變,由於循環依賴的緣故幾乎所有文件都要重編,二三十分鐘後才能重新調試,這個過程讓人極度崩潰。對於iOS,我們用cocoapods把業務拆到不同的pod里面;對於Android,我們把業務拆分打包並用Maven管理起來。

我們拆分方法如下圖所示,其中虛線框部分展示的是公共框架,最開始沒有很細緻分割,只是把它放在一個獨立倉庫里,保證依賴關係充分清楚,後面就可以隨時把代碼獨立出來,使其變成單獨的模塊。

同時,我們也在開發構建系統。原生的構建系統使用起來會有很多問題,它並不支持多人並行開發,如果要實現一個舒適的工作流就需要定製。我們還做了網路和日誌的封裝,將其放在下層。還有一個業務整合的基礎框架,包括滴滴出行的App界面框架、首頁導航欄,各種業務可以註冊自己的入口,並在導航欄里進行切換。業務之間沒有任何代碼耦合,比如計程車和專車業務沒有關聯性,那麼代碼也沒有任何相關的地方,這意味著開發計程車業務的時候,完全沒有必要實時更新專車代碼,集成的時候也不會因為專車代碼而造成問題。最頂層的One Travel可以通過簡單的配置分業務包,比如可以輸出只有計程車業務的包,在這上面開發測試速度比較快,整體也會比較靈活。One Travel裡面只有極少的代碼,未來會改成沒有代碼、通過腳本就可以生成的項目。

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

客戶端的業務代碼其實不關心究竟是如何同步狀態的,所以我們專門寫了一個緩存伺服器狀態的Store層,它是熱數據。如果不需要最新狀態的數據,業務讀取Store時可以讀到上次同步的數據,假設此時Store從未同步過狀態就會自動讀取最新狀態;如果業務一定要最新狀態的數據,那麼就顯示要求緩存失效,這樣Store就會再讀取一次獲取最新的信息。Store還可以自動設置失效時間長度,這個機制跟跟做資料庫緩存是一樣的,為了性能的平衡,要保證讀出準確的數據,同時性能也要最優。同時,Store也有責任負責數據更新,當客戶端變化可能會讓伺服器狀態變化時,Store可以自動讓相關狀態失效,這也是管理緩存的一般做法。

做了這樣一些解耦之後,令人驚喜的是,我們發現所有界面是可以隨意跳轉的,雖然沒有從發單直接跳到評價的必要性,但實際上只要有這個架構,就可以從界面A跳到界面B,不會有任何問題。如果跳到另外一個界面,沒有發現必要的數據,就從伺服器讀取,它自己也會報錯,整個邏輯非常清晰。如果需要在流程A和流程B之間再增加一個流程C,我們可以把流程C直接加進去,流程C沒有破壞A和B之間的依賴,因為原本A和B之間也沒有什麼依賴。

我們也做一些App的組件化,把從服務端API到客戶端邏輯打包在一起,引用客戶端組件就可以實現完整功能。實際封裝方法略微有點複雜(編者註:可以閱讀另外一篇文章《滴滴的組件化實踐與優化》)。圖中所示是做平滑移動組件,地圖上有很多車在移動,這些車就是地圖上的額外信息,把這些車掛在地圖上。如果這個控制項不存在,地圖上就沒有車,控制項存在,地圖上就有車,只要在上面啟動控制項就好了。

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

五、Web App怎麼拆

接下來講Web App的拆解,這實際上是純工程的解耦。首先,我們需要實現一個簡單的公共框架,這跟業務是無關的。我們使用scrat和webpack來實現工程化,將首頁拆分成了許多組件,所有的業務可以根據不同配置選擇使用哪些組件,同時也保證頁面風格的統一、功能的穩定。如果網路比較糟糕,我們會做一系列的降級,首先出來的會是一些統一的控制項,比如上車地點、目的地、廣告等,之後會根據定位的結果得到當前開通的業務線列表,並載入業務代碼,然後默認選擇當前業務線的邏輯。如果業務線代碼載入好了就開始渲染,如果業務載入出錯或代碼執行出錯,業務就會被隱藏。業務線之間也是完全解耦的,大家可以通過公共框架提供的事件機制來通信,但不允許業務之間直接通信。線上的Web App就是如上圖所看到的,每個業務線都有一段獨立js代碼,第一次載入相對較慢,會看到很多請求,如果業務線代碼沒有更新,下次打開就完全不走網路請求。

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

六、伺服器API怎麼拆

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

我先來談談理想方案是什麼。首先,滴滴業務一般都是基於訂單流轉推動各種業務動作。為什麼會發生訂單流轉?是因為對乘客和司機做了一些操作,如果想像成一個客戶端系統,就有點類似於觸發各種用戶事件。客戶端動作根本上決定了信息該如何流轉,所有事情都應該在客戶端觸發,觸發之後來到了組件這一層,所有動作進行消費,然後進行下一步操作。比如,用戶提出一個需求,發單對需求進行過濾,判斷是哪種需求,然後進行一些檢查。快車有拼車和不拼車兩種,發單的時候就可以知道是拼車還是不拼車,對於統一訂單系統來說這就是個標誌。無論拼不拼,這個單對用戶都一樣,無非就是消耗多少人民幣、消耗幾個座位還是消耗整輛車的問題。之後分單系統會進行訂單的匹配。一旦匹配成功,客戶端有很多動作,司機確認接單,乘客可以看到確認。如果直接做成消息,客戶端和服務端用一條匯流排連接,問題就解決了。

這裡有一個很大的優點——可拼接,所有東西都組件化了。但是最大的問題在於抽象程度非常高。這是函數式的思想,要求所有的Worker都是純函數,純函數是非常高的要求,上下文狀態必須要通過參數才行。我們發現很難做到這一點,因為所有系統必須有狀態,一旦這樣這個純函數就不是純函數了,要依賴外部的變數。與面向對象設計的思路差異非常大,做函數式設計時很容易陷入一些抉擇當中,如何定義輸入、輸出,如何劃分流程。有一些流程劃分成三段式,中間的流程非同步調出去,又非同步調回來繼續後續流程,這種設計讓人很糾結。函數很依賴非同步化,非同步化會讓數據流變得複雜。我們思考數據流的流向,以及每次數據流在流轉的時候都需要設置的輸入、輸出。最終,這個方案並沒有實施,雖然我們開發了接近半年的時間

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

這會有什麼缺點呢?長期來講缺點還是比較明顯的,就是不容易擴展。現在我們設計的模型是來源於當前業務現狀,如果業務發生改變,比如多了一種車型,就會遇到該如何擴展的抉擇:應該提供更多API介面滿足新的業務功能,還是在原有API修改上提供更多參數。兩種方法看起來都可以,但是本質上我認為無論用哪種方案都會使模塊本身變得越來越臃腫,其實都是把很多種東西融合在一起,並不是很理想。當一個服務臃腫到一定程度之後又會出現以前的問題,又要再次做拆分和重構,甚至整個RPC調用流程都會發生很大震動。

從項目整體實施效果上來講,這次重構最主要是解決了開發迭代的問題,能夠讓迭代速度更快。讓我們比較意外的情況是,重構前客戶端crash率非常高,重構中我們對代碼進行了非常多的修改,同時還在用戶體驗上做了很多優化,但最終crash率反而大幅下降,從以前1%降低到0.3%。重構後各個業務團隊的開發模式發生了根本的變化,以前是各個業務各耦合在一起進行開發,現在各個業務都能獨立開發,互不干擾,同時平台還會不斷產出更多的公共組件

七、如何避免重蹈覆轍

最後提一下如何重蹈覆轍。我認為,所有的設計應該是自上而下,先從產品層面上規劃核心業務的模式,然後考慮如何讓產品技術實現它。如果把業務模式描述成如圖所示的核心循環,會非常清楚。我們不僅要考慮現在,還要考慮未來。如果讓整個架構保持健康,就要考慮什麼功能是真正緊密相關的。比如在服務端,直覺上感覺各種不同的發單應該是在一起的,但實際上並不是這樣。不同車型的發單介面互相之間並沒有什麼聯繫,每一種發單都會有獨特的個性化定製,這些定製才是真正應該跟發單緊耦合的東西。所以我們應該從產品角度上考慮,把一種發單所調用的所有相關API放在一起,服務端發生變化,調用的組件也會發生變化,做到發單閉環。剛剛提到的今年服務端的重構的方法,實際上並沒有讓各個子系統打通,這是一件很遺憾的事。未來如果開發一些新需求,肯定還會涉及多個模塊、團隊,避免不了一些溝通成本。

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

謝謝大家!

推薦閱讀:

Uber與中國交割,滴滴與Uber國際再戰!
如何看待滴滴將展開國際化擴張?
滴滴面經(3輪面試+代駕事業部+招商顧問)
網約車:自己挖的坑,含著淚也要跳下去
據說,滴滴開始裁員了。。。

TAG:滴滴 | 架構 | 互聯網 |