spring cloud gateway+nacos 服务下线感知延迟,未及时出现503,请求依然转发到下线服务

spring cloud gateway服务下线感知延迟,未及时出现503

本篇算是配合之前的一篇了。整体问题是gateway对下线服务感知延迟,之前那篇文章是从服务角度解决自身注销的问题(使用undertow,服务停止后nacos下线注销延迟问题)。本篇是解决gateway自身发现服务问题。

1.场景描述

注册中心使用的nacos,客户端版本1.4.1。
gateway版本3.0.1。
nacos服务下线(包含手动点下线和服务正常停机)gateway在短暂几秒内还回继续将流量转发到已下线的服务上导致500。过几秒之后恢复正常,响应码变成503。表面上看,应该是gateway服务没有及时发现服务的下线。

2.分析

如果遇到这种问题,可以先排查一下面几种情况在尝试解决
1 服务端注销未正常运行(这个看一下nacos是否及时删除了节点信息就可以排查出来)
2 网关服务未及时发现节点的变化(这个可以在debug级别日志验证)
3 服务端和网关服务不互通

日志级别调整到debug,发现通过netty发送的下线通知已经抵达gateway服务。这说明nacos注册中心和spring boot服务通讯和订阅是没问题的。
从转发的入口着手:ReactiveLoadBalancerClientFilter#choose 这个方法就是gateway转发时选择服务的

private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId,
			Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
		ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory.getInstance(serviceId,
				ReactorServiceInstanceLoadBalancer.class);
		if (loadBalancer == null) {
			throw new NotFoundException("No loadbalancer available for " + serviceId);
		}
		supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
		// 最后是通过ReactorLoadBalancer的实现进行选择
		return loadBalancer.choose(lbRequest);
	}

ReactorLoadBalancer是负载均衡的接口,提供了两个实现,一个随机获取,一个轮询。
默认是使用轮询实现(RoundRobinLoadBalancer)。
RoundRobinLoadBalancer中选择服务的实现逻辑

public Mono<Response<ServiceInstance>> choose(Request request) {
		ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
				.getIfAvailable(NoopServiceInstanceListSupplier::new);
		// 在这个get方法中返回了可选服务器的集合
		return supplier.get(request).next()
				.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
	}

上面那个get的实现是:CachingServiceInstanceListSupplier#CachingServiceInstanceListSupplier这个类中提供的

public CachingServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, CacheManager cacheManager) {
		super(delegate);
		this.serviceInstances = CacheFlux.lookup(key -> {
			// 这里发现有缓存!感觉目的地近了。
			Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
			....
				}).then());
	}

2.1定位问题

调试一下看看:

  • 服务A启动注册到nacos
  • gateway正常将/test/hello转发至服务A
  • 在nacos管理端让服务A下线
  • 立刻访问不停/test/hello
  • 最初几秒内发现gateway还是会把流量打到服务A
  • 之后正常响应503

在获取服务集群信息的地方打断点

public CachingServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, CacheManager cacheManager) {
		super(delegate);
		this.serviceInstances = CacheFlux.lookup(key -> {
			// TODO: configurable cache name
			Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
			if (cache == null) {
				if (log.isErrorEnabled()) {
					log.error("Unable to find cache: " + SERVICE_INSTANCE_CACHE_NAME);
				}
				return Mono.empty();
			}
			// 在异常的时间段,这个list还是有信息。集合没内容之后开始响应503
			List<ServiceInstance> list = cache.get(key, List.class);
			if (list == null || list.isEmpty()) {
				return Mono.empty();
			}
			return Flux.just(list).materialize().collectList();
		}
		...
	}

看来是这个缓存没有及时刷新的原因!后续找了一段时间,没找到刷新缓存的地方就放弃了。还是用笨方法先解决吧

3.解决方案

已经知道了问题所在,想办法解决就是了。
整体思路:在订阅nacos服务变化中进行功能拓展,刷新缓存。

三个类:
MySubscribeConfig:进行订阅配置的入口
MyNacosSubscribe:订阅nacos的实现,用来发布订阅消息
MyNacosEventListener:消息处理的实现,在这里刷新缓存

先在写个spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.dong.gateway.config.MySubscribeConfig

MySubscribeConfig:


import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.dong.common.core.util.SpringContextHolder;
import com.dong.server.gateway.subscribe.MyNacosSubscribe;
import com.dong.server.gateway.subscribe.MyNacosEventListener;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
import org.springframework.cloud.loadbalancer.config.LoadBalancerCacheAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * 首先订阅当前网关关注的服务
 * nacos服务更新通知,但是gateway有一套自己的服务缓存列表。每次接到通知更新不及时导致转发到已经下线的服务
 * gateway获取缓存参考:org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier
 * nacos订阅参考:com.alibaba.cloud.nacos.discovery.NacosWatch#start()
 *
 */
@Configuration
@Import(SpringContextHolder.class)
@AutoConfigureAfter(LoadBalancerCacheAutoConfiguration.class)
public class MySubscribeConfig {

    @Bean
    public MyNacosSubscribe getMyNacosSubscribe(NacosServiceManager nacosServiceManager, NacosDiscoveryProperties properties){
    	LoadBalancerCacheManager cacheManager = SpringContextHolder.getBean(LoadBalancerCacheManager.class);
        return new MyNacosSubscribe(nacosServiceManager,properties,new MyNacosEventListener(loadBalancerCacheManager));
    }
}

MyNacosSubscribe

import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.nacos.api.naming.NamingService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;

import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * 订阅nacos推送更新事件
 * 启动和加载路由时重新订阅
 */
@Slf4j
@AllArgsConstructor
public class MyNacosSubscribe implements ApplicationRunner {
    private NacosServiceManager nacosServiceManager;
    private NacosDiscoveryProperties properties;
    private MyNacosEventListener myEventListener;

    private Set<String> getRouteServices(){
    	// TODO 这里返回自己要订阅的服务名称
        return new HashSet();
    }
    // 这里监听时间是为了在路由信息修改时候,重新订阅服务的。如果说路由重新加载不会有订阅变动的话,可以去掉
@org.springframework.context.event.EventListener({RefreshRoutesEvent.class})
    public void subscribe() {
        NamingService namingService = nacosServiceManager
                .getNamingService(properties.getNacosProperties());
        try {
            Set<String> services = getRouteServices();
            if(CollectionUtil.isNotEmpty(services)){
                for (String service : services) {
                    namingService.subscribe(service, properties.getGroup(),
                            null, myEventListener);
                }
            }
        } catch (Exception e) {
            log.error("namingService subscribe failed, properties:{}", properties, e);
        }
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        subscribe();
    }

}

MyNacosEventListener

import com.alibaba.cloud.nacos.discovery.NacosServiceDiscovery;
import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.naming.listener.Event;
import com.alibaba.nacos.api.naming.listener.EventListener;
import com.alibaba.nacos.api.naming.listener.NamingEvent;
import com.alibaba.nacos.api.naming.pojo.Instance;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier;

import java.util.List;

/**
 * 处理nacos推送更新事件
 */
@Slf4j
@AllArgsConstructor
public class MyNacosEventListener implements EventListener {

    private LoadBalancerCacheManager loadBalancerCacheManager;
    @Override
    public void onEvent(Event event) {
        try {
            if (event instanceof NamingEvent) {
                Cache cache = loadBalancerCacheManager.getCache(CachingServiceInstanceListSupplier.SERVICE_INSTANCE_CACHE_NAME);
                if(cache!=null){
                    NamingEvent namingEvent = ((NamingEvent) event);
                    String serviceName = namingEvent.getServiceName();
                    String[] split = serviceName.split(Constants.SERVICE_INFO_SPLITER);
                    String serviceId = split[1];
                    log.debug("收到更新服务消息:{}",serviceId);
                    List<Instance> instances = namingEvent.getInstances();
                    cache.put(serviceId,  NacosServiceDiscovery.hostToServiceInstanceList(instances,serviceId));
                }
            }
        }catch (Exception e){
            log.error("处理nacos推送失败!",e);
        }
    }
}

20220620更新:MySubscribeConfig中用到的工具类

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;

/**
 * spring工具类
 */
@Slf4j
@Lazy(false)
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {

    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextHolder.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> requiredType) {
        return applicationContext.getBean(requiredType);
    }

    public static void clearHolder() {
        if (log.isDebugEnabled()) {
            log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
        }
        applicationContext = null;
    }


    @Override
    public void destroy() {
        SpringContextHolder.clearHolder();
    }

}
<think>嗯,用户问的是Java体系中的微服务和分布式是怎么实现的,还有单个服务的多实例是如何被发现的。我需要先理清楚这些概念,然后组织一个清晰的回答。 首先,微服务架构。微服务是一种架构风格,把应用拆分成多个小服务,每个服务独立运行和部署。Java中常用的框架有Spring Cloud、Micronaut、Quarkus,可能还需要提到Dubbo。这些框架提供了不同的工具和模块来支持微服务的各个部分。 然后是分布式系统,这涉及到服务之间的通信、协调和容错。需要提到的技术包括RESTful API、RPC(比如gRPC或Dubbo)、消息队列(Kafka、RabbitMQ)。另外,分布式事务和一致性,可能需要提到Seata或者Spring的解决方案,以及分布式配置中心如Apollo。 接下来是服务发现机制,这是用户问题的重点。单个服务多实例如何被发现。需要解释服务发现的基本概念,服务注册与发现的流程。常用的组件比如Eureka、Consul、Zookeeper、Nacos。特别是Spring Cloud中的Eureka,服务启动时注册到Eureka Server,客户端通过Eureka来发现服务实例。同时要提到负载均衡,比如Ribbon或者Spring Cloud LoadBalancer,它们如何与服务发现配合,选择可用的实例。 可能需要比较不同服务发现组件的区别,比如Eureka和Nacos的区别,但用户可能只需要基本实现方式。另外,健康检查机制也很重要,确保注册中心中的实例都是可用的,避免将请求发送到宕机的服务。 还有,关于服务发现的实现细节,比如客户端如何缓存服务列表,如何更新,服务下线时的处理(比如主动注销或者心跳检测失败)。这些细节可能对用户理解整个机制有帮助。 最后要确保回答结构清晰,分点说明,先介绍微服务和分布式的实现方式,再详细讲解服务发现的机制,包括注册中心和负载均衡。可能需要给出一个例子,比如使用Spring Cloud Eureka和Ribbon的组合,说明整个过程。 检查是否有遗漏的部分,比如是否需要提到容器化部署(如Docker和Kubernetes中的服务发现),但用户的问题可能更偏向于传统Java体系,所以可能不需要深入K8s的部分,但可以稍微提一下,作为扩展。 总结下来,回答的结构应该是: 1. 微服务架构的实现:框架、拆分原则、通信方式。 2. 分布式系统的关键技术:通信、消息队列、分布式事务、配置中心。 3. 服务发现机制:注册中心的作用,具体组件,注册与发现的流程,健康检查。 4. 多实例的负载均衡:客户端负载均衡器的工作原理。 5. 可能的例子:Spring Cloud Eureka + Ribbon。 6. 扩展:容器环境中的服务发现(可选)。 确保语言通俗易懂,避免过于技术化的术语,但又要准确。用户可能有一定的Java基础,但需要明确的概念解释。</think>在Java生态中,微服务与分布式的实现主要依赖一系列框架和技术,而服务实例的发现则通过**服务注册与发现机制**实现。以下是核心实现逻辑: --- ### 一、微服务架构的实现 1. **框架支持** - **Spring Cloud**:提供全套微服务解决方案(服务发现、配置中心、熔断器等)。 - **Apache Dubbo**:高性能RPC框架,支持服务治理。 - **Quarkus/Micronaut**:轻量级框架,适合云原生场景。 2. **服务拆分** - 按业务功能将单体应用拆分为独立服务(如订单服务、支付服务),每个服务可独立开发、部署和扩展。 3. **通信机制** - **RESTful API**:基于HTTP/JSON,通过Feign或RestTemplate实现。 - **RPC**:如Dubbo的远程调用或gRPC的高性能通信。 - **异步消息**:通过Kafka、RabbitMQ解耦服务。 --- ### 二、分布式系统的关键技术 1. **服务发现与注册** - **注册中心**(如Eureka、Nacos、Consul)负责记录服务实例的地址和元数据。 - **服务启动时**:向注册中心注册自身信息(IP、端口、健康状态)。 - **服务下线时**:主动注销或通过心跳超时自动移除。 2. **负载均衡** - **客户端负载均衡**(如Ribbon、Spring Cloud LoadBalancer):从注册中心获取实例列表,按策略(轮询、权重等)选择实例。 - **服务端负载均衡**(如Nginx、Gateway):通过反向代理分发请求。 3. **容错与高可用** - **熔断降级**:Hystrix或Resilience4j防止级联故障。 - **分布式配置**:通过NacosSpring Cloud Config统一管理配置。 - **分布式事务**:Seata或Saga模式保证数据一致性。 --- ### 三、多实例服务发现的实现流程 1. **服务注册** - 服务启动时,向注册中心发送注册请求(例如通过Spring Cloud的`@EnableEurekaClient`)。 - 注册信息包括:服务名(如`order-service`)、IP、端口、健康检查端点。 2. **健康检查** - 注册中心定期调用服务的健康检查接口(如`/actuator/health`),剔除不可用实例。 3. **服务发现** - 客户端(或其他服务)通过服务名查询注册中心,获取所有可用实例列表。 - 例如:订单服务调用支付服务时,通过Feign+Ribbon从Eureka获取支付服务的实例列表。 4. **动态更新** - 当新实例启动或旧实例下线时,注册中心实时更新列表,客户端缓存定期刷新。 --- ### 四、典型技术栈示例 - **服务注册中心**:Nacos(同时支持服务发现和配置管理)或Eureka。 - **通信**:OpenFeign声明式HTTP客户端 + Ribbon负载均衡。 - **API网关**:Spring Cloud Gateway统一路由和限流。 - **容器化部署**:Docker + Kubernetes(通过K8s Service实现服务发现)。 ```java // Spring Cloud 示例:服务注册与发现 @SpringBootApplication @EnableEurekaClient // 标记为Eureka客户端 public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } } // 通过Feign调用其他服务 @FeignClient(name = "payment-service") public interface PaymentServiceClient { @PostMapping("/pay") String pay(@RequestBody Order order); } ``` --- ### 五、总结 - **核心逻辑**:服务实例通过注册中心上报自身状态,客户端动态发现可用实例并负载均衡。 - **优势**:实现水平扩展、故障自动恢复和高可用性。 - **演进趋势**:云原生场景下,Kubernetes内置的Service和Ingress逐渐替代传统注册中心,但原理相通。
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值