天天看點

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

個人簡介:

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、保持潔癖:

  1. 及時清理無用邏輯(例如,我每隔一兩個月就會組織清理一批沒有流量的接口,也會對流量增長不正常的接口排查,下遊有時候會怎麼友善怎麼來).
  2. 及時清理無用的配置,不用了立馬幹掉,否則交接幾次之後估計就沒人敢動了.
  3. 及時治理異常和解決錯誤日志,這将大大的減小你告警的噪音和排查問題的幹擾項。

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

4、堅持測試的标準和執行的機制。

  1. 堅持自動化建設
  2. 堅持性能測試
  3. 堅持故障演練

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

6、Keep Simple, Keep Easy.

7、對事不對人。

架構的演進,最好是被業務驅動,有所前瞻,而不是事故驅動。回過頭發現,我們有一半的演進,其實是伴随在事故之後的。值得慶幸的是,那個時候技術可自由支配的時間更多一些。

如果你閱讀到這裡,有很多共鳴和感觸,但是又說不出來,那麼你确實把自己的經曆整理出一些腦圖了。

在實習的半年,每個月都會感覺日新月異,在畢業的最初 1 年半裡,總覺得 3 個月前的自己弱爆了,最初的這 2 年,是我在餓了麼所經曆的最為寶貴的時間之一。

上篇内容就到這裡,如果有所收獲,可以關注公衆号,等待下篇的内容。

作者資訊:

楊凡,花名挽晴,餓了麼進階架構師,2014 年加入餓了麼,2018 年随餓了麼被阿裡巴巴收購一同加入阿裡巴巴,4 年團隊管理經驗,4 年主要從事餓了麼交易系統建設,也曾負責過餓了麼賬号、評價、IM、履約傳遞等系統。