美團外賣訂單中心的演進

【前言】

美團外賣從2013年9月成交第一單以來,已走過了三個年頭。期間,業務飛速發展,美團外賣由日均幾單發展爲日均500萬單(9月11日已突破600萬)的大型O2O互聯網外賣服務平臺。平臺支持的品類也由最初外賣單品拓展爲全品類。

隨着訂單量的增長、業務複雜度的提升,外賣訂單系統也在不斷演變進化,從早期一個訂單業務模塊到現在分佈式可擴展的高性能、高可用、高穩定訂單系統。整個發展過程中,訂單系統經歷了幾個明顯的階段,下面本篇文章將爲大家介紹一下訂單系統的演進過程,重點關注各階段的業務特徵、挑戰及應對之道。

爲方便大家更好地瞭解整個演進過程,我們首先看一下外賣業務。

外賣訂單業務

外賣訂單業務是一個需要即時送的業務,對實時性要求很高。從用戶訂餐到最終送達用戶,一般在1小時內。如果最終送達用戶時間變長,會帶來槽糕的用戶體驗。在1小時內,訂單會快速經過多個階段,直到最終送達用戶。各個階段需要緊密配合,確保訂單順利完成。

下圖是一個用戶視角的訂單流程圖:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

從普通用戶的角度來看,一個外賣訂單從下單後,會經歷支付、商家接單、配送、用戶收貨、售後及訂單完成多個階段。以技術的視角來分解的話,每個階段依賴於多個子服務來共同完成,比如下單會依賴於購物車、訂單預覽、確認訂單服務,這些子服務又會依賴於底層基礎系統來完成其功能。

外賣業務另一個重要特徵是一天內訂單量會規律變化,訂單會集中在中午、晚上兩個“飯點”附近,而其它時間的訂單量較少。這樣,飯點附近系統壓力會相對較大。

下圖是一天內的外賣訂單量分佈圖:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

總結而言,外賣業務具有如下特徵:

  • 流程較長且實時性要求高;

  • 訂單量高且集中。

下面將按時間脈絡爲大家講解訂單系統經歷的各個階段、各階段業務特徵、挑戰以及應對之道。

訂單系統雛型

外賣業務發展早期,第一目標是要能夠快速驗證業務的可行性。技術上,我們需要保證架構足夠靈活、快速迭代從而滿足業務快速試錯的需求。

在這個階段,我們將訂單相關功能組織成模塊,與其它模塊(門店模塊等)一起形成公用jar包,然後各個系統通過引入jar包來使用訂單功能。

早期系統的整體架構圖如下所示:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

早期,外賣整體架構簡單、靈活,公共業務邏輯通過jar包實現後集成到各端應用,應用開發部署相對簡單。比較適合業務早期邏輯簡單、業務量較小、需要快速迭代的情況。但是,隨着業務邏輯的複雜、業務量的增長,單應用架構的弊端逐步暴露出來。系統複雜後,大家共用一個大項目進行開發部署,協調的成本變高;業務之間相互影響的問題也逐漸增多。

早期業務處於不斷試錯、快速變化、快速迭代階段,通過上述架構,我們能緊跟業務,快速滿足業務需求。隨着業務的發展以及業務的逐步成熟,我們對系統進行逐步升級,從而更好地支持業務。

獨立的訂單系統

2014年4月,外賣訂單量達到了10萬單/日,而且訂單量還在持續增長。這時候,業務大框架基本成型,業務在大框架基礎上快速迭代。大家共用一個大項目進行開發部署,相互影響,協調成本變高;多個業務部署於同一VM,相互影響的情況也在增多。

爲解決開發、部署、運行時相互影響的問題。我們將訂單系統進行獨立拆分,從而獨立開發、部署、運行,避免受其它業務影響。

系統拆分主要有如下幾個原則:

  • 相關業務拆分獨立系統;

  • 優先級一致的業務拆分獨立系統;

  • 拆分系統包括業務服務和數據。

