您好,登錄后才能下訂單哦!
Spring Cloud Gateway(下文以SCG代替), 顧名思義這是由Spring 官方出品的一款網(wǎng)關(guān)產(chǎn)品,是Spring Cloud的子項(xiàng)目。
This project provides a library for building an API Gateway on top of Spring MVC. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.
官方介紹主要突出了路由功能的簡單有效,同時(shí)可以在安全、監(jiān)控以及擴(kuò)展性方面提供不錯(cuò)的支持,畢竟靠著Spring Cloud這棵大樹。
這是官方網(wǎng)站的工作原理示意圖,從上圖可以看出SCG在整個(gè)流程中主要擔(dān)任反向代理的角色??蛻舳苏?qǐng)求抵達(dá)SCG后,SCG通過Handler Mapping將請(qǐng)求路由到Web Handler,Web Handler再通過Filter對(duì)原始請(qǐng)求進(jìn)行處理,最終發(fā)送到被代理的服務(wù)端。
在研究SCG之前,我們發(fā)現(xiàn)Spring Cloud下面已經(jīng)有一個(gè)成熟的API套件Spring Cloud Netflix,提供了服務(wù)注冊(cè)發(fā)現(xiàn)(Eureka),熔斷器(Hystrix),智能路由(Zuul)和客戶端負(fù)載均衡(Ribbon)等特性,其中就有我們需要的路由功能Zuul。
那為什么在集成一個(gè)路由功能后,Spring Cloud還要自己開發(fā)一個(gè)用于路由的Gateway項(xiàng)目呢?我們來看看他們的一些對(duì)比,由于Spring Cloud只集成了Zuul1.0,所以比較也集中在Zuul1.0和SCG之間。
連接方式 | 支持服務(wù)器 | 功能 | |
---|---|---|---|
Zuul1.0 | Servlet API | Tomcat,undertow | 基本路由規(guī)則,僅支持Path的路由 |
SCG | Reactor | Netty | 較多路由規(guī)則,可以支持header,cookie,query,method等豐富的predict定義 |
從上面的對(duì)比來看,SCG基于Project Reactor可以獲得更優(yōu)秀的吞吐,在功能方面相當(dāng)于Zuul的優(yōu)化,更加靈活的配置可以滿足幾乎所有的網(wǎng)關(guān)路由需求。
雖然說Zuul2.0也是基于Netty開發(fā),并增強(qiáng)了路由和過濾器功能,然而他的多次跳票最終讓Spring下決心自己做一款網(wǎng)關(guān)路由產(chǎn)品,并表示不會(huì)將Zuul2.0集成進(jìn)以后的Spring Cloud中,也算一段趣聞吧。
下面我們實(shí)際動(dòng)手實(shí)現(xiàn)一個(gè)網(wǎng)關(guān),結(jié)合過程中遇到的問題來熟悉SCG的各項(xiàng)特性。
我們新建一個(gè)基于Spring Boot的Maven項(xiàng)目,添加SCG的依賴,主要是下面兩個(gè)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
這里選擇的是最新的Spring Boot Release版本(2.1.4)以及支持2.1的Spring Cloud分支Greenwich。
最后建一個(gè)SpringBootApplication,參照首頁的Demo去掉Hystrix和RateLimit相關(guān)的內(nèi)容就可以跑起來了(https://spring.io/projects/spring-cloud-gateway)。
光有個(gè)Demo肯定不行,我們的網(wǎng)關(guān)是要實(shí)際投產(chǎn)使用的,在分析了實(shí)際需求之后我們發(fā)現(xiàn)急需的第一個(gè)功能是動(dòng)態(tài)路由。
在文檔中提供了兩種方式的路由配置方式
通過java API
直接通過RouteLocatorBuilder構(gòu)建如下:
builder.routes().route("path_route",
r -> r.path("/get").uri("http://httpbin.org")).build();
通過配置文件
通過YAML文件構(gòu)建路由如下:
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://example.org
predicates:
- Path=/foo/{segment},/bar/{segment}
但是實(shí)際需求中存在動(dòng)態(tài)分配路由的場景,以上兩種方式顯然都不能滿足需求。
通過查看源代碼發(fā)現(xiàn)SCG加載路由是通過RouteDefinitionLocator接口實(shí)現(xiàn),有以下默認(rèn)實(shí)現(xiàn)(框掉的部分可以暫時(shí)忽略,這是我們自己的實(shí)現(xiàn)):
在GatewayAutoConfiguration中通過Primary的方式指定CompositeRouteDefinitionLocator作為路由定義加載的入口,通過組合模式將所有的RouteDefinitionLocator代理。最終通過CompositeRouteDefinitionLocator的getRouteDefinitions方法將所有定義加載出來。
@Bean
@Primary
public RouteDefinitionLocator routeDefinitionLocator(
List<RouteDefinitionLocator> routeDefinitionLocators) {
return new CompositeRouteDefinitionLocator(
Flux.fromIterable(routeDefinitionLocators));
}
public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator {
private final Flux<RouteDefinitionLocator> delegates;
public CompositeRouteDefinitionLocator(Flux<RouteDefinitionLocator> delegates) {
this.delegates = delegates;
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return this.delegates.flatMap(RouteDefinitionLocator::getRouteDefinitions);
}
}
通過源代碼的解讀,我們發(fā)現(xiàn)如果需要定義新的路由加載方式,只需要增加一個(gè)RouteDefinitionLocator的實(shí)現(xiàn)即可,在實(shí)際操作中為了方便路由更新我們仿照已有的實(shí)現(xiàn)
InMemoryRouteDefinitionRepository進(jìn)行實(shí)現(xiàn),類圖如下:
我們通過新增了一個(gè)抽象類類完成RouteDefinitionRepository的擴(kuò)展,在抽象類里我們實(shí)現(xiàn)了基本的get, save, delete方法,另外新增了refresh方法用于刷新緩存,而緩存的實(shí)現(xiàn)參考了InMemory的實(shí)現(xiàn)方式。
在需要進(jìn)行擴(kuò)展的時(shí)候我們可以通過繼承AbstractRoutConfigure來增加我們自己的configure loader,再通過Configuration方式注入即可:
最終的實(shí)現(xiàn)效果是我們通過數(shù)據(jù)庫變更配置后,通過restful接口來調(diào)用refresh方法即可完成路由的動(dòng)態(tài)刷新。
通過上面動(dòng)態(tài)路由的基本實(shí)現(xiàn),我們數(shù)據(jù)庫中的配置是這樣的
但是我們是要做微服務(wù)和集群的網(wǎng)關(guān),直接寫地址顯然是不行的。
針對(duì)這種情況,SCG提供了一種URI的格式:lb://main-service,其中main-service是我們微服務(wù)在注冊(cè)中心的name。
當(dāng)URI以lb開頭,則在進(jìn)行URI解析的時(shí)候會(huì)去尋找zookeeper,consul,eureka 對(duì)應(yīng)的客戶端實(shí)現(xiàn)。我們使用的是eureka,并且在數(shù)據(jù)庫中加上以下配置
這樣我們就可以成功代理微服務(wù)提供的接口了。
容錯(cuò)管理從以下兩方面進(jìn)行考慮
1, 路由未定義
針對(duì)路由未找到的情況,提供有意義的報(bào)錯(cuò)信息進(jìn)行有效反饋。
實(shí)現(xiàn)層面主要通過定義一個(gè)NotFound的路由,通過設(shè)置order確保NotFound路由在所有的路由之后執(zhí)行,這樣當(dāng)所有的路由都沒有匹配上的時(shí)候就會(huì)被路由到NotFound路由,從而反饋有意義的報(bào)錯(cuò)信息。
數(shù)據(jù)庫定義,
代碼部分:
/**未找到路由的時(shí)候提示錯(cuò)誤信息*/
@RequestMapping(value = "/notfoundcontroller")
public Mono<Map<String, String>> notFoundController() {
Map<String, String> res = new HashMap<>();
res.put("code", "-404");
res.put("data", "route definition not found");
return Mono.just(res);
}
2, 熔斷器Hystrix
熔斷器主要應(yīng)用于請(qǐng)求超時(shí),服務(wù)端錯(cuò)誤等使用場景,SCG提供了Hystrix的集成,我們只需要在YAML配置文件里面配置default filter并加入fallbackUri的實(shí)現(xiàn)即可。
YAML
spring:
cloud:
gateway:
default-filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/fallbackcontroller
fallbackUri
/**斷路器對(duì)應(yīng)的服務(wù)降級(jí)地址,對(duì)于請(qǐng)求失敗進(jìn)行處理*/
@RequestMapping(value = "/fallbackcontroller")
public Mono<Map<String, String>> fallBackController() {
Map<String, String> res = new HashMap<>();
res.put("code", "-100");
res.put("data", "service not available");
return Mono.just(res);
}
通過上面兩點(diǎn)的配置,我們?cè)谡?qǐng)求出錯(cuò)如超時(shí)、服務(wù)宕機(jī)的情況都可以得到對(duì)應(yīng)的錯(cuò)誤信息,確保了網(wǎng)關(guān)服務(wù)的魯棒性。
SCG使用的限流機(jī)制(Rate Limiter)基于令牌桶算法,我們先大致了解一下令牌桶算法。
從上圖可以看出,令牌桶算法的主要數(shù)據(jù)結(jié)構(gòu)是個(gè)緩沖區(qū)。通過勻速生成的令牌來填充緩沖區(qū)相當(dāng)于生產(chǎn)者,而實(shí)際流量則相當(dāng)于消費(fèi)者來消費(fèi)緩沖區(qū)中的令牌。
我們?cè)俳Y(jié)合SCG中的實(shí)現(xiàn)來看看令牌桶算法如何限流的。
SCG使用RateLimiter需要引入spring-boot-starter-data-redis-reactive,所以SCG的令牌桶實(shí)現(xiàn)是基于Redis的,這樣可以滿足分布式的要求。
SCG在使用過程中需要設(shè)置三個(gè)參數(shù)replenishRate ,burstCapacity和KeyResolver。
? replenishRate表示的是裝桶的速率,也就是令牌生成的速率;
? burstCapacity表示瞬間高爆發(fā)的容量,官方文檔解釋是一秒內(nèi)允許的最大流量又補(bǔ)充了一句是令牌桶可以裝下的令牌數(shù)。
? KeyResolver很好理解,通過key的定義可以明確規(guī)定限流的層級(jí),用戶級(jí)還是IP級(jí)別等等。
對(duì)于burstCapacity的理解,只有當(dāng)replenishRate和burstCapacity相等時(shí)也就是請(qǐng)求處理基本是勻速的情況下,burstCapacity才表示一秒內(nèi)允許的最大流量,否則解釋為令牌桶的容量更加貼切。
代碼實(shí)現(xiàn)主要通過RedisRateLimiter.class和request_rate_limiter.lua兩個(gè)文件,而主要邏輯是通過腳本文件實(shí)現(xiàn)。
這里主要獲取java傳過來的參數(shù),計(jì)算出ttl,ttl的邏輯是桶裝滿所需時(shí)間的兩倍。
上面這段代碼是實(shí)現(xiàn)限流的關(guān)鍵,每次都會(huì)通過當(dāng)前時(shí)間和上次刷新時(shí)間的間隔計(jì)算填充的令牌,只有填充后的令牌 >= 請(qǐng)求的令牌數(shù)才符合條件允許令牌獲取。
當(dāng)新的請(qǐng)求獲取令牌后,更新令牌桶的令牌數(shù)和最后刷新時(shí)間。
在實(shí)際引用中我們根據(jù)我們服務(wù)器的壓力來設(shè)定rate和capacity,通過不停的調(diào)節(jié)來尋求吞吐和負(fù)載的平衡。
日志配置方面除了基本的logback配置,需要加入access_log的配置,根據(jù)官方文檔我們需要在logback配置文件中加入logger和appender的配置。
<appender name="accessLog" class="ch.qos.logback.core.FileAppender">
<file>access_log.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="accessLog" />
</appender>
<logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false">
<appender-ref ref="async"/>
</logger>
如上所示,通過定義logger接收netty的AccessLog,通過異步發(fā)射器發(fā)送到accessLog Appender。
這里需要注意的是Netty AccessLog的配置要到reactor-netty0.7.9之后才支持,所以在使用這個(gè)功能之前需要確保我們netty的版本滿足要求,項(xiàng)目目前使用的spring版本如下,對(duì)應(yīng)的reactor-netty版本為0.8.6。
配置了這么多,然而access.log文件還是空空如也,因?yàn)槟懵┑袅撕苤匾囊徊剑?br/>在啟動(dòng)參數(shù)中添加 -Dreactor.netty.http.server.accessLogEnabled=true
. 注意這個(gè)屬性是java系統(tǒng)屬性而不是spring配置屬性,也就是說只能通過啟動(dòng)參數(shù)注入。
我們通過一些簡單的介紹了解了SCG的出現(xiàn)背景,然后通過實(shí)際的網(wǎng)關(guān)搭建實(shí)踐來一步步的理解SCG的架構(gòu)理念和實(shí)現(xiàn)細(xì)節(jié)。
通過動(dòng)態(tài)路由部分我們見證了SCG的可擴(kuò)展性架構(gòu),在服務(wù)路由和容錯(cuò)管理部分我們主要和Spring Cloud已有組件(eureka, hystrix)進(jìn)行集成,而在限流機(jī)制部分我們通過閱讀源代碼理解了基于令牌桶的限流算法以及如何結(jié)合Redis實(shí)現(xiàn)分布式系統(tǒng)限流,在日志配置部分主要是結(jié)合Netty的日志機(jī)制來完成網(wǎng)關(guān)的訪問日志配置。
在我們的實(shí)踐中我們沒有用上SCG的所有特性,但是就目前的情況用于我們自己的API 網(wǎng)關(guān)已經(jīng)夠用。
在啟動(dòng)spring boot程序的時(shí)候發(fā)現(xiàn)了下面兩句話
2019-05-20 13:56:32,381 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2019-05-20 13:56:32,875 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
于是懷著好奇心去看了看這個(gè)HikariDataSource是何方神圣。
不看不知道一看嚇一跳,這個(gè)從來沒聽過的東西居然是spring boot默認(rèn)的連接池,作為連接池他的性能居然能超過druid。限于篇幅,我們會(huì)另開一篇來研究一下。
在前面的源碼分析中我們看到,RouteDefinitionLocator是采用的代理模式,通過一個(gè)組合器將所有代理locator中定義的route加載出來,核心代碼:
所以當(dāng)我們定義了多個(gè)locator的時(shí)候(如MySql的locator, Json的locator),SCG是如何對(duì)這些locator進(jìn)行merge的呢?
通過GatewayAutoConfiguration.class 我們發(fā)現(xiàn)定義為Primary的RouteLocator是CachingRouteLocator。
而在CachingRouteLocator中通過裝飾者模式對(duì)所有l(wèi)ocator獲得的route進(jìn)行了排序,排序的依據(jù)是order字段。
綜上所述,如果定義了多個(gè)routeDefinitionLocator,則對(duì)于里面的route會(huì)根據(jù)order進(jìn)行排序,如果order未定義則按照默認(rèn)order為0處理。
排序完成后按照先后順序逐個(gè)匹配請(qǐng)求,如果滿足則不繼續(xù)匹配,也就是說全局來說定義的order越小則優(yōu)先級(jí)越高,不管出自哪個(gè)locator。
這個(gè)錯(cuò)誤是配置了斷路器之后出現(xiàn)的,當(dāng)配置的fallbackuri沒有定義或者無法匹配的時(shí)候會(huì)出現(xiàn)。我們實(shí)踐中的起因是配置了fallbackuri的method為GET,而實(shí)際引起錯(cuò)誤的請(qǐng)求是通過POST發(fā)送過來的。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。