Nacos概念
在学习Dubbo过程中,发现了以Nacos最为注册中心的扩展,就打算学习一下。
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理
目前主要的用途还是注册的注册发现和服务配置
主要的架构图如下:
从上图中,可以看到几个信息:
1、nacos对外的通信,主要采用http接口来通信。同时也提供了SDK的方式,打开SDK的源码会发现,其实内部还是通过http来通信。
2、Config Service 配置中心服务 和 Naming Service 注册/发现服务,从这个命名来看,应该与RocketMq中的nameserver相似。这里可以推测到Naming Service也许也是类似mq中一样,对于服务的下线并不是非常的实时,需要在客户端做好容错和重试的功能。
模型
数据模型
Nacos 数据模型 Key 由三元组唯一确定, Namespace默认是空串,公共命名空间(public),分组默认是 DEFAULT_GROUP。
例如下图
服务领域模型
来自官网的图
Nacos 集群
nacos采用leader-follower集群的模式。leader节点承担事物请求,follower节点承担非事物请求。当事物请求到follower节点时,转发到leader节点。在nacos中节点有3种状态:
- leader 领导
- follower 跟随
- candidate 候选人
nacos中分为2种实例,临时实例和持久实例,分别支持AP和CP 2种模式,本文主要讲由raft算法完成的CP。
CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
CAP原则的精髓就是要么AP,要么CP,要么AC,但是不存在CAP。如果在某个分布式系统中数据无副本, 那么系统必然满足强一致性条件, 因为只有独一数据,不会出现数据不一致的情况,此时C和P两要素具备,但是如果系统发生了网络分区状况或者宕机,必然导致某些数据不可以访问,此时可用性条件就不能被满足,即在此情况下获得了CP系统,但是CAP不可同时满足。
数据提交投票
在raft 算法中,数据同步需要遵循 遵循过半2pc原则,2阶提交。
主 -> 从 写入其他follower的日志里面
主 <- 从 收到来自follower的确认可以写入(过半同意包括自己) 同时主节点回复写入成功
主 -> 从 commit结果
虽然,在nacos的源码中并没有采用2阶过半提交,而是直接发送给follower数据同步,接结束了。但是过半的2pc在分布式中是非常常见的,被广泛接受这种数据同步,如zk同步数据,redis sentinel 客观下线。
这里也许会有疑问,那支持的follower提交了数据,那些不支持的follower怎么保持数据保持一致。一般在follower和leader有一个心跳检测,检测存活和数据的一致性,当发现数据不一致时,leader会向该follower同步数据。
选举投票 & raft算法
类似这种分布式协调的框架,都存在一个2个投票。一个是数据同步时的过半投票,一个就是选举leader时的投票。nacos采用raft算法来完成,这里有2个网站,讲的非常的详细
thesecretlivesofdata.com/raft/ 动画演示网站
niceaz.com/2018/11/03/… raft和zab的区别
尤其是第二个文章,我觉得讲的非常的清晰和有自己的理解。这里就不做多于的描述了,感觉怎么写也不如上面的写的好。
Naming Service
nacos采用定时任务和HTTP请求,来维持消费端和服务端的心跳检测。服务的管理中,无论是在哪个中间件,都非常的关注动态感知服务的上下线。
服务端
服务端通过openapi,向nacos进行服务的注册,同时在在本地开启一个延迟任务BeatTask,来完成后续的心跳检测。当BeatTask 触发时,会向nacos进行心跳的检测,同时若发现在nacos并没有这个服务,则会注册一个服务,然后再最后,会生成一个新的延迟任务BeatTask 用来继续下一次的心跳检测。这个操作很像在zk中的watch机制,watch机制只能监听触发一次,为了能够一直监听,会在一次监听的最后,重新注册上watch。
这样的原因为了防止,因为网络的延迟,导致在上一次心跳检测未完成时,就触发了下一次心跳检测。
消费端
对于消费端的操作,就比较的奢华,采用了pull和push2种机制来维护服务信息。在服务端通过api拉取服务信息时,开启延迟任务UpdateTask定时来更新和拉取订阅的服务信息默认10秒一次,拉取保存到本地内存中。在拉取的同时,本地开启UDP端口,并把端口号发送给nacos,通过PushReceiver中的PushReceiver线程池来完成,其中使用while循环来等待UDP的推送信息。当远程服务变化时,nacos通过UDP的端口来通知消费者。
但是由于UDP的不可靠性,故需要pull和push2种机制来搭配完成。
服务注册
下面为源码中实现部分
API
这些部分是在服务者方发生的,在nacos使用中会有一些中间的包,来提供nacos和spring框架等桥接,这里就不阐述了,就是一些自动装配类
这里直接从Nacos的SDK的注册入口看NamingService#registerInstance
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
// 获取完成服务名称
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
// 判断是否是临时节点,对应的nacos的CAP模型不同,临时节点为AP,持久化节点为CP
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
// 开启心跳检测
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
// 通过api注册,内部就是一个封装好的http请求
serverProxy.registerService(groupedServiceName, groupName, instance);
}
Nacos端
由 InstanceController#register 收到请求信息,然后ServiceManager#registerInstance处理
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
// 创建一个空服务
// 初始化集合
// 初始化服务提供信息
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
Service service = getService(namespaceId, serviceName);
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
// 把客户端传递过来的实例信息,
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
Service service = getService(namespaceId, serviceName);
synchronized (service) {
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
Instances instances = new Instances();
instances.setInstanceList(instanceList);
// 在CP模式下这里是RaftConsistencyServiceImpl,去把本次注册的服务,发布给其他的follower节点
consistencyService.put(key, instances);
}
}
通过RaftCore#signalPublish向其他节点同步数据,
....
// 这里为遍历nacos的节点,通过api同步数据,但是看到这里并没有遵守2pc的原则,但是保留了term 在RaftPeerSet中,故在心跳检测中,会向低term的节点同步数据
for (final String server : peers.allServersIncludeMyself()) {
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;
}
});
}
...
服务发现
API
入口为NacosNamingService#getAllInstances
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,
boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
if (subscribe) {
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
} else {
serviceInfo = hostReactor
.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
}
List<Instance> list;
if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
return new ArrayList<Instance>();
}
return list;
}
这里主要看一下订阅环境下的情况, HostReactor#getServiceInfo
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
String key = ServiceInfo.getKey(serviceName, clusters);
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
// 拉取服务信息
if (null == serviceObj) {
serviceObj = new ServiceInfo(serviceName, clusters);
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
updatingMap.put(serviceName, new Object());
// 立即拉取服务信息,并向nacos发送udp端口
updateServiceNow(serviceName, clusters);
updatingMap.remove(serviceName);
} else if (updatingMap.containsKey(serviceName)) {
// 防止同一时间更新同一个服务,个人感觉这里可以用featuretask的缓存来写,可以更优雅一点
if (UPDATE_HOLD_INTERVAL > 0) {
// hold a moment waiting for update finish
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER
.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
}
}
}
}
scheduleUpdateIfAbsent(serviceName, clusters);
return serviceInfoMap.get(serviceObj.getKey());
}
服务下线
PushReceiver UDP接受nacos的推送
while (!closed) {
try {
// byte[] is initialized with 0 full filled by default
byte[] buffer = new byte[UDP_MSS];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// 当没有收到请求时,这里会阻塞
udpSocket.receive(packet);
String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
String ack;
if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
hostReactor.processServiceJson(pushPacket.data);
// send ack to server
ack = "{\"type\": \"push-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime + "\", \"data\":"
+ "\"\"}";
} else if ("dump".equals(pushPacket.type)) {
// dump data to server
ack = "{\"type\": \"dump-ack\"" + ", \"lastRefTime\": \"" + pushPacket.lastRefTime + "\", \"data\":"
+ "\"" + StringUtils.escapeJavaScript(JacksonUtils.toJson(hostReactor.getServiceInfoMap()))
+ "\"}";
} else {
// do nothing send ack only
ack = "{\"type\": \"unknown-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\"\"}";
}
udpSocket.send(new DatagramPacket(ack.getBytes(UTF_8), ack.getBytes(UTF_8).length,
packet.getSocketAddress()));
} catch (Exception e) {
NAMING_LOGGER.error("[NA] error while receiving push data", e);
}
}
比较简单,就不叙述了
Config Service
下期在写吧。。。。