分布式注册中心-Spring Cloud Alibaba Nacos

分布式架构入门 一节中,我们已经大概了解了分布式架构的产生。也提到了在分布式架构的演进中,发生的一些问题及解决方案。这里我们主要探讨其中注册中心一块的内容。

前言

文章中所说源码版本以及 Nacos 官网文档:

为什么需要注册中心

分布式架构入门 一节中我们了解到,分布式架构的发展,最终(其实也是目前主流的架构)会演变为如下这样的架构:

image.png

在这样的架构下会给我们带来什么样的问题和挑战呢?

  • 单体服务经过拆分,分成了多个独立的服务,服务与服务之前如何通信。
  • 微服务应用为了避免单体故障,会采用多个副本,服务与服务之前调用,如何负载及调度。
  • 主调服务如何获取到被调服务的信息,包括节点信息以及健康状态。

RPC

RPC(Remote Procedure Call)远程过程调用协议,一种通过网络从远程计算机上请求服务,而不需要了解底层网络技术的协议。

RPC 主要是为了解决服务与服务之前远程调用的问题,像 webservice 、restFul 、dubbo 等都是 rpc 协议。 Java 方面的 RPC 框架有:Dubbo、Spring Cloud、Thrift、grpc 等。

本文中使用的 spring cloud 的 demo(restTemplate) 为例来探讨 Nacos 注册中心的实现。

客户端负载均衡

主调服务与多个被调服务之间如何负载及调度呢,则需要使用负载均衡来实现。这里我们主要使用 Spring Cloud Ribbon。如下图是简单的负载均衡的工作方式,在客户端调用服务端时,不直接调用,而是请求负载均衡器,通过负载均衡算法选择一个服务端的信息返回给客户端,然后再进行调用。

image.png

注册中心原理

解决了上述两个问题以后,我们发现仍然有问题在困扰我们。服务之前如何获取服务的信息,及如何确认服务当前的健康状态。

这里就需要引入注册中心。根据目前已有的问题,我们对注册中心有如下要求:

  1. 可以保存微服务的信息。
  2. 微服务之前可以相互查询保存的信息。
  3. 可以检测到服务的状态是否健康,并及时更新。

image.png

使用 Nacos 注册中心

添加依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>${latest.version}</version>
</dependency>
复制代码

服务提供者

在 application.properties 中配置 Nacos Server 的地址

server.port=8070
spring.application.name=service-provider

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
复制代码

通过 Spring Cloud 原生注解 @EnableDiscoveryClient 开启服务注册发现功能

@SpringBootApplication
@EnableDiscoveryClient
public class NacosProviderApplication {

	public static void main(String[] args) {
		SpringApplication.run(NacosProviderApplication.class, args);
	}

	@RestController
	class EchoController {
		@RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
		public String echo(@PathVariable String string) {
			return "Hello Nacos Discovery " + string;
		}
	}
}
复制代码

服务消费方

在 application.properties 中配置 Nacos Server 的地址

@SpringBootApplication
@EnableDiscoveryClient
public class NacosProviderApplication {

	public static void main(String[] args) {
		SpringApplication.run(NacosProviderApplication.class, args);
	}

	@RestController
	class EchoController {
		@RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
		public String echo(@PathVariable String string) {
			return "Hello Nacos Discovery " + string;
		}
	}
}
复制代码

通过 Spring Cloud 原生注解 @EnableDiscoveryClient 开启服务注册发现功能。给 RestTemplate 实例添加 @LoadBalanced 注解,开启 @LoadBalanced 与 Ribbon 的集成:

@SpringBootApplication
@EnableDiscoveryClient
public class NacosConsumerApplication {

    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(NacosConsumerApplication.class, args);
    }

    @RestController
    public class TestController {

        private final RestTemplate restTemplate;

        @Autowired
        public TestController(RestTemplate restTemplate) {this.restTemplate = restTemplate;}

        @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
        public String echo(@PathVariable String str) {
            return restTemplate.getForObject("http://service-provider/echo/" + str, String.class);
        }
    }
}
复制代码

Nacos 作为注册中心的实现

服务注册流程 - 客户端

AbstractAutoServiceRegistration

Spring Cloud Common 中有一个接口用于抽象 Spring Cloud 服务注册的流程:org.springframework.cloud.client.serviceregistry.AutoServiceRegistration 在该接口下有一个抽象实现 org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration。在该抽象类中,会监听 web 容器初始化的事件,并注册服务信息:

