您好,登錄后才能下訂單哦!
Java中怎么設計應用層網關,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
Java 應用層網關的必要性
我們的 Java 網關分為應用層網關和業務嵌入式網關兩部分,架構圖如下
在這里插入圖片描述
Java 網關分為核心網關和業務嵌入式網關服務兩部分,主要工作原理如下
接入層流量首先進入 Java 核心網關,經過一系列的 pipeline 處理(風控,路由協議轉換、流控、降級等操作)后發起泛化調用再打入業務層網關
業務層網關也會經過一系列的 pipeline(接口校驗,驗簽,session 校驗等)進入最終的業務邏輯,然后再調用相關 dubbo 服務最終完成本次 Java 請求的響應。
核心網關與嵌入式業務網關的功能如下
在這里插入圖片描述
其中嵌入式網關是以 jar 包的形式集成到業務的工程里的,具體為啥要這樣設計,后文會詳述。
首先來看 Java 網關為啥要分成核心網關和嵌入式業務網關兩部分,直接從接入層打到業務網關不是更省事嗎,何必多此一舉再加一層核心網關,多加一層不是多了一個損耗嗎。
這里有三個原因
鴻蒙官方戰略合作共建——HarmonyOS技術社區
核心網關主要起著風控,鑒權、路由協議轉換、流控、降級,打點統計(請求報錯等)等作用,這些功能對每一個層請求來說都是通用的,統一將這些功能抽離放在核心網關實現更合理。
當然了,可以統一把第一點所述的這些功能放在接入層實現,但這樣會讓接入層顯得很臃腫,另外第一點中有一個很重要的功能,路由協議轉換(將 http 轉成 dubbo),由于我們的接入層用的是 OpenResty,它是不支持這種協議轉換的,除非基于 OpenResty 做二次開發,這樣費時費力,也無必要,這樣看來抽出一個 Java 核心網關來擔任第一點所述的功能是更合理的,計算機界不有一句話么:任何問題,在計算機界都可以通過加入一個中間層來解決。加一個 Java 核心網關符合單一職責,分層的設計理念。
加入一個核心網關,確實多了一層,也多了一個損耗,不過核心網關并不處理具體的邏輯,它主要起著流量轉發的作用,而且在下文我們可以看到,它采用了 webflux 這種反應式編程框架,帶來的損耗比起引入它帶來的優勢可以忽略不計。
接下來我們簡單談談核心網關和業務網關的設計思路。
核心網關技術選型
同步阻塞 VS 異步非阻塞
上節介紹可知 Java 核心網關承擔著所有的流量入口,本身會調用大量的業務接口(打到業務網關里),所以 IO 操作會很頻繁,在技術選型上是有要求的, 首先來看看傳統的 Spring MVC(servlet 3.0之前)
很明顯它是同步阻塞的, 一個請求需要對應一個 Servlet Thread 來處理,當有 DB,網絡 IO 時,此線程會阻塞,可想而知用這種方案線程很快會占滿,導致系統不可用。
顯然我們應該采用異步非阻塞的編程模型,它是如何工作的呢,如下圖示
工作原理如下
鴻蒙官方戰略合作共建——HarmonyOS技術社區
只有一個 request 線程負責 accept 所有的請求,每個請求都有一個 Event handler 和回調,request 線程接收到 request 請求后,首先會為此請求在 Event Loop 中注冊一個回調函數,緊接著馬上把這個請求丟給線程池中的某個線程處理,然后此 request 線程立馬返回,馬上就可以處理另外的請求了。
線程池中的線程處理完請求的 Event Handler(DB,網絡IO等邏輯) 后,會去調用之前注冊好的回調函數返回請求結果
從以上的工作原理可以看出,負責處理請求的 request 線程只需求一個,線程數大大減少!更少的線程意味著更高的內存利用,也意味著線程間的切換開銷大大減少!所以顯然應該使用這種編程模型。
打個簡單的比方,相信大家都有去酒店就餐的經歷,對于酒店來說,怎么才能最大化地提高接客效率呢
鴻蒙官方戰略合作共建——HarmonyOS技術社區
一種方式是對每一個客人,都安排一位接待員,這名接待員負責客人的接待,入座,上菜等所有流程,顯然如果這樣安排的話有多少位客人就等安排多少位接待員。
第二種方式是只安排一位接待員,這名接待員在接待客人入座后,立刻回到門口迎接客人,剩下的交給上菜服務員(線程池工作),這樣的話接待員的人數就大大減少了,能極大地提升效率。
最終我們選擇了 Spring WebFlux 這種反應式(Reactive),基于事件驅動的異步非阻塞框架。
反應式編程與 Spring WebFlux 簡介
反應式編程簡介
反應式編程 (reactive programming) 是一種基于數據流 (data stream) 和 變化傳遞 (propagation of change) 的 聲明式 (declarative) 的編程范式。它是一種編程思想,能夠基于數據流中的事件(變化)進行相關反應處理,舉個簡單的例子:在 a = b + c 這個語句中,要得到 a 的值,如果用傳統的編程模型,每次 b 或 c 變化后都需要重新計算以獲得 a,而在反應式編程中,我們把 b,c 當作數據流,a 會對 b,c 作出的變化實時響應。
反應式編程有以下幾個特點
1、事件驅動
在事件驅動的程序中,組件之間通過松藕合的生產者(也稱被訂閱者,即 Publisher)和訂閱者模式(Subscriber)來實現,這些事件是以異步和非阻塞的方式來接收和發送的,基于事件驅動的編程有啥好處呢,簡單地說它是依靠推模式而不是拉模式來動作的,也就是說只有生產者有消息(變化)時才會通知消費者作出響應,也就意味著消費者不需要輪詢也不需要等待數據。
2、實時響應
以我們的網關為例, request 線程接收請求后,快速返回存儲結果的上下文,把具體執行交給線程池里的線程(可以認為是后臺線程),處理完成后,異步地將調用結果封裝到結果的上下文中,可以看到此過程是完全異步的,也就是說實時響應必須通過異步編程實現,在 Java 8 中,發起調用后可以快速返回 CompletableFuture 對象。
3、彈性機制
事件驅動的松散耦合提供了組件在失敗下可以抓獲完全隔離的上下文場景,作為消息封裝,發送到其他組件時,在具體編程時可以檢查錯誤比如是否接受到,接受的命令是否可執行等等,并決定如何應對。
反應式編程主要工作流程如下
鴻蒙官方戰略合作共建——HarmonyOS技術社區
被訂閱者主動推送數據給訂閱者,在異步或完成時觸發另外的兩個方法
被訂閱者發生異常,會觸發 onError
所有的推送完成無異常,最終會執行 onSuccess 方法
還有一個問題,如果 Publisher 發送消息過快超過 Subscriber 的處理速度了怎么辦,所以就得提一下背壓(BackPressure)的概念了,知乎網友扔物線對此概念解釋我認為非常到位:
backpressure 是源自工程學中的概念:在管道運輸中,氣流或液流由于管道突然變細、急彎等原因導致由某處出現了下游向上游的逆向壓力,這種情況稱為「backpressure」,相應的在反應式編程中,在數據流從上游生產者向下游消費者傳輸的過程中,上游生產速度大于下游消費速度,導致下游的 Buffer 溢出,這種現象就叫做 Backpressure 出現,這里的重點在于「Buffer 溢出」,為什么需要 buffer, 因為 Publisher 生產速度大于 Subscriber 的消費速度,所以需要 Buffer, 因為外部條件限制,顯然 Buffer 是有上限的,如果生產速度超過 buffer, 則 backpressure 產生,超過 buffer 的話,唯一的選擇就是丟掉新事件。
這就好比,比如你的 server 只能承受 5000~6000 的請求,如果你把 buffer 設置為 5000,則一旦請求數超過 5000,則背壓產生,超過的請求數丟棄,這樣保證了機器不會被源源不斷的 Publisher 生產事件壓垮,有效提升了網關的可用性。
Spring WebFlux 簡介
為了更好地促進反應式編程的應用,在 Java 平臺上,Netflix(開發了 RxJava)、TypeSafe(開發了 Scala、Akka)、Pivatol(開發了 Spring、Reactor)共同制定了一個被稱為 Reactive Streams 項目(規范),用于制定反應式編程相關的規范以及接口。
Reactor 基于 Reactive Stream 定制了一套反應式編程框架,而 WebFlux 則是以 Reactor 為基礎實現了 Web 領域的反應式編程框架,由于反應式編程的異步非阻塞特性,所以 WebFlux 運行于 Netty , Undertow 等支持異步編程模型的 server 之上,當然也可運行于支持 Servlet 3.1 的 Server 容器上(Servlet 3.1 開始支持異步)
如圖示,左側是傳統的 Spring MVC 結構, 右側是 webflux 組件。
為了讓大家更好利用 webflux 編程,Spring 貼心地兼容了 @Controller 等 Spring MVC 的注解在 webflux 的使用,能讓使用者更好地過渡到 webflux 編程中來,不過在底層實現中,與 Spring MVC 的實現的請求 InputStream 和響應 OutputStream 不同,webflux 實現了一套反應式的請求(ServerHttpRequest) 和響應(ServerHttpResponse),這兩個類將請求體與響應體以 Flux(Flux 下文會簡單介紹下)的形式暴露出來,同時 webflux 底層也實現了基于 Flux的 JSON,XML 的序列化和反序列化,HTML 實圖的渲染,Server 發送事件等。
通過介紹可以看到 webflux 實現了從請求到響應,到渲染,事件發送等一整套反應式事件的支持,是的,要最大程度地發揮 webflux 的性能,中間所有的事件都應該以 Mono 或 Flux 響應式事件流的形式存在!
WebFlux 的底層實現其實是基于 Reactor 實現的,在 Reactor 的核心類中,以下兩個類代表了發布者
Mono: 代表 0 到 1 個元素的發布者
Flux:代表 0 到 N 個元素的發布者
這玩意怎么用呢,如下圖示
@RequestMapping("/demo") @RestController public class DemoController { @RequestMapping(value = "/foobar") public Mono<Foobar> foobar() { return Mono.just(new Foobar()); } }
本來是要返回 foobar 對象的,結果最終以 Mono(或 Flux)的形式存在,這樣就構建了響應式編程中的生產者(Publisher),再調用 subscribe 即可完成對生產者的監聽消費。
在我們的網關設計中,當收到請求后,使用了 Mono 來充當發布者,如果中間出現了問題,會調用 onError, 最終成功后會調用 onSuccess,以下是網關實現采用的總體框架。
圖中 Mono.empty 代表創建一個不包含任何元素,只發布消息的隊列。發送消息后,會在線程池里處理網關的 slot ,最后處理成功后會調用 onSuccess 方法,處理失敗則會調用 onError。下一節我們來看看這些網關 slot 是如何處理的。
網關的責任鏈設計
不管是核心網關還是嵌入式網關我們都采用了責任鏈模式來實現網關的核心處理流程,將每個處理邏輯看成一個slot,每個 slot 按照預先設定的順序先后執行,與開源kong,zuul等類似,我們也采用了PRPE模式(Pre、Routing、Post、Error)
Pre 階段:
initParamsSlot 初始化組裝請求上下文參數
sentinelSlot 流控組件引入 ,做集群限流、降級、熔斷使用
riskSlot 風控處理
Route 階段:
dubboSlot 通過 dubbo 泛化調用轉換成 dubbo 協議進行遠程調用
POST Slots: 后置處理
APMMonitorSlot APM 監控處理,請求出錯等打點監控
采用這樣的設計方式,各個 slot 各司其職,也有較好的可擴展性,如果還想加什么 slot,定義好此 slot 功能,指定好其在調用鏈中的位置即可。
需要注意的是有些 Slot 的請求結果依賴于前面 Slot 的執行結果,這種情況下需要對前面的執行事件用 Mono 的形式封裝起來,這樣這些 slot 就構成了一個個的響應式事件流,保證了這些 Slot 都是異步執行的,不會阻塞主線程。
另外注意高亮的 dubboSlot 階段,在 dubbo 2.7 之前 dubbo 底層返回 Future(會一直占用一個線程輪詢結果),對異步編程不友好,2.7 之后返回了 CompleteFuture,與 webflux 的異步編程模型完美結合(發起調用嵌入式網關后立馬返回,等調用完成后才執行,是真正的異步)。
嵌入式網關設計
首先我們要明白為啥會有嵌入式網關的需求,主要有三個原因
目前有 H5, 小程序,app 端,各端的 session 存儲不一樣,需要根據請求的各端來查找 session 對應的 uid,這個操作顯然應該在網關層面來做,放在嵌入式網關來實現更合理
每個請求進入業務層之后,我們需要對其時間戳,app 簽名,小程序簽名等進行校驗,這些校驗對每個端的請求都是必要的,所以顯然應該在網關來做
有些業務需要在執行業務前后做一些擴展,比如執行前后需要打點分析等,對擴展的實現網關也應該支持
那么嵌入式網關如何實現呢,業務服務是以 dubbo 服務的形式存在的,而在 dubbo 中有一個 Filter 機制,是專門為服務提供方和服務消費方調用過程進行攔截設計的,每次遠程方法執行,該攔截都會被執行。這樣就為開發者提供了非常方便的擴展性,所以嵌入式網關的主要設計思路就是自定義 dubbo 的 filter,然后在此 filter 中執行相關的擴展邏輯即可,偽代碼如下:
這樣通過自定義 filter 的方式我們解決了擴展性的問題,注意我們使用了Activate注解,這樣 dubbo 就會把注釋的Filter 作為 dubbo 原生的 Filter 自動加載,而不需要顯示的配置 provider 或者 consumer 的 filter,也就避免了對代碼的侵入性。
這里的業務邏輯執行前后的擴展也是通過責任鏈的模式來執行一個個的的 slot, 我們先定義好時間戳校驗,簽名校驗,Session轉id等 slot, 然后在 xml 中指定這些 slot 的執行順序
每個業務都有一個 gateway.xml 文件,可以在此文件中配置 H5, app, 小程序需要執行的 slot。
以對 app 請求配置需要執行的前置 slot 和后置處理 slot 為例 ,偽代碼如下
這樣只要在啟動函數中引入(ImportResource)需要支持的 gateway 的 xml 文件,配置的 bean 就能生效,然后在 filter 中會分別取 bizChannel(請求必傳,代表是業務哪一端標識,如 biz_h6, biz_app, biz_小程序)對應的 slotBizList 即可執行業務邏輯前后的擴展。
通過這樣的方式就有效地指定了業務邏輯執行前后需要執行的 slot,每個業務如果想在業務邏輯執行前后進行擴展,只要定義好自己的 slot 邏輯,在 xml 文件中指定此 slot 的位置即可生效。
嵌入式網關按以上思路實現后,就通過 jar 包分發到各個業務系統。好處是:穩定性提升,每個業務集成一個穩定版本的網關 Jar,某一個業務系統做網關 Jar 升級時,其他業務系統都不受干擾
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。