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

news2024/11/23 1:54:20

文章目录

    • 前言
    • 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 {

    @Resource
    SpringClientFactory springClientFactory;
    @Resource
    ClearRibbonCacheBean clearRibbonCacheBean;
    @Resource
    private 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.coloradmin.cn/o/1288427.html

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

相关文章

springboot——自动装配

自动装配 Condition: Condition内置方法&#xff1a;boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)&#xff0c;返回值为布尔型 重写matches方法的类&#xff1a;SpringBootCondition等 SpringBootCondition&#xff1a;springboot自带的实现类…

4.数据库

目录 一、数据库的基本信息 1.1 数据库的定义 1.2数据库的分类 1.2.1 关系型数据库 1.2.2 非关系型数据库 1.3 SQL介绍 1.3.1 概念 1.3.2 SQl语言分类 1.3.3 SQL注释 1.3.4 数据库操作命令DDL 1.3.5 数据表操作命令DDL 1.3.6 数据表操作命令DML 1.3.7 数据表中内容…

电容电感特性的理解

04730电子技术基础 语雀 在前面&#xff0c;我们提到过电容元件具有隔直通交&#xff0c;通高阻低的特性&#xff0c;电感元件具有隔交通直&#xff0c;通低阻高的特性。那么如何理解这两句话呢&#xff1f;下面我们一一剖析 电容元件的隔直通交&#xff0c;通高阻低的特性 …

Faster Transformer

Faster Transformer FasterTransformer包含transformer块的高度优化版本的实现&#xff0c;该块包含编码器和解码器部分。基于高效率的开发语言和工具&#xff1a; C, CUDA, cuBLAS and cuBLASlt支持的模型数据格式&#xff1a;FP32, FP16, BF16, INT8 (limited models) and F…

智能优化算法应用:基于人工水母算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于人工水母算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于人工水母算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.人工水母算法4.实验参数设定5.算法结果6.参考…

C语言-详解指针

目录 一.内存 1.内存的定义 2.内存的结构图 二.地址 1.什么是地址 2.什么是变量的地址 三.什么是指针 1.指针的定义 四.如何获取数据存储空间的地址 1.&运算符 五.指针变量 1.什么是指针变量&#xff08;一级指针变量&#xff09; 2.指针变量的定义 3…

Opencv库如何检测图片中鸡蛋数量

Opencv库检测图片中鸡蛋数量 由于需要写一个检测鸡蛋数量的程序&#xff0c;用了几个opencv中的经典方法&#xff0c;实现了图片中鸡蛋的检测。在一步步实现的同时&#xff0c;同时说明每个方法的用途。希望能给学习opencv的小伙伴一些帮助。下图为原始图和实现后的检测边框。…

ubuntu20.04找不到#include<opencv/cv.h>文件

编译ROS包的时候出现错误&#xff1a;fatal error&#xff1a;opencv/cv.h : No such file or directory #include<opencv/cv.h> 查看opencv4版本&#xff1a; pk-config --modversion opencv4: 在opencv4中opencv2的cv.h融合进了imgproc.hpp里: 把源码中的#include …

字节跳动开源基于SD1.5的 MagicAnimate 一张照片秒变真人舞蹈视频

项目地址&#xff1a;https://github.com/magic-research/magic-animate 显卡要求&#xff1a;12G MagicAnimate是一项利用先进的扩散模型实现人体图像动画的创新性项目。其核心优势在于确保生成内容的时间一致性&#xff0c;通过提供预训练的稳定扩散V1.5和MSE微调的VAE基础…

Python-炸弹人【附完整源码】

炸弹人 炸弹人是童年的一款经典电子游戏&#xff0c;玩家控制一个类似"炸弹人"的角色&#xff0c;这个角色可以放置炸弹&#xff0c;并在指定的时间内引爆它们消灭敌人以达到目标&#xff0c;此游戏共设有两节关卡&#xff0c;代码如下&#xff1a; 运行效果&#x…

Qt/C++视频监控拉流显示/各种rtsp/rtmp/http视频流/摄像头采集/视频监控回放/录像存储

一、前言 本视频播放组件陆陆续续写了6年多&#xff0c;一直在持续更新迭代&#xff0c;视频监控行业客户端软件开发首要需求就是拉流显示&#xff0c;比如给定一个rtsp视频流地址&#xff0c;你需要在软件上显示实时画面&#xff0c;其次就是录像保存&#xff0c;再次就是一些…

22、pytest多个参数化的组合

官方实例 # content of test_multi_parametrie.py import pytestpytest.mark.parametrize("x",[0,1]) pytest.mark.parametrize("y",[2,3]) def test_foo(x,y):print("{}-{}".format(x,y))pass解读与实操 要获得多个参数化参数的所有组合&…

vue3里面使用ref和toRef、toRefs

vue3 里面我们经常会使用ref()来接受内部值&#xff0c;返回一个响应式的对象。创建可以使用任何类型的响应式ref。这里对象是响应式的&#xff0c;可以进行更改的&#xff0c;对象有一个value属性&#xff0c;其值就是所传递的原始值。ref() 将传入参数的值包装为一个带 .valu…

【链表Linked List】力扣-83 删除排序链表中的重复元素

目录 题目描述 解题过程 题目描述 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出现一次 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,1,2] 输出&#xff1a;[1,2]示例 2&#xff1a; 输入&#xff1…

