業務的快速增長,要求系統在快速迭代的同時,保持很好的擴充套件性和可用性。其中,交易系統除了滿足上述要求之外,還必須保持資料的強一致性。對系統開 發人員而言,這既是機遇,也是挑戰。本文主要梳理大眾點評支付渠道閘道器係統在面對這些成長煩惱時的演進之路,以及過程中的一些思考和實踐。
在整個系統的演進過程中,核心思路是:大系統做小,做簡單(具體描述可參考《高可用性系統在大眾點評的實踐與經驗》)。在渠道閘道器係統實踐過程中,可以明顯區分出幾個有代表性的階段。
一、能用階段
早期業務流量還不是很大,渠道閘道器係統業務邏輯也很簡單,一句話總結就是:讓使用者在交易的時候,能順利把錢給付了。做的事情可簡單概括成3件:發起 支付請求、接收支付成功通知以及使用者要求退款時原路退回給使用者的支付賬戶。這個階段系統實踐比較簡單,主要就是“短、平、快”,快速接入新的第三方支付渠 道並保證能用。系統架構如圖1。
二、可用階段
在系統演進初期的快速迭代過程中,接入的第三方支付渠道不多,系統執行還算比較平穩,一些簡單問題也可通過開發人員人工快速解決。但隨著接入的第三方支付渠道不斷增多,逐漸暴露出一些新的問題:
(1) 所有的業務邏輯都在同一個物理部署單元,不同業務之間互相影響(例如退款業務出現問題,但是與此同時把支付業務也拖垮了);
(2) 隨著業務流量的增大,資料庫的壓力逐漸增大,資料庫的偶爾波動造成系統不穩定,對使用者的支付體驗影響很大;
(3) 支付、退款等狀態的同步很大程度上依賴第三方支付渠道的非同步通知,一旦第三方支付渠道出現問題,造成大量客訴,使用者體驗很差,開發、運營都很被動。
針對(1)中的業務之間互相影響問題,我們首先考慮進行服務拆分,將之前一個大的物理部署單元拆成多個物理部署單元。有兩種明顯的可供選擇的拆分策略:
- 按照渠道拆分,不同的第三方支付渠道獨立一個物理部署單元,例如微信一個部署單元,支付寶一個部署單元等;
- 按照業務型別拆分,不同的業務獨立一個物理部署單元,例如支付業務一個部署單元,退款業務一個部署單元等。
考慮到在當時的流量規模下,支付業務優先順序最高,退款等業務的優先順序要低;而有些渠道的流量佔比很小,作為一個獨立的部署單元,會造成一定的資源浪 費,且增加了系統維護的複雜度。基於此,我們做了一個符合當時系統規模的trade-off:選擇了第2種拆分策略 — 按照業務型別拆分。
針對(2)中的DB壓力問題,我們和DBA一起分析原因,最終選擇了Master-Slave方案。通過增加Slave來緩解查詢壓力;通過強制走Master來保證業務場景的強一致性;通過公司的DB中介軟體Zebra來做負載均衡和災備切換,保證DB的高可用性。
針對(3)中的狀態同步問題,我們對不同渠道進行梳理,在已有的第三方支付渠道非同步通知的基礎上,通過主動查詢定時批量同步狀態,解決了絕大部分狀態同步問題。對於仍未同步的少量Case,系統開放出供內部使用的API,方便後臺接入和開發人員手動補單。
在完成上述的實踐之後,渠道閘道器係統已達到基本可用階段,通過內部監控平臺可以看到,核心服務介面可用性都能達到99.9%以上。演化之後的系統架構如圖2。
三、柔性可用階段
在解決了業務隔離、DB壓力、狀態同步等問題後,渠道閘道器係統度過一段穩定可用的時期。但架不住業務飛速增長的壓力,之前業務流量規模下的一些小的 系統波動、流量衝擊等異常,在遭遇流量洪峰時被急劇放大,最終可能成為壓垮系統的最後一根稻草。在新的業務流量規模下,我們面臨著新的挑戰:
(1) 隨著團隊的壯大,新加入的同學在接入新的渠道或者增加新的邏輯時,往往都會優先選用自己熟悉的方式完成任務。但熟悉的不一定是合理的,有可能會引入新的風險。特別是在與第三方渠道對接時,系統目前在使用的HTTP互動框架就有 JDK HttpURLConnection/HttpsURLConnection、Httpclient3.x、Httpclient4.x
(4.x版本內部還分別有使用不同的小版本)。僅在這個上面就踩過好幾次慘痛的坑。
(2) 在按業務型別進行服務拆分後,不同業務不再互相影響。但同一業務內部,之前流量規模小的時候,偶爾波動一次影響不大,現在流量增大後,不同渠道之間就開始 互相影響。例如支付業務,對外統一提供分散式的支付API,所有渠道共享同一個服務RPC連線池,一旦某一個渠道的支付介面效能惡化,導致大量佔用服務 RPC連線,其他正常渠道的請求都無法進來;而故障渠道效能惡化直接導致使用者無法通過該渠道支付成功,連鎖反應導致使用者多次重試,從而進一步導致惡化加 劇,最終引起系統雪崩,拒絕服務,且重啟後的服務還有可能被大量的故障渠道重試請求給再次擊垮。
(3) 目前接入的第三方支付渠道,無論是第三方支付公司、銀行或是其他外部支付機構,基本都是通過重定向或SDK的方式引導使用者完成最終支付動作。在這條支付鏈 路中,渠道閘道器係統只是在後端與第三方支付渠道進行互動(生成支付重定向URL或預支付憑證),且只能通過第三方支付渠道的非同步通知或自己主動進行支付查 詢才能得知終端使用者支付結果。一旦某個第三方支付渠道內部發生故障,渠道閘道器係統完全無法得知該支付鏈路已損壞,這對使用者支付體驗造成損害。
(4) 現有的渠道閘道器的DB,某些非渠道閘道器服務仍可直接訪問,這對渠道閘道器係統的DB穩定性、DB容量規劃等帶來風險,進而影響渠道閘道器係統的可用性,內部戲稱被戴了“綠帽子”。
(5) 對於退款鏈路,系統目前未針對退款異常case進行統一收集、整理並分類,且缺乏一個清晰的退款鏈路監控。這導致使用者申請退款後,少量使用者的退款請求最終 未處理成功,使用者發起客訴。同時由於缺乏監控,導致這種異常退款缺乏一個後續推進措施,極端情形下,引起使用者二次客訴,極大損害使用者體驗和公司信譽度。
為最大程度解決問題(1)中描述的風險,在吸取踩坑的慘痛教訓後,我們針對第三方渠道對接,收集並整理不同的應用場景,抽象出一套接入框架。接入框 架定義了請求組裝、請求執行、響應解析和錯誤重試這一整套閘道器互動流程,遮蔽了底層的HTTP或Socket互動細節,並提供相應的擴充套件點。針對銀行渠道 接入存在前置機這種特殊的應用場景,還基於Netty抽象出連線池(Conn Pool)和簡單的負載均衡機制(LB, 提供Round Robin路由策略)。不同渠道在接入時可插入自定義的組裝策略(擴充套件已有的HttpReq、HttpsReq或NettyReq),執行策略[擴充套件已有 (Http、Https或Netty)Sender/Receiver],解析策略(擴充套件已有的HttpResp、HttpsResp或 NettyResp),並複用框架已提供的內容解析(binary/xml/json parser
)、證書載入(keystore/truststore loader
)和加解密簽名(encrypt/decrypt/sign/verify sign
)元件,從而在達到提高渠道接入效率的同時,儘可能減少新渠道接入帶來的風險。接入框架的流程結構如圖3。
為解決問題(2)中渠道之間相互影響,一個簡單直觀的思路就是渠道隔離。如何隔離,隔離到什麼程度?這是2個主要的問題點:
- 如何隔離 考慮過將支付服務進一步按照渠道拆分,將系統繼續做小,但是拆分後,支付API的呼叫端需要區分不同渠道呼叫不同的支付API介面,這相當於將渠道隔離問 題拋給了呼叫端;同時拆分後服務增多,呼叫端需要維護同一渠道支付業務的多個不同RPC-API,複雜度提高,增加了開發人員的維護負擔,這在當前的業務 流量規模下不太可取。所以我們選擇了在同一個支付服務API內部進行渠道隔離。由於共用同一個支付服務服務API連線池,渠道隔離的首要目標就是避免故障 渠道大量佔用AP連線池,對其他正常渠道造成株連影響。如果能夠自動檢測出故障渠道,並在其發生故障的初期階段就快速失敗該故障渠道的請求,則從業務邏輯 上就自動完成了故障渠道的隔離。
- 隔離到什麼程度 一個支付渠道下存在不同的支付方式(信用卡支付、借記卡支付、餘額支付等),而有些支付方式(例如信用卡支付)還存在多個銀行。所以我們直接將渠道隔離的最小粒度定義到支付渠道 -> 支付方式 -> 銀行。
基於上述的思考,我們設計並實現了一個針對故障渠道的快速失敗(fail-fast)機制:
- 將每一筆支付請求所附帶的支付資訊抽象為一個特定的fail-fast路徑,請求抽象成一個fail-fast事務,請求成功即認為事務成功,反之,事務失敗。
- 在fail-fast事務執行過程中,級聯有2個fail-fast斷路開關:
- 靜態開關,根據人工配置(on/off),斷定某個支付請求是否需快速失敗。
- 動態開關,根據歷史統計資訊,確定當前健康狀態,進而斷定是否快速失敗當前支付請求。
- 動態斷路開關抽象了3種健康狀態(closed-放行所有請求;half_open-部分比例的請求放行;open-快速失敗所有請求),並依據歷史統計資訊(總請求量/請求失敗量/請求異常量/請求超時量),在其內部維護了一個健康狀態變遷的狀態機。狀態變遷如圖4。
- 狀態機的每一次狀態變遷都會產生一個健康狀態事件,收銀臺服務可以監聽這個健康狀態事件,實現支付渠道的聯動上下線切換。
- 每一筆支付請求結束後都會動態更新歷史統計資訊。
經過線上流量模擬壓測觀察,fail-fast機制給系統支付請求增加了1~5ms的額外耗時,相比第三方渠道的支付介面耗時,佔比1%~2%,屬於可控範圍。渠道故障fail-fast機制上線之後,結合壓測配置,經過幾次微調,穩定了線上環境的fail-fast配置引數。
在前不久的某渠道支付故障時,通過公司內部的監控平臺,明顯觀察到fail-fast機制起到很好的故障隔離效果,如下圖5。
為解決問題(3)中支付鏈路可用性監測,依賴公司內部的監控平臺上報,實時監控支付成功通知趨勢曲線;同時渠道閘道器係統內部從業務層面自行實現了支 付鏈路端到端的監控。秒級監控支付鏈路端到端支付成功總量及支付成功率,並基於這2個指標的歷史統計資訊,提供實時的支付鏈路郵件或簡訊報警。而在流量高 峰時,該監控還可通過人工手動降級(非同步化或關閉)。這在很大程度上提高了開發人員的核心支付鏈路故障響應速度。
為解決問題(4)中的“綠帽子”,渠道閘道器係統配合DBA回收所有外部系統的DB直接訪問許可權,提供替換的API以供外部系統訪問,這給後續的提升DB穩定性、DB容量規劃以及後續可能的非同步多機房部署打下基礎。
針對問題(5)中退款case,渠道閘道器係統配合退款鏈路上的其他交易、支付系統,從源頭上對第三方渠道退款異常case進行統一收集、整理並分 類,並形成退款鏈路核心指標(退款當日成功率/次日成功率/7日成功率)監控,該部分的系統實踐會隨著後續的“退款鏈路統一優化”一起進行分享;
隨著上述實踐的逐步完成,渠道閘道器係統的可用性得到顯著提高,核心鏈路的API介面可用性達到99.99%,在 公司的917大促中,渠道閘道器係統平穩度過流量高峰,並迎來了新的記錄:提交第三方渠道支付請求的TPS達到歷史新高。且在部分渠道介面發生故障時,能保 證核心支付API介面的穩定性,並做到故障渠道的自動檢測、恢復,實現收銀臺對應渠道的聯動上下線切換。同時,通過核心支付鏈路支付成功率監控,實現第三 方渠道內部故障時,渠道上下線的手動切換。至此,基本保證了在部分第三方渠道有損的情況下,渠道閘道器係統的柔性可用。演化後的此階段系統架構如圖6。
四、經驗與總結
在整個渠道閘道器係統一步步的完善過程中,踩過很多坑,吃過很多教訓,幾點小的收穫:
- 堅持核心思想,拆分、解耦,大系統做小,做簡單;
- 系統總會有出問題的時候,重要的是如何快速定位、恢復、解決問題,這是一個長期而又艱鉅的任務;
- 高可用性的最大敵人不僅是技術,還是使用技術實現系統的人,如何在業務、系統快速迭代的過程中,保證自我驅動,不掉隊;
- 高流量,大併發對每一個工程師既是挑戰,更是機遇。