背景
在之前的文章中,我們介紹過微服務(wù)網(wǎng)關(guān)Spring Cloud Netflix Zuul腋粥,前段時(shí)間有兩篇文章專門介紹了Spring Cloud的全新項(xiàng)目Spring Cloud Gateway胶背,以及其中的過濾器工廠挽荠。本文將會(huì)介紹將微服務(wù)網(wǎng)關(guān)由Zuul遷移到Spring Cloud Gateway啦扬。
Spring Cloud Netflix Zuul是由Netflix開源的API網(wǎng)關(guān)镀迂,在微服務(wù)架構(gòu)下内狗,網(wǎng)關(guān)作為對外的門戶怪嫌,實(shí)現(xiàn)動(dòng)態(tài)路由、監(jiān)控柳沙、授權(quán)岩灭、安全、調(diào)度等功能偎行。
Zuul基于servlet 2.5(使用3.x)川背,使用阻塞API。 它不支持任何長連接蛤袒,如websockets熄云。而Gateway建立在Spring Framework 5,Project Reactor和Spring Boot 2之上妙真,使用非阻塞API缴允。 比較完美地支持異步非阻塞編程,先前的Spring系大多是同步阻塞的編程模式珍德,使用thread-per-request處理模型练般。即使在Spring MVC Controller方法上加@Async注解或返回DeferredResult、Callable類型的結(jié)果锈候,其實(shí)仍只是把方法的同步調(diào)用封裝成執(zhí)行任務(wù)放到線程池的任務(wù)隊(duì)列中薄料,還是thread-per-request模型。Gateway 中Websockets得到支持泵琳,并且由于它與Spring緊密集成摄职,所以將會(huì)是一個(gè)更好的開發(fā)體驗(yàn)。
在一個(gè)微服務(wù)集成的項(xiàng)目中microservice-integration获列,我們整合了包括網(wǎng)關(guān)谷市、auth權(quán)限服務(wù)和backend服務(wù)。提供了一套微服務(wù)架構(gòu)下击孩,網(wǎng)關(guān)服務(wù)路由迫悠、鑒權(quán)和授權(quán)認(rèn)證的項(xiàng)目案例。整個(gè)項(xiàng)目的架構(gòu)圖如下:
具體參見:微服務(wù)架構(gòu)中整合網(wǎng)關(guān)巩梢、權(quán)限服務(wù)创泄。本文將以該項(xiàng)目中的Zuul網(wǎng)關(guān)升級作為示例艺玲。
Zuul網(wǎng)關(guān)
在該項(xiàng)目中,Zuul網(wǎng)關(guān)的主要功能為路由轉(zhuǎn)發(fā)鞠抑、鑒權(quán)授權(quán)和安全訪問等功能板驳。
Zuul中,很容易配置動(dòng)態(tài)路由轉(zhuǎn)發(fā)碍拆,如:
zuul:
ribbon:
eager-load:
enabled: true #zuul饑餓加載
host:
maxTotalConnections: 200
maxPerRouteConnections: 20
routes:
user:
path: /user/**
ignoredPatterns: /consul
serviceId: user
sensitiveHeaders: Cookie,Set-Cookie
默認(rèn)情況下,Zuul在請求路由時(shí)慨蓝,會(huì)過濾HTTP請求頭信息中的一些敏感信息感混,這里我們不過多介紹。
網(wǎng)關(guān)中還配置了請求的鑒權(quán)礼烈,結(jié)合Auth服務(wù)弧满,通過Zuul自帶的Pre過濾器可以實(shí)現(xiàn)該功能。當(dāng)然還可以利用Post過濾器對請求結(jié)果進(jìn)行適配和修改等操作此熬。
除此之外庭呜,還可以配置限流過濾器和斷路器,下文中將會(huì)增加實(shí)現(xiàn)這部分功能犀忱。
遷移到Spring Cloud Gateway
筆者新建了一個(gè)gateway-enhanced
的項(xiàng)目募谎,因?yàn)樽兓艽螅贿m合在之前的gateway
項(xiàng)目基礎(chǔ)上修改阴汇。實(shí)現(xiàn)的主要功能如下:路由轉(zhuǎn)發(fā)数冬、權(quán)重路由、斷路器搀庶、限流拐纱、鑒權(quán)和黑白名單等。本文基于主要實(shí)現(xiàn)如下的三方面功能:
- 路由斷言
- 過濾器(包括全局過濾器哥倔,如斷路器秸架、限流等)
- 全局鑒權(quán)
- 路由配置
- CORS
依賴
本文采用的Spring Cloud Gateway版本為2.0.0.RELEASE
。增加的主要依賴如下咆蒿,具體的細(xì)節(jié)可以參見Github上的項(xiàng)目东抹。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<!--<version>2.0.1.RELEASE</version>-->
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-webflux</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
路由斷言
Spring Cloud Gateway對于路由斷言、過濾器和路由的定義蜡秽,同時(shí)支持配置文件的shortcut和Fluent API府阀。我們將以在本項(xiàng)目中實(shí)際使用的功能進(jìn)行講解。
路由斷言在網(wǎng)關(guān)進(jìn)行轉(zhuǎn)發(fā)請求之前進(jìn)行判斷路由的具體服務(wù)芽突,通呈哉悖可以根據(jù)請求的路徑、請求體寞蚌、請求方式(GET/POST)田巴、請求地址钠糊、請求時(shí)間、請求的HOST等信息壹哺。我們主要用到的是基于請求路徑的方式抄伍,如下:
spring:
cloud:
gateway:
routes:
- id: service_to_web
uri: lb://authdemo
predicates:
- Path=/demo/**
我們定義了一個(gè)名為service_to_web
的路由,將請求路徑以/demo/**
的請求都轉(zhuǎn)發(fā)到authdemo服務(wù)實(shí)例管宵。
我們在本項(xiàng)目中路由斷言的需求并不復(fù)雜截珍,下面介紹通過Fluent API配置的其他路由斷言:
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.host("**.changeuri.org").and().header("X-Next-Url")
.uri("http://blueskykong.com"))
.route(r -> r.host("**.changeuri.org").and().query("url")
.uri("http://blueskykong.com"))
.build();
}
在如上的路由定義中,我們配置了以及請求HOST箩朴、請求頭部和請求的參數(shù)岗喉。在一個(gè)路由定義中,可以配置多個(gè)斷言炸庞,采取與或非的關(guān)系判斷钱床。
以上增加的配置僅作為擴(kuò)展,讀者可以根據(jù)自己的需要進(jìn)行配置相應(yīng)的斷言埠居。
過濾器
過濾器分為全局過濾器和局部過濾器查牌。我們通過實(shí)現(xiàn)GlobalFilter
、GatewayFilter
接口滥壕,自定義過濾器纸颜。
全局過濾器
本項(xiàng)目中,我們配置了如下的全局過濾器:
- 基于令牌桶的限流過濾器
- 基于漏桶算法的限流過濾器
- 全局?jǐn)嗦菲?/li>
- 全局鑒權(quán)過濾器
定義全局過濾器捏浊,可以通過在配置文件中懂衩,增加spring.cloud.gateway.default-filters
,或者實(shí)現(xiàn)GlobalFilter
接口金踪。
基于令牌桶的限流過濾器
隨著時(shí)間流逝浊洞,系統(tǒng)會(huì)按恒定 1/QPS 時(shí)間間隔(如果 QPS=100,則間隔是 10ms)往桶里加入 Token胡岔,如果桶已經(jīng)滿了就不再加了法希。每個(gè)請求來臨時(shí),會(huì)拿走一個(gè) Token靶瘸,如果沒有 Token 可拿了苫亦,就阻塞或者拒絕服務(wù)。
令牌桶的另外一個(gè)好處是可以方便的改變速度怨咪。一旦需要提高速率屋剑,則按需提高放入桶中的令牌的速率。一般會(huì)定時(shí)(比如 100 毫秒)往桶中增加一定數(shù)量的令牌诗眨,有些變種算法則實(shí)時(shí)的計(jì)算應(yīng)該增加的令牌的數(shù)量唉匾。
在Spring Cloud Gateway中提供了默認(rèn)的實(shí)現(xiàn),我們需要引入redis的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
并進(jìn)行如下的配置:
spring:
redis:
host: localhost
password: pwd
port: 6378
cloud:
default-filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@remoteAddrKeyResolver}"
rate-limiter: "#{@customRateLimiter}" # token
注意到,在配置中使用了兩個(gè)SpEL表達(dá)式巍膘,分別定義限流鍵和限流的配置厂财。因此,我們需要在實(shí)現(xiàn)中增加如下的配置:
@Bean(name = "customRateLimiter")
public RedisRateLimiter myRateLimiter(GatewayLimitProperties gatewayLimitProperties) {
GatewayLimitProperties.RedisRate redisRate = gatewayLimitProperties.getRedisRate();
if (Objects.isNull(redisRate)) {
throw new ServerException(ErrorCodes.PROPERTY_NOT_INITIAL);
}
return new RedisRateLimiter(redisRate.getReplenishRate(), redisRate.getBurstCapacity());
}
@Bean(name = RemoteAddrKeyResolver.BEAN_NAME)
public RemoteAddrKeyResolver remoteAddrKeyResolver() {
return new RemoteAddrKeyResolver();
}
在如上的實(shí)現(xiàn)中峡懈,初始化好RedisRateLimiter
和RemoteAddrKeyResolver
兩個(gè)Bean實(shí)例璃饱,RedisRateLimiter
是定義在Gateway中的redis限流屬性;而RemoteAddrKeyResolver
使我們自定義的肪康,基于請求的地址作為限流鍵荚恶。如下為該限流鍵的定義:
public class RemoteAddrKeyResolver implements KeyResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(RemoteAddrKeyResolver.class);
public static final String BEAN_NAME = "remoteAddrKeyResolver";
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
LOGGER.debug("token limit for ip: {} ", exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
RemoteAddrKeyResolver
實(shí)現(xiàn)了KeyResolver
接口,覆寫其中定義的接口磷支,返回值為請求中的地址裆甩。
如上,即實(shí)現(xiàn)了基于令牌桶算法的鏈路過濾器齐唆,具體細(xì)節(jié)不再展開。
基于漏桶算法的限流過濾器
漏桶(Leaky Bucket)算法思路很簡單冻河,水(請求)先進(jìn)入到漏桶里箍邮,漏桶以一定的速度出水(接口有響應(yīng)速率),當(dāng)水流入速度過大會(huì)直接溢出(訪問頻率超過接口響應(yīng)速率)叨叙,然后就拒絕請求锭弊,可以看出漏桶算法能強(qiáng)行限制數(shù)據(jù)的傳輸速率。
這部分實(shí)現(xiàn)讀者參見GitHub項(xiàng)目以及文末配套的書擂错,此處略過味滞。
全局?jǐn)嗦菲?/h5>
關(guān)于Hystrix斷路器,是一種服務(wù)容錯(cuò)的保護(hù)措施钮呀。斷路器
本身是一種開關(guān)裝置剑鞍,用于在電路上保護(hù)線路過載,當(dāng)線路中有發(fā)生短路狀況時(shí)爽醋,斷路器
能夠及時(shí)的切斷故障電路蚁署,防止發(fā)生過載、起火等情況蚂四。
微服務(wù)架構(gòu)中光戈,斷路器模式的作用也是類似的,當(dāng)某個(gè)服務(wù)單元發(fā)生故障之后遂赠,通過斷路器的故障監(jiān)控久妆,直接切斷原來的主邏輯調(diào)用。關(guān)于斷路器的更多資料和Hystrix實(shí)現(xiàn)原理跷睦,讀者可以參考文末配套的書筷弦。
這里需要引入spring-cloud-starter-netflix-hystrix
依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<optional>true</optional>
</dependency>
并增加如下的配置:
default-filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/fallbackcontroller
如上的配置,將會(huì)使用HystrixCommand
打包剩余的過濾器送讲,并命名為fallbackcmd
奸笤,我們還配置了可選的參數(shù)fallbackUri
惋啃,降級邏輯被調(diào)用,請求將會(huì)被轉(zhuǎn)發(fā)到URI為/fallbackcontroller
的控制器處理监右。定義降級處理如下:
@RequestMapping(value = "/fallbackcontroller")
public Map<String, String> fallBackController() {
Map<String, String> res = new HashMap();
res.put("code", "-100");
res.put("data", "service not available");
return res;
}
全局鑒權(quán)過濾器
我們通過自定義一個(gè)全局過濾器實(shí)現(xiàn)边灭,對請求合法性的鑒權(quán)。具體功能不再贅述了健盒,通過實(shí)現(xiàn)GlobalFilter
接口绒瘦,區(qū)別的是Webflux傳入的是ServerWebExchange
,通過判斷是不是外部接口(外部接口不需要登錄鑒權(quán))扣癣,執(zhí)行之前實(shí)現(xiàn)的處理邏輯惰帽。
public class AuthorizationFilter implements GlobalFilter, Ordered {
//....
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (predicate(exchange)) {
request = headerEnhanceFilter.doFilter(request);
String accessToken = extractHeaderToken(request);
customRemoteTokenServices.loadAuthentication(accessToken);
LOGGER.info("success auth token and permission!");
}
return chain.filter(exchange);
}
//提出頭部的token
protected String extractHeaderToken(ServerHttpRequest request) {
List<String> headers = request.getHeaders().get("Authorization");
if (Objects.nonNull(headers) && headers.size() > 0) { // typically there is only one (most servers enforce that)
String value = headers.get(0);
if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
// Add this here for the auth details later. Would be better to change the signature of this method.
int commaIndex = authHeaderValue.indexOf(',');
if (commaIndex > 0) {
authHeaderValue = authHeaderValue.substring(0, commaIndex);
}
return authHeaderValue;
}
}
return null;
}
}
定義好全局過濾器之后,只需要配置一下即可:
@Bean
public AuthorizationFilter authorizationFilter(CustomRemoteTokenServices customRemoteTokenServices,
HeaderEnhanceFilter headerEnhanceFilter,
PermitAllUrlProperties permitAllUrlProperties) {
return new AuthorizationFilter(customRemoteTokenServices, headerEnhanceFilter, permitAllUrlProperties);
}
局部過濾器
我們常用的局部過濾器有增減請求和相應(yīng)頭部父虑、增減請求的路徑等多種過濾器该酗。我們這里用到的是去除請求的指定前綴,這部分前綴只是用戶網(wǎng)關(guān)進(jìn)行路由判斷士嚎,在轉(zhuǎn)發(fā)到具體服務(wù)時(shí)呜魄,需要去除前綴:
- id: service_to_user
uri: lb://user
order: 8000
predicates:
- Path=/user/**
filters:
- AddRequestHeader=X-Request-Foo, Bar
- StripPrefix=1
還可以通過Fluent API,如下:
@Bean
public RouteLocator retryRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("retry_java", r -> r.path("/test/**")
.filters(f -> f.stripPrefix(1)
.retry(config -> config.setRetries(2).setStatuses(HttpStatus.INTERNAL_SERVER_ERROR)))
.uri("lb://user"))
.build();
}
除了設(shè)置前綴過濾器外莱衩,我們還設(shè)置了重試過濾器爵嗅,可以參見:Spring Cloud Gateway中的過濾器工廠:重試過濾器
路由配置
路由定義在上面的示例中已經(jīng)有列出,可以通過配置文件和定義RouteLocator
的對象笨蚁。這里需要注意的是睹晒,配置中的uri
屬性,可以是具體的服務(wù)地址(IP+端口號)括细,也可以是通過服務(wù)發(fā)現(xiàn)加上負(fù)載均衡定義的:lb://user
伪很,表示轉(zhuǎn)發(fā)到user的服務(wù)實(shí)例。當(dāng)然這需要我們進(jìn)行一些配置奋单。
引入服務(wù)發(fā)現(xiàn)的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
網(wǎng)關(guān)中開啟spring.cloud.gateway.discovery.locator.enabled=true
即可是掰。
CORS配置
在Spring 5 Webflux中,配置CORS辱匿,可以通過自定義WebFilter
實(shí)現(xiàn):
private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN";
private static final String ALLOWED_METHODS = "GET, PUT, POST, DELETE, OPTIONS";
private static final String ALLOWED_ORIGIN = "*";
private static final String MAX_AGE = "3600";
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
headers.add("Access-Control-Max-Age", MAX_AGE);
headers.add("Access-Control-Allow-Headers",ALLOWED_HEADERS);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
上述代碼實(shí)現(xiàn)比較簡單键痛,讀者根據(jù)實(shí)際的需要配置ALLOWED_ORIGIN
等參數(shù)。
總結(jié)
在高并發(fā)和潛在的高延遲場景下匾七,網(wǎng)關(guān)要實(shí)現(xiàn)高性能高吞吐量的一個(gè)基本要求是全鏈路異步絮短,不要阻塞線程。Zuul網(wǎng)關(guān)采用同步阻塞模式不符合要求昨忆。
Spring Cloud Gateway基于Webflux丁频,比較完美地支持異步非阻塞編程,很多功能實(shí)現(xiàn)起來比較方便。Spring5必須使用java 8席里,函數(shù)式編程就是java8重要的特點(diǎn)之一叔磷,而WebFlux支持函數(shù)式編程來定義路由端點(diǎn)處理請求。
通過如上的實(shí)現(xiàn)奖磁,我們將網(wǎng)關(guān)從Zuul遷移到了Spring Cloud Gateway改基。在Gateway中定義了豐富的路由斷言和過濾器,通過配置文件或者Fluent API可以直接調(diào)用和使用咖为,非常方便秕狰。在性能上,也是勝于之前的Zuul網(wǎng)關(guān)躁染。
欲了解更詳細(xì)的實(shí)現(xiàn)原理和細(xì)節(jié)鸣哀,大家可以關(guān)注筆者本月底即將出版的《Spring Cloud 微服務(wù)架構(gòu)進(jìn)階》,本書中對Spring Cloud Finchley.RELEASE
版本的各個(gè)主要組件進(jìn)行原理講解和實(shí)戰(zhàn)應(yīng)用吞彤,網(wǎng)關(guān)則是基于最新的Spring Cloud Gateway我衬。
本文的源碼地址:
GitHub:https://github.com/keets2012/microservice-integration
或者 碼云:https://gitee.com/keets/microservice-integration