首页 > 其他分享 >在Spring Boot API Gateway中实现Sticky Session

在Spring Boot API Gateway中实现Sticky Session

时间:2023-09-26 14:32:51浏览次数:40  
标签:API exchange Spring Boot Sticky Session Gateway



文章目录

  • 小结
  • 问题
  • 在API Gateway中实现Sticky Session
  • 在同一个API Gateway中同时支持Sticky Session和RoundRobinLoadBalancer
  • 参考


小结

在Kubernetes微服务的云环境中,如何在Spring Boot API Gateway中实现Sticky Session,当服务请求被某一个服务器处理后,所有后续的请求都被转发到被第一次进行处理的同一个服务器再进行处理,这里进行了尝试,取得了想要的结果。

问题

Spring Boot API Gateway中实现Sticky Session在Spring Boot官方文档并没有特别详细的描述,看来看去语焉不详,如下:
https://docs.spring.io/: 3.9. Request-based Sticky Session for LoadBalancer

解决这个问题不仅要自定义负载均衡策略和方法,并需要Spring Boot API Gateway能够用某种方法取得服务器实例的ID并将每一个收到的服务请求处理并转发到具有相应服务器实例ID的服务器。实际上在Github上已经有大神给出了解决方案,具体地址如下:
Github: tengcomplex/spring-cloud-gateway-lb-sticky-session

在API Gateway中实现Sticky Session

实现的环境为Kubernetes微服务的云环境,这里需要使用cookie,并使用Eureka服务发现模块。具体思路如下:

  • StickySessionLoadBalancer实现ReactorServiceInstanceLoadBalancer,相当于自定义了一个负载均衡策略
  • Spring Boot API Gateway收到http服务请求,StickySessionLoadBalancercookie中找服务器实例ID: 自定义一个scg-instance-idcookie的键值
  • 如果scg-instance-idcookie被找到,而且是一个有效的服务器实例ID,那么这个服务请求就会被路由到这个具有服务器实例ID的服务器实例进行处理
  • 反之,如果没有找到scg-instance-idcookie的键值,或者服务器实例ID无效(有可能服务器已经宕机),那么委托ReactorServiceInstanceLoadBalancer 重新选择一个服务器,并将服务请求转发那个服务器
  • 无论以上路由如何选择,Spring Boot API Gateway会将服务器实例ID更新到cookie中去,scg-instance-id为的键值

在Spring Boot API Gateway中实现Sticky Session_spring boot


注:以上图片是Sticky SessionSpring Boot API Gateway路由示意图,来源于Github: Question: Sticky session in routes with load balancer #1176

首先,在Spring Boot API Gateway的模块中定义LoadBalancerClients

@EnableEurekaClient
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan({"com.aa.bb.configuration"})
//
@LoadBalancerClients({
        @LoadBalancerClient(value = "APPLICATION", configuration = com.aa.bb.configuration.StickySessionLoadBalancerConfiguration.class)})

这里StickySessionLoadBalancerConfiguration.class在有以下StickySessionLoadBalancerBean创建。

@Bean
  @Lazy
  public ReactorLoadBalancer<ServiceInstance> leastConnSticky(Environment environment,
      LoadBalancerClientFactory loadBalancerClientFactory) {
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
    L.debug("name: {}", name);
    return new StickySessionLoadBalancer(loadBalancerClientFactory.getProvider(name, ServiceInstanceListSupplier.class),
        name);
  }

通常情况下Spring Boot API Gateway有关路由的配置是在application.yml,这里在Spring Boot API Gateway的模块中定义过滤器,并使用程序定义路由:

@Value("com.aa.bb.frontendUriAPPLICATION:lb://APPLICATION")
    private String frontendUriAPPLICATION;

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {

        GatewayFilter gatewayFilter = customFilterSticky(clientFactory, properties);
        return builder.routes()
                .route("frontend",
                        r -> r.path("/application/**")
                                .filters(f -> f.filter(gatewayFilter))
                                .uri(frontendUri))
                                .build();
    }

