一、什么是 Nacos
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos的关键特性包括:
- 服务发现和服务健康监测
- 动态配置服务
- 动态 DNS 服务
- 服务及其元数据管理
Nacos 架构
NamingService: 命名服务,注册中心核心接口
ConfigService:配置服务,配置中心核心接口
二、nacos单机、集群部署以及整合prometheus+grafana监控配置
详见:https://blog.csdn.net/qq_43631716/article/details/121177154
三、nacos注册中心原理流程
1、Nacos&Ribbon&Feign核心微服务架构图
2、架构原理
- 微服务系统在启动时将自己注册到服务注册中心,同时向外发布 Http 接口供其它系统调用(一般都是基于Spring MVC)
- 服务消费者基于 Feign 调用服务提供者对外发布的接口,先对调用的本地接口加上注解@FeignClient,Feign会针对加了该注解的接口生成动态代理,服务消费者针对 Feign 生成的动态代理去调用方法时,会在底层生成Http协议格式的请求,类似 /stock/deduct?productId=100
- Feign 最终会调用Ribbon从本地的Nacos注册表的缓存里根据服务名取出服务提供在机器的列表,然后进行负载均衡并选择一台机器出来,对选出来的机器IP和端口拼接之前生成的url请求,生成调用的Http接口地址 http://192.168.0.60:9000/stock/deduct?productId=100,最后基于HTTPClient调用请求
3、Nacos核心功能点
服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)。
服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存。
服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
4、nacos服务注册表结构
Map<namespace, Map<group::serviceName, Service>>
例子:
NameSpace可以用来区分不同的环境,如dev、qa、prod等。所以说一套nacos可以支持多个环境的服务注册。
Group用来区分不同的微服务组,如交易微服务、仓储微服务组等。不同的微服务可以属于一个微服务组。
Service对应一个具体的服务,如订单服务、支付服务。而一个具体的服务中还可以进行区分:Cluster。Cluster可以用来描述一个服务的异地多机房部署。比如一个订单服务,可能在北京有部署,也可能在上海有部署。
5、注册表防止多节点读写并发冲突原理
与CopyOnWrite并发集合原理一致,都是写时复制。并且因为nacos是单线程注册,所以不用在写时加锁,同时复制只是复制某个cluster下的数据,所以性能也不错(都复制的话数据量太大)
四、nacos配置中心原理流程
1、配置中心架构图
2、客户端原理分析
2.1、获取配置
获取配置的主要方法是 NacosConfigService 类的 getConfig 方法,通常情况下该方法直接从本地文件中取得配置的值,如果本地文件不存在或者内容为空,则再通过 HTTP GET 方法从远端拉取配置,并保存到本地快照中。当通过 HTTP 获取远端配置时,Nacos 提供了两种熔断策略,一是超时时间,二是最大重试次数,默认重试三次。
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
group = this.null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
// 从本地文件中首先获取配置信息
String content = LocalConfigInfoProcessor.getFailover(this.agent.getName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", new Object[]{this.agent.getName(), dataId, group, tenant, ContentUtils.truncateContent(content)});
cr.setContent(content);
this.configFilterChainManager.doFilter((IConfigRequest)null, cr);
content = cr.getContent();
return content;
} else { // 远端获取
try {
String[] ct = this.worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(ct[0]);
this.configFilterChainManager.doFilter((IConfigRequest)null, cr);
content = cr.getContent();
return content;
} catch (NacosException var9) {
if (403 == var9.getErrCode()) {
throw var9;
} else {
LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}", new Object[]{this.agent.getName(), dataId, group, tenant, var9.toString()});
LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", new Object[]{this.agent.getName(), dataId, group, tenant, ContentUtils.truncateContent(content)});
content = LocalConfigInfoProcessor.getSnapshot(this.agent.getName(), dataId, group, tenant);
cr.setContent(content);
this.configFilterChainManager.doFilter((IConfigRequest)null, cr);
content = cr.getContent();
return content;
}
}
}
}
2.2、注册监听器
配置中心客户端会通过对配置项注册监听器达到在配置项变更的时候执行回调的功能。
NacosConfigService#getConfigAndSignListener
ConfigService#addListener
Nacos 可以通过以上方式注册监听器,它们内部的实现均是调用 ClientWorker 类的 addCacheDataIfAbsent。其中 CacheData 是一个维护配置项和其下注册的所有监听器的实例,所有的 CacheData 都保存在 ClientWorker 类中的原子 cacheMap 中,其内部的核心成员有:
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = (Listener)this.listenerMap.computeIfAbsent(key, (lst) -> {
return new AbstractSharedListener() {
public void innerReceive(String dataId, String group, String configInfo) {
NacosContextRefresher.refreshCountIncrement();
NacosContextRefresher.this.nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
// 如果监听到服务发生变化,发布一个spring容器刷新事件来更新bean
NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));
if (NacosContextRefresher.log.isDebugEnabled()) {
NacosContextRefresher.log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo));
}
}
};
});
try {
this.configService.addListener(dataKey, groupKey, listener);
} catch (NacosException var6) {
log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), var6);
}
}
2.3、配置长轮询(实时更新最新注册列表)
每个配置项和其下的监听器对应一个cachedata,第一个单线程的线程池每隔10ms拿3000个待轮询的cachedata封装成 LongPollingTask给第二个线程池(线程数=task数)。这个线程池判断哪些是变更的key,拉去这些配置更新本地快照,回调监听器更新每个cachedata的值。
3、服务端原理分析
3.1、配置dump
服务端启动时就会依赖 DumpService 的 init 方法,从数据库中 load 配置存储在本地磁盘上,并将一些重要的元信息例如 MD5 值缓存在内存中。服务端会根据心跳文件中保存的最后一次心跳时间,来判断到底是从数据库 dump 全量配置数据还是部分增量配置数据(如果机器上次心跳间隔是 6h 以内的话)。
全量 dump 当然先清空磁盘缓存,然后根据主键 ID 每次捞取一千条配置刷进磁盘和内存。增量 dump 就是捞取最近六小时的新增配置(包括更新的和删除的),先按照这批数据刷新一遍内存和文件,再根据内存里所有的数据全量去比对一遍数据库,如果有改变的再同步一次,相比于全量 dump 的话会减少一定的数据库 IO 和磁盘 IO 次数。
3.2、配置发布
发布配置的代码位于 ConfigController#publishConfig中。集群部署,请求一开始也只会打到一台机器,这台机器将配置插入Mysql中进行持久化。服务端并不是针对每次配置查询都去访问 MySQL ,而是会依赖 dump 功能在本地文件中将配置缓存起来。因此当单台机器保存完毕配置之后,需要通知其他机器刷新内存和本地磁盘中的文件内容,因此它会发布一个名为 ConfigDataChangeEvent 的事件,这个事件会通过 HTTP 调用通知所有集群节点(包括自身),触发本地文件和内存的刷新。
3.3、处理长轮询
客户端会有一个长轮询任务,拉取服务端的配置变更,服务端处理逻辑在LongPollingService类中,其中有一个 Runnable 任务名为ClientLongPolling,服务端会将收到的轮询请求包装成一个 ClientLongPolling 任务,该任务持有一个 AsyncContext 响应对象,通过定时线程池延后 29.5s 执行。比客户端 30s 的超时时间提前 500ms 返回是为了最大程度上保证客户端不会因为网络延时造成超时。