专题类网站坪山区坪山街道六联社区
news/
2025/9/23 16:30:03/
文章来源:
专题类网站,坪山区坪山街道六联社区,企业网站推广怎么做,xp 做网站服务器吗文章目录 前言1.第一次尝试1.1服务被调用方更新1.2压测第一次尝试1.3 问题分析1.4 同步的不是最新列表 2.第二次尝试2.1调用方过滤下线服务2.2压测第二次尝试2.3优化 写到最后 前言
在上文的基础上#xff0c;通过压测的结果可以看出#xff0c;使用DiscoveryManager下线服务… 文章目录 前言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, ListInteger portParams) {// 获取指定服务的负载均衡器ILoadBalancer loadBalancer clientFactory.getLoadBalancer(user-service);//在主动拉取可用列表而不是走拦截器被动的方式——这里ListServer reachableServers loadBalancer.getReachableServers();//这里从客户端获取会等待客户端同步三级缓存// 在某个时机需要清除Ribbon缓存((BaseLoadBalancer) loadBalancer).setServersList(ableServers); // 清除Ribbon负载均衡器的缓存}
}于是在下线服务的接口中就多了一步自动更新缓存的操作不熟悉这个接口的可以去看上一篇文章
GetMapping(value /service-down-list)public String offLine(RequestParam ListInteger portParams) {ListInteger successList new ArrayList();//得到服务信息ListInstanceInfo instances eurekaClient.getInstancesByVipAddress(appName, false);ListInteger 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 ListInteger portParams) {ListInteger successList new ArrayList();//得到服务信息ListInstanceInfo instances eurekaClient.getInstancesByVipAddress(appName, false);ListInteger 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(ListInteger ports, Server index) {return ports.contains(index.getPort());}public void clearRibbonCache(SpringClientFactory clientFactory, String portParams) {// 获取指定服务的负载均衡器ILoadBalancer loadBalancer clientFactory.getLoadBalancer(user-service);//在主动拉取可用列表而不是走拦截器被动的方式——这里ListServer reachableServers loadBalancer.getReachableServers();//这里从客户端获取会等待客户端同步三级缓存//过滤掉已经下线的端口符合条件端口的服务过滤出来ListInteger portList StringChange.stringToList(portParams);ListServer ableServers reachableServers.stream().filter(temp - !cutDown(portList, temp)).collect(Collectors.toList());log.debug(可用服务列表{}, ableServers);// 在某个时机需要清除Ribbon缓存((BaseLoadBalancer) loadBalancer).setServersList(ableServers); // 清除Ribbon负载均衡器的缓存}
}在服务调用方每次进行跨服务调用前都去从Redis中获取出实时下线的端口并且去更新Ribbon缓存
2.2压测第二次尝试
当下线完服务立即进行压测可以看到所有的跨服务调用请求都落在了还未下线的实例上并且已下线但进程未关闭的服务实例没有再处理请求 并且15S30S的时间节点上也没有任何异常 可见通过此种方式来主动更新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/913161.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!