Nacos源码2:Nacos服务发现基本原理

405 阅读10分钟

前面,我熟悉了service注册基本原理。service已经注册到NacosServer了,接下来服务消费端要调用service,就需要从NacosServer获取server及其实例信息,然后基于Ribbon负载均衡从所有service实例中选择一个,进行服务接口调用。所以本章我们看看service发现(获取service实例)的基本原理。

获取service实例并调用

我们先自己思考下,如果是我们自己要实现这个获取service实例信息,我们该怎样去实现呢???简单的分析是这样的流程:

  1. 先从本地缓存获取service实例
  2. 如果本地有这个service实例,则直接返回
  3. 如果本地没有service实例,则调用NacosServer接口获取
  4. 然后缓存实例到本地,下次从本地获取实例

如上图所示service实例获取。当然这个只是一个简单流程,实际Nacos服务发现流程稍显复杂。由于我们日常开发中,一般service获取都是搭配Ribbon使用,所以我们就以整合了Ribbon之后的service获取流程为例说明。如下图所示为消费端获取service实例流程。

注意:Ribbon负载均衡算法选择实例,不在本章范围之内,后期我们再详细说明。本章只说明service实例获取的基本原理

消费端获取service实例(详细流程)

NacosServer服务端返回service实例流程,如下图。

NacosServer服务端返回service实例流程

一、消费端的流程

1.1 消费端调用service接口的入口

Nacos官方文档给出与SpringCloud整合时会与Ribbon集成,消费者使用restTemplate调用service提供者的接口。

/**
 * 服务消费者
 * @author xg
 */
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceConsumerApplication {
    public static void main( String[] args ) {
        SpringApplication.run(ServiceConsumerApplication.class);
    }

    /**
     * 使用@LoadBalanced注解,开启调用服务负载均衡。开启@LoadBalanced与Ribbon集成
     * @return
     */
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @RestController
    public class TestController {

        private final RestTemplate restTemplate;

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

        /**
         * 调用目标服务接口
         * @param str
         * @return
         */
        @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
        public String echo(@PathVariable String str) {
            // 调用service提供者的接口
            return restTemplate.getForObject("http://provider/echo/" + str, String.class);
        }
    }
}

执行RestTemplate#doExecute:

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
   public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
      ...
      return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
   }
}

封装为request对象,然后执行

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
   protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
			@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
      ClientHttpRequest request = createRequest(url, method);
      ...
      // 封装为request执行
      response = request.execute();
   }
}

1.2 会先执行拦截器逻辑

InterceptingClientHttpRequest#executeInternal:

protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
		InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
		return requestExecution.execute(this, bufferedOutput);
	}

执行InterceptingClientHttpRequest#execute方法中,执行拦截器LoadBalancerInterceptor#intercept方法。

@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
   if (this.iterator.hasNext()) {
	ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
	return nextInterceptor.intercept(request, body, this);

在LoadBalancerInterceptor拦截器中,执行RibbonLoadBalancerClient#execute方法。

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

   public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		...
		return this.loadBalancer.execute(serviceName,
				this.requestFactory.createRequest(request, body, execution));
}
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
			throws IOException {
                // 获取loadBalancer
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		Server server = getServer(loadBalancer, hint);

1.3 ZoneAwareLoadBalancer实例化

getLoadBalancer方法:

protected ILoadBalancer getLoadBalancer(String serviceId) {
   return this.clientFactory.getLoadBalancer(serviceId);
}

SpringClientFactory#getLoadBalancer:

public ILoadBalancer getLoadBalancer(String name) {
   return getInstance(name, ILoadBalancer.class);
}
@Override
public <C> C getInstance(String name, Class<C> type) {
	C instance = super.getInstance(name, type);
	if (instance != null) {
	   return instance;
}

从SpringContext中获取ILoadBalancer这个bean

这个从SpringContext容器中获取bean属于Spring的知识范畴,我们本章重点是说明Nacos服务发现的基本原理,这里Spring的详细内容我们不再展开了。

  public <T> T getInstance(String name, Class<T> type) {
        AnnotationConfigApplicationContext context = this.getContext(name);
        return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0 ? context.getBean(type) : null;
    }

首次获取LoadBalancer这个bean实例时,会调用RibbonClientConfiguration这个Configuration配置类中声明的ribbonLoadBalancer方法。

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
			ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
			IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
		...
		return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,serverListFilter, serverListUpdater);
}

实例化ZoneAwareLoadBalancer

public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule,
                                 IPing ping, ServerList<T> serverList, ServerListFilter<T> filter,
                                 ServerListUpdater serverListUpdater) {
        super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
    }

