歷經4年618流量暴擊,京東京麥開放平臺的高可用架構之路
京麥是京東商家的多端開放式工作平臺,是京東十萬商家唯一的店鋪運營管理平臺。
它為京東商家提供移動端和桌面端的操作業(yè)務,京麥本身是一個開放的端體系架構,由京東官方和 ISV 為商家提供多樣的應用服務。
京麥開放平臺是京東系統(tǒng)與外部系統(tǒng)通訊的重要平臺,技術架構從早期單一 的 Nginx+Tomcat 部署,到現(xiàn)在的單一職責,獨立部署,去中心化,以及自主研發(fā)了 JSF/HTTP 等多種協(xié)議下的 API 網關、TCP 消息推送、APNs 推送、降級、限流等技術。
京麥開放平臺每天承載海量的 API 調用、消息推送,經歷了 4 年京東 618 的流量洗禮。
本文將為您揭開京麥開放平臺高性能 API 網關、高可靠的消息服務的技術內幕。
高性能 API 網關
京東內部的數(shù)據分布在各個獨立的業(yè)務系統(tǒng)中,包括訂單中心、商品中心、商家中心等,各個獨立系統(tǒng)間通過 JSF(Jingdong Service Framework)進行數(shù)據交換。
而 API 網關基于 OAuth2 協(xié)議提供,ISV 調用是通過 HTTP 的 JSON 協(xié)議。
如何將這些內部數(shù)據安全可控地開放給外部 ISV 進行服務調用,以及如何快速地進行 API 接入實現(xiàn)數(shù)據報文轉化,在這個背景下 API 網關誕生。
API 網關在架構設計上采用了多層接口,到達網關的請求首先由網關接入層攔截處理,在接入層進行兩個主要環(huán)節(jié)的處理:
- 網關防御校驗:這里包含降級和限流,以及多級緩存等,進行數(shù)據正確性校驗。
- 網關接入分發(fā):網關分發(fā)會根據網關注冊中心的數(shù)據進行協(xié)議解析,之后動態(tài)構建調用實例,完成服務泛化調用。
API 網關是為了滿足 618 高并發(fā)請求下的應用場景,網關在服務調度、身份授權、報文轉換、負載與緩存、監(jiān)控與日志等關鍵點上進行了針對性的架構優(yōu)化。
API 元數(shù)據統(tǒng)一配置
API 的調用依賴對元數(shù)據的獲取,比如 API 的字段信息、流控信息、APP 密鑰、IP 白名單等、權限配置等。
在 618 場景下,元數(shù)據獲取性能是 API 網關的關鍵點?;?DB 元數(shù)據讀取是不可取的,即使對 DB 做分庫分表處理也不行,因為 DB 就不是用來抗量的。
其次,要考慮到元數(shù)據的更新問題,定時的輪詢更新會產生極大延遲性,而且空輪詢也是對系統(tǒng)資源的極大浪費,采用 MQ 廣播通知不失為一種解決辦法。
但 MQ 僅僅解決數(shù)據同步的問題,數(shù)據緩存在集群里服務如何保證數(shù)據一致性和數(shù)據容災,又極大的增加了系統(tǒng)復雜度。
所以綜合考慮服務器性能和網絡 IO 等因素,在 API 元數(shù)據讀取采用基于 ZooKeeper 的統(tǒng)一配置。
并自研實現(xiàn)多級緩存容災架構方案,從 ZooKeeper、內存和本地文件等進行多級緩存,同時支持數(shù)據變更時即時同步,以及系統(tǒng)宕機網絡異常等情況下的數(shù)據自動容災等策略。
以讀為例,網關首先從內存中讀取配置,如無數(shù)據,從 ZooKeeper 讀取,讀取后同步到內存,并異步保存本次快照。
如果 ZooKeeper 數(shù)據變更,通過監(jiān)聽 ZooKeeper 的 DataChangeWatcher 變更同步數(shù)據。如果 ZooKeeper 宕機,重啟服務器,系統(tǒng)還可以通過本地快照恢復最近一次的元數(shù)據配置。
TCP 全雙工的長鏈接會話通道
API HTTP 網關通過接口提供服務調用獲取請求數(shù)據的,而搭建客戶端與服務平臺的 TCP 網關的雙向通道,以保持客戶端與服務平臺的會話狀態(tài),則可以在 HTTP 網關基礎上提供更多、更靈活的技術實現(xiàn)和業(yè)務實現(xiàn)。
在業(yè)務服務調用上通過 HTTP 網關,在平臺服務調用上則通過 TCP 網關,實現(xiàn)平臺與業(yè)務解耦,并且平臺采用 TCP 通道還可以增加對平臺的控制力,在此背景下誕生了 TCP 網關。
TCP 網關采用長連接通道,實現(xiàn)全雙工會話。TCP 網關采用 Netty 作為 TCP 容器,在 Channel Pipe 中加載自定義 Channel Handler,構建 Container 容器。
并將每個 TCP Connection 封裝到一個 Session 會話中,保存在 Container 容器中,由 Container 容器構建 Session 會話層提供邏輯層請求調用。
自研構建 Session 會話層是因為 HTTP 屬于 OSI 的應用層,而 TCP 屬于 OSI 的傳輸層,面向連接的編程極大的增加了程序復雜度。
所以將 Connection 封裝在每一個 Session 會話里,再以微服務的方式提供服務調用,極大的精簡了 TCP 編程。
斷線重連
客戶端與服務端通過 TCP 長連接進行通信,但在中國復雜的網絡環(huán)境下,移動客戶端可能由于網絡抖動、弱網絡情況下,遭遇非正常網絡閃斷,如何處理斷開后的斷線重連,保證客戶端與服務端的通訊穩(wěn)定呢?
客戶端每通過 TCP 與服務端進行一次建連,都會在服務容器里創(chuàng)建一個 Session 會話,該會話保存 Connection 的句柄,對應 Netty 的一個 Channel 通道。
建連成功后,通過定時的心跳保持 Channel 屬于 Active 活躍。但客戶端進入弱網絡環(huán)境下,客戶端可能已經掉線,但并未向服務端主動發(fā)送關閉 Channel 請求,而服務端仍認為該 Channel 仍存活。
直到在由服務端的會話存活檢測機制檢測到 Channel 已經 InActive,才會由服務端銷毀該 Channel。
服務端的會話存活檢測是 5 分鐘一次,所以存在客戶端掉線后,在 5 分鐘內又重新建連,而這時服務端的建連邏輯,不是重新創(chuàng)建一個 Session,而是去尋找上一次的 Session,并更新標識存活。
具體的實現(xiàn)是在每次建連的 Channel 里存入 SessionId,當網絡閃斷后,判斷 Channel 是否存在 Session。
之所以實現(xiàn)是得益于 Netty 的 ChannelHandlerContext,可以存儲一個自定義屬性到 Channel 的上下文中。
當然,TCP 網關一定是集群,所以,斷線重連也是極有可能請求到不同的服務器上,而這種情況按照新 Connection 創(chuàng)建的 Session 處理,只有出現(xiàn)重連到同一服務器時,才需要考慮上述的處理邏輯。
Protobuf 數(shù)據交換格式
HTTP 網關基于 JSON 進行數(shù)據傳輸,JSON 是 key-value 的鍵值對通信協(xié)議,所以生成報文會很大,影響傳輸性能。
考慮到報文傳輸大小,在 TCP 網關中則通過 Protobuf 定義通信協(xié)議,提升數(shù)據傳輸效率。
Protobuf 支持 Java、Objective-C 和 C++ 等語言,現(xiàn)支持了京麥平臺 PC 桌面客戶端、移動 iOS 和 Android 客戶端基于 Protobuf 通過 TCP 與服務端進行通信。
多維度流量控制
由于各個 API 的服務能力不一致,為了保證各個 API 能夠穩(wěn)定提供服務,不會被暴漲的請求流量擊垮,那么多維度流量控制是 API 網關的一個重要環(huán)節(jié)。
目前 API 網關是采用令牌桶的方法,實現(xiàn)方式是 Guava RateLimter,簡單有效,再結合統(tǒng)一配置中心,可以動態(tài)調整限流閾值。不用重啟服務器即可實現(xiàn)快速限流策略調整。
在 API 網關里面還有一個設置,就是并發(fā)度,這個是方法粒度的,對每一個調用接口都有一個并發(fā)度數(shù)值設置。
而且是動態(tài)設置,也是通過 ZooKeeper 下發(fā)到每一個服務節(jié)點上。并發(fā)度的具體實現(xiàn)是通過 JDK 的 Semaphore。
高可靠的消息服務
API 網關提供 ISV 獲取數(shù)據,但實時數(shù)據的獲取,如果通過輪詢網關,大量空轉不僅非常的低效且浪費服務器資源。
基于此,開放平臺推出了消息推送技術,提供一個實時的、可靠的、異步的雙向數(shù)據交換通道,提升 API 網關性能。
AnyCall 和推送系統(tǒng)
AnyCall
負責接收各業(yè)務中心的訂單、商品、商家等消息,進行統(tǒng)一的消息過濾、轉換、存儲,及監(jiān)控和統(tǒng)計等。
各個過程中的消息狀態(tài),通過消息采集器存儲到 ElasticSearch 和 HBase 進行存儲。
推送系統(tǒng)
基于 Netty 作為網絡層框架,構建海量推送模型,使用靜默長連接通道,實現(xiàn)從消息接收、推送、確認,整個過程的完全異步化處理。
解耦消息接入層和消息推送層,消息接入層只負責Request-Response 和 Notice-Repley。
而消息解析、適配、推送等邏輯處理都全部由消息推送層處理,而消息接入層和消息推送層之間則有消息隊列異步進行通信。
半推半拉還是半推半查?
半推半拉
半推半拉模式中的“推”指的是由服務器推送 消息通知 到客戶端,“拉”指的是客戶端收到通知后再從服務器拉取 消息實體 到客戶端本地存儲。
其中消息通知發(fā)送的僅是一個命令關鍵字,這樣的設計是考慮消息推送可能存在丟失,通過拉取的方式,確保即使消息通知未送達,在下次消息通知觸發(fā)下的拉取也能把上一次消息拉取到本地。
采用的半推半拉,每次僅推送通知,推送量小,實時性高。
半推半查
后期京麥消息推送模式由“拉”改“查”,“查”指的是消息通知依舊推送,但客戶端收到消息通知后 不再拉取消息實體,僅更新消息未讀數(shù)和進行消息提醒等操作。
而消息內容則是由服務端進行云端存儲,采用輕客戶端,重服務端的架構方案,只有用戶點擊查詢消息時,才會按需進行數(shù)據查詢,在客戶端展示,但不存儲。
這種推送模式的改動主要考慮了客戶端拉取消息內容到本地存儲,占用資源,重裝之后客戶端會丟失消息,以及多端存儲的數(shù)據存在不一致等問題。
消息云端存儲基于 ElasticSearch 進行消息存儲,并根據業(yè)務類型區(qū)分索引,通過 Routing 優(yōu)化查詢性能,支持多維度進行查詢,性能穩(wěn)定。
消息確認
評估消息系統(tǒng)的一個核心指標是消息送達率。為保證每一條消息準確送達,為每條消息都會開啟一個事務,從推送開始,到確認結束,如果超時未確認就會重發(fā)這條消息,這就是消息確認。
由于互聯(lián)網環(huán)境復雜,消息超時時間不能設置太短,尤其在移動弱網絡環(huán)境下。在本系統(tǒng)中,超時設置為 10 秒。
我們通過實現(xiàn) Future 自定義 NotifyFuture,為每個下行通知分配一個 seq,并定義 NotifyFuture 的 timeout。
即每個下行通知分配一個 seq 存儲緩存中,等待客戶端回應這個應答,如果應答, 則從緩存移出這個 seq,否則等待超時,自動從緩存中被移出。
APNs 消息推送
iOS 在系統(tǒng)層面與蘋果 APNs(Apple Push Notification Service)服務器建立連接,應用通過 Socket 向 APNs Server 推送消息,然后再由 APNs 進行推送。
但是基于 Socket 的 APNs 協(xié)議是一種反人類的設計,在推送消息時存在很多問題。
鑒于此,對 APNs 推送服務進行重構,基于 Netty 構建了 HTTP2 協(xié)議的推送服務,支持同步和異步的推送方式。
解決 Channel 異常及 InActive 時重連等問題,保證 HTTP2 推送管道的問題,同時通過 IdleStateHandler 保持 HTTP2 長連接的心跳 。
總結和感悟
***,總結歷次的大促,京麥開放平臺在進行服務化架構的演進過程中,所面臨的技術難點,最重要的還是服務治理,即調用關系的梳理。
因為我們要打造的不是一個系統(tǒng),也不是一堆系統(tǒng),而是一個平臺生態(tài),能夠持續(xù)地提高系統(tǒng)的運營能力。
這里以“精打細算,大道至簡”這句話結束此次京麥開放平臺的分享。
張松然 ,2013 年加入京東,一直從事京東商家京麥開放平臺的架構設計和開發(fā)工作,熟悉大規(guī)模分布式系統(tǒng)架構。在 Web 開發(fā)、架構優(yōu)化上有較豐富的實戰(zhàn)經歷。有多年 NIO 領域的設計、開發(fā)經驗,對 HTTP、TCP 長連接有深入研究與領悟。目前主要致力于多端開放平臺技術架構的優(yōu)化與實現(xiàn)。