微服务网关Zuul迁移到Spring Cloud Gateway

背景

在以前的文章中,咱们介绍过微服务网关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网关

在该项目中,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过滤器对请求结果进行适配和修改等操做。编程

除此以外,还能够配置限流过滤器和断路器,下文中将会增长实现这部分功能。安全

迁移到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(限流)

相关文章
相关标签/搜索