Nacos注册中心源码分析-服务注册(上)

441 阅读10分钟

为什么需要注册中心

分布式微服务架构下,一个服务通常会部署N个节点。每个节点作为一个独立的服务实例对外提供访问接口。对于客户端来说,如何获取所有的服务实例,以及某个实例不可用时如何动态的感知,这个就是服务注册中心的核心能力。简单来说,分布式的环境需要一个注册中心服务来统一管理所有的其他业务服务,该服务需要具备以下核心能力:

  1. 服务注册(实例启动后向服务注册中心上报)
  2. 服务保持(通过心跳机制,确保服务的感知)
  3. 服务下线(心跳超时,不可用时随时可更新)

如何设计一个注册中心

【架构师的想法】:这三个需求很好实现啊,我们这样做:

  1. 写一个注册中心服务,对外提供一个供客户端调用的服务注册http接口。【我的想法:这个接口需要哪些信息呢?】
  2. 客户端服务实例启动时,直接调用http接口,上报实例信息到注册中心服务,这里不要指望客户端配合你来做,最好能够提供一个Jar包,客户端只要引入就自动帮他搞定。【我的想法:这个jar包怎么做呢?引入的服务启动时怎么才能被调用?什么时候应该被调用?请求的注册中心http接口地址怎么获取?】
  3. 注册中心服务接口被调用后,将信息存起来,再提供一个获取接口。【我的想法:存是用本地缓存还是Redis还是DB?存Redis的话还得依赖Redis,似乎不太好。如果是本地缓存,重启后如何确保不丢失?】
  4. 注册中心与服务实例之间维持心跳,确保服务实例可用。【我的想法:心跳机制如何设计?长链接?http? 如果是长链接,是否要考虑最大连接数?如果是http,那么接口是在客户端还是服务端,或者说是客户端发送心跳包,还是注册中心服务主动请求客户端?心跳间隔要设置多久合适?如果间隔时间内,客户端实例不可用,如何处理?
  5. 客户端服务A需要调用客户端服务B,那么客户端服务A可以从注册中心获取客户端服务B的可用实例。【我的想法:获取后服务端A如何保存客户端B的服务实例?如果存储,如何确保实时性?如果不存储,每次请求都要去注册中心获取么?
  6. 心跳超时或者服务主动下线时,将实例从可用服务列表移除。【我的想法:如果客户端B服务网络不稳定,被下线,如何告知依赖B的其他客户端服务?另外客户端B如何才能在恢复时上线?
  7. 考虑到注册中心高可用,还需要提供集群模式。【我的想法:使用哪种集群模式?主从?主备?哨兵?数据一致性如何保证,不可用如何感知?如何选举?另外客户端如何配置注册中心服务集群地址?配置其中的单节点,还是集群所有节点?如果是单节点,那单个节点挂了怎么用其他节点?

嗯,暂时就这些吧,你先按这个思路做个概设吧,到时候拿出来我们评一下。【我的想法:emm....】


emm... 想一想挺简单,实现起来可真难。算了,先搞一版吧。

我的设计

思考and选择

事项选择备注
注册信息注册信息应该包含,服务名称、IP、端口。除上述必要信息外,还可增加一些性能指标,便于权重负载处理
客户端jar包实现基于SpringBoot自动装配原理,以及Spring扩展点,实现客户端启动注册1. 注册中心地址需要客户端在引入jar后进行配置。
2. 还需要考虑调用依赖服务时,如何与RPC配合使用。
注册中心数据存储存储在本地缓存中如果存在Redis中,还需要增加Redis依赖,Redis没了怎么办?所以尽可能的少依赖,保持自身独立性。
本地缓存,单机环境,注册中心重启数据没了,等客户端重新连接就行(客户端本地也应该存储依赖服务,不能完全依赖注册中心)。

集群环境,可以从其他服务同步数据。
心跳机制心跳应该由客户端发起,选用长链接,选择一个较为合理的时间: 10s/30s服务端主动与客户端交互,会增加服务端的压力(调用其他服务1W次,与自身被调用1W次,损耗肯定是不同的,主动的肯定更累)

当然把主动权交出去也有一定的坏处,中央集权与分封制各有各的好处,也是一个取舍问题。这里主要还是追求高可用,降低服务压力。

