萬字Spring Cloud Gateway2.0,面向未來的技術,瞭解一下?

你的點贊就是對我最大的支持。

原創:小姐姐味道(微信公衆號ID:xjjdog),歡迎分享,轉載請保留出處。

本文將從知識拓撲講起,談一下api網關的功能,以及spring cloud gateway的使用方法。文章很長,可以先過一下目錄。

一、知識拓撲 (使用和原理)
二、網關的作用
三、Predicate,路由匹配
四、Filter,過濾器編寫
五、自定義過濾器
六、常見問題

爲什麼很多人覺得spring cloud gateway難用?因爲它的背後用的是webflux,涉及到響應式編程,而不是傳統的過程式編程。

我們把背後的技術梳理一下,不難發現,這個晦澀的根源,就來自於project reactor,與spring項目並駕齊驅的,”面向未來”的響應式編程框架。

結果最後的代碼,都長的和lambda一樣。其背後的思想,是觀察者模式和非阻塞雜交的產物,學習曲線相對陡峭

一、知識拓撲

spring cloud gateway涉及到許多比較新的知識和理念,但僅僅對於使用來說,坡度並不是很大。

1.1 使用相關

我們可以想象一下一個路由的必要元素:web請求,通過一些匹配條件,定位到真正的服務節點。並在這個轉發過程的前後,進行一些精細化控制

其中,predicate就是我們的匹配條件;而filter,就可以理解爲一個無所不能的攔截器。有了這兩個元素,再加上目標uri,就可以實現一個具體的路由了。

由於spring cloud gateway是基於springboot的,所以使用yml進行路由的配置。yml的層次通常比較深,這就造成了配置文件看起來非常的亂。它也可以使用java代碼(或者kotlin)進行路由的編寫,風格偏向函數編程,所以需要首先了解lambda表達式的寫法。

spring cloud gateway大多數時候是作爲http服務的網關,可以針對http的報文進行一些細粒度的控制,所以還需要對http協議有較多的理解,才能在使用時遊刃有餘。

1.2 原理相關

而在原理方面,卻複雜的多。由於實踐方面的滯後性,現有的組件大多數還沒有追上“響應式”這個“超前”的理念,催生了一堆晦澀的組件(主要是專用函數太多)。好在,使用spring cloud gateway並不需要直接接觸這些api。

最重要的,就是對webflux框架的封裝。webflux是可以替代spring mvc的一套解決方案,可以編寫響應式的應用,兩者之間的關係可以看下圖。它的底層使用的是netty,所以操作是異步非阻塞的。

再往下走,webflux是運行在project reactor之上的一個封裝,其根本特性是由後者提供的。這個東西和vert.x一樣,初次接觸使用起來會感覺特別怪異。

reactor是觀察者模式的發揚,所以裏面有Publisher的概念,其中最主要的實現,就是Flux和Mono。所謂的webflux,取名就在於此。

reactor參考:https://url.cn/5B7f5iY

從傳統的開發模式過渡到reactor的開發模式,是有一定成本的。如果有時間可以瞭解一下背後的原理,對spring cloud gateway的使用,還是有好處的。

二、網關的作用

從名字就可以看到,它是一個網絡的關卡,無論後端多麼的複雜,這個對外的關卡表現是一致的。

更加重要的是,隱藏在關卡後面的一些通用的事務,都可以抽象出來進行處理。可以把網關,想像成一個類似於海關的東西,你的簽證資料準備、安檢、調度等,都可以統一進行處理。

api網關就是伴隨着微服務概念興起的一種架構模式,當然也不僅限於微服務。從圖中我們可以看到網關的位置。


且看下面網關的具體作用。

2.1 反向代理

這個是所有網關,包括nginx的基本功能。除了能夠對服務進行整形,網關一個非常重要的附加收益,就是對後端的服務細節進行了屏蔽。

反向代理同時會帶有負載均衡的功能,包括帶權重的流量分配。

2.2 鑑權

就是權限認證,也就是常說的權限系統。由於鑑權服務有非常高的相似性,就可以進行抽象處理,放在網關層。

比如https協議的統一接入,分佈式session的處理問題,新的登錄鑑權通道的接入等。

2.3 流量控制

流量控制如果分散到每個服務裏去做,就是一種災難,網關是最適合的地方。

流量控制通常有多種策略,對後端服務進行屏蔽。非正常請求和超出負載能力的請求,都會被快速攔截在外,爲系統的穩定性提供了必不可少的支持。

