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

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

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

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

1、知識拓撲 (使用和原理)
2、網關的做用
3、Predicate,路由匹配
4、Filter,過濾器編寫
5、自定義過濾器
6、常見問題
複製代碼

爲何不少人以爲spring cloud gateway難用?由於它的背後用的是webflux,涉及到響應式編程,而不是傳統的過程式編程。程序員

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

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

1、知識拓撲

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

1.1 使用相關

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

其中,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參考:url.cn/5B7f5iY

從傳統的開發模式過渡到reactor的開發模式,是有必定成本的。若是有時間能夠了解一下背後的原理,對spring cloud gateway的使用,仍是有好處的。

2、網關的做用

從名字就能夠看到,它是一個網絡的關卡,不管後端多麼的複雜,這個對外的關卡表現是一致的。

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

api網關就是伴隨着微服務概念興起的一種架構模式,固然也不只限於微服務。從圖中咱們能夠看到網關的位置。

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

2.1 反向代理

這個是全部網關,包括nginx的基本功能。除了可以對服務進行整形,網關一個很是重要的附加收益,就是對後端的服務細節進行了屏蔽。

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

2.2 鑑權

就是權限認證,也就是常說的權限系統。因爲鑑權服務有很是高的類似性,就能夠進行抽象處理,放在網關層。

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

2.3 流量控制

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

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

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

2.4 熔斷

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

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

2.5 灰度控制

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

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

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

2.6 日誌監控

網關是最適合進行日誌監控的地方。經過對訪問日誌的精細分析,可以獲得不少有價值的數據,進而對後端服務的優化提供決策依據。

好比,某個「業務」的訪問趨勢,運營數據,QPS峯值,同比、環比等。

3、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。

4、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。

限流的維度不少,須要自行開發管理後臺。因爲篇幅緣由,咱們不作展開討論。

5、自定義過濾器

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

若是內置的過濾器不能知足需求,則可經過自定義過濾器解決。經過實現GatewayFilterOrdered接口,能夠進行更加靈活的控制。

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

6、常見問題

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,歡迎添加好友,​進一步交流。​

相關文章
相關標籤/搜索