Nacos源码(十)总结篇
前言
本章是Nacos源码阅读的最后一章,总结一下Nacos配置中心和注册中心。
配置中心:配置查询、配置监听、配置发布、配置注入
注册中心:服务注册、服务发现、健康检查、Distro协议
一、配置中心
Namspace(Tenant):命名空间(租户),默认命名空间是public。一个命名空间可以包含多个Group。
Group:组,默认分组是DEFAULT_GROUP。一个组可以包含多个dataId。
DataId:在nacos中DataId代表一整个配置文件,是配置的最小单位。
1.4.x
配置查询
从客户端角度来看。
优先考虑failover开关是否开启,如果failover开关开启,读取本地磁盘中的failover配置。
大多数情况下不使用failover配置,会实时查询服务端实时配置(/v1/cs/configs),每次实时查询配置后,都会同步配置到本地snapshot文件中。
如果实时查询服务端异常或查询成功响应但返回code非403Forbidden,会查询本地snapshot文件中的配置返回。
从服务端角度来看,这里分为了两种情况。
// ConfigServletInner.doGetConfig
if (PropertyUtil.isDirectRead()) {// #1
configInfoBase = persistService.findConfigInfo(dataId, group, tenant);
} else { // #2
file = DiskUtil.targetFile(dataId, group, tenant);
}
// PropertyUtil
public static boolean isDirectRead() {
return EnvUtil.getStandaloneMode() && isEmbeddedStorage();
}
第一种是-Dnacos.standalone=true -DembeddedStorage=true,单机嵌入式数据源的情况。此时使用derby数据源中数据返回。
其他情况,如单机mysql/集群mysql/集群derby,会实时读取当前节点文件系统里的配置返回。
配置监听
从客户端角度来看。
addListener添加监听,只是注册在内存配置项CacheData中,没有实际与服务端发生通讯(区别于注册中心的服务监听)。
// CacheData.java
// 注册在这个tenant-group-dataId配置上的监听器
private final CopyOnWriteArrayList<ManagerListenerWrap> listeners;
public void addListener(Listener listener) {
ManagerListenerWrap wrap =
(listener instanceof AbstractConfigChangeListener) ? new ManagerListenerWrap(listener, md5, content)
: new ManagerListenerWrap(listener, md5);
if (listeners.addIfAbsent(wrap)) {
LOGGER.info(;
}
}
当客户端CacheData中的md5发生变化,通知所有CacheData对应的Listener。
// CacheData.java
// 配置的md5
private volatile String md5;
void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
// 比较CacheData中的md5与Listener中上次的md5是否相同
if (!md5.equals(wrap.lastCallMd5)) {
// 不相同则触发监听器
safeNotifyListener(dataId, group, content, type, md5, wrap);
}
}
}
真正的监听逻辑属于客户端的长轮询逻辑。
每3000个CacheData,客户端会开启一个LongPollingRunnable长轮询任务,请求服务端/v1/cs/configs/listener监听配置变更,长轮询超时时间是30s。
当配置发生变更后,服务端会通过长轮询推送给客户端,返回变更的配置项(groupKey)。
客户端走配置查询逻辑,根据配置项反查服务端具体配置/v1/cs/configs。
反查配置更新到CacheData,如果配置md5发生变更,触发对应Listener。
从服务端角度来看。
/v1/cs/configs/listener利用了Servlet3.0的AsyncContext来实现长轮询。
// LongPollingService
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map, int probeRequestSize) {
String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
// 确定长轮询实际的超时时间
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
if (isFixedPolling()) {
timeout = Math.max(10000, getFixedPollingInterval());
} else {
// 用内存缓存的md5比较,是否有配置项发生变更,如果有的话立即返回
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
if (changedGroups.size() > 0) {
generateResponse(req, rsp, changedGroups);
return;
} else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
// 如果客户端的本次长轮询请求,请求头包含Long-Pulling-Timeout-No-Hangup,则立即返回200
return;
}
}
String ip = RequestUtil.getRemoteIp(req);
// 开启AsyncContext
final AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(0L);
// 提交长轮询任务到其他线程
ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
/v1/cs/configs/listener处理流程:
比较报文中监听配置项的md5是否已经发生了改变,如果已经发生改变,立即返回客户端发生变更的配置项groupKey
如果没有配置项发生改变,提交到异步线程执行ClientLongPolling
ClientLongPolling将提交一个超时检测任务,超时时间和长轮询的超时时间呼应,超时后自动返回客户端空数据,表示没有配置发生变更。
class ClientLongPolling implements Runnable {
@Override
public void run() {
// 1. 提交超时处理任务
asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
@Override
public void run() {
// ...
}
}, timeoutTime, TimeUnit.MILLISECONDS);
// ...
}
}
配置发布
响应客户端的长轮询有两种方式,一种是等待30s超时直接返回空数据给客户端,另一种就是通过发布配置响应客户端。
当使用非集群Derby模式启动时,配置发布流程如下:
POST /configs,更新数据库,响应客户端。
当前节点异步请求所有节点(包括自己)/v1/cs/communication/dataChange,执行配置同步。
被同步节点执行Dump流程,将配置写入本地文件系统并更新内存中配置项的MD5值。
被同步节点查看是否有监听在变更配置项上的客户端长轮询请求,如果有则将变更配置项响应客户端长轮询。
当使用集群Derby模式启动时,配置发布流程引入了JRaft框架,使用Raft一致性算法实现读写一致。
对于配置发布,集群Derby与上面的流程的不同点在于写数据库这一步。因为每个节点存储了一份数据,这里需要走一次Raft写,半数以上节点log commit成功以后,将会把log应用到本地状态机derby数据库里。
另外在Dump流程里,要通过配置项查一次实时配置,这里的读是JRaft实现的线性一致读。
2.x
2.x配置中心主要的改动在于引进长连接代替了短连接长轮询。
客户端改动点:
改动点一:
1.x每3000个CacheData,客户端会开启一个LongPollingRunnable长轮询任务;2.x每3000个CacheData,客户端会开启一个RpcClient,每个RpcClient与服务端建立一个长连接。
改动点二:
客户端增加定时全量拉取配置的逻辑。
在1.x中,Nacos配置中心通过长轮询的方式更新客户端配置,对于客户端来说只有配置推送;
在2.x中支持客户端定时同步配置,所以2.x属于推拉结合的方式。
拉:每5分钟,客户端会对全量CacheData发起配置监听请求ConfigBatchListenRequest,如果配置md5发生变更,会同步收到变更配置项,发起ConfigQuery请求查询实时配置。
推:服务端配置变更,会发送ConfigChangeNotifyRequest请求给与当前节点建立长连接的客户端通知配置变更项。
服务端改动点:
改动点一:
由于2.x使用长连接代替长轮询,监听请求ConfigBatchListenRequest不会被服务端hold住,会立即返回。服务端只是将监听关系保存在内存中,方便后续通知。
groupKey和connectionId的映射关系,方便后续通过变更配置项找到对应客户端长连接;connectionId和groupKey的映射关系,只是为了控制台展示。这些关系保存在服务端的ConfigChangeListenContext单例中。
@Component
public class ConfigChangeListenContext {
/**
* groupKey-> connection set.
*/
private ConcurrentHashMap<String, HashSet<String>> groupKeyContext = new ConcurrentHashMap<String, HashSet<String>>();
/**
* connectionId-> group key set.
*/
private ConcurrentHashMap<String, HashMap<String, String>> connectionIdContext = new ConcurrentHashMap<String, HashMap<String, String>>();
}
改动点二:
对应改动点一,1.x需要通过groupKey找到仍然在进行长轮询的客户端AsyncContext;2.x是通过groupKey找到connectionId,再通过connectionId找到长连接,发送ConfigChangeNotifyRequest通知客户端配置变更。
// RpcConfigChangeNotifier
public void configDataChanged(String groupKey, String dataId, String group, String tenant, boolean isBeta,
List<String> betaIps, String tag) {
// 从注册监听的上下文中,获取groupKey对应的所有监听客户端connectionId
Set<String> listeners = configChangeListenContext.getListeners(groupKey);
if (!CollectionUtils.isEmpty(listeners)) {
for (final String client : listeners) {
// 通过connectionId获取实际gRPC长连接
Connection connection = connectionManager.getConnection(client);
if (connection == null) {
continue;
}
// ...
// 构建同步配置请求参数
ConfigChangeNotifyRequest notifyRequest = ConfigChangeNotifyRequest.build(dataId, group, tenant);
// 为了不阻塞其他事件处理,这里提交一个任务到其他线程池处理
RpcPushTask rpcPushRetryTask = new RpcPushTask(notifyRequest, 50, client, clientIp,
connection.getMetaInfo().getAppName());
push(rpcPushRetryTask);
}
}
}
stater
对于spring-cloud-starter-alibaba-nacos-config,目前只支持到1.4.1版本,不过支持到2.x也只是底层逻辑变化,对于客户端是无感的(排除nacos-client原有1.4.1依赖,加入2.x的nacos-client即可)。
配置注入
Nacos利用PropertySourceBootstrapConfiguration这个ApplicationContextInitializer,在Application容器刷新前(prepareContext阶段)使用NacosPropertySourceLocator将nacos配置转换为PropertySource注入了Environment。
NacosPropertySourceLocator读取Nacos配置,底层也是调用了nacos-client的ConfigService里的方法。
配置优先级
利用Spring的CompositePropertySource内部链表结构,越靠前的配置项,优先级越高。
spring:
application:
name: nacos-config-example
profiles:
active: DEV
---
spring:
profiles: DEV
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: 789b5be0-0286-4cda-ac0c-e63f5bae3652
group: DEFAULT_GROUP
extension-configs:
- data_id: arch.properties
group: arch
refresh: true
- data_id: jdbc.properties
group: data
refresh: false
shared-configs:
- data_id: share.properties
group: DEFAULT_GROUP
refresh: true
在SpringCloud中Nacos配置分为三类:
应用配置:对应Nacos的一个命名空间下一个分组下的dataId。dataId = {prefix}-{spring.profiles.active}.{file-extension}。对于prefix前缀,优先级spring.cloud.nacos.config.name > spring.cloud.nacos.config.prefix > spring.application.name。应用配置内部也有优先级,从低到高:
{prefix}
{prefix}-{spring.profiles.active}
{prefix}-{spring.profiles.active}.{file-extension}
扩展配置extension-configs:默认不能刷新。
共享配置shared-configs:默认不能刷新。
配置监听
当Spring容器完全启动以后,NacosContextRefresher会收到ApplicationReadyEvent,此时开启监听。
NacosContextRefresher循环所有NacosPropertySource,调用ConfigService.addListener注册监听。
// NacosContextRefresher
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}
private void registerNacosListenersForApplications() {
// spring.cloud.nacos.config.isRefreshEnabled总控开关默认开启
if (isRefreshEnabled()) {
// 从缓存中获取所有Nacos配置
for (NacosPropertySource propertySource : NacosPropertySourceRepository.getAll()) {
if (!propertySource.isRefreshable()) {
continue;
}
String dataId = propertySource.getDataId();
// 注册监听
registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
当监听器被触发回调时,会发布RefreshEvent事件,只要单个dataId发生变更,将导致所有RefreshScope里的Bean被销毁并重新创建,不支持单个dataId对应的配置重新注入。
// NacosContextRefresher
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
}
});
configService.addListener(dataKey, groupKey, listener);
}
二、注册中心
1.4.x
注册中心模型
Namspace(Tenant):命名空间(租户),默认命名空间是public。一个命名空间可以包含多个Group。
Group:组,默认分组是DEFAULT_GROUP。
Service:应用服务。
Cluster:集群,默认集群是DEFAULT。
Instance:服务实例。
服务注册
客户端发送POST /nacos/v1/ns/instance请求给服务端,将自身Instance信息发送给服务端。
服务端针对临时实例和持久实例使用不同的方式存储和检测客户端健康状态。
临时实例
如果客户端Instance是临时实例(默认),Instance.ephemeral=true。
从客户端角度看。
客户端定时执行心跳任务,向服务端发送心跳请求PUT /nacos/v1/ns/instance/beat,维持自己在服务端的注册信息。心跳间隔为preserved.heart.beat.interval,默认为5s。如果发送心跳时,服务端没有当前实例的注册信息,会返回RESOURCE_NOT_FOUND(20404),客户端会发起一次注册请求POST /nacos/v1/ns/instance。
从服务端角度看。
服务端处理POST /nacos/v1/ns/instance注册请求如上图步骤:
将Service写入内存注册表(ServiceManager.serviceMap)
开启Service心跳检测,检测Service下所有Instance是否按时发送心跳,每5s检测一次,如果15s没有发送心跳,标记Instance非健康,如果30s没有发送心跳,直接从注册表中删除这个Instance,服务注销。(ClientBeatCheckTask)
DistroConsistencyServiceImpl将服务名作为key,服务下所有Instance作为value写入内存KV存储组件DataStore。
异步,当KV变化,更新内存注册表(ServiceManager.serviceMap),异步通过UDP推送给有监听当前服务变更的客户端。
异步,当KV变化,延迟1s,调用集群其他节点的PUT /v1/ns/distro/datum,同步服务实例列表。
服务端处理PUT /nacos/v1/ns/instance/beat心跳请求:
更新内存注册表中实例健康状况:Instance.healthy=true
如果实例健康状况从非健康变为健康,UDP推送给监听客户端
持久实例
如果客户端Instance是持久实例,Instance.ephemeral=false。这部分在前面没有说过,因为非默认方案直接带过。
与临时实例的区别:
服务注册和服务注销等写请求,走Raft写流程,写入本地文件系统(FileKvStorage),然后异步写入内存注册表(ServiceManager.serviceMap)。
客户端不需要给服务端发送心跳,服务端使用TCP连接探测客户端是否存活,不会摘除Instance,只会标记成非健康。
服务发现
从客户端角度看。
HostReactor负责服务发现。
对于服务订阅,从存储上看,客户端服务注册表有三层。
服务订阅流程如下:
failover故障转移开关开启的情况下,会读取本地文件系统中的注册表,加载到FailoverReactor的serviceMap变量中。用户查询时,会读取FailoverReactor的serviceMap获取服务信息。
一般情况下,failover开关不开启,这里会优先读取HostReactor.serviceMap内存注册表。
如果HostReactor.serviceMap中没有读到服务,会请求Nacos服务端。一方面获取服务注册信息并更新HostReactor.serviceMap内存注册表,另一方面查询请求将本地启动的UDP端口告知服务端,告知服务端自己订阅服务。
对于每个订阅服务,客户端会开启服务更新任务UpdateTask,定时请求服务端获取最新注册表。客户端默认1秒拉取一次,但是服务端会通过返回报文控制客户端10秒拉取一次。
对于服务查询,用户即可以走订阅逻辑,也可以走实时查询逻辑,具体取决于NacosNamingService的getAllInstances方法的第四个入参,subscribe=true表示走服务订阅流程,subscribe=false代表不走服务订阅流程,直接查询服务端最新服务注册表。
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters, boolean subscribe)
服务订阅主要是为了更新客户端的内存注册表,此外,用户代码可以监听服务变更。
nacosNamingService.subscribe("nacos.test.3", new AbstractEventListener() {
@Override
public void onEvent(Event event) {
System.out.println(((NamingEvent) event).getServiceName());
System.out.println(((NamingEvent) event).getInstances());
}
});
InstancesChangeNotifier管理客户端监听服务,当服务发生变更时,通知所有监听器。
public class InstancesChangeNotifier extends Subscriber<InstancesChangeEvent> {
// 监听注册表
// service唯一标识groupName+@@+serviceName+@@+clusterName - 监听器
private final Map<String, ConcurrentHashSet<EventListener>> listenerMap = new ConcurrentHashMap<String, ConcurrentHashSet<EventListener>>();
从服务端角度看。
无论是服务订阅,还是服务查询,都是走GET /nacos/v1/ns/instance/list。区别在于前者请求参数中包含了客户端的udp端口号;后者udp端口号是0。
如果是服务订阅,服务端会把订阅服务和订阅Client信息,保存在PushService的Map里,待服务发生变更后,通过UDP推送给客户端。
public class PushService implements ApplicationContextAware, ApplicationListener<ServiceChangeEvent> {
// 第一个key是namespace+groupService 第二个key是PushClient.toString
private static ConcurrentMap<String, ConcurrentMap<String, PushClient>> clientMap = new ConcurrentHashMap<>();
无论是服务订阅还是服务查询,无论是临时实例还是持久实例,都会从服务端ServiceManager获取到Cluster,再从Cluster获取到Service,再从Service获取到所有Instance返回给客户端。
// ServiceManager.java
// namespace - groupName@@serviceName - Service
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
// Service.java
// key是集群名称
private Map<String, Cluster> clusterMap = new HashMap<>();
// Cluster.java
// 持久Instance
private Set<Instance> persistentInstances = new HashSet<>();
// 临时Instance
private Set<Instance> ephemeralInstances = new HashSet<>();
此外,如果客户端只查询健康的实例,服务端还有个保护模式。这是AP模式注册中心的一个代表性功能,如Eureka。
保护模式的开启,取决于控制台配置的保护阈值(protectThreshold),这会保存在Service实例上。
当存活实例/总实例<= protectThreshold时,认为当前服务端节点发生故障,进入保护模式,返回服务下所有实例。对于默认protectThreshold=0,如果存活实例为0,会返回所有实例。
Distro
Nacos注册中心使用Distro协议,属于AP。
对于客户端来说,Nacos所有节点是对等的,这意味着请求任意一个Nacos节点都能处理读写请求。如果某个节点处理失败,客户端会重新选择一个节点请求。
但是从服务端来看,没那么简单。
读请求:
由于Distro协议非强一致,每个节点都可以以当前节点内存中的数据为准,响应客户端。
写请求:
对于客户端写请求,如服务注册、客户端心跳,DistroFilter会拦截(基于@CanDistro注解)。判断请求参数中的groupServiceName是否属于当前节点管理范围(通过hash取模),如果不属于当前节点管理,转发其他节点处理,用其他节点的返回信息返回客户端;如果属于当前节点管理,直接进入Controller处理。
集群数据同步:
当发生服务注册或服务注销(包含客户端30s心跳超时),责任节点会将服务数据同步至其他非责任节点;
当服务端检测到客户端心跳15s超时(不满30s),只会在当前责任节点标记实例为非健康状态,不会将非健康状态同步至其他节点;
当服务端重新接收到客户端心跳后(15-30s中间),重新标记实例为健康,也不会做数据同步。
责任节点每5秒(默认nacos.core.protocol.distro.data.verify_interval_ms=5000ms)执行VERIFY,同步所有自己负责Service的Instance列表的MD5到其他节点,其他节点如果发现MD5发生变更,会反查责任节点,然后更新本地数据。
集群管理:
Nacos默认用nacos.home/cluster/cluster.conf配置文件的方式,初始化集群列表。
Tomcat启动后,每个节点每2s执行POST /v1/core/cluster/report,向集群随机节点(包含DOWN)发送当前节点信息,一方面是为了同步当前节点信息,另一方面也是健康检查。
健康检查是双向的,每个节点都会主动发起健康检查,也会被动接收健康检查。如果健康检查失败,对端节点标记为SUSPICIOUS,表示对端可能下线,但是可以作为责任节点处理写请求;当连续超过3次健康检查失败,会标记为对端节点为DOWN,不能作为责任节点处理写请求。
2.x
注册中心模型
2.x在模型上做了大变更,服务端不再使用Service、Cluster、Instance模型,改为Service、Instance、Client、Connection。
Service:服务,namespace+group+name=单例Service。Service与Instance不会直接发生关系,由ServiceManager管理
Instance:实例,InstancePublishInfo,由Client管理。
Client:一个客户端长连接对应一个Client,一个Client持有对应客户端注册和监听的的Service&Instance。Client使Service和Instance发生关联,由ClientManager管理。
Connection:连接(长连接),一个Connection对应一个Client,由ConnectionManager管理。
模型索引:Service与Instance没有直接关系,需要通过遍历所有Client注册的服务和实例,得到Service下所有Instance。为了加速查询,提供了两个索引服务
ClientServiceIndexesManager:Service->Client,服务与发布这个服务&服务与监听这个服务的客户端的关联关系。
ServiceStorage:Service->Instance,服务与服务下实例的关联关系。
服务注册
对于客户端来说,临时实例注册,走gRPC;持久实例注册走http。
对于服务端来说,无论是gRPC还是http,底层流程都是一样的:
建立Connection->Client->Service->Instance的关系
构建索引,用于辅助查询
通知订阅客户端
集群数据同步
服务发现
服务查询,走ServiceStorage索引服务。如果ServiceStorage查询不到数据,走复杂查询逻辑,然后再放入ServiceStorage缓存。当服务发生变更时,ServiceStorage缓存数据会更新。
服务订阅,服务端会把Client订阅的服务,注册到Client里管理。
public abstract class AbstractClient implements Client {
// Client订阅的服务
protected final ConcurrentHashMap<Service, Subscriber> subscribers = new ConcurrentHashMap<>(16, 0.75f, 1);
}
客户端内存注册表更新,还是推拉结合的方式。
推:2.x服务端不再走UDP推送服务变更,改为走gRPC推送给客户端NotifySubscriberRequest。
拉:还是定时请求服务端,更新内存注册表,只不过时间间隔从10s改为了60s。
健康检查
持久实例健康检查和1.x一样。
临时实例客户端与服务端建立长连接,通过双向健康检查确保Client存活。
服务端检测20s空闲连接,向客户端发起探测请求,如果客户端1s内响应,认为健康检查通过。如果检查不通过,实例直接下线,注销服务;
客户端检测5s空闲连接,向服务端发起健康检查请求,如果服务端3s内响应,认为健康检查通过。如果检查不通过,会选择下一个Nacos节点建立长连接。
Distro
读写请求处理:
无论是读还是写,都会走与当前客户端建立长连接的节点,不走服务端DistroFilter。
读请求不同于1.x会打到不同节点上,一直会打到建立长连接的节点上;
写请求不同于1.x中可能被二次转发,与客户端建立长连接的节点,就是责任节点。不同于1.x,基于Service设置责任节点,2.x基于Client设置责任节点。
ConnectionBasedClient的isNative属性为true,当前实例就是责任节点;ConnectionBasedClient的isNative属性为false,代表当前实例是非责任节点。
public class ConnectionBasedClient extends AbstractClient {
/**
* {@code true} means this client is directly connect to current server. {@code false} means this client is synced
* from other server.
*/
private final boolean isNative;
}
集群数据同步:
与1.x相同的是,服务发生变更,会当时同步给其他非责任节点。
与1.x不同的是,2.x责任节点VERIFY任务不再仅发送服务实例列表的摘要md5给非责任节点。
责任节点每5s向非责任节点发送VERIFY续租Client,包含Client的所有数据,不仅仅是md5,这避免了非责任节点反查;非责任节点定时扫描isNative=false的Client数据,如果超过30s没有续租,移除这些非native的Client。
集群管理:
与1.x一致。