支付網關 | 京東618、雙11用戶支付的核心承載系統(上篇)

  二零一七年六月二十一日,就是年中大促剛結束的那一天,我午飯時間獨在辦公室裏徘徊,遇見X君,前來問我道,“可曾爲這次大促寫了一點什麼沒有?”我說“沒有”。他就正告我,“還是寫一點罷;小夥伴們很想了解支撐起這麼大的用戶支付流量所採用的技術。”

「摘要」由於設計時我跟小夥伴們把系統的定位更偏向於具有用戶支付事務處理能力的消息總線。業務深度耦合涉及比較廣,感覺一次性到位說清楚不太可能。故本篇分爲上下兩篇,上篇僅對支付網關架構和支付業務流程進行基本介紹,採用的全時在線技術裏的基礎部分說明也放在上篇,下篇則着重介紹全時在線技術的具體化應用以及下級系統重構和遷移過程中的備份切量邏輯。更詳盡或代碼層級的文章將後續單獨推出。

  • 上  篇
    支付網關架構和支付業務流程基本簡介
    UUID併發序列生成器
    平行遷移
    本地化存儲
    緩存雙備

  • 下  篇
    自行收單、
    補單
    異步交互
    路由分流功能

    支付結算平臺的交互細節

支付網關作爲支付目前的總入口,在最近一次618大促的實戰檢驗中:

  承載的峯值用戶支付流量TPS爲2.2萬以上
  承載了期間用戶支付的全部流量

經過持續的優化,整個京東支付在用戶層就具備了完整的自我閉環能力,完全解決業內普遍存在依賴單支付清算機構的瓶頸問題。在日常和大促期間我們的系統定位主要集中在3個方面:
一、提供高效穩定的扣款支付能力
二、保障友好化的商城客戶支付體驗
三、高併發場景下的全時在線服務:
    a)        業務模塊降級
    b)        基礎工具模塊熱插拔
    c)        流量分佈式平行轉移
本次618預先啓用了分流和緩衝機制,在保證支付體驗的情況下,儘可能的預防了核心業務系統、各支付機構被流量衝擊打垮的情況。 

支付網關架構和支付業務流程基本簡介

1、支付網關的架構模塊體系:

wKiom1lfAV_CxtqtAALAJMoAI64735.jpg

按照功能主要分爲:業務模塊、支付渠道模塊、體驗保障與高速化模塊、基礎工具模塊。大體的縱向各個功能模塊的組織譜系關係如下:

wKioL1lfAeqRUnYWAAOYMqaJvYg495.jpg

2、支付渠道(以下稱爲:渠道)主要爲支付網關(以下稱爲:網關)提供了支付工具的扣款能力,網關綜合各渠道的扣款能力屏蔽機構間的差異,生成核心支付組件。再結合核心的業務控制能力(風控、路由、商品訂單)進行流程化封裝生成對支付接口。收銀臺增添一些先決認證條件:生物識別,手機認證(短信、尾號)、SSO登錄、設備認證通過後調用網關的支付接口進行支付。


3、詳細的業務交互時序流程,以其中一個支付流程爲例,大體時序爲:

wKioL1lfAjTCYQZNAAU4oYsh07Q340.jpg

根據業務時序圖,做過大型企業ERP、BOSS系統的人看來並不複雜。但在互聯網最大的挑戰就是大併發的情況。
4、網關的技術架構思想爲:
1. 保障核心業務邏輯穩定或無損平行轉移,極端情況的下的failback
2. 拆解非核心業務邏輯到異步分支流程.
3. 非核心業務邏輯性能差或故障時降級,並實現failover
4.   取消數據庫依賴
5.   基礎工具組件雙備甚至多備下的熱插拔


UUID併發序列生成器

UUID是於2016年2月完全自主研發的業務單號生成系統,對代碼進行了開源,完全遵循LGPL協議。支付單號生成是我們收單最重要的一步,由支付單號來流轉整個支付流程。

它理論基礎是按照能有效組織資源的最細粒度拆分服務單元,根據服務單元的屬性差異進行唯一化。唯一化的服務單元互相隔離,由於具有唯一性各節點構成分佈式,服務單元內部再按照能有效組織的最細粒度資源進行功能克隆拆分子服務單元,共享資源需要被其中一個子服務單元使用是進行排它獨佔。