基於以上原則,我們將訂單系統進行獨立拆分,所有訂單服務通過RPC接口提供給外部使用。訂單系統內部,我們將功能按優先級拆分爲不同子系統,避免相互影響。訂單系統通過MQ(隊列)消息,通知外部訂單狀態變更。

獨立拆分後的訂單系統架構如下所示:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

其中,最底層是數據存儲層,訂單相關數據獨立存儲。訂單服務層,我們按照優先級將訂單服務劃分爲三個系統,分別爲交易系統、查詢系統、異步處理系統。

獨立拆分後,可以避免業務間的相互影響。快速支持業務迭代需求的同時,保障系統穩定性。

高性能、高可用、高穩定的訂單系統

訂單系統經過上述獨立拆分後,有效地避免了業務間的相互干擾,保障迭代速度的同時,保證了系統穩定性。這時,我們的訂單量突破百萬,而且還在持續增長。之前的一些小問題,在訂單量增加後,被放大,進而影響用戶體驗。比如,用戶支付成功後,極端情況下(比如網絡、數據庫問題)會導致支付成功消息處理失敗,用戶支付成功後依然顯示未支付。訂單量變大後,問題訂單相應增多。我們需要提高系統的可靠性,保證訂單功能穩定可用。

另外,隨着訂單量的增長、訂單業務的複雜,對訂單系統的性能、穩定性、可用性等提出了更高的要求。

爲了提供更加穩定、可靠的訂單服務,我們對拆分後的訂單系統進行進一步升級。下面將分別介紹升級涉及的主要內容。

性能優化

系統獨立拆分後,可以方便地對訂單系統進行優化升級。我們對獨立拆分後的訂單系統進行了很多的性能優化工作,提升服務整體性能,優化工作主要涉及如下幾個方面。

異步化

服務所需要處理的工作越少,其性能自然越高。可以通過將部分操作異步化來減少需要同步進行的操作,進而提升服務的性能。異步化有兩種方案。

  • 線程或線程池:將異步操作放在單獨線程中處理,避免阻塞服務線程;

  • 消息異步:異步操作通過接收消息完成。

異步化帶來一個隱患,如何保障異步操作的執行。這個場景主要發生在應用重啓時,對於通過線程或線程池進行的異步化,JVM重啓時,後臺執行的異步操作可能尚未完成。這時,需要通過JVM優雅關閉來保證異步操作進行完成後,JVM再關閉。通過消息來進行的,消息本身已提供持久化,不受應用重啓影響。

具體到訂單系統,我們通過將部分不必同步進行的操作異步化,來提升對外服務接口的性能。不需要立即生效的操作即可以異步進行,比如發放紅包、PUSH推送、統計等。

以訂單配送PUSH推送爲例,將PUSH推送異步化後的處理流程變更如下所示:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

PUSH異步化後,線程#1在更新訂單狀態、發送消息後立即返回,而不用同步等待PUSH推送完成。而PUSH推送異步在線程#2中完成。

並行化

操作並行化也是提升性能的一大利器,並行化將原本串行的工作並行執行,降低整體處理時間。我們對所有訂單服務進行分析,將其中非相互依賴的操作並行化,從而提升整體的響應時間。

以用戶下單爲例,第一步是從各個依賴服務獲取信息,包括門店、菜品、用戶信息等。獲取這些信息並不需要相互依賴,故可以將其並行化,並行後的處理流程變更如下所示:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

通過將獲取信息並行化,可有效縮短下單時間,提升下單接口性能。

緩存

通過將統計信息進行提前計算後緩存,避免獲取數據時進行實時計算,從而提升獲取統計數據的服務性能。比如對於首單、用戶已減免配送費等,通過提前計算後緩存,可以簡化實時獲取數據邏輯,節約時間。

