流程分析
- 服务实例在启动时注册到服务注册表,并在关闭时注销
- 服务消费者查询服务注册表,获得可用实例
- 服务注册中心需要调用服务实例的健康检查API来验证它是否能够处理请求
源码解读:
1、客户端注册:
在nacos-discovery的META-INF/spring.factories中包含自动装配的配置信息如下:
很多个自动配置类被加载了,其中跟服务注册有关的就是NacosServiceRegistryAutoConfiguration这个类,在NacosServiceRegistryAutoConfiguration这个类中,包含一个跟自动注册有关的Bean:
在初始化时,其父类AbstractAutoServiceRegistration也被初始化了。
AbstractAutoServiceRegistration类中,实现了ApplicationListener接口,监听Spring容器启动过程中的事件,在监听到WebServerInitializedEvent(web服务初始化完成)的事件后,执行了bind 方法。
类的关系图如下
bind方法
@Deprecated
public void bind(WebServerInitializedEvent event)
// 获取 ApplicationContext
WebServerApplicationContext context = event.getApplicationContext();
// 判断服务的 namespace,一般都是null
if(!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
// 记录当前 web 服务的端口
this.port.compareAndSet(0, event.getWebServer().getPort());
// 启动当前服务注册流程
this.start();
}
}
start方法
if(!this.isEnabled()) {
if(logger.isDebugEnabled()) {
logger.debug("Discovery Lifecycle disabled. Not starting");
}
} else {
// 当前服务处于未运行状态时,才进行初始化
if(!this.running.get()) {
// 发布服务开始注册的事件
this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
// 开始注册
this.register();
if(this.shouldRegisterManagement()) {
this.registerManagement();
}
// 发布注册完成事件
this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
// 服务状态设置为运行状态,基于AtomicBoolean
this.running.compareAndSet(false, true);
}
}
register 注册方法(重要)
public void register(Registration registration) {
// 判断serviceId是否为空,也就是spring.application.name不能为空
if(StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
} else {
// 获取Nacos的命名服,其实就是注册中心服务
NamingService namingService = this.namingService();
// 获取 serviceId 和 Group
String serviceId = registration.getServiceId();
String group = this.nacosDiscoveryProperties.getGroup();
// 封装服务实例基本信息,如 cluster-name、是否为临时实例、权重、IP、端口等
Instance instance = this.getNacosInstanceFromRegistration(registration);
try {
// 开始注册
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", new Object[]{group, serviceId, instance.getIp(), Integer.valueOf(instance.getPort())});
} catch (Exception var7) {
log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var7});
ReflectionUtils.rethrowRuntimeException(var7);
}
}
}
registerInstance方法,最终调用的默认实现是NacosNamingService,提供了服务注册、订阅等功能
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
// 检查超时参数是否异常。心跳超时时间(默认15秒)必须大于心跳周期(默认5秒)
NamingUtils.checkInstanceIsLegal(instance);
this.clientProxy.registerService(serviceName, groupName, instance);
}
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});
// 拼接得到新的服务名,格式为:groupName@@serviceId
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
// 判断是否为临时实例,默认为 true。
if(instance.isEphemeral()) {
// 如果是临时实例,需要定时向 Nacos 服务发送心跳(后面会讲到)
BeatInfo params = this.beatReactor.buildBeatInfo(groupedServiceName, instance);
this.beatReactor.addBeatInfo(groupedServiceName, params);
}
// 完成服务注册
HashMap params1 = new HashMap(32);
// namespace_id:环境
params1.put("namespaceId", this.namespaceId);
// service_name:服务名称
params1.put("serviceName", groupedServiceName);
// group_name:组名称
params1.put("groupName", groupName);
// cluster_name:集群名称
params1.put("clusterName", instance.getClusterName());
// ip: 当前实例的ip地址
params1.put("ip", instance.getIp());
// port: 当前实例的端口
params1.put("port", String.valueOf(instance.getPort()));
params1.put("weight", String.valueOf(instance.getWeight()));
params1.put("enable", String.valueOf(instance.isEnabled()));
params1.put("healthy", String.valueOf(instance.isHealthy()));
params1.put("ephemeral", String.valueOf(instance.isEphemeral()));
params1.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
// 通过POST请求将上述参数,发送到 /nacos/v1/ns/instance
this.reqApi(UtilAndComs.nacosUrlInstance, params1, "POST");
}
至此,客户端注册示例结束,大致流程如下
2、服务端
对于nacos服务端,对外提供的服务接口请求地址为nacos/v1/ns/instance,实现代码在nacos-naming模块下的InstanceController类中,进入InstanceController类,可以看到register方法,就是服务注册的方法了
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
// 尝试获取namespaceId
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
// 尝试获取serviceName,其格式为 group_name@@service_name
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
// 解析出实例信息,封装为Instance对象
final Instance instance = parseInstance(request);
// 注册实例
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
ServiceManager就是Nacos中管理服务、实例信息的核心API
/**
* Register an instance to a service in AP mode.
*
* <p>This method creates service or cluster silently if they don't exist.
*
* @param namespaceId id of namespace
* @param serviceName service name
* @param instance instance to register
* @throws Exception any error occurred in the process
*/
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
// 创建一个空的service(如果是第一次来注册实例,要先创建一个空service出来,放入注册表)
// 此时不包含实例信息
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
// 拿到创建好的service
Service service = getService(namespaceId, serviceName);
// 拿不到则抛异常
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
// 添加要注册的实例到service中
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
addInstance方法
/**
* Add instance to service.
*
* @paramnamespaceId namespace
* @paramserviceName service name
* @paramephemeral whether instance is ephemeral
* @paramips instances
* @throwsNacosException nacos exception
*/
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
// 监听服务列表用到的key,服务唯一标识,例如:com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@order-service
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
// 获取服务
Service service = getService(namespaceId, serviceName);
// 同步锁,避免并发修改的安全问题
synchronized (service) {
// 1)获取要更新的实例列表
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
// 2)封装实例列表到Instances对象
Instances instances = new Instances();
instances.setInstanceList(instanceList);
// 3)完成 注册表更新 以及 Nacos集群的数据同步
consistencyService.put(key, instances);
}
}
该方法中对修改服务列表的动作加锁处理,确保线程安全。而在同步代码块中,包含下面几步:
- 先获取要更新的实例列表,addIpAddresses(service, ephemeral, ips);
- 然后将更新后的数据封装到Instances对象中,后面更新注册表时使用
- 最后,调用consistencyService.put()方法完成Nacos集群的数据同步,保证集群一致性。
Nacos集群一致性
consistencyService.put(key, instances),这里的ConsistencyService接口,代表集群一致性的接口,有很多中不同实现:
临时实例的一致性实现:DistroConsistencyServiceImpl类的put方法
public void put(String key, Record value) throws NacosException {
// 先将要更新的实例信息写入本地实例列表
onPut(key, value);
// 开始集群同步
distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
globalConfig.getTaskDispatchPeriod() / 2);
}
- onPut(key, value):其中value就是Instances,要更新的服务信息。这行主要是基于线程池方式,异步的将Service信息写入注册表中(就是那个多重Map)
- distroProtocol.sync():就是通过Distro协议将数据同步给集群中的其它Nacos节点
onPut方法
public void onPut(String key, Record value) {
// 判断是否是临时实例
if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
// 封装 Instances 信息到 数据集:Datum
Datum<Instances> datum = new Datum<>();
datum.value = (Instances) value;
datum.key = key;
datum.timestamp.incrementAndGet();
// 放入DataStore
dataStore.put(key, datum);
}
if (!listeners.containsKey(key)) {
return;
}
// 放入阻塞队列,这里的 notifier维护了一个阻塞队列,并且基于线程池异步执行队列中的任务
notifier.addTask(key, DataOperation.CHANGE);
}
// DistroConsistencyServiceImpl.Notifier类的 addTask 方法:
public void addTask(String datumKey, DataOperation action) {
if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
return;
}
if (action == DataOperation.CHANGE) {
services.put(datumKey, StringUtils.EMPTY);
}
// 任务放入阻塞队列
tasks.offer(Pair.with(datumKey, action));
}
distroProtocol.sync()集群同步方法
public void sync(DistroKey distroKey, DataOperation action, long delay) {
// 遍历 Nacos 集群中除自己以外的其它节点
for (Member each : memberManager.allMembersWithoutSelf()) {
DistroKey distroKeyWithTarget = new DistroKey(distroKey.getResourceKey(), distroKey.getResourceType(),
each.getAddress());
// 定义一个Distro的同步任务
DistroDelayTask distroDelayTask = new DistroDelayTask(distroKeyWithTarget, action, delay);
// 交给线程池去执行
distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask);
if (Loggers.DISTRO.isDebugEnabled()) {
Loggers.DISTRO.debug("[DISTRO-SCHEDULE] {} to {}", distroKey, each.getAddress());
}
}
}
NacosDelayTaskExecuteEngine
,这个类维护了一个线程池,并且接收任务,执行任务,执行任务的方法为processTasks()方法:
protected void processTasks() {
Collection<Object> keys = getAllTaskKeys();
for (Object taskKey : keys) {
AbstractDelayTask task = removeTask(taskKey);
if (null == task) {
continue;
}
NacosTaskProcessor processor = getProcessor(taskKey);
if (null == processor) {
getEngineLog().error("processor not found for task, so discarded. " + task);
continue;
}
try {
// 尝试执行同步任务,如果失败会重试
if (!processor.process(task)) {
retryFailedTask(taskKey, task);
}
} catch (Throwable e) {
getEngineLog().error("Nacos task execute error : " + e.toString(), e);
retryFailedTask(taskKey, task);
}
}
}
基于Distro模式的同步是异步进行的,并且失败时会将任务重新入队并充实,因此不保证同步结果的强一致性,属于AP模式的一致性策略。
服务端流程图:
3、总结
客户端启动时会将当前服务的信息包含ip、端口号、服务名、集群名、实例类型等信息封装为一个Instance对象,然后创建一个定时任务,每隔一段时间向Nacos服务器发送PUT请求并携带相关信息。
nacos服务器端在接收到心跳请求后,会去检查当前服务列表中有没有该实例,如果没有的话将当前服务实例重新注册,注册完成后立即开启一个异步任务,更新客户端实例的最后心跳时间,如果当前实例是非健康状态则将其改为健康状态。
nacos服务器端接收到注册实例请求后,会将携带的数据封装为Instance对象,然后为这个服务实例创建一个服务Service,一个Service下可能有多个服务实例,服务在Nacos保存到ConcurrentHashMap中Map(namespace,Map(group::serviceName, Service));
Nacos存储模型,最外层通过namespace来实现环境隔离,然后是group分组,分组下就是服务,一个服务有可以分为不同的集群,集群中包含多个实例。因此其注册表结构为一个Map,类型是:
Map<String, Map<String, Service>>,
外层key是namespace_id,内层key是group+serviceName.
Service内部维护一个Map,结构是:Map<String,Cluster>,key是clusterName,值是集群信息
Cluster内部维护一个Set集合,元素是Instance类型,代表集群中的多个实例。
nacos将实例添加到对应服务列表中会根据AP和CP不同的模式,采用不同协议。
- CP模式就是基于Raft协议(通过leader节点将实例数据更新到内存和磁盘文件中,并且通过CountDownLatch实现了一个简单的raf写入数据的逻辑,必须集群半数以上节点写入成功才会给客户端返回成功)
- AP模式基于Distro协议(向任务阻塞队列添加一个本地服务实例改变任务,去更新本地服务列表,然后在遍历集群中所有节点,分别创建数据同步任务放进阻塞队异步进行集群数据同步,不保证集群节点数据同步完成即可返回)
- nacos在将服务实例更新到服务注册表中时,为了防止并发读写冲突,采用的是写时复制的思想,将原注册表数据拷贝一份,添加完成之后再替换回真正的注册。
nacos在更新完成之后,通过发布服务变化事件,将服务变动通知给客户端,采用的是UDP通信,客户端接收到UDP消息后会返回一个ACK信号,如果一定时间内服务端没有收到ACK信号,还会尝试重发,当超出重发时间后就不在重发。
客户端通过定时任务定时从服务端拉取服务数据保存在本地缓存。
服务端在发生心跳检测、服务列表变更或者健康状态改变时会触发推送事件,在推送事件中会基于UDP通信将服务列表推送到客户端,虽然通过UDP通信不能保证消息的可靠抵达,但是由于Nacos客户端会开启定时任务,每隔一段时间更新客户端缓存的服务列表,通过定时轮询更新服务列表做兜底,所以不用担心数据不会更新的情况,这样既保证了实时性,又保证了数据更新的可靠性。
4、心跳机制
服务的健康检查分为两种模式:
临时实例:
- 采用客户端心跳检测模式,心跳周期5秒
- 心跳间隔超过15秒则标记为不健康
- 心跳间隔超过30秒则从服务列表删除
永久实例:
- 采用服务端主动健康检测方式
- 周期为2000 + 5000毫秒内的随机数
- 检测异常只会标记为不健康,不会删除
nacos 目前的instance有一个ephemeral字段属性,该字段表示实例是否是临时实例还是持久化实例。如果是临时实例则不会在nacos中持久化,需要通过心跳上报,如果一段时间没有上报心跳,则会被nacos服务端删除。删除后如果又重新开始上报,则会重新实例注册。而持久化实例会被nacos服务端持久化,此时即使注册实例的进程不存在,这个实例也不会删除,只会将健康状态设置成不健康。
这里就涉及到了nacos的AP和CP模式 ,默认是AP,即nacos的client的节点注册时ephemeral=true,那么nacos集群中这个client节点就是AP,采用的是distro 协议,而ephemeral=false时就是CP采用的是raft协议实现。所以nacos可以很好的解决了业务常见的不同需求。
#false为永久实例,true表⽰临时实例开启,注册为临时实例,默认是true
spring.cloud.nacos.discovery.ephemeral=true
为什么nacos有两种心跳机制?
对于临时实例,健康检查失败,则直接删除。这种特性适合于需要应对流量突增的场景,服务可以弹性扩容,当流量过去后,服务停掉即可自动注销。
对于持久化实例,健康检查失败,会设置为不健康状态。它的优点就是可以实时的监控到实例的健康状态,便于后续的告警和扩容等一系列处理。