0. 启动调试 console

  1. 加入 VM option:-Dnacos.standalone=true -Dnacos.functionMode=naming
  2. 启动 console 项目,查看 banner

1. service 与 instance 的逻辑关系

每个服务可以有多个实例,每个实例又区分为两种实例,一种为临时实例,一种是非临时实例,它们的关系在 ServiceManager 中保存,具体的 service、instance、cluster 类都在 com.alibaba.nacos.api.naming.pojo 包下,对外的接口主要是 service、instance 的 CRUD 操作。其中有个 cluster 的逻辑结构,属于 service 和 instanc 之间,对应的是某个 service 有多少个 instance,就有多少个 cluster

2. service 的操作

nacos 本身支持两种协议,一种是 Raft 一种是 Distro
而 service 创建操作使用了 Raft 协议

2.1 请求的是 Leader 节点时

先在本节点发布 service change 事件,接着通过 http 发送到其它节点,同步该服务创建操作,如果这个请求超过 5s,就认为该操作失败。里面使用了代理类 consistencyDelegate,除了临时节点,其它类型的一致性操作统一走 Raft 协议,最终会到 RaftCore.signalPublish() 方法,遍历其它注册中心节点,POST 请求 /v1/ns/raft/datum/commit 接口,该接口主要做了如下判断:对比请求节点的 term 是否比自己大,如果请求节点的 term 大于等于自己节点的 term ,就执行该任务,将服务信息同步到本节点

2.2 请求的是非 Leader 节点时

如果当前节点是非 leader,直接得到 leader 节点的 url,并请求 /v1/ns/raft/datum/ 接口,将 service 创建事件给 leader 处理。注意,本节点的 term 是没有变的,这种情况下 leader 接收到请求后 term 会增加,并同步到其它节点,其它节点判断 leader 的 term 和自己等于或大于时就执行该操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public void signalPublish(String key, Record value) throws Exception {
// 注释:步骤1 如果当前不是 leader,直接将该请求给 leader
if (!isLeader()) {
JSONObject params = new JSONObject();
params.put("key", key);
params.put("value", value);
Map<String, String> parameters = new HashMap<>(1);
parameters.put("key", key);

raftProxy.proxyPostLarge(getLeader().ip, API_PUB, params.toJSONString(), parameters);
return;
}

try {
OPERATE_LOCK.lock();
long start = System.currentTimeMillis();
final Datum datum = new Datum();
datum.key = key;
datum.value = value;
if (getDatum(key) == null) {
datum.timestamp.set(1L);
} else {
datum.timestamp.set(getDatum(key).timestamp.incrementAndGet());
}

JSONObject json = new JSONObject();
json.put("datum", datum);
json.put("source", peers.local());
// 注释:本节点发布 service 发布事件
onPublish(datum, peers.local());

final String content = JSON.toJSONString(json);

final CountDownLatch latch = new CountDownLatch(peers.majorityCount());
for (final String server : peers.allServersIncludeMyself()) {
// 注释:这里应该是避免特殊情况:步骤1 之前正在选举 leader,在这里之间选举出了 leader
if (isLeader(server)) {
latch.countDown();
continue;
}
final String url = buildURL(server, API_ON_PUB);
HttpClient.asyncHttpPostLarge(url, Arrays.asList("key=" + key), content, new AsyncCompletionHandler<Integer>() {
@Override
public Integer onCompleted(Response response) throws Exception {
if (response.getStatusCode() != HttpURLConnection.HTTP_OK) {
Loggers.RAFT.warn("[RAFT] failed to publish data to peer, datumId={}, peer={}, http code={}",
datum.key, server, response.getStatusCode());
return 1;
}
latch.countDown();
return 0;
}

@Override
public STATE onContentWriteCompleted() {
return STATE.CONTINUE;
}
});

}
// 注释:超时 5s 就算失败,但是其实本节点已经执行了 service 创建事件
if (!latch.await(UtilsAndCommons.RAFT_PUBLISH_TIMEOUT, TimeUnit.MILLISECONDS)) {
// only majority servers return success can we consider this update success
Loggers.RAFT.error("data publish failed, caused failed to notify majority, key={}", key);
throw new IllegalStateException("data publish failed, caused failed to notify majority, key=" + key);
}

long end = System.currentTimeMillis();
Loggers.RAFT.info("signalPublish cost {} ms, key: {}", (end - start), key);
} finally {
OPERATE_LOCK.unlock();
}
}

3. instance 的操作