流量控制有單機限流和分佈式限流兩種方式,後者控制更加精細一些,spring cloud gateway都有提供。

2.4 熔斷

熔斷與流控的主要區別,在於前者在一段時間內,服務“不可用”,而後者僅概率性失敗。

除了服務之間的調用涉及到熔斷,在網關層的熔斷,作用範圍會更大,需要對服務進行準確的分級。

2.5 灰度控制

網關的一個終極功能,就是實現服務的灰度發佈。比如常說的AB test,就是一種灰度發佈方式。

灰度會進行精細化控制,比如針對一類用戶,某個物理區域,特定請求路徑,特定模塊,隨機百分比等方面的一些灰度控制等。

灰度是一個整體架構配合的結果,但協調的入口就是網關,通過對請求頭或者參數加入一些特定的標誌,就可以對每個請求進行劃分,決定是否落入灰度。

2.6 日誌監控

網關是最適合進行日誌監控的地方。通過對訪問日誌的精細分析,能夠得到很多有價值的數據,進而對後端服務的優化提供決策依據。

比如,某個“業務”的訪問趨勢,運營數據,QPS峯值,同比、環比等。

三、Predicate,路由匹配

spring cloud gateway的配置方式有Fluent API和yml兩種方式,都操蛋的很。

Predicate在英文中是斷言的意思。這裏我們可以看作是條件匹配,能夠根據http頭或者http參數進行匹配。

3.1 時間匹配

在某個時間點之前,或者之後的匹配。比如讓路由在某個時間段內生效。

配置文件類似於:

spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: https://example.org
        predicates:
        - After=2020-10-20T17:42:47.789-07:00[America/Denver]

其中。id是本路由的唯一不可重複名稱,uri指定匹配後的路由地址,而predicates的After,就是我們的時間匹配器。

1.之後

或者翻譯成代碼方式。

builder.routes().route(
r -> r.after(LocalDateTime.of(2020, 10, 17, 42, 47).atZone(ZoneId.of("America/Denver")))
    .uri("https://example.org")
);

由於代碼大部分類似,下面的篇幅,我們只截取最主要的片段。

2.之前

上面是某個時間點之後,之前的寫法,如下:

Before=2017-01-20T17:42:47.789-07:00[America/Denver]

r.before(LocalDateTime.of(2020, 10, 17, 42, 47).atZone(ZoneId.of("America/Denver")))

3.之間
還有在某個時間段之內的

Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

r.between(
LocalDateTime.of(2020, 10, 17, 42, 47).atZone(ZoneId.of("America/Denver")),
LocalDateTime.of(2027, 10, 17, 42, 47).atZone(ZoneId.of("America/Denver"))
)

3.2 Http信息

我們簡單看一下一個http請求的信息,其中,General和Request Headers中的信息,都可以進行匹配控制。對於Cookie、Host等常用的信息,還進行了專門的優化。這其中,最常用的,就是path、cookie、host、query等。


Path

path是最重要的匹配方式,多個path可以使用,分隔。

Path=/foo/{segment},/bar/{segment}
r.path("/foo/{segment}","/bar/{segment}")

注意,我們將{segment}使用大括號圍了起來,這個值,可以通過代碼取出來。

Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange);

String segment = uriVariables.get("segment");

Header頭信息

Header=X-Request-Id, \d+
r.header("Header=X-Request-Id", "\\d+")

與cookie類似,這裏指的是http頭方面的匹配,很多灰度信息,或者trace信息,就喜歡放在這裏。

Cookie [header]

Cookie=chocolate, ch.p
r.cookie("chocolate","ch.p")

http信息中,是否有一個名字叫做chocolate的Cookie,是否與正則ch.p匹配。

Host信息 [header]
雖然host信息也在header信息裏,但是由於它太常用了,所以有專門的匹配器。

Host=**.somehost.org,**.anotherhost.org
r.host("Host=**.somehost.org","**.anotherhost.org")

注意,這裏的匹配字符串,是Ant風格的,更簡潔一些,並不是java中的正則表達式。多個host使用,進行分隔。

Request Method

Method=GET
r.method("GET")

注意,我在源代碼裏沒有找到大小寫轉換的代碼,所以路由中切記保持大寫方式。除了CONNECT,都支持。


Query

這裏指的就是url問號後面的一串參數。

Query=baz
r.query("baz")

Query=foo, ba.
r.query("foo","ba.")

太簡單,都不需要我做過多介紹了。

