1次訂單系統遷移,頭發都快掉完了...
本文主要介紹知乎訂單系統后端語言棧的轉型升級過程,包括其間踩過的一些坑和遇到的一些問題。
圖片來自 包圖網
一來是想通過本篇文章為其他應用服務轉型提供借鑒經驗,二來是總結對于訂單系統的理解。
遷移背景
隨著知乎整體技術棧的變化,原有的 Python 技術棧逐漸被拋棄,新的 Go 和 Java 技術棧逐漸興起。
知乎交易系統的穩定性相比其它業務系統的穩定性重要很多,因為交易系統核心鏈路發生故障不僅會造成數據問題,還會造成嚴重的資損問題。
隨著公司業務的不斷壯大發展,交易場景變得復雜,重構和優化難以避免,因為語言特性,Python 雖然開始擼代碼很爽,但是后期的維護成本慢慢變高。
不過 Python 在數據分析和人工智能方向上還是有很大優勢的,只是在交易領域目前看起來不太合適。
從技術生態上來說,用 Java 做交易系統會更有優勢,所以接下來要說的知乎訂單系統語言棧轉型。
另外一個因素是 Python 的 GIL 鎖導致它無法發揮多核的優勢,性能上受到很大限制,在實際情況中遇到過多次主線程被 hang 住導致的可用性故障,所以堅定決心來遷移掉舊系統。
前期準備
工欲善其事,必先利其器。
語言棧轉型首先要明確轉型的三個開發流程,即 MRO(Migration,Reconstruction,Optimization)。
- 遷移:就是把原語言代碼照著抄一遍到新語言項目上,按照新語言的工程實現風格來做就可以。其間最忌摻雜代碼優化和 bug 修復,會容易引起新的問題,增加驗證代碼的難度。
- 重構:目的是提高項目代碼的可維護性和可迭代性,讓代碼更優雅和易讀懂,可以放到遷移完成來做。
- 優化:通過在模塊依賴、調用關系、接口字段等方面的調整來降低項目的復雜性,提高合理性。
對于語言棧轉型來說,遷移流程是肯定要做的,重構和優化如何選擇,可以按模塊劃分功能拆成子任務來分別評估方案。
參考依據為現有模塊如果同時優化或重構帶來的直接收益和間接收益有多少:
- 收益:完成新舊語言棧的轉換,系統維護性更好,模塊邊界更清晰。
- 成本:需要投入的人力成本,遷移過程中的并行開發成本,使有更高價值的工作被阻塞的損失。
- 風險:引入新的 bug,增加測試的復雜性。
在風險可控的前提下,成本與收益要互相權衡,一般會有兩種方案可供參考:
- 第一種是鎖定需求,堆人力開發上線,一步到位。
- 第二種則是小步快走,迭代上線,分批交付。
基于以上分析,在本次轉型過程中,人力成本是一個更重要的因素,所以采用只遷移的方案,來壓縮人力成本,降低 bug 引入風險的同時也具有很好的可測試性。
并且為了不阻塞業務需求,采用小步快走的方式分批交付,以最長兩周作為一個迭代周期進行交付。
遷移方案
確定了交付方式,下面我們需要梳理當前系統中的功能模塊,做好任務拆分和排期計劃。
知乎交易系統在遷移前的業務是針對虛擬商品的交易場景,交易路徑比較短,用戶從購買到消費內容的流程如下:
- 在商品詳情頁瀏覽
- 生成訂單進入收銀臺和用戶支付
- 確認支付后訂單交付
- 用戶回到詳情頁消費內容
- 特定商品的七天無理由退款
當時訂單系統支持的功能還不多,業務模型和訂單模型沒有足夠地抽象,梳理訂單系統業務如下:
完成了訂單模塊的拆分后,新老系統如何無縫切換?如何做到業務無感?如何保障交易系統穩定性?出現故障如何及時止損?
基于上面講述的原則,將整個系統的遷移劃分成兩個階段,遷移前后的數據存儲和模型都不變。
①接口驗證
不論是在遷移的哪個階段,總需要調整訂單接口,可以從訂單操作角度分為讀操作和寫操作,需要針對讀接口和寫接口做不同的驗證方案。
寫操作可以通過白名單測試以及灰度放量的方式進行驗證上線,將接口未預期異常輸出到 IM 工具以得到及時響應。
主要的寫操作相關接口有:
- 訂單的創建接口。
- 訂單綁定支付單的提交接口。
- 用戶支付后回調確認接口。
- 用戶發起退款接口。
下圖展示的是 AB 平臺的流量配置界面:
下圖展示了部分交易預警通知消息:
讀操作往往伴隨在寫操作中。我們利用平臺的錄制回放功能進行接口的一致性檢查,通過對比得出差異排查問題。
主要的讀操作接口有:
- 獲取支付方式列表接口
- 獲取訂單支付履約狀態接口
- 獲取充值列表接口
- 批量查詢用戶新客狀態接口
下圖展示的是流量錄制回放系統的數據大盤:
②指標梳理
監控是我們系統的『第三只眼』,可以及時反應系統的健康狀況,及時發出告警信息,并幫助我們在出現故障時分析問題和快速縮小排查范圍。
硬件、數據庫、中間件的監控已經在平臺層得到支持,這里只需要梳理出應用的監控指標。
日志監控:請求日志、服務端的錯誤日志。
訂單業務指標:
- 下單量、成單量、掉單量
- 單量環比數據
- 首次履約異常量
- 補償機制履約量
- 各通知事件 P95 耗時
- 成功履約 P95 耗時
- 履約準時率/成功率
支付業務指標:
- 支付渠道履約延遲 P95
- 支付履約延遲 P95
- 用戶購買完整耗時 P95
③可用性保障
在整個交付的過程中,轉型前后對 SLA 要提供一致的可用性保障,可以看看下面的幾個衡量標準:
一般 3 個 9 的可用性全年宕機時間約為 8.76 小時,不同系統不同用戶規模對于系統可用性的要求不一樣,邊緣業務的要求可能會低一些,但是對于核心鏈路場景 TPS 可能不高,但是必須要求保證高可用級別。
如何保證或者提升服務的 SLA 是我們接下來要探討的內容,一般有下面兩個影響因素:
- MTBF(Mean Time Between Failures):系統服務平均故障時間間隔
- MTTR(Mean Time To Recover):系統服務平均故障恢復時長
也就是說我們要盡可能地降低故障頻率,并確保出現故障后可以快速恢復。基于這兩點我們在做系統平穩過渡時,要充分測試所有 case ,并且進行灰度方案和流量錄制回放,發現異常立即回滾,定位問題解決后再重新灰度。
④MTTR 快速響應
持續監控:感知系統穩定性的第一步就是監控,通過監控來反映系統的健康狀況以及輔助定位問題。
監控有兩個方向:第一個方向是指標型監控,這里監控是在系統代碼中安排各種實時打點,上報數據后通過配置報表呈現出來的。
基礎設施提供的機器監控以及接口粒度的響應穩定性監控:
- 物理資源監控,如 CPU、硬盤、內存、網絡 IO 等。
- 中間件監控,消息隊列、緩存、Nginx 等。
- 服務接口,HTTP、RPC 接口等。
- 數據庫監控,連接數、QPS、TPS、緩存命中率、主從延遲等。
業務數據層面的多維度監控,從客戶端和服務端兩個角度來劃分。
- 從客戶端角度來監控服務端的接口成功率,支付成功率等維度:
從服務端角度從單量突變、環比變化、交易各階段耗時等維度持續監控。
以上兩點基于公司的 statsd 組件進行業務打點,通過配置 Grafana 監控大盤實時展示系統的健康狀況。
第二個方向是日志型監控,這主要依賴公司的 ELK 日志分析平臺和 Sentry 異常捕獲平臺。
通過 Sentry 平臺可以及時發現系統告警日志和新發生的異常,便于快速定位異常代碼的發生位置。
ELK 平臺則可以將關鍵的日志詳細記錄下來以便于分析產生的場景和復現問題,用來輔助修復問題。
⑤異常告警
基于以上實時監控數據配置異常告警指標,能夠提前預知故障風險,并及時發出告警信息。然而達到什么閾值需要告警?對應的故障等級是多少呢?
首先我們要在交易的黃金鏈路上制定比較嚴格的告警指標,從下單、提單、確認支付到履約發貨的每個環節做好配置,配置的嚴重程度依次遞增分為 Info、Warning、Critical。
按照人員類別和通知手段來舉例說明告警渠道:
IM 中的預警消息截圖如下:
訂單主要預警點如下:
- 核心接口異常
- 掉單率、成單率突變
- 交易各階段耗時增加
- 用戶支付后履約耗時增加
- 下單成功率過低
⑥MTBF 降低故障率
系統監控告警以及日志系統可以幫我們快速的發現和定位問題,以及時止損。
接下來說的質量提升則可以幫助我們降低故障發生率以避免損失,主要從兩個方向來說明:
規范化的驗收方案:
- 開發完成包括邏輯功能和單元測試,優先保證單測行數覆蓋率再去保證分支覆蓋率。然后在聯調測試環境中自測,通過后向 QA 同學提測。
- QA 同學可以在測試環境下同時進行功能驗收和接口測試,測試通過后便部署到 Staging 環境。
- 在 Staging 環境下進行功能驗收并通過。
- 灰度交付以及雙讀驗證可以根據實際情況選擇性使用。
上線后需要最后進行回歸測試。
統一的編碼規約以及多輪 CR 保障:代碼上線前一般至少要經過兩次代碼評審,太小的 MR 直接拉一位同事在工位 CR 即可,超過百行的變更需要拉會研討,兩次評審的關注點也不同。
第一次評審應關注編碼風格,這樣可以避免一些因在寫法上自由發揮而帶來的坑,以此來沉淀出組內相對統一的編碼規約,在編碼的穩定性上建立基本的共識,提升代碼質量。
第二次評審應關注代碼邏輯,這里有個需要注意的點是,如果明確只做遷移,那么其間發現舊邏輯難理解的地方不要隨便優化,因為在不了解背景的情況下很有可能會寫一個 bug 帶上線(這種事見過好幾次)。
另外這樣也好去對比驗證,驗證通過上線后再去優化。只有通過明確目的和流程并且遵循這個流程做,才能更快更好地交付有質量的代碼。
一致性保障:每一個微服務都有自己的數據庫,微服務內部的數據一致性由數據庫事務來保障,Java 中采用 Spring 的 @Transtaction注解可以很方便地實現。
而跨微服務的分布式事務,像支付、訂單、會員三個微服務之間采用最終一致性,類似 TCC 模式的兩階段提交,訂單通過全局發號器生成訂單 ID,然后基于訂單 ID 創建支付單。
如果用戶支付后訂單會變更自身狀態后通知會員微服務,履約成功則事務結束,履約失敗則觸發退款,如果用戶未支付,那么訂單系統將該訂單以及支付單做關單處理。
對應一致性保障,我們對訂單接口做了兩個方面的處理:
分布式鎖:對于上游的支付消息監聽、支付 HTTP 回調、訂單主動查詢支付結果三個同步機制分別基于訂單 ID 加鎖后再處理,保證同步機制不會被并發處理。
接口冪等:加鎖后對訂單狀態做了檢查,處理過則響應成功,否則處理后響應成功,保證上游消息不會被重復處理。
訂單對于下游的履約,是通過訂單 ID 作為冪等 key 來實現的,以保證同一個訂單不會被重復履約,并且通過 ACK 機制保證履約后不會再重復調到下游。
其中分布式鎖采用 etcd 鎖,通過鎖租約續期機制以及數據庫唯一索引來進一步保障數據的一致性。
補償模式,雖然我們通過多種手段來保證了系統最終一致,但是分布式環境下會有諸多的因素,如網絡抖動、磁盤 IO、數據庫異常等都可能導致我們的處理中斷。
這時我們有兩種補償機制來恢復我們的處理:
- 帶懲罰機制的延時重試:如果通知中斷,或者未收到下游的 ACK 響應,則可以將任務放到延遲隊列進行有限次的重試,重試間隔逐次遞增。最后一次處理失敗報警人工處理。
- 定時任務兜底:為了防止以上機制都失效,我們的兜底方案是定時掃描異常中斷的訂單再進行處理。如果處理依然失敗則報警人工處理。
事后總結
①目標回顧
目標一:統一技術棧,降低項目維護成本。目標結果是下線舊訂單系統。
目標二:簡化下單流程,降低端接入成本。目標結果是后端統一接口,端上整合 SDK。
②執行計劃
遷移的執行總共分成了三個大階段:
第一階段是遷移邏輯,即將客戶端發起的 HTTP 請求轉發到 RPC 接口,再由新系統執行。第一階段做到所有的新功能需求都在新系統上開發,舊系統只需要日常維護。
第二階段是通過和客戶端同學合作,遷移并整合當前知乎所有下單場景,提供統一的下單購買接口,同時客戶端也統一提供交易 SDK,新組件相對更加穩定和可監控,在經過灰度放量后于去年底完全上線。
第二階段做到了接口層的統一,更利于系統的維護和穩定,隨著新版的發布,舊接口流量已經變得很低,大大降低了下階段遷移的風險。
第三階段是舊 HTTP 接口遷移,由新系統承載所有端的請求,提供相同規格的 HTTP 接口,最后通過修改 NGINX 配置完成接口遷移。第三階段遷移完成后舊系統最終實現了下線。
③執行結果
截至此文撰寫時間,語言棧已經 100% 遷移到新的系統上,舊系統已經完全下線,總計下線 12 個系統服務, 32 個對外 HTTP 接口,21 個 RPC 接口,15 個后臺 HTTP 接口。
根據 halo 指標,遷移前后接口 P95 耗時平均減少約 40%,硬件資源消耗減少約 20%。根據壓測結果比較,遷移后支撐的業務容量增長約 10 倍。
系統遷移完成只是取得了階段性的勝利,接下來系統還需要經過一些小手術來消除病灶。
主要是以下幾點:
- 不斷細化監控粒度,優化告警配置,繼續提高服務的穩定性。
- 對于 Python 的硬翻譯還需要不斷重構和優化,這里借鑒 DDD 設計思想。
- 完善監控大盤,通過數據驅動來運營優化我們的流程。
- 項目復盤總結以及業務普及宣講,提升人員對于業務細節的認知。
④問題整理
遷移總是不能一帆風順的,期間遇到了很多奇奇怪怪的問題,為此頭發是真沒少掉。
問題 1:遷移了一半新需求來了,又沒有人力補上來怎么辦?
遷移后再做重構和優化過程,其實很大一部分考量是因為人力不足啊,而且現狀也不允許鎖定需求。那么只能寫兩遍了,優先支持需求,后面再遷移。如果人力充足可以選擇一個小組維護新的系統一個小組維護舊的系統。
問題 2:我明明請求了,可日志怎么就是不出來呢?
不要懷疑平臺的問題,要先從自身找問題。總結兩個原因吧:
- 一個是新舊系統的遷移點太分散導致灰度不好控制。
- 另一個是灰度開關忘記操作了,導致流量沒有成功導到新系統上。這里要注意一個點就是在遷移過程中要盡可能的快速交付上線。
問題 3:公司 Java 基礎服務不夠完善,很多基礎平臺沒有支持怎么辦?
于是自研了分布式延遲隊列、分布式定時任務等組件,這里就不展開聊了。
問題 4:如何保證遷移過程中兩個系統數據的一致性?
首先我們前面講到的是系統代碼遷移,而數據存儲不變,也就是說兩個系統處理的數據會存在競爭,解決的辦法是在處理時加上分布式鎖,同時接口的處理也是要冪等的。
這樣即使在上下游系統做數據同步的時候也能避免競爭,保證數據的一致性。
就用戶支付后支付結果同步到訂單系統這一機制來說,采用推拉的機制:
- 用戶支付后訂單主動輪詢支付結果,則是在主動拉取數據。
- 支付系統發出 MQ 消息被訂單系統監聽到,這是被動推送。
- 支付成功后觸發的訂單系統 HTTP 回調機制,這也是被動推送。
以上三種機制結合使用使得我們系統數據一致性有一個比較高的保障。我們要知道,一個系統絕非 100% 可靠,作為交易支付的核心鏈路,需要有多條機制保證數據的一致性。
問題 5:用戶支付后沒有收到會員權益是怎么回事?
在交易過程中,訂單、支付、會員是三個獨立的服務,如果訂單丟失了支付的消息或者會員丟失了訂單的消息都會導致用戶收不到會員權益。
上一個問題中已經講到最終一致性同步機制,可能因為中間件或者網絡故障導致消息無法同步。
這時可以再增加一個補償機制,通過定時任務掃描未完成的訂單,主動檢查支付狀態后去會員業務履約,這是兜底策略,可保障數據的最終一致。
⑤業務沉淀
從接收項目到現在也是對訂單系統從懵懂到逐漸加深理解的一個過程,對于當前交易的業務和業務架構也有了一個理解。
交易系統本身作為支付系統的上層系統,提供商品管理能力、交易收單能力、履約核銷能力。
外圍業務子系統主要關注業務內容資源的管理。業務的收單履約管理接入交易系統即可,可減輕業務的開發復雜度。
收單流程展示如下:
- 業務定制商品詳情頁,然后通過詳情頁底欄調用端能力進入訂單收銀臺。在這里客戶端需要調用業務后端接口來獲取商品詳情,然后調用交易底欄的展示接口獲取底部按鈕的情況。
- 用戶通過底部按鈕進入收銀臺后,在收銀臺可以選擇支付方式和優惠券,點擊確認支付調起微信或者支付寶付款。收銀臺展示以及獲取支付參數的接口由交易系統提供。
- 訂單后臺確認收款后會通知業務履約,用戶端會回到詳情頁,用戶在詳情頁進入內容播放頁享受權益。履約核銷流程是業務后端與交易系統后端的接口調用來完成的。
現在知乎站內主要是虛擬商品的交易,一個通用的交易流程如下圖:
用戶經歷了從商品的瀏覽到進入收銀臺下單支付,再回到內容頁消費內容。隨著業務的發展,不同的交易場景和交易流程疊加,系統開始變得復雜,一個交易的業務架構慢慢呈現。
訂單系統主要承載知乎站內站外的各種交易服務,提供穩定可靠的交易場景支撐。
主要分為以下幾個部分:
- 首先產品服務層是面向用戶能感受到的交互界面,提供對于這些頁面的統一下單支付 API 網關。
- 然后是訂單服務層,由上層網關調用,提供著不同場景下的交易服務支撐。
- 再往下是訂單領域層,承載訂單最核心邏輯代碼,首先是用戶購買需要的算價聚合,然后是管理訂單模型的交易聚合,最后是買完商品后的履約處理的交付聚合。
- 最底層是基礎支撐服務層,主要是提供基本的服務支持以及交易依賴的一些服務。
- 最后是運營服務,提供交易相關的后臺功能支持。
⑥方法論實踐
凡此以上,不論系統遷移方案還是架構理解都歸結于參與人員的理解與認知,一個優秀的方案或合適的架構不是設計出來的,是迭代出來的。
人的認知也是這樣,需要不斷的迭代升級,和很多的方法論一樣,PDCA 循環為我們提煉了一個提升路徑:
- Plan 計劃:明確我們遷移的目標,調研現狀指定計劃。
- Do 執行:實現計劃中的內容。
- Check 檢查:歸納總結,分析哪些做好了,還有什么問題。
- Action 調整:總結經驗教訓,在下一個循環中解決。
很多時候,也許你只做了前兩步,但其實后兩步對你的提升會有很大幫助。
所以一個項目的復盤,一次 Code Review 很重要,有語言的交流和碰撞才更容易打破你的固有思維,做到業務認知的提升。
作者:張旭,知乎后端開發工程師,主要負責知乎商業基礎相關系統研發,專注于電商交易營銷領域。
編輯:陶家龍
出處:轉載自公眾號高可用架構(ID:ArchNotes)