1.1 SpringCloud Gateway簡介
Spring Cloud Gateway是Spring Cloud的一個全新項目,基於Spring 5.0+Spring Boot 2.0和Project Reactor等技術開發的網關,它旨在為微服務架構提供一種簡單有效的統一的API路由管理方式。
Spring Cloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway的目標提供統一的路由方式且基於Filter鏈的方式提供了網關基本的功能,如:安全、監控/指標、和限流。
Spring Cloud Gateway底層使用了高性能的通信框架Netty。
1.2 Spring cloud Gateway特性
- 基於Spring Framework 5,Project Reactor和Spring Boot 2.0。
- 集成Hystrix斷路器。
- 集成Spring Cloud DiscoveryClient。
- Predicates和Filters作用於特定路由,易於編寫的Predicates和Filters。
- 具備一些網關的高級功能:動態路由、限流、路徑重寫
從以上的特征來說,和Zuul的特征區別不大。SpringCloud Gateway和Zuul主要的區別,還是在底層的通信框架上。
三大核心概念:
- Route(路由):路由是構建網關的基本模塊,它由ID,目標URI,一系列的斷言和過濾器組成,如果斷言為true則匹配該路由,目標URI會被訪問。
- Predicate(斷言):這是一個java 8的Predicate,可以使用它來匹配來自HTTP請求的任何內容,如:請求頭和請求參數。斷言的輸入類型是一個ServerWebExchange。
- Filter(過濾器):指的是Spring框架中GatewayFilter的實例,使用過濾器,可以在請求被路由前或者后對請求進行修改。
總結:web請求,通過一些匹配條件,定位到真正的服務節點。並在這個轉發過程的前后,進行一些精細化控制。predicate就是匹配條件,而filter,就可以理解為一個無所不能的攔截器。有了這兩個元素,再加上目標URI,就可以實現具體的路由了。
1.3 Spring Cloud Gateway和框架
Spring的Webflux的響應式編程不僅僅是編程風格的改變,而且對於一系列的著名框架,都提供了響應式訪問的開發包,比如Netty、Redis等。
SpringCloud Gateway使用的Webflux中的reactor-netty響應式編程組件,底層使用了Netty通訊框架。
1.3.1 spring Cloud Zuul的IO模型
SpringCloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是傳統的Servlet IO處理模型。
servlet由servlet container進行生命周期管理。container啟動時構造servlet對象並調用servlet init()進行初始化;container關閉時調用servlet destory()銷毀servlet;container運行時接收請求,並為每個請求分配一個線程(一般從線程池中獲取空閑線程)然后調用service()。
弊端:servlet是一個簡單的網絡IO模型,當請求進入servlet container時,servlet container就會為其綁定一個線程,在並發不高的場景下這種模型是適用的,但是一旦並發上升,線程數量就會上漲,而線程資源代價是昂貴的(上下文切換,內存消耗大)嚴重影響請求的處理時間。在一些簡單的業務場景下,不希望為每個request分配一個線程,只需要1個或幾個線程就能應對極大並發的請求,這種業務場景下servlet模型沒有優勢。
所以SpringCloud Zuul是基於servlet之上的一個阻塞式處理模型,即Spring實現了處理所有request請求的一個servlet(DispatcherServlet),並由該servleet阻塞式處理。因此SpringCloud Zuul無法擺脫servlet模型的弊端。雖然Zuul 2.0開始使用了Netty,並且有了大規模Zuul 2.0集群部署的成熟案例。但是,SpringCloud官方已經沒有集成該版本的計划了。
1.3.2 Webflux模型
Webflux模型替換了舊的Servlet線程模型。用少量的線程處理request和response io操作,這些線程稱為Loop線程,而業務交給響應式編程框架處理,響應式編程是非常靈活的,用戶可以將業務中阻塞的操作提交到響應式框架的work線程中執行,而不阻塞的操作依然可以在Loop線程中進行處理,大大提高了Loop線程的利用率。
Webflux雖然可以兼容 多個底層的通信框架,但是一般情況下,底層使用的還是Netty,畢竟Netty是目前業界認可的最高新能的通信框架。而Webflux的Loop線程,正好就是著名的Reactor模式IO處理模型的Reactor線程,如果使用的是高性能的通信框架Netty,者就是Netty的EventLoop線程。
1.3.3 Spring Cloud Gateway的處理流程
客戶端向Spring Cloud Gateway發出請求。然后在Gateway Handler Mapping中找到與請求相匹配的路由,將其發送到Gateway Web Handler。Handler再通過指定的過濾器鏈來將請求發送到實際的服務執行業務邏輯,然后返回。過濾器之間用虛線分開是因為過濾器可能會在發送代理請求之前(Pre)或之后(post)執行業務邏輯。
1.4 Spring Cloud Gateway路由配置方式
1.4.1 基礎URI一種路由配置方式
如果請求的目標地址,是單個的URI資源路徑,配置文件示例如下:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: url-proxy-1
uri: https://blog.csdn.net
predicates:
- Path=/csdn
說明:
- id:自定義的路由ID,保持唯一
- uri:目標服務地址
- predicates:路由條件,Predicate接收一個輸入參數,返回一個布爾結果。該接口包含多種默認方法將Predicate組合成其他復雜的邏輯(如:與,或,非)
上面配置的意思是,配置了一個id為url-proxy-1的URI代理規則,路由的規則為:當訪問地址http://localhost:8080/csdn/1.jsp時,會路由到上游地址https://blog.csdn.net/1.jsp
1.4.2 基於代碼的路由配置方式
轉發功能同樣可以通過代碼來實現,可以在啟動類GateWayApplication中添加方法customRouteLocator()來定制轉發規則。
@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("path_route", r -> r.path("/csdn") .uri("https://blog.csdn.net")) .build(); }
1.4.3 和注冊中心相結合的路由配置方式
在uri的schema協議部分為自定義的lb:類型,表示從微服務注冊中心(如Eureka),並且進行服務的路由。
server:
port: 8084
spring:
cloud:
discovery: locator: enabled: true #開啟從注冊中心動態創建路由的功能,利用微服務進行路由 gateway: routes: - id: seckill-provider-route #路由的ID,沒有固定規則但要求唯一,建議配合服務名 uri: lb://seckill-provider #匹配后提供服務的路由地址 predicates: - Path=/seckill-provider/** #斷言,路徑相匹配的進行路由 - id: message-provider-route uri: lb://message-provider predicates: - Path=/message-provider/**
application:
name: cloud-gateway
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8888/eureka/
注冊中心相結合的路由配置方式,與單個URI的路由配置,區別其實很小,僅僅在於URI的schema協議不同。單個URI的地址的schema協議,一般為http或者https協議。
1.5 Spring Cloud Gateway匹配規則
SpringCloud Gateway是通過Spring WegFlux的HandlerMapping作為底層支持來匹配到轉發路由,Spring Cloud Gateway內置了很多Predicates工廠,這些Predicates工廠通過不同的HTTP請求參數來匹配,多個Predicates工廠可以組合使用。
1.5.1 Predicate斷言條件介紹
Predicate來源於Java 8,是Java 8中引入的一個函數,Predicate接收一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將Predicate組合成其他復雜的邏輯(如:與,或,非)。可以用於接口請求參數校驗、判斷新老數據是否有變化需要進行更新操作。
在Spring Cloud Gateway中Spring利用Predicate的特性實現了各種路由匹配規則,有通過Header、請求參數等不同的條件來進行作為條件匹配到對應的路由。
SpringCloud內置的集中Predicate的實現,如下圖:
1.5.2 通過請求參數匹配
Query Route Predicate支持傳入兩個參數,一個是屬性名一個是屬性值,屬性值可以是正則表達式。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates: - Query=smile
這樣配置,只要請求中包含了smile屬性的參數即可匹配路由。
使用curl測試,命令行輸入:curl localhost:8080?smile=x&id=2
經過測試發現只要請求中帶有smile參數就會匹配路由,不帶smile參數則不會匹配路由。
還可以將Query的值以鍵值對的方式進行配置,這樣在請求過來時對屬性值和正則進行匹配,匹配上才會走路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Query=keep, pu.
這樣只要當請求中包含keep屬性並且參數值是以pu開頭的長度為三位的字符串才會進行匹配和路由。
使用curl測試,命令行輸入: curl localhost:8080?keep=pub
測試可以返回頁面代碼,將keep的屬性值改為pubx再次訪問就會報404,證明路由需要匹配正則表達式才會進行路由。
1.5.3 通過Header屬性匹配
Header Route Predicate和Cookie Route Predicate一樣,也是接收2個參數,一個header中屬性名和一個正則表達式,這個屬性值和正則表達式匹配則執行。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Header=X-Request-Id, \d+
使用curl測試,命令行輸入:
curl http://localhost:8080 -H "X-Request-Id:88"
則返回頁面代碼證明匹配成功。將參數-H "X-Request-Id:88"改為-H "X-Request-Id:spring"再次執行時返回404證明沒有匹配。
1.5.4 通過Cookie匹配
Cookie Route Predicate 可以接受兩個參數,一個是Cookie name,一個是正則表達式,路由規則會通過獲取對應的Cookie name值和正則表達式去匹配,如果匹配上就會執行路由,如果沒有匹配上則不執行。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Cookie=sessionId, test
使用curl測試,命令行輸入:
curl http://localhost:8080 --cookie "sessionId=test"
則會返回頁面代碼,如果去掉--cookie "sessionId=test",后台會報404錯誤。
1.5.5 通過Host匹配
Host Route Predicate接收一組參數,一組匹配的域名列表,這個模板是一個ant分隔的模板,用.號作為分隔符。它通過參數中的主機地址作為匹配規則。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Host=**.baidu.com
使用curl測試,命令行輸入:
curl http://localhost:8080 -H "Host:www.baidu.com"
curl http://localhost:8080 -H "Host:md.baidu.com"
經測試以上兩種host均可以匹配到host_route錄音,去掉host參數則會報404錯誤。
1.5.6 通過請求方式匹配
可以通過POST、GET、PUT、DELETE等不同的請求方式來進行路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Method=GET
使用curl測試,命令行輸入:
# curl 默認是以GET的方式去請求
curl http://localhost:8080
測試返回頁面代碼,證明匹配到路由,再以POST的方式請求測試。
curl -x POST http://localhost:8080
返回404沒有找到,證明沒有匹配上路由。
1.5.7 通過請求路徑匹配
Path Route Predicate接收一個匹配路徑的參數來判斷是否走路由
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: http://ityouknow.com
order: 0
predicates:
- Path=/foo/{segment}
如果請求路徑符合要求,則此路由將匹配。
使用curl測試,命令行輸入:
curl http://localhost:8080/foo/1
curl http://localhost:8080/foo/xx
curl http://localhost:8080/boo/xx
經過測試第一和第二條命令可以正常獲取頁面返回值,最后一個命令報404,證明路由是通過指定路由來匹配。
1.5.8 通過請求ip地址進行匹配
Predicate也支持通過設置某個ip區間號段的請求才會路由,RemoteAddr Route Predicate接收cidr符號(IPv4或IPv6)字符串的列表(最小大小為1),如:192.168.0.1/16(其中192.168.0.1是IP地址,16是子網掩碼)。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- RemoteAddr=192.168.1.1/24
可以將此地址設置為本機的ip地址進行測試。
curl localhost:8080
如果請求的遠程地址是192.168.1.10,則此路由將匹配。
1.5.9 組合使用
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Host=**.foo.org
- Path=/headers
- Method=GET
- Header=X-Request-Id, \d+
- Query=foo, ba.
- Query=baz
- Cookie=chocolate, ch.p
各種Predicates同時存在於同一個路由時,請求必須同時滿足所有條件才被這個路由匹配。
一個請求滿足多個路由的斷言條件時,請求只會被首個成功匹配的路由轉發。
1.6 Spring Cloud gateway高級功能
1.6.1 實現熔斷降級
在分布式系統中,網關作為流量的入口,因此會有大量的請求進入網關,向其他服務發起調用,其他服務不可避免的會出現調用失敗(超時,異常),失敗時不能讓請求堆積在網關上,需要快速失敗並返回給客戶端,想要實現這個要求,就必須在網關上做熔斷、降級操作。
server.port: 8082
spring:
application:
name: gateway
redis:
host: localhost
port: 6379
password: 123456
cloud:
gateway:
routes:
- id: rateLimit_route
uri: http://localhost:8000
order: 0
predicates:
- Path=/test/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackCmdA
fallbackUri: forward:/fallbackA
hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000
這里的配置,使用了兩個過濾器:
1)過濾器Strprefix,作用是去掉請求路徑的最前面n個部分截取掉。
StriPrefix=1就代表截取路徑的個數為1,比如前端過來請求/test/good/1/view,匹配成功后,路由到后端的請求路徑就會變成http://localhost:8888/good/1/view
2)過濾器Hystrix,作用是通過Hystrix進行熔斷降級
當上游的請求,進入了Hystrix熔斷機制時,就會調用fallbackUri配置的降級地址。需要注意的是,還需要單獨設置Hystrix的commandKey的超時時間。
fallbackUri配置的降級地址的代碼如下:
@RestController public class FallbackController { @GetMapping("/fallbackA") public Response fallbackA() { Response response = new Response(); response.setCode("100"); response.setMessage("服務暫時不可用"); return response; } }
1.6.2 分布式限流
從某種意義上講,令牌桶算法是對漏桶算法的一種改進,桶算法能夠限制請求調用的頻率,而令牌桶算法能夠在限制調用的平均速率的同時還允許一定程度的突發調用。在令牌桶算法中,存在一個桶,用來存放固定數量的令牌。算法中存在一種機制,以一定的速率往桶中放令牌。每次請求調用需要先獲取令牌,只有拿到令牌,才有機會繼續執行。否則選擇等待可用的令牌、或者直接拒絕。放令牌這個動作是持續不斷的進行,如果桶中令牌數達到上限,就丟棄令牌,所以就存在這種情況,桶中就已經有100個令牌了,這是服務還沒完全啟動好,等啟動完成對外提供服務時,該限流器可以抵擋瞬時的100個請求。所以只用桶中沒有令牌時,請求才會進行等待,最后相當於以一定的速率進行。
在SpringCloud Gateway中,有Filter過濾器,因此可以在"pre"類型的Filter中自行實現上述三種過濾器。但是限流作為網關最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFacotry這個類,適用於Redis內的通過執行Lua腳本實現令牌桶的方式。具體實現邏輯RequestRateLimiterGatewayFilterFactory類中,lua腳本在如下圖所示的文件夾中:
首先在工程的pom文件中引入gateway的起步依賴和redis的reactive依賴,代碼如下:
配置如下:
server:
port: 8081
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://httpbin.org:80/get
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@userKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
application:
name: cloud-gateway
redis:
host: localhost
port: 6379
database: 0
在上面的配置文件,指定程序的端口8081,配置了redis的信息,並配置了RequestRateLimiter的限流過濾器,該過濾器需要配置三個參數:
- burstCapacity:令牌桶總容量
- replenishRate:令牌桶每秒填充平均速率
- key-resolver:用於限流的鍵的解析器的Bean對象的名字。它使用SpEL表達式根據#{@beanName}從Spring容器中獲取Bean對象。
這里根據用戶ID限流,請求路徑中必須攜帶userId參數
@Bean KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); }
KeyResolver需要實現resolve方法,比如根據userid進行限流,則需要用userid去判斷。實現完KeyResolver之后,需要將這個類的Bean注冊到Ioc容器中。
如果需要根據ip限流,定義的獲取限流key的bean為:
@Bean public KeyResolver ipKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); }
通過exchange對象可以獲取到請求信息,這邊用了HostName,如果你想根據用戶來做限流的話這邊可以獲取當前請求的用戶ID或者用戶名就可以了,比如:
如果需要根據接口的URI進行限流,則需要獲取請求地址的uri作為限流key,定義的Bean對象為:
@Bean KeyResolver apiKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getPath().value()); }