餓了麼交易系統 5 年演化史

阿里巴巴中介軟體發表於2022-12-07


餓了麼交易系統 5 年演化史

Photo @ Bluehouse Skis 

文  |  挽晴


個人簡介:
2014年12月加入餓了麼,當時參與後臺系統的研發(Walis+Javis=>Walle),主要面向客服和BD。
2015年5月開始接觸訂單系統的研發,7月負責訂單研發組;度過單體應用到服務化這個階段。
2016年初搭建訂單的測試團隊,訂單拆分為正逆向後,主要負責正向和交付部分。
2017年做了一些平臺搭建的探索。
2018年初負責整個訂單正逆向和交付,年中將下單、購物車部分一起歸併,年底和商戶訂單部分整合,形成交易中臺。
2019年10月從交易中臺轉出,近期做了一小段時間的組織效能和架構。


我為什麼會寫這篇文章,究其緣由:
 
一是自己在交易域做了 4 年,有很多隻有我才知道,才能串起來的故事,想把這些記錄並保留下來。

二是發現後邊的很多同學看交易體系時,一接觸就是分散式、SOA、每日百萬、千萬資料量,只知道它是這個樣子,很難理解背後的思考和緣由。伴隨自己這幾年的經驗,想讓大家能夠更容易的理解這個演化過程的原因和歷程,有甘有苦。

三是很多總結也好,方法論也好,更多是去除了“糟粕”呈現在大家面前,這裡可能會稍微加一點“毒雞湯”,現實不一定那麼美好,我們有很多抉擇,現在回過頭來看,也許是慶幸,也許是錯誤。
 
這篇文章希望透過一些發展的故事和思考來給讀者呈現整個歷程,大家可以看到非常多野蠻生長的痕跡,並會附帶一些思考和總結,但不會像快餐式的總結很多大道理。

那我們就從2012年的太古時期講起。
 
太古



在談訂單之前,我們往前再考古考古,在太古時代,有一套使用 Python 寫的系統,叫做 Zeus 的系統,這個 Zeus 包含了當時餓了麼最核心的幾大模組,比如訂單、使用者、餐廳,這些統統在一個程式碼庫中,並且部署在同一臺機器, Zeus 之外還有兩大核心,即餓了麼 PC ,也就是很多老人常提的「主站」,以及面向商戶的 NaposPC 。這些系統透過 Thrif 協議通訊。除開這條鏈路之外,所有雜亂的內部功能,全在一個叫 walle 的系統中,這個 Walle 系統是採用 PHP 寫的。

 
那麼當時的 Zeus ,大概長這個樣子:
                                餓了麼交易系統 5 年演化史
 據不嚴格考究,從 Git 的提交歷史看,訂單部分的第一個 commit 是餘立鑫同學於 2012 年 9 月 1 日提交的,內容是" add eos service for zeus. currently only defind a simple get api. ",這個 EOS 指的就是訂單系統,即 ElemeOrderService 的簡稱,這個名詞沿用到了今天,成為交易正向的訂單部分,甚至一段時間是訂單組的代名詞。
 
 Zeus 在後來其實經過了一定的重構,叫做 Zeus2 ,但具體時間已不可考。
 

萌芽



2014 年 10 月我到餓了麼來面試,面試官是商戶端負責人磊哥。 12 月 1 日,我入職餓了麼, HR 領著帶著一臉萌新的我,到磊哥面前時,磊哥把我帶到 JN 面前說,“這就是那個實習生”,然後扭頭就跑了。後來得知,當時面試結束後,磊哥和 JN 同學說,剛剛面了一個實習生,湊合能用,正巧商戶組有計劃轉型 Java ,而佳寧還很缺 python 的人,然後就騙了 JN 一頓飯把我賣了。

 
回到正題,在 2014 年 12 月~ 2014 年 4 月這幾個月的時間裡,我配合完成了一個更老的 BD 系統後端遷移到 Walis ,並且在我的導師轉崗到 CI 團隊後,自己完成了 Walis 從單應用遷移到分散式應用。
 

訂單組的成立

 
對我來說,完全是運氣和緣分...
  
接近 2015 年 5 月的時候,我的主管,JN同學,有一天突然找到我,看起來很興奮,告訴我,公司打算成立一個訂單組,這個訂單組由他來負責,除了他之外,他唯獨選中了我(大概是因為上段我提到的一些經歷,在可選的人裡,還湊合~),說是我怎麼怎麼讓他相中,這個男人忽悠起人來,一套一套的。
 
作為一個技術人員,內心非常沸騰。一是高併發、高流量、分散式這些耳熟能詳的高大上名詞之前只是聽說過,不曾想這麼快就能夠接觸到這樣的系統;二是我們此前做的系統很“邊緣”,有多邊緣呢,白天幾乎沒什麼請求, BD 走訪商戶回來,恰巧晚上才是高峰期,即使是晚上,關鍵的單介面也就偶爾幾個、十幾個請求,是當時那種掛 2 個小時才可能有人發現,掛半天不一定有人叫的系統,那時候我們幸福的晚上 7 點前就下班了,第一次釋出的時候非常鄭重的和我說,可能要加班到晚上 8 點半。
 
之所以選擇 JN 做訂單組負責人,因為他雖然是個前端工程師起家,做的是“邊緣”後臺系統,但卻是對整個公司所有系統和業務都比較熟悉的人,很適合發展訂單系統。
 
嗯,沒錯,這個組在成立前一天,一直只有我們兩個人。當時的我還沒畢業,除了興奮,更多的是忐忑。
 
2015 年 5 月 12 日,訂單組正式成立,成立當天,拉來了隔壁組的 ZH (是個PHPer,招進來的時候是計劃去接Walle),然後聊到一半的時候,當時的部門總監跑過來,說正巧有個小哥哥當天入職,還不錯,正好給訂單組吧,是個 Java 工程師。於是乎,成立當天,我們人數翻了一倍,變成了 4 個人。
 
我們給自己的第一個任務: 讀程式碼,理業務,畫圖。和 CTO 申請到了 1 個月的時間來緩衝,這段時間不接任何業務需求!

分別請來了訂單的前主程、Python 框架負責人、Zeus 系應用運維負責人給我們講解。實際上,每個人的分享也就 1 個多小時。那一個月真是從幾萬行 Python 程式碼,沒有任何產品文件,極其稀少的註釋,一行行的啃,每個人解讀一部分。我最後彙總把整個訂單的生命週期、關鍵操作、關鍵業務邏輯,畫在了一張大圖裡,這張圖,我們後來用了一年多。
 
其實,當時年中旬的餓了麼,產研規模已經達到幾百人左右,新 CTO ,雪峰老師是年初加入餓了麼,整個基礎設施的起步是 2015 年下半年,整個體系的飛速搭建是在 2016 年。
       
可以說是正處於相當混亂,又高速發展的時期。我們稱那個時間是一邊開著跑車一邊換輪胎。
 

Zeus 解耦

 
和訂單真正密切相關的第一個 Super 任務,大概是從 6 月左右開始 --- Zeus 解耦,HC老師是 Python 框架的負責人,也是個人最佩服和敬仰的技術專家之一,在美國舉行 Qcon 上,作為首席架構師介紹過當時餓了麼整體技術架構。剛才在太古時期已經說到, Zeus 是一個巨型單體應用,為了今後各個部分能夠快速發展,降低耦合和牽連影響等,公司啟動了 zeus 解耦專案,總之就兩個字,拆分
      
經過 1 個多月的密集會議,完成了拆分的方案。說的似乎沒那麼難,但是這場口水戰當時打的不可開交,拆分後不同的服務歸屬於誰?模組和模組之間並沒有切分的那麼幹淨,A和B服務中的邊界怎麼定等等一系列問題。當時的我還不夠格參與討論。
 