以用戶已減免配送費爲例,如果需要實時計算,則需要取到用戶所有訂單後,再進行計算,這樣實時計算成本較高。我們通過提前計算,緩存用戶已減免配送費。需要取用戶已減免配送費時,從緩存中取即可,不必實時計算。具體來說,包括如下幾點:

  • 通過緩存保存用戶已減免配送費;

  • 用戶下單時,如果訂單有減免配送費,增加緩存中用戶減免配送費金額(異步進行);

  • 訂單取消時,如果訂單有減免配送費,減少緩存中用戶減免配送費金額(異步進行)。

一致性優化

訂單系統涉及交易,需要保證數據的一致性。否則,一旦出現問題,可能會導致訂單不能及時配送、交易金額不對等。

交易一個很重要的特徵是其操作具有事務性,訂單系統是一個複雜的分佈式系統,比如支付涉及訂單系統、支付平臺、支付寶/網銀等第三方。僅通過傳統的數據庫事務來保障不太可行。對於訂單交易系統的事務性,並不要求嚴格滿足傳統數據庫事務的ACID性質,只需要最終結果一致即可。針對訂單系統的特徵,我們通過如下種方式來保障最終結果的一致性。

重試/冪等

通過延時重試,保證操作最終會最執行。比如退款操作,如退款時遇到網絡或支付平臺故障等問題,會延時進行重試,保證退款最終會被完成。重試又會帶來另一個問題,即部分操作重複進行,需要對操作進行冪等處理,保證重試的正確性。

以退款操作爲例,加入重試/冪等後的處理流程如下所示:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

退款操作首先會檢查是否已經退款,如果已經退款,直接返回。否則,向支付平臺發起退款,從而保證操作冪等,避免重複操作帶來問題。如果發起退款失敗(比如網絡或支付平臺故障),會將任務放入延時隊列,稍後重試。否則,直接返回。

通過重試+冪等,可以保證退款操作最終一定會完成。

2PC

2PC是指分佈式事務的兩階段提交,通過2PC來保證多個系統的數據一致性。比如下單過程中,涉及庫存、優惠資格等多個資源,下單時會首先預佔資源(對應2PC的第一階段),下單失敗後會釋放資源(對應2PC的回滾階段),成功後會使用資源(對應2PC的提交階段)。對於2PC,網上有大量的說明,這裏不再繼續展開。

高可用

分佈式系統的可用性由其各個組件的可用性共同決定,要提升分佈式系統的可用性,需要綜合提升組成分佈式系統的各個組件的可用性。

針對訂單系統而言,其主要組成組件包括三類:存儲層、中間件層、服務層。下面將分層說明訂單系統的可用性。

存儲層

存儲層的組件如MySQL、ES等本身已經實現了高可用,比如MySQL通過主從集羣、ES通過分片複製來實現高可用。存儲層的高可用依賴各個存儲組件即可。

中間件層

分佈式系統會大量用到各類中間件,比如服務調用框架等,這類中間件一般使用開源產品或由公司基礎平臺提供,本身已具備高可用。

服務層

在分佈式系統中,服務間通過相互調用來完成業務功能,一旦某個服務出現問題,會級聯影響調用方服務,進而導致系統崩潰。分佈式系統中的依賴容災是影響服務高可用的一個重要方面。

依賴容災主要有如下幾個思路:

  • 依賴超時設置;

  • 依賴災備;

  • 依賴降級;

  • 限制依賴使用資源。

訂單系統會依賴多個其它服務,也存在這個問題。當前訂單系統通過同時採用上述四種方法,來避免底層服務出現問題時,影響整體服務。具體實現上,我們採用Hystrix框架來完成依賴容災功能。Hystrix框架採用上述四種方法,有效實現依賴容災。訂單系統依賴容災示意圖如下所示:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

通過爲每個依賴服務設置獨立的線程池、合理的超時時間及出錯時回退方法,有效避免服務出現問題時,級聯影響,導致整體服務不可用,從而實現服務高可用。

另外,訂單系統服務層都是無狀態服務,通過集羣+多機房部署,可以避免單點問題及機房故障,實現高可用。

