前面,我熟悉了service注册基本原理。service已经注册到NacosServer了,接下来服务消费端要调用service,就需要从NacosServer获取server及其实例信息,然后基于Ribbon负载均衡从所有service实例中选择一个,进行服务接口调用。所以本章我们看看service发现(获取service实例)的基本原理。
获取service实例并调用
我们先自己思考下,如果是我们自己要实现这个获取service实例信息,我们该怎样去实现呢???简单的分析是这样的流程:
- 先从本地缓存获取service实例
- 如果本地有这个service实例,则直接返回
- 如果本地没有service实例,则调用NacosServer接口获取
- 然后缓存实例到本地,下次从本地获取实例
如上图所示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服务实例的整个基本流程,就算结束了。当然还有很多细节问题,我们后面再来看看。