instance 增加时,如果没有对应的 service,会默认创建该 service,如果已经有了 service ,会直接塞入到该 service 的 instanceList 中,具体细节在 ServiceManager 中进行操作。
接着判断增加的 instance 是否临时来使用不同的一致性协议,如果为临时实例,使用 distro 协议,如果非临时实例,使用 raft 协议。distro 协议大致为定时任务广播其它节点+保存内存,其中广播的接口为 /distro/dump。

distro 协议为自制协议,AP

4. 代码地址

https://github.com/LiWenGu/nacos.git

心跳和选举主要依靠容器启动时的两个定时任务,分别为 HeartBeat 和 MasterElection 两个。这两个定时任务,每 0.5s 执行一次。
当一段时间内(15~20s)没有接受到心跳,就会执行 MasterElection 里面的选举请求逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 注释:master 选举
GlobalExecutor.registerMasterElection(new MasterElection());
// 注释:节点间心跳
GlobalExecutor.registerHeartbeat(new HeartBeat());

public class MasterElection implements Runnable {
@Override
public void run() {
try {

if (!peers.isReady()) {
return;
}

RaftPeer local = peers.local();
// 注释:每次递减0.5s,直到小于0.5s,初始是15~20s的范围,这个任务0.5s执行一次,说明选举在没心跳最坏的情况
// 是15~20s进行一次选举,即发投票。但是心跳任务每次都会重置这个 leaderDueMs 为 15s
local.leaderDueMs -= GlobalExecutor.TICK_PERIOD_MS;

if (local.leaderDueMs > 0) {
return;
}

// reset timeout
// 注释:重置选举时间间隔
local.resetLeaderDue();
// 注释:重置心跳时间间隔为5s
local.resetHeartbeatDue();

sendVote();
} catch (Exception e) {
Loggers.RAFT.warn("[RAFT] error while master election {}", e);
}

}
}

public class HeartBeat implements Runnable {
@Override
public void run() {
try {

if (!peers.isReady()) {
return;
}

RaftPeer local = peers.local();
// 注释:5s以上发一次心跳(初始随机0~5s)
local.heartbeatDueMs -= GlobalExecutor.TICK_PERIOD_MS;
if (local.heartbeatDueMs > 0) {
return;
}
// 注释:重置心跳间隔
local.resetHeartbeatDue();
// 注释:发送心跳,里面重置了 leaderDueMs 时间,即当心跳一直正常时,不会发起master选举
sendBeat();
} catch (Exception e) {
Loggers.RAFT.warn("[RAFT] error while sending beat {}", e);
}

}
}

参考了更加细节的文章:
文章一:心跳的源码细节:https://www.jianshu.com/p/b0cdaa64688e
文章二:选举的源码细节:https://www.jianshu.com/p/5a2d965174ae

代码地址:https://github.com/LiWenGu/nacos.git

1. 本地编译

  1. fork 源地址:https://github.com/alibaba/nacos.git
  2. git clone 自己 fork 版本
  3. 现在基于 1.1.3 版本,nacos 项目基于标签做的版本发布,因此直接 git checkout 1.1.3 然后再基于此新建一个分支,用于源码的修改等
  4. 在根目录下运行:mvn clean -Dmaven.test.skip=true,下载相关依赖包,检查环境
  5. 运行 console 项目主类,加上 VM options:-Dnacos.standalone=true

  6. 我们可以在控制台查看 banner 的相关运行信息

2. 修改字符分隔符方便调试

因为报文,使用了特殊的字符,不方便 http 测试请求,因此修改 com.alibaba.nacos.config.server.utils 包下的 MD5Util 类:

1
2
WORD_SEPARATOR_CHAR = '~'
LINE_SEPARATOR_CHAR = '*'

3. 发布订阅者模式

配置管理的实现,使用了大量的 ScheduledExecutorService 以及事件发布订阅者模式

  1. 事件分发器/发布者:com.alibaba.nacos.config.server.utils.event.EventDispatcher
  2. 两个监听者:
    AsyncNotifyService 监听 ConfigDataChangeEvent 事件,当配置做更新操作时,进行同步并异步做 http 请求到其它的健康节点
    LongPollingService 监听 LocalDataChangeEvent 事件,当配置做更新操作时,一是更新 CacheItem(内存级),二是对当前监听的客户端们做响应
  3. 两种事件:
    ConfigDataChangeEvent:需要节点间同步配置改动的事件
    LocalDataChangeEvent:本节点配置改动的事件

4. 数据库访问数据

