spring cloud gateway核心流程


 

一、      網關種類

流量型網關和業務型網關,也是自己的一個理解,流量型網關可以通常看成是nginx,kong這種更加專注於高性能進行流量分發,業務相對簡單,但是對於“復雜”型業務網關,尤其系統實現使用的是java,那么使用openresty這種無疑是加大了研發成本,而且不利於調試和定位問題,畢竟需要通過規定統一接口來進行交互。

二、      網關產生背景

  1. 客戶端會多次請求不同的微服務,增加了客戶端的復雜性。
  2. 認證復雜,每個服務都需要獨立認證。
  3. 項目的迭代,可能需要重新划分微服務,這樣客戶端調用的邏輯會有調整,導致級聯客戶端的調整,給整個系統帶來很大的麻煩,牽一發而動全身
  4. 提取通用業務到網關可以使具體的服務更專注於業務本身,通用業務如:統一身份認證,統一的數據轉換處理,定向轉發,以及負載均衡策略,限流,訪問日志。

三、      網關技術

目前市場比較的成形網關有zuul(1.x,2.x)| spring cloud gateway | nginx | kong|other

能夠支撐網關的核心技術點就是能夠搞定高tps即可!

    說到這里核心的關注點來了,高tps的支撐就是要快速的去處理請求,快速的接收到請求,不能因為服務器資源的問題而拒絕請求,或造成在底層操作系統級別的排隊阻塞。

    這個問題讓我們想到了nodejs,而nodejs正是基於事件分發機制的reactor模型的實現,是異步非阻塞的。

    相比於傳統的阻塞IO,異步非阻塞接受請求只需要一條線程即可。是的,這個線程只進行請求的接收,收到后會保存請求到一個指定的位置,然后會由一個looper來進行請求的獲取,

   交給請求處理器去處理,這里的請求處理器可以理解成一個線程池的機制。

四、      Spring家族的網關

Spring cloud gateway(下面簡稱gateway) 是spring cloud在進一步放棄了zuul 1.x后的新作。也是spring自己的東西,相比於外部依賴更加的可控些。

SpringBoot 2.2.2. RELEASE,SpringCloud Hoxton.SR1

 

Release Train

Boot Version

Hoxton

2.2.x

Greenwich

2.1.x

Finchley

2.0.x

Edgware

1.5.x

Dalston

1.5.x

  1.  
  2.  

五、      Gateway啟動時的自動配置

學習一個項目,或者一個技術的關鍵點在於,了解這個項目的運轉過程,了解項目的結構,別一下進入到細節中,也不要僅僅停留於最簡單的demo中。

l  GatewayAutoConfiguration  網關基礎配置類,當中承載着核心的配置邏輯

l  GatewayClassPathWarningAutoConfiguration  網關類加載配置類,就是用於校驗是否加載的時webFlux依賴,而不是普通的web依賴。

l  GatewayLoadBalancerClientAutoConfiguration  網關客戶端負載均衡配置類

l  GatewayRedisAutoConfiguration   網關限流器配置類

我們在啟動spring boot的時候基本都會使用@EnableAutoConfiguration注解,那么當你引入gateway項目的時候上面的三個配置類就會被加載。

首先看GatewayAutoConfiguration  中的NettyConfiguration,這個類為初始化netty通信的一系列流程,分別注冊了3個bean。這里只是拋磚引玉,里面詳細的配置做了哪些事情,可以自己找下gateway代碼對號入座。

       GatewayAutoConfiguration 作為基礎配置,內部又注冊了這些bean

a)   NettyConfiguration

b)   GlobalFilter

c)   FilteringWebHandler

d)   GatewayProperties

e)   PrefixPathGatewayFilterFactory

f)   RoutePredicateFactory

g)   RouteDefinitionLocator

h)   RouteLocator

i)   RoutePredicateHandlerMapping

j)   GatewayWebfluxEndpoint

六、      Gateway核心概念

  • Routepredicategatewayfilter,globalfilter
  1. Route可以看成是一個請求服務器資源的對象,里面包含着請求信息,下面我會列出屬性和關鍵方法,當然里面包含Predicate以及filter。

那么在使用的時候需要給route對象中指定屬性,uri,path參數,那么predicate用來檢查是否合規。

Route對象屬性:

屬性

含義

private final String id;

路由編號

private final URI uri;

即將路由向的 URI

private final int order;

路由順序

Predicate <ServerWebExchange> predicate;

校驗訪問信息否合規,調用了包裝對象的test方法,因為Predicate本身也有test方法,在gateway中又做了擴展,接口名稱為:GatewayPredicate,還要注意,該字段為數組,可add操作bool表達式。

List<GatewayFilter> gatewayFilters;

過濾器鏈

