瘋狂創客圈 Java 分佈式聊天室【 億級流量】實戰系列之 -25【 博客園 總入口 】html
### 前言前端
瘋狂創客圈(筆者尼恩建立的高併發研習社羣)Springcloud 高併發系列文章,將爲你們介紹三個版本的 高併發秒殺:java
1、版本1 :springcloud + zookeeper 秒殺react
2、版本2 :springcloud + redis 分佈式鎖秒殺程序員
3、版本3 :springcloud + Nginx + Lua 高性能版本秒殺web
以及有關Springcloud 幾篇核心、重要的文章:面試
1、Springcloud 配置, 史上最全 一文全懂正則表達式
2、Springcloud 中 SpringBoot 配置全集 , 收藏版redis
3、Feign Ribbon Hystrix 三者關係 , 史上最全 深度解析算法
4、SpringCloud gateway 詳解 , 史上最全
本文爲《SpringCloud gateway 詳解》篇,爲你們解讀若是作到使用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。
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。
Spring在2017年下半年迎來了Webflux,Webflux的出現填補了Spring在響應式編程上的空白,Webflux的響應式編程不只僅是編程風格的改變,並且對於一系列的著名框架,都提供了響應式訪問的開發包,好比Netty、Redis等等。
SpringCloud Gateway 使用的Webflux中的reactor-netty響應式編程組件,底層使用了Netty通信框架。
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官方已經沒有集成改版本的計劃了。
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高併發實戰》一書,這裏不作過多的贅述。
客戶端向 Spring Cloud Gateway 發出請求。而後在 Gateway Handler Mapping 中找到與請求相匹配的路由,將其發送到 Gateway Web Handler。Handler 再經過指定的過濾器鏈來將請求發送到咱們實際的服務執行業務邏輯,而後返回。過濾器之間用虛線分開是由於過濾器可能會在發送代理請求以前(「pre」)或以後(「post」)執行業務邏輯。
若是請求的目標地址,是單個的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。
轉發功能一樣能夠經過代碼來實現,咱們能夠在啓動類 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 指向對外提供服務的項目地址,統一對外輸出接口。
在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協議。
Spring Cloud Gateway 的功能很強大,咱們僅僅經過 Predicates 的設計就能夠看出來,前面咱們只是使用了 predicates 進行了簡單的條件匹配,其實 Spring Cloud Gataway 幫咱們內置了不少 Predicates 功能。
Spring Cloud Gateway 是經過 Spring WebFlux 的 HandlerMapping 作爲底層支持來匹配到轉發路由,Spring Cloud Gateway 內置了不少 Predicates 工廠,這些 Predicates 工廠經過不一樣的 HTTP 請求參數來匹配,多個 Predicates 工廠能夠組合使用。
Predicate 來源於 Java 8,是 Java 8 中引入的一個函數,Predicate 接受一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將 Predicate 組合成其餘複雜的邏輯(好比:與,或,非)。能夠用於接口請求參數校驗、判斷新老數據是否有變化須要進行更新操做。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性實現了各類路由匹配規則,有經過 Header、請求參數等不一樣的條件來進行做爲條件匹配到對應的路由。網上有一張圖總結了 Spring Cloud 內置的幾種 Predicate 的實現。
[
說白了 Predicate 就是爲了實現一組匹配規則,方便讓請求過來找到對應的 Route 進行處理,接下來咱們接下 Spring Cloud GateWay 內置幾種 Predicate 的使用。
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,證實路由須要匹配正則表達式纔會進行路由。
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證實沒有匹配。
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 錯誤。
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 錯誤。
能夠經過是 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 的方式去請求
curl http://localhost:8080
測試返回頁面代碼,證實匹配到路由,咱們再以 POST 的方式請求測試。
# curl 默認是以 GET 的方式去請求
curl -X POST http://localhost:8080
返回 404 沒有找到,證實沒有匹配上路由
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,證實路由是經過指定路由來匹配。
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,則此路由將匹配。
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 同時存在於同一個路由時,請求必須同時知足全部的條件才被這個路由匹配。
一個請求知足多個路由的斷言條件時,請求只會被首個成功匹配的路由轉發
爲何要實現熔斷降級?
在分佈式系統中,網關做爲流量的入口,所以會有大量的請求進入網關,向其餘服務發起調用,其餘服務不可避免的會出現調用失敗(超時、異常),失敗時不能讓請求堆積在網關上,須要快速失敗並返回給客戶端,想要實現這個要求,就必須在網關上作熔斷、降級操做。
爲何在網關上請求失敗須要快速返回給客戶端?
由於當一個客戶端請求發生故障的時候,這個請求會一直堆積在網關上,固然只有一個這種請求,網關確定沒有問題(若是一個請求就能形成整個系統癱瘓,那這個系統能夠下架了),可是網關上堆積多了就會給網關乃至整個服務都形成巨大的壓力,甚至整個服務宕掉。所以要對一些服務和頁面進行有策略的降級,以此緩解服務器資源的的壓力,以保證核心業務的正常運行,同時也保持了客戶和大部分客戶的獲得正確的相應,因此須要網關上請求失敗須要快速返回給客戶端。
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; } }
從某種意義上講,令牌桶算法是對漏桶算法的一種改進,桶算法可以限制請求調用的速率,而令牌桶算法可以在限制調用的平均速率的同時還容許必定程度的突發調用。在令牌桶算法中,存在一個桶,用來存放固定數量的令牌。算法中存在一種機制,以必定的速率往桶中放令牌。每次請求調用須要先獲取令牌,只有拿到令牌,纔有機會繼續執行,不然選擇選擇等待可用的令牌、或者直接拒絕。放令牌這個動做是持續不斷的進行,若是桶中令牌數達到上限,就丟棄令牌,因此就存在這種狀況,桶中一直有大量的可用令牌,這時進來的請求就能夠直接拿到令牌執行,好比設置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 (Netty) 聊天程序【 億級流量】實戰 開源項目實戰
瘋狂創客圈 【 博客園 總入口 】