Nacos内部接收到注册的请求时,不会立即写数据,而是将服务注册的任务放入一个阻塞队列,就立即响应给客户端。然后利用线程池读取阻塞队列中的任务,异步来完成实例更新,从而提高并发写能力。
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
// 从request 获取namespaceId
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);
// 把request中的参数封装为Instance对象
final Instance instance = parseInstance(request);
// 注册实例
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
/**
* Map(namespace, Map(group::serviceName, Service)).
*/
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
// 如果是第一次, 则创建空服务
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);
}
public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
createServiceIfAbsent(namespaceId, serviceName, local, null);
}
/**
* Create service if not exist.
*
* @param namespaceId namespace
* @param serviceName service name
* @param local whether create service by local
* @param cluster cluster
* @throws NacosException nacos exception
*/
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
// 尝试从注册表中获取服务
Service service = getService(namespaceId, serviceName);
if (service == null) {
// 如果为空,说明该服务是第一次注册,创建新的Service
Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
// 因为serviceName是group和服务名拼接,所以需要截取
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.validate();
putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
}
private void putServiceAndInit(Service service) throws NacosException {
// 把服务放入注册表
putService(service);
service = getService(service.getNamespaceId(), service.getName());
// 初始化,健康监测
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());
}
public void putService(Service service) {
if (!serviceMap.containsKey(service.getNamespaceId())) {
synchronized (putServiceLock) {
if (!serviceMap.containsKey(service.getNamespaceId())) {
serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
}
}
}
serviceMap.get(service.getNamespaceId()).putIfAbsent(service.getName(), service);
}
private void putServiceAndInit(Service service) throws NacosException {
putService(service);
service = getService(service.getNamespaceId(), service.getName());
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());
}
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
// 给当前服务生成一个唯一标识,可以理解为ServiceId
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
// 从注册表中拿到Service
Service service = getService(namespaceId, serviceName);
// 以service为锁对象,同一个服务的多个实例,只能串行来完成注册
synchronized (service) {
// 拷贝注册表中旧的实例列表,然后结合新注册的示例,得到最终的示例列表
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
// 封装新实例列表到Instances对象中
Instances instances = new Instances();
instances.setInstanceList(instanceList);
// 更新注册表(更新本地注册表、同步给Nacos集群中的其他节点)
consistencyService.put(key, instances);
}
}
com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl
@Override
public void put(String key, Record value) throws NacosException {
// 异步,只是放入队列,更新本地注册表
onPut(key, value);
// 同步给Nacos集群中的其他节点
distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
globalConfig.getTaskDispatchPeriod() / 2);
}
public void onPut(String key, Record value) {
// 判断是否临时实例
if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
// 封装实例列表,封装到Datum
Datum<Instances> datum = new Datum<>();
// value是服务中的实例列表 Instances
datum.value = (Instances) value;
// key是服务的唯一标识
datum.key = key;
datum.timestamp.incrementAndGet();
// 以serviceId为key,以Datum位置,缓存起来
dataStore.put(key, datum);
}
if (!listeners.containsKey(key)) {
return;
}
// 把ServiceId和当前的操作类型存入notifier
notifier.addTask(key, DataOperation.CHANGE);
}
初始化线程池
@PostConstruct
public void init() {
// 利用线程池,执行notifier
GlobalExecutor.submitDistroNotifyTask(notifier);
}
@Override
public void run() {
Loggers.DISTRO.info("distro notifier started");
for (; ; ) {
try {
// 从阻塞队列中获取任务,如果没有任务则阻塞等待
Pair<String, DataOperation> pair = tasks.take();
// 执行任务,更新注册列表
handle(pair);
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
}
}
}
在NacosServiceRegistry.java中调用NacosNamingService的registerInstance方法注册
serverProxy发送restApi注册
beatReactor注册心跳
Nacos在更新实例列表时,会采用CopyOnWrite技术,首先将旧的实例列表拷贝一份,然后更新拷贝的实例列表,在用更新后的实例列表来覆盖旧的实例列表。
这样在更新的过程中,就不会对读实例列表的请求产生影响,也不会出现脏读问题了。
Nacos | Eureka | |
---|---|---|
接口方式 | 对外暴露Rest风格的API用来实现注册、发现等功能 | 对外暴露Rest风格的API用来实现注册、发现等功能 |
实例类型 | 有永久和临时实例之分 | 只支持临时实例 |
健康监测 | 对临时实例采用心跳模式监测,对永久实例采用主动请求来监测 | 只支持心跳模式 |
服务发现 | 支持定时拉去和订阅推送两种模式 | 只支持定时拉取模式 |