美團外賣從2013年9月成交第一單以來,已走過了三個年頭。期間,業務飛速發展,美團外賣由日均幾單發展爲日均500萬單(9月11日已突破600萬)的大型O2O互聯網外賣服務平臺。平臺支持的品類也由最初外賣單品拓展爲全品類。redis
隨着訂單量的增加、業務複雜度的提高,外賣訂單系統也在不斷演變進化,從早期一個訂單業務模塊到如今分佈式可擴展的高性能、高可用、高穩定訂單系統。整個發展過程當中,訂單系統經歷了幾個明顯的階段,下面本篇文章將爲你們介紹一下訂單系統的演進過程,重點關注各階段的業務特徵、挑戰及應對之道。數據庫
爲方便你們更好地瞭解整個演進過程,咱們首先看一下外賣業務。緩存
外賣訂單業務是一個須要即時送的業務,對實時性要求很高。從用戶訂餐到最終送達用戶,通常在1小時內。若是最終送達用戶時間變長,會帶來槽糕的用戶體驗。在1小時內,訂單會快速通過多個階段,直到最終送達用戶。各個階段須要緊密配合,確保訂單順利完成。性能優化
下圖是一個用戶視角的訂單流程圖。服務器
從普通用戶的角度來看,一個外賣訂單從下單後,會經歷支付、商家接單、配送、用戶收貨、售後及訂單完成多個階段。以技術的視角來分解的話,每一個階段依賴於多個子服務來共同完成,好比下單會依賴於購物車、訂單預覽、確認訂單服務,這些子服務又會依賴於底層基礎系統來完成其功能。網絡
外賣業務另外一個重要特徵是一天內訂單量會規律變化,訂單會集中在中午、晚上兩個「飯點」附近,而其它時間的訂單量較少。這樣,飯點附近系統壓力會相對較大。架構
下圖是一天內的外賣訂單量分佈圖框架
總結而言,外賣業務具備以下特徵:運維
- 流程較長且實時性要求高;
- 訂單量高且集中。
下面將按時間脈絡爲你們講解訂單系統經歷的各個階段、各階段業務特徵、挑戰以及應對之道。異步
外賣業務發展早期,第一目標是要可以快速驗證業務的可行性。技術上,咱們須要保證架構足夠靈活、快速迭代從而知足業務快速試錯的需求。
在這個階段,咱們將訂單相關功能組織成模塊,與其它模塊(門店模塊等)一塊兒造成公用jar包,而後各個系統經過引入jar包來使用訂單功能。
早期系統的總體架構圖以下所示:
早期,外賣總體架構簡單、靈活,公共業務邏輯經過jar包實現後集成到各端應用,應用開發部署相對簡單。比較適合業務早期邏輯簡單、業務量較小、須要快速迭代的狀況。可是,隨着業務邏輯的複雜、業務量的增加,單應用架構的弊端逐步暴露出來。系統複雜後,你們共用一個大項目進行開發部署,協調的成本變高;業務之間相互影響的問題也逐漸增多。
早期業務處於不斷試錯、快速變化、快速迭代階段,經過上述架構,咱們能緊跟業務,快速知足業務需求。隨着業務的發展以及業務的逐步成熟,咱們對系統進行逐步升級,從而更好地支持業務。
2014年4月,外賣訂單量達到了10萬單/日,並且訂單量還在持續增加。這時候,業務大框架基本成型,業務在大框架基礎上快速迭代。你們共用一個大項目進行開發部署,相互影響,協調成本變高;多個業務部署於同一VM,相互影響的狀況也在增多。
爲解決開發、部署、運行時相互影響的問題。咱們將訂單系統進行獨立拆分,從而獨立開發、部署、運行,避免受其它業務影響。
系統拆分主要有以下幾個原則:
- 相關業務拆分獨立系統;
- 優先級一致的業務拆分獨立系統;
- 拆分系統包括業務服務和數據。
基於以上原則,咱們將訂單系統進行獨立拆分,全部訂單服務經過RPC接口提供給外部使用。訂單系統內部,咱們將功能按優先級拆分爲不一樣子系統,避免相互影響。訂單系統經過MQ(隊列)消息,通知外部訂單狀態變動。
獨立拆分後的訂單系統架構以下所示:
其中,最底層是數據存儲層,訂單相關數據獨立存儲。訂單服務層,咱們按照優先級將訂單服務劃分爲三個系統,分別爲交易系統、查詢系統、異步處理系統。
獨立拆分後,能夠避免業務間的相互影響。快速支持業務迭代需求的同時,保障系統穩定性。
訂單系統通過上述獨立拆分後,有效地避免了業務間的相互干擾,保障迭代速度的同時,保證了系統穩定性。這時,咱們的訂單量突破百萬,並且還在持續增加。以前的一些小問題,在訂單量增長後,被放大,進而影響用戶體驗。好比,用戶支付成功後,極端狀況下(好比網絡、數據庫問題)會致使支付成功消息處理失敗,用戶支付成功後依然顯示未支付。訂單量變大後,問題訂單相應增多。咱們須要提升系統的可靠性,保證訂單功能穩定可用。
另外,隨着訂單量的增加、訂單業務的複雜,對訂單系統的性能、穩定性、可用性等提出了更高的要求。
爲了提供更加穩定、可靠的訂單服務,咱們對拆分後的訂單系統進行進一步升級。下面將分別介紹升級涉及的主要內容。
系統獨立拆分後,能夠方便地對訂單系統進行優化升級。咱們對獨立拆分後的訂單系統進行了不少的性能優化工做,提高服務總體性能,優化工做主要涉及以下幾個方面。
服務所須要處理的工做越少,其性能天然越高。能夠經過將部分操做異步化來減小須要同步進行的操做,進而提高服務的性能。異步化有兩種方案。
異步化帶來一個隱患,如何保障異步操做的執行。這個場景主要發生在應用重啓時,對於經過線程或線程池進行的異步化,JVM重啓時,後臺執行的異步操做可能還沒有完成。這時,須要經過JVM優雅關閉來保證異步操做進行完成後,JVM再關閉。經過消息來進行的,消息自己已提供持久化,不受應用重啓影響。
具體到訂單系統,咱們經過將部分沒必要同步進行的操做異步化,來提高對外服務接口的性能。不須要當即生效的操做便可以異步進行,好比發放紅包、PUSH推送、統計等。
以訂單配送PUSH推送爲例,將PUSH推送異步化後的處理流程變動以下所示:
PUSH異步化後,線程#1在更新訂單狀態、發送消息後當即返回,而不用同步等待PUSH推送完成。而PUSH推送異步在線程#2中完成。
操做並行化也是提高性能的一大利器,並行化將本來串行的工做並行執行,下降總體處理時間。咱們對全部訂單服務進行分析,將其中非相互依賴的操做並行化,從而提高總體的響應時間。
以用戶下單爲例,第一步是從各個依賴服務獲取信息,包括門店、菜品、用戶信息等。獲取這些信息並不須要相互依賴,故能夠將其並行化,並行後的處理流程變動以下所示:
經過將獲取信息並行化,可有效縮短下單時間,提高下單接口性能。
經過將統計信息進行提早計算後緩存,避免獲取數據時進行實時計算,從而提高獲取統計數據的服務性能。好比對於首單、用戶已減免配送費等,經過提早計算後緩存,能夠簡化實時獲取數據邏輯,節約時間。
以用戶已減免配送費爲例,若是須要實時計算,則須要取到用戶全部訂單後,再進行計算,這樣實時計算成本較高。咱們經過提早計算,緩存用戶已減免配送費。須要取用戶已減免配送費時,從緩存中取便可,沒必要實時計算。具體來講,包括以下幾點:
訂單系統涉及交易,須要保證數據的一致性。不然,一旦出現問題,可能會致使訂單不能及時配送、交易金額不對等。
交易一個很重要的特徵是其操做具備事務性,訂單系統是一個複雜的分佈式系統,好比支付涉及訂單系統、支付平臺、支付寶/網銀等第三方。僅經過傳統的數據庫事務來保障不太可行。對於訂單交易系統的事務性,並不要求嚴格知足傳統數據庫事務的ACID性質,只須要最終結果一致便可。針對訂單系統的特徵,咱們經過以下種方式來保障最終結果的一致性。
經過延時重試,保證操做最終會最執行。好比退款操做,如退款時遇到網絡或支付平臺故障等問題,會延時進行重試,保證退款最終會被完成。重試又會帶來另外一個問題,即部分操做重複進行,須要對操做進行冪等處理,保證重試的正確性。
以退款操做爲例,加入重試/冪等後的處理流程以下所示:
退款操做首先會檢查是否已經退款,若是已經退款,直接返回。不然,向支付平臺發起退款,從而保證操做冪等,避免重複操做帶來問題。若是發起退款失敗(好比網絡或支付平臺故障),會將任務放入延時隊列,稍後重試。不然,直接返回。
經過重試+冪等,能夠保證退款操做最終必定會完成。
2PC是指分佈式事務的兩階段提交,經過2PC來保證多個系統的數據一致性。好比下單過程當中,涉及庫存、優惠資格等多個資源,下單時會首先預佔資源(對應2PC的第一階段),下單失敗後會釋放資源(對應2PC的回滾階段),成功後會使用資源(對應2PC的提交階段)。對於2PC,網上有大量的說明,這裏再也不繼續展開。
分佈式系統的可用性由其各個組件的可用性共同決定,要提高分佈式系統的可用性,須要綜合提高組成分佈式系統的各個組件的可用性。
針對訂單系統而言,其主要組成組件包括三類:存儲層、中間件層、服務層。下面將分層說明訂單系統的可用性。
存儲層的組件如MySQL、ES等自己已經實現了高可用,好比MySQL經過主從集羣、ES經過分片複製來實現高可用。存儲層的高可用依賴各個存儲組件便可。
分佈式系統會大量用到各種中間件,好比服務調用框架等,這類中間件通常使用開源產品或由公司基礎平臺提供,自己已具有高可用。
在分佈式系統中,服務間經過相互調用來完成業務功能,一旦某個服務出現問題,會級聯影響調用方服務,進而致使系統崩潰。分佈式系統中的依賴容災是影響服務高可用的一個重要方面。
依賴容災主要有以下幾個思路:
訂單系統會依賴多個其它服務,也存在這個問題。當前訂單系統經過同時採用上述四種方法,來避免底層服務出現問題時,影響總體服務。具體實現上,咱們採用Hystrix框架來完成依賴容災功能。Hystrix框架採用上述四種方法,有效實現依賴容災。訂單系統依賴容災示意圖以下所示
經過爲每一個依賴服務設置獨立的線程池、合理的超時時間及出錯時回退方法,有效避免服務出現問題時,級聯影響,致使總體服務不可用,從而實現服務高可用。
另外,訂單系統服務層都是無狀態服務,經過集羣+多機房部署,能夠避免單點問題及機房故障,實現高可用。
上面都是經過架構、技術實現層面來保障訂單系統的性能、穩定性、可用性。實際中,有不少的事故是人爲緣由致使的,除了好的架構、技術實現外,經過規範、制度來規避人爲事故也是保障性能、穩定性、可用性的重要方面。訂單系統經過完善需求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三個維度查詢時,數據均在一個分片,保證較高的查詢效率。
訂單表分表後,訂單表的存儲架構以下所示:
能夠看到,分表後,每一個維度共有100張表,分別放在4個庫上面。對於同一個訂單,冗餘存儲了三份。將來,隨着業務發展,還能夠繼續經過將表分到不一樣機器上來持續得到容量的提高。
分庫分表後,訂單數據存儲到多個庫多個表中,爲應用層查詢帶來必定麻煩,解決分庫分表後的查詢主要有三種方案:
因爲MySQL服務器端不能支持,咱們只剩下中間件和應用層兩個方案。中間件方案對應用透明,可是開發難度相對較大,當時這塊沒有資源去支持。因而,咱們採用應用層方案來快速支持。結合應用開發框架(SPRING+MYBATIS),咱們實現了一個輕量級的分庫分表訪問插件,避免將分庫分表邏輯嵌入到業務代碼。分庫分表插件的實現包括以下幾個要點。
經過分庫分表,解決了寫容量擴展問題。可是分表後,會給查詢帶來必定的限制,只能支持主要維度的查詢,其它維度的查詢效率存在問題。
訂單表分表以後,對於ID、用戶ID、門店ID外的查詢(好比按照手機號前綴查詢)存在效率問題。這部分一般是複雜查詢,能夠經過全文搜索來支持。在訂單系統中,咱們經過ES來解決分表後非分表維度的複雜查詢效率問題。具體來講,使用ES,主要涉及以下幾點。
經過對存儲層的可擴展性改造,使得訂單系統存儲層具備較好的可擴展性。對於中間層的可擴展性與上面提到的中間層可用性同樣,中間層自己已提供解決方案,直接複用便可。對於服務層,訂單系統服務層提供的都是無狀態服務,對於無狀態服務,經過增長機器,便可得到更高的容量,完成擴容。
經過對訂單系統各層可擴展性改造,使得訂單系統具有了較好的可擴展性,可以支持業務的持續發展,當前,訂單系統已具體千萬單/日的容量。
上面幾部分都是在介紹如何經過架構、技術實現等手段來搭建一個可靠、完善的訂單系統。可是,要保障系統的持續健康運行,光搭建系統還不夠,運維也是很重要的一環。
早期,對系統及業務的運維主要是採用人肉的方式,即外部反饋問題,RD經過排查日誌等來定位問題。隨着系統的複雜、業務的增加,問題排查難度不斷加大,同時反饋問題的數量也在逐步增多。經過人肉方式效率偏低,並不能很好的知足業務的需求。
爲提高運維效率、下降人力成本,咱們對系統及業務運維進行自動化、智能化改進,改進包括事前、事中、過後措施。
事前措施的目的是爲提早發現隱患,提早解決,避免問題惡化。
在事前措施這塊,咱們主要採起以下幾個手段:
事中措施的目的是爲及時發現問題、快速解決問題。
事中這塊,咱們採起的手段包括:
過後措施是指問題發生後,分析問題緣由,完全解決。並將相關經驗教訓反哺給事前、事中措施,不斷增強事先、事中措施,爭取儘可能提早發現問題,將問題扼殺在萌芽階段。
經過將以前人肉進行的運維操做自動化、智能化,提高了處理效率、減小了運維的人力投入。