【SpringCloud】通过Redis手动更新Ribbon缓存来解决Eureka微服务架构中服务下线感知的问题

文章目录

    • 前言
    • 1.第一次尝试
      • 1.1服务被调用方更新
      • 1.2压测第一次尝试
      • 1.3 问题分析
      • 1.4 同步的不是最新列表
    • 2.第二次尝试
      • 2.1调用方过滤下线服务
      • 2.2压测第二次尝试
      • 2.3优化
    • 写到最后

前言

在上文的基础上,通过压测的结果可以看出,使用DiscoveryManager下线服务之后进行压测是不会出现异常情况的,但唯一缺点就是下线服务的方式是取消注册与续约,之后并没有结束进程。也就使得在调用api下线后的服务其实是还存在处理请求的能力的。加之eureka三种级别的缓存同步需要一定时间,Eureka-Client从三级缓存中拉取的并不是实时的服务列表,进而使得Ribbon从Eureka-Client拉取的也不是实时的服务列表。最终导致Ribbon负载均衡到了已经下线的服务实例,并且此时该实例(进程还未关闭)刚好能处理请求!就造成了下线了两个端口的服务实例,但是却还是被负载均衡到来处理请求!
按照这个思路,再去看这张图:
在这里插入图片描述
可不可以通过某种手段,当服务下线后去越过三级缓存直接去更新Ribbon缓存来缩短感知时间?

我先说答案——是可以的

1.第一次尝试

1.1服务被调用方更新

手动从Eureka-Client同步服务缓存信息:

在之前分析Ribbon源码的时候,说到了接口路径从http://服务名称/接口路径——>http://服务地址/接口路径,这个过程中调用方的请求被Ribbon拦截器拦截,并且通过负载均衡最终被改写成为了一个准确的服务地址,其中有一个非常重要的方法,getLoadBalancer(“服务名称”)
在这里插入图片描述
可见,他通过服务名称就拿到了该服务名称下的所有服务列表(allServerList)和可用服务列表(upServerList),我们通过这个操作可不可以直接获取到最新一手的可用服务列表并且手动去set进Ribbon的可用服务列表缓存里,让他不再去每过30S同步?

