nacos 新鲜源码解读

简单上手


源码环境搭建
导入源码模块 nacos-distribution 自带 sql 脚本
启动 nacos-console 模块
添加启动参数
-Dnacos.standalone=true
-DuseAddressServer=false
-Ddb.num=1
-Ddb.url=jdbc:mysql://localhost:3306/nacos_config
-Ddb.user=root
-Ddb.password=root
-Dnacos.home=D:\Java\nacos

至此 nacos 服务端已经启动

spring-boot 整合 nacos

引入依赖


	<dependencies>
		<!-- spring-mvc -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		
		<!-- spring-properties语法提示 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		
		<!-- nacos动态配置依赖 -->
		<dependency>
			<groupId>com.alibaba.boot</groupId>
			<artifactId>nacos-config-spring-boot-starter</artifactId>
			<version>0.2.7</version>
		</dependency>
		
		<!-- nacos服务发现依赖 -->
		<dependency>
			<groupId>com.alibaba.boot</groupId>
			<artifactId>nacos-discovery-spring-boot-starter</artifactId>
			<version>0.2.7</version>
		</dependency>
	</dependencies>

自身服务自动注册


import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.alibaba.nacos.api.annotation.NacosInjected;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;

/**
 * @author nacos
 */
@SpringBootApplication
public class SpringBootNacosProvider {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootNacosProvider.class, args);
	}

}

配置文件 bootstrap.properties


注意 这里 一定要把配置写在 bootstrap.properties 不要写在 application.properties
nacos 会读取不到配置

spring.application.name=aming-spring-boot-nacos-provider
server.port=18080

# nacos动态配置相关
nacos.config.server-addr=127.0.0.1:8848
nacos.config.auto-refresh=true
nacos.config.data-id=dev.properties
nacos.config.group=aming-dev
nacos.config.namespace=9f4224d9-f001-48ef-a12b-6ec2ad4be807

# nacos服务注册与发现相关配置
nacos.discovery.auto-register=true
nacos.discovery.server-addr=127.0.0.1:8848

# 这里的 data-id group namespace 要在控制台手动添加 再配置到自身上

至此客户端已完成注册到 nacos 服务端 可以在控制台看到注册的服务

spring-cloud 整合 nacos

新建 provider 模块
引入依赖


	<dependencies>
		<!-- spring-mvc -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!-- nacos 动态配置依赖 -->
		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
		</dependency>

		<!-- nacos 服务注册发现依赖 -->
		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
		</dependency>

		<!-- openfeign 远程调用与负责均衡 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>
	</dependencies>

	<!-- 统一管理依赖版本 注意依赖在 阿里私服 https://maven.aliyun.com/mvn/search -->
	<!-- Spring Cloud Version, Spring Cloud Alibaba Version, Spring Boot Version, 
		各个版本选择 详见连接 https://linjinp.blog.csdn.net/article/details/99292705?utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-2.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-2.control -->
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>com.alibaba.cloud</groupId>
				<artifactId>spring-cloud-alibaba-dependencies</artifactId>
				<version>2.1.2.RELEASE</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Greenwich.SR6</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

自身服务自动注册


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudNacosProvider {

	public static void main(String[] args) {
		SpringApplication.run(SpringCloudNacosProvider.class, args);
	}

	/**
	 * 这个是是用来测试 消费端能否连接服务端的
	 */
	@RestController
	public class TestController {
		@RequestMapping(value = "/queryUserById/{id}", method = RequestMethod.GET)
		public String queryUserById(@PathVariable String id) {
			return "远程调用成功 参数: " + id;
		}
	}

}

provider 配置文件 bootstrap.properties


注意 这里 一定要把配置写在 bootstrap.properties 不要写在 application.properties
nacos 会读取不到配置

spring.application.name=aming-spring-cloud-nacos-provider
server.port=18090

spring.application.name=aming-spring-cloud-nacos-consumer
server.port=18091

