本文主要介紹知乎訂單系統後端語言棧的轉型更新過程,包括其間踩過的一些坑和遇到的一些問題。一來是想通過本篇文章為其它應用服務轉型提供借鑒經驗,二來是總結對于訂單系統的了解。鑒于文字功底不足,對于業務了解不充分的地方,歡迎留言交流。
遷移背景
随着知乎整體技術棧的變化,原有的 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 很重要,有語言的交流和碰撞才更容易打破你的固有思維,做到業務認知的提升。
參考文章
https://mp.weixin.qq.com/s/eKc8qoqNCgqrnont2nYNgA
https://zhuanlan.zhihu.com/p/138222300
https://blog.csdn.net/g6U8W7p06
作者:知一
出處:https://zhuanlan.zhihu.com/p/383640330