什么是Nacos的寻址机制?
Nacos 支持单机部署以及集群部署,针对单机模式,Nacos 只是自己和自己通信;对于集群模式,则集群内的每个 Nacos 成员都需要相互通信。因此这就带来一个问题,该以何种方式去管理集群内的 Nacos 成员节点信息,这就是 Nacos 内部的寻址机制。
源码分析
寻址初始化
在Nacos中,ServerMemberManager 类存储着本节点所知道的所有成员节点列表信息,提供了针对成员节点的增删改查操作,同时维护了一个 MemberLookup 列表,方便进行动态切换成员节点寻址方式,是寻址逻辑的核心。
首先看ServerMemberManager类中的初始化方法init()。主要负责一些基本的设置、发布MembersChangeEvent事件并订阅IPChangeEvent事件,初始化寻址模式适配器并启动。
protected void init() throws NacosException {
Loggers.CORE.info("Nacos-related cluster resource initialization");
this.port = EnvUtil.getProperty(SERVER_PORT_PROPERTY, Integer.class, DEFAULT_SERVER_PORT);
this.localAddress = InetUtils.getSelfIP() + ":" + port;
this.self = MemberUtil.singleParse(this.localAddress);
this.self.setExtendVal(MemberMetaDataConstants.VERSION, VersionUtils.version);
// init abilities.
this.self.setAbilities(initMemberAbilities());
serverList.put(self.getAddress(), self);
//发布MembersChangeEvent事件并订阅IPChangeEvent事件
// register NodeChangeEvent publisher to NotifyManager
registerClusterEvent();
//初始化寻址模式适配器并启动
// Initializes the lookup mode
initAndStartLookup();
if (serverList.isEmpty()) {
throw new NacosException(NacosException.SERVER_ERROR, "cannot get serverlist, so exit.");
}
Loggers.CORE.info("The cluster resource is initialized");
}
首先看registerClusterEvent方法,发布了MembersChangeEvent事件,订阅了订阅IPChangeEvent事件,事件触发后回调onEvent方法,将节点的属性赋上更新后的值。
private void registerClusterEvent() {
//发布MembersChangeEvent事件
// Register node change events
NotifyCenter.registerToPublisher(MembersChangeEvent.class,
EnvUtil.getProperty(MEMBER_CHANGE_EVENT_QUEUE_SIZE_PROPERTY, Integer.class,
DEFAULT_MEMBER_CHANGE_EVENT_QUEUE_SIZE));
//订阅IPChangeEvent事件
// The address information of this node needs to be dynamically modified
// when registering the IP change of this node
NotifyCenter.registerSubscriber(new Subscriber<InetUtils.IPChangeEvent>() {
@Override
public void onEvent(InetUtils.IPChangeEvent event) {
String newAddress = event.getNewIP() + ":" + port;
ServerMemberManager.this.localAddress = newAddress;
EnvUtil.setLocalAddress(localAddress);
Member self = ServerMemberManager.this.self;
self.setIp(event.getNewIP());
String oldAddress = event.getOldIP() + ":" + port;
ServerMemberManager.this.serverList.remove(oldAddress);
ServerMemberManager.this.serverList.put(newAddress, self);
ServerMemberManager.this.memberAddressInfos.remove(oldAddress);
ServerMemberManager.this.memberAddressInfos.add(newAddress);
}
@Override
public Class<? extends Event> subscribeType() {
return InetUtils.IPChangeEvent.class;
}
});
}
然后看initAndStartLookup方法,该方法进行寻址模式适配器的初始化与启动,寻址模式包括单机寻址、文件寻址、地址服务器寻址。
private void initAndStartLookup() throws NacosException {
//获取寻址模式适配器
this.lookup = LookupFactory.createLookUp(this);
isUseAddressServer = this.lookup.useAddressServer();
//适配器启动
this.lookup.start();
}
先看createLookUp方法,查看获取适配器的逻辑,寻址模式通过LOOKUP_MODE_TYPE静态变量表示的"nacos.core.member.lookup.type"指定,取值为“file”或者“address-server”,也就是文件寻址和地址服务器寻址。
public static MemberLookup createLookUp(ServerMemberManager memberManager) throws NacosException {
if (!EnvUtil.getStandaloneMode()) {
String lookupType = EnvUtil.getProperty(LOOKUP_MODE_TYPE);
LookupType type = chooseLookup(lookupType);
LOOK_UP = find(type);
currentLookupType = type;
} else {
LOOK_UP = new StandaloneMemberLookup();
}
//给寻址适配器注入ServerMemberManager对象,方便利用 ServerMemberManager 的存储、查询能力
LOOK_UP.injectMemberManager(memberManager);
Loggers.CLUSTER.info("Current addressing mode selection : {}", LOOK_UP.getClass().getSimpleName());
return LOOK_UP;
}
寻址机制的实现
- 单机寻址
单机寻址对应StandaloneMemberLookup类,查看核心的doStart方法。单机模式的寻址模式很简单,就是找到自己的IP:PORT组合信息,然后格式化为一个节点信息,调用afterLookup 然后将信息存储到 ServerMemberManager 中。
public void doStart() {
String url = InetUtils.getSelfIP() + ":" + EnvUtil.getPort();
afterLookup(MemberUtil.readServerConf(Collections.singletonList(url)));
}
- 文件寻址
文件寻址模式就是每个 Nacos 节点需要维护一个叫做 cluster.conf 的文件,其中填写了每个成员节点的 IP 信息。
文件寻址对应FileConfigMemberLookup类,查看核心的doStart方法。调用readClusterConfFromDisk方法读取本节点的cluster.conf文件,获取保存的节点成员列表。并注册FileWatcher监听cluster.conf的变化,有变更会被监听并更新缓存地址列表。
public void doStart() throws NacosException {
readClusterConfFromDisk();
// Use the inotify mechanism to monitor file changes and automatically
// trigger the reading of cluster.conf
try {
WatchFileCenter.registerWatcher(EnvUtil.getConfPath(), watcher);
} catch (Throwable e) {
Loggers.CLUSTER.error("An exception occurred in the launch file monitor : {}", e.getMessage());
}
}
查看readClusterConfFromDisk方法。从磁盘中的cluster.conf文件里读取节点列表并存储到 ServerMemberManager 中。
private void readClusterConfFromDisk() {
Collection<Member> tmpMembers = new ArrayList<>();
try {
//从磁盘文件中读取节点列表
List<String> tmp = EnvUtil.readClusterConf();
tmpMembers = MemberUtil.readServerConf(tmp);
} catch (Throwable e) {
Loggers.CLUSTER
.error("nacos-XXXX [serverlist] failed to get serverlist from disk!, error : {}", e.getMessage());
}
//将节点列表信息存储到 ServerMemberManager 中。
afterLookup(tmpMembers);
}
查看注册的监听器FileWatcher,监听器会自动发现文件修改,重新读取文件内容、加载 IP 列表信息、更新新增的节点。
private FileWatcher watcher = new FileWatcher() {
@Override
public void onChange(FileChangeEvent event) {
readClusterConfFromDisk();
}
@Override
public boolean interest(String context) {
return StringUtils.contains(context, DEFAULT_SEARCH_SEQ);
}
};
缺点:每一个Nacos节点都需要单独手动维护一个 cluster.conf 文件,既增大了运维难度,又容易造成集群间成员节点列表数据的不一致性。
- 地址服务器寻址
地址服务器寻址模式是 Nacos 官方推荐的一种集群成员节点信息管理,该模式利用了一个简易的 web 服务器,用于管理 cluster.conf 文件的内容信息,这样,运维人员只需要管理这一份集群成员节点内容即可,而每个Nacos 成员节点,只需要向这个 web 节点定时请求当前最新的集群成员节点列表信息即可。
地址服务器寻址对应AddressServerMemberLookup类,查看核心的doStart方法。initAddressSys方法进行一些简单的初始化定义,run方法为核心的逻辑。
public void doStart() throws NacosException {
this.maxFailCount = Integer.parseInt(EnvUtil.getProperty(HEALTH_CHECK_FAIL_COUNT_PROPERTY, DEFAULT_HEALTH_CHECK_FAIL_COUNT));
initAddressSys();
run();
}
查看run方法。在启动时需要执行同步成员节点的pull操作,也就是进行syncFromAddressUrl方法从地址服务器中获取节点列表,最多进行5次重试。并且创建一个5秒一次的定时任务AddressServerSyncTask,每五秒请求一次地址服务器。
private void run() throws NacosException {
// With the address server, you need to perform a synchronous member node pull at startup
// Repeat three times, successfully jump out
boolean success = false;
Throwable ex = null;
int maxRetry = EnvUtil.getProperty(ADDRESS_SERVER_RETRY_PROPERTY, Integer.class, DEFAULT_SERVER_RETRY_TIME);
for (int i = 0; i < maxRetry; i++) {
try {
syncFromAddressUrl();
success = true;
break;
} catch (Throwable e) {
ex = e;
Loggers.CLUSTER.error("[serverlist] exception, error : {}", ExceptionUtil.getAllExceptionMsg(ex));
}
}
if (!success) {
throw new NacosException(NacosException.SERVER_ERROR, ex);
}
GlobalExecutor.scheduleByCommon(new AddressServerSyncTask(), DEFAULT_SYNC_TASK_DELAY_MS);
}
查看定时任务AddressServerSyncTask,核心也是执行syncFromAddressUrl方法。
class AddressServerSyncTask implements Runnable {
@Override
public void run() {
if (shutdown) {
return;
}
try {
syncFromAddressUrl();
} catch (Throwable ex) {
addressServerFailCount++;
if (addressServerFailCount >= maxFailCount) {
isAddressServerHealth = false;
}
Loggers.CLUSTER.error("[serverlist] exception, error : {}", ExceptionUtil.getAllExceptionMsg(ex));
} finally {
GlobalExecutor.scheduleByCommon(this, DEFAULT_SYNC_TASK_DELAY_MS);
}
}
}
查看核心的syncFromAddressUrl方法,该方法向地址服务器发送请求获取节点列表,然后调用afterLookup方法将节点列表存储到 ServerMemberManager 中。
private void syncFromAddressUrl() throws Exception {
RestResult<String> result = restTemplate
.get(addressServerUrl, Header.EMPTY, Query.EMPTY, genericType.getType());
if (result.ok()) {
isAddressServerHealth = true;
Reader reader = new StringReader(result.getData());
try {
afterLookup(MemberUtil.readServerConf(EnvUtil.analyzeClusterConf(reader)));
} catch (Throwable e) {
Loggers.CLUSTER.error("[serverlist] exception for analyzeClusterConf, error : {}",
ExceptionUtil.getAllExceptionMsg(e));
}
addressServerFailCount = 0;
} else {
addressServerFailCount++;
if (addressServerFailCount >= maxFailCount) {
isAddressServerHealth = false;
}
Loggers.CLUSTER.error("[serverlist] failed to get serverlist, error code {}", result.getCode());
}
}
afterLookup方法
不同的寻址模式都会调用afterLookup方法将节点信息存储到ServerMemberManager中,核心方法为ServerMemberManager类中的memberChange方法。
synchronized boolean memberChange(Collection<Member> members) {
if (members == null || members.isEmpty()) {
return false;
}
//首先判断是否包含本地地址,不包含的话就将本地地址加入节点列表。
boolean isContainSelfIp = members.stream()
.anyMatch(ipPortTmp -> Objects.equals(localAddress, ipPortTmp.getAddress()));
if (isContainSelfIp) {
isInIpList = true;
} else {
isInIpList = false;
members.add(this.self);
Loggers.CLUSTER.warn("[serverlist] self ip {} not in serverlist {}", self, members);
}
//如果新旧集群个数不一致,则显示集群信息;如果集群数量相同,则比较是否有一个区别;如果存在差异,则集群节点将发生更改。涉及到所有收件人需要通知节点更改事件
// If the number of old and new clusters is different, the cluster information
// must have changed; if the number of clusters is the same, then compare whether
// there is a difference; if there is a difference, then the cluster node changes
// are involved and all recipients need to be notified of the node change event
boolean hasChange = members.size() != serverList.size();
ConcurrentSkipListMap<String, Member> tmpMap = new ConcurrentSkipListMap<>();
Set<String> tmpAddressInfo = new ConcurrentHashSet<>();
for (Member member : members) {
final String address = member.getAddress();
Member existMember = serverList.get(address);
if (existMember == null) {
hasChange = true;
tmpMap.put(address, member);
} else {
//to keep extendInfo and abilities that report dynamically.
tmpMap.put(address, existMember);
}
if (NodeState.UP.equals(member.getState())) {
tmpAddressInfo.add(address);
}
}
serverList = tmpMap;
memberAddressInfos = tmpAddressInfo;
Collection<Member> finalMembers = allMembers();
Loggers.CLUSTER.warn("[serverlist] updated to : {}", finalMembers);
// Persist the current cluster node information to cluster.conf
// <important> need to put the event publication into a synchronized block to ensure
// that the event publication is sequential
//如果节点列表有变更,就将新数据写入cluster.conf文件中,并发布MembersChangeEvent事件。
if (hasChange) {
MemberUtil.syncToFile(finalMembers);
Event event = MembersChangeEvent.builder().members(finalMembers).build();
NotifyCenter.publishEvent(event);
}
return hasChange;
}
小结
Nacos默认使用文件推荐使用地址服务器寻址,但是都还是属于人工操作,未来的可扩展点就是实现全自动的管理。