【Nacos源码系列】服务注册的原理

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。

文章目录

  • Nacos介绍
  • 服务注册
    • 客户端注册
    • 服务端
  • 总结

Nacos介绍

Nacos是一个基于云原生的动态服务发现、配置管理和服务治理平台,由阿里巴巴开源。它提供了服务注册与发现、配置管理、动态DNS、流量管理、服务降级、负载均衡、限流、路由管理等一系列核心功能,可以帮助企业构建弹性可扩展的微服务架构。

【Nacos源码系列】服务注册的原理_第1张图片

本文将从源码的角度介绍一下Nacos服务注册原理。

服务注册

服务注册是指将某个服务的相关信息(如服务名称、IP地址、端口号等)注册到服务注册中心中,以便其他服务或客户端能够发现和访问该服务。

在微服务架构中,由于服务的数量众多,服务之间的调用关系也变得复杂且动态,因此需要一种统一和自动化的方式来管理和发现服务。而服务注册中心就是这样一种机制,它可以帮助服务消费者快速地找到可用的服务提供者,并完成服务调用。

本文将从客户端和服务端的角度介绍Nacos服务注册的原理。

  • 客户端注册主要介绍客户端是怎么注册到Nacos中的。
  • 服务端注册主要介绍接收到客户端注册请求后服务端是怎么处理的。
客户端 服务端
版本 Spring Cloud Alibaba 2021.0.1.0 1.4.0

客户端注册

Spring Cloud Alibaba Nacos客户端是基于Springboot自动装配功能实现的,了解Springboot自动装配原理的应该知道首先要查看 spring.factories 文件有哪些自动装配的类。

【Nacos源码系列】服务注册的原理_第2张图片

从名字可以看出NacosServiceRegistryAutoConfiguration类是进行服务注册的,源码如下:

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

	@Bean
	public NacosServiceRegistry nacosServiceRegistry(
			NacosDiscoveryProperties nacosDiscoveryProperties) {
		return new NacosServiceRegistry(nacosDiscoveryProperties);
	}

	@Bean
	@ConditionalOnBean(AutoServiceRegistrationProperties.class)
	public NacosRegistration nacosRegistration(
			ObjectProvider<List<NacosRegistrationCustomizer>> registrationCustomizers,
			NacosDiscoveryProperties nacosDiscoveryProperties,
			ApplicationContext context) {
		return new NacosRegistration(registrationCustomizers.getIfAvailable(),
				nacosDiscoveryProperties, context);
	}

	@Bean
	@ConditionalOnBean(AutoServiceRegistrationProperties.class)
	public NacosAutoServiceRegistration nacosAutoServiceRegistration(
			NacosServiceRegistry registry,
			AutoServiceRegistrationProperties autoServiceRegistrationProperties,
			NacosRegistration registration) {
		return new NacosAutoServiceRegistration(registry,
				autoServiceRegistrationProperties, registration);
	}
}

@ConditionalOnNacosDiscoveryEnabled 注解用于使@EnableDiscoveryClient 注解生效,但其实有没有在启动类使用@EnableDiscoveryClient都能让客户端生效。因为@ConditionalOnProperty 注解的属性matchIfMissing = true,也就是有没有对应的value值,自动配置都会生效。

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@ConditionalOnProperty(value = "spring.cloud.nacos.discovery.enabled",
		matchIfMissing = true)
public @interface ConditionalOnNacosDiscoveryEnabled {
}

从源码看到NacosServiceRegistryAutoConfiguration 类会注入以下3个类:

  1. NacosServiceRegistry :实现了Springcloud的ServiceRegistry接口,ServiceRegistry接口是Springcloud提供的一个服务注册的接口,如果想集成Springcloud注册中心必须要实现的接口。NacosServiceRegistry实现了它,并完成nacos服务注册功能。

  2. NacosRegistration :存储nacos服务端信息,它实现了Registration接口,Registration接口中没有任何东西,只是实现了ServiceInstance 接口,ServiceInstance 接口存储一些服务实例的信息。

  3. NacosAutoServiceRegistration:继承了AbstractAutoServiceRegistration抽象类,AbstractAutoServiceRegistration抽象类实现了AutoServiceRegistration , ApplicationContextAware, ApplicationListener ,通过spring的事件监听机制来完成服务的注册。