結論是, Zeus 將要拆分成下邊的幾個主服務:
 

  • zeus.eos => 訂單服務
  • zeus.eus => 使用者服務
  • zeus.ers => 商家服務
  • zeus.eps => 營銷服務(新產物)
  • zeus.sms => 簡訊服務
  • ...

 
第一階段
每個被拆分後的服務,隨之進行的是新的一波重構和拆分。例如從 zeus.eos 分離出來 biz.booking ,拿走了下單和購物車部分能力;分離出來 biz.ugc 拿走了訂單評價相關能力。
       餓了麼交易系統 5 年演化史

拆分主要經歷的幾個階段:
1、(7月份)共享程式碼倉庫,按模組獨立執行。即,把 Zeus 所有程式碼都打包到伺服器後,按照劃分,在特定機器上只將特定模組單獨啟動,開放特定埠。
2、(8月份) Proxy 階段。即在原服務中,要遷出去的介面上增加一個代理,可以代理到新服務的介面,由服務註冊中心開關能力來控制切換流量大小。
3、(8月份至9月初)指令碼、模組的完全切分改造。
4、(9月份)程式碼倉庫獨立。使用了 Git 的核彈武器 filter-branch ,將模組中的程式碼和變更歷史,完全完整的從原始碼庫中分離。而此時部署卻仍然為混布,在釋出工具中,某個獨立應用釋出後實際是替換了 Zeus 這個大專案下的某個目錄。
5、(9月份)配置獨立。原來的配置由 saltstack 刷到伺服器上,被伺服器上多個應用所共用,我們將其直接改成使用服務註冊中心的配置下發能力獲取單個應用配置。在這個階段也基本上過渡到了軟負載。
6、(次年3月份)物理部署獨立。當然這是解耦二期的內容了。
 
當然,這次拆分,還帶來了另外一個產物, Python 的 SOA 框架 zeus_core,zeus_core 要大概在 4 月份左右先於業務服務被拆分出來。
 
整個解耦一期,持續了大概半年時間。在期間,沒有發生因為拆分導致的事故,也幾乎沒有什麼冒煙。想想當時沒有用什麼高深的東西,工具落後,沒有專職測試,完全靠著一幫早期工程師和運維同學的技術素養。
 
分庫分表
 
仍然是在 2015 年,大概是 9、10 月左右確定分庫分表要開始實施,而分庫分表的方案,在我介入時已經幾乎敲定,並由 CI 部門的 DAL 團隊主導。
 
為什麼要做分庫分表?
 
一是扛不住併發。當時我們的訂單庫的 MySQL 是採取 1 主 5 從的架構,還有 1 臺做 MHA 。DB 不太能承受住當時的併發壓力,並且,對風險的抵抗能力非常的弱。業務如果做一些活動沒提前告知,我們的從庫一旦掛了一個,就只能來回切,嚴重的時候只能大量限流。而且,那段時間,作為技術,我們也在祈禱美團外賣別在高峰期掛,美團外賣一旦掛了,流量就會有一部分流到餓了麼,我們就開始也緊張起來了。同樣的,那段時間,我們整站掛了,美團外賣也不太能扛得住,大家都在經歷相似的發展階段。
 
二是 DDL 成本太高,業務又處於戰鬥高峰。當時餓了麼的單量在日均百萬出頭。有一些業務需求,希望在訂單上新增欄位,然而,我們找到 DBA 評估的時候,給的答案是,樂觀估計需要停服 3 小時,悲觀估計要 5 小時,並且需要 CEO 審批。顯然,這個風險,技術團隊難以接受,而業務團隊也無法接受。那麼投機取巧的方案,就是在預留的 Json 擴充套件欄位中不斷的塞,這種方式一定程度上緩解了很長一段時間的壓力,然而,也埋下了非常多的隱患。
 
當然,還有一些特殊的業務場景以及一些開放出去顆粒度很大的介面,會產生一些效能極差的 SQL ,都會引爆全站。
 
Shardin 後物理結構如下:
 
餓了麼交易系統 5 年演化史
一次更新操作邏輯如下:

                       餓了麼交易系統 5 年演化史
   
我們其實是做了兩維 Sharding ,兩個維度都是 120 個分片,但是可以透過三種方式路由(使用者 ID、商戶ID、訂單ID),寫入優先保證使用者維度成功。由於資源的原因,使用者和商戶分片是交錯混合部署的。

 (加粗部分其實是有一些坑的,這個特殊定製也是餓了麼唯一,如果有興趣以後可以展開)
 
更具體分庫分表的技術細節不在這裡展開,大致經歷了幾個階段:
 
1、制定新的訂單號生成規則,並完成改造接入。
2、資料雙寫,讀舊,對比資料。
3、對不相容的 SQL 進行改造,比如跨分片的排序、統計,不帶shardingkey的SQL等等。
4、資料雙寫,讀新。(與3有部分同步進行)
5、完成資料庫切換,資料寫新讀新。
 
這段日子,作為業務團隊,大部分時間其實花在第三部分,也曾奮鬥過好幾次到凌晨3、4點。
 
在 2016 年的春節前夕,為了頂過業務峰值和系統穩定,我們甚至把 DB 裡的資料做歸檔只留最近 15 天內的訂單
 
記得最終切換的那一天,大概在 2016 年 3 月中旬,我和幾位同學早上 5 點多就到了公司,天矇矇亮。整個餓了麼開始停服,然後阻斷寫請求,完成 DB 指向的配置,核對無誤,恢復寫請求,核驗業務無誤,慢慢放開前端流量,重新開服。整個過程核心部分大概 10 分鐘,整個停服到完全開放持續了半個小時。
 
到了第二天,我們才得以匯入最近 3 個月的歷史訂單。
 
這次變更做完,我們基本擺脫了 DB 的瓶頸和痛點(當然,後邊的故事告訴我們,有時候還是有點天真的~~~)
 
訊息廣播
 
那個時期,也是在 15 年的 7 月左右,受到一些架構文章的影響,也是因為 JN 提到了這一點,我們決定做訂單的訊息廣播,主要目的是為了進一步解耦。
 
在調研了 RabbitMQ、NSQ、RocketMQ、Kafka、ActiveMQ 之後,我得出的最終結論,選型還是 RabbitMQ ,其實當時我認為,RocketMQ 更為適合,特別是順序訊息的特性,在交易某些業務場景下能夠提供天然的支援,然而,運維團隊主要的運維經驗是在 RabbitMQ 。框架團隊和運維團隊的同學很自信,自從搭建以來,也沒有出過任何問題,穩的一匹,如果選擇 RabbitMQ ,就能夠得到運維團隊的天然支援,這對於我們當時的業務團隊來說,能夠避免很多風險。
 
於是由框架團隊承接了對 RabbitMQ 進行一輪嚴謹的效能測試,給出部分效能指標。這一場測試,最終搭建了一個 3Broker 組成的叢集,單獨為訂單服務,在此之前只有一個 MQ 節點,服務於 Zeus 體系的非同步訊息任務。
 
為了保證對交易主流程不產生影響,然後在 Client 端 SOA 框架進行了一系列的容錯改造,主要是針對連線 MQ 叢集時的傳送超時、斷開等容錯,訊息傳送非同步進行且重試一定次數。最終全新搭建了由 3 個節點組成的 MQ 叢集,訂單的訊息最終發往這個叢集。
 