# nacos 动态配置相关
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
#spring.cloud.nacos.config.namespace=9f4224d9-f001-48ef-a12b-6ec2ad4be807
#spring.cloud.nacos.config.group=aming-dev
# dataId = ${spring.cloud.nacos.config.prefix} + '-' + ${spring.profiles.active} + . + ${spring.cloud.nacos.config.file-extension}.
# 指定配置的后缀,支持 properties、yaml、yml,默认为 properties
spring.cloud.nacos.config.prefix=application
spring.profiles.active=redis,mysql
spring.cloud.nacos.config.file-extension=properties

# nacos 服务注册发现相关
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.namespace=9f4224d9-f001-48ef-a12b-6ec2ad4be807
#spring.cloud.nacos.discovery.group=aming-dev

# 禁止 nacos 配置同步和服务注册
#spring.cloud.nacos.config.enabled=false
#spring.cloud.nacos.discovery.enabled=false

经过上面配置 客户端会从 nacos 注册中心拉取以下配置文件
application.properties
application-redis.properties
application-mysql.properties

新建 consumer 模块


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
public class SpringCloudNacosConsumer {

	public static void main(String[] args) {
		SpringApplication.run(SpringCloudNacosConsumer.class, args);
	}

	@LoadBalanced
	@Bean
	public RestTemplate restTemplate() {
		return new RestTemplate();

	}

	/**
	 * 这个是是用来测试 能否连接provider端的
	 */
	@RestController
	public class TestController {

		@Autowired
		private RestTemplate restTemplate;

		@RequestMapping(value = "/queryUserById/{id}", method = RequestMethod.GET)
		public String echo(@PathVariable String id) {
			return restTemplate.getForObject("http://aming-spring-cloud-nacos-provider/queryUserById/" + id,
					String.class);
		}
	}

}

consumer 配置文件


consumer 和 provider 模块大致相同 改下 配置文件的 spring.application.name 即可

消费者调用提供者是通过 服务名 调用的 那是如何识别这个服务的
是通过 openfeign 整合的 ribbon 实现的

nacos 服务端 存储数据结构


Map<namespace,Map<group::serviceName, Service对象>>

内存结构是一个 双Map 很好理解 通过 namespace 去隔离不同的开发和生产环境 
里面对应的 value 是 组 + 服务名称(aming-dev@@aming-spring-cloud-nacos-provider)  最里面是 Service对象
Service对象 里面 有 Map<ClusterName, Cluster对象> 
Cluster对象 里面 有   
    private Set<Instance> persistentInstances = new HashSet<>();
    private Set<Instance> ephemeralInstances = new HashSet<>();
重点来了 Cluster对象 这里面才是存储 服务实例 的地方

在这里插入图片描述

nacos 客户端 注册流程


client 构建心跳任务每 5秒 向注册中心发送心跳
发送心跳通过 JDK 提供的 ScheduledExecutorService 实现的
然后通过 rest接口 向 服务端 注册自己
这里利用的 java 原生 HttpURLConnection 请求的
默认自己是 临时节点 存内存 
如果是 持久化节点 存磁盘

nacos 服务端 接收注册流程


1. 首先创建 空Service
把当前 client 放进 Service 里 
2. 然后开始对当前 client 进行心跳检查 大于15秒 
没有回应的 client 状态设置false 
大于30秒 没有回应的 client 剔除
3. 开始存放实例 instance
判断 临时节点 还是持久节点
临时 ap distro协议 
持久 cp raft协议
默认 ap distro 协议 去 put 实例
这里用 异步方式 放到队列
因为 服务端启动时 用 @PostConstruct 开启 ScheduledThreadPoolExecutor 线程池
设置为了 守护线程
死循环 从 阻塞队列取 放进来的实例
取出来干什么 CopyOnWrite 旧实例 注册新实例 
防止并发访问修改数据异常
4. 同步集群信息 去给集群的服务端发送 rest 请求集群同步

在这里插入图片描述

nacos 服务端 集群同步思考


client 注册流程最后会 给从节点发 异步请求 存盘逻辑
这里利用信号量控制 
如果有3个节点。取3/2 + 1 = 2 
于是开始构造 CountDownLatch(2)遍历所有节点 
如果是 leader 直接 CountDownLatch.countDown()
此时还差一个 countDown
也就是说剩下两个从节点只要一个同步成功就返回
如果此时 leader 挂掉呢 CountDownLatch.await() 加上最长等待时间即可 
超时 狠心抛出异常
如何集群信息同步 批量同步一次 1000 减少每次同步开销
集群某节点挂掉 重试机制 代码有待优化
可以隔不同时间段重试 超过十次记录日志人工干预

nacos 核心功能点总结


服务注册 
	client 会发送 rest 请求 向 service 端注册自己
	service 会把 client 的 ip端口 实例信息存内存 双层Map
客户端发送心跳
	服务注册后 client 会维护定时心跳向 service 端 防止被剔除 默认 5s 频率 
服务同步
	service 端集群互相同步服务实例信息
服务发现
	client 发送 rest 请求 获取注册清单 缓存在 client 本地 
	同时 client 本地开启定时任务拉取最新注册信息并更新本地缓存
服务健康检查
	service 端 开启定时任务检查注册实例健康情况
	超过 15秒 没有响应的 client 设置健康状态 false
	超过 30秒 直接剔除

nacos AP CP 模式


默认 AP 模式 保证高可用
使用 distro协议
此时集群nacos没有 leader 概念 不关心是否互相同步 能保证可用就行了

持久 CP 模式 保证各节点一致性
使用 简化版 raft 协议实现
有 leader 和 flower 概念
只有 leader 才能写数据
过半同步成功返回就算成功

客户端如何服务发现调用
客户端本地缓存服务注册表信息 客户端定时拉取最新服务注册表 服务端还通过 udp 推送新注册的服务给客户端

nacos 如何自动注册到 spring-boot


首先利用 spring-boot 自动装配技术
获取 nacos-discovery-spring-boot-autoconfigure 包下的 spring.factories文件
根据 NacosDiscoveryAutoConfiguration 里面实例化了 NacosDiscoveryAutoRegister
NacosServiceRegistry 刚好实现了 ApplicationListener<WebServerInitializedEvent>
spring 容器启动web服务器完毕后会 执行 onApplicationEvent(WebServerInitializedEvent) 方法 
在里面动手脚去注册服务实例

nacos 如何自动注册到 spring-cloud


首先利用 spring-boot 自动装配技术
获取 spring-cloud-starter-alibaba-nacos-discovery 包下的 spring.factories文件
根据 NacosServiceRegistryAutoConfiguration 里面实例化了 NacosServiceRegistry
NacosServiceRegistry 里面的 register() 里面有自动注册逻辑 
这里通过 spring-cloud 去调用 自己不进行调用
client 启动时通过 spring-cloud-commons 包的 ServiceRegistry 扩展点 
这个接口是 sping-cloud 提供的统一外部服务注册接口
NacosServiceRegistry 实现了这个接口
最终调用 register() 的注册方法 

nacos 集群选举


nacos CP 模式下 集群选举流程
简化版的 raft协议 实现
这个协议中 主要是对 term 这个概念的操作
term 英文本意是 任期 一届 
nacos 集群选举中 主要操作的是这个 term
每个 节点都有自己的 term
每个 节点初次注册时 自己的 term + 100
public AtomicLong term = new AtomicLong(0L);
抢 leader 间隔时间
leaderDueMs = 15 + RandomUtils.nextLong(0, 5); 单位是 秒

1. 节点初始化 建立定时任务 
   开始执行选举方法 每 500ms 执行一次
2. 发起投票准备
   发起投票的倒计时初始化是 0-15000ms 之间的随机值
   任务每500ms执行一次,每次进来 -500ms
   如果倒计时减到 0 则开始为自己拉票 否则直接 return
3. 为自己拉票
   重置leader选举时间 leaderDueMs 
   重置心跳发送时间
   先为自己投一票 term + 1
   候选人给自己
   通过 httpClient 异步 给 nacos 集群的 其他节点 发送选票信息
   其他节点 怎么处理别人发过来的投票信息呢 ???
   如果本机节点 term 的值大于远程节点 term 的值,那么本机节点选票为自己
   选自己作为Leader节点
   反之则将选票信息设置为远程节点信息
   
