Springboot+Nacos项目

微服务

微服务(Microservices)是一种软件架构风格,他区别与单体架构,将拆分为多个小型的、独立的服务,每个服务都可以独立开发、部署和维护。这些服务通过轻量级的API进行通信。

Nacos简述

Nacos 用于发现、配置和管理微服务。nacos有2个核心功能,一个是注册中心,一个是配置中心。这里Nacos学习就是围绕着注册中心和配置中心2个核心功能展开的。

  • 注册中心的重点
    服务的注册:将服务注册到Nacos上
    服务的发现:找到相关的服务,使用RPC组件完成服务的调用

  • 配置中心的重点
    发现配置
    获取配置

当前是以Naocs2.4.3版本学习总结的。

Spring Boot整合Nacos步骤

服务版本说明:

官网链接:https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明

添加依赖:

    //    nacos依赖引入
    implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery:2021.0.5.0'
    implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config:2021.0.5.0'

在resources目录下增加bootstrap.yml文件

spring:
  application:
    name: springboot_demo
  cloud:
    nacos:
      config:
        file-extension: yml
        server-addr: 127.0.0.1:8848
        enabled: true
      discovery:
        server-addr: 127.0.0.1:8848
        enabled: true

启动服务后在浏览器访问http://127.0.0.1:8848/nacoss查看下

服务已经注册到Nacos上了

增加测试用例直接调用接口注册服务

@SpringBootTest
class SpringbootDemo2ApplicationTests {

    @Test
    void contextLoads() throws Exception {
        NamingService namingService = NamingFactory.createNamingService("127.0.0.1:8848");
        // 将服务注册到nacos上
        namingService.registerInstance("nacos.test.1", "11.11.11.11", 8888, "test");
        Thread.sleep(Integer.MAX_VALUE);
    }

Nacos的服务注册源码

客户端注册服务到Nacos上逻辑:

是使用这个方法
namingService.registerInstance("nacos.test.1", "11.11.11.11", 8888, "test");

将当前服务信息存到Map集合里,然后调用接口(/nacos/v1/ns/instance),将服务信息发给nacos

public class NacosNamingService implements NamingService {
    @Override
    public void registerInstance(String serviceName, String groupName, String ip, int port, String clusterName) throws NacosException {

        // 生成实例对象,填充ip,端口,和服务名
        Instance instance = new Instance();
        instance.setIp(ip);
        instance.setPort(port);
        instance.setWeight(1.0);
        instance.setClusterName(clusterName);

        registerInstance(serviceName, groupName, instance);
    }
	
    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {

        // 判断是否是临时实例
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
			// 拼接服务名 groupName@@serviceName
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
            beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
            // 心跳检测服务
            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }
        // 调用api注册服务信息
        serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }
	
}
public class NamingProxy {
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {

        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}",
            namespaceId, serviceName, instance);

        final Map<String, String> params = new HashMap<String, String>(9);
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, serviceName);
        params.put(CommonParams.GROUP_NAME, groupName);
        params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
        params.put("ip", instance.getIp());
        params.put("port", String.valueOf(instance.getPort()));
        params.put("weight", String.valueOf(instance.getWeight()));
        params.put("enable", String.valueOf(instance.isEnabled()));
        params.put("healthy", String.valueOf(instance.isHealthy()));
        params.put("ephemeral", String.valueOf(instance.isEphemeral()));
        params.put("metadata", JSON.toJSONString(instance.getMetadata()));

        reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, HttpMethod.POST);

    }

}

服务端Nacos接收到注册请求的逻辑

是/nacos/v1/ns/instance接口提供服务的

是在InstanceController这个类里处理实例相关的请求。

@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + UtilsAndCommons.NACOS_NAMING_INSTANCE_CONTEXT)
@ExtractorManager.Extractor(httpExtractor = NamingDefaultHttpParamExtractor.class)
public class InstanceController {
    
    
    /**
     * Register new instance.
     * 注册一个新实例
     * @param request http request
     * @return 'ok' if success
     * @throws Exception any error during register
     */
    @CanDistro
    @PostMapping
    @TpsControl(pointName = "NamingInstanceRegister", name = "HttpNamingInstanceRegister")
    @Secured(action = ActionTypes.WRITE)
    public String register(HttpServletRequest request) throws Exception {
        // 获取命名空间namespaceId
        final String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID,
                Constants.DEFAULT_NAMESPACE_ID);
		// 获取服务提供者的服务名称,从请求里
        final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
		// 检查服务名称是否合法default@@serviceName
        NamingUtils.checkServiceNameFormat(serviceName);
        // 构建instance,服务提供者相关信息封装成Instance对象
        final Instance instance = HttpRequestInstanceBuilder.newBuilder()
                .setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();
        // 完成服务的注册功能
        getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
		// 服务注册完成,发布相关事件,触发相关监听器
        NotifyCenter.publishEvent(new RegisterInstanceTraceEvent(System.currentTimeMillis(),
                NamingRequestUtil.getSourceIpForHttpRequest(request), false, namespaceId,
                NamingUtils.getGroupName(serviceName), NamingUtils.getServiceName(serviceName), instance.getIp(),
                instance.getPort()));
        return "ok";
    }
	
}
@org.springframework.stereotype.Service