以上的屬性是通過讀取配置文件得來的,或者使用Routes.locator()進行對象鏈式創建。目前這個階段可以理解成一個請求信息收集。

  1. Predicate,在gateway-core包的handle中,以一個時間的Predicate來舉例,AfterRoutePredicateFactory用來校驗在某一時間點后生效的斷言。

Predicate也很好解釋,java8中的Predicate函數式關鍵字實質也是一個判斷條件,滿足條件即放行。而Route其內部是包含了關於ServerWebExchange的Boolean表達式。

在請求到了gateway,是通過DispatchHandle來進行處理的,它會去匹配HandlerMapping,gateway實現了一個RoutePredicateHandlerMapping;在這個類中的核心

方法是getHandlerInternal,這個方法中去判斷當前斷言是否通過,核心方法是調用每個路由斷言的test方法,代碼如下:

.filter(route -> route.getPredicate().test(exchange))

   

如果路由斷言條件沒有通過,則lookupRoute(ServerwebExchange)方法,返回空的集合。后面的邏輯不會在執行。

   

 

   

 

 

如果我們想自己定義一個predicate,按照官方的做法繼承AbstractRoutePredicateFactory即可,不過目前原始提供的已經比較豐富,或許不用我們擴展!如果需要擴展應該想下我們的方案是否出現在了正確位置

   那么如果能通過predicate,就會調用Mono.just(webHandler)方法繼續后面的FilteringWebHandler,而這個類中會持有全局的過濾鏈。

  1. gateway中的網關還有一個重要的成員就是filter,分為gatewayFilter和globalFilter兩種,下面詳細解釋下過濾器的相關問題,在此之前先打個感嘆號!

        

 

  


  1. 請求接入filter

先說下請求接入的整個過程吧。這里就會涉及到gateway本身提供的全局過濾器。如有針對路由的過濾器也會根據order方法返回值順序,與globalFilter進行統一排序。

   

 

 

 

請求實際走過handle的順序

         i.      HttpWebHandleAdapter;

       ii.      DispatcherHandle;負責轉發到具體的請求處理器;

      iii.      RoutePredicatehandlerMapping;匹配處理器后進行route的斷言,成功則取執行過濾鏈,否則直接response;

       iv.      FilteringWebHandle;這個handle中初始化了9個spring全局的globaFilter (有一個是自定義的)

   

 

 

DefaultGatewayFilterChain 用來處理filter過濾鏈的關鍵類,該類持有了filter鏈;請求在與路由匹配時,FilteringWebHandler組件創建的時候會將所有的 GlobalFilter 構建一個GatewayFilterAdapter,而該對象僅持有GlobalFilter接口方法,在轉換成OrderGatewayFilter這樣也持有了getOrder方法,根據getOrder方法的返回值順序組成ArrayList。

   

在FilteringWebHandler這個類中很關鍵,如果你有自定義的globalfilter那么就會加入到這個ArrayList中,首次入過濾鏈是通過WebClientWriteResponseFilter這個過濾器,因為這個過濾器中包含了請求和響應的全狀態。整個過濾鏈都是在這個過濾器中進行的,代碼如下:

   

    Lambda表達式中是處理響應階段的,而chain的filter方法就是在循環ArrayList進行filter的執行;如果你在自定義filter中放行,並繼續執行下面的filter那么會在代碼中調用chain的filter,如果確定結束,那么需要返回一個Mono對象,由Mono對象去執行then方法,取進行響應內容的操作,最后writeWith到客戶端。

系統提供的重要的全局過濾器:

  • RemoveCachedBodyFilter order為-2147483648

清除exchange的attributes中cachedRequestBody值。這個key的名稱來自exchangeUtile中CACHED_REQUEST_BODY_ATTR = "cachedRequestBody";

  • AdaptCachedBodyGlobalFilter order為-2147483648+1000

作用是從exchange的attributes中獲取cachedRequestBody屬性值作為request的body,注意使用此功能首先必須預設cachedRequestBody屬性至attributes中。

  • NettyWriteResponseFilter order為-1

NettyWriteResponseFilter將結果數據流寫入ServerHttpResponse中發生在NettyRouting獲取到遠程調用的結果數據流之后,當NettyRouting拿到結果數據流之后會將其寫入當前請求exchange的attributes中。

  • ForwardPathFilter order-0

處理uri為forword開頭的服務地址,形如:forword://xxxxxx.com,否則也忽略。

  • RouteToRequestUrlFilter order為10000

過濾器RouteToRequestUrlFilter是必須的全局過濾器,主要任務是將原始的url請求根據route中配置的uri,將請求的具體資源信息組合到一起,形成一個真正往后端服務的請求,將真實的請求url路徑,配置到exchange中attribute的Map中,key為“包全名.ServerWebExchangeUtils.gatewayRequestUrl”,直接發送到下一個過濾器,如果為lb://模式則會通過LoadBalancerClientFilter進行處理。

  • LoadBalancerClientFilter(我們可以在此處做自定義負載均衡)