【Nacos源码系列】服务注册的原理_第3张图片

先来看下NacosAutoServiceRegistration的父类AbstractAutoServiceRegistration抽象类部分源码:

public abstract class AbstractAutoServiceRegistration<R extends Registration>
implements AutoServiceRegistration,ApplicationContextAware,ApplicationListener<WebServerInitializedEvent>{
    // 具体的服务注册接口
    private final ServiceRegistry<R> serviceRegistry;
    // 自动注册配置
    private AutoServiceRegistrationProperties properties;

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

    // 监听接口
    @Override
    @SuppressWarnings("deprecation")
    public void onApplicationEvent(WebServerInitializedEvent event) {
        bind(event);
    }

    // 绑定WebServerInitializedEvent事件
    @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;
        }

        // 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);
        }
    }

    //销毁服务
    @PreDestroy
    public void destroy() {
        stop();
    }

    /**
    *  注册服务
    * Register the local service with the {@link ServiceRegistry}.
    */
    protected void register() {
        //真正注册服务的地方
        this.serviceRegistry.register(getRegistration());
    }
}

从源码可以看到 AbstractAutoServiceRegistration实现了ApplicationListener 监听接口,并监听了WebServerInitializedEvent 事件,WebServerInitializedEvent 事件在容器启动时会进行事件发布。

AbstractAutoServiceRegistration 启动流程是这样的:
容器启动时,发布了WebServerInitializedEvent 事件,AbstractAutoServiceRegistration会调用onApplicationEvent(WebServerInitializedEvent)方法,onApplicationEvent()方法会调用绑定了WebServerInitializedEvent 事件的bind(WebServerInitializedEvent) 方法,同时后续调用start()方法启动注册流程,start()方法会调用ServiceRegistry#register() 开始进行注册。

【Nacos源码系列】服务注册的原理_第4张图片

NacosServiceRegistry实现了ServiceRegistry接口,所以 注册从NacosServiceRegistry#register()方法开始的。

@Override
public void register(Registration registration) {
    if (StringUtils.isEmpty(registration.getServiceId())) {
        log.warn("No service to register for nacos client...");
        return;
    }
    // 通过配置文件获取服务相关的信息
    NamingService namingService = namingService();

    String serviceId = registration.getServiceId();
    String group = nacosDiscoveryProperties.getGroup();
    // 通过配置文件获取实例相关的信息
    Instance instance = getNacosInstanceFromRegistration(registration);

    try {
        //服务注册
        namingService.registerInstance(serviceId, group, instance);
        log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
                instance.getIp(), instance.getPort());
    }
    catch (Exception e) {
        if (nacosDiscoveryProperties.isFailFast()) {
            log.error("nacos registry, {} register failed...{},", serviceId,
                    registration.toString(), e);
            rethrowRuntimeException(e);
        }
        else {
            log.warn("Failfast is false. {} register failed...{},", serviceId,
                    registration.toString(), e);
        }
    }
}

NacosServiceRegistry#register()方法中,会先通过客户端的配置文件创建一个 NamingService ,然后把封装过服务实例的基本信息的 Registration 对象(Registration 接口只是空继承了 ServiceInstance 接口,类中没有任何方法)生成一个 Instance 对象,最后通过 NamingService#registerInstance() 将服务实例注册到Nacos注册中心去。

追溯源码可以发现 NamingService 是通过**NamingFactory#createNamingService(Properties)**方法 创建的:

public static NamingService createNamingService(Properties properties) throws NacosException {
    try {
        Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
        Constructor constructor = driverImplClass.getConstructor(Properties.class);
        NamingService vendorImpl = (NamingService) constructor.newInstance(properties);
        return vendorImpl;
    } catch (Throwable e) {
        throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
    }
}

createNamingService 方法通过反射调用**NacosNamingService(Properties)**构造方法创建对象,