期間,其實踩了一個小坑。雖然框架團隊已經進行了異常情況的容錯。但畢竟訊息廣播的傳送時機是和主流程狀態扭轉緊密相連的,程式碼在上線前,當時一向謹慎的我,為首次上線加上了一個訊息傳送的開關。那是一個晚上,大概 8 點多,現在回想,當時灰度和觀察時間是有一些短的,當我全部發布完成後,很快,監控上顯著看到介面開始嚴重超時(我們當時採用框架預設的超時設定, 30s,其實這個配置很嚴重),進而產生了大量介面嚴重超時,很明顯,有什麼拖慢了介面。交易曲線斷崖式的下降,我立馬就被NOC 進行了 on call ,迅速將訊息傳送的開關關閉,恢復也是一瞬間的事情,然後,人肉跑到架構團隊前邊跪求協助排查原因(終歸還是當時的自己太菜)。
 
當晚,我們開、關、開、關、開、關...流量從 5% 、10% 、30% 等等,不同嘗試、驗證之後,最後得出的結論,是和當時的 HAProxy 配置有關,由於 HAProxy 提前關閉了和 RabbitMQ 叢集的連線,服務的 Client 仍然拿著壞死的連線去請求,進而造成了這次問題,並且, Client 確實沒對這種超時進行容錯。在調整了 HAProxy 的連結超時配置之後,症狀就消除了。雖然,從日誌上看遺留有一些隱患。
 
此時,是長這樣的,每個接入的業務方需要申請一個 Topic , Topic 之下掛多少 Queue 可以根據業務需求自己確定。
 
餓了麼交易系統 5 年演化史
 
 這個物理架構部署穩定執行了不到1年時間就存在不少問題,下章會再展開。
 
在使用上,當時定下了這麼幾條原則:
1、訂單不對外直接暴露自身狀態,而是以事件的方式對外暴露。因為狀態是一個描述,而事件則代表了一個動作,同時可以將訂單狀態細節和接入方解耦。
2、訊息廣播僅用於廣播事件,而不用於資料同步,如消費者需要更多的資料則反查訂單資料介面,時間戳包含事件產生時間和傳送時間(時間是後來加上的)。即訊息體包括 header 資訊,僅放入用於解釋這個事件的內容,還包括交易雙方主鍵和一些能夠用於做通用過濾或二次路由的資訊。
3、費者在消費訊息時應當保證自身的冪等性,同時應當讓自己在消費時無狀態。如果一定要順序消費,那麼自行透過Redis等方案實現。
4、消費者接入時, Topic 和 Queue 需要按照一定命名規範,同時, Queue 的最大積壓深度為 10k ,超過則捨棄。消費者要明確自身是否接受訊息可損,同時要保證自身的消費效能。按照當時評估,訊息堆積到達百萬時會使得整個叢集效能下降 10% 。(在全域性架構的建議下,我們還提供了以 Redis 為介質,作為映象儲存了訂單事件,不過體驗並不夠優雅)
 
而這套訊息廣播的邏輯架構,一直持續使用到今天,在解耦上產生了巨大的紅利。
 

初探


15 年中旬到 16 年初,我們處在每天的單量在百萬以上並逐步快速增長這麼一個階段。


OSC
 
在那個時期,也看了很多架構文章,ESB、SOA、微服務、CQRS、EventSource 等等,我們也在積極探討訂單系統如何重構,以支撐更高的併發。當時聽的最多的,是京東的 OFC ,還特地買了《京東技術解密》在研讀,不過很快得出結論,幾乎無太大參考價值。主要原因是京東的 OFC ,很明顯是由零售業務的特性決定的,很多 OFC 裡的概念,作為入行尚淺的我們,套到餐飲 O2O ,幾乎難以理解。但我們還是深受其影響,給小組取了一個相似的縮寫,OSC,Order Service Center 。
 
由於手頭上這套訂單已經服役了 3 年多,公司的主要語言棧從人數上也由 Python 傾向到 Java ,沒多久,我們打算重寫這套訂單體系。於是,我設計了一套架構體系,以 osc 為應用的域字首。這套體系的核心理念: 訂單是為了保持交易時刻的快照,儘可能的保持自己的簡潔,減少對各方的依賴,減輕作為資料通道的作用。
 
我們選取的語言棧選型是 Java ,也就是計劃開始轉型 Java 。(很不巧,我們真正轉型到 Java 最後發生在 2019 年).

此時,正值 9 月。很巧的是,公司開始第一次開始設立新服務的架構評審制度,我這個方案,大概就是參與評審的 Top1、2 小白鼠,新鮮的大錘正等著敲人。
 
其實,在那之後的1年回過頭來看,還挺感謝這次架構評審,不是因為透過了,而是因為被拒絕了。
 
說來也好笑,那一次,依稀記得參與架構評審的評委成員, DA 負責人、基礎 OPS 負責人、入職沒多久的一個架構師。
       
架構師當時的提問關注點在這套架構是能夠用1年還是3年,而基礎OPS負責人的提問,特別有意思,他問了第一個問題,這套系統是關鍵路徑嗎?我心想,這不是廢話嗎,我直接回答,最中間那部分是的。

然後第二個問題,出了問題,這個應用可以降級嗎?我一想,這不也是廢話嗎,這個鏈路當然沒法降級,這是最核心最基礎的鏈路,公司的核心業務就是圍繞交易。(可能是雙方的理解不在一個頻道上)。

於是,他給的結論是,關鍵路徑,又是核心的訂單,沒法降級,一旦出了問題,大家都沒飯吃。於是評審結束,結論是不透過。
 

組建測試團隊

 
交易團隊一直沒有專職的測試,也就是說,所有的內容,都是由研發自測來保證的。而公司當時的自動化測試非常的弱,幾乎所有的測試都是依靠手工進行。但是,我此時覺得非常有必要拿到測試資源。我強烈的要求成立一個測試小組來給訂單上線質量加上一層防護。
 
當時還發生了一些有趣的事情,據 JN 去了解,框架團隊是沒有測試的,然而他們似乎沒出什麼問題,當時他們很自豪的解釋,技術憑什麼不應該自己保障程式碼的質量。簡直理直氣壯,無懈可擊。我覺得這個觀點有一些理想,研發自己可能沒那麼容易發現自己的錯誤,引入另外一批人從另外一個角度切入,能夠進一步提升質量的保障,畢竟這個系統是如此的重要和高風險,但是我們也並不應該建立一個只能提供“點點點”的測試團隊。
 
最後,在和 JN 長時間的溝通後,我們確定了當時測試小組的定位和職責: 保證程式碼質量是研發自己應盡的責任,測試開發在此基礎上,主要提供工具支援,讓測試成本降低,同時在精力允許的情況,提供一定程度的測試保障。
 
於是,在 2016 年 2、3 月左右,交易團隊來了第一位測試,差不多在 4 月的時候,測試 HC 達到了 4 人,整個測試小組由我來負責。
 

第一件事情,搭建自動化整合測試


技術棧上的選擇,採用了 RobotFramework ,主要原因是整個團隊當時仍然以 Python 為主要語言,測試開發同學實際上 Python 和 Java 也都能寫;另外一點是  RobotFramwork 的關鍵字驅動,有一套自己的規範,和系統相關的lib可以被提煉出來,即使做語言棧轉型時,成本也不會很高。
 
除了測試的流程規範和標準外,開始想搭建一個平臺,用於管理測試用例、執行情況和執行報告。
 
這套體系我命名為 WeBot :

  • 採用 RobotFramwork 來作為測試用例執行的基礎
  • Jenkins 來實際調配在何處執行,並且滿足執行計劃的管理
  • 基於 Django 搭建了一個簡單的管理介面,用來管理用例和測試報告,並使得每一個測試用例可以被作為一個單元隨意組裝,如果對 Java 很熟悉的同學,這裡做一個近似的類比,這裡每一個用例都可以當成一個 SPI 。
  • 另外引入了 Docker 來部署 slave 的環境,用的很淺,雖然當時餓了麼在生產還沒使用 Docker (餓了麼生產上的容器化應該在 17 年左右)。

 