RemoteAddr

 RemoteAddr=192.168.1.1/24
 r->r.remoteAddr("192.168.1.1/24")

3.3 權重

權重信息的配置,有點2b。比如,我們後面有2臺服務器,spring cloud gateway對其做了兩個路由,其中鏈接的樞紐就是一個叫做Weight的group。

spring:
  cloud:
    gateway:
      routes:
      - id: weight_high
        uri: https://weighthigh.org
        predicates:
        - Weight=group1, 8
      - id: weight_low
        uri: https://weightlow.org
        predicates:
        - Weight=group1, 2

同樣的代碼如下。

builder.routes()
.route("weight_high",r -> r.weight("group1", 8).uri("https://weighthigh.org"))
.route("weight_low",r -> r.weight("group1", 2).uri("https://weightlow.org"));

假如服務有100個節點,還有一堆filter,要重複配置100次?不得不說非常的fuck。

四、Filter,過濾器編寫

匹配,能夠定位到要進行代理的路由。現在,已經進入到了我們的路由內部。上面提到的路由的作用,大部分功能就是在這裏進行配置的。

用過zuul網關的可能都知道,在自定義路由時,會有pre和post兩個註解控制在代理前後的路由行爲。spring cloud gatewa有着同樣的功效。

4.1 信息修改

crud不僅僅存在SSM中,路由的配置也是如此。你可能在路由到真正的後端服務之前,對http頭或者其他信息修改;或者在代理到相應的鏈接之後,再進行一些修改。

按照我們的理解,所謂request對應的是pre,而response對應的是post。

AddRequestHeader=X-Request-Foo, Bar
AddRequestParameter=foo, bar
AddResponseHeader=X-Response-Foo, Bar

RemoveRequestHeader=X-Request-Foo
RemoveResponseHeader=X-Response-Foo
RemoveRequestParameter=foo

SetRequestHeader=X-Request-Foo, Bar
SetResponseHeader=X-Response-Foo, Bar

SetStatus=401

4.2 Request Body修改

這個就蛋疼了一些,原因還是由webflux引起的,在寫法上比較個性一些。