心跳时间,其实最好能够结合业务自身情况灵活配置,以便能够提高服务的实时准确性。(假设注册中心服务认为,连续5次没有心跳即可认为服务不在线,那么10s一次心跳,中间的误差就是50s。即使衡量标准改为时间维度:30s没有心跳,那也可能存在30s的误差)这么设计复杂度肯定有,而且需要客户端,服务端双向支持,收益就结结合自身业务特点来看吧,大部分业务都不追求100%的成功率,能够容忍超时,重试。
客户端保存依赖服务客户端是需要保存依赖的服务实例的。否则每次都从主从中心获取,那么多的客户端,那么多的请求,注册中心要被压爆了实例信息的保存还是考虑本地缓存,保持自身独立。而且这么做,如果发现依赖服务的某个实例不可用,就可以直接在本地标记,做熔断处理。
客户端离线通知及恢复订阅关系的维护能够实现通知,心跳机制能够完成恢复
集群模式去中心,相互注册了解有限,先采用Eureka设计思路

设计思路

交互流程

  1. 服务注册中心启动。维护注册客户端实例信息,以及客户端服务之间订阅关系。
  2. 客户端启动时,连接注册中心,注册服务、订阅服务并保心跳。
  3. 注册中心服务感知到服务变更时,将变更情况推送给服务订阅者。

image.png

注册中心服务能力

image.png

客户端能力

image.png


【架构师】:emm... 挺好,挺好。不过我们还是不要重复造轮子了,这些功能 Nacos 已经实现了,你去研究一下吧。

【我】:......

Nacos注册中心是如何设计的

先来一张官网上,高大上的图。

image.png

这个图简单说了一下主要的设计思想。看上去很简洁。

  • 外部客户端的服务注册发现(生产消费模型)
  • 内部的Nacos Server 集群
    • 统一的对外API
    • 配置中心、注册中心
    • 核心层
    • 一致性协议。(为什么要单独把这个拉出来?)
  • Nacos的控制

image.png

这个就比较细了,不过我们这次主要是针对一些核心功能的研究。主要是为了了解其底层设计思想,同时结合一开始自己对注册中心的理解,比对验证与开源框架的差距。

服务注册

服务注册按照我们的理解:服务端开放一个接口,客户端在启动后,直接调用接口完成服务注册。注册实例信息存储在服务端本地缓存。


Nacos 基本也是按照这个思路设计的。不过 Nacos 2.0 采用更高效的GRPC协议,客户端与服务端建立长链接。

客户端发送注册请求

先看一下大概得流程:

image.png

  • Spring 启动
  • Spring 完成 Nacos 的自动装配。
  • Nacos 响应 Spring 启动完成事件 WebServerInitializedEvernt 以此为入口进行注册

有几个比较关键的点需要看一下:

客户端与服务端长链接的建立

先看入口

com.alibaba.cloud.nacos.registry.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);
      }
   }
}

初始化 NamingService

这里还是用了双重检查锁获取单例。PS:双重检查锁获取的对象一定要使用 volatile 否则出现指令重排序问题会造成NPE。

com.alibaba.cloud.nacos.NacosServiceManager#buildNamingService


private volatile NamingService namingService;

private NamingService buildNamingService(Properties properties) {
   if (Objects.isNull(namingService)) {
      synchronized (NacosServiceManager.class) {
         if (Objects.isNull(namingService)) {
            namingService = createNewNamingService(properties);
         }
      }
   }
   return namingService;
}

不太清楚为什么这里要使用反射机制获取。

com.alibaba.nacos.api.naming.NamingFactory#createNamingService(java.util.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);
        return (NamingService) constructor.newInstance(properties);
    } catch (Throwable e) {
        throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
    }
}

准备建立连接:

com.alibaba.nacos.client.naming.NacosNamingService#init]

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

private void init(Properties properties) throws NacosException {
    final NacosClientProperties nacosClientProperties = NacosClientProperties.PROTOTYPE.derive(properties);
    
    ValidatorUtils.checkInitParam(nacosClientProperties);
    this.namespace = InitUtils.initNamespaceForNaming(nacosClientProperties);
    InitUtils.initSerialization();
    InitUtils.initWebRootContext(nacosClientProperties);
    initLogName(nacosClientProperties);

    this.notifierEventScope = UUID.randomUUID().toString();
    this.changeNotifier = new InstancesChangeNotifier(this.notifierEventScope);
    NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384);
    NotifyCenter.registerSubscriber(changeNotifier);
    this.serviceInfoHolder = new ServiceInfoHolder(namespace, this.notifierEventScope, nacosClientProperties);
    // 初始化客户端代理委派类(这个类负责 new GRPC、HTTP)
    this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, nacosClientProperties, changeNotifier);
}

