微服務架構spring cloud - Gateway(十一)

本篇文章主要介紹了什麼是 Spring Cloud Gateway,並基於 Spring Cloud Gateway 的 Finchley.RC1 版本編寫一個 Spring Cloud Gateway 的入門案例,即基本代理的路由轉發配置。

img

 

概述

Spring Cloud Gateway 是 Spring Cloud 的一個全新項目,該項目是基於 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的網關,它旨在爲微服務架構提供一種簡單有效的統一的 API 路由管理方式。

Spring Cloud Gateway 作爲 Spring Cloud 生態系統中的網關,目標是替代 Netflix Zuul,其不僅提供統一的路由方式,並且基於 Filter 鏈的方式提供了網關基本的功能,例如:安全、監控、埋點和限流等。

Spring Cloud Gateway 的特徵:

  • 基於 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
  • 動態路由
  • Predicates 和 Filters 作用於特定路由
  • 集成 Hystrix 斷路器
  • 集成 Spring Cloud DiscoveryClient
  • 易於編寫的 Predicates 和 Filters
  • 限流
  • 路徑重寫

vs Netflix Zuul

Zuul 基於 Servlet 2.5(使用 3.x),使用阻塞 API,它不支持任何長連接,如 WebSockets。而 Spring Cloud Gateway 建立在 Spring Framework 5,Project Reactor 和 Spring Boot 2 之上,使用非阻塞 API,支持 WebSockets,並且由於它與 Spring 緊密集成,所以將會是一個更好的開發體驗。

要說缺點,其實 Spring Cloud Gateway 還是有的。目前它的文檔還不是很完善,官方文檔有許多還處於 TODO 狀態,網絡上關於它的文章也還比較少。如果你決定要使用它,那麼你必須得有耐心通過自己閱讀源碼來解決可能遇到的問題。(2018.5.7)

坑總是會有的,就看我們怎麼來填平。

術語

  • Route(路由):這是網關的基本構建塊。它由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。如果斷言爲真,則路由匹配。
  • Predicate(斷言):這是一個 Java 8 的 Predicate。輸入類型是一個 ServerWebExchange。我們可以使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或參數。
  • Filter(過濾器):這是org.springframework.cloud.gateway.filter.GatewayFilter的實例,我們可以使用它修改請求和響應。

流程

客戶端向 Spring Cloud Gateway 發出請求。然後在 Gateway Handler Mapping 中找到與請求相匹配的路由,將其發送到 Gateway Web Handler。Handler 再通過指定的過濾器鏈來將請求發送到我們實際的服務執行業務邏輯,然後返回。
過濾器之間用虛線分開是因爲過濾器可能會在發送代理請求之前(“pre”)或之後(“post”)執行業務邏輯。

實戰

我們之前使用 Zuul 實現了一個網關,這裏我們就用 Spring Cloud Gateway 來替代它實現相同的功能。在這裏我們繼續沿用之前已經寫好的服務並依次啓動:

  • eureka
  • producer
  • consumer

新建一個標準的 Spring Boot 工程,命名爲 gateway,然後在 pom.xml 中引入以下依賴座標

複製

1
2
3
4
5
6
7
8
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

application.yml 配置文件內容如下

複製

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
#      routes:
#        - id: default_path_to_http
#          uri: https://windmt.com
#          order: 10000
#          predicates:
#            - Path=/**
#          filters:
#            - SetPath=/
server:
  port: 10000
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka/
logging:
  level:
    org.springframework.cloud.gateway: debug

配置說明:

  • spring.cloud.gateway.discovery.locator.enabled:是否與服務註冊於發現組件進行結合,通過 serviceId 轉發到具體的服務實例。默認爲false,設爲true便開啓通過服務中心的自動根據 serviceId 創建路由的功能。
  • spring.cloud.gateway.routes用於配合具體的路由規則,是一個數組。這裏我創建了一個 id 爲default_path_to_http的路由,其中的配置是將未匹配的請求轉發到https://windmt.com。實際上開啓了服務發現後,如果只使用默認創建的路由規則,這個 routes 不配置也是可以的,所以我就先註釋掉了不用它了。
  • 網關服務監聽 10000 端口
  • 指定註冊中心的地址,以便使用服務發現功能
  • 調整相關包的 log 級別,以便排查問題

Spring Boot 的啓動類不用修改,直接啓動即可(少了要加@Enable***註解這步我還有點不適應),啓動後我們便可在 Eureka 中看到我們的網關服務

gateway in eureka

然後我們像之前使用 Zuul 那樣訪問 http://localhost:10000/consumer/hello/windmt

我們期待像直接訪問 consumer 那樣能返回 “Hello windmt!”,但是實際上卻出錯了,返回了 404 錯誤。我們來看一下 log