public NacosNamingService(Properties properties) throws NacosException {
    init(properties);
}

//初始化操作
private void init(Properties properties) throws NacosException {
    //验证properties参数是否正确
    ValidatorUtils.checkInitParam(properties);
    //初始化namespace
    this.namespace = InitUtils.initNamespaceForNaming(properties);
    InitUtils.initSerialization();
    //初始化server 地址
    initServerAddr(properties);

    InitUtils.initWebRootContext();
    //初始化缓存目录
    initCacheDir();
    //初始化日志名
    initLogName(properties);

    //创建事件转发器
    this.eventDispatcher = new EventDispatcher();
    this.serverProxy = new NamingProxy(this.namespace, this.endpoint, this.serverList, properties);
    //初始化心跳反应
    this.beatReactor = new BeatReactor(this.serverProxy, initClientBeatThreadCount(properties));
    this.hostReactor = new HostReactor(this.eventDispatcher, this.serverProxy, beatReactor, this.cacheDir,
    isLoadCacheAtStart(properties), initPollingThreadCount(properties));
}

再看一下 NacosNamingService#registerInstance() 方法:

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    // 临时节点
    if (instance.isEphemeral()) {
        //封装心跳信息
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        //加入定时任务
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    //实际进行代理注册服务
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

//添加定时任务
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
    NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
    // 心跳key,形如:  DEFAULT_GROUP@@provider#192.168.71.70#9093
    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);
    //心跳定时任务 默认5s一次
    executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
    MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}

NacosNamingService#registerInstance() 方法会对临时节点一个心跳封装,创建一个 BeatTask 心跳任务以保证客户端的健康,默认5s执行一次,最后通过NamingProxy#registerService执行服务注册。

进行服务注册的是
com.alibaba.nacos.client.naming.net.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()));

    // 封装请求
    reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}

NamingProxy#registerService方法会将服务实例的信息放到Map 集合中,然后传递给reqApi方法。
reqApi方法调用callServer方法,callServer方法最终会把这些包含客户端信息的参数封装起来并生成一个url,通过Nacos提供的openAPI形式调用服务端接口完成注册。

【Nacos源码系列】服务注册的原理_第5张图片

总结一下客户端注册流程,客户端启动时,会向Nacos注册中心发送注册请求。注册请求中包含了服务名、IP地址、端口号和其他元数据信息。客户端将自己的信息注册到Nacos中心,这样服务消费者才能够发现它。
在Nacos中,服务提供者注册的信息被称为服务实例(Instance),一个服务可以有多个实例,每个实例都有一个唯一的ID来标识自己。

【Nacos源码系列】服务注册的原理_第6张图片

服务端

客户端注册完成之后,服务端在接到注册请求之后会做什么呢?

通过actuator找到到客户端请求注册之后服务端的controller:

【Nacos源码系列】服务注册的原理_第7张图片

InstanceController类用于处理服务注册和发现的HTTP请求,包括注册、注销和查询服务实例列表等操作。

@Autowired
private ServiceManager serviceManager;

@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);
    //检查服务格式是否正确
    checkServiceNameFormat(serviceName);
    //将请求中的参数转化为实例信息
    final Instance instance = parseInstance(request);
    //服务端注册实例信息
    serviceManager.registerInstance(namespaceId, serviceName, instance);
    return "ok";
}

InstanceController#register方法将客户端传过来的参数进行解析并进行一系列操作之后,通过ServiceManager#registerInstance方法进行实例注册。

ServiceManager 类提供了一些注册和管理服务的方法。来看下ServiceManager#registerInstance方法将一个服务实例注册到Nacos注册中心中,且看源码:

public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
    // 创建一个服务
    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(namespaceId, serviceName, instance.isEphemeral(), instance);
}

createEmptyService 方法会初始化一个服务,使服务能注册到nacos注册中心。

public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
    createServiceIfAbsent(namespaceId, serviceName, local, null);
}