public class InstanceOperatorClientImpl implements InstanceOperator {
    /**
     * This method creates {@code IpPortBasedClient} if it doesn't exist.
     */
    @Override
    public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
	    // 校验Instance对象是否合法
        NamingUtils.checkInstanceIsLegal(instance);
        // 判断是否是临时实例
        boolean ephemeral = instance.isEphemeral();
		// 获取客户端ID address + ID_DELIMITER + ephemeral
		// 每个客户端都有一个clientID,是nacos2.0之后提供的,一个grpc长连接对应一个client
        String clientId = IpPortBasedClient.getClientId(instance.toInetAddr(), ephemeral);
		// clientId不存在处理,创建client对象,和clientId绑定,并且把client对象存储到client集合中
        createIpPortClientIfAbsent(clientId);
		// 获取创建的服务
        Service service = getService(namespaceId, serviceName, ephemeral);
		// 完成具体的服务注册
        clientOperationService.registerInstance(service, instance, clientId);
    }
	
	// clientManager用于客户端的管理
    private void createIpPortClientIfAbsent(String clientId) {
        if (!clientManager.contains(clientId)) {
            ClientAttributes clientAttributes = ClientAttributesFilter.getCurrentClientAttributes()
                    .orElse(new ClientAttributes());
			// 设置客户端连接
            clientManager.clientConnected(clientId, clientAttributes);
        }
    }

}
@DependsOn({"clientServiceIndexesManager", "namingMetadataManager"})
@Component("clientManager")
public class ClientManagerDelegate implements ClientManager {
    // 基于TCP连接的客户端管理器,负责管理长连接client和clientID的映射关系
    private final ConnectionBasedClientManager connectionBasedClientManager;
    // 临时实例,基于IP和PORT的客户端管理器
    private final EphemeralIpPortClientManager ephemeralIpPortClientManager;
    // 持久化实例,基于IP和PORT的客户端管理器
    private final PersistentIpPortClientManager persistentIpPortClientManager;
    
    public ClientManagerDelegate(ConnectionBasedClientManager connectionBasedClientManager,
            EphemeralIpPortClientManager ephemeralIpPortClientManager,
            PersistentIpPortClientManager persistentIpPortClientManager) {
        this.connectionBasedClientManager = connectionBasedClientManager;
        this.ephemeralIpPortClientManager = ephemeralIpPortClientManager;
        this.persistentIpPortClientManager = persistentIpPortClientManager;
    }
	
}
@DependsOn("clientServiceIndexesManager")
@Component("ephemeralIpPortClientManager")
public class EphemeralIpPortClientManager implements ClientManager {
    // 存储所有客户端信息
    private final ConcurrentMap<String, IpPortBasedClient> clients = new ConcurrentHashMap<>();
    
    private final DistroMapper distroMapper;
    
    private final ClientFactory<IpPortBasedClient> clientFactory;
    
    public EphemeralIpPortClientManager(DistroMapper distroMapper, SwitchDomain switchDomain) {
        this.distroMapper = distroMapper;
        GlobalExecutor.scheduleExpiredClientCleaner(new ExpiredClientCleaner(this, switchDomain), 0,
                Constants.DEFAULT_HEART_BEAT_INTERVAL, TimeUnit.MILLISECONDS);
        clientFactory = ClientFactoryHolder.getInstance().findClientFactory(ClientConstants.EPHEMERAL_IP_PORT);
    }
    
    @Override
    public boolean clientConnected(String clientId, ClientAttributes attributes) {
	    // 根据clinetId,创建了新client对象,然后把client对象加到clients集合中
        return clientConnected(clientFactory.newClient(clientId, attributes));
    }
	
    @Override
    public boolean clientConnected(final Client client) {
	    // 如果clients集合中没有client,就重新创建一个并添加到集合中
        clients.computeIfAbsent(client.getClientId(), s -> {
            Loggers.SRV_LOG.info("Client connection {} connect", client.getClientId());
            IpPortBasedClient ipPortBasedClient = (IpPortBasedClient) client;
			// 设置心跳检测的定时任务
            ipPortBasedClient.init();
            return ipPortBasedClient;
        });
        return true;
    }
}
@DependsOn("namingSubscriberServiceV2Impl")
@SuppressWarnings("PMD.ServiceOrDaoClassShouldEndWithImplRule")
@Component
public class ClientOperationServiceProxy implements ClientOperationService {
    
    
    @Override
    public void registerInstance(Service service, Instance instance, String clientId) throws NacosException {
	    // 根据是否临时实例,选择需要的service实现
        final ClientOperationService operationService = chooseClientOperationService(instance);
        operationService.registerInstance(service, instance, clientId);
    }
	
}
@Component("ephemeralClientOperationService")
public class EphemeralClientOperationServiceImpl implements ClientOperationService {
    
    private final ClientManager clientManager;
    
    @Override
    public void registerInstance(Service service, Instance instance, String clientId) throws NacosException {
	    // 检查实例对象
        NamingUtils.checkInstanceIsLegal(instance);
        // 一个服务对应一个Service对象,有2个集合存储了所有Service信息,所以用了单例
        Service singleton = ServiceManager.getInstance().getSingleton(service);
        if (!singleton.isEphemeral()) {
            throw new NacosRuntimeException(NacosException.INVALID_PARAM,
                    String.format("Current service %s is persistent service, can't register ephemeral instance.",
                            singleton.getGroupedServiceName()));
        }
		// 根据clientId获取到Client对象
        Client client = clientManager.getClient(clientId);
		// 检查client是否合法
        checkClientIsLegal(client, clientId);
		// InstancePublishInfo需要注册的实例信息
        InstancePublishInfo instanceInfo = getPublishInfo(instance);
		// 服务注册实例,就是Service和Instace的关系
		// 保存了Service和Instance的映射关系,在publishers;通过事件发布机制,发布了ClientChangedEvent事件,完成Nacos节点信息的同步
		// 完成了Client Service Instance的关联
		// client对象保存了publishers对象
        client.addServiceInstance(singleton, instanceInfo);
		// 记录最后更新时间
        client.setLastUpdatedTime();
		// 重新计算版本
        client.recalculateRevision();
		// 发布服务注册的事件,完成客户端订阅的通知
        NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
		// 元数据处理
        NotifyCenter
                .publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
    }
}
public class ServiceManager {
    
    private static final ServiceManager INSTANCE = new ServiceManager();
    
    private final ConcurrentHashMap<Service, Service> singletonRepository;
    
    private final ConcurrentHashMap<String, Set<Service>> namespaceSingletonMaps;
    
    
    /**
     * Get singleton service. Put to manager if no singleton.
     *
     * @param service new service
     * @return if service is exist, return exist service, otherwise return new service
     */
    public Service getSingleton(Service service) {
	    // singletonRepository中没有service时候,新建放进去
        Service result = singletonRepository.computeIfAbsent(service, key -> {
		    // 发布了ServiceMetadataEvent事件,干啥的?
            NotifyCenter.publishEvent(new MetadataEvent.ServiceMetadataEvent(service, false));
            return service;
        });
		// 将Service对象放到namespaceSingletonMaps里
        namespaceSingletonMaps.computeIfAbsent(result.getNamespace(), namespace -> new ConcurrentHashSet<>()).add(result);
        return result;
    }
}
public abstract class AbstractClient implements Client {
    
    protected final ConcurrentHashMap<Service, InstancePublishInfo> publishers = new ConcurrentHashMap<>(16, 0.75f, 1);
    
    protected final ConcurrentHashMap<Service, Subscriber> subscribers = new ConcurrentHashMap<>(16, 0.75f, 1);
    
    protected volatile long lastUpdatedTime;
    
    protected final AtomicLong revision;
    
    protected ClientAttributes attributes;
    
    
    @Override
    public boolean addServiceInstance(Service service, InstancePublishInfo instancePublishInfo) {
        if (instancePublishInfo instanceof BatchInstancePublishInfo) {
            InstancePublishInfo old = publishers.put(service, instancePublishInfo);
            MetricsMonitor.incrementIpCountWithBatchRegister(old, (BatchInstancePublishInfo) instancePublishInfo);
        } else {
            if (null == publishers.put(service, instancePublishInfo)) {
                MetricsMonitor.incrementInstanceCount();
            }
        }
		// 发布ClientChangedEvent事件
		// 做同步的,客户端在Nacos的某个节点上注册了服务的信息,如果Nacos是集群部署,完成Nacos节点信息的同步
        NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(this));
        Loggers.SRV_LOG.info("Client change for service {}, {}", service, getClientId());
        return true;
    }
}

心跳服务原理

客户端向Nacos发起心跳检测请求

当前服务默认是临时实例,其中是临时实例才做发送心跳功能。心跳发送间隔时间默认5秒,延迟5秒执行发送心跳服务。调用接口/nacos/v1/ns/instance/beat 发送服务信息到Nacos。然后检查响应信息,如果响应状态码是20404,则重新发起服务注册。这个发送心跳服务是每隔5秒执行一次的。

客户端源码

客户端每隔5秒发送一次心跳服务到Nacos

public class NacosNamingService implements NamingService { 
 public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {

        if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
			// 默认发生心跳检测的时间,5秒
            beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());

            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }

        serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }
}

BeatReactor 发送心跳代码

public class BeatReactor {
    public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        //fix #1733
        if ((existBeat = dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }
        dom2Beat.put(key, beatInfo);
		// 定时任务,是延迟5秒执行的,BeatTask是要执行的任务
        executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }


    class BeatTask implements Runnable {

        BeatInfo beatInfo;

        public BeatTask(BeatInfo beatInfo) {
            this.beatInfo = beatInfo;
        }

        @Override
        public void run() {
            if (beatInfo.isStopped()) {
                return;
            }
            long nextTime = beatInfo.getPeriod();
            try {
			    // 发送心跳服务
                JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
				// 获取心跳的间隔时间,是服务端返回的
                long interval = result.getIntValue("clientBeatInterval");
                boolean lightBeatEnabled = false;
                if (result.containsKey(CommonParams.LIGHT_BEAT_ENABLED)) {
                    lightBeatEnabled = result.getBooleanValue(CommonParams.LIGHT_BEAT_ENABLED);
                }
                BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
                if (interval > 0) {
                    nextTime = interval;
                }
                int code = NamingResponseCode.OK;
                if (result.containsKey(CommonParams.CODE)) {
                    code = result.getIntValue(CommonParams.CODE);
                }
                if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
                    Instance instance = new Instance();
                    instance.setPort(beatInfo.getPort());
                    instance.setIp(beatInfo.getIp());
                    instance.setWeight(beatInfo.getWeight());
                    instance.setMetadata(beatInfo.getMetadata());
                    instance.setClusterName(beatInfo.getCluster());
                    instance.setServiceName(beatInfo.getServiceName());
                    instance.setInstanceId(instance.getInstanceId());
                    instance.setEphemeral(true);
                    try {
					    // Nacos上没找到当前服务,重新注册本地服务
                        serverProxy.registerService(beatInfo.getServiceName(),
                            NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
                    } catch (Exception ignore) {
                    }
                }
            } catch (NacosException ne) {
                NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                    JSON.toJSONString(beatInfo), ne.getErrCode(), ne.getErrMsg());

            }
			// 开启一个定时任务,延迟5秒
            executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
        }
    }
}

Nacos接收心跳检测请求处理逻辑

@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + UtilsAndCommons.NACOS_NAMING_INSTANCE_CONTEXT)
@ExtractorManager.Extractor(httpExtractor = NamingDefaultHttpParamExtractor.class)
public class InstanceController {

    // Nacos提供心跳处理接口
    @CanDistro
    @PutMapping("/beat")
    @TpsControl(pointName = "HttpHealthCheck", name = "HttpHealthCheck")
    @Secured(action = ActionTypes.WRITE)
    @ExtractorManager.Extractor(httpExtractor = NamingInstanceBeatHttpParamExtractor.class)
    public ObjectNode beat(HttpServletRequest request) throws Exception {
        
        ObjectNode result = JacksonUtils.createEmptyJsonNode();
		// 设置响应里的心跳间隔时长,默认也是5秒
        result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());
        // 从请求中获取心跳注册的相关信息
        String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);
        RsInfo clientBeat = null;
        if (StringUtils.isNotBlank(beat)) {
            clientBeat = JacksonUtils.toObj(beat, RsInfo.class);
        }
        String clusterName = WebUtils.optional(request, CommonParams.CLUSTER_NAME,
                UtilsAndCommons.DEFAULT_CLUSTER_NAME);
        String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);
        int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));
        if (clientBeat != null) {
            if (StringUtils.isNotBlank(clientBeat.getCluster())) {
                clusterName = clientBeat.getCluster();
            } else {
                // fix #2533
                clientBeat.setCluster(clusterName);
            }
            ip = clientBeat.getIp();
            port = clientBeat.getPort();
        }
        String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
        String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
        NamingUtils.checkServiceNameFormat(serviceName);
        Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}, namespaceId: {}", clientBeat,
                serviceName, namespaceId);
        BeatInfoInstanceBuilder builder = BeatInfoInstanceBuilder.newBuilder();
        builder.setRequest(request);
		// 处理心跳服务
        int resultCode = getInstanceOperator().handleBeat(namespaceId, serviceName, ip, port, clusterName, clientBeat,
                builder);
		// 封装响应信息,心跳间隔时间,是否简化心跳信息
        result.put(CommonParams.CODE, resultCode);
        result.put(SwitchEntry.CLIENT_BEAT_INTERVAL,
                getInstanceOperator().getHeartBeatInterval(namespaceId, serviceName, ip, port, clusterName));
		// lightBeatEnabled 默认为true 意思是下一场请求可以简化心跳信息
        result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
        return result;
    }
}

InstanceOperatorClientImpl.handleBeat 处理心跳检测请求