以上是由以下程序定义,此函数返回GatewayFilter,注意这里不是GlobalFilter, 否则无法在同一个API Gateway中同时支持Sticky SessionRoundRobinLoadBalancer

@Bean
    public GatewayFilter customFilterSticky(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
        return new ReactiveLoadBalancerStickySessionFilter(clientFactory, properties);
    }

客户端服务请求被以上过滤器拦截后,交给了以下具体的由ReactiveLoadBalancerStickySessionFilter实现的过滤器方法filter处理 ,其中ReactiveLoadBalancerStickySessionFilterGatewayFilter的实现:

@Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
    String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
    L.debug("Filtering, url: {}, schemePrefix: {}", url, schemePrefix);
    if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
      L.debug("Not choosing, go further in the chain");
      return chain.filter(exchange);
    }
    // preserve the original url
    addOriginalRequestUrl(exchange, url);
    L.trace("{} url before: {}", ReactiveLoadBalancerStickySessionFilter.class.getSimpleName(), url);

    return choose(exchange).doOnNext(response -> {

      if (!response.hasServer()) {
        throw NotFoundException.create(true, "Unable to find instance for " + url.getHost());
      }
      URI uri = exchange.getRequest().getURI();
      // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
      // if the loadbalancer doesn't provide one.
      String overrideScheme = null;
      if (schemePrefix != null) {
        overrideScheme = url.getScheme();
      }
      DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(response.getServer(), overrideScheme);
      URI requestUrl = reconstructURI(serviceInstance, uri);
      L.debug("Url chosen: {}", requestUrl);
      exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
    }).then(chain.filter(exchange));
  }

接下来具体的choose(exchange)方法跳到以下进行处理:

private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
    URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
    L.debug("We are choosing, uri: {}", uri);
    ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory.getInstance(uri.getHost(),
        ReactorLoadBalancer.class, ServiceInstance.class);
    if (loadBalancer == null) {
      throw new NotFoundException("No loadbalancer available for " + uri.getHost());
    }
    L.debug("Using loadbalancer {}", loadBalancer.getClass().getSimpleName());
    Mono<Response<ServiceInstance>> ret = loadBalancer.choose(createRequest(exchange));
    ret.subscribe(r -> L.debug("We have {}", r.getServer().getUri()));
    return ret;
  }