@Override
@SuppressWarnings("deprecation")
public void onApplicationEvent(WebServerInitializedEvent event) {
    // 监听 WebServerInitializedEvent 事件,事件触发时执行 bind 方法
    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());
    // 启动 discovery 
    this.start();
}
复制代码

从这里我们看到,当程序监听到 WebServerInitializedEvent 事件时,会执行到 org.springframework.cloud.client.serviceregistry.AbstractAutoServiceRegistration#start 方法

AbstractAutoServiceRegistration#start

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

    // only initialize if nonSecurePort is greater than 0 and it isn't already running
    // because of containerPortInitializer below
    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);
    }

}
复制代码

AbstractAutoServiceRegistration#start 中,其实就是调用的 register方法进行服务注册,并在注册前后发布了注册前后的回调事件。

AbstractAutoServiceRegistration#register

protected AbstractAutoServiceRegistration(ServiceRegistry<R> serviceRegistry,
            AutoServiceRegistrationProperties properties) {
    this.serviceRegistry = serviceRegistry;
    this.properties = properties;
}

protected void register() {
    this.serviceRegistry.register(getRegistration());
}
复制代码

在抽象类中的 register 将请求代理到了构造方法 serviceRegistry#register。而我们这里是使用的 Nacos 作为注册中心,这里的具体实现是 com.alibaba.cloud.nacos.registry.NacosServiceRegistry

NacosServiceRegistry#register

@Override
public void register(Registration registration) {

    if (StringUtils.isEmpty(registration.getServiceId())) {
            log.warn("No service to register for nacos client...");
            return;
    }
    // 获取 namingserver 
    NamingService namingService = namingService();
    // serviceId 其实就是 spring.applicatio.name
    String serviceId = registration.getServiceId();
    String group = nacosDiscoveryProperties.getGroup();

    // 组装需要注册的节点信息
    Instance instance = getNacosInstanceFromRegistration(registration);

    try {
            // 通过 namingServer 注册节点信息
            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);
    }
}
复制代码

com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register 中先获取了 namingServer 服务,这里可以简单理解为根据 application.properties 配置的 nacosServer 地址获取一个 httpClient。然后组装当前需要注册的节点信息,并通过 namingService.registerInstance 注册服务。

NacosNamingService#registerInstance

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    NamingUtils.checkInstanceIsLegal(instance);
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    // 是否是临时节点,临时节点的话,构建心跳任务
    if (instance.isEphemeral()) {
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    // 注册服务
    serverProxy.registerService(groupedServiceName, groupName, instance);
}
复制代码

这里的具体实现是 com.alibaba.nacos.client.naming.NacosNamingService#registerInstance 。 这个方法中主要做了两个事情:

  • 判断是否是临时节点,如果是临时节点,构建心跳任务。
  • 注册服务。

NamingProxy#registerService

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>(16);
    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", JacksonUtils.toJson(instance.getMetadata()));

    // 调用 API 注册服务
    reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);

}
复制代码

com.alibaba.nacos.client.naming.net.NamingProxy#registerService这里逻辑比较简单:封装请求并调用 API

NamingProxy#reqApi

public String reqApi(String api, Map<String, String> params, Map<String, String> body, List<String> servers,
        String method) throws NacosException {

    params.put(CommonParams.NAMESPACE_ID, getNamespaceId());

    // 判断 nacos-Server 服务信息是否为空
    if (CollectionUtils.isEmpty(servers) && StringUtils.isBlank(nacosDomain)) {
        throw new NacosException(NacosException.INVALID_PARAM, "no server available");
    }

    NacosException exception = new NacosException();

    // 如果只配置一个节点,则重试执行
    if (StringUtils.isNotBlank(nacosDomain)) {
        for (int i = 0; i < maxRetry; i++) {
            try {
                return callServer(api, params, body, nacosDomain, method);
            } catch (NacosException e) {
                exception = e;
                if (NAMING_LOGGER.isDebugEnabled()) {
                    NAMING_LOGGER.debug("request {} failed.", nacosDomain, e);
                }
            }
        }
    } else {
    // 如果配置了多个节点,则随机一个节点执行
        Random random = new Random(System.currentTimeMillis());
        int index = random.nextInt(servers.size());

        for (int i = 0; i < servers.size(); i++) {
            String server = servers.get(index);
            try {
                return callServer(api, params, body, server, method);
            } catch (NacosException e) {
                exception = e;
                if (NAMING_LOGGER.isDebugEnabled()) {
                    NAMING_LOGGER.debug("request {} failed.", server, e);
                }
            }
            index = (index + 1) % servers.size();
        }
    }

    NAMING_LOGGER.error("request: {} failed, servers: {}, code: {}, msg: {}", api, servers, exception.getErrCode(),
            exception.getErrMsg());

    throw new NacosException(exception.getErrCode(),
            "failed to req API:" + api + " after all servers(" + servers + ") tried: " + exception.getMessage());

}
复制代码

