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

本文涉及的产品
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介:

背景

在之前的文章中,我们介绍过微服务网关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服务。提供了一套微服务架构下,网关服务路由、鉴权和授权认证的项目案例。整个项目的架构图如下:

11f0395f3f70b2d790978eab02f4ddd2c6550c4f

Zuul网关

在该项目中,Zuul网关的主要功能为路由转发、鉴权授权和安全访问等功能。

Zuul中,很容易配置动态路由转发,如:

 1zuul:
2  ribbon:
3    eager-load:
4      enabled: true     #zuul饥饿加载
5  host:
6    maxTotalConnections: 200
7    maxPerRouteConnections: 20
8  routes:
9    user:
10      path: /user/**
11      ignoredPatterns: /consul
12      serviceId: user
13      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上的项目。

 1    <dependencies>
2        <dependency>
3            <groupId>org.springframework.cloud</groupId>
4            <artifactId>spring-cloud-starter-gateway</artifactId>
5            <!--<version>2.0.1.RELEASE</version>-->
6        </dependency>
7        <dependency>
8            <groupId>org.springframework.cloud</groupId>
9            <artifactId>spring-cloud-gateway-webflux</artifactId>
10        </dependency>
11    </dependencies>
12
13    <dependencyManagement>
14        <dependencies>
15            <dependency>
16                <groupId>org.springframework.cloud</groupId>
17                <artifactId>spring-cloud-dependencies</artifactId>
18                <version>Finchley.RELEASE</version>
19                <type>pom</type>
20                <scope>import</scope>
21            </dependency>
22        </dependencies>
23    </dependencyManagement>        

路由断言

Spring Cloud Gateway对于路由断言、过滤器和路由的定义,同时支持配置文件的shortcut和Fluent API。我们将以在本项目中实际使用的功能进行讲解。

路由断言在网关进行转发请求之前进行判断路由的具体服务,通常可以根据请求的路径、请求体、请求方式(GET/POST)、请求地址、请求时间、请求的HOST等信息。我们主要用到的是基于请求路径的方式,如下:

1spring:
2  cloud:
3    gateway:
4      routes:
5      - id: service_to_web
6        uri: lb://authdemo
7        predicates:
8        - Path=/demo/**

我们定义了一个名为service_to_web的路由,将请求路径以/demo/**的请求都转发到authdemo服务实例。

我们在本项目中路由断言的需求并不复杂,下面介绍通过Fluent API配置的其他路由断言:

1    @Bean
2    public RouteLocator routeLocator(RouteLocatorBuilder builder) {
3        return builder.routes()
4                .route(r -> r.host("**.changeuri.org").and().header("X-Next-Url")
5                        .uri("http://blueskykong.com"))
6                .route(r -> r.host("**.changeuri.org").and().query("url")
7                        .uri("http://blueskykong.com"))
8                .build();
9    }

在如上的路由定义中,我们配置了以及请求HOST、请求头部和请求的参数。在一个路由定义中,可以配置多个断言,采取与或非的关系判断。

以上增加的配置仅作为扩展,读者可以根据自己的需要进行配置相应的断言。

过滤器

过滤器分为全局过滤器和局部过滤器。我们通过实现GlobalFilterGatewayFilter接口,自定义过滤器。

全局过滤器

本项目中,我们配置了如下的全局过滤器:

  • 基于令牌桶的限流过滤器

  • 基于漏桶算法的限流过滤器

  • 全局断路器

  • 全局鉴权过滤器

定义全局过滤器,可以通过在配置文件中,增加spring.cloud.gateway.default-filters,或者实现GlobalFilter接口。

基于令牌桶的限流过滤器

随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token,如果桶已经满了就不再加了。每个请求来临时,会拿走一个 Token,如果没有 Token 可拿了,就阻塞或者拒绝服务。

令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。

在Spring Cloud Gateway中提供了默认的实现,我们需要引入redis的依赖:

1        <dependency>
2            <groupId>org.springframework.boot</groupId>
3            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
4        </dependency>

并进行如下的配置:

 1spring:
2  redis:
3    host: localhost
4    password: pwd
5    port: 6378
6  cloud:
7    default-filters:
8      - name: RequestRateLimiter
9        args:
10          key-resolver: "#{@remoteAddrKeyResolver}"
11          rate-limiter: "#{@customRateLimiter}"   # token

注意到,在配置中使用了两个SpEL表达式,分别定义限流键和限流的配置。因此,我们需要在实现中增加如下的配置:

 1    @Bean(name = "customRateLimiter")
2    public RedisRateLimiter myRateLimiter(GatewayLimitProperties gatewayLimitProperties) {
3        GatewayLimitProperties.RedisRate redisRate = gatewayLimitProperties.getRedisRate();
4        if (Objects.isNull(redisRate)) {
5            throw new ServerException(ErrorCodes.PROPERTY_NOT_INITIAL);
6        }
7        return new RedisRateLimiter(redisRate.getReplenishRate(), redisRate.getBurstCapacity());
8    }
9
10        @Bean(name = RemoteAddrKeyResolver.BEAN_NAME)
11    public RemoteAddrKeyResolver remoteAddrKeyResolver() {
12        return new RemoteAddrKeyResolver();
13    }

在如上的实现中,初始化好RedisRateLimiterRemoteAddrKeyResolver两个Bean实例,RedisRateLimiter是定义在Gateway中的redis限流属性;而RemoteAddrKeyResolver使我们自定义的,基于请求的地址作为限流键。如下为该限流键的定义:

 1public class RemoteAddrKeyResolver implements KeyResolver {
2    private static final Logger LOGGER = LoggerFactory.getLogger(RemoteAddrKeyResolver.class);
3
4    public static final String BEAN_NAME = "remoteAddrKeyResolver";
5
6    @Override
7    public Mono<String> resolve(ServerWebExchange exchange) {
8        LOGGER.debug("token limit for ip: {} ", exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
9        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
10    }
11
12}

RemoteAddrKeyResolver实现了KeyResolver接口,覆写其中定义的接口,返回值为请求中的地址。

如上,即实现了基于令牌桶算法的链路过滤器,具体细节不再展开。

基于漏桶算法的限流过滤器

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

这部分实现读者参见GitHub项目以及文末配套的书,此处略过。

全局断路器

关于Hystrix断路器,是一种服务容错的保护措施。断路器本身是一种开关装置,用于在电路上保护线路过载,当线路中有发生短路状况时,断路器能够及时的切断故障电路,防止发生过载、起火等情况。

微服务架构中,断路器模式的作用也是类似的,当某个服务单元发生故障之后,通过断路器的故障监控,直接切断原来的主逻辑调用。关于断路器的更多资料和Hystrix实现原理,读者可以参考文末配套的书。

这里需要引入spring-cloud-starter-netflix-hystrix依赖:

1        <dependency>
2            <groupId>org.springframework.cloud</groupId>
3            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
4            <optional>true</optional>
5        </dependency>

并增加如下的配置:

1      default-filters:
2      - name: Hystrix
3        args:
4          name: fallbackcmd
5          fallbackUri: forward:/fallbackcontroller

如上的配置,将会使用HystrixCommand打包剩余的过滤器,并命名为fallbackcmd,我们还配置了可选的参数fallbackUri,降级逻辑被调用,请求将会被转发到URI为/fallbackcontroller的控制器处理。定义降级处理如下:

1    @RequestMapping(value = "/fallbackcontroller")
2    public Map<String, String> fallBackController() {
3        Map<String, String> res = new HashMap();
4        res.put("code""-100");
5        res.put("data""service not available");
6        return res;
7    }
全局鉴权过滤器

我们通过自定义一个全局过滤器实现,对请求合法性的鉴权。具体功能不再赘述了,通过实现GlobalFilter接口,区别的是Webflux传入的是ServerWebExchange,通过判断是不是外部接口(外部接口不需要登录鉴权),执行之前实现的处理逻辑。

 1public class AuthorizationFilter implements GlobalFilterOrdered {
2
3    //....
4
5    @Override
6    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
7        ServerHttpRequest request = exchange.getRequest();
8        if (predicate(exchange)) {
9            request = headerEnhanceFilter.doFilter(request);
10            String accessToken = extractHeaderToken(request);
11
12            customRemoteTokenServices.loadAuthentication(accessToken);
13            LOGGER.info("success auth token and permission!");
14        }
15
16        return chain.filter(exchange);
17    }
18    //提出头部的token
19    protected String extractHeaderToken(ServerHttpRequest request) {
20        List<String> headers = request.getHeaders().get("Authorization");
21        if (Objects.nonNull(headers) && headers.size() > 0) { // typically there is only one (most servers enforce that)
22            String value = headers.get(0);
23            if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
24                String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
25                // Add this here for the auth details later. Would be better to change the signature of this method.
26                int commaIndex = authHeaderValue.indexOf(',');
27                if (commaIndex > 0) {
28                    authHeaderValue = authHeaderValue.substring(0, commaIndex);
29                }
30                return authHeaderValue;
31            }
32        }
33
34        return null;
35    }
36
37}

定义好全局过滤器之后,只需要配置一下即可:

1    @Bean
2    public AuthorizationFilter authorizationFilter(CustomRemoteTokenServices customRemoteTokenServices,
3                                                   HeaderEnhanceFilter headerEnhanceFilter,
4                                                   PermitAllUrlProperties permitAllUrlProperties)
 
{
5        return new AuthorizationFilter(customRemoteTokenServices, headerEnhanceFilter, permitAllUrlProperties);
6    }

局部过滤器

我们常用的局部过滤器有增减请求和相应头部、增减请求的路径等多种过滤器。我们这里用到的是去除请求的指定前缀,这部分前缀只是用户网关进行路由判断,在转发到具体服务时,需要去除前缀:

1      - id: service_to_user
2        uri: lb://user
3        order: 8000
4        predicates:
5        - Path=/user/**
6        filters:
7        - AddRequestHeader=X-Request-Foo, Bar
8        - StripPrefix=1

还可以通过Fluent API,如下:

1    @Bean
2    public RouteLocator retryRouteLocator(RouteLocatorBuilder builder) {
3        return builder.routes()
4                .route("retry_java", r -> r.path("/test/**")
5                        .filters(f -> f.stripPrefix(1)
6                                .retry(config -> config.setRetries(2).setStatuses(HttpStatus.INTERNAL_SERVER_ERROR)))
7                        .uri("lb://user"))
8                .build();
9    }

除了设置前缀过滤器外,我们还设置了重试过滤器,可以参见:Spring Cloud Gateway中的过滤器工厂:重试过滤器

路由配置

路由定义在上面的示例中已经有列出,可以通过配置文件和定义RouteLocator的对象。这里需要注意的是,配置中的uri属性,可以是具体的服务地址(IP+端口号),也可以是通过服务发现加上负载均衡定义的:lb://user,表示转发到user的服务实例。当然这需要我们进行一些配置。

引入服务发现的依赖:

1        <dependency>
2            <groupId>org.springframework.cloud</groupId>
3            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
4        </dependency>

网关中开启spring.cloud.gateway.discovery.locator.enabled=true即可。

CORS配置

在Spring 5 Webflux中,配置CORS,可以通过自定义WebFilter实现:

 1    private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN";
2    private static final String ALLOWED_METHODS = "GET, PUT, POST, DELETE, OPTIONS";
3    private static final String ALLOWED_ORIGIN = "*";
4    private static final String MAX_AGE = "3600";
5
6    @Bean
7    public WebFilter corsFilter() {
8        return (ServerWebExchange ctx, WebFilterChain chain) -> {
9            ServerHttpRequest request = ctx.getRequest();
10            if (CorsUtils.isCorsRequest(request)) {
11                ServerHttpResponse response = ctx.getResponse();
12                HttpHeaders headers = response.getHeaders();
13                headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
14                headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
15                headers.add("Access-Control-Max-Age", MAX_AGE);
16                headers.add("Access-Control-Allow-Headers",ALLOWED_HEADERS);
17                if (request.getMethod() == HttpMethod.OPTIONS) {
18                    response.setStatusCode(HttpStatus.OK);
19                    return Mono.empty();
20                }
21            }
22            return chain.filter(ctx);
23        };
24    }

上述代码实现比较简单,读者根据实际的需要配置ALLOWED_ORIGIN等参数。

总结

在高并发和潜在的高延迟场景下,网关要实现高性能高吞吐量的一个基本要求是全链路异步,不要阻塞线程。Zuul网关采用同步阻塞模式不符合要求。

Spring Cloud Gateway基于Webflux,比较完美地支持异步非阻塞编程,很多功能实现起来比较方便。Spring5必须使用java 8,函数式编程就是java8重要的特点之一,而WebFlux支持函数式编程来定义路由端点处理请求。

通过如上的实现,我们将网关从Zuul迁移到了Spring Cloud Gateway。在Gateway中定义了丰富的路由断言和过滤器,通过配置文件或者Fluent API可以直接调用和使用,非常方便。在性能上,也是胜于之前的Zuul网关。



原文发布时间为:2018-10-16
本文作者:aoho
本文来自云栖社区合作伙伴“云时代架构”,了解相关信息可以关注“云时代架构”。
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6天前
|
NoSQL Java 关系型数据库
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
本文介绍在 Spring Boot 中集成 Redis 的方法。Redis 是一种支持多种数据结构的非关系型数据库(NoSQL),具备高并发、高性能和灵活扩展的特点,适用于缓存、实时数据分析等场景。其数据以键值对形式存储,支持字符串、哈希、列表、集合等类型。通过将 Redis 与 Mysql 集群结合使用,可实现数据同步,提升系统稳定性。例如,在网站架构中优先从 Redis 获取数据,故障时回退至 Mysql,确保服务不中断。
43 0
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
|
6天前
|
安全 Java Apache
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 身份和权限认证
本文介绍了 Apache Shiro 的身份认证与权限认证机制。在身份认证部分,分析了 Shiro 的认证流程,包括应用程序调用 `Subject.login(token)` 方法、SecurityManager 接管认证以及通过 Realm 进行具体的安全验证。权限认证部分阐述了权限(permission)、角色(role)和用户(user)三者的关系,其中用户可拥有多个角色,角色则对应不同的权限组合,例如普通用户仅能查看或添加信息,而管理员可执行所有操作。
39 0
|
6天前
|
安全 Java 数据安全/隐私保护
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 三大核心组件
本课程介绍如何在Spring Boot中集成Shiro框架,主要讲解Shiro的认证与授权功能。Shiro是一个简单易用的Java安全框架,用于认证、授权、加密和会话管理等。其核心组件包括Subject(认证主体)、SecurityManager(安全管理员)和Realm(域)。Subject负责身份认证,包含Principals(身份)和Credentials(凭证);SecurityManager是架构核心,协调内部组件运作;Realm则是连接Shiro与应用数据的桥梁,用于访问用户账户及权限信息。通过学习,您将掌握Shiro的基本原理及其在项目中的应用。
40 0
|
6天前
|
Java 数据安全/隐私保护 微服务
微服务——SpringBoot使用归纳——Spring Boot中使用监听器——Spring Boot中自定义事件监听
本文介绍了在Spring Boot中实现自定义事件监听的完整流程。首先通过继承`ApplicationEvent`创建自定义事件,例如包含用户数据的`MyEvent`。接着,实现`ApplicationListener`接口构建监听器,用于捕获并处理事件。最后,在服务层通过`ApplicationContext`发布事件,触发监听器执行相应逻辑。文章结合微服务场景,展示了如何在微服务A处理完逻辑后通知微服务B,具有很强的实战意义。
29 0
|
6天前
|
缓存 Java 数据库
微服务——SpringBoot使用归纳——Spring Boot中使用监听器——监听器介绍和使用
本文介绍了在Spring Boot中使用监听器的方法。首先讲解了Web监听器的概念,即通过监听特定事件(如ServletContext、HttpSession和ServletRequest的创建与销毁)实现监控和处理逻辑。接着详细说明了三种实际应用场景:1) 监听Servlet上下文对象以初始化缓存数据;2) 监听HTTP会话Session对象统计在线用户数;3) 监听客户端请求的Servlet Request对象获取访问信息。每种场景均配有代码示例,帮助开发者理解并应用监听器功能。
27 0
|
6天前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——常见问题总结
本文总结了Spring Boot中使用事务的常见问题,虽然通过`@Transactional`注解可以轻松实现事务管理,但在实际项目中仍有许多潜在坑点。文章详细分析了三个典型问题:1) 异常未被捕获导致事务未回滚,需明确指定`rollbackFor`属性;2) 异常被try-catch“吃掉”,应避免在事务方法中直接处理异常;3) 事务范围与锁范围不一致引发并发问题,建议调整锁策略以覆盖事务范围。这些问题看似简单,但一旦发生,排查难度较大,因此开发时需格外留意。最后,文章提供了课程源代码下载地址,供读者实践参考。
24 0
|
6天前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——Spring Boot 事务配置
本文介绍了 Spring Boot 中的事务配置与使用方法。首先需要导入 MySQL 依赖,Spring Boot 会自动注入 `DataSourceTransactionManager`,无需额外配置即可通过 `@Transactional` 注解实现事务管理。接着通过创建一个用户插入功能的示例,展示了如何在 Service 层手动抛出异常以测试事务回滚机制。测试结果表明,数据库中未新增记录,证明事务已成功回滚。此过程简单高效,适合日常开发需求。
32 0
|
6天前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——事务相关
本文介绍Spring Boot事务配置管理,阐述事务在企业应用开发中的重要性。事务确保数据操作可靠,任一异常均可回滚至初始状态,如转账、购票等场景需全流程执行成功才算完成。同时,事务管理在Spring Boot的service层广泛应用,但根据实际需求也可能存在无需事务的情况,例如独立数据插入操作。
17 0
|
6天前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
28 0
|
6天前
|
消息中间件 存储 Java
微服务——SpringBoot使用归纳——Spring Boot中集成ActiveMQ——ActiveMQ安装
本教程介绍ActiveMQ的安装与基本使用。首先从官网下载apache-activemq-5.15.3版本,解压后即可完成安装,非常便捷。启动时进入解压目录下的bin文件夹,根据系统选择win32或win64,运行activemq.bat启动服务。通过浏览器访问`http://127.0.0.1:8161/admin/`可进入管理界面,默认用户名密码为admin/admin。ActiveMQ支持两种消息模式:点对点(Queue)和发布/订阅(Topic)。前者确保每条消息仅被一个消费者消费,后者允许多个消费者同时接收相同消息。
36 0
微服务——SpringBoot使用归纳——Spring Boot中集成ActiveMQ——ActiveMQ安装