瘋狂創客圈 Java 分布式聊天室【 億級流量】實戰系列之 -25【 博客園 總入口 】
前言
前言
瘋狂創客圈(筆者尼恩創建的高並發研習社群)Springcloud 高並發系列文章,將為大家介紹三個版本的 高並發秒殺:
一、版本1 :springcloud + zookeeper 秒殺
二、版本2 :springcloud + redis 分布式鎖秒殺
三、版本3 :springcloud + Nginx + Lua 高性能版本秒殺
以及有關Springcloud 幾篇核心、重要的文章:
二、Springcloud 中 SpringBoot 配置全集 , 收藏版
三、Feign Ribbon Hystrix 三者關系 , 史上最全 深度解析
四、SpringCloud gateway 詳解 , 史上最全
本文為《SpringCloud gateway 詳解》篇,為大家解讀如果做到使用SpringCloud gateway 。
1.1 SpringCloud Gateway 簡介
SpringCloud Gateway 是 Spring Cloud 的一個全新項目,該項目是基於 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的網關,它旨在為微服務架構提供一種簡單有效的統一的 API 路由管理方式。
SpringCloud Gateway 作為 Spring Cloud 生態系統中的網關,目標是替代 Zuul,在Spring Cloud 2.0以上版本中,沒有對新版本的Zuul 2.0以上最新高性能版本進行集成,仍然還是使用的Zuul 2.0之前的非Reactor模式的老版本。而為了提升網關的性能,SpringCloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway 的目標,不僅提供統一的路由方式,並且基於 Filter 鏈的方式提供了網關基本的功能,例如:安全,監控/指標,和限流。
提前聲明:Spring Cloud Gateway 底層使用了高性能的通信框架Netty。
1.2 SpringCloud Gateway 特征
SpringCloud官方,對SpringCloud Gateway 特征介紹如下:
(1)基於 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
(2)集成 Hystrix 斷路器
(3)集成 Spring Cloud DiscoveryClient
(4)Predicates 和 Filters 作用於特定路由,易於編寫的 Predicates 和 Filters
(5)具備一些網關的高級功能:動態路由、限流、路徑重寫
從以上的特征來說,和Zuul的特征差別不大。SpringCloud Gateway和Zuul主要的區別,還是在底層的通信框架上。
簡單說明一下上文中的三個術語:
(1)Filter(過濾器):
和Zuul的過濾器在概念上類似,可以使用它攔截和修改請求,並且對上游的響應,進行二次處理。過濾器為org.springframework.cloud.gateway.filter.GatewayFilter類的實例。
(2)Route(路由):
網關配置的基本組成模塊,和Zuul的路由配置模塊類似。一個Route模塊由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。如果斷言為真,則路由匹配,目標URI會被訪問。
(3)Predicate(斷言):
這是一個 Java 8 的 Predicate,可以使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或參數。斷言的輸入類型是一個 ServerWebExchange。
1.3 SpringCloud Gateway和架構
Spring在2017年下半年迎來了Webflux,Webflux的出現填補了Spring在響應式編程上的空白,Webflux的響應式編程不僅僅是編程風格的改變,而且對於一系列的著名框架,都提供了響應式訪問的開發包,比如Netty、Redis等等。
SpringCloud Gateway 使用的Webflux中的reactor-netty響應式編程組件,底層使用了Netty通訊框架。
1.3.1 SpringCloud Zuul的IO模型
Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是傳統的Servlet IO處理模型。
大家知道,servlet由servlet container進行生命周期管理。container啟動時構造servlet對象並調用servlet init()進行初始化;container關閉時調用servlet destory()銷毀servlet;container運行時接受請求,並為每個請求分配一個線程(一般從線程池中獲取空閑線程)然后調用service()。
弊端:servlet是一個簡單的網絡IO模型,當請求進入servlet container時,servlet container就會為其綁定一個線程,在並發不高的場景下這種模型是適用的,但是一旦並發上升,線程數量就會上漲,而線程資源代價是昂貴的(上線文切換,內存消耗大)嚴重影響請求的處理時間。在一些簡單的業務場景下,不希望為每個request分配一個線程,只需要1個或幾個線程就能應對極大並發的請求,這種業務場景下servlet模型沒有優勢。
所以Springcloud Zuul 是基於servlet之上的一個阻塞式處理模型,即spring實現了處理所有request請求的一個servlet(DispatcherServlet),並由該servlet阻塞式處理處理。所以Springcloud Zuul無法擺脫servlet模型的弊端。雖然Zuul 2.0開始,使用了Netty,並且已經有了大規模Zuul 2.0集群部署的成熟案例,但是,Springcloud官方已經沒有集成改版本的計划了。
1.3.2 Webflux模型
Webflux模式替換了舊的Servlet線程模型。用少量的線程處理request和response io操作,這些線程稱為Loop線程,而業務交給響應式編程框架處理,響應式編程是非常靈活的,用戶可以將業務中阻塞的操作提交到響應式框架的work線程中執行,而不阻塞的操作依然可以在Loop線程中進行處理,大大提高了Loop線程的利用率。官方結構圖:
Webflux雖然可以兼容多個底層的通信框架,但是一般情況下,底層使用的還是Netty,畢竟,Netty是目前業界認可的最高性能的通信框架。而Webflux的Loop線程,正好就是著名的Reactor 模式IO處理模型的Reactor線程,如果使用的是高性能的通信框架Netty,這就是Netty的EventLoop線程。
關於Reactor線程模型,和Netty通信框架的知識,是Java程序員的重要、必備的內功,個中的原理,具體請參見尼恩編著的《Netty、Zookeeper、Redis高並發實戰》一書,這里不做過多的贅述。
1.3.3 Spring Cloud Gateway的處理流程
客戶端向 Spring Cloud Gateway 發出請求。然后在 Gateway Handler Mapping 中找到與請求相匹配的路由,將其發送到 Gateway Web Handler。Handler 再通過指定的過濾器鏈來將請求發送到我們實際的服務執行業務邏輯,然后返回。過濾器之間用虛線分開是因為過濾器可能會在發送代理請求之前(“pre”)或之后(“post”)執行業務邏輯。
1.4 Spring Cloud Gateway路由配置方式
1.4.1 基礎URI一種路由配置方式
如果請求的目標地址,是單個的URI資源路徑,配置文件示例如下:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: url-proxy-1
uri: https://blog.csdn.net
predicates:
-Path=/csdn
各字段含義如下:
id:我們自定義的路由 ID,保持唯一
uri:目標服務地址
predicates:路由條件,Predicate 接受一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(比如:與,或,非)。
上面這段配置的意思是,配置了一個 id 為 url-proxy-1的URI代理規則,路由的規則為:當訪問地址http://localhost:8080/csdn/1.jsp時,會路由到上游地址https://blog.csdn.net/1.jsp。
1.4.2 基於代碼的路由配置方式
轉發功能同樣可以通過代碼來實現,我們可以在啟動類 GateWayApplication 中添加方法 customRouteLocator() 來定制轉發規則。
package com.springcloud.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/csdn")
.uri("https://blog.csdn.net"))
.build();
}
}
我們在yaml配置文件中注銷掉相關路由的配置,重啟服務,訪問鏈接:http://localhost:8080/ csdn, 可以看到和上面一樣的頁面,證明我們測試成功。
上面兩個示例中 uri 都是指向了我的CSDN博客,在實際項目使用中可以將 uri 指向對外提供服務的項目地址,統一對外輸出接口。
1.4.3 和注冊中心相結合的路由配置方式
在uri的schema協議部分為自定義的lb:類型,表示從微服務注冊中心(如Eureka)訂閱服務,並且進行服務的路由。
一個典型的示例如下:
server:
port: 8084
spring:
cloud:
gateway:
routes:
-id: seckill-provider-route
uri: lb://seckill-provider
predicates:
- Path=/seckill-provider/**
-id: message-provider-route
uri: lb://message-provider
predicates:
-Path=/message-provider/**
application:
name: cloud-gateway
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8888/eureka/
注冊中心相結合的路由配置方式,與單個URI的路由配置,區別其實很小,僅僅在於URI的schema協議不同。單個URI的地址的schema協議,一般為http或者https協議。
1.5 詳解:SpringCloud Gateway 匹配規則
Spring Cloud Gateway 的功能很強大,我們僅僅通過 Predicates 的設計就可以看出來,前面我們只是使用了 predicates 進行了簡單的條件匹配,其實 Spring Cloud Gataway 幫我們內置了很多 Predicates 功能。
Spring Cloud Gateway 是通過 Spring WebFlux 的 HandlerMapping 做為底層支持來匹配到轉發路由,Spring Cloud Gateway 內置了很多 Predicates 工廠,這些 Predicates 工廠通過不同的 HTTP 請求參數來匹配,多個 Predicates 工廠可以組合使用。
1.5.1 Predicate 斷言條件介紹
Predicate 來源於 Java 8,是 Java 8 中引入的一個函數,Predicate 接受一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(比如:與,或,非)。可以用於接口請求參數校驗、判斷新老數據是否有變化需要進行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性實現了各種路由匹配規則,有通過 Header、請求參數等不同的條件來進行作為條件匹配到對應的路由。網上有一張圖總結了 Spring Cloud 內置的幾種 Predicate 的實現。
[
說白了 Predicate 就是為了實現一組匹配規則,方便讓請求過來找到對應的 Route 進行處理,接下來我們接下 Spring Cloud GateWay 內置幾種 Predicate 的使用。
1.5.2 通過請求參數匹配
Query Route Predicate 支持傳入兩個參數,一個是屬性名一個為屬性值,屬性值可以是正則表達式。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
-Query=smile
這樣配置,只要請求中包含 smile 屬性的參數即可匹配路由。
使用 curl 測試,命令行輸入:
curl localhost:8080?smile=x&id=2
經過測試發現只要請求匯總帶有 smile 參數即會匹配路由,不帶 smile 參數則不會匹配。
還可以將 Query 的值以鍵值對的方式進行配置,這樣在請求過來時會對屬性值和正則進行匹配,匹配上才會走路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
-Query=keep, pu.
這樣只要當請求中包含 keep 屬性並且參數值是以 pu 開頭的長度為三位的字符串才會進行匹配和路由。
使用 curl 測試,命令行輸入:
curl localhost:8080?keep=pub
測試可以返回頁面代碼,將 keep 的屬性值改為 pubx 再次訪問就會報 404,證明路由需要匹配正則表達式才會進行路由。
1.5.3 通過 Header 屬性匹配
Header Route Predicate 和 Cookie Route Predicate 一樣,也是接收 2 個參數,一個 header 中屬性名稱和一個正則表達式,這個屬性值和正則表達式匹配則執行。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Header=X-Request-Id, \d+
使用 curl 測試,命令行輸入:
curl http://localhost:8080 -H "X-Request-Id:88"
則返回頁面代碼證明匹配成功。將參數-H "X-Request-Id:88"改為-H "X-Request-Id:spring"再次執行時返回404證明沒有匹配。
1.5.4 通過 Cookie 匹配
Cookie Route Predicate 可以接收兩個參數,一個是 Cookie name ,一個是正則表達式,路由規則會通過獲取對應的 Cookie name 值和正則表達式去匹配,如果匹配上就會執行路由,如果沒有匹配上則不執行。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Cookie=sessionId, test
使用 curl 測試,命令行輸入:
curl http://localhost:8080 --cookie "sessionId=test"
則會返回頁面代碼,如果去掉--cookie "sessionId=test",后台匯報 404 錯誤。
1.5.5 通過 Host 匹配
Host Route Predicate 接收一組參數,一組匹配的域名列表,這個模板是一個 ant 分隔的模板,用.號作為分隔符。它通過參數中的主機地址作為匹配規則。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Host=**.baidu.com
使用 curl 測試,命令行輸入:
curl http://localhost:8080 -H "Host: www.baidu.com"
curl http://localhost:8080 -H "Host: md.baidu.com"
經測試以上兩種 host 均可匹配到 host_route 路由,去掉 host 參數則會報 404 錯誤。
1.5.6 通過請求方式匹配
可以通過是 POST、GET、PUT、DELETE 等不同的請求方式來進行路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Method=GET
使用 curl 測試,命令行輸入:
# curl 默認是以 GET 的方式去請求
測試返回頁面代碼,證明匹配到路由,我們再以 POST 的方式請求測試。
# curl 默認是以 GET 的方式去請求
curl -X POST http://localhost:8080
返回 404 沒有找到,證明沒有匹配上路由
1.5.7 通過請求路徑匹配
Path Route Predicate 接收一個匹配路徑的參數來判斷是否走路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: http://ityouknow.com
order: 0
predicates:
-Path=/foo/{segment}
如果請求路徑符合要求,則此路由將匹配,例如:/foo/1 或者 /foo/bar。
使用 curl 測試,命令行輸入:
curl http://localhost:8080/foo/1
curl http://localhost:8080/foo/xx
curl http://localhost:8080/boo/xx
經過測試第一和第二條命令可以正常獲取到頁面返回值,最后一個命令報404,證明路由是通過指定路由來匹配。
1.5.8 通過請求 ip 地址進行匹配
Predicate 也支持通過設置某個 ip 區間號段的請求才會路由,RemoteAddr Route Predicate 接受 cidr 符號(IPv4 或 IPv6 )字符串的列表(最小大小為1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子網掩碼)。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- RemoteAddr=192.168.1.1/24
可以將此地址設置為本機的 ip 地址進行測試。
curl localhost:8080
如果請求的遠程地址是 192.168.1.10,則此路由將匹配。
1.5.10 組合使用
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Host=**.foo.org
- Path=/headers
- Method=GET
- Header=X-Request-Id, \d+
- Query=foo, ba.
- Query=baz
- Cookie=chocolate, ch.p
各種 Predicates 同時存在於同一個路由時,請求必須同時滿足所有的條件才被這個路由匹配。
一個請求滿足多個路由的斷言條件時,請求只會被首個成功匹配的路由轉發
1.6 Springcloud gateway 高級功能
1.6.1 實現熔斷降級
為什么要實現熔斷降級?
在分布式系統中,網關作為流量的入口,因此會有大量的請求進入網關,向其他服務發起調用,其他服務不可避免的會出現調用失敗(超時、異常),失敗時不能讓請求堆積在網關上,需要快速失敗並返回給客戶端,想要實現這個要求,就必須在網關上做熔斷、降級操作。
為什么在網關上請求失敗需要快速返回給客戶端?
因為當一個客戶端請求發生故障的時候,這個請求會一直堆積在網關上,當然只有一個這種請求,網關肯定沒有問題(如果一個請求就能造成整個系統癱瘓,那這個系統可以下架了),但是網關上堆積多了就會給網關乃至整個服務都造成巨大的壓力,甚至整個服務宕掉。因此要對一些服務和頁面進行有策略的降級,以此緩解服務器資源的的壓力,以保證核心業務的正常運行,同時也保持了客戶和大部分客戶的得到正確的相應,所以需要網關上請求失敗需要快速返回給客戶端。
server.port: 8082
spring:
application:
name: gateway
redis:
host: localhost
port: 6379
password: 123456
cloud:
gateway:
routes:
- id: rateLimit_route
uri: http://localhost:8000
order: 0
predicates:
- Path=/test/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackCmdA
fallbackUri: forward:/fallbackA
hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000
這里的配置,使用了兩個過濾器:
(1)過濾器StripPrefix,作用是去掉請求路徑的最前面n個部分截取掉。
StripPrefix=1就代表截取路徑的個數為1,比如前端過來請求/test/good/1/view,匹配成功后,路由到后端的請求路徑就會變成http://localhost:8888/good/1/view。
(2)過濾器Hystrix,作用是通過Hystrix進行熔斷降級
當上游的請求,進入了Hystrix熔斷降級機制時,就會調用fallbackUri配置的降級地址。需要注意的是,還需要單獨設置Hystrix的commandKey的超時時間
fallbackUri配置的降級地址的代碼如下:
package org.gateway.controller;
import org.gateway.response.Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FallbackController {
@GetMapping("/fallbackA")
public Response fallbackA() {
Response response = new Response();
response.setCode("100");
response.setMessage("服務暫時不可用");
return response;
}
}
1.6.2 分布式限流
從某種意義上講,令牌桶算法是對漏桶算法的一種改進,桶算法能夠限制請求調用的速率,而令牌桶算法能夠在限制調用的平均速率的同時還允許一定程度的突發調用。在令牌桶算法中,存在一個桶,用來存放固定數量的令牌。算法中存在一種機制,以一定的速率往桶中放令牌。每次請求調用需要先獲取令牌,只有拿到令牌,才有機會繼續執行,否則選擇選擇等待可用的令牌、或者直接拒絕。放令牌這個動作是持續不斷的進行,如果桶中令牌數達到上限,就丟棄令牌,所以就存在這種情況,桶中一直有大量的可用令牌,這時進來的請求就可以直接拿到令牌執行,比如設置qps為100,那么限流器初始化完成一秒后,桶中就已經有100個令牌了,這時服務還沒完全啟動好,等啟動完成對外提供服務時,該限流器可以抵擋瞬時的100個請求。所以,只有桶中沒有令牌時,請求才會進行等待,最后相當於以一定的速率執行。
在Spring Cloud Gateway中,有Filter過濾器,因此可以在“pre”類型的Filter中自行實現上述三種過濾器。但是限流作為網關最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory這個類,適用在Redis內的通過執行Lua腳本實現了令牌桶的方式。具體實現邏輯在RequestRateLimiterGatewayFilterFactory類中,lua腳本在如下圖所示的文件夾中:
首先在工程的pom文件中引入gateway的起步依賴和redis的reactive依賴,代碼如下:
配置如下:
server:
port: 8081
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://httpbin.org:80/get
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@userKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
application:
name: cloud-gateway
redis:
host: localhost
port: 6379
database: 0
在上面的配置文件,指定程序的端口為8081,配置了 redis的信息,並配置了RequestRateLimiter的限流過濾器,該過濾器需要配置三個參數:
-
burstCapacity,令牌桶總容量。
-
replenishRate,令牌桶每秒填充平均速率。
-
key-resolver,用於限流的鍵的解析器的 Bean 對象的名字。它使用 SpEL 表達式根據#{@beanName}從 Spring 容器中獲取 Bean 對象。
這里根據用戶ID限流,請求路徑中必須攜帶userId參數
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
KeyResolver需要實現resolve方法,比如根據userid進行限流,則需要用userid去判斷。實現完KeyResolver之后,需要將這個類的Bean注冊到Ioc容器中。
如果需要根據IP限流,定義的獲取限流Key的bean為:
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
通過exchange對象可以獲取到請求信息,這邊用了HostName,如果你想根據用戶來做限流的話這邊可以獲取當前請求的用戶ID或者用戶名就可以了,比如:
如果需要根據接口的URI進行限流,則需要獲取請求地址的uri作為限流key,定義的Bean對象為:
@Bean
KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
通過exchange對象可以獲取到請求信息,這邊用了HostName,如果你想根據用戶來做限流的話這邊可以獲取當前請求的用戶ID或者用戶名就可以了,比如:
如果需要根據接口的URI進行限流,則需要獲取請求地址的uri作為限流key,定義的Bean對象為:
@Bean
KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
最后,介紹一下瘋狂創客圈:瘋狂創客圈,一個Java 高並發研習社群 【博客園 總入口 】
瘋狂創客圈,傾力推出:面試必備 + 面試必備 + 面試必備 的基礎原理+實戰 書籍 《Netty Zookeeper Redis 高並發實戰》
瘋狂創客圈 Java 死磕系列
- Java (Netty) 聊天程序【 億級流量】實戰 開源項目實戰
- Netty 源碼、原理、JAVA NIO 原理
- Java 面試題 一網打盡
- 瘋狂創客圈 【 博客園 總入口 】