【每日OJ —— 94. 二叉树的中序遍历】

每日OJ —— 94. 二叉树的中序遍历 1.题目&#xff1a;94. 二叉树的中序遍历2.解法2.1.算法讲解2.2.代码实现2.3.提交通过展示 1.题目&#xff1a;94. 二叉树的中序遍历 2.解法 2.1.算法讲解 1.首先如果在每次每个节点遍历的时候都去为数组开辟空间&#xff0c;这样的效率太低…

android开发市场被抢占,鸿蒙能入行吗?

根据最新的数据&#xff0c;华为Mate60系列在上市第二周就成功占据了国内手机市场的17%份额&#xff0c;排名第二。而机构预测&#xff0c;华为手机在第37周有望超过20%的市场份额&#xff0c;成为国内手机市场的冠军。 一开始&#xff0c;人们对HarmonyOSNEXT持保留态度&…

国产麒麟操作系统部署记录

前提&#xff1a;部署项目首先要安装各种软件&#xff0c;在内网环境下无法在线下载。 思路&#xff1a;首先部署一台能上网的系统&#xff0c;在此系统下只下载包&#xff0c;然后传到另一台内网系统下进行安装&#xff1b; 1、最开始yum未安装&#xff0c;因此需要先安装yu…

PHP短信接口防刷防轰炸多重解决方案三(可正式使用)

短信接口盗刷轰炸&#xff1a;指的是黑客利用非法手段获取短信接口的访问权限&#xff0c;然后使用该接口发送大量垃圾短信给目标用户 短信验证码轰炸解决方案一(验证码类解决)-CSDN博客 短信验证码轰炸解决方案二(防止海外ip、限制ip、限制手机号次数解决)-CSDN博客 PHP短信…

JavaScript 数组方法 reduce() 的用法

一、概述 在JavaScript中&#xff0c;reduce()方法是一个非常实用的数组方法&#xff0c;它接收一个函数作为累加器&#xff08;accumulator&#xff09;&#xff0c;数组中的每个值&#xff08;从左到右&#xff09;开始缩减&#xff0c;最终为一个值。这个方法在处理数组…

数据结构 | 二叉树的各种遍历

数据结构 | 二叉树的各种遍历 文章目录 数据结构 | 二叉树的各种遍历创建节点 && 创建树二叉树的前中后序遍历二叉树节点个数二叉树叶子节点个数二叉树第k层节点个数二叉树查找值为x的节点二叉树求树的高度二叉树的层序遍历判断二叉树是否是完全二叉树 我们本章来实现二…