微服務閘道器實戰——Spring Cloud Gateway

博雲技術社群發表於2019-05-24

導讀

作為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進行訪問時,則可以獲取到後端服務返回結果。

 

相關文章