在以前的文章中,咱們介紹過微服務網關Spring Cloud Netflix Zuul,前段時間有兩篇文章專門介紹了Spring Cloud的全新項目Spring Cloud Gateway,以及其中的過濾器工廠。本文將會介紹將微服務網關由Zuul遷移到Spring Cloud Gateway。java
Spring Cloud Netflix Zuul是由Netflix開源的API網關,在微服務架構下,網關做爲對外的門戶,實現動態路由、監控、受權、安全、調度等功能。react
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類型的結果,其實仍只是把方法的同步調用封裝成執行任務放到線程池的任務隊列中,仍是thread-per-request模型。Gateway 中Websockets獲得支持,而且因爲它與Spring緊密集成,因此將會是一個更好的開發體驗。git
在一個微服務集成的項目中microservice-integration,咱們整合了包括網關、auth權限服務和backend服務。提供了一套微服務架構下,網關服務路由、鑑權和受權認證的項目案例。整個項目的架構圖以下:github
具體參見:微服務架構中整合網關、權限服務。本文將以該項目中的Zuul網關升級做爲示例。web
在該項目中,Zuul網關的主要功能爲路由轉發、鑑權受權和安全訪問等功能。redis
Zuul中,很容易配置動態路由轉發,如:算法
zuul:
ribbon:
eager-load:
enabled: true #zuul飢餓加載
host:
maxTotalConnections: 200
maxPerRouteConnections: 20
routes:
user:
path: /user/**
ignoredPatterns: /consul
serviceId: user
sensitiveHeaders: Cookie,Set-Cookie
複製代碼
默認狀況下,Zuul在請求路由時,會過濾HTTP請求頭信息中的一些敏感信息,這裏咱們不過多介紹。spring
網關中還配置了請求的鑑權,結合Auth服務,經過Zuul自帶的Pre過濾器能夠實現該功能。固然還能夠利用Post過濾器對請求結果進行適配和修改等操做。編程
除此以外,還能夠配置限流過濾器和斷路器,下文中將會增長實現這部分功能。安全
筆者新建了一個gateway-enhanced
的項目,由於變化很大,不適合在以前的gateway
項目基礎上修改。實現的主要功能以下:路由轉發、權重路由、斷路器、限流、鑑權和黑白名單等。本文基於主要實現以下的三方面功能:
本文采用的Spring Cloud Gateway版本爲2.0.0.RELEASE
。增長的主要依賴以下,具體的細節能夠參見Github上的項目。
<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對於路由斷言、過濾器和路由的定義,同時支持配置文件的shortcut和Fluent API。咱們將以在本項目中實際使用的功能進行講解。
路由斷言在網關進行轉發請求以前進行判斷路由的具體服務,一般能夠根據請求的路徑、請求體、請求方式(GET/POST)、請求地址、請求時間、請求的HOST等信息。咱們主要用到的是基於請求路徑的方式,以下:
spring:
cloud:
gateway:
routes:
- id: service_to_web
uri: lb://authdemo
predicates:
- Path=/demo/**
複製代碼
咱們定義了一個名爲service_to_web
的路由,將請求路徑以/demo/**
的請求都轉發到authdemo服務實例。
咱們在本項目中路由斷言的需求並不複雜,下面介紹經過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、請求頭部和請求的參數。在一個路由定義中,能夠配置多個斷言,採起與或非的關係判斷。
以上增長的配置僅做爲擴展,讀者能夠根據本身的須要進行配置相應的斷言。
過濾器分爲全局過濾器和局部過濾器。咱們經過實現GlobalFilter
、GatewayFilter
接口,自定義過濾器。
本項目中,咱們配置了以下的全局過濾器:
定義全局過濾器,能夠經過在配置文件中,增長spring.cloud.gateway.default-filters
,或者實現GlobalFilter
接口。
隨着時間流逝,系統會按恆定 1/QPS 時間間隔(若是 QPS=100,則間隔是 10ms)往桶裏加入 Token,若是桶已經滿了就再也不加了。每一個請求來臨時,會拿走一個 Token,若是沒有 Token 可拿了,就阻塞或者拒絕服務。
令牌桶的另一個好處是能夠方便的改變速度。一旦須要提升速率,則按需提升放入桶中的令牌的速率。通常會定時(好比 100 毫秒)往桶中增長必定數量的令牌,有些變種算法則實時的計算應該增長的令牌的數量。
在Spring Cloud Gateway中提供了默認的實現,咱們須要引入redis的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
複製代碼
並進行以下的配置:
spring:
redis:
host: localhost
password: pwd
port: 6378
cloud:
default-filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@remoteAddrKeyResolver}"
rate-limiter: "#{@customRateLimiter}" # token
複製代碼
注意到,在配置中使用了兩個SpEL表達式,分別定義限流鍵和限流的配置。所以,咱們須要在實現中增長以下的配置:
@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();
}
複製代碼
在如上的實現中,初始化好RedisRateLimiter
和RemoteAddrKeyResolver
兩個Bean實例,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
實現了KeyResolver
接口,覆寫其中定義的接口,返回值爲請求中的地址。
如上,即實現了基於令牌桶算法的鏈路過濾器,具體細節再也不展開。
漏桶(Leaky Bucket)算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以必定的速度出水(接口有響應速率),當水流入速度過大會直接溢出(訪問頻率超過接口響應速率),而後就拒絕請求,能夠看出漏桶算法能強行限制數據的傳輸速率。
這部分實現讀者參見GitHub項目以及文末配套的書,此處略過。
關於Hystrix斷路器,是一種服務容錯的保護措施。斷路器
自己是一種開關裝置,用於在電路上保護線路過載,當線路中有發生短路情況時,斷路器
可以及時的切斷故障電路,防止發生過載、起火等狀況。
微服務架構中,斷路器模式的做用也是相似的,當某個服務單元發生故障以後,經過斷路器的故障監控,直接切斷原來的主邏輯調用。關於斷路器的更多資料和Hystrix實現原理,讀者能夠參考文末配套的書。
這裏須要引入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
複製代碼
如上的配置,將會使用HystrixCommand
打包剩餘的過濾器,並命名爲fallbackcmd
,咱們還配置了可選的參數fallbackUri
,降級邏輯被調用,請求將會被轉發到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;
}
複製代碼
咱們經過自定義一個全局過濾器實現,對請求合法性的鑑權。具體功能再也不贅述了,經過實現GlobalFilter
接口,區別的是Webflux傳入的是ServerWebExchange
,經過判斷是否是外部接口(外部接口不須要登陸鑑權),執行以前實現的處理邏輯。
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);
}
複製代碼
咱們經常使用的局部過濾器有增減請求和相應頭部、增減請求的路徑等多種過濾器。咱們這裏用到的是去除請求的指定前綴,這部分前綴只是用戶網關進行路由判斷,在轉發到具體服務時,須要去除前綴:
- 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();
}
複製代碼
除了設置前綴過濾器外,咱們還設置了重試過濾器,能夠參見:Spring Cloud Gateway中的過濾器工廠:重試過濾器
路由定義在上面的示例中已經有列出,能夠經過配置文件和定義RouteLocator
的對象。這裏須要注意的是,配置中的uri
屬性,能夠是具體的服務地址(IP+端口號),也能夠是經過服務發現加上負載均衡定義的:lb://user
,表示轉發到user的服務實例。固然這須要咱們進行一些配置。
引入服務發現的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
複製代碼
網關中開啓spring.cloud.gateway.discovery.locator.enabled=true
便可。
在Spring 5 Webflux中,配置CORS,能夠經過自定義WebFilter
實現:
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);
};
}
複製代碼
上述代碼實現比較簡單,讀者根據實際的須要配置ALLOWED_ORIGIN
等參數。
在高併發和潛在的高延遲場景下,網關要實現高性能高吞吐量的一個基本要求是全鏈路異步,不要阻塞線程。Zuul網關採用同步阻塞模式不符合要求。
Spring Cloud Gateway基於Webflux,比較完美地支持異步非阻塞編程,不少功能實現起來比較方便。Spring5必須使用java 8,函數式編程就是java8重要的特色之一,而WebFlux支持函數式編程來定義路由端點處理請求。
經過如上的實現,咱們將網關從Zuul遷移到了Spring Cloud Gateway。在Gateway中定義了豐富的路由斷言和過濾器,經過配置文件或者Fluent API能夠直接調用和使用,很是方便。在性能上,也是勝於以前的Zuul網關。
欲瞭解更詳細的實現原理和細節,你們能夠關注筆者本月底即將出版的《Spring Cloud 微服務架構進階》,本書中對Spring Cloud Finchley.RELEASE
版本的各個主要組件進行原理講解和實戰應用,網關則是基於最新的Spring Cloud Gateway。
本文的源碼地址:
GitHub:github.com/keets2012/m… 或者 碼雲:gitee.com/keets/micro…