LoadBalancerClientFilter負責服務真實ip的映射,主要針對對個服務節點的情況進行負載均衡,默認采用的netflix-ribbon作為負載均衡器,首先如果scheme不是服務節點映射的話直接過濾,獲取服務節點,choose函數是真實負載均衡發生的函數,獲取一個本次選出的服務server instance(如果是單節點則無負載計算),然后將服務的真實ip+port替換掉path中的lb://{serviceId}前綴。實際就是拿到一個能夠真實請求的地址。那么這個過濾器如果不是lb://servername

則該過濾器也直接忽略

  • WebsocketRoutingFilter 

過濾器實現了gateway對於websocket的支持,內部通過websocketClient實現將一個http請求協議換轉成websocket,如果uri不是ws開頭的這種則不起作用,

ws://xxxx.com或者wss://zxxxxxx.cn

   

  • NettyRouting   order為2147483647

NettyRouting獲取到遠程調用的結果數據流會將其寫入當前請求exchange的attributes中,發送回DispatchHandle,又webflux處理。

  • ForwardRoutingFilter  order為2147483647

最終將exchange交還給Webhandler做http請求處理,已經准備返回數據給客戶端(如果是forward則會發送到gateway本地的控制器處理)。

   

七、      關於網關做統一認證的問題

  • 讀取requestBody

gateway用於統一請求信息校驗。我們可以校驗header中的信息,通過exchange來獲得,如果是一個post請求我們有時也需要校驗請求體body的合法性。

每個filter中都持有exchange對象,獲取header的時候使用exchange.getRequest().getHeaders();

那么現在如果想獲取requestBody呢。你會看到網上不天蓋地的各種文章,針對各種版本進行處理。拿出一種方式來舉個反例:

照貓畫虎:exchange.getBody()獲取出來的是Flux<DataBuffer>對象,我們知道fulx使用訂閱方法可以取出body,但是如果請求體過大使用sub方法沒法取出。

因為sub方法只能取出發過來的第一份元素。 見了網上的各種hack方式,如果我們僅僅需要校驗一個requestBody內容,則只需要在builder.routes()構建每個具體的route對象時對predicate進行readBody的設置,這里需要傳入一個參數,是body的傳入類型。

   

這樣在gateway啟動后,一個請求過來就會去匹配我們事先定義好的route對象。嗯,我們來看下route方法的第二個參數,Function<PredicateSeqc>類型。

   

我們找到這個類,這里如果使用了readBody則會調用ReadBodyPredicateFactory的applyAsync方法,該方法為讀取body的核心操作。

   

進入applyAsync方法后我們會看到關鍵的對請求信息做put.attribute的操作,key為一個工具類中的常量,進入方法首先判斷是否有緩存,然后我們想到了FilteringWebHandle中的第一個全局過濾器RemoveCacheBodyFilter,所以我們在這里一定能夠進入到else。那就是使用exchangeUtil中的cacheBody方法,最后將body獲取並存儲ccHashMap。

   

到這里我們看到了一個整體的reqbody的緩存流程,接下來可以在任意的filter中取出使用。

寫到這里我有個小想法,還是想把這個predicate的readBody用filter頂替掉,因為在斷言表達式成功了以后就可以進行緩存了,我們需要body可以隨時拉出來校驗,這個方法不會花費較長時間。不然每個路由都需要配置一次,實在是麻煩!

后來看了下gateway平台提供了一個modifiyRequestBody的全局Filter。經過改造(去除了一些修改請求的操作,僅僅是將原來的請求body訂閱出來,緩存起來,然后構建一個新的exchange對象),order優先級以-2147483647+10排序時機執行,選擇這個時機執行因為它處於removeCacheBody和AdapCachaeBody兩個過濾器之間。即使有人使用了predicate的readBody(String.class,b-> true)方式,那么在AdapCachaeBodyGlobaleFilter全局過濾器中我們仍然遵循默認的gateway原則去執行,map中緩存的key都是spring gateway項目提供的,所以沒有沖突。

   

  https://www.cnblogs.com/zzq-include/p/12944680.html

  這里敲下黑板!!!!!!,可以后面關注下這個readBody方法。

  

  • 修改requestBody

修改requestBody,由於上面的readBody的提示,我們自然而然的就想到了應該也有一個類似方法來控制,在這里我們需要注意下readBody是以predicate的形式出現的,而modifyBody是以過濾器的身份出現的,非全局過濾器。

   

如果我們需要全局對每一個請求Body都可能有監控修改的需求,建議按照modifyRequestBodyFilterFactory的內容,自己定義一個全局過濾器這樣也免去了配置的麻煩。

八、      還沒想好

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM