Nacos源码5:Nacos缓存服务实例的基本流程

253 阅读6分钟

1. 背景描述

首先,说明下整个流程图,如下:

整个流程图

我们在前面的Nacos系列文章中,了解到获取服务的实例信息的代码如下:

public abstract class AbstractClient implements Client {
   
    protected final ConcurrentHashMap<Service, InstancePublishInfo> publishers = new ConcurrentHashMap<>(16, 0.75f, 1);

   @Override
   public InstancePublishInfo getInstancePublishInfo(Service service) {
      return publishers.get(service);
   }
}

我们看到是从publishers缓存中获取服务实例信息,这个publishers缓存就是一个ConcurrentHashMap。服务实例节点信息缓存到这个map中。那么缓存service实例到publishers中的流程到底是怎样的???又是什么时候缓存的???我们下面分析下。

2. NacosServer提供服务注册接口

首先,启动服务提供者时,会发起注册服务请求,也就是调用NacosServer服务端的注册接口(这个之前的Nacos文章有详细说明),NacosServer服务端的注册接口如下:

@PostMapping
public String register(HttpServletRequest request) throws Exception {
   ...
   // 从Request请求对象中获取serviceName和instance实例信息
   final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
        NamingUtils.checkServiceNameFormat(serviceName);
   final Instance instance = HttpRequestInstanceBuilder.newBuilder()
                .setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();
   // 注册服务实例节点信息
   getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
  • NacosServer服务端提供接口,接收服务提供者发送的注册请求

  • 从Request请求中获取serviceName,获取实例信息

  • 然后执行registerInstance注册方法(这个注册方法实际上就是缓存服务实例信息,下文会详细说明)

3. 缓存服务提供者实例对应的Client对象

@Override
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
        ...
        // 为服务提供者实例,维护一个Client对象
        String clientId = IpPortBasedClient.getClientId(instance.toInetAddr(), ephemeral);
        createIpPortClientIfAbsent(clientId);
        Service service = getService(namespaceId, serviceName, ephemeral);
        // 检查Client连接 && 缓存服务实例节点信息
        clientOperationService.registerInstance(service, instance, clientId);
}

为服务提供者实例,缓存对应的Client对象(这个Client对象封装了服务实例的ip、port信息)

 private void createIpPortClientIfAbsent(String clientId) {
        if (!clientManager.contains(clientId)) {
            ...
            clientManager.clientConnected(clientId, clientAttributes);
        }
    }
 @Override
    public boolean clientConnected(String clientId, ClientAttributes attributes) {
        return clientConnected(clientFactory.newClient(clientId, attributes));
    }
    
    @Override
    public boolean clientConnected(final Client client) {
        // 缓存Client对象(这个client对象封装了服务实例的ip、port信息)到Map中
        clients.computeIfAbsent(client.getClientId(), s -> {
            Loggers.SRV_LOG.info("Client connection {} connect", client.getClientId());
            IpPortBasedClient ipPortBasedClient = (IpPortBasedClient) client;
            ipPortBasedClient.init();
            return ipPortBasedClient;
        });
        return true;
    }

此clientManager.clientConnected的意义在于:

  • 为服务提供者实例维护(缓存)了一个与之对应的Client对象(其中封装了ip、port信息)

  • 这个Client对象代表着与服务提供者实例之间的一个connect连接(用于后面判断connect连接是否断开提供依据)

4. 缓存服务实例信息到publisher

4.1 NacosServer服务端获取Client对象,并检查连接