.filters(f -> f.modifyRequestBody(String.class, String.class, MediaType.APPLICATION_JSON_VALUE,
    (exchange, s) -> {
            return Mono.just(s.toUpperCase());
})

上面的代碼,將requestBody中的內容,全部轉成了大寫方式。

相似的,response對應的是modifyResponseBody,寫法是類似的。具體的可以參見
ModifyRequestBodyGatewayFilterFactory
的代碼。如果沒有接觸過上面說到的理論部分,讀起來還是比較吃力的。

4.3 重定向

RedirectTo=302, https://acme.org

.filters(f -> f.redirect(302,"https://acme.org"))

直接重定向。這個比較簡單,不做過多介紹。

4.4 去掉前綴

重點。

StripPrefix=2

.filters(f->f.stripPrefix(2))

StripPrefix可以接受一個非負整數,用於去掉對應的前綴。比如,外部訪問的path是
/a/b/c/d
那麼,轉向後端服務的path,就是/c/d,去掉了/a/b前綴。

這屬於路徑重寫的一種特殊方式,常用在對uri爲lb://協議的微服務路徑重寫。

4.5 路徑重寫

RewritePath是和nginx的路徑重寫非常相近的一個東西。

RewritePath=/foo(?<segment>/?.*), $\{segment}

f.rewritePath("/foo(?<segment>/?.*)", "${segment}")

官方說說明,由於yml配置文件的緣故。要把$寫成$\的方式,但是java代碼中並不需要這麼做。由於內部使用的還是java的正則,同時用上了group的概念,代碼真是髒的可以。

4.6 熔斷配置

默認集成的斷路器,依然是hystrix。

Hystrix=myCommandName

.filters(f -> f.hystrix(c->c.setName("myCommandName")))

另外,熔斷還有一個參數叫做fallbackUri,但可惜的是,只支持forward方式。比如:

fallbackUri: forward:/myfallback

4.7 重試配置

對於一些對穩定性要求非常高的服務,一個無法迴避的問題,就是重試。重試的參數比較多,一個典型的配置如下:

- name: Retry
    args:
        retries: 3
        statuses: BAD_GATEWAY
        backoff:
            firstBackoff: 10ms
            maxBackoff: 50ms
            factor: 2
            basedOnPreviousValue: false

其中,backoff指定了重試的策略和間隔,會按照公式firstBackoff * (factor ^ n)進行增長。

熔斷保證了服務的安全性,重試保證了服務的健壯性,要注意甄別使用場景。

4.8 限流

內置的限流器,如果被觸發,將返回"HTTP 429 - Too Many Requests"錯誤。

限流器的參數是一個叫做KeyResolver實現,其中,就有我們上面提到的概念Mono。所以如果你想要擴展這個限流器的話,就需要了解webflux那一套東西。

public interface KeyResolver {
    Mono<String> resolve(ServerWebExchange exchange);
}

同時,基於redis的令牌桶原理的分佈式限流。由於底層使用的是"spring-boot-starter-data-redis-reactive",所以就擁有了“響應式”的應用特點,支持 WebFlux (Reactor) 的背壓(Backpressure)。對於其中的配置,是有些繞的,比如官方的這段配置。

- name: RequestRateLimiter
    args:
        key-resolver: '#{@ipKeyResolver}'
        redis-rate-limiter.replenishRate: 10
        redis-rate-limiter.burstCapacity: 20

我們就需要定一個名字叫做ipKeyResolver的bean。

限流的維度很多,需要自行開發管理後臺。由於篇幅原因,我們不做展開討論。

五、自定義過濾器

spring cloud gateway的過濾器,有全局過濾器和局部過濾器之分,對應的接口爲GatewayFilterGlobalFilter

如果內置的過濾器不能滿足需求,則可通過自定義過濾器解決。通過實現GatewayFilterOrdered接口,可以進行更加靈活的控制。

可以參考內置過濾器的實現方式。後面的文章,我們將詳細介紹這方面的具體代碼實現。

六、常見問題

lb://表示什麼?

lb://serviceName是spring cloud gateway在微服務中自動爲我們創建的負載均衡uri,在某些特殊情況下,可以直接書寫。比如,在eureka中的註冊名稱爲pay-rpc,則此時的寫法爲:

lb://pay-rpc

如何修改http內容?比如method?

注意ServerWebExchange這個東西。使用它的
exchange.mutate()函數,可以進入修改模式。比如,把GET轉成POST方式:

ServerHttpRequest request = exchange.getRequest();
if (request.getMethod() == HttpMethod.GET) {
    exchange = exchange.mutate().request(request.mutate().method(HttpMethod.POST).build()).build();
}

如何動態更新路由?
主要是通過actuator管理接口,確保這些內容放在了內網中。

GET /actuator/gateway/routes 路由列表
GET  /actuator/gateway/routes/{id} 獲取某個路由信息
GET /actuator/gateway/globalfilters 全局過濾器
GET /actuator/gateway/routefilters filter列表
POST /actuator/gateway/refresh 刷新路由
POST /gateway/routes/{id_route_to_create} 創建路由
DELETE /gateway/routes/{id_route_to_delete}  刪除某個路由

如何做一些數據統計

這個功能簡單的很,我們只需要實現一個全局的過濾器,就可以加入任何統計功能。常用的方式有兩種:通過日誌進行分析;通過應用內聚合進行分析。

這兩者都不是很難,主要在於對功能的規劃而不是代碼。

我有更高級的功能,比如解密數據的需求,該如何做?

這個就要自己實現過濾器了。

 Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

通過ServerWebExchange,可以控制整個請求過程中的任何一個參數的添加,修改,刪除,重寫等。在代理方法前後,可以通過

exchange.getAttributes().put();
exchange.getAttribute()

這兩個函數,進行參數傳遞

所以,即使官方不編寫任何上面提到的filter,我們依然可以用這個基本接口玩的轉。

End

微信公衆號真的是不太適合寫一些教程類的文章,所以本文依然是一個總結性的經驗之談。

隨着zuul1的退出和zuul2的難產,親生的SCG成爲了最優的選擇。Spring團隊很有意思,直接採用了webflux作爲後端的技術(改怕了?),這會讓很多人痛痛痛:又要學習新技術了。

本文並沒有測試SCG的性能,這個已經有很多團隊進行驗證了,效果都不錯。

但現在的spring cloud gateway,問題還很多。好在這個問題是使用問題,並不是功能問題。它已經內置了非常多的Predicate和Filter,但很多時候並不能解決問題,需要使用者自行創建自己的過濾器。好吧,我的大多數過濾器全是自行創建的。

另外吐槽一下Fluent API和yml的配置方式,真是醜的一b,需要開發一個管理後臺。還有複雜的java正則的那些東西,都讓人抓狂--請看牆上那些深深的爪痕,就是我的傑作。

作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公衆號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高併發世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,​進一步交流。​

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