前言:
上一篇我们从 Nacos Client 分析了了服务注册流程,本篇我们从 Nacos Server 端来分析 Nacos 服务注册流程。
Nacos 系列文章传送门:
Nacos 配置管理模型 – 命名空间(Namespace)、配置分组(Group)和配置集ID(Data ID)
Nacos Server 端服务注册流程源码分析
上一篇我们有分析到 Nacos Client 提交注册的地址是:/nacos/v1/ns/instance,我们在 nacos-server 源码中找到该接口,也就找到了服务端的注册逻辑,它位于 naming 模块中的 controllers 包下的 InstanceController 中,源码如下:
//com.alibaba.nacos.naming.controllers.InstanceController#register
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
//获取注册服务的 namespaceId 默认值是 public
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
//获取注册服务的名称
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
//检查服务名称格式
NamingUtils.checkServiceNameFormat(serviceName);
//解析请求参数 封装服务实例对象
final Instance instance = parseInstance(request);
//注册服务
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
//com.alibaba.nacos.naming.controllers.InstanceController#parseInstance
private Instance parseInstance(HttpServletRequest request) throws Exception {
//获取注册服务的名称
String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
//获取 app 没有配置默认 DEFAULT
String app = WebUtils.optional(request, "app", "DEFAULT");
//获取注册服务的 ip port 健康状态 权重等信息封装为 instance
Instance instance = getIpAddress(request);
//设置 app
instance.setApp(app);
//设置 服务名称
instance.setServiceName(serviceName);
// Generate simple instance id first. This value would be updated according to
// INSTANCE_ID_GENERATOR.
//设置 InstanceId 格式:getIp() + "#" + getPort() + "#" + getClusterName() + "#" + getServiceName()
instance.setInstanceId(instance.generateInstanceId());
//设置当前时间为最后一次心跳时间
instance.setLastBeat(System.currentTimeMillis());
//获取元数据
String metadata = WebUtils.optional(request, "metadata", StringUtils.EMPTY);
//元数据为空判断
if (StringUtils.isNotEmpty(metadata)) {
//机械元数据后 设置元数据
instance.setMetadata(UtilsAndCommons.parseMetadata(metadata));
}
//实例格式校验
//权重范围校验 MAX_WEIGHT_VALUE = 10000.0D MIN_WEIGHT_VALUE = 0.00D;
instance.validate();
return instance;
}
register 方法会从请求对象中拿到注册的参数,例如IP、权重、健康状况等,然后封装为 instance对象,调用 ServiceManager#registerInstance 完成服务注册。
ServiceManager#registerInstance 方法源码分析
ServiceManager#registerInstance 方法会尝试从服务注册表 serviceMap 中获取到服务实例,如果没有就会创建一个 Service,并设置好属性 GroupName namespaceId serviceName,然后存储到 ServiceManager 的服务注册表 ConcurrentHashMap 中,并调用 Service#init 方法实现心跳检测(设置实例健康状态、剔除心跳超时的服务实例),使用监听机制完成数据一致性监听。
//com.alibaba.nacos.naming.core.ServiceManager#registerInstance
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
//创建空服务
//会尝试从服务注册表 serviceMap 中获取到服务实例 如果没有就会创建一个 Service 并设置好属性 GroupName namespaceId serviceName 然后存储到 ServiceManager 的一个 ConcurrentHashMap 中(服务注册表)
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
//从注册表中获取服务 先根据 namespaceId 取得到 Map<String,Service> 然后再根据 serviceName 获取 Service
Service service = getService(namespaceId, serviceName);
//为空判断
if (service == null) {
//为空抛出参数无效异常
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
//添加 instance 服务实例到注册表
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
//com.alibaba.nacos.naming.core.ServiceManager#createEmptyService
public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
createServiceIfAbsent(namespaceId, serviceName, local, null);
}
//com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
//根据 namespaceId 从 serviceMap 中获取 Map<String,Service> map 接着使用 serviceName 从 map 中获取 Service
Service service = getService(namespaceId, serviceName);
//service 为空判断
if (service == null) {
Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
//创建一个空的 Service
service = new Service();
//设置 ServiceName
service.setName(serviceName);
//设置 namespaceId
service.setNamespaceId(namespaceId);]
//设置 GroupName
service.setGroupName(NamingUtils.getGroupName(serviceName));
// now validate the service. if failed, exception will be thrown
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
//集群为空判断
if (cluster != null) {
//设置到集群中
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
//service 校验
service.validate();
//put serviceMap
putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
}
//com.alibaba.nacos.naming.core.ServiceManager#putServiceAndInit
private void putServiceAndInit(Service service) throws NacosException {
//保存 service
putService(service);
//再次获取 service
service = getService(service.getNamespaceId(), service.getName());
//初始化 service
service.init();
//一致性监听
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}
//com.alibaba.nacos.naming.core.ServiceManager#putService
public void putService(Service service) {
//根据 NamespaceId 判断 serviceMap 中是否有 service
if (!serviceMap.containsKey(service.getNamespaceId())) {
//不包含 sync 保证线程安全
synchronized (putServiceLock) {
//再次检查 service 是否存储 double check
if (!serviceMap.containsKey(service.getNamespaceId())) {
//不存在 NamespaceId 为空 value 为一个新的 map
serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
}
}
}
//根据 NamespaceId 获取到 Map CAS 把 service 存入到 serviceMap
serviceMap.get(service.getNamespaceId()).putIfAbsent(service.getName(), service);
}
Service#init 方法源码分析
Service#init 方法把 Service 封装到 ClientBeatCheckTask 对象中,ClientBeatCheckTask 是一个Runnable 线程对象,然后使用定时任务5s执行一次健康检查, ClientBeatCheckTask 的作用是检查并更新实例的状态,如果实例心跳超时,则将超时实例删除。
//com.alibaba.nacos.naming.core.Service#init
public void init() {
//健康检查
//clientBeatCheckTask 是一个Runnable 它持有 service 作用是检查并更新实例的状态 如果已过期 则删除
HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
//设置 service
entry.getValue().setService(this);
//初始化集群
entry.getValue().init();
}
}
//com.alibaba.nacos.naming.healthcheck.HealthCheckReactor#scheduleCheck(com.alibaba.nacos.naming.healthcheck.ClientBeatCheckTask)
public static void scheduleCheck(ClientBeatCheckTask task) {
//定时检查服务的状态 5 秒一次
futureMap.computeIfAbsent(task.taskKey(),
k -> GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, TimeUnit.MILLISECONDS));
}
ClientBeatCheckTask #run 方法源码分析
ClientBeatCheckTask 的 run 方法是真正执行健康检测的方法,它会判断当前时间和实例最后一次心跳的差值是否大于心跳超时时间,如果大于则设置当前实例不健康,并发布服务实例状态变更事件和心跳超时事件(默认15秒);如果当前时间和实例最后一次心跳的差值是否大于实例删除时间(默认30秒),则删除实例。
//com.alibaba.nacos.naming.healthcheck.ClientBeatCheckTask#run
@Override
public void run() {
try {
if (!getDistroMapper().responsible(service.getName())) {
return;
}
//是否开启监控检查
if (!getSwitchDomain().isHealthCheckEnabled()) {
return;
}
//获取service的所有实例
List<Instance> instances = service.allIPs(true);
// first set health status of instances:
//设置实例的健康状态
for (Instance instance : instances) {
//当前时间-实例最后的心跳时间 是否大于 实例心跳超时时间 默认 15 秒
if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {
//判断实例是否被标记
if (!instance.isMarked()) {
//判断实例是否健康
if (instance.isHealthy()) {
//设置实例不健康
instance.setHealthy(false);
//日志打印 客户端超时
Loggers.EVT_LOG
.info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}",
instance.getIp(), instance.getPort(), instance.getClusterName(),
service.getName(), UtilsAndCommons.LOCALHOST_SITE,
instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat());
//发布服务状态变更事件 通知 nacos-client 服务已经下线
getPushService().serviceChanged(service);
//发布心跳超时事件
ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
}
}
}
}
//实例是否过期
if (!getGlobalConfig().isExpireInstance()) {
return;
}
// then remove obsolete instances:
//删除过期的实例
for (Instance instance : instances) {
//实例被标记 跳过
if (instance.isMarked()) {
continue;
}
//当前时间-实例最后心跳时间 是否大于实例删除时间 默认 30 秒
if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {
// delete instance
Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(),
JacksonUtils.toJson(instance));
//大于 则删除实例
deleteIp(instance);
}
}
} catch (Exception e) {
Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);
}
}
ServiceManager#addInstance 方法源码分析
ServiceManager#addInstance 方法中会拿到 service 中的 List 实例列表,然后重新设置到 Instances 中,调用 consistencyService 完成 Nacos 集群中的数据同步,值得注意的是在重新设置 Instance 值的时候使用了 CopyOnWriteArrayList,使用 CopyOnWriteArrayList 完成对实例状态更新,会用新的 Instance 列表直接覆盖旧 Instance 列表,这样在更新过程中,旧 Instance 列表不受影响,用户依然可以读取。
//com.alibaba.nacos.naming.core.ServiceManager#addInstance
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
//构建实例的 key 就是拼接 key
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
//获取service
Service service = getService(namespaceId, serviceName);
//synchronized 保证线程安全
synchronized (service) {
//获取 service 中的所有 instance 包括当前对象
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
//创建 Instances 对象
Instances instances = new Instances();
//重新赋值 这里使用了 CopyOnWriteArrayList
instances.setInstanceList(instanceList);
//使用 consistencyService.put() 方法完成 Nacos 集群的数据同步 保证集群数据一致性
consistencyService.put(key, instances);
}
}
public void setInstanceList(List<Instance> instanceList) {
this.instanceList = new CopyOnWriteArrayList<>(instanceList);
}
至此,Nacos Server 端的注册主流程已经结束,但 Nacos Server 是如何通知 Nacos Client 服务下线以及如何保证 Nacos 集群中数据一致性,本篇暂不做分析,敬请期待后续分析。
欢迎提出建议及对错误的地方指出纠正。