直接看GRPC实现:com.alibaba.nacos.client.naming.remote.gprc.NamingGrpcClientProxy#start

public NamingGrpcClientProxy(String namespaceId, SecurityProxy securityProxy, ServerListFactory serverListFactory,
        NacosClientProperties properties, ServiceInfoHolder serviceInfoHolder) throws NacosException {
    super(securityProxy);
    this.namespaceId = namespaceId;
    this.uuid = UUID.randomUUID().toString();
    this.requestTimeout = Long.parseLong(properties.getProperty(CommonParams.NAMING_REQUEST_TIMEOUT, "-1"));
    Map<String, String> labels = new HashMap<>();
    labels.put(RemoteConstants.LABEL_SOURCE, RemoteConstants.LABEL_SOURCE_SDK);
    labels.put(RemoteConstants.LABEL_MODULE, RemoteConstants.LABEL_MODULE_NAMING);
    // 创建 RPC 连接客户端
    this.rpcClient = RpcClientFactory.createClient(uuid, ConnectionType.GRPC, labels);
    this.redoService = new NamingGrpcRedoService(this);
    // 启动
    start(serverListFactory, serviceInfoHolder);
}

private void start(ServerListFactory serverListFactory, ServiceInfoHolder serviceInfoHolder) throws NacosException {
    rpcClient.serverListFactory(serverListFactory);
    rpcClient.registerConnectionListener(redoService);
    rpcClient.registerServerRequestHandler(new NamingPushRequestHandler(serviceInfoHolder));
    // 开始建立连接
    rpcClient.start();
    NotifyCenter.registerSubscriber(this);
}

建立连接:

com.alibaba.nacos.common.remote.client.RpcClient#start

public final void start() throws NacosException {
    
    // 先设置状态为 STARTING
    boolean success = rpcClientStatus.compareAndSet(RpcClientStatus.INITIALIZED, RpcClientStatus.STARTING);
    if (!success) {
        return;
    }
    // 初始化一个线程池 用于处理 连接事件、连接超时
    clientEventExecutor = new ScheduledThreadPoolExecutor(2, r -> {
        Thread t = new Thread(r);
        t.setName("com.alibaba.nacos.client.remote.worker");
        t.setDaemon(true);
        return t;
    });
    
    // connection event consumer.
    clientEventExecutor.submit(() -> {
        while (!clientEventExecutor.isTerminated() && !clientEventExecutor.isShutdown()) {
            ConnectionEvent take;
            try {
                take = eventLinkedBlockingQueue.take();
                if (take.isConnected()) {
                    notifyConnected();
                } else if (take.isDisConnected()) {
                    notifyDisConnected();
                }
            } catch (Throwable e) {
                // Do nothing
            }
        }
    });
    
    clientEventExecutor.submit(() -> {
        while (true) {
            try {
                if (isShutdown()) {
                    break;
                }
                ReconnectContext reconnectContext = reconnectionSignal
                        .poll(rpcClientConfig.connectionKeepAlive(), TimeUnit.MILLISECONDS);
                if (reconnectContext == null) {
                    // check alive time.
                    if (System.currentTimeMillis() - lastActiveTimeStamp >= rpcClientConfig.connectionKeepAlive()) {
                        boolean isHealthy = healthCheck();
                        if (!isHealthy) {
                            if (currentConnection == null) {
                                continue;
                            }
                            LoggerUtils.printIfInfoEnabled(LOGGER,
                                    "[{}] Server healthy check fail, currentConnection = {}",
                                    rpcClientConfig.name(), currentConnection.getConnectionId());
                            
                            RpcClientStatus rpcClientStatus = RpcClient.this.rpcClientStatus.get();
                            if (RpcClientStatus.SHUTDOWN.equals(rpcClientStatus)) {
                                break;
                            }
                            
                            boolean statusFLowSuccess = RpcClient.this.rpcClientStatus
                                    .compareAndSet(rpcClientStatus, RpcClientStatus.UNHEALTHY);
                            if (statusFLowSuccess) {
                                reconnectContext = new ReconnectContext(null, false);
                            } else {
                                continue;
                            }
                            
                        } else {
                            lastActiveTimeStamp = System.currentTimeMillis();
                            continue;
                        }
                    } else {
                        continue;
                    }
                    
                }
                
                if (reconnectContext.serverInfo != null) {
                    // clear recommend server if server is not in server list.
                    boolean serverExist = false;
                    for (String server : getServerListFactory().getServerList()) {
                        ServerInfo serverInfo = resolveServerInfo(server);
                        if (serverInfo.getServerIp().equals(reconnectContext.serverInfo.getServerIp())) {
                            serverExist = true;
                            reconnectContext.serverInfo.serverPort = serverInfo.serverPort;
                            break;
                        }
                    }
                    if (!serverExist) {
                        LoggerUtils.printIfInfoEnabled(LOGGER,
                                "[{}] Recommend server is not in server list, ignore recommend server {}",
                                rpcClientConfig.name(), reconnectContext.serverInfo.getAddress());
                        
                        reconnectContext.serverInfo = null;
                        
                    }
                }
                reconnect(reconnectContext.serverInfo, reconnectContext.onRequestFail);
            } catch (Throwable throwable) {
                // Do nothing
            }
        }
    });
    
    // connect to server, try to connect to server sync retryTimes times, async starting if failed.
    Connection connectToServer = null;
    // 这里有点多此一举 执行到这里肯定是 STARTING
    rpcClientStatus.set(RpcClientStatus.STARTING);
    
    int startUpRetryTimes = rpcClientConfig.retryTimes();
    while (startUpRetryTimes > 0 && connectToServer == null) {
        try {
            startUpRetryTimes--;
            ServerInfo serverInfo = nextRpcServer();
            
            LoggerUtils.printIfInfoEnabled(LOGGER, "[{}] Try to connect to server on start up, server: {}",
                    rpcClientConfig.name(), serverInfo);
            // 建立连接
            connectToServer = connectToServer(serverInfo);
        } catch (Throwable e) {
            LoggerUtils.printIfWarnEnabled(LOGGER,
                    "[{}] Fail to connect to server on start up, error message = {}, start up retry times left: {}",
                    rpcClientConfig.name(), e.getMessage(), startUpRetryTimes, e);
        }
        
    }
    
    if (connectToServer != null) {
        LoggerUtils
                .printIfInfoEnabled(LOGGER, "[{}] Success to connect to server [{}] on start up, connectionId = {}",
                        rpcClientConfig.name(), connectToServer.serverInfo.getAddress(),
                        connectToServer.getConnectionId());
        this.currentConnection = connectToServer;
         // 连接建立成功
        rpcClientStatus.set(RpcClientStatus.RUNNING);
        eventLinkedBlockingQueue.offer(new ConnectionEvent(ConnectionEvent.CONNECTED));
    } else {
        switchServerAsync();
    }
    
    registerServerRequestHandler(new ConnectResetRequestHandler());
    
    // register client detection request.
    registerServerRequestHandler(request -> {
        if (request instanceof ClientDetectionRequest) {
            return new ClientDetectionResponse();
        }
        
        return null;
    });
    
}

