## 概述 在 Spring Cloud Netflix 阶段我们采用 Eureka 做作为我们的服务注册与发现服务器,现利用 Spring Cloud Alibaba 提供的 Nacos 组件替代该方案。 [Nacos 官网](https://nacos.io/zh-cn/) ## 什么是 Nacos Nacos是一款比较新的技术产品,在2018年7月开源,它致力于发现、配置和管理微服务,可以作为服务的注册中心和配置中心。Nacos 提供了一组简单易用的特性集,以“服务”为中心,能够提供快速实现动态服务发现、服务配置、服务元数据及流量管理能力。 ![](https://img.kancloud.cn/d0/f9/d0f9f56b71851b3b1a25d1b4a8c68930_1130x495.png) ## Nacos数据模型 在Nacos的架构中有一个名字服务 (Naming Service),提供了分布式系统中所有对象(Object)、实体(Entity)的“名字”到关联的元数据之间的映射管理服务,解决了namespace到clusterId的路由问题。Nacos数据模型相对复杂,因为Nacos为服务的管理提供了很多特性,这些特性都被体现在数据的建模上。下面我们先来看看这张图: ![](https://img.kancloud.cn/00/00/0000d2b3791f8043d23c46644f7e0ae5_1217x768.png) 服务层和实例层很好理解,因为在分布式中一个服务必然会有多个节点,但是Nacos在服务层与实例层中间新增了一个集群层,一个服务对应多个集群,一个集群对应多个实例。这个集群层带来了以下收益: * Nacos的隔离性也从物理节点级别上升到了集群级别; * 在进行水平扩缩容的时候,也可以进行集群级别的伸缩; * 在应用需要使用Nacos时候,可以直接以集群为单位进行支撑。 实例分为两种临时实例和持久化实例: * 临时实例:使用客户端上报模式,需要能够自动摘除不健康实例,并且不需要持久化,而上层的业务服务,例如微服务或者 Dubbo 服务,服务的 Provider 端支持添加汇报心跳的逻辑,就可以作为临时实例进行注册。 * 持久化实例:服务端来主动探测是否健康,因为客户端不会上报心跳,那么自然就不能去自动摘除下线的实例。一些基础的组件例如数据库、缓存等,这些往往不能上报心跳,这种类型的服务在注册时,就需要作为持久化实例注册。 上面是从服务出发的数据模型。下面则是把视野放大,从不同环境出发,建立起更广泛的数据模型,也可以理解为对不同环境的相同服务做了数据隔离。 ## 不同粒度的服务数据隔离 ![](https://img.kancloud.cn/e9/0d/e90db8791f0aa5fc13315fbde16d5cc3_485x401.png) 从命名空间到单个实例,中间经历了组、服务、集群的层级划分。每一个层级对应着不一样的粒度的数据隔离,有效的满足多业务多环境下的不同数据隔离需求。 这种图要表达的意思很简单,在Service层外面还包有两层,分别是命名空间和组。 * 命名空间:业务在开发的时候可以将开发环境和生产环境分开,或者根据不同的业务线存在多个生产环境,命名空间常用场景之一就是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。 * 组:对服务进行分组,可以满足接口级别的隔离。 ## 一致性协议 Nacos作为服务的注册中心,同样也需要面对数据的一致性问题,也就是需要在CAP理论中选择,在最新的版本中,Nacos支持AP原则和CP原则两种原则。 Nacos中的CP原则实现是基于简化的Raft,主要是一主多从策略,Raft有三类角色Leader(领袖)、Follower(群众)以及Candidate(候选人)。Raft的选举过程和上一小节将到的ZAB协议选举过程是一样的,也需要获得半数以上的票才能够成功选出Leader。有一点小的区别是,ZAB的Follower在投票给一个Leader之前必须和Leader的日志达成一致,而Raft的Follower则简单地说是谁的term高就投票给谁。Raft 协议也和ZAB协议一样强依赖 Leader 节点的可用性来确保集群数据的一致性。只有Leader节点才有权力领导写操作。它的写操作流程跟我在上一小节讲到的ZAB协议流程一样类似于二阶段提交的流程。在心跳检测中Raft协议的心跳是从Leader到Follower, 而ZAB协议则相反。Raft和ZAB如此相似,归根结底是因为Raft和ZAB都是Paxos算法的简化和优化,并且把Paxos更加的具象化,让人易于实现和理解。 Nacos中的AP原则实现基于阿里自研协议 Distro。Distro 协议则是参考了内部 ConfigServer 和开源 Eureka。该实现没有主从之分,当客户端请求的某个节点挂了后,不会有类似于选主的过程,客户端请求会自动切换到新的Nacos节点,当宕机的节点恢复后,又会重新回到集群管理内。所要做的就是同步一些新的服务注册信息给重启的Nacos节点,达到数据一致的效果。 在Nacos中如何切换AP原则和CP原则,主要切换的条件是注册的服务实例是否是临时实例。如果是临时实例,则选用的是Nacos中的AP原则。 ## 服务注册中心技术对比 ![](https://img.kancloud.cn/1d/f5/1df59f5920cc17caa24003cd64902ed9_1751x742.png) ## eureka与nacos的区别 * eureka采用AP模式实现注册中心,无中心节点无leader概念,采用peertopeer集群架构 * nacos 默认采用AP模式,在1.0版本之后增加了AP+CP的混合模式实现注册中心,采用raft协议,有leader概念 ## 安装部署 ### 准备环境 Nacos 依赖 Java 环境来运行。如果您是从代码开始构建并运行 Nacos,还需要为此配置 Maven 环境,请确保是在以下版本环境中安装使用: * 64 bit OS,支持 Linux/Unix/Mac/Windows,推荐选用 Linux/Unix/Mac。 * 64 bit JDK 1.8+ ### 启动 * windowd启动方式 ``` D:\open-capacity-platform\register-center>h: H:\>cd alibaba H:\alibaba>cd open-capacity-platform H:\alibaba\open-capacity-platform>cd register-center H:\alibaba\open-capacity-platform\register-center>cd nacos-server H:\alibaba\open-capacity-platform\register-center\nacos-server>cd bin H:\alibaba\open-capacity-platform\register-center\nacos-server\bin>startup.cmd ``` * Linux启动方式 ./startup.sh -m standalone ## 访问服务 打开浏览器访问:http://localhost:8848/nacos ![](https://img.kancloud.cn/1d/c1/1dc1245c9b1a77a0f9e19a0df0ddf181_1920x753.png) **注:从 0.8.0 版本开始,需要登录才可访问,默认账号密码为 nacos/nacos** ## nacos服务注册,可参考user-ceneter 引入POM ``` <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> ``` bootstrap.yml增加以下配置 ``` spring: cloud: nacos: discovery: server-addr: ip:prot #nacos的地址 ``` 工程启动类添加以下注解 ``` @EnableDiscoveryClient ``` ## nacos API > 用API服务注册,由于未心跳检测,服务发现服务不会一直有效 ### 服务注册API ![](https://img.kancloud.cn/a3/89/a389514090e691500b44458b9a472daa_1718x417.png) ### 服务发现API ![](https://img.kancloud.cn/b4/fd/b4fd86b96178a319748c26faf12b2f05_1708x702.png) ### 控制台查看 ![](https://img.kancloud.cn/91/40/9140c967ac967f2ee003fb676e9784b2_1909x474.png) ## nacos源码分析 ### nacos数据结构 ![](https://img.kancloud.cn/eb/94/eb942db612db49dfa908b8b005934b1b_1137x569.png) ### nacos 整体结构 ![](https://img.kancloud.cn/39/66/3966d5879d1cb084697fc3dc4c3ec6f2_954x766.png) ### 内核原理 通过NacosServiceRegistry实现了ServiceRegistry接口,ServiceInstance可以拿到当前注册实例。 #### 注册 nacos-client注册 ``` @Override public void register(Registration registration) { if (StringUtils.isEmpty(registration.getServiceId())) { log.warn("No service to register for nacos client..."); return; } String serviceId = registration.getServiceId(); Instance instance = new Instance(); instance.setIp(registration.getHost()); instance.setPort(registration.getPort()); instance.setWeight(nacosDiscoveryProperties.getWeight()); instance.setClusterName(nacosDiscoveryProperties.getClusterName()); instance.setMetadata(registration.getMetadata()); try { namingService.registerInstance(serviceId, instance); log.info("nacos registry, {} {}:{} register finished", serviceId, instance.getIp(), instance.getPort()); } catch (Exception e) { log.error("nacos registry, {} register failed...{},", serviceId, registration.toString(), e); } } ``` #### 心跳 定时发送给nacos-server ``` package com.alibaba.nacos.client.naming.beat; import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.client.monitor.MetricsMonitor; import com.alibaba.nacos.client.naming.net.NamingProxy; import com.alibaba.nacos.client.naming.utils.UtilAndComs; import java.util.Map; import java.util.concurrent.*; import static com.alibaba.nacos.client.utils.LogUtils.NAMING_LOGGER; /** * @author harold */ public class BeatReactor { private ScheduledExecutorService executorService; private volatile long clientBeatInterval = 5 * 1000; private NamingProxy serverProxy; public final Map<String, BeatInfo> dom2Beat = new ConcurrentHashMap<String, BeatInfo>(); public BeatReactor(NamingProxy serverProxy) { this(serverProxy, UtilAndComs.DEFAULT_CLIENT_BEAT_THREAD_COUNT); } public BeatReactor(NamingProxy serverProxy, int threadCount) { this.serverProxy = serverProxy; executorService = new ScheduledThreadPoolExecutor(threadCount, new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); thread.setName("com.alibaba.nacos.naming.beat.sender"); return thread; } }); executorService.schedule(new BeatProcessor(), 0, TimeUnit.MILLISECONDS); } public void addBeatInfo(String serviceName, BeatInfo beatInfo) { NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo); dom2Beat.put(buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort()), beatInfo); MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size()); } public void removeBeatInfo(String serviceName, String ip, int port) { NAMING_LOGGER.info("[BEAT] removing beat: {}:{}:{} from beat map.", serviceName, ip, port); dom2Beat.remove(buildKey(serviceName, ip, port)); MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size()); } public String buildKey(String serviceName, String ip, int port) { return serviceName + Constants.NAMING_INSTANCE_ID_SPLITTER + ip + Constants.NAMING_INSTANCE_ID_SPLITTER + port; } class BeatProcessor implements Runnable { @Override public void run() { try { for (Map.Entry<String, BeatInfo> entry : dom2Beat.entrySet()) { BeatInfo beatInfo = entry.getValue(); if (beatInfo.isScheduled()) { continue; } beatInfo.setScheduled(true); executorService.schedule(new BeatTask(beatInfo), 0, TimeUnit.MILLISECONDS); } } catch (Exception e) { NAMING_LOGGER.error("[CLIENT-BEAT] Exception while scheduling beat.", e); } finally { executorService.schedule(this, clientBeatInterval, TimeUnit.MILLISECONDS); } } } class BeatTask implements Runnable { BeatInfo beatInfo; public BeatTask(BeatInfo beatInfo) { this.beatInfo = beatInfo; } @Override public void run() { long result = serverProxy.sendBeat(beatInfo); beatInfo.setScheduled(false); if (result > 0) { clientBeatInterval = result; } } } } ``` ### 功能说明 ServiceManager是nacos naming server中service核心管理类。在启动时会执行一次本地信息到其他服务器,while(true)发生改变的service队列内容进行本地更新,同时启动对com.alibaba.nacos.naming.domains.meta.前缀的key的监听;在运行过程中,执行controller接受的相关请求的功能,完成本地状态和远程服务器同步。 ## nacos流程图 ![](https://img.kancloud.cn/da/df/dadf4b5214a7f5a62e9e13241d25c0b4_7146x6173.png) ## Nacos 注册中心的心跳机制 与 Nacos 服务器之间的通信过程。在微服务启动后每过5秒,会由微服务内置的 Nacos 客户端主动向 Nacos 服务器发起心跳包(HeartBeat)。心跳包会包含当前服务实例的名称、IP、端口、集群名、权重等信息。 ![](https://img.kancloud.cn/39/26/3926f8f22e304cf8bd1b56de55248556_1159x307.png) 如果你开启微服务 Debug 日志,会清晰地看到每 5 秒一个心跳请求被发送到 Nacos 的 /nacos/v1/ns/instance/beat 接口,该请求会被 Nacos 服务器内置的 naming 模块处理。 ``` 17:11:23.826 DEBUG 10720 --- [ing.beat.sender] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@665891d213 pairs: {PUT /nacos/v1/ns/instance/beat?app=unknown&serviceName=DEFAULT_GROUP%40%40sample-service&namespaceId=public&port=9000&clusterName=DEFAULT&ip=119.27.173.249 HTTP/1.1: null}{Content-Type: application/x-www-form-urlencoded}{Accept-Charset: UTF-8}{Accept-Encoding: gzip,deflate,sdch}{Content-Encoding: gzip}{Client-Version: 1.3.2}{User-Agent: Nacos-Java-Client:v1.3.2}{RequestId: 6447aa06-9d70-41ea-83ef-cd27af1d3422}{Request-Module: Naming}{Host: 119.27.173.249:8848}{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}{Connection: keep-alive}{Content-Length: 326} 17:11:28.837 DEBUG 10720 --- [ing.beat.sender] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@5f00479a12 pairs: {PUT /nacos/v1/ns/instance/beat?app=unknown&serviceName=DEFAULT_GROUP%40%40sample-service&namespaceId=public&port=9000&clusterName=DEFAULT&ip=119.27.173.249 HTTP/1.1: null}{Content-Type: application/x-www-form-urlencoded}{Accept-Charset: UTF-8}{Accept-Encoding: gzip,deflate,sdch}{Content-Encoding: gzip}{Client-Version: 1.3.2}{User-Agent: Nacos-Java-Client:v1.3.2}{RequestId: 9fdf2264-9704-437f-bd34-7c9ee5e0be41}{Request-Module: Naming}{Host: 119.27.173.249:8848}{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}{Connection: keep-alive} 17:11:38.847 DEBUG 10720 --- [ing.beat.sender] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@3521283812 pairs: {PUT /nacos/v1/ns/instance/beat?app=unknown&serviceName=DEFAULT_GROUP%40%40sample-service&namespaceId=public&port=9000&clusterName=DEFAULT&ip=119.27.173.249 HTTP/1.1: null}{Content-Type: application/x-www-form-urlencoded}{Accept-Charset: UTF-8}{Accept-Encoding: gzip,deflate,sdch}{Content-Encoding: gzip}{Client-Version: 1.3.2}{User-Agent: Nacos-Java-Client:v1.3.2}{RequestId: ccb6a586-897f-4036-9c0d-c614e2ff370a}{Request-Module: Naming}{Host: 119.27.173.249:8848}{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}{Connection: keep-alive} ``` naming 模块在接收到心跳包后,会按下图逻辑处理心跳包并返回响应: naming 模块收到心跳包,首先根据 IP 与端口判断 Nacos 是否存在该服务实例?如果实例信息不存在,在 Nacos 中注册登记该实例。而注册的本质是将新实例对象存储在Map集合中; ![](https://img.kancloud.cn/90/d0/90d0c8a2998646f696fa8e86dc21f895_763x862.png) 如果实例信息已存在,记录本次心跳包发送时间; * 设置实例状态为“健康”; * 推送“微服务状态变更”消息; * naming 模块返回心跳包时间间隔。 那 Nacos 又是如何将无效实例从可用实例中剔除呢?Nacos Server 内置的逻辑是每过 20 秒对“实例 Map”中的所有“非健康”实例进行扫描,如发现“非健康”实例,随即从Map中将该实例删除。