@org.springframework.stereotype.Service
public class InstanceOperatorClientImpl implements InstanceOperator {
    @Override
    public int handleBeat(String namespaceId, String serviceName, String ip, int port, String cluster,
            RsInfo clientBeat, BeatInfoInstanceBuilder builder) throws NacosException {
        Service service = getService(namespaceId, serviceName, true);
        String clientId = IpPortBasedClient.getClientId(ip + InternetAddressUtil.IP_PORT_SPLITER + port, true);
        IpPortBasedClient client = (IpPortBasedClient) clientManager.getClient(clientId);
        if (null == client || !client.getAllPublishedService().contains(service)) {
		    // 说明接收心跳注册的服务不存在,之前没有注册过
            if (null == clientBeat) {
                return NamingResponseCode.RESOURCE_NOT_FOUND;
            }
			// 说明这个心跳服务是不存在,心跳服务提供的信息是完整的
            Instance instance = builder.setBeatInfo(clientBeat).setServiceName(serviceName).build();
			// 注册这个服务
            registerInstance(namespaceId, serviceName, instance);
			// 获取client对象
            client = (IpPortBasedClient) clientManager.getClient(clientId);
        }
		
        if (!ServiceManager.getInstance().containSingleton(service)) {
            throw new NacosException(NacosException.SERVER_ERROR,
                    "service not found: " + serviceName + "@" + namespaceId);
        }
        if (null == clientBeat) {
            clientBeat = new RsInfo();
            clientBeat.setIp(ip);
            clientBeat.setPort(port);
            clientBeat.setCluster(cluster);
            clientBeat.setServiceName(serviceName);
        }
        ClientBeatProcessorV2 beatProcessor = new ClientBeatProcessorV2(namespaceId, clientBeat, client);
		// 异步处理心跳服务
        HealthCheckReactor.scheduleNow(beatProcessor);
        client.setLastUpdatedTime();
        return NamingResponseCode.OK;
    }
}

异步任务处理心跳服务

public class ClientBeatProcessorV2 implements BeatProcessor {
    
    @Override
    public void run() {
        if (Loggers.EVT_LOG.isDebugEnabled()) {
            Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString());
        }
        String ip = rsInfo.getIp();
        int port = rsInfo.getPort();
        String serviceName = NamingUtils.getServiceName(rsInfo.getServiceName());
        String groupName = NamingUtils.getGroupName(rsInfo.getServiceName());
		
        Service service = Service.newService(namespace, groupName, serviceName, rsInfo.isEphemeral());
		// 根据心跳请求中的信息找到Nacos里注册的服务
        HealthCheckInstancePublishInfo instance = (HealthCheckInstancePublishInfo) client.getInstancePublishInfo(service);
		// 判断Nacos注册的服务和心跳请求的信息是否一致
        if (instance.getIp().equals(ip) && instance.getPort() == port) {
		    // 信息一致,之前成功注册过
            if (Loggers.EVT_LOG.isDebugEnabled()) {
                Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo);
            }
			// 设置最后一次心跳检查的时间
            instance.setLastHeartBeatTime(System.currentTimeMillis());
            if (!instance.isHealthy()) {
			    // 说明当前服务已经是不健康状态
				// 更新服务为健康状态
                instance.setHealthy(true);
                Loggers.EVT_LOG.info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok",
                        rsInfo.getServiceName(), ip, port, rsInfo.getCluster(), UtilsAndCommons.LOCALHOST_SITE);
				// 发布事件 向订阅者发起请求,同步Nacos节点信息,健康状态变化事件
                NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service));
                NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(client));
                NotifyCenter.publishEvent(new HealthStateChangeTraceEvent(System.currentTimeMillis(),
                        service.getNamespace(), service.getGroup(), service.getName(), instance.getIp(),
                        instance.getPort(), true, "client_beat"));
            }
        }
    }
}

服务器端Nacos反向探测机制

@org.springframework.stereotype.Service
public class InstanceOperatorClientImpl implements InstanceOperator {
    @Override
    public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
        NamingUtils.checkInstanceIsLegal(instance);
        
        boolean ephemeral = instance.isEphemeral();
        String clientId = IpPortBasedClient.getClientId(instance.toInetAddr(), ephemeral);
		// 创建Client对象,同时和clientId绑定
		//同时完成心跳服务的定时任务
        createIpPortClientIfAbsent(clientId);
        Service service = getService(namespaceId, serviceName, ephemeral);
        clientOperationService.registerInstance(service, instance, clientId);
    }
	
}

在注册实例的方法中createIpPortClientIfAbsent这个方法会完成相关的操作。

private void createIpPortClientIfAbsent(String clientId) {
    // 如果当前注册的服务 之前没有记录信息
    if (!clientManager.contains(clientId)) {
        ClientAttributes clientAttributes;
        if (ClientAttributesFilter.threadLocalClientAttributes.get() != null) {
            clientAttributes = ClientAttributesFilter.threadLocalClientAttributes.get();
        } else {
            clientAttributes = new ClientAttributes();
        }
        // 测试客户端的连接
        clientManager.clientConnected(clientId, clientAttributes);
    }
}

最后的方法是测试客户端的连接测试,这里有区分是临时实例还是永久实例的处理

这里是临时实例的处理