1.4 DynamicServerListLoadBalancer获取并更新service实例

实例化ZoneAwareLoadBalancer实例化时,会调用父类DynamicServerListLoadBalancer执行updateListOfServers。然后父类DynamicServerListLoadBalancer实例化时调用restOfInit方法。

  public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                         ServerList<T> serverList, ServerListFilter<T> filter,
        ...
        restOfInit(clientConfig);

DynamicServerListLoadBalancer#restOfInit,更新service实例列表。

void restOfInit(IClientConfig clientConfig) {
   ...
   updateListOfServers();

执行DynamicServerListLoadBalancer#updateListOfServers方法,

public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
            servers = serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);

            if (filter != null) {
                servers = filter.getFilteredListOfServers(servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
            }
        }
        updateAllServerList(servers);

重点是serverListImpl.getUpdatedListOfServers()这个逻辑。这个serverListImpl就是NacosServerList。

1.5 NacosNamingService获取service实例

NacosServerList执行getServers

public List<NacosServer> getUpdatedListOfServers() {
   return getServers();
}

NacosNamingService获取service实例

private List<NacosServer> getServers() {
   ...
   String group = discoveryProperties.getGroup();
   // 调用NacosNamingService#selectInstances
   List<Instance> instances = discoveryProperties.namingServiceInstance().selectInstances(serviceId, group, true);

1.6 HostReactor获取service实例

上面的selectInstances方法,会调用NacosNamingService#selectInstances方法,

public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,
            boolean subscribe) throws NacosException {
        
        ServiceInfo serviceInfo;
        if (subscribe) {
            serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
                    StringUtils.join(clusters, ","));
        }
        // 筛选处健康的实例
        return selectInstances(serviceInfo, healthy);

此时调用HostReactor#getServiceInfo方法,我们进入此方法。

我们看到在getServiceInfo0方法中,从HostReactor的serviceInfoMap属性中获取service信息,如果存在,则返回service实例,这个service信息中包含了serviceName、以及对应的service实例。


public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
   String key = ServiceInfo.getKey(serviceName, clusters);
   // 从serviceInfoMap获取service信息
   ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);

   // 如果 从serviceInfoMap获取service信息为null
   if (null == serviceObj) {
      serviceObj = new ServiceInfo(serviceName, clusters);
      // 缓存service信息到map,但是此时service对应的host为空
      serviceInfoMap.put(serviceObj.getKey(), serviceObj);
      // 调用NacosServer接口,获取service实例,然后缓存到serviceInfoMap中
      updateServiceNow(serviceName, clusters);
   }
   ...
   // 返回service信息
   return serviceInfoMap.get(serviceObj.getKey());
}

private ServiceInfo getServiceInfo0(String serviceName, String clusters) {
        String key = ServiceInfo.getKey(serviceName, clusters);
        return serviceInfoMap.get(key);
}

