nacos原理三-注册中心原理&源码启动.md

1. nacos 服务端源码启动

资源信息:

操作系统:mac
JDK: 8
nacos: 1.1.4 (2.2.1 版本需要protobuf, 插件比较麻烦就放弃了)
  1. 下载项目

选择 1.1.4 版本

  1. mvn 编译所有项目
  2. 创建数据库:nacos/distribution/conf/nacos-mysql.sql
  3. 修改数据库连接信息:console/src/main/resources/application.properties
# mysql datasource
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos
db.user=root
db.password=
  1. 修改主类设置单机启动:

console/src/main/java/com/alibaba/nacos/Nacos.java

System.setProperty("nacos.standalone", "true");

2. 关于nacos 简单理解

1. 数据存储

nacos 简单分为注册中心、配置中心。

配置中心相关数据会落库(内置derby,支持切换为mysql, 策略模式切换), 注册中心的数据不会落库。

2. 注册中心模块集群间数据同步

内存存储,当前内存增加之后,同步给集群中的其他兄弟节点。

// 大概两种实现:distro(异步发送同步)、raft(同步发送,需要半数节点进行ACK)

  1. 平等节点:一个名为Distro的一致性协议算法, 当前注册完之后同步给其他兄弟节点

com.alibaba.nacos.naming.consistency.ephemeral.distro.DistroConsistencyServiceImpl#put

数据分区:
	Distro算法将数据分割成多个块。
	每个Nacos服务器节点负责存储和管理一个特定的数据块。
责任分配:
	每个数据块的生成、删除和同步操作都由负责该块的服务器执行。
	这意味着每个Nacos服务器仅处理总服务数据子集的写操作,降低了单个节点的压力。
数据同步:
	所有的Nacos服务器同时接收其他服务器的数据同步。
	通过这种方式,随着时间的推移,每个服务器都将最终拥有完整的数据集。
	这确保了即使在分布式环境中,数据的一致性和可用性。

这种设计有助于提高系统的可扩展性和容错性,因为任何单个节点的故障都不会导致整个数据集的丢失,而且可以并行处理数据操作以提高性能。
  1. 叫做Raft 协议的一致性: 从集群中选一个leader 节点,节点就分为主节点和follower节点

com.alibaba.nacos.naming.consistency.persistent.raft.RaftConsistencyServiceImpl#put

在Raft算法中,只有Leader才拥有数据处理和信息分发的权利。因此当服务启动时,如果注册中心指定为Follower节点,则步骤如下:

1、Follower 会自动将注册心跳包转给 Leader 节点;
2、Leader 节点完成实质的注册登记工作;
3、完成注册后向其他 Follower 节点发起“同步注册日志”的指令;
4、所有可用的 Follower 在收到指令后进行“ack应答”,通知 Leader 消息已收到;
5、当 Leader 接收过半数 Follower 节点的 “ack 应答”后,返回给微服务“注册成功”的响应信息。
此外,如果某个Follower节点无ack反馈,Leader也会不断重复发送,直到所有Follower节点的状态与Leader同步为止。

3. 鉴权

使用过滤器进行前置校验,包括如果UA包含Nacos-Server 直接放行,都是 com.alibaba.nacos.core.auth.AuthFilter#doFilter 处理的。

鉴权方式: 简单的基于登录状态+权限的校验

转交给鉴权模块com.alibaba.nacos.plugin.auth.impl.NacosAuthPluginService

  1. 校验账号密码合法性:判断传的账号密码是否合法, jwt 生成token, 校验token 合法性
  2. 校验权限: 判断用户是否有对应的权限码
  3. 如果是python、sdk,请求的参数会携带账号密码。 如果是java sdk 会调用自己的登录接口获取到token,之后每次访问请求参数携带token。

3. Nacos-sdk-python服务注册相关API

​ 服务注册相关的API 主要涉及三个,服务注册、心跳、获取服务。

​ 如果自己实现注册中心也是重点实现这三个接口即可。 对实例的负载、缓存等是在SDK 做的。(如果需要鉴权,还需要实现一个登录界面,登录之后生成token 返回给sdk, sdk 每次请求会在参数携带token)

  1. 注册
uri: http://localhost:8848/nacos/v1/ns/instance?username=XXX&password=lbg-nacosxxx'
method: POST
data:b'ip=127.0.0.1&port=8848&serviceName=qlq_cus&weight=1.0&enable=True&healthy=True&clusterName=None&ephemeral=True&groupName=DEFAULT_GROUP&metadata=%7B%22token%22%3A+%2220e05776c4b810ac9557c7ce081dfe32%22%7D'