NamingProxy#callServer

public String callServer(String api, Map<String, String> params, Map<String, String> body, String curServer,
        String method) throws NacosException {
    long start = System.currentTimeMillis();
    long end = 0;
    injectSecurityInfo(params);
    Header header = builderHeader();

    // 拼装请求 url
    String url;
    if (curServer.startsWith(UtilAndComs.HTTPS) || curServer.startsWith(UtilAndComs.HTTP)) {
        url = curServer + api;
    } else {
        if (!IPUtil.containsPort(curServer)) {
            curServer = curServer + IPUtil.IP_PORT_SPLITER + serverPort;
        }
        url = NamingHttpClientManager.getInstance().getPrefix() + curServer + api;
    }

    try {
        // 通过 nacosRestTemplate 发起 http 请求
        HttpRestResult<String> restResult = nacosRestTemplate
                .exchangeForm(url, header, Query.newInstance().initParams(params), body, method, String.class);
        end = System.currentTimeMillis();

        MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(restResult.getCode()))
                .observe(end - start);
        // 判断是否正常返回
        if (restResult.ok()) {
            return restResult.getData();
        }
        if (HttpStatus.SC_NOT_MODIFIED == restResult.getCode()) {
            return StringUtils.EMPTY;
        }
        throw new NacosException(restResult.getCode(), restResult.getMessage());
    } catch (Exception e) {
        NAMING_LOGGER.error("[NA] failed to request", e);
        throw new NacosException(NacosException.SERVER_ERROR, e);
    }
}
复制代码

这里最终就是通过 http 请求到服务端,将节点的信息注册到服务端。

服务注册流程 - 服务端

从客户端的代码来看,最终发送 http 请求时,请求的是 /v1/ns/instance 接口。这个接口是由 com.alibaba.nacos.naming.controllers.InstanceController#register 提供。

InstanceController#register

@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {

    final String namespaceId = WebUtils
            .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
    final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    NamingUtils.checkServiceNameFormat(serviceName);
    // 解析请求参数,获取节点信息
    final Instance instance = parseInstance(request);

    // 发起注册
    getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
    return "ok";
}
复制代码

InstanceOperatorServiceImpl#registerInstance

@Override
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
    com.alibaba.nacos.naming.core.Instance coreInstance = (com.alibaba.nacos.naming.core.Instance) instance;
    serviceManager.registerInstance(namespaceId, serviceName, coreInstance);
}
复制代码

ServiceManager#registerInstance

public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {

    //创建一个空服务,在Nacos控制台服务列表展示的服务信息,实际上是初始化一个serviceMap,它
是一个ConcurrentHashMap集合
    createEmptyService(namespaceId, serviceName, instance.isEphemeral());
    // 根据命名空间和服务名称获取一个待注册的服务节点
    Service service = getService(namespaceId, serviceName);

    if (service == null) {
        throw new NacosException(NacosException.INVALID_PARAM,
                "service not found, namespace: " + namespaceId + ", service: " + serviceName);
    }
    //调用addInstance创建一个服务实例
    addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
复制代码

ServiceManager#addInstance

public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
        throws NacosException {

    String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);

    Service service = getService(namespaceId, serviceName);

    synchronized (service) {
        List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);

        Instances instances = new Instances();
        instances.setInstanceList(instanceList);

        consistencyService.put(key, instances);
    }
}
复制代码

把服务实例添加到集合中,然后基于一致性协议进行数据的同步。

RaftConsistencyServiceImpl#put

image.png 这里可以看到,ConsistencyService 有很多的实现类,在集群模式下,一般 Nacos 的使用 raft 协议

@Override
public void put(String key, Record value) throws NacosException {
    checkIsStopWork();
    try {
        raftCore.signalPublish(key, value);
    } catch (Exception e) {
        Loggers.RAFT.error("Raft put failed.", e);
        throw new NacosException(NacosException.SERVER_ERROR, "Raft put failed, key:" + key + ", value:" + value,
                e);
    }
}
复制代码