實際解決的問題:生產單號多采用數據庫、隨機數生成,在請求量較少時問題不顯著。隨着請求量的加大出現重複、卡死的概率逐漸增加且部署數據庫、運營成本較高。很多公司的數據庫也不光給uuid使用,在海量事務處理時是常出現僅爲生成id就耗費CPU時間片,造成正常業務處理延時。

具體技術實現:
我總共寫過三種實現:netty、tomcat做中間件各一版、linuxC一版。我們目前生產環境中使用tomcat版本:

wKioL1lfArehRhDhAAFxKnOBdek643.jpg

1、註冊中心的實現:因爲只起到分配實例號的作用。正常情況下tomcat實例終身也只在第一次啓動時獲取一次實例號。由於壓力不在生成實例號,簡單實現的話使用數據庫單表,表的自增主鍵作爲實例號,表中的md5值爲唯一鍵。自己也可以生成一個註冊中心,只需要注意號記錄md5和實例號所在文件的文件鎖問題即可。生產環境當中我們使用的是mysql.
2、三種生成id的細節性問題:
第一種方式是性能最高最可靠的方式。但由於加入了時間維度,如果在極短的時間內重啓完畢,存在單位時間裏內存遞增變量歸0遞增後重復的概率,於是加入了延時等待的功能讓單個實例啓動後延時一段時間再提供id。延時的時間>=時間的維度步長。如:時間維度爲1s則實例啓動後至少應該延時1s在提供生成服務。
第二方式:適合id號必須連續的場景,比如會計憑證號.但是第二種方式由於沒有操作系統文件文件鎖的保護,只能當單臺機器上只有一個tomcat實例的情況下使用。
第三種方式:適合id號必須連續的場景,比如會計憑證號。由於有操作系統文件鎖保護適合單個機器上存在多個tomcat實例的情況使用。
注意:第二、三種方式linux系統對同時打開的文件句柄有數量限制,由於序列名跟文件名一一對應,存在文件句柄資源池管理的機制控制文件句柄能最大效率的使用和按需關閉。

3、有一些公司往往在一臺機器上部署多個tomcat實例,所以向註冊中心註冊時使用的是$catalina.base 而不是$catalina.home.  由於我們用的是docer和jvm虛擬機,一臺虛擬機上只能部署一個tomcat不用顧慮這個問題。當然程序進行通用性兼容可以讓PE們部署的時候放心用。當然這也就爲什麼會存在第二種方式獲取id方式的原因。

4、Id分單個獲取和批量獲取,批量獲取時採用共享變量直接+批量步長的方式,而不是for循環。減小cpu時間片佔用和減少鎖長時間佔用導致類似starvation現象的發生.

5、存在堆gc對生成id的性能影響,雖然看來非常細微,但是生成id是持久化的第一步。它的每延遲增加1ms往往帶來全鏈路的延時放大。我們後來找到辦法,在大促時段消除了gc影響。這個方法我們在後續的技術文章中會專門說明。

wKiom1lfA3SwxijkAAI2Sa4dxIY951.jpg

wKioL1lfA07BELFOAAMaIn_oU2A766.jpg


平行遷移

平行遷移功能,是我們做故障遷移,流量定位轉移的工具。基於UUID生成實例號的原理,所有的實例數量都已經存在了註冊中立裏,區別在於還要把能標記自己的資源定位符也一起給出來.於是註冊中心搖身一變成管理端,起到中介者的角色。以一次業務調用爲例:

正常情況下:

wKioL1lfA8DjY3r5AAHEMzDsmY4318.jpg

 當實例1故障時:

wKioL1lfA-uzwk_5AAHh3oxM27g470.jpg

1、調用端並不是每次都到管理中心拿映射關係,正常情況下調用端只在第一次系統啓動時到管理中心獲取對端的URI並記錄到本地,只要對端正常就一直會訪問。實際我們調用端有個開關工功能控制出現異常時查詢還是每次都查詢。

2、如果每次都查詢管理中心,那管理中心的性能如何保證。目前我們採用純本地JVM的K-V類型Map + ReentrantReadWriteLock+數據庫 進行解決.

wKioL1lfBCHRAP74AAM3LDkSHxE591.jpg

3、在第1條中有說過調用端存在開關機制是異常時查詢或每次都查詢的開關,我們在進行服務端取餘結果跟實例對應關係的時候,如果NormalCache賦值的時候以非常小的概率遇到了賦值非原子性操作的問題,無非是兩種情況:
一種情況:調用端在利用返回URI訪問實例的時候出現異常,這個時候調用端會再去訪問管理端查詢URI從而避免。
另一種情況:調用端利用返回的URI能正常訪問實例,但是我們已經調整映射關係到希望它能訪問另外一個實例。其實這個時候場景一般出現在我們密集調整對應關係的時候,這種調整和效果觀察的持續時間往往不會短(肯定是秒級以上吧),這個時候我們會打開每次都查詢管理中心的開關並持續一段時間,觀察訪問到了再切回這個開關到異常時查詢從而避免。
當然如果你考量這個應用場景還是覺得不放心,那可以在讀取的時候用writeLock實現.實際由於全部是內存操作、並且數據庫讀取在獲取Lock之前,這種情況下采用Lock的性能損失接近於無,也非常好。
4、Hash一致性問題,由於這個實例號的生成邏輯是穩定的,由於是實例號是累增不會中間插入,所以目前不存在Hash一致性問題。當然有一些應用場景可能我沒遇到,也歡迎大家探討。


本地化存儲

本地存儲主要分爲兩類:
1、一般消息性存儲,即把要對外發送的消息出現異常時先存儲到本地,然後單獨再起線程向原來的接受方進行  傳輸。由於目前應用場景主要面向消息隊列,已經逐漸被我們的部門統一研發的mqSender取代,是對消息隊列在客戶端的failover機制的一種擴充。如果要是自己實現的話,單就存儲而言在採用MappedByteBuffer做內存和刷盤工具+ ReentrantReadWriteLock進行線程隔離就能滿足需求,就不多說了。
2、有順序保障的結構性存儲,是我們進行自行收單的基礎下篇會詳細講到。如:同一筆支付需要創建支付單(類似財務的應收概念)、寫支付結果(類似財務的實收概念)兩個動作.業務上要順序發生並且必須要用數據庫進行持久化存儲。問題是創建支付單、寫支付結果這兩個動作實際流程裏因爲中間涉及用戶交互,延時掉單等問題往往存在支付結果先有,而創建支付單延時的情況或者創建支付單的很久以後支付結果才通過別的方式寫入(通過銀行發異步接口回調,對賬單核對)。
首先交易系統層的小夥伴已經把支付結果和支付單放到不同的表內,做insert操作而不是update。其次是如果入庫操作出現異常他們首先也會入緩存,等數據庫情況變好後再調度入庫。等同一筆支付的支付單、支付結果都存在緩存或數據庫時再發起下級非實時業務。
而在支付網關的場景是:調用交易系統出現網絡失敗,寫入延時較高的情況下先斷掉與交易系統的交互自行發送保存創建支付單和支付結果到緩存和本地。是由於取消了數據庫存儲,不使用掃描庫的方式。而是使用java的io事件selector進行。通過監聽SelectionKey.OP_READ事件,根據同一個payid到本地文件和緩存內進行條件判斷。判斷創建支付單、寫支付主任務都成功後再發送消息。


緩存雙備雙切

在研發體系內有兩個自主研發類似redis的緩存系統:JIMDB和R2M,我們同時採用。目的是預防其中中一個出現問題能自動或立即切換到另外一個,採用主從異步模式進行互切:
1、寫入時同步寫數據到主緩存,異步寫數據到從緩存。
2、讀取時採用先從主緩存讀取,出現異常和超時再在從緩存中讀取。如果主緩存使用寫入失敗,立即調整主從對應的實際緩存。
其實只需要在set和get的時候加一箇中間層,與Concurrent框架裏的Executor的newCachedThreadPool(ThreadFactory threadFactory)類似,這個結構和實現比較簡單:

wKiom1lfBJGigE8BAAUXwTVZ8mw366.jpg

未完待續

那上篇就到此結束了,下篇將會重點介紹基於這些技術的上層應用功能:
爲取消數據庫依賴而使用的自行收單、補單功能。
爲增加併發量而使用的異步交互功能。
最重要的爲預防支付清算機構掛掉而使用的路由分流功能。
爲保證下級系統進行重構使用的切量平移功能。
以及爲保障歷次618,雙11活動提前進行的保障和洪峯消解工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章