Nacos注册实例信息包含什么

private Instance getNacosInstanceFromRegistration(Registration registration) {
   Instance instance = new Instance();
   instance.setIp(registration.getHost());
   instance.setPort(registration.getPort());
   instance.setWeight(nacosDiscoveryProperties.getWeight());
   instance.setClusterName(nacosDiscoveryProperties.getClusterName());
   instance.setEnabled(nacosDiscoveryProperties.isInstanceEnabled());
   instance.setMetadata(registration.getMetadata());
   instance.setEphemeral(nacosDiscoveryProperties.isEphemeral());
   return instance;
}

使用委派模式选择使用 GRPC / HTTP

public class NamingClientProxyDelegate implements NamingClientProxy {

    private NamingClientProxy getExecuteClientProxy(Instance instance) {
        return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
    }
}

image.png

会在客户端本地缓存注册实例

@Override
public void registerService(String serviceName, String groupName, Instance instance) 
throws NacosException {
    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}",
    namespaceId, serviceName,instance);
    // 缓存实例信息
    redoService.cacheInstanceForRedo(serviceName, groupName, instance);
    // 调用注册
    doRegisterService(serviceName, groupName, instance);
}

注册完成后会修改本地缓存中实例状态为 已注册。

public void doBatchRegisterService(String serviceName, String groupName, List<Instance> instances)
        throws NacosException {
    BatchInstanceRequest request = new BatchInstanceRequest(namespaceId, serviceName, groupName,
            NamingRemoteConstants.BATCH_REGISTER_INSTANCE, instances);
    requestToServer(request, BatchInstanceResponse.class);
    // 修改状态
    redoService.instanceRegistered(serviceName, groupName);
}

服务端的响应过程下一篇文章继续分析。