微服务将自己的实例注册到nacos注册中心,nacos服务端存储了注册列表,然后通过ribbon调用服务,具体是如何调用?如果nacos服务挂了,还能正常调用服务吗?调用的服务列表发生变化,调用方是如何感知变化的?带着这些问题,来探索一下服务发现的原理。

版本 2.1.1

  • Nacos Server:2.1.1
  • spring-cloud-starter-alibaba:2.1.1.RELEASE
  • spring-boot:2.1.1.RELEASE
  • spring-cloud-starter-netflix-ribbon:2.1.1.RELEASE

客户端和服务端版本号都为2.1.1

从 Ribbon 讲起

使用ribbon来调用服务,就添加ribbon依赖:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

ribbon依赖包含spring-cloud-commons依赖,而在spring-cloud-commons包中spring.factories自动配置LoadBalancerAutoConfiguration类:

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList(); @Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}

只要标注了@LoadBalanced注解的restTemplates都会添加负载均衡拦截器LoadBalancerInterceptor

使用Ribbon组件调用服务:

restTemplate.getForObject("http://service-name",String.class);

restTemplatehttp请求方法,最终会调用到doExecute方法。doExecute在发起http请求之前,会先执行LoadBalancerInterceptor负载均衡拦截器的intercept方法。 该方法调用execute方法。

而在execute方法中,主要有两个方法:

ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);

execute先通过getLoadBalancer获取ILoadBalancer实例,然后再通过getServer获取Server实例。

getLoadBalancer最终会调用RibbonServerList接口,具体调用流程:

getLoadBalancer() ->
ZoneAwareLoadBalancer ->
DynamicServerListLoadBalancer ->
restOfInit()->
updateListOfServers()->
ServerList.getUpdatedListOfServers()->

Nacos实现类NacosServerList实现了ServerList接口。

总之我们在进行微服务调用的时候,Ribbon最终会调用NacosServerList类中的getUpdatedListOfServers方法。

Nacos 获取服务

NacosServerList类的getUpdatedListOfServers方法调用了该类的getServers方法:

private List<NacosServer> getServers() {
try {
// 获取分组
String group = discoveryProperties.getGroup();
// 重点,查询实例列表
List<Instance> instances = discoveryProperties.namingServiceInstance()
.selectInstances(serviceId, group, true);
return instancesToServerList(instances);
}
catch (Exception e) {
throw new IllegalStateException(
"Can not get service instances from nacos, serviceId=" + serviceId,
e);
}
}

重点看NacosNamingService类的selectInstances方法,会调用以下selectInstances三个重载方法:

@Override
public List<Instance> selectInstances(String serviceName, String groupName, boolean healthy) throws NacosException {
return selectInstances(serviceName, groupName, healthy, true);
} @Override
public List<Instance> selectInstances(String serviceName, String groupName, boolean healthy, boolean subscribe) throws NacosException {
return selectInstances(serviceName, groupName, new ArrayList<String>(), healthy, subscribe);
} @Override
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, ","));
} else {
serviceInfo = hostReactor.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","));
}
return selectInstances(serviceInfo, healthy);
}

最后一个selectInstances方法里面的hostReactor.getServiceInfo方法是获取服务的核心方法:

public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {

    NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
String key = ServiceInfo.getKey(serviceName, clusters);
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
// 先在本地缓存查询
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
// 查询不到
if (null == serviceObj) {
serviceObj = new ServiceInfo(serviceName, clusters); serviceInfoMap.put(serviceObj.getKey(), serviceObj);
updatingMap.put(serviceName, new Object());
// 请求Nacos Server实例,并更新服务实例
updateServiceNow(serviceName, clusters);
updatingMap.remove(serviceName); } else if (updatingMap.containsKey(serviceName)) { if (UPDATE_HOLD_INTERVAL > 0) {
// hold a moment waiting for update finish
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
}
}
}
}
// 定时更新本地缓存
scheduleUpdateIfAbsent(serviceName, clusters); return serviceInfoMap.get(serviceObj.getKey());
}