@DependsOn("clientServiceIndexesManager")
@Component("ephemeralIpPortClientManager")
public class EphemeralIpPortClientManager implements ClientManager {
    @Override
    public boolean clientConnected(final Client client) {
        clients.computeIfAbsent(client.getClientId(), s -> {
            Loggers.SRV_LOG.info("Client connection {} connect", client.getClientId());
            IpPortBasedClient ipPortBasedClient = (IpPortBasedClient) client;
			// 客户端信息的初始化操作,在这里完成心跳检测的任务
            ipPortBasedClient.init();
            return ipPortBasedClient;
        });
        return true;
    }
}

init方法里是重点
其中永久节点的健康检查是由服务端定时与客户端建立tcp连接做健康检查,是服务端主动发起的探测,服务器定时请求客户端判断是否健康。

public class IpPortBasedClient extends AbstractClient {
    public void init() {
	    // 初始化客户端Client的
        if (ephemeral) { // 临时实例,延迟5秒,开启一个间隔5秒的定时任务来做心跳健康检查
            beatCheckTask = new ClientBeatCheckTaskV2(this);
            HealthCheckReactor.scheduleCheck(beatCheckTask);
        } else { // 持久化实例,客户端不发心跳检查请求
            healthCheckTaskV2 = new HealthCheckTaskV2(this);
            HealthCheckReactor.scheduleCheck(healthCheckTaskV2);
        }
    }
	
}

这里是临时实例:使用定时任务处理

public class HealthCheckReactor {
    public static void scheduleCheck(BeatCheckTask task) {
        Runnable wrapperTask =
                task instanceof NacosHealthCheckTask ? new HealthCheckTaskInterceptWrapper((NacosHealthCheckTask) task)
                        : task;
		// 开启一个定时任务,延迟5秒执行,间隔时间5秒
        futureMap.computeIfAbsent(task.taskKey(),
                k -> GlobalExecutor.scheduleNamingHealth(wrapperTask, 5000, 5000, TimeUnit.MILLISECONDS));
    }
}

开启了一个延迟5秒执行。间隔5秒的定时任务,来执行心跳检测逻辑

public class ClientBeatCheckTaskV2 extends AbstractExecuteTask implements BeatCheckTask, NacosHealthCheckTask {
    

    @Override
    public void doHealthCheck() {
        try {
		    // 获取所有的服务
            Collection<Service> services = client.getAllPublishedService();
            for (Service each : services) {
			    // 遍历每个服务
                HealthCheckInstancePublishInfo instance = (HealthCheckInstancePublishInfo) client
                        .getInstancePublishInfo(each);
				// 通过InstanceBeatCheckTask的检测器去检测实例的健康状态
                interceptorChain.doInterceptor(new InstanceBeatCheckTask(client, each, instance));
            }
        } catch (Exception e) {
            Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
        }
    }
	
}

拦截器链执行的是InstanceBeatCheckTask中的方法,这里检查是否过期,服务是否健康

public class InstanceBeatCheckTask implements Interceptable {
    
    private static final List<InstanceBeatChecker> CHECKERS = new LinkedList<>(); 
    static {
        CHECKERS.add(new UnhealthyInstanceChecker());
        CHECKERS.add(new ExpiredInstanceChecker());
        CHECKERS.addAll(NacosServiceLoader.load(InstanceBeatChecker.class));
    }
    
    @Override
    public void passIntercept() {
        for (InstanceBeatChecker each : CHECKERS) {
            each.doCheck(client, service, instancePublishInfo);
        }
    }
	
}

非健康检查的逻辑如下,在isUnhealthy方法里完成判断

public class UnhealthyInstanceChecker implements InstanceBeatChecker {
    
    @Override
    public void doCheck(Client client, Service service, HealthCheckInstancePublishInfo instance) {
        if (instance.isHealthy() && isUnhealthy(service, instance)) {
		    // 改变实例健康状态
            changeHealthyStatus(client, service, instance);
        }
    }
    // 具体实现
    private boolean isUnhealthy(Service service, HealthCheckInstancePublishInfo instance) {
	    // 心跳检测的超时时间,默认是15秒
        long beatTimeout = getTimeout(service, instance);
		// 当前时间 - 上一次心跳检测时候, 大于15秒,则任务超时了,实例不健康
        return System.currentTimeMillis() - instance.getLastHeartBeatTime() > beatTimeout;
    }
	
    private void changeHealthyStatus(Client client, Service service, HealthCheckInstancePublishInfo instance) {
	    // 设置当前实例为非健康
        instance.setHealthy(false);
        Loggers.EVT_LOG
                .info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client last beat: {}", instance.getIp(),
                        instance.getPort(), instance.getCluster(), service.getName(), UtilsAndCommons.LOCALHOST_SITE,
                        instance.getLastHeartBeatTime());
		// 发布相关事件
        NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service));
        NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(client));
        NotifyCenter.publishEvent(new HealthStateChangeTraceEvent(System.currentTimeMillis(),
                service.getNamespace(), service.getGroup(), service.getName(), instance.getIp(), instance.getPort(),
                false, "client_beat"));
    }
	
}

