nacos源码分析
代码目录结构
- address模块: 主要查询nacos集群中节点个数以及IP的列表.
- api模块: 主要给客户端调用的api接口的抽象.
- common模块: 主要是通用的工具包和字符串常量的定义
- client模块: 主要是对依赖api模块和common模块,对api的接口的实现,给nacos的客户端使用.
- cmdb模块: 主要是操作的数据的存储在内存中,该模块提供一个查询数据标签的接口.
- config模块: 主要是服务配置的管理, 提供api给客户端拉去配置信息,以及提供更新配置 的,客户端通过长轮询的更新配置信息.数据存储是mysql.
- naming模块: 主要是作为服务注册中心的实现模块,具备服务的注册和服务发现的功能.
- console模块: 主要是实现控制台的功能.具有权限校验、服务状态、健康检查等功能.
- core模块: 主要是实现Spring的PropertySource的后置处理器,用于加载nacos的default的配置信息.
- distribution模块: 主要是打包nacos-server的操作,使用maven-assembly-plugin进行自定义打包
框架图
客户端
心跳保活
定时向nacos注册中心发送心跳。类:BeatReactor.BeatTask。默认5s发送一次,也会从上一次心跳响应报文里获取最新的delay时间。
心跳报文如下:
{"cluster":"DEFAULT","ip":"xxxx","metadata":{"preserved.register.source":"SPRING_CLOUD"},
"period":5000,"port":8091,"scheduled":false,
"serviceName":"DEFAULT_GROUP@@service-preposedata-testD","stopped":false,"weight":1.0}
实例下线后取消心跳。上线和下线接口实现类NacosServiceRegistry,一般托管于spring容器。但是springboot在停服的最后时刻才进行nacos服务下线,这就会造成服务消费方会有短暂的请求继续发送到提供方而造成超时。
解决办法是监听spring容器ContextClosedEvent事件,在该时间里主动对nacos服务进行下线。代码如下:
@Override
public void onApplicationEvent(ContextClosedEvent event) {
NacosAutoServiceRegistration nacosAutoServiceRegistration = ApplicationContextUtil
.getApplicationContext()
.getBean("nacosAutoServiceRegistration",NacosAutoServiceRegistration.class);
nacosAutoServiceRegistration.destroy();
}
但是这样并没有根本解决问题。因为消费方会缓存server列表在本地。
服务订阅
初始化获取
服务消费方在启动时会通过NacosNamingService类的getAllInstances/selectInstances接口获取对应服务的实例集合。
spring-cloud-started-alibaba-nacos-discovery包里有如下代码:
private List<NacosServer> getServers() {
try {
String group = discoveryProperties.getGroup();
List<Instance> instances = discoveryProperties.namingServiceInstance()
.selectInstances(serviceId, group, true);
return instancesToServerList(instances);
}
catch (Exception e) {
throw new IllegalStateException(
"Can not get service instances from nacos, serviceId=" + serviceId,
e);
}
}
调用链如下:
以上是初始化时获取server列表的思路。NacosNamingService类在实例化时还创建了HostReactor类。
定时更新server信息
HostReactor类的一个作用是实例化一个scheduled任务UpdateTask。该类的作用是更新服务信息。更新频率是10s。
实时更新server信息
HostReactor类的另一个作用是实例化一个scheduled任务PushReceiver。该类的所用是以udp的方式实时接收变化的服务信息。
问题解决
服务上下线事件,对于client即有定时更新也有实时通知,但是这对于高并发的系统来说,依然会有可能出现上文所说的问题。从两个方向继续深入解决问题。
-
tomcat,被动等待
从服务提供者下线到消费者接收到消息,这是秒级延迟。在这若干秒内tomcat依然会接收到上游系统发送过来的请求。所以本地服务停止后,要监听spring容器的ContextClosedEvent,在第一时间主动对nacos进行下线,不要依赖springcloud的实现;第二监听tomcat线程池和自建线程池,直到所有线程池里的任务都完全消费,此时服务才算优雅停机。
注:spring容器是在最后关闭了数据库连接池,在此之前如果有定时调度没有停止,则会出现数据库连接异常 -
ribbon,主动下线
如果项目中用了ribbon+feign框架,那么ribbon获取的server列表又再次进行了缓存。ribbon持有的server列表是由PollingServerListUpdater类的定时调度任务(delay=1s)从nacos更新到loadbalancer里。综上所述,nacos client通过udp实时获取到了server列表,但是ribbon自己又通过调度任务定时更新表列,如此延时越来越长。代码如下。若想ribbon实时接受nacos更新消息,则需修改源码实时监听nacos注册中心,如此则代码入侵,如果简单的的等几秒钟。
类:PollingServerListUpdater
@Override
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs,
refreshIntervalMs,
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}
类:DynamicServerListLoadBalancer
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
服务注册
springcloud注册nacos服务是从此处入口:
nacos提供的注册入口:
注册时会启动心跳保活线程。