Tips:在我们的SpringCloud项目中有一个非常重要的组件SpringClientFactory是Spring Cloud中用于管理和获取客户端实例的工厂类。在这里面可以获取特定服务的负载均衡器(即ILoadBalancer

于是,便有了下面的操作,专门配置一个Bean去更新Ribbon缓存,每当调用服务下线接口去下线指定服务后就去自动同步Ribbon缓存,不用再Ribbon每隔30S去自动同步:

@Configuration
@Slf4j
public class ClearRibbonCache {public void clearRibbonCache(SpringClientFactory clientFactory, List<Integer> portParams) {// 获取指定服务的负载均衡器ILoadBalancer loadBalancer = clientFactory.getLoadBalancer("user-service");//在主动拉取可用列表,而不是走拦截器被动的方式——这里List<Server> reachableServers = loadBalancer.getReachableServers();//这里从客户端获取,会等待客户端同步三级缓存// 在某个时机需要清除Ribbon缓存((BaseLoadBalancer) loadBalancer).setServersList(ableServers); // 清除Ribbon负载均衡器的缓存}
}

于是在下线服务的接口中,就多了一步自动更新缓存的操作(不熟悉这个接口的可以去看上一篇文章):

@GetMapping(value = "/service-down-list")public String offLine(@RequestParam List<Integer> portParams) {List<Integer> successList = new ArrayList<>();//得到服务信息List<InstanceInfo> instances = eurekaClient.getInstancesByVipAddress(appName, false);List<Integer> servicePorts = instances.stream().map(InstanceInfo::getPort).collect(Collectors.toList());//去服务列表里挨个下线OkHttpClient client = new OkHttpClient();log.error("开始时间:{}", System.currentTimeMillis());portParams.parallelStream().forEach(temp -> {if (servicePorts.contains(temp)) {String url = "http://" + ipAddress + ":" + temp + "/control/service-down";try {Response response = client.newCall(new Request.Builder().url(url).build()).execute();if (response.code() == 200) {log.debug(temp + "服务下线成功");successList.add(temp);} else {log.debug(temp + "服务下线失败");}} catch (IOException e) {log.error(e.toString());}}});log.debug("开始清除Ribbon缓存");clearRibbonCache.clearRibbonCache(clientFactory,portParams);return successList + "优雅下线成功";}

1.2压测第一次尝试

同样我们采用(100线程-3S)的JMeter压测模型去在调用服务下线接口后的15S,30S后压测,压测的接口即为一个普通的跨服务调用接口
下线服务:
在这里插入图片描述
下线服务的15S:
在这里插入图片描述
此时,观察控制台的日志输出可以发现,已经下线的两个服务实例还是被负载均衡到了(已下线但进程未退出),好像更新了缓存没有任何效果诶。
在这里插入图片描述
下线服务的30S:
在这里插入图片描述
情况和15S如出一辙,并且请求负载均衡到了已下线但进程未退出的服务上。

下线服务的45S:
在这里插入图片描述
在这里插入图片描述
可见调用api下线服务直到45S左右,已经下线的服务才从每层缓存信息中完全清除,这个时间是非常致命的!

1.3 问题分析

在服务发布的场景就会出现这样一个业务问题:开发调用api下线了某两个服务,通知运维可以去关闭这两个服务进程了,运维kill-9杀掉了这两个进程准备发布新服务。但此时客户端(用户)向服务端发送了请求,刚好该请求涉及跨服务调用,并且由于Ribbon同步Eureka-Client缓存,Eureka-Client同步Eurek-Server中的三级缓存需要一定时间,Ribbon缓存中的可用服务列表不是最新的,同步过来已下线(进程也被kill)的服务。最后请求受到Ribbon负载均衡落到了一个开发通过api下线的服务实例,分发到了一个运维kill-9的服务实例上,造成接口返回500、404、connect time out、connect refused…等错误,造成频繁告警。

1.4 同步的不是最新列表

透过现象看本质:

为什么手动同步Ribbon缓存没有起到效果?是不是同步的内容出了问题?下面打断点开启debug,看看服务下线后到底拿到的是什么服务列表:
在这里插入图片描述
意外发现,曾经天真以为可以拿到的实时的服务列表,到头来确实一场空,小丑竟是我自己。明明8083,8084已经下线可为什么还在可用服务列表里,并且还set到了Ribbon缓存中

原来啊,通过那个方法获取服务列表是从Eureka-client拿的,而这其实就是client去三级缓存那里同步的问题。 你说到为什么手动更新了缓存还是会有一段同步时间? 那就是client从三级缓存同步来的服务列表还存在没下线的服务,所以导致手动更新到ribbon缓存里的列表也还存在没下线的服务。看到这里,Eureka的“牺牲一致性保证高可用”是不是体现的淋漓尽致呢?

这个一致性难道真的不能解决了吗?
其实我还有一招
同时结合Eureka-Ribbon架构的服务调用链路,其实在服务调用方去更新Ribbon缓存才能更好保证Ribbon负载均衡的服务列表是我所控制的
PS:(这里节省了一次尝试,即在服务被调用方去引入过滤操作,尝试过压测结果还是和以前一样,所以就忽略了。直接去服务调用方尝试)
在这里插入图片描述

2.第二次尝试

2.1调用方过滤下线服务

从拿到的服务列表中过滤下线服务,并且在调用方执行:
在调用方执行?那被调用方下线的端口信息怎么让调用方知道呢,跨进程通信你选择MQ?还是Redis?这里我选择Redis
在上述更新缓存的操作中稍作更改,把更新操作移动到服务调用方,并且引入Redis来作为通信支持(这里采用hash的数据结结构),那么被调用方现在所需要的就是更新下线的端口信息到redis中:
在这里插入图片描述

    @GetMapping(value = "/service-down-list")public String offLine(@RequestParam List<Integer> portParams) {List<Integer> successList = new ArrayList<>();//得到服务信息List<InstanceInfo> instances = eurekaClient.getInstancesByVipAddress(appName, false);List<Integer> servicePorts = instances.stream().map(InstanceInfo::getPort).collect(Collectors.toList());//去服务列表里挨个下线OkHttpClient client = new OkHttpClient();log.error("开始时间:{}", System.currentTimeMillis());portParams.parallelStream().forEach(temp -> {if (servicePorts.contains(temp)) {String url = "http://" + ipAddress + ":" + temp + "/control/service-down";try {Response response = client.newCall(new Request.Builder().url(url).build()).execute();if (response.code() == 200) {log.debug(temp + "服务下线成功");successList.add(temp);} else {log.debug(temp + "服务下线失败");}} catch (IOException e) {log.error(e.toString());}}});// todo Redis通知stringRedisTemplate.opsForHash().put("port-map","down-ports",portParams.toString());return successList + "优雅下线成功";}

并且以前更新Ribbon可用服务列表操作也有稍微变化,即新增了一个手动过滤操作:

@Configuration
@Slf4j
public class ClearRibbonCache {/*** 削减*/public static boolean cutDown(List<Integer> ports, Server index) {return ports.contains(index.getPort());}public void clearRibbonCache(SpringClientFactory clientFactory, String portParams) {// 获取指定服务的负载均衡器ILoadBalancer loadBalancer = clientFactory.getLoadBalancer("user-service");//在主动拉取可用列表,而不是走拦截器被动的方式——这里List<Server> reachableServers = loadBalancer.getReachableServers();//这里从客户端获取,会等待客户端同步三级缓存//过滤掉已经下线的端口,符合条件端口的服务过滤出来List<Integer> portList = StringChange.stringToList(portParams);List<Server> ableServers = reachableServers.stream().filter(temp -> !cutDown(portList, temp)).collect(Collectors.toList());log.debug("可用服务列表:{}", ableServers);// 在某个时机需要清除Ribbon缓存((BaseLoadBalancer) loadBalancer).setServersList(ableServers); // 清除Ribbon负载均衡器的缓存}
}

在服务调用方,每次进行跨服务调用前都去从Redis中获取出实时下线的端口并且去更新Ribbon缓存:
在这里插入图片描述

2.2压测第二次尝试

当下线完服务,立即进行压测,可以看到所有的跨服务调用请求都落在了还未下线的实例上,并且已下线但进程未关闭的服务实例没有再处理请求:
在这里插入图片描述
在这里插入图片描述
并且15S,30S的时间节点上,也没有任何异常:
在这里插入图片描述
可见通过此种方式来主动更新Ribbon可用服务列表确实可行,特别是在运维那边发布新服务的一个特定场景下可以解决Eureka感知下线服务迟钝从而影响Ribbon负载到不可用的服务实例上这一问题。

2.3优化

其实,如果每次在新发布服务的场景下告警的接口都可以精确定位到,并且数量不多的情况我觉得在那几个业务接口里去手动同步一下Ribbon缓存没有什么大问题也可以解决问题。但是如果每次告警的接口有很多,并且不固定那上述的方法就显得有些许臃肿。而且这也是一种入侵式编程,我其实是不推荐的!
说起入侵式编程不禁就会想到无入侵式编程——Aop
直接把出现错误的模块作为切面,并把更新Ribbon的操作作为切入点写到表达式里,就完美做到了不改变已有业务而实现了更新功能,就像这样:

@Aspect
@Component
@Slf4j
public class RequestAspect {@ResourceSpringClientFactory springClientFactory;@ResourceClearRibbonCacheBean clearRibbonCacheBean;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Before(value = "execution(* com.yu7.order.web.*.*(..))")public void refreshBefore(JoinPoint joinPoint) {String ports = (String) stringRedisTemplate.opsForHash().get("port-map", "down-ports");log.debug("从Redis获取的端口为:{}", ports);//下线了才会有值,没有值说明没下线不用更新if (ObjectUtils.isNotEmpty(ports)) {clearRibbonCacheBean.clearRibbonCache(springClientFactory, ports);}}
}

进行压测,结果和预期完全一致~

写到最后

我想说:其实我的方案只是相当于提出了一个大体框架和构想,粗略地实现了基于Eureka的微服务架构中服务状态感知的问题,当业务里存在不止一种调用关系,下线服务类型不一致,服务断断续续下线会造成value值丢失…方案就需要进一步细化(还存在硬编码问题,嘻嘻),并且为了切面不影响业务还应该给存到Redis的数据加上TTL等其他保险措施,总而言之也欢迎大家提出建议,共同精进,一起解决这一难题!

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

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

相关文章

程序员如何开发高级python爬虫?

之前我有写过一篇“高级爬虫和低级爬虫的区别”的文章&#xff0c;我们知道它并非爬虫领域中专用术语。只是根据爬虫的复杂性来断定是否是高级爬虫。以我个人理解&#xff1a;高级爬虫是可能具有更复杂的功能和更高的灵活性的爬虫。下面我们围绕高级爬虫来了解下有趣的事情。 低…

python可视化plotly 图例(legend)设置大全,值得收藏!

文章目录 一、图例(legend)二、update\_layout(legend{}) 相关参数及示例关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python…

多向通信----多人聊天

package 多人聊天; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; publ…

K8s中安装calico一直无法启动成功

问题描述 #k8s版本为v1.20.9查看对应日志 #calico-node-xxx 对应pod名称 kubectl logs calico-node-xxxx -n kube-system #没有错误但是一直启动不起来应该是版本不匹配问题解决方案 删除 calico重新安装对应版本删除podskubectl delete -f calico.yaml删除文件 rm -f calico.…

【NLP】如何管理大型语言模型 (LLM)

什么是LLM编排&#xff1f; LLM 编排是管理和控制大型语言模型 (LLM)的过程&#xff0c;以优化其性能和有效性。这包括以下任务&#xff1a; 提示LLM&#xff1a;生成有效的提示&#xff0c;为LLMs提供适当的背景和信息以产生所需的输出。链接LLM&#xff1a; 结合多个LLM的输…

SpringBoot读取properties文字乱码问题及相关问题

问题&#xff1a;在idea的编辑器中properties文件一般用UTF-8编码&#xff0c;SpringBoot2读取解码方式默认不是UTF-8&#xff0c;当值出现中文时SpringBoot读取时出现了乱码。 解决方式1&#xff1a;在SpringBoot框架层面解决&#xff0c;在配置类注解上添加encoding属性值为…

5G - NR物理层解决方案支持6G非地面网络中的高移动性

文章目录 非地面网络场景链路仿真参数实验仿真结果 非地面网络场景 链路仿真参数 实验仿真结果 Figure 5 && Figure 6&#xff1a;不同信噪比下的BER和吞吐量 变量 SISO 2x2MIMO 2x4MIMO 2x8MIMOReyleigh衰落、Rician衰落、多径TDL-A(NLOS) 、TDL-E(LOS)(a)QPSK (b)16…

[足式机器人]Part4 南科大高等机器人控制课 Ch02 Rigid Body Configuration and Velocity

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;CLEAR_LAB 笔者带更新-运动学 课程主讲教师&#xff1a; Prof. Wei Zhang 南科大高等机器人控制课 Ch02 Rigid Body Configuration and Velocity 1. Rigid Body Configuration1.1 Special Orthogonal Group1.2 Use of Ro…

Centos7安装宝塔面板8.0.3并实现公网远程登录宝塔面板【内网穿透】

文章目录 一、使用官网一键安装命令安装宝塔二、简单配置宝塔&#xff0c;内网穿透三、使用固定公网地址访问宝塔 宝塔面板作为建站运维工具&#xff0c;适合新手&#xff0c;简单好用。当我们在家里/公司搭建了宝塔&#xff0c;没有公网IP&#xff0c;但是想要在外也可以访问内…

组合总和II(回溯、去重)

40. 组合总和 II - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定一个候选人编号的集合 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用 一次 。 注意&#xff1a…

相交链表(LeetCode 160)

文章目录 1.问题描述2.难度等级3.热门指数4.解题思路方法一&#xff1a;暴力法方法二&#xff1a;哈希表方法三&#xff1a;双栈方法四&#xff1a;双指针&#xff1a;记录链表长度方法五&#xff1a;双指针&#xff1a;互换遍历 5.实现示例参考文献 1.问题描述 给两个单链表的…

Java第二十一章网络通信

一、网络程序设计基础 1、局域网与互联网 为了实现两台计算机的通信&#xff0c;必须用一个网络线路连接两台计算机&#xff0c;如下图所示。 2、网络协议 1.IP协议 IP指网际互连协议&#xff0c;Internet Protocol的缩写&#xff0c;是TCP/IP体系中的网络层协议。设计IP的目的…

深入理解数据在内存中是如何存储的,位移操作符如何使用(能看懂文字就能明白系列)文章超长,慢慢品尝

系列文章目录 C语言笔记专栏 能看懂文字就能明白系列 &#x1f31f; 个人主页&#xff1a;古德猫宁- &#x1f308; 信念如阳光&#xff0c;照亮前行的每一步 文章目录 系列文章目录&#x1f308; *信念如阳光&#xff0c;照亮前行的每一步* 前言引子一、2进制和进制转化为什么…

Verilog开源项目——百兆以太网交换机(四)令牌桶管理单元设计

Verilog开源项目——百兆以太网交换机&#xff08;四&#xff09;令牌桶管理单元设计 &#x1f508;声明&#xff1a;未经作者允许&#xff0c;禁止转载 &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN主页 &#x1f511;全新原创以太网交换机项目&#xff0c;Blog内容将聚焦…

前缀和|二分查找|LeetCode2234| 花园的最大总美丽值

作者推荐 贪心算法LeetCode2071:你可以安排的最多任务数目 本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 二分查找算法合集 题目 Alice 是 n 个花园的园丁&#xff0c;她想通过种花&#xff0c;最大化她所有花…

pycharm中debug,py文件

1、先把需要的实参传入 2、在合适位置打上断点 3、在小三角旁边右键调用调试 4.步进/步出查看 5.选择单步执行&#xff0c;走的更慢

使用Python实现爬虫IP负载均衡和高可用集群

做大型爬虫项目经常遇到请求频率过高的问题&#xff0c;这里需要说的是使用爬虫IP可以提高抓取效率&#xff0c;那么我们通过什么方法才能实现爬虫IP负载均衡和高可用集群&#xff0c;并且能快速的部署并且完成爬虫项目。 通常在Python中实现爬虫ip负载均衡和高可用集群需要一…

基于ssm助学贷款网站论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本助学贷款管理系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息…

CTF 7

信息收集 存活主机探测 arp-scan -l 端口探测 nmap -sT --min-rate 10000 -p- 192.168.0.5 服务版本等信息 nmap -sT -sV -sC -O -p22,80,137,138,139,901,5900,8080,10000 192.168.0.5Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-02 21:23 CST Stats: 0:01:30 elaps…

​劲松中西医结合医院专家讲解hpv36阳性是否严重

​劲松中西医结合医院专家讲解hpv36阳性严重性问题 HPV36阳性&#xff0c;就像一场潜在的暴风雨&#xff0c;预示着可能的危机。它代表了一种高危型的HPV感染&#xff0c;就像一只隐藏在暗处的猛兽&#xff0c;随时可能暴起伤人。然而&#xff0c;就像生活中的许多挑战&#x…