微服務閘道器Zuul遷移到Spring Cloud Gateway

aoho發表於2018-09-24

背景

在之前的文章中,我們介紹過微服務閘道器Spring Cloud Netflix Zuul,前段時間有兩篇文章專門介紹了Spring Cloud的全新專案Spring Cloud Gateway,以及其中的過濾器工廠。本文將會介紹將微服務閘道器由Zuul遷移到Spring Cloud Gateway。

Spring Cloud Netflix Zuul是由Netflix開源的API閘道器,在微服務架構下,閘道器作為對外的門戶,實現動態路由、監控、授權、安全、排程等功能。

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緊密整合,所以將會是一個更好的開發體驗。

在一個微服務整合的專案中microservice-integration,我們整合了包括閘道器、auth許可權服務和backend服務。提供了一套微服務架構下,閘道器服務路由、鑑權和授權認證的專案案例。整個專案的架構圖如下:

微服務閘道器Zuul遷移到Spring Cloud Gateway

具體參見:微服務架構中整合閘道器、許可權服務。本文將以該專案中的Zuul閘道器升級作為示例。

Zuul閘道器

在該專案中,Zuul閘道器的主要功能為路由轉發、鑑權授權和安全訪問等功能。

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請求頭資訊中的一些敏感資訊,這裡我們不過多介紹。

閘道器中還配置了請求的鑑權,結合Auth服務,通過Zuul自帶的Pre過濾器可以實現該功能。當然還可以利用Post過濾器對請求結果進行適配和修改等操作。

除此之外,還可以配置限流過濾器和斷路器,下文中將會增加實現這部分功能。

遷移到Spring Cloud Gateway

筆者新建了一個gateway-enhanced的專案,因為變化很大,不適合在之前的gateway專案基礎上修改。實現的主要功能如下:路由轉發、權重路由、斷路器、限流、鑑權和黑白名單等。本文基於主要實現如下的三方面功能:

  • 路由斷言
  • 過濾器(包括全域性過濾器,如斷路器、限流等)
  • 全域性鑑權
  • 路由配置
  • CORS

依賴

本文采用的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、請求頭部和請求的引數。在一個路由定義中,可以配置多個斷言,採取與或非的關係判斷。

以上增加的配置僅作為擴充套件,讀者可以根據自己的需要進行配置相應的斷言。

過濾器

過濾器分為全域性過濾器和區域性過濾器。我們通過實現GlobalFilterGatewayFilter介面,自定義過濾器。

全域性過濾器

本專案中,我們配置瞭如下的全域性過濾器:

  • 基於令牌桶的限流過濾器
  • 基於漏桶演算法的限流過濾器
  • 全域性斷路器
  • 全域性鑑權過濾器

定義全域性過濾器,可以通過在配置檔案中,增加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();
    }

複製程式碼

在如上的實現中,初始化好RedisRateLimiterRemoteAddrKeyResolver兩個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即可。

CORS配置

在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。

Spring Cloud 微服務架構進階

本文的原始碼地址:
GitHub:github.com/keets2012/m… 或者 碼雲:gitee.com/keets/micro…

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

Spring Cloud Gateway(限流)

相關文章