在Spring Boot API Gateway中实现Sticky Session

文章目录

  • 小结
  • 问题
  • 在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为的键值

sticky session
注:以上图片是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@Lazypublic 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;@Beanpublic 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

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

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

@Overridepublic 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 urladdOriginalRequestUrl(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")@Overridepublic 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@Lazypublic 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;@Beanpublic 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();}@Beanpublic 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
CSDN: 基于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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/136281.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023年7月京东平板电脑行业品牌销售排行榜(京东销售数据分析)

鲸参谋监测的京东平台7月份平板电脑市场销售数据已出炉&#xff01; 根据鲸参谋电商数据分析平台的相关数据显示&#xff0c;今年7月份&#xff0c;京东平台上平板电脑的销量为68万&#xff0c;同比增长超过37%&#xff1b;销售额为22亿&#xff0c;同比增长约54%。从价格上看…

win系统环境搭建(四)——Windows安装mysql8压缩包版本

windows环境搭建专栏&#x1f517;点击跳转 win系统环境搭建&#xff08;四&#xff09;——Windows安装mysql8压缩包版本 本系列windows环境搭建开始讲解如何给win系统搭建环境&#xff0c;本人所用系统是腾讯云服务器的Windows Server 2022&#xff0c;你可以理解成就是你用…

mysql知识大全

MySQL知识大全&#xff08;2&#xff09; MySqL 基础为1—7&#xff08;增删改查基础语法&#xff09;&#xff0c;MySQL进阶知识为8—11&#xff08;约束、数据库设计、多表查询、事务&#xff09; 1、数据库相关概念 以前我们做系统&#xff0c;数据持久化的存储采用的是文件…

直线模组的常用语

在工业生产中&#xff0c;直线模组的叫法有很多种&#xff0c;对于新手小白来说&#xff0c;很容易就会被绕晕&#xff0c;今天我们就来简单说一下直线模组的常用称呼吧&#xff01; 1、直线模组&#xff1a;与直线滑台同义&#xff0c;基本可以相互互换。直线模组一般是指可以…

微信小程序隐私授权

微信开发者平台新公告&#xff1a;2023年9月15之后&#xff0c;隐私协议将被启用&#xff0c;所以以后的小程序都要加上隐私协议的内容提示用户&#xff0c; 首先设置好隐私协议的内容&#xff0c;登录小程序的开发者后台&#xff0c;在设置--》服务内容声明--》用户隐私保护指…

【笔试强训选择题】Day44.习题(错题)解析

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;笔试强训选择题 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01;&#xff01;&#xff…

阿里云无影云电脑角色AliyunServiceRoleForGws什么意思?

阿里云无影云电脑服务关联角色是指角色名称&#xff1a;AliyunServiceRoleForGws&#xff0c;并赋予角色权限策略&#xff1a;AliyunServiceRolePolicyForGws的过程&#xff0c;简单来说&#xff0c;就是允许无影云电脑服务访问您VPC、CEN和NAS中的资源&#xff0c;使用该权限查…

数据库管理-第105期 安装Database Valut组件(20230919)

数据库管理-第105期 安装Database Valut组件&#xff08;20230919&#xff09; 之前无论是是EXPDP还是PDB中遇到的一些问题&#xff0c;其实都跟数据库的DV&#xff08;Database Valut&#xff09;组件有关&#xff0c;因为目标库没有安装DV导致启动时会出现问题。 1 DV/OLS …

1000K数据在Java中用哪种选型进行存储?

​ 如题&#xff0c;笔者在面试时被考察到该问题&#xff0c;Java中面临这样大的数组&#xff0c;选择ArrayLIst还是LinkedList来进行存储呢&#xff1f; 给出你的依据。 结论&#xff1a;大多数场景下直接使用ArrayList和ArrayDeque即可。 ArrayList和LinkedList均是实现了Lis…

一个React组件:动态递增展示数字特效

在可视化展示界面时有一种场景&#xff0c;就是页面在初始化的时候&#xff0c;有些数字展示想要从某个值开始动态递增到实际值&#xff0c;形成一种动画效果。例如&#xff1a; 写一个数字递增的组件&#xff0c;代码如下&#xff1a; import {useEffect, useRef, useState} f…

升级:远程桌面软件玩游戏指南

你有没有遇到过这样的场景&#xff1a;你想玩一款特定的游戏&#xff0c;但却受到设备功能的限制&#xff1f;这就是游戏远程桌面的概念变得非常宝贵的地方。从本质上讲&#xff0c;它允许您远程利用高端游戏计算机的功能&#xff0c;使您能够在自己的设备上玩游戏。 可以考虑…

14.抽象工厂模式

UML 代码 #include <iostream> #include <list> using namespace std;class AbstractProductA { public:virtual void showa() 0; }; class ProductA1:public AbstractProductA { public:virtual void showa(){cout << "我是A1" << endl;}…

解决npm install遇到的问题:Error while executing:

目录 一、遇到问题 二、解决办法 方法一 方法二 方法三 方法四 一、遇到问题 npm ERR! Error while executing: npm ERR! D:\IT_base\git\Git\cmd\git.EXE ls-remote -h -t ssh://gitgithub.com/sohee-lee7/Squire.git npm ERR! npm ERR! fatal: unable to access ht…

二叉树顺序结构及实现

&#x1f449;二叉树顺序结构及实现 1.二叉树的顺序结构2.堆的概念及结构3.堆的实现3.1堆向下调整算法3.2堆向上调整算法 4.堆的创建4.1堆创建方法14.1.1构建堆结构体4.1.2堆的初始化4.1.3堆数据添加向上调整4.1.4主函数内容 4.2堆的创建方法24.2.1堆数据添加向下调整 4.3堆数据…

CRM软件系统价格不同的原因

很多人在了解CRM系统时&#xff0c;发现不同品牌的CRM价格有着很大的区别。一些CRM系统只需要几千块钱&#xff0c;一些CRM系统的报价却要上万&#xff0c;甚至十几万。为什么CRM系统价格不同&#xff1f;下面我们就来说说。 1、功能不同 从功能方面来说&#xff0c;一些CRM系…

基于Spring Boot+ Vue的健身房管理系统与实现

小熊学Java全能学面试指南&#xff1a;https://javaxiaobear.cn 摘要 随着健身行业的快速发展&#xff0c;健身房管理系统成为了提高管理效率和用户体验的重要工具。本论文旨在设计与实现一种基于前后端分离的健身房管理系统&#xff0c;通过前后端分离的架构模式&#xff0c;…

类模板深度剖析

类模板可以定义任意多个不同的类型参数 类模板可以被特化 可以指定类模板的特定实现 部分类型参数必须显示指定 根据类型参数分开实现类模板 类模板的特化类型 部分特化 - 用特定规则约束类型参数 完全特化 - 完全显示指定类型参数 类模板特化注意事项 特化只是模板的分开…

【张兔兔送书第一期:考研必备书单】

考研书单必备 《数据结构与算法分析》《计算机网络&#xff1a;自顶向下方法》《现代操作系统》《深入理解计算机系统》《概率论基础教程&#xff08;原书第10版》《线性代数&#xff08;原书第10版&#xff09;》《线性代数及其应用》赠书活动 八九月的朋友圈刮起了一股晒通知…

基于STC15单片机温度光照检测系统-proteus仿真-源程序

一、系统方案 1、本设计采用STC15单片机作为主控器。 2、光敏电阻采集光照值送到液晶1602和串口显示。 3、DS18B20采集温度值&#xff0c;送到液晶1602和串口显示。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计 1、首先是系统初始化 /-----------------------…

rv1126-rv1109-编译的剖析

./build.sh uboot:cmds./build.sh ubootcd u-boot make rv1126_defconfig make menuconfig ### 保存配置到对应的⽂件rv1126_defconfig make savedefconfig cp defconfig configs/rv1126_defconfig //剖析 ./build.sh uboot //调用 ./mk-loader.sh build.sh -> mk-all.sh …