/**
    * Create service if not exist.
    *
    * @param namespaceId namespace
    * @param serviceName service name
    * @param local       whether create service by local
    * @param cluster     cluster
    * @throws NacosException nacos exception
    */
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
        throws NacosException {
    //根据namespace和服务名称获取服务,获取不到新建一个服务。
    Service service = getService(namespaceId, serviceName);
    if (service == null) {

        Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
        service = new Service();
        service.setName(serviceName);
        service.setNamespaceId(namespaceId);
        service.setGroupName(NamingUtils.getGroupName(serviceName));
        // now validate the service. if failed, exception will be thrown
        service.setLastModifiedMillis(System.currentTimeMillis());
        service.recalculateChecksum();
        if (cluster != null) {
            cluster.setService(service);
            service.getClusterMap().put(cluster.getName(), cluster);
        }
        //验证服务名是否正确
        service.validate();
        //将服务放入serviceMap本地缓存中并进行初始化
        putServiceAndInit(service);

        if (!local) {
            addOrReplaceService(service);
        }
    }
}

【Nacos源码系列】服务注册的原理_第8张图片

通过 createServiceIfAbsent 会进行创建服务,并在最后将创建好的服务放到本地缓存中。

/**
* Map(namespace, Map(group::serviceName, Service)).
* 同一个namespace下放到一起,同一个组的服务
*/
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();

private void putServiceAndInit(Service service) throws NacosException {
    // 放到本地缓存中
    putService(service);
    //初始化服务,对客户端进行心跳检查和服务集群进行初始化
    service.init();
    //监听数据的变化保持集群中数据的一致性
    consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
    consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
    Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}

 /**
* Put service into manager.
*
* @param service service
*/
public void putService(Service service) {
    if (!serviceMap.containsKey(service.getNamespaceId())) {
        // 同步,确保只有一个线程进行操作
        synchronized (putServiceLock) {
            if (!serviceMap.containsKey(service.getNamespaceId())) {
                serviceMap.put(service.getNamespaceId(), new ConcurrentHashMap<>(16));
            }
        }
    }
    serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
}

可以看到最后nacos服务端将服务缓存到private final Map> serviceMap = new ConcurrentHashMap<>();中,nacos通过不同的 namespace 来管理服务,在同一个 namespace 下又使用不同的group来管理服务。

至此,服务端接收到客户端的简单流程就结束了,至于服务端怎么更新缓存、服务集群怎么更新实例、还有诸如心跳健康检查之类的后续再介绍。

总结一下服务端注册流程,Nacos注册中心接收到服务提供者的注册请求后,将服务实例的信息保存到本地缓存或持久化存储中。同时,Nacos还会根据服务名和实例的标识来生成一个全局唯一的服务实例ID。

总结

最后总结一下服务注册的流程:

  1. 服务启动时,根据spring.factories文件自动注入 spring-cloud-common包下的 AutoServiceRegistrationConfiguration 类,同时注入 Nacos的 NacosServiceRegistryNacosRegistration类。
  2. AutoServiceRegistrationConfiguration 类通过监听 WebServerInitializedEvent 容器启动事件,调用start()方法开始注册流程。
  3. start()方法会调用 ServiceRegistry 接口的实现类 NacosServiceRegistryregister() 进行注册。
  4. NacosServiceRegistry#register()方法中会创建一个 NamingServiceInstance 对象,然后通过NacosNamingService#registerInstance()方法将Instance 对象注册到注册中心。
  5. NacosNamingService#registerInstance()方法会给客户端
    开启一个心跳定时任务以保证服务端能感知客户端的存活,同时通过 NamingProxy#registerService将客户端服务名、IP地址、端口号和其他元数据信息以openAPI的形式请求给服务端,完成客户端的服务注册。
  6. 服务端在InstanceController#register方法中处理客户端的注册请求。
  7. 接收到客户端请求后ServiceManager#registerInstance方法会将客户端注册进来,并将客户端缓存到一个名为 serviceMap 数据结构为 ConcurrentHashMap 的本地缓存中。

你可能感兴趣的:(Spring,Cloud,Alibaba,微服务,spring,cloud,java,Nacos,服务注册原理)