1
2
3
4
5
6
7
8
9
10
2018-05-07 16:20:34.643 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition CompositeDiscoveryClient_PRODUCER applying {pattern=/PRODUCER/**} to Path
2018-05-07 16:20:34.652 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition CompositeDiscoveryClient_PRODUCER applying filter {regexp=/PRODUCER/(?<remaining>.*), replacement=/${remaining}} to RewritePath
2018-05-07 16:20:34.657 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: CompositeDiscoveryClient_PRODUCER
2018-05-07 16:20:34.657 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition CompositeDiscoveryClient_CONSUMER applying {pattern=/CONSUMER/**} to Path
2018-05-07 16:20:34.659 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition CompositeDiscoveryClient_CONSUMER applying filter {regexp=/CONSUMER/(?<remaining>.*), replacement=/${remaining}} to RewritePath
2018-05-07 16:20:34.660 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: CompositeDiscoveryClient_CONSUMER
2018-05-07 16:20:34.662 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition CompositeDiscoveryClient_CLOUD-GATEWAY applying {pattern=/CLOUD-GATEWAY/**} to Path
2018-05-07 16:20:34.664 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition CompositeDiscoveryClient_CLOUD-GATEWAY applying filter {regexp=/CLOUD-GATEWAY/(?<remaining>.*), replacement=/${remaining}} to RewritePath
2018-05-07 16:20:34.665 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: CompositeDiscoveryClient_CLOUD-GATEWAY
2018-05-07 16:20:34.732  WARN 69502 --- [ctor-http-nio-2] .a.w.r.e.DefaultErrorWebExceptionHandler : Failed to handle request [GET http://127.0.0.1:10000/consumer/hello/windmt]: Response status 404

可以看到 Spring Cloud Gateway 確實爲我們的 producer 和 consumer 自動創建了對應的路由,但是這裏的 pattern/regexp 裏都是大寫的。那我們就換成大寫的來試一下。

訪問 http://localhost:10000/CONSUMER/hello/windmt 確實返回了 “Hello, windmt!”,這時再看 log

1
2
3
2018-05-07 16:32:06.473 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping   : Route matched: CompositeDiscoveryClient_CONSUMER
2018-05-07 16:32:06.473 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.h.RoutePredicateHandlerMapping   : Mapping [Exchange: GET http://localhost:10000/CONSUMER/hello/windmt] to Route{id='CompositeDiscoveryClient_CONSUMER', uri=lb://CONSUMER, order=0, predicate=org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$511/504641976@1ccb8d30, gatewayFilters=[OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory$$Lambda$513/1057677564@19e672a5, order=1}]}
2018-05-07 16:32:06.473 DEBUG 69502 --- [ctor-http-nio-2] o.s.c.g.handler.FilteringWebHandler      : Sorted gatewayFilterFactories: [OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@376e7531}, order=-1}, OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory$$Lambda$513/1057677564@19e672a5, order=1}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@5782d777}, order=10000}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@75e710b}, order=10100}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@23202c31}, order=2147483637}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@b016b4e}, order=2147483646}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@26f7cdf8}, order=2147483647}, OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@4f824872}, order=2147483647}]

可以看出,Spring Cloud Gateway 自動的爲我們的 consumer 創建了一個路由,類似於下邊這樣

1
2
3
4
5
6
7
8
routes:
    - id: CompositeDiscoveryClient_CONSUMER
      uri: lb://CONSUMER
      order: 0
      predicates:
          - Path=/CONSUMER/**
      filters:
          - RewritePath=/CONSUMER/(?<segment>.*), /$\{segment}

但是這種必須將 serviceId 大寫的方式還是比較蛋疼的,雖然 Eureka 註冊中心默認顯示的都是大寫的,但是這大寫的路徑放在 URL 真的好嗎?我唯一能想到的好處就是能清晰分辨出 serviceId 和 path。

如果大寫的 URL 在瀏覽器裏自動變成了小寫的,可以試試:清空緩存、使用無痕模式(command+shift+n)、在終端直接用curl

上邊是基於服務發現的默認路由規則,如果我們要自定義路由規則怎麼辦呢?

比如我們的這個服務是跟客戶服務相關的(嗯,目前它功能比較單一,只會跟客戶 say hi,但是這沒有影響),我們希望這個服務的 path 以 /customer/ 開頭,具體到這個例子,就是 /costomer/hello/{name}。並且,我們還要爲每個響應添加一個響應頭X-Response-Default-Foo: Default-Bar

讓我們來修改一下配置,主要是增加一個 route,其他配置不變

1
2
3
4
5
6
7
8
9
routes:
  - id: service_customer
    uri: lb://CONSUMER
    order: 0
    predicates:
      - Path=/customer/**
    filters:
      - StripPrefix=1
      - AddResponseHeader=X-Response-Default-Foo, Default-Bar

新增的StripPrefix可以接受一個非負整數,對應的具體實現是StripPrefixGatewayFilterFactory,從名字就可以看出它的作用是去掉前綴的,那個整數即對應層數。具體到本例中,我們通過 Spring Cloud Gateway 訪問 /customer/hello/windmt,那麼當網關服務向後轉發請求時,會去掉/customer,微服務收到的就是/hello/windmt
現在這個 filters 下面的配置不用深究,按這麼配置就好,下一篇會詳細講 Filter。

我們現在訪問 http://localhost:10000/customer/hello/windmt 可以看到能正常返回數據並且響應頭也加上了。這時候 http://localhost:10000/CONSUMER/hello/windmt 雖然依舊能正常返回數據,但是並沒有我們自定義的響應頭。

image-20180507170005882

Spring Cloud Gateway 也支持通過 Java 的流式 API 進行路由的定義,如下就是一個和上邊通過配置文件配置的等效的路由,並且可以和配置文件搭配使用。

複製

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
    // @formatter:off
    return builder.routes()
            .route(r -> r.path("/fluent/customer/**")
                         .filters(f -> f.stripPrefix(2)
                                        .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
                         .uri("lb://CONSUMER")
                         .order(0)
                         .id("fluent_customer_service")
            )
            .build();
    // @formatter:on
}

總結

本文我們簡單瞭解了 Spring Cloud Gateway,並用它實現了一個簡單的網關服務。既介紹了通過結合註冊中心 Eureka 來爲微服務提供默認的路由,也介紹瞭如何通過配置文件和 API 去自定義路由,相信大家對 Spring Cloud Gateway 已經有了個初步的認識。後面的文章我們也會繼續去發現 Spring Cloud Gateway 更多強大的功能。

示例代碼可以從 Github 獲取:https://github.com/zhaoyibo/spring-cloud-study

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章