如果从serviceInfoMap获取service信息为null,则执行HostReactor#updateServiceNow:

  private void updateServiceNow(String serviceName, String clusters) {
            updateService(serviceName, clusters);

HostReactor#updateService:

public void updateService(String serviceName, String clusters) throws NacosException {
        ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
        String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
        if (StringUtils.isNotEmpty(result)) {
             processServiceJson(result);

1.7 调用NacosServer接口获取服务实例

执行NamingProxy#queryList方法,调用NacosServer接口,获取服务实例。

注意:NacosServer的处理逻辑是怎样的,后面会说明。

public class NamingProxy implements Closeable {
   public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly)
            throws NacosException {
        
        final Map<String, String> params = new HashMap<String, String>(8);
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, serviceName);
        params.put("clusters", clusters);
        params.put("udpPort", String.valueOf(udpPort));
        params.put("clientIP", NetUtils.localIP());
        params.put("healthyOnly", String.valueOf(healthyOnly));
        // 调用NacosServer接口,获取服务实例。
        return reqApi(UtilAndComs.nacosUrlBase + "/instance/list", params, HttpMethod.GET);
}

HostReactor#processServiceJson,缓存service实例信息到serviceInfoMap

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

缓存从NacosServer获取的service实例

1.8 NacosNamingService筛选健康的实例

获取到了service实例之后,NacosNamingService#selectInstances,从中筛选健康的实例。

private List<Instance> selectInstances(ServiceInfo serviceInfo, boolean healthy) {
        List<Instance> list;
        if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
            return new ArrayList<Instance>();
        }
        
        Iterator<Instance> iterator = list.iterator();
        while (iterator.hasNext()) {
            Instance instance = iterator.next();
            // 循环遍历实例列表,筛选处健康的实例
            if (healthy != instance.isHealthy() || !instance.isEnabled() || instance.getWeight() <= 0) {
                iterator.remove();
            }
        }
        
        return list;

筛选完健康的service实例之后,返回到NacosServiceList类,执行instancesToServerList方法

	private List<NacosServer> getServers() {
		try {
			String group = discoveryProperties.getGroup();
			List<Instance> instances = discoveryProperties.namingServiceInstance()
					.selectInstances(serviceId, group, true);
			return instancesToServerList(instances);
		}

把service实例包装为NacosServer对象,放List集合之中。

private List<NacosServer> instancesToServerList(List<Instance> instances) {
		List<NacosServer> result = new ArrayList<>();
		if (CollectionUtils.isEmpty(instances)) {
			return result;
		}
		for (Instance instance : instances) {
			result.add(new NacosServer(instance));
		}

		return result;
	}

代码依次往上返回,回到DynamicServerListLoadBalancer类。之前调用就是从这个类往下传递的,现在获取到了service实例之后,返回到此类

   public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
            servers = serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);

            if (filter != null) {
                servers = filter.getFilteredListOfServers(servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
            }
        }
        // 重点看这里的更新service实例
        updateAllServerList(servers);

重点看这段代码updateAllServerList(servers);下文会详细介绍。

1.9 BaseLoadBalancer缓存最新的service实例列表

获取到健康的service实例之后,代码回到updateAllServerList(servers),来更新service实例列表。重点看 setServersList(ls)这段代码。

 protected void updateAllServerList(List<T> ls) {
        if (serverListUpdateInProgress.compareAndSet(false, true)) {
            try {
                for (T s : ls) {
                    s.setAlive(true); // set so that clients can start using these
                                      // servers right away instead
                                      // of having to wait out the ping cycle.
                }
                setServersList(ls);

父类BaseLoadBalancer缓存service实例列表。

@Override
public void setServersList(List lsrv) {
        super.setServersList(lsrv);
public void setServersList(List lsrv) {
   ...
    for (Object server : lsrv) {
                if (server == null) {
                    continue;
                }

                if (server instanceof String) {
                    server = new Server((String) server);
                }

                if (server instanceof Server) {
                    // 缓存service实例
                    allServers.add((Server) server);
                ...
     }
     // allServerList指向缓存的服务实例
     allServerList = allServers;

1.10 ZoneAwareLoadBalancer缓存到SpringContext容器

同时此时Spring的机制会把ZoneAwareLoadBalancer缓存到SpringContext容器中,

public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
			throws IOException {
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		Server server = getServer(loadBalancer, hint);

下次执行getLoadBalancer方法时,直接从SpringContext中获取ZoneAwareLoadBalancer。

1.11 从ZoneAwareLoadBalancer的缓存获取service实例

然后执行getServer方法,由RibbonLoadBalancerClient执行getServer方法,以此来选择service实例。

	protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
		...
		return loadBalancer.chooseServer(hint != null ? hint : "default");

从缓存的服务实例中,选择1个服务实例。

具体的选择逻辑,是Ribbon的知识范畴,不在本章讨论的范围。本章重点是熟悉Nacos服务发现的流程。

 public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);

这段代码lb.getAllServers(),是在获取服务的所有实例。

lb就是ZoneAwareLoadBalancer,由于ZoneAwareLoadBalancer中的allServerList集合缓存了service实例,因此可以直接返回。

从allServerList获取服务的所有实例。allServerList就是前面缓存service实例的ArrayList集合。


public class BaseLoadBalancer extends AbstractLoadBalancer implements
        PrimeConnections.PrimeConnectionListener, IClientConfigAware {
   ...
   public List<Server> getAllServers() {
        return Collections.unmodifiableList(allServerList);
   }
}

二、NacosServer服务端的流程

前面章节说到,消费端会调用NacosServer端的/instance/list接口来获取service实例。这里NacosServer服务端接收到消费端的接口请求,然后进行下面的处理流程。

2.1 NacosServer的list接口接收客户端请求

@GetMapping("/list")
public Object list(HttpServletRequest request) throws Exception {

   ...
   return getInstanceOperator().listInstance(namespaceId, serviceName, subscriber, clusters, healthyOnly);
}

InstanceOperatorClientImpl#listInstance:

@org.springframework.stereotype.Service
public class InstanceOperatorClientImpl implements InstanceOperator {

   @Override
   public ServiceInfo listInstance(String namespaceId, String serviceName, Subscriber subscriber, String cluster,
            boolean healthOnly) {
      // 生成一个service对象
      Service service = getService(namespaceId, serviceName, true);
      ...
      // 获取service实例
      ServiceInfo serviceInfo = serviceStorage.getData(service);
   }
}

2.2 从serviceDataIndexes缓存中获取service实例

ServiceStorage#getData, 我们看到是从serviceDataIndexes返回service实例。serviceDataIndexes就是用来缓存service实例的容器

public ServiceInfo getData(Service service) {
        return serviceDataIndexes.containsKey(service) ? serviceDataIndexes.get(service) : getPushData(service);
    }

判断如果serviceDataIndexes中存在service实例,则从serviceDataIndexes这个map中返回service实例。

如果不存在service实例则,执行下面的getPushData。

2.3 ServiceStorage#getAllInstancesFromIndex

如果不存在service实例则,执行下面的getPushData

 public ServiceInfo getPushData(Service service) {
        ServiceInfo result = emptyServiceInfo(service);
        ...
        // 获取service注册时缓存的service信息
        Service singleton = ServiceManager.getInstance().getSingleton(service);
        // 获取service实例
        result.setHosts(getAllInstancesFromIndex(singleton));
        serviceDataIndexes.put(singleton, result);
        return result;
    }

获取service注册时缓存的service信息,如下。

ServiceStorage#getAllInstancesFromIndex:

public class ServiceStorage {  
   private List<Instance> getAllInstancesFromIndex(Service service) {
        Set<Instance> result = new HashSet<>();
        Set<String> clusters = new HashSet<>();
        for (String each : serviceIndexesManager.getAllClientsRegisteredService(service)) {
        
        }
}

2.3.1 获取service的host主机集合

ClientServiceIndexesManager#getAllClientsRegisteredService,从publisherIndexes中获取注册的service对应的clientId,这个clientId也就是服务提供者的host/port集合

@Component
public class ClientServiceIndexesManager extends SmartSubscriber { 
    public Collection<String> getAllClientsRegisteredService(Service service) {
        return publisherIndexes.containsKey(service) ? publisherIndexes.get(service) : new ConcurrentHashSet<>();
    }
}

注意:那么publisherIndexes中的service实例host/port信息是什么时候缓存的???后期的文章我们再说明。此时这里,我们认为publisherIndexes中有缓存service对应的host/port信息。

这里从Map缓存中返回service服务的host/port:

2.3.2 获取Client对象

然后,代码继续往下运行到这里:

for (String each : serviceIndexesManager.getAllClientsRegisteredService(service)) {
            Optional<InstancePublishInfo> instancePublishInfo = getInstanceInfo(each, service);

ServiceStorage#getInstanceInfo,获取service服务host/port对应的Client对象

注意:这个host主机的Client对象是何时缓存的,我们后期文章再说明。这里我们暂且认为已经缓存了host对应的Client对象。

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

EphemeralIpPortClientManager#getClient:

@Override
public Client getClient(String clientId) {
     return clients.get(clientId);
}

获取到的Client对象

2.3.3 从Client对象中返回缓存的service实例

Client的抽象父类AbstractClient#getInstancePublishInfo,来获取service实例。

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

Client对象的publiser属性中缓存了service对应的实例,因此从publiser返回缓存的service实例。

注意:publiser是何时缓存service实例信息的?这个后期文章中我们再详细看看。

2.4 ServiceStorage缓存实例并返回

获取到缓存的service实例之后,代码依次向上返回,然回到这里getAllInstancesFromIndex:把service实例包装一下并返回。

private List<Instance> getAllInstancesFromIndex(Service service) {
  ...  
  for (String each : serviceIndexesManager.getAllClientsRegisteredService(service)) {
     // 获取缓存的service实例   
     Optional<InstancePublishInfo> instancePublishInfo = getInstanceInfo(each, service);
     ...
     // 把service实例包装一下,放到set集合中
     Instance instance = parseInstance(service, instancePublishInfo.get());
     result.add(instance);
     ...
     // 返回service实例
     return new LinkedList<>(result);
}

代码继续向上返回到ServiceStorage#getPushData方法,缓存service实例serviceDataIndexes这个map中并返回。

public ServiceInfo getPushData(Service service) {
        ServiceInfo result = emptyServiceInfo(service);
        if (!ServiceManager.getInstance().containSingleton(service)) {
            return result;
        }
        Service singleton = ServiceManager.getInstance().getSingleton(service);
        // 获得service实例
        result.setHosts(getAllInstancesFromIndex(singleton));
        // 缓存service实例到serviceDataIndexes,并返回
        serviceDataIndexes.put(singleton, result);
        return result;
}

到这里,获取service服务实例的整个基本流程,就算结束了。当然还有很多细节问题,我们后面再来看看。