以上loadBalancer.choose(createRequest(exchange)方法调用具体的定制化的StickySessionLoadBalancerchoose方法进行处理:

@SuppressWarnings("rawtypes")
  @Override
  public Mono<Response<ServiceInstance>> choose(Request request) {
    ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
        .getIfAvailable(NoopServiceInstanceListSupplier::new);
    return supplier.get().next().flatMap(list -> getInstanceResponse(list, request));
  }

那么,最后的重点,也就是核心的核心在Spring Boot API Gateway中实现Sticky Session的实现在这里,在此实现了服务请求拦截,选择Sticky Session决定的服务器,并更新cookie的操作:

@SuppressWarnings("rawtypes")
  private Mono<Response<ServiceInstance>> getInstanceResponse(List<ServiceInstance> instances, Request request) {
    if (instances.isEmpty()) {
      L.warn("如果没有可用的服务: {}", this.serviceId);
      return Mono.just(new EmptyResponse());
    }
    L.debug("request: {}, instances: {}", request.getClass().getSimpleName(), instances);
    Object context = request.getContext();
    L.debug("Context class name: {}", context.getClass().getSimpleName());
    if (!(context instanceof ServerWebExchange)) {
      throw new IllegalArgumentException("The context must be a ServerWebExchange");
    }
    ServerWebExchange exchange = (ServerWebExchange) context;
    L.debug("exchange: {}", exchange);
    // 检查 exchange 有一个 cookie 指向了一个可用的 ServiceInstance,这里使用`scg-instance-id`为`cookie`的键值
    return serviceInstanceFromCookie(exchange, instances)
        // 如果ServiceInstance存在, 那么路由到这个ServiceInstance
        .map(instance -> Mono.just((Response<ServiceInstance>) new DefaultResponse(instance)))
        // 否则使用ReactorServiceInstanceLoadBalancer委托选择一个服务器
        .orElseGet(() -> delegate.choose(request))
        // 无论如何,需要更新`cookie`键值为`scg-instance-id`的值
        .doOnNext(response -> setCookie(exchange, response));
  }

以上是实现的全部过程,在控制台可以看到以下输出:

DEBUG 2023-09-19 11:47:28.008 [reactor-http-nio-6] - Mapping [Exchange: POST http://127.0.0.1:8080/application/Process] to Route{id='frontend', uri=com.aa.bb.frontendUri:lb://APPLICATION, order=0, predicate=Paths: [/application/**], match trailing slash: true, gatewayFilters=[com.aa.bb.configuration.ReactiveLoadBalancerStickySessionFilter@97002113], metadata={}}

在同一个API Gateway中同时支持Sticky Session和RoundRobinLoadBalancer

以上提到了在同一个API Gateway中同时支持Sticky SessionRoundRobinLoadBalancer, 需要使用GatewayFilter,而不是GlobalFilter
例如这里需要同时支持RoundRobinLoadBalancer,那么,类似的可以自定义一个返回ReactorLoadBalancerRoundRobinLoadBalancer,实际上是一个标准的实现,应该会有更好办法,为了简便,暂时使用:

@Bean
  @Lazy
  public ReactorLoadBalancer<ServiceInstance> leastConn(Environment environment,
      LoadBalancerClientFactory loadBalancerClientFactory) {
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
    L.debug("client name: {}", name);
    return new RoundRobinLoadBalancer(loadBalancerClientFactory.getProvider(name, ServiceInstanceListSupplier.class),
        name);
  }
}

同前,再定义一个GatewayFilter, 不赘述。
同理,在Spring Boot API Gateway的模块中定义LoadBalancerClients

@LoadBalancerClients({
        @LoadBalancerClient(value = "APPLICATION_B", configuration = com.aa.bb.configuration.RoundRobinSessionLoadBalancerConfiguration.class)})

Spring Boot API Gateway的主模块中如下操作:

@Value("com.aa.bb.frontendUriAPPLICATION_B:lb://APPLICATION_B")
    private String frontendUriAPPLICATION_B;
    @Bean
    public RouteLocator customRouteLocatorSAMMSCP(RouteLocatorBuilder builder) {

        GatewayFilter gatewayFilter = customFilter(clientFactory, properties);
        return builder.routes()
                .route("frontendapplicationb",
                        r -> r.path("/Application_B/**")
                                .filters(f -> f.filter(gatewayFilter))
                                .uri(frontendUriAPPLICATION_B))
                .build();
    }
    @Bean
    public GatewayFilter customFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
        return new RoundRobinLoadBalancerFilter(clientFactory, properties);
    }

与先前类似,使用RoundRobinLoadBalancerFilter实现的过滤器方法处理 ,其中RoundRobinLoadBalancerFilterGatewayFilter的实现,在这里,RoundRobinLoadBalancerFilter不需要做任何处理,因为RoundRobinLoadBalancer已经由Spring Boot API Gateway标准包实现过了。

参考

Github: Question: Sticky session in routes with load balancer #1176

Github: LoadBalancer: Add Sticky LB implementation. #689

Github: tengcomplex/spring-cloud-gateway-lb-sticky-session

Github: POC for a session/cookie based sitcky load balancer implementation. #764

Saturn Cloud: Spring Cloud Gateway Route with Multiple Instances and Sticky Session

Stackoverflow: Sticky session loadbalancing in spring Microservices [closed]

Stackoverflow: How to use a Spring Cloud Gateway Custom Filter to filter every request?

Stackoverflow: No found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations:

Stackoverflow: Sticky session loadbalancing in spring Microservices [closed]

Stackoverflow: Request-based Sticky Session configuration with Spring Cloud LoadBalancer

基于springcloud3.0.1版本Loadbalancer切换默认负载均衡策略

51 CTO: Spring Cloud LoadBalancer–自定义负载均衡策略–方法/实例

https://docs.spring.io/: 3.9. Request-based Sticky Session for LoadBalancer