小結

上面都是通過架構、技術實現層面來保障訂單系統的性能、穩定性、可用性。實際中,有很多的事故是人爲原因導致的,除了好的架構、技術實現外,通過規範、制度來規避人爲事故也是保障性能、穩定性、可用性的重要方面。訂單系統通過完善需求review、方案評審、代碼review、測試上線、後續跟進流程來避免人爲因素影響訂單系統穩定性。

通過以上措施,我們將訂單系統建設成了一個高性能、高穩定、高可用的分佈式系統。其中,交易系統tp99爲150ms、查詢系統tp99時間爲40ms。整體系統可用性爲6個9。

可擴展的訂單系統

訂單系統經過上面介紹的整體升級後,已經是一個高性能、高穩定、高可用的分佈式系統。但是系統的的可擴展性還存在一定問題,部分服務只能通過垂直擴展(增加服務器配置)而不能通過水平擴展(加機器)來進行擴容。但是,服務器配置有上限,導致服務整體容量受到限制。

到2015年5月的時候,這個問題就比較突出了。當時,數據庫服務器寫接近單機上限。業務預期還會繼續快速增長。爲保障業務的快速增長,我們對訂單系統開始進行第二次升級。目標是保證系統有足夠的擴展性,從而支撐業務的快速發展。

分佈式系統的擴展性依賴於分佈式系統中各個組件的可擴展性,針對訂單系統而言,其主要組成組件包括三類:存儲層、中間件層、服務層。下面將分層說明如何提高各層的可擴展性。

存儲層

訂單系統存儲層主要依賴於MySQL持久化、tair/redis cluster緩存。tair/redis cluster緩存本身即提供了很好的擴展性。MySQL可以通過增加從庫來解決讀擴展問題。但是,對於寫MySQL存在單機容量的限制。另外,數據庫的整體容量受限於單機硬盤的限制。

存儲層的可擴展性改造主要是對MySQL擴展性改造。

分庫分表

寫容量限制是受限於MySQL數據庫單機處理能力限制。如果能將數據拆爲多份,不同數據放在不同機器上,就可以方便對容量進行擴展。

對數據進行拆分一般分爲兩步,第一步是分庫,即將不同表放不同庫不同機器上。經過第一步分庫後,容量得到一定提升。但是,分庫並不能解決單表容量超過單機限制的問題,隨着業務的發展,訂單系統中的訂單表即遇到了這個問題。

針對訂單表超過單庫容量的問題,需要進行分表操作,即將訂單表數據進行拆分。單表數據拆分後,解決了寫的問題,但是如果查詢數據不在同一個分片,會帶來查詢效率的問題(需要聚合多張表)。由於外賣在線業務對實時性、性能要求較高。我們針對每個主要的查詢維度均保存一份數據(每份數據按查詢維度進行分片),方便查詢。

具體來說,外賣主要涉及三個查詢維度:訂單ID、用戶ID、門店ID。對訂單表分表時,對於一個訂單,我們存三份,分別按照訂單ID、用戶ID、門店ID以一定規則存儲在每個維度不同分片中。這樣,可以分散寫壓力,同時,按照訂單ID、用戶ID、門店ID三個維度查詢時,數據均在一個分片,保證較高的查詢效率。

訂單表分表後,訂單表的存儲架構如下所示:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

可以看到,分表後,每個維度共有100張表,分別放在4個庫上面。對於同一個訂單,冗餘存儲了三份。未來,隨着業務發展,還可以繼續通過將表分到不同機器上來持續獲得容量的提升。

分庫分表後,訂單數據存儲到多個庫多個表中,爲應用層查詢帶來一定麻煩,解決分庫分表後的查詢主要有三種方案:

  • MySQL服務器端支持:目前不支持

  • 中間件

  • 應用層