这里就是基于 raft 协议完成后续的节点信息的同步。

服务发现流程 - 消费方

看完提供方如何将自己的信息注册到注册中心,接下来我们看看消费方是如何获取到提供方服务列表的。

NacosDiscoveryClient#getInstances

@Override
public List<ServiceInstance> getInstances(String serviceId) {
    try {
            return serviceDiscovery.getInstances(serviceId);
    }
    catch (Exception e) {
            throw new RuntimeException(
                            "Can not get hosts from nacos server. serviceId: " + serviceId, e);
    }
}
复制代码

在 Spring Cloud 中,服务发现的客户端是 org.springframework.cloud.client.discovery.DiscoveryClient 其中提供两个主要的方法:

  • org.springframework.cloud.client.discovery.DiscoveryClient#getInstances 获取指定服务的所有节点。
  • org.springframework.cloud.client.discovery.DiscoveryClient#getServices 获取所有的服务列表。 这里的实现类是 com.alibaba.cloud.nacos.discovery.NacosDiscoveryClient

NacosServiceDiscovery#getInstances

public List<ServiceInstance> getInstances(String serviceId) throws NacosException {
    String group = discoveryProperties.getGroup();
    List<Instance> instances = namingService().selectInstances(serviceId, group,
                    true);
    return hostToServiceInstanceList(instances, serviceId);
}
复制代码

这里主要做了两个事:

  • 通过 namingService 获取服务列表。
  • 将 nacos 返回的服务列表转换为 Spring Cloud 中通用的 org.springframework.cloud.client.ServiceInstance 对象

NacosNamingService#selectInstances

@Override
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,
        boolean subscribe) throws NacosException {

    ServiceInfo serviceInfo;
    // 是否订阅服务地址的变化,默认为 ture
    if (subscribe) {
        serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
                StringUtils.join(clusters, ","));
    } else {
        serviceInfo = hostReactor
                .getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
                        StringUtils.join(clusters, ","));
    }
    // 有入参 healthy,挑选列表中健康的节点并返回。
    return selectInstances(serviceInfo, healthy);
}
复制代码

HostReactor#getServiceInfo

public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {

    NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
    String key = ServiceInfo.getKey(serviceName, clusters);
    if (failoverReactor.isFailoverSwitch()) {
        return failoverReactor.getService(key);
    }

    // 获取缓存对象
    ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);

    // 如果缓存为空,则更新缓存
    if (null == serviceObj) {
        serviceObj = new ServiceInfo(serviceName, clusters);

        serviceInfoMap.put(serviceObj.getKey(), serviceObj);
        
        // 将 serviceName 加入待更新列表
        updatingMap.put(serviceName, new Object());
        // 立刻启动更新
        updateServiceNow(serviceName, clusters);
        // 将 serviceName 从待更新列表中移出
        updatingMap.remove(serviceName);

    // 如果当前 serviceName 在待更新列表中,等待。
    } else if (updatingMap.containsKey(serviceName)) {

        if (UPDATE_HOLD_INTERVAL > 0) {
            // hold a moment waiting for update finish
            synchronized (serviceObj) {
                try {
                    serviceObj.wait(UPDATE_HOLD_INTERVAL);
                } catch (InterruptedException e) {
                    NAMING_LOGGER
                            .error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
                }
            }
        }
    }
    // 开启任务更新环境
    scheduleUpdateIfAbsent(serviceName, clusters);

    return serviceInfoMap.get(serviceObj.getKey());
}
复制代码

com.alibaba.nacos.client.naming.core.HostReactor#getServiceInfo 中主要有两个逻辑:

  • 本地缓存为空,updateServiceNow,立即加载服务信息。
  • scheduleUpdateIfAbsent 开启定时调度,定时去查询服务信息。

HostReactor#updateServiceNow & HostReactor#updateService

private void updateServiceNow(String serviceName, String clusters) {
    try {
        // 调用 updateService
        updateService(serviceName, clusters);
    } catch (NacosException e) {
        NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
    }
}
public void updateService(String serviceName, String clusters) throws NacosException {
    // 获取更新前的节点
    ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
    try {
        // 调用 api 获取服务的列表。
        String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);

        如果服务端返回的列表不为空,则更新缓存
        if (StringUtils.isNotEmpty(result)) {
            processServiceJson(result);
        }
    } finally {
        if (oldService != null) {
            synchronized (oldService) {
                oldService.notifyAll();
            }
        }
    }
}
复制代码