想想自己當時在測試環境玩的還是蠻歡樂的,很喜歡折騰。
 
大致的思路如:
 
餓了麼交易系統 5 年演化史

測試單元: Bussiness Library 其實是對 SOA 服務介面到 RobotFramwork 中的一層封裝,每一個測試單元可以呼叫一個或多個介面完成一次原子的業務活動。

校驗元件: 提供了對返回值,或者額外配置對Redis、資料庫資料的校驗。

整合測試: 多個測試單元序列編排起來就完成了一個整合測試用例。其中每個測試單元執行後,請求的入參和出餐,在整合測試用例的執行域內任何地方都是可以獲取到的。

迴歸測試: 選取多個整合測試,可以當成一個方案,配置執行。
 
這樣就實現了多層級不同粒度的複用。根據整合測試和迴歸測試的方案搭配,後臺會編譯生成對應的  Robot 檔案。
 
這個專案,最後其實失敗了。最主要的原因,測試開發的同學在開發上能力還不足,而介面上需要比較多的前端開發工作,一開始我直接套用了 Django 的擴充套件管理介面 xadmin ,進行了簡單的擴充套件,然而當時的精力,不允許自己花太多精力在上邊,內建的前端元件在體驗上有一些硬傷,反而導致效率不高。直到 5 月份,基本放棄了二次開發。
 
但這次嘗試也帶來了另外的一些成果。我們相當於捨棄了使用系統管理用例,而 Jenkins + RobotFramwork 的組合被保留了下來。我們把寫好的一些整合測試用例託管在 Git 上,研發會把自己開發好的分支部署在指定環境,每天凌晨拉取執行,研發會在早上根據自動化測試報告來看最近一次要釋出的內容是否有問題。同時,也允許研發手動執行,文武和曉東兩位同學在這塊貢獻了非常多的精力。
 
這個自動化整合迴歸的建立,為後續幾次訂單系統的拆分和小範圍重構提供了重要的保障。讓研發膽子更大,步子能夠邁得更長了。研發自己會非常積極的使用這套工具,嚐到了很多顯而易見的甜頭。
 

第二件事情,搭建效能測試。


背景:

記得在 15 年剛剛接觸訂單的時候,有幸拜訪了還沒來餓了麼,但後來成為餓了麼全域性架構負責人的 XL 老師,談及如何做好訂單系統,重點提及的一點,也是壓測。

當時有一些問題和效能、容量有一些關係,我們沒有什麼提前預知的能力。比如,在我們完成 sharding 前有一次商戶端上線了一次訂單列表改版,因為使用了現有的一個通用介面(這個介面粒度很粗,條件組合自由度很強),我們都沒能預先評估,這個查詢走了一個效能極差的索引。當時午高峰接近,一個幾 k QPS 的查詢介面,從庫突然( 15 年我們的監控告警體系還沒有那麼完備)就被打垮了,從庫切一個掛一個,不得不採取介面無差別限流 50% 才緩過來,整個持續了接近半個小時。最後追溯到近期變更,商戶端回滾了這次變更才真的恢復。而事後排查,造成此次事故的慢 SQL, QPS 大概幾百左右。
 
整個公司的效能測試組建,早於我這邊的規劃,但是當時公司的效能測試是為了 517 外賣節服務的,有一波專門的測試同學,這是餓了麼第一次造節,這件事的籌備和實施其實花了很長時間。

在壓測的時候需要不斷的解決問題,重複再壓測,這件事使得當時很多同學見到了近鐵城市廣場每一個小時的樣子,回憶那段時光,我記得最晚的一次,大概是 5 月 6 號,我們到樓下已經是凌晨 5 點半,我到家的時候兩旁的路燈剛剛關。
 
上邊是一點題外話,雖然全鏈路壓測一定會帶上我們,但是我們也有一些全鏈路壓不到的地方,還有一些介面或邏輯需要單獨進行,需要隨時進行。
 

搭建:

技術選型上選擇了 Locust ,因為 Python 的 SOA 框架及其元件,可以帶來極大的便利。此前在做公司級的全鏈路壓測時,是基於 JMeter 的, JMeter 並不是很容易和 Java 的 SOA 框架進行整合,需要有一個前端 HaProxy 來做流量的分流,不能直接使用軟負載,這在當時造成了一定的不便性。另外一個原因, Locust 的設計理念,可以使一些效能測試的用例更為貼近業務實際場景,只觀測 QPS 指標,有時候會有一些失真。
 
有了全鏈路效能測試團隊在前邊趟坑,其實我自己效能測試能力的搭建很快就完成了,整個搭建過程花費了 1 個多月, 8、9 月基本可以對域內服務自行組織效能測試。效能測試人員包括研發的學習,需要一點過程。很快,我們這個小組的效能測試就鋪開到整個部門內使用,包括之後和金融團隊合併之後。
 
這次搭建使得我們在對外提供介面時,對自己服務負載和效能上限有一定的預期,規避了一些有效能隱患的介面上線,特別是面向商戶端複雜查詢條件;也能夠模擬高併發場景,在我們一些重構的階段,提前發現了一些併發鎖和呼叫鏈路依賴問題。
 

第三件事情,隨機故障演練


1.0版本:

一開始的雛形其實很簡單,大致的思路是:
 