由於MySQL服務器端不能支持,我們只剩下中間件和應用層兩個方案。中間件方案對應用透明,但是開發難度相對較大,當時這塊沒有資源去支持。於是,我們採用應用層方案來快速支持。結合應用開發框架(SPRING+MYBATIS),我們實現了一個輕量級的分庫分表訪問插件,避免將分庫分表邏輯嵌入到業務代碼。分庫分表插件的實現包括如下幾個要點。

  • 配置文件管理分庫分表配置信息;

  • JAVA註解說明SQL語句分庫分表信息;

  • JAVA AOP解析註解+查詢配置文件,獲取數據源及表名;

  • MYBATIS動態替換表名;

  • SPRING動態替換數據源。

通過分庫分表,解決了寫容量擴展問題。但是分表後,會給查詢帶來一定的限制,只能支持主要維度的查詢,其它維度的查詢效率存在問題。

ES搜索

訂單表分表之後,對於ID、用戶ID、門店ID外的查詢(比如按照手機號前綴查詢)存在效率問題。這部分通常是複雜查詢,可以通過全文搜索來支持。在訂單系統中,我們通過ES來解決分表後非分表維度的複雜查詢效率問題。具體來說,使用ES,主要涉及如下幾點:

  • 通過databus將訂單數據同步到ES。

  • 同步數據時,通過批量寫入來降低ES寫入壓力。

  • 通過ES的分片機制來支持擴展性。

小結

通過對存儲層的可擴展性改造,使得訂單系統存儲層具有較好的可擴展性。對於中間層的可擴展性與上面提到的中間層可用性一樣,中間層本身已提供解決方案,直接複用即可。對於服務層,訂單系統服務層提供的都是無狀態服務,對於無狀態服務,通過增加機器,即可獲得更高的容量,完成擴容。

通過對訂單系統各層可擴展性改造,使得訂單系統具備了較好的可擴展性,能夠支持業務的持續發展,當前,訂單系統已具體千萬單/日的容量。

上面幾部分都是在介紹如何通過架構、技術實現等手段來搭建一個可靠、完善的訂單系統。但是,要保障系統的持續健康運行,光搭建系統還不夠,運維也是很重要的一環。

智能運維的訂單系統

早期,對系統及業務的運維主要是採用人肉的方式,即外部反饋問題,RD通過排查日誌等來定位問題。隨着系統的複雜、業務的增長,問題排查難度不斷加大,同時反饋問題的數量也在逐步增多。通過人肉方式效率偏低,並不能很好的滿足業務的需求。

爲提升運維效率、降低人力成本,我們對系統及業務運維進行自動化、智能化改進,改進包括事前、事中、事後措施。

事前措施

事前措施的目的是爲提前發現隱患,提前解決,避免問題惡化。

在事前措施這塊,我們主要採取如下幾個手段:

  1. 定期線上壓測:通過線上壓測,準確評估系統容量,提前發現系統隱患;

  2. 週期性系統健康體檢:通過週期檢測CPU利用率、內存利用率、接口QPS、接口TP95、異常數,取消訂單數等指標是否異常,可以提前發現提前發現潛在問題、提前解決;

  3. 全鏈路關鍵日誌:通過記錄全鏈路關鍵日誌,根據日誌,自動分析反饋訂單問題原因,給出處理結果,有效提高反饋處理效率。

事中措施

事中措施的目的是爲及時發現問題、快速解決問題。

事中這塊,我們採取的手段包括:

  1. 訂單監控大盤:實時監控訂單業務指標,異常時報警;

  2. 系統監控大盤:實時監控訂單系統指標,異常時報警;

  3. 完善的SOP:報警後,通過標準流程,快速定位問題、解決問題。

事後措施

事後措施是指問題發生後,分析問題原因,徹底解決。並將相關經驗教訓反哺給事前、事中措施,不斷加強事先、事中措施,爭取儘量提前發現問題,將問題扼殺在萌芽階段。

通過將之前人肉進行的運維操作自動化、智能化,提升了處理效率、減少了運維的人力投入。




640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章