到这里临时实例的注册服务的健康检查完成。其实就是比较了上一次发送的心跳时间是否超过了间隔时间15秒。

这里是持久化的实例的处理如下:

public class ExpiredInstanceChecker implements InstanceBeatChecker {
    
    @Override
    public void doCheck(Client client, Service service, HealthCheckInstancePublishInfo instance) {
        boolean expireInstance = ApplicationUtils.getBean(GlobalConfig.class).isExpireInstance();
		// 如果实例过期了,就把节点移除掉,当前时间和上次心跳更新时间大于30秒
        if (expireInstance && isExpireInstance(service, instance)) {
            deleteIp(client, service, instance);
        }
    }
}

在HealthCheckTaskV2 处理的,这里是持久化实例的健康检查

public class HealthCheckTaskV2 extends AbstractExecuteTask implements NacosHealthCheckTask {
    
    @Override
    public void doHealthCheck() {
        try {
            initIfNecessary();
            for (Service each : client.getAllPublishedService()) {
                if (switchDomain.isHealthCheckEnabled(each.getGroupedServiceName())) {
                    InstancePublishInfo instancePublishInfo = client.getInstancePublishInfo(each);
                    ClusterMetadata metadata = getClusterMetadata(each, instancePublishInfo);
					// getBean获取处理健康检查的对象
                    ApplicationUtils.getBean(HealthCheckProcessorV2Delegate.class).process(this, each, metadata);
                    if (Loggers.EVT_LOG.isDebugEnabled()) {
                        Loggers.EVT_LOG.debug("[HEALTH-CHECK] schedule health check task: {}", client.getClientId());
                    }
                }
            }
        } catch (Throwable e) {
            Loggers.SRV_LOG.error("[HEALTH-CHECK] error while process health check for {}", client.getClientId(), e);
        } finally {
            if (!cancelled) {
                initCheckRT();
                HealthCheckReactor.scheduleCheck(this);
                // worst == 0 means never checked
                if (this.getCheckRtWorst() > 0) {
                    // TLog doesn't support float so we must convert it into long
                    long checkRtLastLast = getCheckRtLastLast();
                    this.setCheckRtLastLast(this.getCheckRtLast());
                    if (checkRtLastLast > 0) {
                        long diff = ((this.getCheckRtLast() - this.getCheckRtLastLast()) * 10000) / checkRtLastLast;
                        if (Loggers.CHECK_RT.isDebugEnabled()) {
                            Loggers.CHECK_RT.debug("{}->normalized: {}, worst: {}, best: {}, last: {}, diff: {}",
                                    client.getClientId(), this.getCheckRtNormalized(), this.getCheckRtWorst(),
                                    this.getCheckRtBest(), this.getCheckRtLast(), diff);
                        }
                    }
                }
            }
        }
    }
}

进入process方法里

@Component
public class HttpHealthCheckProcessor implements HealthCheckProcessorV2 {
    
    
    @Override
    public void process(HealthCheckTaskV2 task, Service service, ClusterMetadata metadata) {
        HealthCheckInstancePublishInfo instance = (HealthCheckInstancePublishInfo) task.getClient()
                .getInstancePublishInfo(service);
        if (null == instance) {
            return;
        }
        try {
            // TODO handle marked(white list) logic like v1.x.
            if (!instance.tryStartCheck()) {
                SRV_LOG.warn("http check started before last one finished, service: {} : {} : {}:{}",
                        service.getGroupedServiceName(), instance.getCluster(), instance.getIp(), instance.getPort());
                healthCheckCommon
                        .reEvaluateCheckRT(task.getCheckRtNormalized() * 2, task, switchDomain.getHttpHealthParams());
                return;
            }
            
            Http healthChecker = (Http) metadata.getHealthChecker();
            int ckPort = metadata.isUseInstancePortForCheck() ? instance.getPort() : metadata.getHealthyCheckPort();
            URL host = new URL(HTTP_PREFIX + instance.getIp() + ":" + ckPort);
            URL target = new URL(host, healthChecker.getPath());
            Map<String, String> customHeaders = healthChecker.getCustomHeaders();
            Header header = Header.newInstance();
            header.addAll(customHeaders);
            // 发起服务提供者的异步请求,来检查服务是否可用,new HttpHealthCheckCallback(instance, task, service)是访问后的回调方法
            ASYNC_REST_TEMPLATE.get(target.toString(), header, Query.EMPTY, String.class,
                    new HttpHealthCheckCallback(instance, task, service));
            MetricsMonitor.getHttpHealthCheckMonitor().incrementAndGet();
        } catch (Throwable e) {
            instance.setCheckRt(switchDomain.getHttpHealthParams().getMax());
            healthCheckCommon.checkFail(task, service, "http:error:" + e.getMessage());
            healthCheckCommon.reEvaluateCheckRT(switchDomain.getHttpHealthParams().getMax(), task,
                    switchDomain.getHttpHealthParams());
        }
    }
}