@Override
public void registerInstance(Service service, Instance instance, String clientId) throws NacosException {
    ...
    // 获取NacosServer端为服务提供者实例维护(缓存)的Client对象
    Client client = clientManager.getClient(clientId); 
    // 检查客户端connection连接是否断开
    checkClientIsLegal(client, clientId);
    // 缓存服务实例节点信息
    client.addServiceInstance(singleton, instanceInfo);
    ...
  • 获取NacosServer端为服务提供者实例维护(缓存)的Client对象

【特别注意】:每个注册服务的提供者实例,进行服务注册时,在Nacos服务端都会为其维护一个Client对象。

这个Client对象的作用:

  1. 用于判断提供者实例与Nacos服务的连接是正常还是断开
  2. Client对象用于维护缓存服务提供者实例信息(ip、port等信息),这个下文会介绍

后文中Client对象都是指在NacosServer服务端为注册服务的提供者实例缓存维护的Client对象。

  • 检查客户端connection连接是否断开
    private void checkClientIsLegal(Client client, String clientId) {
        if (client == null) {
            Loggers.SRV_LOG.warn("Client connection {} already disconnect", clientId);
            throw new NacosRuntimeException(NacosException.CLIENT_DISCONNECT,
                    String.format("Client [%s] connection already disconnect, can't register ephemeral instance.",
                            clientId));
        }

这个检查很关键,主要通过Client对象是否存在,来判断提供者实例与NacosServer端的连接是正常还是断开。

4.2 由Client对象维护(缓存)服务提供者实例信息(主要是ip、port等信息)

也就是缓存服务实例信息(ip、port信息)到Client对象的publisher中,这里的publiser就是一个ConcurrentHashMap中。

  public boolean addServiceInstance(Service service, InstancePublishInfo instancePublishInfo) {
       ...
       publishers.put(service, instancePublishInfo);

缓存服务->实例信息到publishers这个map中。这里的service就是服务名称等信息,如下:

service就是服务名称等信息

也就是咱们的服务提供端的应用名称,如下图所示:

服务提供端的应用名称

实例信息其实就是服务提供者实例的ip、port等信息,如下:

实例信息即服务提供者实例的ip、port信息

所以,最终就是把服务->服务提供者实例的ip、port等信息缓存到Client对象的publisher这个map中。

然而这个publisher属性又是属于Client对象的。这个Client对象就是每个提供者实例发送注册请求到NacosServer时,NacosServer服务端为其维护建立的Client对象。

所以,实际上就是每个注册服务的提供者实例的Client对象缓存了服务实例信息。如下图:

提供者实例的Client对象缓存了服务实例信息

5. 从Client对象中获取服务实例

因为在前文说明过,服务实例就是由每个服务提供者实例对应的Client对象缓存到publiser属性中,所以这里消费者获取服务实例肯定最终是要从Client对象中获取。基本流程如下图:

从Client对象中获取服务实例

5.1 消费者获取(发现)服务

我们启动服务消费者,调用这个provider服务的接口,调用之前会先去获取(发现)provider服务实例。

/**
  * 调用服务
  * @param str
  * @return
  */
@RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
public String echo(@PathVariable String str) {
     // 注意这里的provider是服务提供者配置文件中配置的应用名称
     return restTemplate.getForObject("http://provider/echo/" + str, String.class);
}

5.2 调用NacosServer的list接口获取服务

如果消费者本地没有缓存服务实例信息,则会去调用NacosServer的list接口获取服务实例(这个流程在之前的Nacos系列文章中有详细说明)。

@GetMapping("/list")
public Object list(HttpServletRequest request) throws Exception {
   ...
   return getInstanceOperator().listInstance(namespaceId, serviceName, subscriber, clusters, healthyOnly);
}

5.3 获取服务对应的Client对象

首先,由service对象获取注册服务的clientId信息,这个clientId就是注册服务的实例的ip、port信息。

   private List<Instance> getAllInstancesFromIndex(Service service) {
        ...
        // 获取服务对应的clientId
        for (String each : serviceIndexesManager.getAllClientsRegisteredService(service)) {
            Optional<InstancePublishInfo> instancePublishInfo = getInstanceInfo(each, service);

然后,由clientId来获取Nacos服务端维护的Client对象(这个Client对象维护(缓存)了服务实例),因为NacosServer为每个服务实例维护了一个Client对象(这个前面章节已经有过说明)

private Optional<InstancePublishInfo> getInstanceInfo(String clientId, Service service) {
        // 由clientId获取对应的Client对象
        Client client = clientManager.getClient(clientId);
        ...
        return Optional.ofNullable(client.getInstancePublishInfo(service));
    }

从Client对象获取服务实例

5.4 最后从这个Client对象中获取服务实例信息

因为从上文中,可知Client对象维护了注册服务的提供者实例自己的实例信息,因此,可以从这个Client对象中获取服务实例信息。

  @Override
    public InstancePublishInfo getInstancePublishInfo(Service service) {
        return publishers.get(service);
    }

根据serviceName服务名称等信息,获取服务实例信息(ip、port等信息),如下:

获取服务实例信息(ip、port等信息)