getServiceInfo是服务发现的核心方法,先查询serviceInfoMap集合中查询本地缓存,本地缓存查询不到就请求Nacos Server实例,并更新本地缓存。

请求Nacos Server实例,实际就是发送http请求Nacos Server

public void updateServiceNow(String serviceName, String clusters) {
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
// 调用 Nacos Server 查询服务
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUDPPort(), false);
// 结果不为空,更新缓存
if (StringUtils.isNotEmpty(result)) {
processServiceJSON(result);
}
} catch (Exception e) {
NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
} finally {
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
} //向 Nacos Server发起 HTTP 列表查询
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)); return reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/list", params, HttpMethod.GET);
}

queryList方法主要封装号请求参数,然后向Nacos Server服务端发送http请求。

当服务端实例发生改变时,Nacos Server会推送最新的实例给服务端。

服务发现是先获取本地缓存,如果没有本地缓存,就请求Nacos Server服务端获取数据,如果Nacos Server挂了,也不会影响服务的调用。

总结

  • Ribbon

    • 项目启动时,会创建一个负载均衡拦截器。
    • Ribbon发起服务请求开始,最终会调用到拦截器的拦截方法。
    • 拦截方法又调用ServerList获取实例接口,而NacosServerList实现获取实例列表。
  • Nacos调用服务
    • NacosServerList实现了获取服务实例列表。
    • NacosServerListselectInstances方法最终调用了hostReactor.getServiceInfo方法
    • getServiceInfo方法先从serviceInfoMap集合中获取本地缓存,如果本地缓存找不到,就请求Nacos Server获取服务实例,并更新本地缓存。
    • 获取服务之后,定时更新本地缓存。

参考