我们可以发现作为持久化实例的处理。是通过http请求来访问服务提供方来检测是否可用访问来实现的。

心跳检测机制总结

启动当前Springboot程序如何将服务注册到Nacos上呢?

首先nacos-discovery依赖包里面有个文件spring.factories,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\
  com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration,\
  com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\
  com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\
  com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\
  com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
  com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration

启动过程中会加载这些配置类,实例化需要的bean

查看NacosServiceRegistryAutoConfiguration配置类,注册流程是在NacosServiceRegistry里开始做的

@Configuration
@EnableConfigurationProperties
@ConditionalOnNacosDiscoveryEnabled
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
@AutoConfigureAfter({ AutoServiceRegistrationConfiguration.class,
		AutoServiceRegistrationAutoConfiguration.class,
		NacosDiscoveryAutoConfiguration.class })
public class NacosServiceRegistryAutoConfiguration {

    // 生成NacosServiceRegistry类型的bean,注入到容器中
	@Bean
	public NacosServiceRegistry nacosServiceRegistry(
			NacosDiscoveryProperties nacosDiscoveryProperties) {
		return new NacosServiceRegistry(nacosDiscoveryProperties);
	}
	......

}

注册本地服务

在com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register里调用namingService.registerInstance方法,最后调用http接口,将服务信息注册到nacos上。
这个入口怎么进来呢?
是在启动springboot项目时,在其中spring生命周期里,getLifecycleProcessor().onRefresh();做的。
NacosAutoServiceRegistration继承了AbstractAutoServiceRegistration抽象类,在执行listener监听器那里做的,去注册本地的服务。实现了AbstractAutoServiceRegistration的register接口(org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration#register)

public abstract class AbstractAutoServiceRegistration<R extends Registration>
		implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> {

	@Override
	@SuppressWarnings("deprecation")
	public void onApplicationEvent(WebServerInitializedEvent event) {
		bind(event);
	}

	@Deprecated
	public void bind(WebServerInitializedEvent event) {
		ApplicationContext context = event.getApplicationContext();
		if (context instanceof ConfigurableWebServerApplicationContext) {
			if ("management".equals(((ConfigurableWebServerApplicationContext) context).getServerNamespace())) {
				return;
			}
		}
		this.port.compareAndSet(0, event.getWebServer().getPort());
		this.start();
	}

	public void start() {
		if (!isEnabled()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Discovery Lifecycle disabled. Not starting");
			}
			return;
		}

		if (!this.running.get()) {
			this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
			register();
			if (shouldRegisterManagement()) {
				registerManagement();
			}
			this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration()));
			this.running.compareAndSet(false, true);
		}

	}
	protected void register() {
		this.serviceRegistry.register(getRegistration());
	}
}
public class NacosServiceRegistry implements ServiceRegistry<Registration> {

	private static final Logger log = LoggerFactory.getLogger(NacosServiceRegistry.class);

	private final NacosDiscoveryProperties nacosDiscoveryProperties;

	private final NamingService namingService;

	public NacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {
		this.nacosDiscoveryProperties = nacosDiscoveryProperties;
		this.namingService = nacosDiscoveryProperties.namingServiceInstance();
	}

	@Override
	public void register(Registration registration) {

		if (StringUtils.isEmpty(registration.getServiceId())) {
			log.warn("No service to register for nacos client...");
			return;
		}

		String serviceId = registration.getServiceId();
		String group = nacosDiscoveryProperties.getGroup();

		Instance instance = getNacosInstanceFromRegistration(registration);

		try {
		    // 这里将服务注册到nacos上
			namingService.registerInstance(serviceId, group, instance);
			log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
					instance.getIp(), instance.getPort());
		}
		catch (Exception e) {
			log.error("nacos registry, {} register failed...{},", serviceId,
					registration.toString(), e);
			// rethrow a RuntimeException if the registration is failed.
			// issue : https://github.com/alibaba/spring-cloud-alibaba/issues/1132
			rethrowRuntimeException(e);
		}
	}
	
}

总结:

  1. 在服务启动的时候,会调用api发起服务注册,通知nacos可以提供服务了
  2. 服务消费者在启动的时候会拉取自已要用的服务的列表
  3. 服务消费者每隔10秒去Nacos拉取一次服务实例,消费者根据返回的实例列表来发起请求。
  4. Nacos服务检测到服务实例发生变化(服务上下线)就会发送UDP协议通知服务消费者进行更新。
  5. 客户端会每5秒定时发送心跳到服务端,来维持他的一个健康的检查,
  6. Nacos定时任务检测:通过每5秒检查一下心跳信息来判断是否超时,就是用当前时间减去上次一心跳的时间,如果超过15秒则将节点设置为非健康状态并进行广播,如果超过30秒则将节点进行移除,说明节点不可用。
posted @ 2024-12-17 13:37  二十四桥冷月夜  阅读(402)  评论(0)    收藏  举报