目录
1. 什么是寻址机制
2. 源码讲解
MemberLookup
AbstractMemberLookup
2.1 单机寻址
2.2. 文件寻址
2.3 地址服务器寻址
1. 什么是寻址机制
假设存在一个 Nacos 集群,其内部具有 A , B , C 三个节点。
客户端如何决定向集群中的哪个节点发送请求
在 application.yml 配置中,会列出 Nacos 集群的全部节点地址信息。
spring:
cloud:
nacos:
discovery:
username: nacos
password: nacos
server-addr: 127.0.0.1:8848,127.0.0.1:8850,127.0.0.1:8852
在最终实际发送请求时,会将 127.0.0.1:8848,127.0.0.1:8850,127.0.0.1:8852
会解析为 List<String> servers,然后随机选择一个节点发送请求。
public String reqApi(String api, Map<String, String> params, Map<String, String> body, List<String> servers,String method) throws NacosException {
// 省略非关键代码
Random random = new Random();
// 随机选择一个节点
int index = random.nextInt(servers.size());
String server = servers.get(index);
return callServer(api, params, body, server, method);
}
现有如下客户端请求
客户端 E 向 集群节点 A 发送一个注册服务的请求。
随后客户端 F 向 集群节点 B 发送一个服务列表查询请求。
此时整个系统有问题呀,明明已经有了注册服务,但是后面的请求反没有返回服务。
我们的目的就是让客户端能够拿到整个集群的数据。
第一种方式:增加访问前置路由
也就是说,客户端通过一个前置路由的角色将请求定位到正确的节点上然后返回数据。
这种方式很常用在数据分片上,即每个节点只存储整个集群的部分数据。
前置路由容易成为系统瓶颈。
第二种方式:每个节点存储全量数据
这样处理后,每个节点都可以独立完成工作,直接处理请求。
这就要求,当一个节点接受到注册请求后需要将信息同步到集群里的全部节点上。
那么客户端如何知道其他节点呢?这就是本节要探讨的主题:寻址机制。
寻址机制即 集群中的节点如何感知彼此
目前 Nacos 中,集群寻址有 文件 和 地址服务器 两种方式。而由于还存在单机模式,也相应的有个单机寻址方式。
单机寻址:自己就是节点
文件寻址:读取 cluster.conf 文件,获得节点信息
地址服务器寻址:请求地址服务器,通过接口返回节点信息
2. 源码讲解
MemberLookup
寻址机制的顶层接口为 com.alibaba.nacos.core.cluster.MemberLookup,代表一个寻址机制
public interface MemberLookup {
// 省略非关键代码
// 启动寻址
void start() throws NacosException;
// 找到节点后 调用该方法将节点传进来
void afterLookup(Collection<Member> members);
// 销毁寻址
void destroy() throws NacosException;
}
AbstractMemberLookup
整个寻址类图如上。
AbstractMemberLookup 作为一个抽象类实现了 MemberLookup 接口,完成了一些通用功能供子类使用。
public abstract class AbstractMemberLookup implements MemberLookup {
protected ServerMemberManager memberManager;
protected AtomicBoolean start = new AtomicBoolean(false);
@Override
public void injectMemberManager(ServerMemberManager memberManager) {
this.memberManager = memberManager;
}
@Override
public void afterLookup(Collection<Member> members) {
this.memberManager.memberChange(members);
}
@Override
public void destroy() throws NacosException {
if (start.compareAndSet(true, false)) {
doDestroy();
}
}
@Override
public void start() throws NacosException {
if (start.compareAndSet(false, true)) {
doStart();
}
}
// 供子类重写
protected abstract void doStart() throws NacosException;
// 供子类重写
protected abstract void doDestroy() throws NacosException;
}
该类完成了start、destory 时 start 变量状态的维护,以及当子类找到集群节点时会调用 父类 AbstractMemberLookup 的afterLookup 方法,该方法通过调用 memberManager.memberChange(members); 完成集群节点变更。
2.1 单机寻址
单机模式,配置 JVM 虚拟机参数 -Dnacos.standalone=true 即可触发单机寻址。
源码
public class StandaloneMemberLookup extends AbstractMemberLookup {
@Override
public void doStart() {
String url = EnvUtil.getLocalAddress();
// 将自己封装为节点 Member 对象调用告知父类
afterLookup(MemberUtil.readServerConf(Collections.singletonList(url)));
}
@Override
protected void doDestroy() throws NacosException {
}
@Override
public boolean useAddressServer() {
return false;
}
}
可以看出,单机寻址,就是找到自己就行了。
2.2. 文件寻址
文件寻址即通过 cluster.conf
文件来发现其他节点,cluster.conf 为 Nacos 的集群配置文件,文件内容就是其他集群中其他节点的地址信息
配置 JVM 虚拟机参数 -Dnacos.home=xxx,需要保证文件在 xxx/conf/cluster.conf
源码
public class FileConfigMemberLookup extends AbstractMemberLookup {
private static final String DEFAULT_SEARCH_SEQ = "cluster.conf";
// 文件变动监听器
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);
}
};
@Override
public void doStart() throws NacosException {
readClusterConfFromDisk();
// 注册文件监听器
WatchFileCenter.registerWatcher(EnvUtil.getConfPath(), watcher);
}
@Override
protected void doDestroy() throws NacosException {
// 取消注册
WatchFileCenter.deregisterWatcher(EnvUtil.getConfPath(), watcher);
}
// 从磁盘中读取 cluster.conf 文件内容
private void readClusterConfFromDisk() {
Collection<Member> tmpMembers = new ArrayList<>();
List<String> tmp = EnvUtil.readClusterConf();
tmpMembers = MemberUtil.readServerConf(tmp);
// 通知父类找到了节点信息
afterLookup(tmpMembers);
}
}
首先 Nacos 启动的时候会读取一次硬盘里 cluster.conf 文件内容。
然后向操作系统的 _inotify_
机制注册一个目录监听器,对${NacosHome}/conf 下的文件监听变化情况,当产生变化了,会再次触发读取硬盘 cluster.conf 文件内容。
2.3 地址服务器寻址
当节点需要收缩扩容的时候,需要手动修改 cluster.conf 的 成员节点列表内容。集群节点少了还好,多了话就很费事费力,不易维护了。
地址服务器寻址就是 将节点信息存储在一个 web 服务器上,节点通过 HTTP 访问 Web 服务器 URL 获取节点信息。
每个节点定期向该web服务器请求cluster.conf的文件内容,来实现集群间的寻址、扩容、收缩。
服务器去哪找呢,我们自己能不能写个接口给用呢?
其实nacos已经实现了, nacos-address
模块。
其实 nacos-address 也是一个 nacos 节点,我们用单机模式启动一个address-server
我们先请求接口注册一个节点。
由于地址服务器需要授权登录,但是这里我暂时没搞懂,所以先做一个魔改。
这里改为 permitAll() 即直接放行。
然后注册节点。直接生成 HTTP 调用代码
POST http://localhost:8080/nacos/v1/as/nodes?product=life&cluster=default&ips=127.0.0.1
其中要填写产品名、集群名、还有注册的ip列表(逗号分隔)
这里我随便写了个,运行,然后注册成功。
接下来验证下获取节点,看能不能获取到。
这个接口要传两个路径参数,就是注册时填的 product 和 集群。然后请求发现了返回成功。
然后是以 地址服务器寻址方式启动 Nacos
节点启动配置项:(JVM 参数)
-Dnacos.home=D:\NacosClusterHome\NacosHome8848 // 配置 nacos home -Dnacos.core.member.lookup.type=address-server // 配置寻址方式为地址服务器寻址 -Daddress.server.domain=127.0.0.1 // 地址服务器域名 -Daddress.server.port=8080 // 地址服务器端口 -Daddress.server.url=/life/default // 服务列表接口
配置好后,启动节点,进入控制台,观察集群节点信息,发现可以看到刚才注册的节点信息。
源码
public class AddressServerMemberLookup extends AbstractMemberLookup {
// 省略非关键代码
// 同步任务延迟毫秒 5000 毫秒 = 5 秒
private static final long DEFAULT_SYNC_TASK_DELAY_MS = 5_000L;
@Override
public void doStart() throws NacosException {
run();
}
private void run() throws NacosException {
// 从地址服务器URL同步数据
syncFromAddressUrl();
// 创建定时任务,定期同步数据
GlobalExecutor.scheduleByCommon(new AddressServerSyncTask(), DEFAULT_SYNC_TASK_DELAY_MS);
}
// 从地址服务器URL同步数据
private void syncFromAddressUrl() {
// 调用地址服务器接口
RestResult<String> result = restTemplate
.get(addressServerUrl, Header.EMPTY, Query.EMPTY, genericType.getType());
// 读取接口返回值
Reader reader = new StringReader(result.getData());
// 通知父类找到节点信息了
afterLookup(MemberUtil.readServerConf(EnvUtil.analyzeClusterConf(reader)));
}
// 同步任务
class AddressServerSyncTask implements Runnable {
@Override
public void run() {
syncFromAddressUrl();
// 同步完成后再创建下一次的定时同步任务
GlobalExecutor.scheduleByCommon(this, DEFAULT_SYNC_TASK_DELAY_MS);
}
}
}
关键代码如上
本质还是通过调用接口获取节点信息,然后又创建了定时任务,定期同步数据,默认5秒。