4. 集中处理返回的 http 投票请求
   找出得票最多的节点的信息以及该节点的得票数
   判断得票数是否超过了一半的 nacos 集群节点数量
   如果没有超过,直接返回之前的 leader
   如果超过了则把票数最多的 节点设置为 leader
   

nacos 服务发现和 ribbon 调用


简明
ribbon 负载均衡器如何确定调用节点  (By 拦截器)
nacos 如何插入 ribbon
ribbon 制定了获取服务列表的规范 继承  
AbstractServiceList
具体客户端如何获取服务列表呢 
优先从缓存取 缓存没有 从注册中心拉取 
客户端开启定时器定时拉取
本地缓存和拉取的最新服务进行合并


迎合 ribbon 接入 实现发现服务列表功能
spring-cloud-starter-alibaba-nacos-discovery-2.1.2.RELEASE 包下 
spring.factories 里面有 RibbonNacosAutoConfiguration 配置类
该配置类有个重要的 引入 
@RibbonClients(defaultConfiguration = NacosRibbonClientConfiguration.class)
配置类 实例化一个  ServerList 实例 
该实例类为 NacosServerList  实现了 com.netflix.loadbalancer.ServerList 接口

首先在 spring-cloud-commons 包的 spring.factories 中定义了 LoadBalancerAutoConfiguration 配置类
LoadBalancerAutoConfiguration 中获取了 @LoadBalanced 的 RestTemplate
然后对被 @LoadBalanced 标注的  RestTemplate 注入拦截器 LoadBalancerInterceptor
RestTemplate 的 execute方法中,先获取ILoadBalancer实例
在调用 ILoadBalancer 实例的 chooseServer 方法获取到具体 Server 实例
会根据 serviceId 创建一个子容器,让这个子容器去加载 @RibbonClient@RibbonClients 引入的配置类
这就与上面对上了

nacos 动态配置实现原理

说起动态配置 先说说静态配置
Environment 是 spring 提供的环境配置信息
Environment 中所有外部化配置 针对不同类型的配置都会有与之对应的 PropertySource
那 NacosClient 在启动的时候 会从配置中心获取配置加载到 Environment 中

spring 启动中 我们如何自定义 PropertySource 配置源 让 spring 调用呢
先说答案是 实现 ApplicationContextInitializer 接口

springApplication 启动中 run 方法里面 的 prepareContext 方法 
它会回调所有实现了 ApplicationContextInitializer 的实例来做一些初始化工作
然后是  执行 AbstractApplication 的 refresh() 方法
首先获取 Environment 的 继承接口 ConfigurableEnvironment
遍历 获取到的 所有 实现 PropertySourceLocator 接口的 配置源
回调 locate(Environment) 方法 
nacos 的 NacosPropertySourceLocator 也 实现了 PropertySourceLocator 
nacos 的 locate 方法 中 具体逻辑是 分别调用三个方法来加载配置
Nacos配置加载顺序:共享配置 < 扩展配置 > 自身配置(后面优先级高)

客户端发起长轮训请求,
服务端收到请求以后,先比较服务端缓存中的数据是否相同,如果不通,则直接返回
如果相同,则通过 schedule 延迟29.5s之后再执行比较
为了保证当服务端在29.5s之内发生数据变化能够及时通知给客户端,服务端采用事件订阅的方式来监听服务端本地数据变化的事件,一旦收到事件,则触发DataChangeTask的通知,并且遍历 allStubs 队列中的 ClientLongPolling ,把结果写回到客户端,就完成了一次数据的推送
如果 DataChangeTask 任务完成了数据的 “推送” 之后,ClientLongPolling 中的调度任务又开始执行了怎么办呢?很简单,只要在进行 “推送” 操作之前,先将原来等待执行的调度任务取消掉就可以了,这样就防止了推送操作写完响应数据之后,调度任务又去写响应数据,这时肯定会报错的。所以,在 ClientLongPolling 方法中,最开始的一个步骤就是删除订阅事件
  所以总的来说,Nacos采用推 + 拉 的形式,来解决最开始关于长轮训时间间隔的问题。当然,30s这个时间是可以设置的,而之所以定30s,应该是一个经验值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值