对应java 接口:
com.alibaba.nacos.naming.controllers.InstanceController#register
  1. 心跳:
uri: http://127.0.0.1:8848/nacos/v1/ns/instance/beat?serviceName=qlq_cus&beat=%7B%22serviceName%22%3A+%22qlq_cus%22%2C+%22ip%22%3A+%22127.0.0.1%22%2C+%22port%22%3A+%228848%22%2C+%22weight%22%3A+1.0%2C+%22ephemeral%22%3A+true%7D&groupName=DEFAULT_GROUP&username=XXX&password=lbg-nacosxxx
	解码后:
http://127.0.0.1:8848/nacos/v1/ns/instance/beat?serviceName=qlq_cus&beat={"serviceName": "qlq_cus", "ip": "127.0.0.1", "port": "8848", "weight": 1.0, "ephemeral": true}&groupName=DEFAULT_GROUP&username=XXX&password=lbg-nacosxxx
method: PUT

对应java 接口:
com.alibaba.nacos.naming.controllers.InstanceController#beat
  1. 获取服务:
uri: 'http://localhost:8848/nacos/v1/ns/instance/list?serviceName=qlq_cus&healthyOnly=False&namespaceId=public&groupName=DEFAULT_GROUP'
method: GET

对应java 接口:
com.alibaba.nacos.naming.controllers.InstanceController#list

4. nacos 数据同步原理-CP|AP

​ nacos 有AP\CP, 默认是AP,也就是默认走distro 协议,可以通过配置设置为走Raft 协议来满足CP 原则。

1. distro 协议 -AP (默认)

​ 核心参考: com.alibaba.nacos.core.distributed.distro.DistroProtocol

​ 自研的一致性协议,主要用于在集群节点之间实现数据的快速同步和一致性保证。 思想是实现AP,节点分数据处理,也就是一个节点处理部分数据。 处理完之后通过异步任务,同步到其他节点,趋向于最终一致性。

思路:读请求所有节点都可以处理;写请求是根据service_name 或者 实例信息(ip:port) 进行分片,分到对应的节点进行处理,节点处理完异步通知其他节点进行同步。

1. com.alibaba.nacos.naming.web.DistroFilter#doFilter
(1). 判断是否能处理响应: 根据服务名称或者ip加端口获取服务下标
int index = distroHash(responsibleTag) % servers.size();
return servers.get(index);

private int distroHash(String responsibleTag) {
	return Math.abs(responsibleTag.hashCode() % Integer.MAX_VALUE);
}
(2). 如果是当前服务自己,放行到后面的处理过程
(3). 否则拿到请求参数和请求头,http 调用到指定的机器进行处理
2. com.alibaba.nacos.naming.core.InstanceOperatorServiceImpl#registerInstance 处理注册逻辑
(1). 内部处理相关的注册逻辑
(2). 异步调用其他兄弟节点进行同步数据:com.alibaba.nacos.core.distributed.distro.DistroProtocol#sync
    public void sync(DistroKey distroKey, DataOperation action, long delay) {
        for (Member each : memberManager.allMembersWithoutSelf()) {
            syncToTarget(distroKey, action, each.getAddress(), delay);
        }
    }

大致过程:

  1. 初始化: 节点启动时初始化distro 相关组件,包含数据同步处理器以及服务监听器。
  2. 全量同步: 新加入集群的节点会请求全量数据,其他节点发送全量数据; 新节点收到全量数据后,进行校验和处理
  3. 增量同步:全量同步完成后,后续的同步是基于增量同步,只同步新产生的数据。 每个节点定期向其他节点发送增量请求,同步到本地进行处理
  4. 心跳与确认:节点之前定时发送心跳,以检测对方的存活状态

2. Raft 协议 - CP

核心参考: com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore

思想: 有一个主节点,所有的写请求打到主节点。主节点然后同步阻塞将数据同步给其他节点,如果半数以上成功,就代表写入成功; 否则写入失败会抛出非法状态异常,可能就需要排查nacos 集群是否正常。

核心思想:

  1. 领导选举:

(1). Raft 系统中的节点在任何时候都处于三种状态之一:领导者(Leader)、跟随者(Follower)或候选者(Candidate)。
(2). 当系统启动或领导者失效时,节点会转换为候选者状态并发起选举,通过请求投票(RequestVote) RPC 向其他节点寻求支持。
(3). 基于任期(Term)的概念,每个选举都有一个独一无二的任期号,确保过时的投票无效,防止选票分裂。
(4). 获得大多数节点支持的候选者将成为新的领导者,负责处理客户端请求和管理日志复制
简单理解:

  • 每个节点有一个随机等等时间,当集群暂未选出主节点或者没收到主节点心跳,且自己的随机等待时间到期之后,把自己的角色设为CANDIDATE、然后给自己投一票,然后向兄弟节点收集投票结果,假设B先到期 (com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore.MasterElection#sendVote);
  • 兄弟节点A收到后和自己的票数term 比较,如果小于A当前的票数,那A投给A自己; 否则,A把自己的角色改为FOLLOWER 从节点,然后把自己的票数terms 设为和B节点的一样、重置自己的等待时间(com.alibaba.nacos.naming.controllers.RaftController#vote)
  • B每次收集完结果之后,判断票数最多且超过半数,那么就可以设为leader 主节点, 这是B已经知道自己是主节点
  • 主节点每次心跳会将自己的信息同步给兄弟节点(只有主节点会发心跳给其他节点,其他节点记录主节点等信息-com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore.HeartBeat#sendBeat)。
  1. 数据同步
    (1). 所有的写请求会到达leader 节点

(2). leader 同步将数据发给其他兄弟节点,超过半数回复正常才会认为写入成功; 否则会抛出非法状态异常,这时候可能需要排查nacos 集群状态

参考: com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore#signalPublish

    public void signalPublish(String key, Record value) throws Exception {
        if (stopWork) {
            throw new IllegalStateException("old raft protocol already stop work");
        }
      	// 不是主节点,就转发到主节点
        if (!isLeader()) {
            ObjectNode params = JacksonUtils.createEmptyJsonNode();
            params.put("key", key);
            params.replace("value", JacksonUtils.transferToJsonNode(value));
            Map<String, String> parameters = new HashMap<>(1);
            parameters.put("key", key);
            
            final RaftPeer leader = getLeader();
            
            raftProxy.proxyPostLarge(leader.ip, API_PUB, params.toString(), parameters);
            return;
        }
        
        OPERATE_LOCK.lock();
        try {
            final 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());
            }
            
            ObjectNode json = JacksonUtils.createEmptyJsonNode();
            json.replace("datum", JacksonUtils.transferToJsonNode(datum));
            json.replace("source", JacksonUtils.transferToJsonNode(peers.local()));
            
            onPublish(datum, peers.local());
            
            final String content = json.toString();
            
          	// 使用闭锁,等待一般以上节点回复成功
            final CountDownLatch latch = new CountDownLatch(peers.majorityCount());
            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 Callback<String>() {
                    @Override
                    public void onReceive(RestResult<String> result) {
                        if (!result.ok()) {
                            Loggers.RAFT
                                    .warn("[RAFT] failed to publish data to peer, datumId={}, peer={}, http code={}",
                                            datum.key, server, result.getCode());
                            return;
                        }
                        latch.countDown();
                    }
                    
                    @Override
                    public void onError(Throwable throwable) {
                        Loggers.RAFT.error("[RAFT] failed to publish data to peer", throwable);
                    }
                    
                    @Override
                    public void onCancel() {
                    
                    }
                });
                
            }
            
          	// 超时就抛出非法状态异常
            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();
        }
    }

com.alibaba.nacos.naming.consistency.persistent.raft.RaftPeerSet#majorityCount

    public int majorityCount() {
        return peers.size() / 2 + 1;
    }

5. 实现自己的注册中心需要实现的接口

1. 关于存储

1、内存存储:可以选择数据存储到内存中,节点数据存储到内存,数据同步可以参考nacos 的distro 协议。

2、持久化:存储到redis,比较简单。 可以选择redis 的hash 结构,key是服务名称、hash内部的key 是instanceId 实例ID,value 是实例信息(包括上次心跳时间等)。

2. 关于接口

参考nacos,如果实现一个自己的注册中心核心的接口有三个:
1、服务注册:生成服务信息和实例信息,存储到db
2、心跳: 续期
3、获取服务: 根据服务获取所有实例信息

如果有权限相关,还需要一个登录接口

参考:

nacos github: https://github.com/alibaba/nacos

nacos-sdk-python: https://github.com/nacos-group/nacos-sdk-python

posted @ 2024-06-25 20:24  QiaoZhi  阅读(93)  评论(0编辑  收藏  举报