Nacos服务发现原理分析的更多相关文章

  1. Nacos服务注册原理分析

    在分布式服务中,原来的单体服务会被拆分成一个个微服务,服务注册实例到注册中心,服务消费者通过注册中心获取实例列表,直接请求调用服务. 服务是如何注册到注册中心,服务如果挂了,服务是如何检测?带着这些问 ...

  2. SpringCloud使用Nacos服务发现实现远程调用

    本文使用SpringCloud结合Nacos服务发现,Feign远程调用做一个简单的Demo. 1 Nacos 关于Nacos之前写了两篇文章关于SpringBoot对它的使用,感兴趣可以查看一下. ...

  3. SpringBoot使用Nacos服务发现

    本文介绍SpringBoot应用使用Nacos服务发现. 上一篇文章介绍了SpringBoot使用Nacos做配置中心,本文介绍SpringBoot使用Nacos做服务发现. 1.Eureka闭源 相 ...

  4. Docker Kubernetes 服务发现原理详解

    Docker Kubernetes  服务发现原理详解 服务发现支持Service环境变量和DNS两种模式: 一.环境变量 (默认) 当一个Pod运行到Node,kubelet会为每个容器添加一组环境 ...

  5. Nacos服务发现

    基础配置初始化 NacosDiscoveryClientConfiguration NacosDiscoveryProperties 初始化Nacos基础配置信息的bean,主要指yaml中配置Nac ...

  6. 配置中心Nacos(服务发现)

    服务演变之路 单体应用架构 在刚开始的时候,企业的用户量.数据量规模都⽐较⼩,项⽬所有的功能模块都放在⼀个⼯程中编码.编译.打包并且部署在⼀个Tomcat容器中的架构模式就是单体应用架构,这样的架构既 ...

  7. SpringCloud之Nacos服务发现(十七)

    一 Nacos简介 Nacos是以服务为主要服务对象的中间件,Nacos支持所有主流的服务发现.配置和管理. Nacos主要提供以下四大功能: 服务发现与服务健康检查 Nacos使服务更容易注册自己并 ...

  8. Spring Cloud Alibaba基础教程:Nacos服务发现与配置管理

    随着微服务概念的流行,越来越多的公司采用`Spring Cloud`全家桶构建微服务系统,实现业务的快速迭代.`Spring Cloud`提供了快速构建分布式微服务常用组件,包括`Spring Clo ...

  9. Spring Cloud Alibaba学习笔记(2) - Nacos服务发现

    1.什么是Nacos Nacos的官网对这一问题进行了详细的介绍,通俗的来说: Nacos是一个服务发现组件,同时也是一个配置服务器,它解决了两个问题: 1.服务A如何发现服务B 2.管理微服务的配置 ...

  10. Alibaba Nacos 服务发现组件集群部署

    前面学习了单机模式下的启动,生产环境中部署nacos肯定是使用集群模式cluster保证高可用. 官方文档的集群部署推荐使用VIP+域名模式,把所有服务列表放到一个vip下面,然后挂到一个域名下面. ...

随机推荐

  1. warning: ‘setAxisX‘ is deprecated

    解决 将 chart->setAxisX(valueAxisX,lineSeries); 改为: chart->addSeries(lineSeries); chart->creat ...

  2. linux mint 归档管理器报错Extraction not performd

    解决办法 后缀名的问题,后缀名与文件的真正类型不符合,至于到底是上面压缩类型,那只能靠尝试了,比如我这个是rar, 实际是zip,很坑,网上也没有这个问题的描述 其他 感觉 linux 对于文件类型方 ...

  3. Py2neo:一种快速导入百万数据到Neo4j的方式

    Py2neo:一种快速导入百万数据到Neo4j的方式 Py2neo是一个可以和Neo4j图数据库进行交互的python包.虽然py2neo操作简单方便,但是当节点和关系达几十上百万时,直接创建和导入节 ...

  4. [数据结构]哈希拉链法化解冲突+ASL计算

    看这篇就好(自用,别骂我) 摘自博客园 存疑:学校考试空节点算不算查找次数???

  5. 高性能 Jsonpath 框架,Snack3 3.2.50 发布

    Snack3,一个高性能的 JsonPath 框架 借鉴了 Javascript 所有变量由 var 申明,及 Xml dom 一切都是 Node 的设计.其下一切数据都以ONode表示,ONode也 ...

  6. 微软出品自动化神器【Playwright+Java】系列(六) 之 字符输入、单元素键盘事件操作、上传文件、聚焦、拖拽、悬浮操作

    前言: 今天一早起床,就一直太阳穴疼,吃了四片去痛片已经无效,真的是疼的直恶心. 如果说学习或者写文章,能够或者头疼的话,那我想说,我还能坚持一会..... 很久没更新这系列的文章了,那么我们将Pla ...

  7. java中的复合赋值运算符

    本文主要阐明复合赋值运算符 即 i = i+ 1.2 ==> i += 1.2: int i = 1; i += 1.2; System.out.println(i); // i == 2 注意 ...

  8. 开发一个MyBatis通用Mapper的轮子

    一.前言 程序猿为什么如此执着于造轮子?MyBatis-Plus如此强大的工具流行这么多年了,我为啥还在重复造这样的轮子? 1.公司的技术规范不允许使用MyBatis-Plus,咱也不知道什么原因: ...

  9. JavaScript:显式转换数据类型:如何转换为数值、字符串和布尔值类型?

    JS的运算符以及某些内置函数,会自动进行数据类型的转换,方便计算,即隐式转换数据类型: 但是很多时候,我们希望可以手动控制数据类型的转换,即显示转换数据类型: 转换为字符串 String()函数 使用 ...

  10. 【c#】分享一个简易的基于时间轮调度的延迟任务实现

    在很多.net开发体系中开发者在面对调度作业需求的时候一般会选择三方开源成熟的作业调度框架来满足业务需求,比如Hangfire.Quartz.NET这样的框架.但是有些时候可能我们只是需要一个简易的延 ...