updateService 里面主要做了两个事情:

  • 请求服务端获取 serviceName 的节点列表
  • 根据查询的数据更新缓存

HostReactor#processServiceJson

public ServiceInfo processServiceJson(String json) {
    ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class);
    ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());

    if (pushEmptyProtection && !serviceInfo.validate()) {
        //empty or error push, just ignore
        return oldService;
    }

    boolean changed = false;

    // 如果 oldService 不为空,则根据与 newService 做比较,确认新增及删除的部分并发送节点变化的事件
    if (oldService != null) {

        if (oldService.getLastRefTime() > serviceInfo.getLastRefTime()) {
            NAMING_LOGGER.warn("out of date data received, old-t: " + oldService.getLastRefTime() + ", new-t: "
                    + serviceInfo.getLastRefTime());
        }

        serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);

        Map<String, Instance> oldHostMap = new HashMap<String, Instance>(oldService.getHosts().size());
        for (Instance host : oldService.getHosts()) {
            oldHostMap.put(host.toInetAddr(), host);
        }

        Map<String, Instance> newHostMap = new HashMap<String, Instance>(serviceInfo.getHosts().size());
        for (Instance host : serviceInfo.getHosts()) {
            newHostMap.put(host.toInetAddr(), host);
        }

        Set<Instance> modHosts = new HashSet<Instance>();
        Set<Instance> newHosts = new HashSet<Instance>();
        Set<Instance> remvHosts = new HashSet<Instance>();

        // ... 省略部分代码
        serviceInfo.setJsonFromServer(json);

        if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
            NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
                    serviceInfo.getClusters(), serviceInfo.getHosts()));
            DiskCache.write(serviceInfo, cacheDir);
        }

    // 如果 oldService 为空则直接更新缓存并发送节点变化的事件
    } else {
        changed = true;
        NAMING_LOGGER.info("init new ips(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
                + JacksonUtils.toJson(serviceInfo.getHosts()));
        serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
        NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(),
                serviceInfo.getClusters(), serviceInfo.getHosts()));
        serviceInfo.setJsonFromServer(json);
        DiskCache.write(serviceInfo, cacheDir);
    }

    MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());

    if (changed) {
        NAMING_LOGGER.info("current ips:(" + serviceInfo.ipCount() + ") service: " + serviceInfo.getKey() + " -> "
                + JacksonUtils.toJson(serviceInfo.getHosts()));
    }

    return serviceInfo;
}
复制代码

com.alibaba.nacos.client.naming.core.HostReactor#processServiceJson 的代码比较长,主要有两个分支:

  • 如果 oldService 不为空,则比较 OldService 与 newService,如果是节点信息变化,需要更新心跳信息。最后更新缓存并发送节点变化的事件。
  • 如果 oldService 为空,则直接更新缓存并发送节点变化的事件。 更新缓存后再由 com.alibaba.nacos.client.naming.core.HostReactor#getServiceInfo 返回给 NacosNamingService#selectInstances 至此。就是通过 NacosDiscoveryClient 获取指定 service 的节点信息的大概流程。

总结

本小节主要讨论了 Nacos 作为注册中心在 Spring Cloud 中的实现。

关于 Nacos 如何实现注册中心的一些基本应用:

  1. 可以保存微服务的信息。
    • 服务端保存微服务的注册信息,并使用 raft 协议进行数据同步,保证数据的一致性。
  2. 微服务之前可以相互查询保存的信息。
    • 通过 NacosServiceDiscovery#getInstances 查询服务端的节点信息,并同步修改本地的缓存。
  3. 可以检测到服务的状态是否健康,并及时更新。
    • HostReactor#getServiceInfo 方法中,会开启定时任务,定时检测服务节点的变化情况,如有节点发生变化,则会重新拉取新的数据,并更新缓存。

以及 Nacos 在 Sring Cloud 中的实现。

  • 服务提供方主要通过 NacosServiceRegistry#register 完成服务的注册,将服务的节点信息注册到 Nacos 中,服务端的 InstanceController#register 接收到注册请求,把服务实例添加到集合中,然后基于一致性协议进行数据的同步。

  • 服务消费方主要通过 NacosDiscoveryClient#getInstances 查找对应服务的节点信息。

分类:
后端
标签: