這篇SpringCloud GateWay 詳解,你用的到
- 2022 年 6 月 11 日
- 筆記
- Gateway, JAVA, Spring Cloud
點贊再看,養成習慣,微信搜索【牧小農】關注我獲取更多資訊,風裡雨里,小農等你,很高興能夠成為你的朋友。
項目源碼地址:公眾號回復 sentinel,即可免費獲取源碼
背景
在微服務架構中,通常一個系統會被拆分為多個微服務,面對這麼多微服務客戶端應該如何去調用呢?如果沒有其他更優方法,我們只能記錄每個微服務對應的地址,分別去調用,但是這樣會有很多的問題和潛在因素。
- 客戶端多次請求不同的微服務,會增加客戶端代碼和配置的複雜性,維護成本比價高。
- 認證複雜,每個微服務可能存在不同的認證方式,客戶端去調用,要去適配不同的認證,
- 存在跨域的請求,調用鏈有一定的相對複雜性(防火牆 / 瀏覽器不友好的協議)。
- 難以重構,隨着項目的迭代,可能需要重新劃分微服務
為了解決上面的問題,微服務引入了 網關 的概念,網關為微服務架構的系統提供簡單、有效且統一的API路由管理,作為系統的統一入口,提供內部服務的路由中轉,給客戶端提供統一的服務,可以實現一些和業務沒有耦合的公用邏輯,主要功能包含認證、鑒權、路由轉發、安全策略、防刷、流量控制、監控日誌等。
網關在微服務中的位置:
網關對比
-
Zuul 1.0 : Netflix開源的網關,使用Java開發,基於Servlet架構構建,便於二次開發。因為基於Servlet內部延遲嚴重,並發場景不友好,一個線程只能處理一次連接請求。
-
Zuul 2.0 : 採用Netty實現異步非阻塞編程模型,一個CPU一個線程,能夠處理所有的請求和響應,請求響應的生命周期通過事件和回調進行處理,減少線程數量,開銷較小
-
GateWay : 是Spring Cloud的一個全新的API網關項目,替換Zuul開發的網關服務,基於Spring5.0 + SpringBoot2.0 + WebFlux(基於⾼性能的Reactor模式響應式通信框架Netty,異步⾮阻塞模型)等技術開發,性能高於Zuul
-
Nginx+lua : 性能要比上面的強很多,使用Nginx的反向代碼和負載均衡實現對API服務器的負載均衡以及高可用,lua作為一款腳本語言,可以編寫一些簡單的邏輯,但是無法嵌入到微服務架構中
-
Kong : 基於OpenResty(Nginx + Lua模塊)編寫的高可用、易擴展的,性能高效且穩定,支持多個可用插件(限流、鑒權)等,開箱即可用,只支持HTTP協議,且二次開發擴展難,缺乏更易用的管理和配置方式
GateWay
官方文檔://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-starter
Spring Cloud Gateway 是Spring Cloud的一個全新的API網關項目,目的是為了替換掉Zuul1,它基於Spring5.0 + SpringBoot2.0 + WebFlux(基於⾼性能的Reactor模式響應式通信框架Netty,異步⾮阻塞模型)等技術開發,性能⾼於Zuul,官⽅測試,Spring Cloud GateWay是Zuul的1.6倍 ,旨在為微服務架構提供⼀種簡單有效的統⼀的API路由管理⽅式。
-
可以與Spring Cloud Discovery Client(如Eureka)、Ribbon、Hystrix等組件配合使用,實現路由轉發、負載均衡、熔斷、鑒權、路徑重寫、⽇志監控等
-
Gateway還內置了限流過濾器,實現了限流的功能。
-
設計優雅,容易拓展
基本概念
路由(Route)是GateWay中最基本的組件之一,表示一個具體的路由信息載體,主要由下面幾個部分組成:
- id:路由唯一標識,區別於其他的route
- url: 路由指向的目的地URL,客戶端請求最終被轉發到的微服務
- order: 用於多個Route之間的排序,數值越小越靠前,匹配優先級越高
- predicate:斷言的作用是進行條件判斷,只有斷言為true,才執行路由
- filter: 過濾器用於修改請求和響應信息
核心流程
核心概念:
Gateway Client
向Spring Cloud Gateway
發送請求- 請求首先會被
HttpWebHandlerAdapter
進行提取組裝成網關上下文 - 然後網關的上下文會傳遞到
DispatcherHandler
,它負責將請求分發給RoutePredicateHandlerMapping
RoutePredicateHandlerMapping
負責路由查找,並根據路由斷言判斷路由是否可用- 如果過斷言成功,由
FilteringWebHandler
創建過濾器鏈並調用 - 通過特定於請求的
Fliter
鏈運行請求,Filter
被虛線分隔的原因是Filter可以在發送代理請求之前(pre)和之後(post)運行邏輯 - 執行所有pre過濾器邏輯。然後進行代理請求。發出代理請求後,將運行「post」過濾器邏輯。
- 處理完畢之後將
Response
返回到Gateway
客戶端
Filter過濾器:
- Filter在pre類型的過濾器可以做參數效驗、權限效驗、流量監控、日誌輸出、協議轉換等。
- Filter在post類型的過濾器可以做響應內容、響應頭的修改、日誌輸出、流量監控等
核心思想
當用戶發出請求達到 GateWay
之後,會通過一些匹配條件,定位到真正的服務節點,並且在這個轉發過程前後,進行一些細粒度的控制,其中 Predicate(斷言) 是我們的匹配條件,Filter 是一個攔截器,有了這兩點,再加上URL,就可以實現一個具體的路由,核心思想:路由轉發+執行過濾器鏈
這個過程就好比考試,我們考試首先要找到對應的考場,我們需要知道考場的地址和名稱(id和url),然後我們進入考場之前會有考官查看我們的准考證是否匹配(斷言),如果匹配才會進入考場,我們進入考場之後,(路由之前)會進行身份的登記和考試的科目,填寫考試信息,當我們考試完成之後(路由之後)會進行簽字交卷,走出考場,這個就類似我們的過濾器
Route(路由) :構建網關的基礎模塊,由ID、目標URL、過濾器等組成
Predicate(斷言) :開發人員可以匹配HTTP請求中的內容(請求頭和請求參數),如果請求斷言匹配賊進行路由
Filter(過濾) :GateWayFilter的實例,使用過濾器,可以在請求被路由之前或者之後對請求進行修改
框架搭建
通過上述講解已經了解了基礎概念,我們來動手搭建一個GateWay
項目,來看看它到底是如何運行的
新建項目:cloud-alibaba-gateway-9006
版本對應:
GateWay屬於SprinigCloud且有web依賴,在我們導入對應依賴時,要注意版本關係,我們這裡使用的版本是 2.2.x的版本,所以配合使用的Hoxton.SR5
版本
在這裡我們要注意的是引入GateWay一定要刪除spring-boot-starter-web依賴,否則會有衝突無法啟動
父類pom引用:
<spring-cloud-gateway-varsion>Hoxton.SR5</spring-cloud-gateway-varsion>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-gateway-varsion}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
子類POM引用:
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
yml配置
server:
port: 9006
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: false #開啟註冊中心路由功能
routes: # 路由
- id: nacos-provider #路由ID,沒有固定要求,但是要保證唯一,建議配合服務名
uri: //localhost:9001/nacos-provider # 匹配提供服務的路由地址 lb://表示開啟負載均衡
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
我們在之前的cloud-alibaba-nacos-9001
項目中添加下面測試代碼
@RestController
@RequestMapping("/mxn")
public class DemoController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/hello")
public String hello(){
return "hello world ,my port is :"+serverPort;
}
}
啟動Nacos、cloud-alibaba-nacos-9001
、cloud-alibaba-gateway-9006
通過gateway網關去訪問9001的mxn/order看看。
首先我們在Nacos中看到我們服務是註冊到Nacos中了
然後我們訪問//localhost:9001/mxn/hello
,確保是成功的,在通過//localhost:9006/mxn/hello
去訪問,也是OK,說明我們GateWay搭建成功,我們進入下一步
在上述方法中我們是通過YML去完成的配置,GateWay
還提供了另外一種配置方式,就是通過代碼的方式進行配置,@Bean
注入一個 RouteLocator
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GateWayConfig {
/*
配置了一個id為path_mxn的路由規則
當訪問地址//localhost:9999/mxn/**
就會轉發到//localhost:9001/nacos-provider/mxn/任何地址
*/
@Bean
public RouteLocator gateWayConfigInfo(RouteLocatorBuilder routeLocatorBuilder){
// 構建多個路由routes
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
// 具體路由地址
routes.route("path_mxn",r -> r.path("/mxn/**").uri("//localhost:9001/nacos-provider")).build();
// 返回所有路由規則
return routes.build();
}
}
我們可以將路由注釋掉之後看一下,重啟9006服務,訪問地址//localhost:9006/mxn/hello
就可以轉發到9001中具體的接口中
這裡並不推薦,使用代碼的方式來進行配置gateWay
,大家有個了解就可以,因為代碼的配置維護的成本比較高,而且對於一些需要修改的項,需要改代碼才可以完成,這樣不利於維護和拓展,所以還是推薦大家使用yml進行配置。
GateWay負載均衡
在上述的講解中,我們已經掌握了 GateWay
的一些基本配置和兩種使用方式,下面我們就來講解一下 GateWay
如何實現負載均衡
我們只需要在9006中添加lb://nacos-provider
就可以顯示負載均衡。
當我們去訪問//localhost:9006/mxn/hello
的時候,就可以看到9001和9002不停的切換
Predicate 斷言
在這一篇中我們來研究一下 斷言 ,我們可以理解為:當滿足條件後才會進行轉發路由,如果是多個,那麼多個條件需要同時滿足
在官方提供的斷言種類有11種(最新的有12種類型):
- After:匹配在指定日期時間之後發生的請求。
- Before:匹配在指定日期之前發生的請求。
- Between:需要指定兩個日期參數,設定一個時間區間,匹配此時間區間內的請求。
- Cookie:需要指定兩個參數,分別為name和regexp(正則表達式),也可以理解Key和Value,匹配具有給定名稱且其值與正則表達式匹配的Cookie。
- Header:需要兩個參數header和regexp(正則表達式),也可以理解為Key和Value,匹配請求攜帶信息。
- Host:匹配當前請求是否來自於設置的主機。
- Method:可以設置一個或多個參數,匹配HTTP請求,比如GET、POST
- Path:匹配指定路徑下的請求,可以是多個用逗號分隔
- Query:需要指定一個或者多個參數,一個必須參數和一個可選的正則表達式,匹配請求中是否包含第一個參數,如果有兩個參數,則匹配請求中第一個參數的值是否符合正則表達式。
- RemoteAddr:匹配指定IP或IP段,符合條件轉發。
- Weight:需要兩個參數group和weight(int),實現了路由權重功能,按照路由權重選擇同一個分組中的路由
1. After : 表示配置時間之後才進行轉發
時間戳獲取代碼,用於時間代碼的獲取:
public static void main(String[] args) {
ZonedDateTime zbj = ZonedDateTime.now();//默認時區
System.out.println(zbj);
}
spring:
application:
name: cloud-gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true #開啟註冊中心路由功能
routes: # 路由
- id: nacos-provider #路由ID,沒有固定要求,但是要保證唯一,建議配合服務名
uri: lb://nacos-provider # 匹配提供服務的路由地址 lb://表示開啟負載均衡
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
- After=2022-06-11T16:30:40.785+08:00[Asia/Shanghai] #在這個時間之後的請求夠可以進行通過,之前的則不能進行訪問
如果在時間段之前訪問則404
Before
匹配ZonedDateTime
類型的時間,表示匹配在指定日期時間之前的請求,之後的請求則拒絕404錯誤
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
# - After=2022-06-11T16:30:40.785+08:00[Asia/Shanghai] #在這個時間之後的請求夠可以進行通過,之前的則不能進行訪問
- Before=2022-06-11T15:30:40.785+08:00[Asia/Shanghai]
Between
Between
可以匹配ZonedDateTime
類型的時間,由兩個ZonedDateTime
參數組成,第一個參數為開始時間,第二參數為結束時間,逗號進行分隔,匹配在指定的開始時間與結束時間之內的請求,配置如下:
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
# - After=2022-06-11T16:30:40.785+08:00[Asia/Shanghai] #在這個時間之後的請求夠可以進行通過,之前的則不能進行訪問
# - Before=2022-06-11T15:30:40.785+08:00[Asia/Shanghai]
- Between=2022-06-11T15:30:40.785+08:00[Asia/Shanghai],2022-06-11T16:30:40.785+08:00[Asia/Shanghai]
Cookie
由兩個參數組成,分別為name(Key)
和regexp(正則表達式)(Value
),匹配具有給定名稱且其值與正則表達式匹配的Cookie。
路由規則會通過獲取Cookie name值和正則表達式去匹配,如果匹配上就會執行路由,如果匹配不上則不執行。
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
# - After=2022-06-11T16:30:40.785+08:00[Asia/Shanghai] #在這個時間之後的請求夠可以進行通過,之前的則不能進行訪問
# - Before=2022-06-11T15:30:40.785+08:00[Asia/Shanghai]
# - Between=2022-06-11T15:30:40.785+08:00[Asia/Shanghai],2022-06-11T16:30:40.785+08:00[Asia/Shanghai]
- Cookie=muxiaonong,[a-z]+ # 匹配Cookie的key和value(正則表達式)表示任意字母
小寫字母匹配成功:
數字匹配不成功:
Header
由兩個參數組成,第一個參數為Header名稱
,第二參數為Header的Value值
,指定名稱的其值和正則表達式相匹配的Header的請求
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
# - After=2022-06-11T16:30:40.785+08:00[Asia/Shanghai] #在這個時間之後的請求夠可以進行通過,之前的則不能進行訪問
# - Before=2022-06-11T15:30:40.785+08:00[Asia/Shanghai]
# - Between=2022-06-11T15:30:40.785+08:00[Asia/Shanghai],2022-06-11T16:30:40.785+08:00[Asia/Shanghai]
# - Cookie=muxiaonong,[a-z]+ # 匹配Cookie的key和value(正則表達式)表示任意字母
- Header=headerName, \d+ # \d表示數字
請求頭攜帶數字斷言請求成功,
斷言字母匹配失敗:
Host
匹配當前請求是否來自於設置的主機。
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
# - After=2022-06-11T16:30:40.785+08:00[Asia/Shanghai] #在這個時間之後的請求夠可以進行通過,之前的則不能進行訪問
# - Before=2022-06-11T15:30:40.785+08:00[Asia/Shanghai]
# - Between=2022-06-11T15:30:40.785+08:00[Asia/Shanghai],2022-06-11T16:30:40.785+08:00[Asia/Shanghai]
# - Cookie=muxiaonong,[a-z]+ # 匹配Cookie的key和value(正則表達式)表示任意字母
# - Header=headerName, \d+ # \d表示數字
- Host=**.muxiaonong.com #匹配當前的主機地址發出的請求
滿足Host斷言,請求成功
不滿足Host斷言失敗
**Method **
可以設置一個或多個參數,匹配HTTP請求,比如POST,PUT,GET,DELETE
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
# - After=2022-06-11T16:30:40.785+08:00[Asia/Shanghai] #在這個時間之後的請求夠可以進行通過,之前的則不能進行訪問
# - Before=2022-06-11T15:30:40.785+08:00[Asia/Shanghai]
# - Between=2022-06-11T15:30:40.785+08:00[Asia/Shanghai],2022-06-11T16:30:40.785+08:00[Asia/Shanghai]
# - Cookie=muxiaonong,[a-z]+ # 匹配Cookie的key和value(正則表達式)表示任意字母
# - Header=headerName, \d+ # \d表示數字
# - Host=**.muxiaonong.com #匹配當前的主機地址發出的請求
- Method=POST,GET
GET斷言成功
PUT斷言請求失敗
Query
由兩個參數組成,第一個為參數名稱(必須),第二個為參數值(可選-正則表達式),匹配請求中是否包含第一個參數,如果有兩個參數,則匹配請求中第一個參數的值是否符合第二個正則表達式。
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
# - After=2022-06-11T16:30:40.785+08:00[Asia/Shanghai] #在這個時間之後的請求夠可以進行通過,之前的則不能進行訪問
# - Before=2022-06-11T15:30:40.785+08:00[Asia/Shanghai]
# - Between=2022-06-11T15:30:40.785+08:00[Asia/Shanghai],2022-06-11T16:30:40.785+08:00[Asia/Shanghai]
# - Cookie=muxiaonong,[a-z]+ # 匹配Cookie的key和value(正則表達式)表示任意字母
# - Header=headerName, \d+ # \d表示數字
# - Host=**.muxiaonong.com #匹配當前的主機地址發出的請求
# - Method=POST,GET
- Query=id,.+ # 匹配任意請求參數,這裡如果需要匹配多個參數,可以寫多個- Query=
斷言匹配 請求成功
RemoteAddr
參數由CIDR 表示法(IPv4 或 IPv6)字符串組成,也就是匹配的ID地址,配置如下:
predicates: # 斷言
- Path=/mxn/** # 斷言,路徑相匹配進行路由
# - After=2022-06-11T16:30:40.785+08:00[Asia/Shanghai] #在這個時間之後的請求夠可以進行通過,之前的則不能進行訪問
# - Before=2022-06-11T15:30:40.785+08:00[Asia/Shanghai]
# - Between=2022-06-11T15:30:40.785+08:00[Asia/Shanghai],2022-06-11T16:30:40.785+08:00[Asia/Shanghai]
# - Cookie=muxiaonong,[a-z]+ # 匹配Cookie的key和value(正則表達式)表示任意字母
# - Header=headerName, \d+ # \d表示數字
# - Host=**.muxiaonong.com #匹配當前的主機地址發出的請求
# - Method=POST,GET
# - Query=id,.+ # 匹配任意請求參數,這裡如果需要匹配多個參數,可以寫多個Query
- RemoteAddr=192.168.1.1/24
RemoteAddr
需要兩個參數group和weight(int)權重數值,實現了路由權重功能,表示將相同的請求根據權重跳轉到不同的uri地址,要求group的名稱必須一致
routes: # 路由
- id: weight_high #路由ID,沒有固定要求,但是要保證唯一,建議配合服務名
uri: //blog.csdn.net/qq_14996421
predicates: # 斷言
- Weight=groupName,8
- id: weight_low #路由ID,沒有固定要求,但是要保證唯一,建議配合服務名
uri: //juejin.cn/user/2700056290405815
predicates: # 斷言
- Weight=groupName,2
直接訪問//localhost:9006/
可以看到我們請求的地址成8/2比例交替顯示, 80% 的流量轉發到//blog.csdn.net/qq_14996421,將約 20% 的流量轉發到//juejin.cn/user/2700056290405815
Predicate就是為了實現一組匹配規則,讓請求過來找到對應的Route進行處理。如果有多個斷言則全部命中後進行處理
GateWay Filter
路由過濾器允許修改傳入的HTTP請求或者返回的HTTP響應,路由過濾器的範圍是特定的路由.
Spring Cloud GateWay 內置的Filter生命周期有兩種:pre(業務邏輯之前)、post(業務邏輯之後)
GateWay本身自帶的Filter分為兩種: GateWayFilter(單一)、GlobalFilter(全局)
GateWay Filter提供了豐富的過濾器的使用,單一的有32種,全局的有9種,有興趣的小夥伴可以了解一下。
官方參考網址://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#global-filters
StripPrefix
StripPrefix 在我們當前請求中,通過規則值去掉某一部分地址,比如我們有一台服務中加入了一個前端nacos-provider
想要通過這個去訪問,我們在項目cloud-alibaba-nacos-9001
中加入 context-path
server:
port: 9001
servlet:
context-path: /nacos-provider
現在9001的訪問路徑變為//localhost:9001/nacos-provider/mxn/hello
,但是如果我們通過網關去訪問路徑就會變成//localhost:9006/mxn/nacos-provider/mxn/hello
這個時候我們通過這個路徑去訪問是訪問不成功的,想要解決這個方法,這個就用到了我們FIlter
中的 StripPrefix
routes: # 路由
- id: nacos-provider #路由ID,沒有固定要求,但是要保證唯一,建議配合服務名
uri: lb://nacos-provider
predicates: # 斷言
- Path=/mxn/** # 匹配對應地址
filters:
- StripPrefix=1 # 去掉地址中的第一部分
我們重新啟動9006項目,再去訪問
自定義Filter
雖然Gateway給我們提供了豐富的內置Filter,但是實際項目中,自定義Filter的場景非常常見,因此單獨介紹下自定義FIlter的使用。
想要實現GateWay自定義過濾器,那麼我們需要實現GatewayFilter接口和Ordered接口
@Slf4j
@Component
public class MyFilter implements Ordered, GlobalFilter {
/**
* @param exchange 可以拿到對應的request和response
* @param chain 過濾器鏈
* @return 是否放行
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//獲取第一個參數
String id = exchange.getRequest().getQueryParams().getFirst("id");
//打印當前時間
log.info("MyFilter 當前請求時間為:"+new Date());
//判斷用戶是否存在
if(StringUtils.isEmpty(id)){
log.info("用戶名不存在,非法請求!");
//如果username為空,返回狀態碼為407,需要代理身份驗證
exchange.getResponse().setStatusCode(HttpStatus.PROXY_AUTHENTICATION_REQUIRED);
// 後置過濾器
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 設定過濾器的優先級,值越小則優先級越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
當我們訪問//localhost:9006/mxn/nacos-provider/mxn/hello
請求,沒有攜帶ID參數,請求失敗
當我們訪問//localhost:9006/mxn/nacos-provider/mxn/hello?id=1
請求,請求成功
總結
到這裡我們的GateWay
就講解完了,對於GateWay的核心點主要有三個Route\Predicate\Filter
,我們搞懂了這三點,基本上對於GateWay
的知識就掌握的差不多了,GateWay核心的流程就是:路由轉發+執行過濾器鏈,如果對文中有疑問的小夥伴,歡迎留言討論。
創作不易,如果文中對你有幫助,記得點贊關注,您的支持是我創作的最大動力。
我是牧小農,怕什麼真理無窮,進一步有進一步的歡喜~