单机版使用 derby 嵌入式数据库,只能由一个进程访问,查看数据需要停止 Nacos 进程才行,稍微麻烦点,不过 idea 支持 derby 数据查看,会方便点:

数据库的具体信息,从 LocalDataSourceServiceImpl 可以查看,包括数据存放地址等,这是可以通过全局配置来设置的

url:/Users/{user.home}/nacos/data/derby-data
username:nacos
password:空

后台 console 的账号和密码根据配置文件可以查看:console/src/main/resources/META-INF/schema.sql,如果想修改,因为还有一个 Role 表,需要改动 role 和 user 表

5. 配置接口

5.1 配置的监听

客户端监听注册中心的配置,其实本质是根据 groupKey(AppId+groupId+namespace)获取对应的 md5,即配置内容的 md5 进行对比是否有更新。默认等待 30s,即是否在 30s 内有改动。

配置的改动监听有三种场景:A 表示客户端,B表示注册中心

  1. A->B,其实A的配置很老了,B的配置是最新的,B的监听接口会先根据本节点的 CacheItem 内存的值进行匹配,如果匹配失败,说明A 的值是旧的,会立刻返回给 A groupKey(不是md5或内容),让A去查询最新的配置值(多此一举么~)
  2. A->B,其实A的配置就是最新的,B的配置和A的配置一样,那么 A 请求阻塞 30s(默认Long-Pulling-Timeout配置),B的监听接口使用 Servlet 3.0 异步进行 10s 一次的循环,来根据 groupKey 拿配置的md5,进行对比是否改动,如果改动则返回,不改动理论上会等待30s。
  3. A->B,属于情况2的变种,就是,在情况2等待30s过程中,B2配置改变了,那么会根据事件分发以及 http 内部节点之间请求,B2会通知B,B最后来通知A,此时A会立刻返回

5.2 配置的修改

post/put/update/delete 等情况,这里有两个要注意,一是自己节点的内存更新,和其它节点的通知更新

  1. 自己节点:先存数据库 derby,然后做事件 ConfigDataChangeEvent 的发送,而这个事件会被 AsyncNotifyService 监听到,这个 AsyncNotifyService 会根据节点的健康情况发送给其它的对等节点(B1,B2,B3),这个是通过内部接口 communication 来发送请求的
  2. 其它节点:得到http请求后,这个communication接口做了两件事,一是 dump:即根据是否是集群模式,如果是集群模式来将配置信息写入到硬盘(TODO:为什么硬盘?不写MYSQL吗?),二是 TaskMgr 0.1s 死轮询关键任务,这个关键任务就包括的事件改动的通知,这个任务内部做了 CacheItem 刷本节点的内存最新值,二是发布 LocalDataChangeEvent 事件给 LongPollingService 监听,其中 LongPollingService 内部会做 DataChangeTask 的任务来让响应 ClientLongPolling.sendResponse() 方法,从而让监听者即时返回 asyncContext.complete()
    修改会有点绕,因为里面的发布订阅和定时任务太多了

5.3 配置的删除

  1. 数据库物理删除
  2. 发布 ConfigDataChangeEvent 事件

5.4 配置的获取

  1. 直接获取本节点的 CacheItem 值,这个是 concurrenthashmap 类型,存储在内存中

5.5 配置的整体流程


https://github.com/LiWenGu/nacos.git
https://www.processon.com/view/link/5d441f3fe4b0bc1bbedcf559

6. 代码地址

https://github.com/LiWenGu/nacos.git

除非你觉得你的时间不是很宝贵,否则不要看这篇流水账式的博文,这只是篇个人的工作的学习一个总结而已,没有包含任何的技术细节
阅读全文 »

除非你觉得你的时间不是很宝贵,否则不要看这篇流水账式的博文,这只是篇个人的工作的学习一个总结而已,没有包含任何的技术细节
阅读全文 »

除非你觉得你的时间不是很宝贵,否则不要看这篇流水账式的博文,这只是篇个人的工作的学习一个总结而已,没有包含任何的技术细节
阅读全文 »

除非你觉得你的时间不是很宝贵,否则不要看这篇流水账式的博文,这只是篇个人的工作的学习一个总结而已,没有包含任何的技术细节
阅读全文 »

除非你觉得你的时间不是很宝贵,否则不要看这篇流水账式的博文,这只是篇个人的工作的学习一个总结而已,没有包含任何的技术细节
阅读全文 »

除非你觉得你的时间不是很宝贵,否则不要看这篇流水账式的博文,这只是篇个人的工作的学习一个总结而已,没有包含任何的技术细节
阅读全文 »