1、 在測試環境單拉出一個專門的環境,有單獨的監控和 DB 。
2、構造一個 Client ,模擬使用者行為造數。(我們自動化整合測試積累的經驗就排上用場了。
3、提供了一個工具來構建被依賴服務的 Mock Server ,解決長鏈路服務依賴問題。Mock Server 可以根據輸入返回一些設定好的輸出。
4、另外,框架團隊幫忙做了一些手腳,發了一個特殊版本,使得我們可以對流量打標。可以根據 Client 對流量的標記,來讓 Mock Server 模擬阻塞、超時等一些異常行為,反饋到我們的被測 server 上。
 
這是一個很簡單的雛形,而訂單經過我們的幾次治理,對外依賴已經很少,所以不到 2、3 天就完全成型。但僅僅是玩具而已,並不具備足夠的參考意義。因為併發沒有做的很高, Mock Server 能夠做的事情也有限。
 

2.0版本:

JN 召集了一些同學,參照 Netflix 的 Choas Monkey 為原型,造了一個輪子,我們稱之為 Kennel 。
 
控制中心設計圖如下:
餓了麼交易系統 5 年演化史
 
在專項同學和運維同學的幫助下,Kennel 在 2016 年的 10 月左右初步可用。這個工具提供了諸如: 模擬網路丟包;介面異常注入;摘除叢集中的某節點;暴力幹掉服務程式等等。
 
這東西大家之前都沒嘗試過,我們也不知道能夠測出什麼來,我在11月的時候想做第一波嘗試,我嘗試制定了 5 個需要驗收的場景:
1、超長分散式事務
2、某個介面異常引起整個服務雪崩
3、叢集中某個節點重啟或者機器重啟,呼叫方反應明顯
4、叢集某個節點CPU負載變高,負載不均
5、服務是單點的,叢集行為不一致
 
根據這幾個場景,在測試同學中挑選一個人牽頭實施。不同服務的測試報告略有差異,其中一份的部分截圖如下:

餓了麼交易系統 5 年演化史
餓了麼交易系統 5 年演化史
餓了麼交易系統 5 年演化史
 
透過對交易主要的幾個服務測試一輪之後,我們確實發現了一些隱患:
 

  • 一些情況下部署的叢集和服務註冊中心機器數量可能不一致,即服務節點被暴力幹掉後,服務註冊中心不能主動發現和踢出。這是一個比較大的隱患。
  • 每個叢集都存在負載不均的現象,個別機器可能 CPU 利用率會偏高。(和負載均衡策略有關)
  • 進行“毀滅打擊”自恢復時,某幾個節點的 CPU 利用率會顯著高於其他節點,幾個小時之後才會逐漸均勻。(和負載均衡策略有關)
  • 單節點 CPU 負載較高時,負載均衡不會將流量路由到其它節點,即使這部分請求效能遠差於其它節點,甚至出現很多超時。(和負載均衡、熔斷的實現機制有關,Python 的 SOA 是在服務端做的熔斷,而客戶端沒有)
  • 大量服務的超時設定配置有誤,框架支援配置軟超時和硬超時,軟超時只告警不阻斷,然而預設的硬超時長達 20s 之久,很多服務只配置了軟超時甚至沒有配置,這其實是一個低階錯誤埋下的嚴重隱患,可能會沒法避免一些雪崩。
  • 個別場景下超時配置失效,透過對呼叫鏈路的埋點,以及和框架團隊復現,最後鎖定是一些使用訊息佇列傳送訊息的場景,Python 框架是利用了Gevent 來實現高併發的支援,框架沒能抓住這個超時。
  • ...

 
這個專案,幾個道理顯而易見,我們做了很多設計和防範,都必須結合故障演練來進行驗收,無論是低階錯誤還是設計不足,能夠一定程度提前發現。

當然我們也造成了一些失誤,一條信心滿滿的補償鏈路(平時不work),自己攻擊的時候,它失效了,後來發現是某次變更埋下的隱患。自己親手造的鍋,含著淚也要往身上背,但我反而更覺得故障演練是更值得去做的,誰能保證真正的故障來臨時,不是一個更嚴重的事故。
 
除了系統利好外,人員也拿到了很多收益,比如測試和研發同學經過這個專案的實時,對我們的 trace 和 log 系統在使用上爐火純青,對我們 SOA 框架的運作了解也更為透徹,這裡的很多隱患和根因,就是測試同學刨根挖底找到的。高水準的 QA 同學很重要,提升 QA 同學的水平也同樣重要。
 
當然,除了測試團隊的工作外,單元測試我們也沒有落下,在 16 年長時間保持 80%~90% 的一個程式碼行覆蓋率。

伴隨體量上漲的一系列問題

 

Redis使用的改進

 

使用姿勢的治理:

 
2016 年年初主要瓶頸在資料庫,在上文其實已經提到了分庫分表的事,可以稍微喘口氣,到了 6 月,大家最擔憂的,變成了 Redis 。當時 Zabbix 只能監控到機器的執行情況, Zabbix 其實也在逐步下線中, SRE 團隊搭建了一套時效更高的機器指標收集體系,直接讀取了 Linux 的一些資料,然而,整個 Redis 執行情況仍然完全是黑盒。
 
餓了麼在 twemproxy 和 codis 上也踩了不少坑, redis-cluster 在業界還沒被大規模使用,於是自研了一套 Redis proxy: corvus ,還提供了強大指標上報,可以監控到 redis 的記憶體、連結、 hit 率、key 數量、傳輸資料量等等。正好在這個時間點推出,用以取代 twemproxy ,這使得 Redis 的治理迎來轉機。
 
我們配合進行了這次遷移,還真是不遷不知道,一遷嚇一跳。
 
當時我們使用 Reids 主要有三個用途,一是快取,類似表和介面緯度;二是分散式鎖,部分場景用來防併發寫;三是餐廳流水號的生成。程式碼已經是好幾年前的前人寫的。
 
老的使用姿勢,把表級快取和介面快取,配置在一個叢集中;其餘配置在另外一個叢集,但是在使用上,框架包裝了兩種 Client ,有不同的容錯機制(即是否強依賴或可擊穿)。
 
大家都知道外賣交易有個特點,一筆訂單在短時間內,交易階段的推進會更快,因此訂單快取的更新更頻繁,我們在短暫灰度驗證 Redis 叢集的可用性之後,就進行了全面切換(當時的具體切換方案細節記不太清了,現在回想起來其實可以有更穩妥的方案)。
 
參照原快取的叢集是 55G , OPS 準備了一個 100G 的叢集。在切換後 10min 左右,叢集記憶體就佔滿了。
 
餓了麼交易系統 5 年演化史
 
我們得出一個驚人的結論...舊叢集的 55G ,之前就一直是超的(巧了,配合我們遷移的OPS也叫超哥)。
 
從監控指標上看,keys 增長很快而ttl下降也很快,我們很快鎖定了兩個介面, query_order 和 count_order ,當時這兩個介面高峰期前者大概是 7k QPS ,後者是10k QPS ,這兩個介面之前的rt上看一點問題也沒有,平均也就 10ms 。
 
還得從我們的業務場景說起,這兩個介面的主要作用是查詢一段時間內某家餐廳的訂單,為了保證商家能夠儘快的看到新訂單,商戶端是採取了輪詢重新整理的機制,而這個問題主要出在查詢引數上。這兩個介面使用了介面級快取,所謂的介面級快取,就是把入參生成個 Hash 作為 key ,把返回值作為 value , cache 起來, ttl 為秒級,咋一看沒什麼問題。如果查詢引數的時間戳,截止時間是當天最後一秒的話,確實是的。看到這我相信很多人已經猜到,截止時間戳傳入的其實是當前時刻,這是一個滑動的時間,也就引發了 cache 接近 100% miss 的同時,高頻的塞入了新的資料。
 
 (因為新舊叢集的記憶體回收策略不一樣,新叢集在這種情況下,頻繁 GC 會引發效能指標抖動劇烈)
 
這兩個 cache ,其實沒任何用處...回滾過了一天後,經過灰度,全面去掉了這兩個介面的 cache ,我們又進行了一次切換,順帶將介面級快取和表級快取拆分到兩個叢集。

接著,我們又發現了一些有趣的事情...
 
先來看看,我們業務單量峰值的大致曲線,對外賣行業來說,一天有兩個峰值,中午和傍晚,中午要顯著高於傍晚。
 
餓了麼交易系統 5 年演化史
 
       切換後那天的下午大概 3 點多,記憶體再次爆了... ,記憶體佔用曲線近似下圖:
餓了麼交易系統 5 年演化史
緊急擴容後,我們一直觀察到了晚上,最後的曲線變成了下圖,從 hit 率上看,也有一定提升(具體資料已不可考,在 88%~95% 之間,後來達到 98% 以上)。
 
餓了麼交易系統 5 年演化史
 
為什麼和業務峰值不太一樣...
 
其實還是要結合業務來說,很簡單,商戶端當時的輪詢有多個場景,最長是查詢最近 3 天內的訂單,還有一個頁面單獨查詢當天訂單。
 
後端在輪詢時查了比前端每頁需要的更多條目,並且,並不是每個商戶當天訂單一開始就是大於一頁的,因此,隨著當天時間的推移,出現了上邊的現象。
 
為什麼以前的效能指標又沒看出什麼問題呢?一是和舊 Redis 叢集的記憶體回收策略選取有關,二是 QPS 的量很高,如果只看平均響應時間,差的指標被平均了, hit 率也被平均拉高了。
 
嗯,解決了這個問題之後,又又發現了新的問題...
 
大概1、2點這個夜深人靜的時候,被 oncall 叫起來,監控發現記憶體使用急劇飆升。
 
我們鎖定到一個呼叫量不太正常的介面上,又是 query_order。前段日子,清結算剛剛改造,就是在這種夜深人靜的時候跑賬,當時我們的賬期比較長(這個是由於訂單可退天數的問題,下文還有地方會展開),這時候會拉取大量歷史訂單,導致佔用了大量記憶體,而我們的表級快取時效是 12h ,如果不做清理,對早高峰可能會產生一定的影響。後來我們次日就提供了一個不走快取的介面,單獨給到清結算。
 
這裡核心的問題在於, 我們服務化也就不到 1 年的時間,服務的治理還不能做到很精細,服務開放出去的介面,暴露在內網中,誰都可以來呼叫,我們的介面協議也是公開的,任何人都很容易知道查閱到介面,並且,在公司的老人路子都比較野(不需要對接,有啥要啥,沒有就自己加)。Git 倉庫程式碼合併許可權和釋出許可權早在 15 年底就回收管控了,但那一刻 SOA 化還未完全,介面授權直到很後邊才支援。
 
Redis 的使用還是需要建立在深刻理解業務場景基礎上,並且關注各類指標。
 

快取機制的改進 

我們當時的快取機制是這樣的:
 
餓了麼交易系統 5 年演化史
這個架構設計的優點:
1、有一條獨立的鏈路來做快取的更新,對原有服務入侵性較小
2、元件可複用性較高
3、有 MQ 削峰,同時還有一級 Redis,做了聚合,進一步減小併發
 
在很多場景,是一套蠻優秀的架構。
 
缺點: 
1、用到了兩級佇列,鏈路較長
2、實時性較差
 
驅動我們改造的原因,也源自一次小事故。

商戶訂單列表的查詢其實根據的是訂單狀態來查,獲取到的訂單應當是支付好了的。然而有一部分錯誤的判斷邏輯,放在了當時商戶端接單後端,這個邏輯會判斷訂單上的流水號是否是0(預設值),如果是0推斷出訂單還未支付,就將訂單過濾掉。
 
在那次事故中,快取更新元件跪了(並且沒有人知道...雖然這個架構是框架的某些同學早期設計的,但太穩定了以至於都被遺忘...)。由於快取更新的不夠及時,拿到了過時的資料,表象就是,商戶看不到部分新訂單,看到的時候,已經被超時未接單自動取消的邏輯取消了,真是精彩的組合...
 
後邊改造成下邊的樣子:
 
餓了麼交易系統 5 年演化史
 
相比起來,這個架構鏈路就減少了很多,而且實時性得到了保障。但是為了不阻塞流程,進行了一定的容錯,這就必須增加一條監控補償鏈路。這次改進之後,我們立馬去除了對 ZeroMQ 在程式碼和配置上的依賴。
 

訊息使用的改進

 
分庫分表做完後,我們對 MQ 沒有什麼信心,在接下來的幾個月,MQ 接連出了幾次異常...真的是墨菲定律,遺憾的是我們只是感覺它要出事情而不知道它哪裡會出事情。
 

錯誤的姿勢

在之前的章節,我提到過曾經搭建了一套訂單訊息廣播機制,基於這套訊息為契機,商戶端針對高頻輪詢做了一個技術最佳化,希望透過長連線,推拉結合,減小輪詢的壓力。簡單介紹一下這套方案,商戶端有一個後端服務,接收訂單的訊息廣播,如果有新訂單(即剛剛扭轉到完成支付商家可見的訂單),會透過與端上的長連線推送觸達到端上,接著端上會觸發一次主動重新整理,併發出觸達聲音提醒商戶。原先的輪詢則增加時間間隔,降低頻次。
 
餓了麼交易系統 5 年演化史
那麼問題在哪? 有部分時候,藍色這條線,整體花費的時間居然比紅色這條線更少,也就是說,一部分比例的請求兜到外網溜一圈比內網資料庫的主從同步還快。

商戶端提出要輪主庫,禽獸啊,顯然,這個頻次,想是不用想的,不可能答應,畢竟之前輪詢從庫還打掛過。由消費者在本地 hold 一段時間再消費,也不太友好。畢竟有時候,快不一定是好事情,那麼我們能不能讓它慢一點出來?
 
於是,binding 的拓撲被我們改成了這樣,前段粉紅的這個 Queue ,使用了 RabbitMQ 死進佇列的特性(即訊息設定一個過期時間,等過期時間到了就可以從佇列中捨棄或挪到另外的地方):
餓了麼交易系統 5 年演化史
眼前的問題解決了,但也埋了坑,對 RabbitMQ 和架構設計稍有經驗的同學,應該很快意識到這裡犯了什麼錯誤。binding 關係這類 Meta 資訊每一個 Broker 都會儲存,用於路由。然而,訊息的持久化卻是在 Queue 中,而 queue 只會存在一個節點,本來是叢集,在這個時候,拓撲中靠前的一部分變成了單點。
 
回到我一開始提到的 MQ 叢集事故,因為一些原因牽連,我們這個 MQ 叢集某些節點跪了,很不幸,包含這個粉紅粉紅的 Queue 。於此同時,暴露了另外一個問題,這個拓撲結構,不能自動化運維,得依靠一定的人工維護,重建新的節點, meta 資訊需要從舊節點匯出匯入,但是會產生一定的衝突。並且,早期我們的 Topic 和 Queue 的宣告沒有什麼經驗,沒有根據消費者實際的消費情況來分配 Queue ,使得部分節點過熱。權衡自動運維和相對的均衡之下,後邊的做法,實際是隨機選擇了一個節點來宣告 Queue 。

之後我們做了兩個改進,一是拓撲結構支援在服務的配置檔案中宣告,隨服務啟動時自動到 MQ 中宣告;二是由商戶端後端服務,接到新單訊息來輪詢時,對新單by單單獨請求一次(有 cache,如果 miss 會路由到主庫)。
 
於是,訊息的拓撲結構變成了下邊這樣:
 
餓了麼交易系統 5 年演化史

訊息叢集拆分

仍然是上邊這個故事的上下文,我們回到影響這次事故的原因。根據我們對 RabbitMQ 叢集的效能測試,這個吞吐應該能夠承受,然而 CPU 負載非常的高,還影響了生產者傳送訊息(觸發了 RabbitMQ 的自保護機制),甚至掛掉。
 
經過架構師的努力下,最後追溯到,這次事故的原因,在於商戶端使用的公共 SOA 框架中,訊息佇列的客戶端,是部門自己獨立封裝的,這個客戶端,沒有很好理解 RabbitMQ 的一些 Client 引數(例如 get 和 fetch 模式, fetch 下的 prefetch_count引數等),其實這個引數需要一定的計算才能得到合理值,否則,即使機器還有 CPU 可用,消費能力也上不去。

和訂單的關係又是什麼?答案是 混布。這個叢集透過 vhost 將不同業務的訊息廣播隔開,因此上邊部署了訂單、運單、商戶端轉接的訊息等。
 
在事故發生當天,運營技術部老大一聲令下,無論怎麼騰挪機器,當天都必須搭建出一個獨立訊息廣播叢集給到訂單,運營技術部和我們,聯合所有的消費方,當天晚上,即搭建了一個7節點的叢集,將訂單的訊息廣播從中單獨拆出來。
 
(一年後,這個叢集也到了瓶頸,而且無法透過擴容解決,主要原因,一是消費方沒有使用RabbitMQ的特性來監聽訊息,而是本地過濾,導致白白耗費一部分處理資源;二是隨著叢集規模的上升,連線數達到了瓶頸。後者我們在生產者額外發了一份訊息到新搭建的一個叢集,得到了一定的緩解。真正解決,還是在餓了麼在 RabbitMQ 栽了這麼多跟頭,使用 Go 自研的 MaxQ 取代 RabbitMQ 之後)。
 
PS: 如果時光倒流,當初的改進項裡,會提前加一個第三點,針對使用`*`這個萬用字元來訂閱訊息的,都要求訂閱方根據真實需要更改。這裡腐化的原因,主要還是把控和治理的力度不夠,標準和最佳實踐建議在最初的說明文件就有,後續也提供了一些可供調整引數的計算公式,不能完全指望所有消費者都是老實人,也不完全由技術運營來把控,服務提供方是需要。
 

虛擬商品交易以及創新

 

早餐:

2015 年下旬到 2016 年上旬,餓了麼的早餐業務,雖然單量佔比不高,但對當時技術架構衝擊感,是比較大的。
 
一開始外賣和早餐的互動是這樣的:

餓了麼交易系統 5 年演化史
 
我猜這時候,一定會有小朋友有一堆問號...
我解釋一下背景:
1、早餐獨立於餐飲完全搭建了一套新的體系(使用者、店鋪、訂單、配送等等)。
2、因為支付沒法獨立搞,而支付在2016年初之前,是耦合在使用者系統裡的,並且,這套支付就是純粹為外賣定製的。
 
於是,作為「創新」部門的「創新業務」,為了快速試錯,完全自己搭建了一套完整的電商雛形,而為了使用支付,硬湊著“借”用了外賣的交易鏈路。這個方案是早餐的研發同學和支付的研發同學確定並實施的,訂單無感知的當了一把工具人。
 
當初我知道的時候,就已經長這樣了。我是什麼時候知道的,出鍋的時候,很真實。當時 PPE 和 PROD 沒有完全隔離,一次錯誤的操作導致 PROD 的非同步任務被拉取到 PPE ,再經過一次轉移,最後沒有 worker 消費導致訂單被取消。
 

餓配送會員卡

在 2016 年初,業務方提過來一個需求,希望餓了麼配送會員卡的售賣能夠線上化,此前是做了實體卡依靠騎手線下推銷的方式。正好,經過之前的架構評審,我們也需要一個流量較小的業務模式,來實踐我們新的架構設想,於是,就有了我們這套虛擬商品售賣的訂單系統。
 
我們抽象了一套最簡單的狀態模型:
 
餓了麼交易系統 5 年演化史
最核心的觀點:
 1、天下所有的交易,萬變不離其宗,主要的節點是較為穩定的。
2、C 端購買行為較為簡單,而 B 端的交付則可能千變萬化。
3、越是核心的系統,越應該保持簡單。
 
餓了麼交易系統 5 年演化史
上下游互動如上,商品的管理、營銷、導購等,都交給業務團隊自己,交易系統最核心的職責是提供一條通路和承載交易的資料。
 
在資料上的設計,買賣雙方、標的物、進行階段,這三個是當時我們認為較為必要的,當然,現在我可以給出更為標準的模型,但是,當時,我們真沒想那麼多。
 
 所以,交易主表拆成了兩。

 一張基礎表,包含主要買方ID、買方ID、狀態碼、業務型別、支付金額。業務型別是用來區分不同買賣方體系的。

另一張成為擴充套件表,包含標的物列表、營銷資訊列表、收貨手機號等等,屬於明細,允許業務方有一定的自由空間。
 
(PS: 事後來看,標的物、營銷資訊等等,雖然是可供上游自己把控的,但是需要對正規化從程式碼層面進行約束,否則治理會比較麻煩,業務方真是什麼都敢塞...)
 
拆兩張表,背後的原因,一是訂單一旦生成,快照的職責就幾乎完成了,剩下最關鍵的是狀態維護,高頻操作也集中在狀態上,那麼讓每條記錄足夠的小有助於保障核心流程;二是參照餐飲訂單的經驗, 2/3 的儲存空間是用在了明細上,特別是幾個 Json 欄位。
 
整個虛擬訂單系統搭建好之後,很多平臺售賣性質的業務都透過這套系統接入,對我們自身來說,接入成本開發+測試只需要 2~3 天以內,而整個業務上線一般一個星期以內就可以,我們很開心,前臺業務團隊也很開心。因為沒有大規模查詢的場景,很長一段時間,穩定支援每日幾十萬的成單,幾十核的資源綽綽有餘。
 
這其實是一個簡單的平臺化系統的雛形了。
 

其它

圍繞交易,我們其實還衍生出一些業務,廣義上,當時是訂單團隊來負責,也是組織架構影響導致,
 
例如「準時達」這個IP,技術側是我團隊主own從無到有實現的,同時又衍生出一塊 「交易賠付中心」,用來收口一筆交易過程中所有的賠付(包括紅包、代金券、現金、積分等),;
 
為了提升使用者交易體驗,我們發起了一個「交易觸達中心」(後演化為公司通用的觸達中心),收口了交易過程中對使用者的簡訊、push、電話等等觸達方式,特別是提升了極端case的觸達率,同時,減少對使用者的反覆騷擾。
 

服務和業務治理

 
上邊說的大都是一些技術細節上的提升,下邊兩件事,則是應用架構上的重大演化,也奠定了之後應用架構的走向。
 

逆向中的售中和售後

2016  年中旬,業務背景,為了提升使用者在不滿場景下的體驗(在我們的白板上密密麻麻貼了幾十個case),同時為了縮短結算賬期(因為逆向有效時間長達七天,結算強依賴了這個時間)。
 
在 JN 的發起下,我們從原來的訂單中,單獨把逆向拆出來,並且將原來的訂單組拆分成兩個團隊,我推薦了其中一位同學成為新團隊的 Team Leader 。
 
對於正向來說,最核心的職責是保障交易的順暢,因此它重點追求的是高效能、高併發和穩定性,越是清晰簡單越好,主次清楚,依賴乾淨,越容易快速定位問題,快速恢復。
 
逆向的併發遠小於正向,只有 1% 的訂單才會需要走到逆向,然而,業務邏輯的分支和層次關係複雜度,則遠大於正向,需要更強的業務抽象。雖然穩定和效能對逆向同樣很重要,但是相對沒那麼高。
 
因為核心問題域不同,服務要求級別不同,拆分是順理成章的事情。
 
實際拆分過程,還是蠻痛苦的,大家都是在探索,我和逆向組,包括和老闆,我們口水戰打了無數次。
 
當時的最終形態如下(也還是有問題的,在後邊的幾年我負責逆向後,把售中和售後合併了):

餓了麼交易系統 5 年演化史
第一步,是增加一個訂單狀態,用以表示訂單完成(約等於收貨,因為收貨後一般立馬就完成了,但二者概念上還是有一些差別)。光增加這個狀態,推動上下游,包括APP的升級,花費了近3個月。

第二步,搭建一套退單,訂單完成狀態灰度完成後,以這個狀態作為訂單生命週期的完結點,後續由退單負責。這樣清結算的入賬和扣款也就相互獨立了。

第三步,將訂單中涉及到售中的邏輯也一併切流到售中服務。(關於售中、售後的演化,後邊還有機會再展開)
 
我們當時踏入的其中一個坑,是沒有把狀態和上層事件剝離的比較乾淨,最終體現在業務邊界和分散式事務上有很多問題。
 
後來吵過幾次之後,訂單系統的主幹邏輯其實已經被剝離的比較簡單了,主要工作就是定義了狀態之間的關係,比如 A->C,B->C,A->B,這裡的A、B、C和能否扭轉都是訂單定義的,這層的業務含義很輕,重點在 *->C 我們認為是一個場景,上層來負責。

舉個例子, C 這個狀態是訂單無效,除開完結狀態的訂單,任何狀態都有一定條件可變到無效,滿足什麼樣的條件是由業務形態決定,適合放在售中服務中,他來決定要不要觸發訂單去扭轉狀態。類似的還有訂單收貨。
 
這個時候已經有了狀態機的神在(重構成狀態機的實現方式,放到17年初再說)
 
特別要說明的是紅色的那條線,確實是這種時效要求較高的交易場景下一個折中的設計,這條線最主要的任務,純粹就是打標,在訂單上打一個標表示是否有售後。我們參考了當時的電商(淘寶、京東),從端上的頁面就完成垂直拆開,對系統設計來說,要簡單的多,而我們沒辦法這麼做,這個是由業務形態決定的,商家在極短時間內要完成接單,同時還要時刻關注異常case,很多頁面在權衡下,要照顧使用者體驗。也就是說,雖然系統拆開了,但是在最上層的業務仍然不能拆開,甚至,內部也有很多聲音,我們只是希望退款,為什麼要我識別、區分並對接兩套系統。因此,一部分資料是回寫到了訂單上。
 
在這個階段,最受用的兩句話:
 
1、對事不對人: 無論怎麼吵,大家都是想把事情做的更好,底線是不要上升到人;(沒有什麼是一杯下午茶解決不了的)。
2、堅持讓一件事情變成更有益的: 誰也不是聖賢,無論當初的決定是什麼,沒有絕對的說服對方,拍板後就執行,發現問題就解決,而不是抱怨之前的決策。(與之相對的是,及時止損,二者並不衝突,但同樣需要決斷)。
 

物流對接

8月初計劃把 MQ 業務邏輯交接給我,因為設計理念不同,語言棧也不同,第一件事情便是著手重構。
 
在這裡先談談兩個“過時的”架構設計。
 

ToC & ToB & ToD:


在2016年初,有一個老的名詞,現在絕大部分人都不知道的東西: BOD。

這是早起餓了麼自配送的形態,這套業務體現,把訂單、店鋪、配送、結算等在業務上全耦合在一團。餓了麼自己的大物流體系從 2015 年中旬開始搭建,到了這個時間,順應著要做一個大工程, BOD 解耦。
 
這次解耦,誕生了服務包、ToB單、ToD單。
 
稍稍解釋一下業務背景,那時候的訴求,平臺將一些服務打包售賣給商戶,和商戶簽約,這裡售賣的服務中就包括了配送服務。那麼,商戶使用配送與否,就影響到了商戶的佣金和應收,然而,這個行業的特色創新,就是在商戶接單的時候,告訴商戶,交易完成,你確切能夠收入的錢是多少,相當於預先讓商戶看到一個大機率正確(不考慮售中的異常)的賬單,還得告訴商家,最終以賬單為準。
 
這其實是分賬和分潤的一些邏輯,就把清結算域的業務引入到交易鏈路上,清結算是常年做非實時業務的,那麼計算商戶預計收入這件事,撕了幾天之後,自然就落到到了訂單團隊上。另外一個背景,當時有很多攜程系過來的同學,攜程的業務形態是使用者向平臺下單,平臺再到供應商去下單,於是,ToC、ToB、ToD的概念,就這麼被引入了。
 
我接到的任務,就是要做一套 ToB 單。當時覺得這個形態不對,餓了麼的交易和攜程的交易是不一樣的。我向主管表示反對這個方案,但是,畢竟畢業半年沒多少沉澱,我拿不出來多少清晰有力的理由,也有一些其他人掙扎過,總之,3月初正式上線灰度。
 
餓了麼交易系統 5 年演化史
這個圖可以看出來幾個顯而易見的問題:
1、交易被拆成了幾段,而使用者、商戶實際都需要感知到每一段。並且每個階段對時效、一致性都有一定的要求。
2、平臺和物流只透過紅色的先來互動,這個通道很重
3、公式線下同步...

ToD

 上邊的架構實施後,到了 7 月份,ToD 這部分,變成了平臺和物流唯一的通道,太重了,業務還沒發展到那個階段,弊大於利。商戶端配送組的同學不開心,物流的同學不開心,訂單的同學也不開心。
 
正好,訂單在做增加完結狀態這個事。我們認為,訂單需要管控的生命週期,應該延伸到配送,並且配送屬於子生命週期,是交易的一部分。於是,7 月底, ToD 也交給了我,又到了喜聞樂見的重構環節。
 
作為商戶端技術體系的外部人員來看,當時 ToD 的設計非常的反人類。
餓了麼交易系統 5 年演化史
 
我們真正接手的時候發現,當時商戶端的應用架構大概是這樣的:

           餓了麼交易系統 5 年演化史

有這麼一個基礎設施公共層,這一層封裝了對 DB、Redis 等公共操作。也就是說,同一個領域的業務邏輯和資料,是根據這個體系的分層原則分在了不同層級的服務中,一個域內的業務層要操作它自己的資料,也需要透過介面進行。它可能有一定道理在(包括 2020 年我在面試一些候選人的時候發現,也有一些公司是這種做法),但是,交接出來的時候,痛苦!複雜的耦合,相當於要從一個錯綜複雜的體系裡剝出一條比較乾淨獨立的線。

那後來,我們改成下邊的樣子:
 
餓了麼交易系統 5 年演化史
 
1、ToB 和 ToD 被合併成為了一層,放在了 osc.blink 這個服務裡,並且消滅這兩個概念,作為訂單的擴充套件資料,而不是從交易中切出來的一段。
2、平臺和物流如果有資料互動,不一定需要透過這個對接層,這條鏈路最好只承載實時鏈路上配送所必須的資料。物流 Apollo 可以自己到平臺其它地方取其需要的資料。(這裡其實有一些問題沒解,osc.blink 和 Apollo 在兩方的定位並不完全一致,Apollo 作為運單中心收攏了和平臺對接的所有資料)
3、節點與節點之間的互動儘可能簡單,節點自身保證自身的健壯性。原先推單是透過訊息進行,現在改成了 RPC 進行,推的一方可以主動重推(有一個憑證保證冪等),拉的一方有補償拉取鏈路。

 (圖示的3.1,是由於當時外賣平臺和物流平臺,機房部署在不同城市,多次跨機房請求影響巨大,所以鏈路上由這個服務進行了一次封裝)。

到了8月底,呼單部分就完成上線。9月份開始把資料進行重構。
 
 

小結


 

到了 2016 年底,我們的交易體系整體長這樣:


餓了麼交易系統 5 年演化史
 
當時一些好的習慣和意識,挺重要:
 
1、理清權力和職責:程式碼倉庫許可權的回收,釋出許可權的回收,資料庫和訊息佇列連線串管控等等。

2、保持潔癖:
a. 及時清理無用邏輯(例如,我每隔一兩個月就會組織清理一批沒有流量的介面,也會對流量增長不正常的介面排查,下游有時候會怎麼方便怎麼來).
b. 及時清理無用的配置,不用了立馬乾掉,否則交接幾次之後估計就沒人敢動了.
c. 及時治理異常和解決錯誤日誌,這將大大的減小你告警的噪音和排查問題的干擾項。

3、理想追求極致但要腳踏實地。

4、堅持測試的標準和執行的機制。
a. 堅持自動化建設
b. 堅持效能測試
c. 堅持故障演練

5、不斷的請教、交流和思維衝撞。

6、Keep Simple, Keep Easy.

7、對事不對人。
 
架構的演進,最好是被業務驅動,有所前瞻,而不是事故驅動。回過頭發現,我們有一半的演進,其實是伴隨在事故之後的。值得慶幸的是,那個時候技術可自由支配的時間更多一些。
 
如果你閱讀到這裡,有很多共鳴和感觸,但是又說不出來,那麼你確實把自己的經歷整理出一些腦圖了。
 
在實習的半年,每個月都會感覺日新月異,在畢業的最初 1 年半里,總覺得 3 個月前的自己弱爆了,最初的這 2 年,是我在餓了麼所經歷的最為寶貴的時間之一。
 
上篇內容就到這裡,如果有所收穫,可以關注公眾號,等待下篇的內容。

作者資訊:
楊凡,花名挽晴,餓了麼高階架構師,2014 年加入餓了麼,2018 年隨餓了麼被阿里巴巴收購一同加入阿里巴巴,4 年團隊管理經驗,4 年主要從事餓了麼交易系統建設,也曾負責過餓了麼賬號、評價、IM、履約交付等系統。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69972138/viewspace-2687106/,如需轉載,請註明出處,否則將追究法律責任。

相關文章