https://docs.spring.io/: 3.2. Switching between the load-balancing algorithms3.2. Switching between the load-balancing algorithms

https://docs.spring.io/: 2.1. The @EnableDiscoveryClient Annotation

标签:API,exchange,Spring,Boot,Sticky,Session,Gateway
From: https://blog.51cto.com/u_11949423/7608455

相关文章

  • springboot大文件上传、分片上传、断点续传、秒传的实现
    对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率。 本文是基于springboot+vue实现的文件上传,本文主要介绍服务端实现文件上传的......
  • 基于 COLA 架构的 Spring Cloud Alibaba(六) Spring Cloud Gateway
    在之前的篇章中,我们访问账户服务、商品服务、订单服务时,都要分别指定服务对应的端口进行访问。在实际应用中,服务的实际端口是不对外暴露的。如果要搭建更多的服务,那么我们对服务的访问将要维护更多的端口和访问路径。对此,本篇将介绍使用SpringCloudGateway对服务入口进行统一管......
  • 使用 Spring Integration 实现基于 Redis 的分布式锁以及踩坑
    背景分布式锁的应用场景应该还是蛮多的,这里就不赘述了。之前在开发中实现分布式锁都是自己基于Redis造轮子,虽然也不复杂并且自己实现一次能对分布式锁有更深的了解,但是终归有些麻烦。尤其是新项目需要的时候还得CV一次。然后在查询过程中(毫不意外地)发现Spring有现成的组......
  • springMVC
               ......
  • Spring框架
    1.OCP开闭原则什么是COP?COP是软件七大开发原则当中最基本的原则之一:开闭原则对扩展开放,对修改关闭。COP原则是最核心最基本的,其他六个原则都是为了这个原则服务的。COP开闭的原则核心是:只要当你在扩展系统功能的时候,没有修改之前写好的代码,那么就是符合COP原则的。反之,如果......
  • 基于springboot学生请假管理系统-计算机毕业设计源码+LW文档
    摘要:本学生请假管理系统是针对目前学生请假的实际需求,从实际工作出发,对过去的学生请假管理系统存在的问题进行分析,完善用户的使用体会。采用计算机系统来管理信息,取代人工管理模式,查询便利,信息准确率高,节省了开支,提高了工作的效率。本系统结合计算机系统的结构、概念、模型、原理......
  • 基于Spring的大学生竞赛活动平台-计算机毕业设计源码+LW文档
    摘要:本大学生课余休闲平台是针对目前大学生课余休闲平台的实际需求,从实际工作出发,对过去的大学生课余休闲平台存在的问题进行分析,完善用户的使用体会。采用计算机系统来管理信息,取代人工管理模式,查询便利,信息准确率高,节省了开支,提高了工作的效率。本系统结合计算机系统的结构、概......
  • 全志H616在低温reboot过程中进入休眠解决方法
    主题H618在DDR物料适配支持时候,reboot实验异常进休眠,在reboot老化测试中报如下log1[2023-07-11,16:56:44][40.325238][T1]init:Untrackedpid1888exitedwithstatus0[2023-07-11,16:56:44][40.325295][T5]binder:undelivereddeathnotification,0000000......
  • Spring Boot 目录遍历--表达式注入--代码执行--(CVE-2021-21234)&&(CVE-2022-22963)&&
    SpringBoot目录遍历--表达式注入--代码执行--(CVE-2021-21234)&&(CVE-2022-22963)&&(CVE-2022-22947)&&(CVE-2022-2296)SpringBoot目录遍历(CVE-2021-21234)漏洞简介spring-boot-actuator-logview是一个简单的日志文件查看器作为SpringBoot执行器端点,在0.2.13版本之前存......
  • SpringBoot 整合 Devtools 热部署工具
    什么是热部署实际开发过程中,修改应用的业务逻辑代码时常常需要重启应用,这显得非常繁琐,降低了开发效率,所以热部署对于开发来说显得十分必要。应用启动后会把编译好的Class文件加载到虚拟机中,正常情况下载项目修改了Java源文件是需要全部重新编译并加载(需要重启应用),而热部署......