導讀
作為Netflix Zuul的替代者,Spring Cloud Gateway是一款非常實用的微服務網關,在Spring Cloud微服務架構體系中發揮非常大的作用。本文對Spring Cloud Gateway常見使用場景進行了梳理,希望對微服務開發人員提供一些幫助。
微服務網關SpringCloudGateway
1.概述
Spring cloud gateway是spring官方基於Spring 5.0、Spring Boot2.0和Project Reactor等技術開發的網關,Spring Cloud Gateway旨在為微服務架構提供簡單、有效和統一的API路由管理方式,Spring Cloud Gateway作為Spring Cloud生態系統中的網關,目標是替代Netflix Zuul,其不僅提供統一的路由方式,並且還基於Filer鏈的方式提供了網關基本的功能,例如:安全、監控/埋點、限流等。
2.核心概念
網關提供API全托管服務,豐富的API管理功能,輔助企業管理大規模的API,以降低管理成本和安全風險,包括協議適配、協議轉發、安全策略、防刷、流量、監控日志等貢呢。一般來說網關對外暴露的URL或者接口信息,我們統稱為路由信息。如果研發過網關中間件或者使用過Zuul的人,會知道網關的核心是Filter以及Filter Chain(Filter責任鏈)。Sprig Cloud Gateway也具有路由和Filter的概念。下面介紹一下Spring Cloud Gateway中幾個重要的概念。
-
路由。路由是網關最基礎的部分,路由信息有一個ID、一個目的URL、一組斷言和一組Filter組成。如果斷言路由為真,則說明請求的URL和配置匹配
-
斷言。Java8中的斷言函數。Spring Cloud Gateway中的斷言函數輸入類型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的斷言函數允許開發者去定義匹配來自於http request中的任何信息,比如請求頭和參數等。
-
過濾器。一個標准的Spring webFilter。Spring cloud gateway中的filter分為兩種類型的Filter,分別是Gateway Filter和Global Filter。過濾器Filter將會對請求和響應進行修改處理
如上圖所示,Spring cloudGateway發出請求。然后再由Gateway Handler Mapping中找到與請求相匹配的路由,將其發送到Gateway web handler。Handler再通過指定的過濾器鏈將請求發送到我們實際的服務執行業務邏輯,然后返回。
快速入門
以Spring Boot框架開發為例,啟動一個Gateway服務模塊(以Consul作為注冊中心),一個后端服務模塊。client端請求經gateway服務把請求路由到后端服務。
前提條件:
-
Consul:版本1.5.0。
-
Spring bot:版本2.1.5。
-
Spring cloud:版本Greenwich.SR1。
-
Redis:版本5.0.5。
1.微服務開發
這里以使用Spring Boot框架開發微服務為例,啟動一個服務並注冊到Consul。
引入依賴:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency>
注冊服務到Consul,配置文件配置如下:
spring: application: name: service-consumer cloud: consul: host: 127.0.0.1 port: 8500 discovery: service-name: service-consumer
如下定義RestController,發布HTTP接口。
@RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @GetMapping(value = "/info") public User info() { return userService.info(); } }
注:此為服務端配置,經Gateway把請求路由轉發到該服務上。
2.網關配置
創建一個Gateway服務,引入以下依賴:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency>
啟動類配置如下:
@SpringBootApplication @EnableDiscoveryClient public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
Spring Cloud Gateway對client端請求起到路由功能,主要配置如下:
server: port: 8098 spring: application: name: service-gateway cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true consul: host: 127.0.0.1 #注冊gateway網關到consul port: 8500 discovery: service-name: service-gateway
此時使用http://localhost:8089/service-consumer/user/info訪問服務,網關即可對服務進行路由轉發,把請求轉發到具體后端服務上。此時,url中使用的url前綴service-consumer,是后端服務在Consul注冊的服務名稱轉為小寫字母以后的字符串。
最佳實踐
01
Gateway網關配置
本文第二部分開發規范中定義了網關進行路由轉發的配置,除了上述配置方式還可以使用下面的方式進行配置:
gateway: discovery: locator: enabled: true lower-case-service-id: true routes: - id: service_consumer uri: lb://service-consumer predicates: - Path= /consumer/** filters: - StripPrefix=1
在上面的配置中,配置了一個Path的predicat,將以/consumer/**開頭的請求都會轉發到uri為lb://service-consumer的地址上,lb://service-consumer(注冊中心中服務的名稱)即service-consumer服務的負載均衡地址,並用StripPrefix的filter 在轉發之前將/consumer去掉。同時將spring.cloud.gateway.discovery.locator.enabled改為false,如果不改的話,之前的http://localhost:8081/service-consumer/user/info這樣的請求地址也能正常訪問,因為這時為每個服務創建了2個router。
本文第二部分和本節一共講述了兩種配置方式,兩種配置都可以實現請求路由轉發的功能。參數spring.cloud.gateway.discovery.locator.enabled為true,表明Gateway開啟服務注冊和發現的功能,並且Spring Cloud Gateway自動根據服務發現為每一個服務創建了一個router,這個router將以服務名開頭的請求路徑轉發到對應的服務。spring.cloud.gateway.discovery.locator.lowerCaseServiceId是將請求路徑上的服務名配置為小寫(因為服務注冊的時候,向注冊中心注冊時將服務名轉成大寫的了)。
gateway: discovery: locator: enabled: true lower-case-service-id: true
02
Gateway跨域訪問
Spring Cloud Gateway還針對跨域訪問做了設計,可以使用以下配置解決跨域訪問問題:
spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': allowedOrigins: "https://docs.spring.io" allowedMethods: - GET allowHeaders: - Content-Type
在上面的示例中,允許來自https://docs.spring.io的get請求進行訪問,並且表明服務器允許請求頭中攜帶字段Content-Type。
03
Gateway 過濾器
Spring Cloud Gateway的filter生命周期不像Zuul那么豐富,它只有兩個:“pre”和“post”:
-
pre:這種過濾器在請求被路由之前調用。可以利用這個過濾器實現身份驗證、在集群中選擇請求的微服務、記錄調試的信息。
-
post:這種過濾器在路由到服務器之后執行。這種過濾器可用來為響應添加HTTP Header、統計信息和指標、響應從微服務發送給客戶端等。
Spring Cloud gateway的filter分為兩種:GatewayFilter和Globalfilter。GlobalFilter會應用到所有的路由上,而Gatewayfilter將應用到單個路由或者一個分組的路由上。
利用Gatewayfilter可以修改請求的http的請求或者是響應,或者根據請求或者響應做一些特殊的限制。更多時候可以利用Gatewayfilter做一些具體的路由配置。
下面的配置是AddRequestParameter Gatewayfilter的相關配置。
spring: application: name: service-gateway cloud: gateway: discovery: locator: enabled: true routes: - id: parameter_route uri: http://localhost:8504/user/info filters: - AddRequestParameter=foo, bar predicates: - Method=GET
上述配置中指定了轉發的地址,設置所有的GET方法都會自動添加foo=bar,當請求符合上述路由條件時,即可在后端服務上接收到Gateway網關添加的參數。
另外再介紹一種比較常用的filter,即StripPrefix gateway filter。
配置如下:
spring: cloud: gateway: routes: - id: stripprefixfilter uri: lb://service-consumer predicates: - Path=/consumer/** filters: - StripPrefix=1
當client端使用http://localhost:8098/consumer/user/info路徑進行請求時,如果根據上述進行配置Gateway會將請求轉換為http://localhost:8098/service-consumer/user/info。以此作為前端請求的最終目的地。
04
Gateway請求匹配
Gateway網關可以根據不同的方式進行匹配進而把請求分發到不同的后端服務上。
通過header進行匹配,把請求分發到不同的服務上,配置如下:
spring: cloud: gateway: routes: - id: header_route uri: http://baidu.com predicates: - Header=X-Request-Id, \d+
通過curl測試:curl http://localhost:8080 -H "X-Request-Id:666666",返回頁面代碼證明匹配成功。
如果是以Host進行匹配,配置如下:
spring: cloud: gateway: routes: - id: host_route uri: http://baidu.com predicates: - Host=**.baidu.com
通過curl http://localhost:8098 -H "Host: www.baidu.com"進行測試,返回頁面代碼即轉發成功。
可以通過POST、GET、PUT、DELTE等不同的方式進行路由:
spring: cloud: gateway: routes: - id: method_route uri: http://baidu.com predicates: - Method=GET
通過 curl http://localhost:8098 進行測試,返回頁面代碼即表示成功。
上述是單個匹配進行路由,如果把多個匹配合在一起進行路由,必須滿足所有的路有條件才會進行路由轉發。
05
Gateway熔斷
Spring Cloud Gateway也可以利用Hystrix的熔斷特性,在流量過大時進行服務降級,同時項目中必須加上Hystrix的依賴。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
配置后,Gateway將使用fallbackcmd作為名稱生成HystrixCommand對象進行熔斷處理。如果想添加熔斷后的回調內容,需要添加以下配置:
spring: cloud: gateway: routes: - id: hystrix_route uri: lb://consumer-service predicates: - Path=/consumer/** filters: - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/fallback - StripPrefix=1 hystrix: command: fallbackcmd: execution: isolation: thread: timeoutInMilliseconds: 5000 #超時時間,若不設置超時時間則有可能無法觸發熔斷
上述配置中給出了熔斷之后返回路徑,因此,在Gateway服務模塊添加/fallback路徑,以作為服務熔斷時的返回路徑。
@RestController public class GatewayController { @RequestMapping(value = "/fallback") public String fallback(){ return "fallback nothing"; } }
fallbackUri: forward:/fallback配置了 fallback 時要會調的路徑,當調用 Hystrix 的 fallback 被調用時,請求將轉發到/fallback這個 URI,並以此路徑的返回值作為返回結果。
06
Gateway重試路由器
通過簡單的配置,Spring Cloud Gateway就可以支持請求重試功能。
spring: cloud: gateway: routes: - id: header_route uri: http://localhost:8504/user/info predicates: - Path=/user/** filters: - name: Retry args: retries: 3 status: 503 - StripPrefix=1
Retry GatewayFilter通過四個參數來控制重試機制,參數說明如下:
-
retries:重試次數,默認值是 3 次。
-
statuses:HTTP 的狀態返回碼,取值請參考:org.springframework.http.HttpStatus。
-
methods:指定哪些方法的請求需要進行重試邏輯,默認值是 GET 方法,取值參考:org.springframework.http.HttpMethod。
-
series:一些列的狀態碼配置,取值參考:org.springframework.http.HttpStatus.Series。符合的某段狀態碼才會進行重試邏輯,默認值是 SERVER_ERROR,值是 5,也就是 5XX(5 開頭的狀態碼),共有5個值。
使用上述配置進行測試,當后台服務不可用時,會在控制台看到請求三次的日志,證明此配置有效。
07
Gateway 限流操作
Spring Cloud Gateway本身集成了限流操作,Gateway限流需要使用Redis,pom文件中添加Redis依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
配置文件中配置如下:
spring: cloud: gateway: routes: - id: rate_limit_route uri: lb://service-consumer predicates: - Path=/user/** filters: - name: RequestRateLimiter args: key-resolver: "#{@hostAddrKeyResolver}" redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 3 - StripPrefix=1 consul: host: 127.0.0.1 port: 8500 discovery: service-name: service-gateway instance-id: service-gateway-233 redis: host: localhost port: 6379
在上面的配置問價中,配置了Redis的信息,並配置了RequestRateLimiter的限流過濾器,該過濾器需要配置三個參數:
-
BurstCapacity:令牌桶的總容量。
-
replenishRate:令牌通每秒填充平均速率。
-
Key-resolver:用於限流的解析器的Bean對象的名字。它使用SpEL表達式#{@beanName}從Spring容器中獲取bean對象。
注意:filter下的name必須是RequestRateLimiter。
Key-resolver參數后面的bean需要自己實現,然后注入到Spring容器中。KeyResolver需要實現resolve方法,比如根據ip進行限流,則需要用hostAddress去判斷。實現完KeyResolver之后,需要將這個類的Bean注冊到Ioc容器中。還可以根據uri限流,同hostname限流是一樣的。例如以ip限流為例,在gateway模塊中添加以下實現:
public class HostAddrKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } public HostAddrKeyResolver hostAddrKeyResolver() { return new HostAddrKeyResolver(); } }
把該類注入到spring容器中:
@SpringBootApplication @EnableDiscoveryClient public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } @Bean public HostAddrKeyResolver hostAddrKeyResolver(){ return new HostAddrKeyResolver(); } }
基於上述配置,可以對請求基於ip的訪問進行限流。
08
自定義Gatewayfilter
Spring Cloud Gateway內置了過濾器,能夠滿足很多場景的需求。當然,也可以自定義過濾器。在Spring Cloud Gateway自定義過濾器,過濾器需要實現GatewayFilter和Ordered這兩個接口。
下面的例子實現了Gatewayfilter,它可以以log日志的形式記錄每次請求耗費的時間,具體實現如下:
public class RequestTimeFilter implements GatewayFilter, Ordered { private static final Log log = LogFactory.getLog(GatewayFilter.class); private static final String REQUEST_TIME_BEGIN = "requestTimeBegin"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis()); return chain.filter(exchange).then( Mono.fromRunnable(() -> { Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN); if (startTime != null) { log.info("請求路徑:"+exchange.getRequest().getURI().getRawPath() + "消耗時間: " + (System.currentTimeMillis() - startTime) + "ms"); } }) ); } @Override public int getOrder() { return 0; } } 上述代碼中定義了自己實現的過濾器。Ordered的int getOrder()方法是來給過濾器定優先級的,值越大優先級越低。還有一個filter(ServerWebExchange exchange, GatewayFilterChain chain)方法,在該方法中,先記錄了請求的開始時間,並保存在ServerWebExchange中,此處是一個“pre”類型的過濾器。然后再chain.filter()的內部類中的run()方法中相當於"post"過濾器,在此處打印了請求所消耗的時間。 接下來將該過濾器注冊到router中,代碼如下。 @Bean public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(r -> r.path("/user/**") .filters(f -> f.filter(new RequestTimeFilter()) .addResponseHeader("X-Response-Default-Foo", "Default-Bar")) .uri("http://localhost:8504/user/info") .order(0) .id("customer_filter_router") ) .build(); }
除了上述代碼的方式配置我們自定義的過濾器的方式之外,也可以在application.yml文件中直接配置,這里不再贅述。
啟動程序,通過curl http://localhost:8098/user/info控制台會打印出請求消耗時間,日志如下:
.... 2019-05-22 15:13:31.221 INFO 19780 --- [ctor-http-nio-4] o.s.cloud.gateway.filter.GatewayFilter : 請求路徑:/user/info消耗時間: 54ms ... 2019-05-22 16:46:23.785 INFO 29928 --- [ctor-http-nio-1] o.s.cloud.gateway.filter.GatewayFilter : 請求路徑:/user/info3消耗時間: 5ms ....
09
自定義GlobalFilter
Spring Cloud Gateway根據作用范圍分為GatewayFilter和GlobalFilter,二者區別如下:
-
GatewayFilter : 需要通過spring.cloud.routes.filters 配置在具體路由下,只作用在當前路由上或通過spring.cloud.default-filters配置在全局,作用在所有路由上。
-
GlobalFilter:全局過濾器,不需要在配置文件中配置,作用在所有的路由上,最終通過GatewayFilterAdapter包裝成GatewayFilterChain可識別的過濾器,它為請求業務以及路由的URI轉換為真實業務服務的請求地址的核心過濾器,不需要配置,系統初始化時加載,並作用在每個路由上。
在上一小節中定義的是Gatewayfilter,下面實現的是Globalfilter:
public class TokenFilter implements GlobalFilter, Ordered { Logger logger= LoggerFactory.getLogger( TokenFilter.class ); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getQueryParams().getFirst("token"); if (token == null || token.isEmpty()) { logger.info( "token 為空,無法進行訪問." ); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
上述代碼實現了Globalfilter,具體邏輯是判斷請求中是否含參數token,如果沒有,則校驗不通過,對所有請求都有效。如果含有token則轉發到具體后端服務上,如果沒有則校驗不通過。
通過curl http://localhost:8098/user/info進行訪問,因為路徑中不含有參數token,則無法通過校驗,打印日志如下:
2019-05-22 15:27:11.078 INFO 5956 --- [ctor-http-nio-1] com.song.gateway.TokenFilter : token 為空,無法進行訪問. ...
通過curl http://localhost:8098/user/info?token=123進行訪問時,則可以獲取到后端服務返回結果。