Nacos简介—4.Nacos架构和原理二
大纲
1.Nacos的定位和优势
2.Nacos的整体架构
3.Nacos的配置模型
4.Nacos内核设计之一致性协议
5.Nacos内核设计之自研Distro协议
6.Nacos内核设计之通信通道
7.Nacos内核设计之寻址机制
8.服务注册发现模块的注册中心的设计原理
9.服务注册发现模块的注册中心的服务数据模型
10.服务注册发现模块的健康检查机制
11.配置管理模块的配置一致性模型
12.Zookeeper、Eureka和Nacos的对比总结
5.Nacos内核设计之自研Distro协议
(1)Distro协议的背景和设计思想
(2)Distro协议的工作原理之数据初始化
(3)Distro协议的工作原理之数据校验
(4)Distro协议的工作原理之写操作
(5)Distro协议的工作原理之读操作
(6)总结
(1)Distro协议的背景和设计思想
一.Distro协议的背景
Distro协议是Nacos自研的⼀种AP分布式协议,也是面向临时实例设计的⼀种分布式协议。它保证了在某些Nacos节点宕机后,整个临时实例处理系统依旧可正常工作。
作为⼀种有状态的中间件应用的内嵌协议,Distro协议保证了各个Nacos节点对于海量注册请求的统⼀协调和存储。
二.Distro协议的设计思想
设计一:Nacos每个节点是平等的
每个节点都会处理写请求,以及都会把新数据同步到其他节点。
设计二:每个节点只负责部分数据
每个节点都会定时发送自己负责数据的校验值到其他节点来保持数据⼀致性。
设计三:每个节点独立处理读请求
所以每个节点都能够及时从本地发出响应。
(2)Distro协议的工作原理之数据初始化
新加入的Nacos节点会拉取全量数据。具体操作就是轮询所有Nacos节点,向其他节点发送请求来拉取全量数据。
完成拉取全量数据后,Nacos的每个节点都拥有了当前所有注册上来的非持久化实例数据。
(3)Distro协议的工作原理之数据校验
在Nacos集群启动后,各节点间会定期发送心跳。心跳信息主要是当前节点上的所有数据的元信息,使用元信息的原因是要保证在网络中传输的数据量维持在⼀个较低水平。
这种数据校验会以心跳的形式进行。即每个节点会在固定时间间隔,向其他节点发起⼀次数据校验请求。在数据校验过程中,一旦某节点发现其他节点上的数据与本地数据不⼀致,则会发起⼀次全量拉取请求,将数据补齐。
(4)Distro协议的工作原理之写操作
对于⼀个已经启动完成的Nacos集群,在⼀次客户端发起写操作的流程中,当注册非持久化的服务实例的写请求发送到某台Nacos节点时,Nacos集群处理的流程如下:
一.前置的Filter拦截写请求
首先,收到写请求的Nacos节点,其前置Filter会拦截写请求。然后根据请求中包含的IP和Port信息计算其所属的Nacos责任节点,并将该请求转发到所属的Nacos责任节点上。
二.Controller会解析写请求
所属的Nacos责任节点收到写请求后,会让其Controller解析写请求。
三.Distro协议下Nacos节点会定时执行同步任务
定时同步任务会将本机所负责的所有实例信息同步到其他节点上。
(5)Distro协议的工作原理之读操作
由于Nacos的每个节点上都存放了全量数据,因此在每⼀次读操作中,Nacos节点会直接从本地拉取数据进行快速响应。
这种机制保证了Distro协议作为⼀种AP协议,能及时响应读操作的请求。在网络分区情况下,对于所有的读操作也能够正常返回。当网络分区恢复时,各Nacos节点都会把各数据分片的数据进行合并恢复。
(6)总结
Distro协议是Nacos对临时服务实例数据开发的⼀致性协议。其数据存储在缓存中,在启动时进行全量数据同步,并定期进行数据校验。在Distro协议的设计思想下,每个节点都可以接收和处理读写请求。
Distro协议的请求场景主要分为如下三种情况:
情况一:当节点接收到属于自己负责的服务实例的写请求时,直接写入。
情况二:当节点接收到不属于自己负责的服务实例的写请求时,首先在集群内部路由,然后转发给对应的节点,从而完成写请求。
情况三:当节点接收到任何读请求时,都直接在本机查询并返回,因为所有服务实例的数据都被同步到了各机器节点上。
Distro协议作为Nacos临时服务实例的⼀致性协议,保证了分布式环境下各节点上的服务信息状态,都能及时通知其他节点。Distro协议可让Nacos维持数十万量级的服务实例数据的存储和⼀致性。
6.Nacos内核设计之通信通道
(1)现状背景
(2)场景分析
(3)长连接核心诉求
(4)长连接选型对比
(1)现状背景
Nacos 1.x版本的Config/Naming模块,各自的推送通道都是按照自己的设计模型来实现的。配置和服务器模块的数据推送通道不统⼀,HTTP短连接性能压力巨大,未来Nacos需构建能同时支持配置及服务的长连接通道,以标准的通信模型重构推送通道。
(2)场景分析
场景一:配置管理
一.Client和Server之间的通信
客户端Client需要感知服务端Server的节点列表,需要按照某种策略选择其中⼀个Server节点进行连接。当底层连接断开时,客户端Client就需要切换Server进行重连。
客户端基于当前可用的长连接,进行配置的查询、发布、删除、监听等RPC语义的接口通信。
服务端需要将配置变更消息推送给当前监听的客户端。当网络不稳定时,客户端接收失败,就需要支持重推并告警。
服务端需要感知客户端连接断开事件,将连接注销,并且清空连接对应的上下文。
二.Server之间的通信
每个Server都需要能够获取集群所有Server的地址列表,然后为每个Server创建独立的长连接。当长连接断开时,需要进行重连。当节点列表变更时,需要创建新节点的长连接和销毁下线节点的长连接。
Server间需要进行数据同步,包括配置信息、当前连接数信息、系统负载信息、负载调节信息等同步。
场景二:服务注册发现
一.Client和Server之间的通信
客户端Client需要感知服务端Server的节点列表,需要按照某种策略选择其中⼀个Server节点进行连接。当底层连接断开时,客户端Client就需要切换Server进行重连。
客户端基于当前可用的长连接,进行服务的查询、注册、注销、订阅等RPC语义的接口通信。
服务端需要将服务变更消息推送新给客户端,完成推送后客户端需要ACK,方便服务端进行metrics统计和重推判定等。
服务端需要感知客户端连接断开事件,将连接注销,并且清空连接对应的上下文。
二.Server之间的通信
服务端之间需要通过长连接感知对端存活状态,需要通过长连接汇报服务状态(同步RPC能力)。
服务端之间进行AP Distro数据同步,需要异步RPC带ACK能力。
(3)长连接核心诉求
一.功能性诉求
二.性能要求
三.负载均衡
四.连接生命周期
一.功能性诉求
客户端的诉求:
诉求一:实时感知连接生命周期的能力,包括连接建立、连接断开事件。
诉求二:客户端调用服务端支持同步阻塞、异步Future、异步CallBack三种模式。
诉求三:底层连接自动切换能力。
诉求四:响应服务端连接重置消息进行连接切换。
诉求五:选址/服务发现。
服务端的诉求:
诉求一:实时感知连接生命周期的能力,包括连接建立、连接断开事件。
诉求二:服务端往客户端主动进行数据推送,需要客户端进行ACK返回以支持可靠推送,并且需要进行失败重试。
诉求三:服务端主动推送负载调节能力。
二.性能要求
性能方面,需要能够满足阿里的生产环境可用性要求,能支持百万级的长连接规模及请求量和推送量,并且要保证足够稳定。
三.负载均衡
说明一:常见的负载均衡策略
随机、Hash、轮询、权重、最小连接数、最快响应速度等。
说明二:短连接和长连接负载均衡的异同
由于短连接会快速建立和销毁,所以"随机、Hash、轮询、权重"四种策略下服务端重启也不会影响均衡,而"最小连接数、最快响应速度",如果数据延时则容易造成堆积问题。
长连接建立连接后,如果没有异常情况,连接会⼀直保持。如果出现断连,需要重新选择⼀个新的服务节点。当服务节点出现重启后,最终会出现连接不均衡的情况。"随机、轮询、权重"的策略在客户端重连切换时可以使用。"最小连接数、最快响应速度",同样会出现数据延时造成堆积问题。
说明三:长连接和短连接的差别
在于在整体连接稳定时,服务端需要⼀个重平衡的机制。将集群视角的连接数重新洗牌分配,趋向另外⼀种稳态。
说明四:客户端随机 + 服务端柔性调整
核心的策略是客户端 + 服务端双向调节策略,也就是客户端随机选择 + 服务端运行时柔性调整。
说明五:什么是客户端随机选择
客户端在启动时获取服务列表,按照随机规则进行节点选择。逻辑比较简单,整体能够保持随机。
说明六:什么是服务端柔性调整
(当前实现版本)人工管控方案:集群视角的系统负载控制台,提供连接数、负载等视图,实现人工调节每个Server节点的连接数,人工触发重平衡,人工削峰填谷。比如展示总节点数量,总长链接数量、平均数量、系统负载信息、每个节点的地址、长链接数量、与平均数量的差值、对高于平均值的节点进行数量调控、设置数量上限(临时和持久化),并且可以指定服务节点进行切换。
(未来终态版本)自动化管控方案:基于每个Server间连接数及负载自动计算节点合理连接数,自动触发Reblance、自动削峰填谷,实现周期长,比较依赖算法准确性。
四.连接生命周期
Nacos需要什么:
第一:低成本快速感知
客户端要在服务端不可用时尽快切换到新的服务节点,降低不可用时间。并且客户端要能够感知底层连接切换事件,重置上下文。服务端需要在客户端断开连接时剔除客户端连接对应的上下文,包括配置监听、服务订阅上下文,且处理客户端连接对应的实例上下线。客户端正常重启:客户端主动关闭连接,服务端实时感知。服务端正常重启:服务端主动关闭连接,客户端实时感知。
第二:防抖
当网络短暂不可用时,客户端需要能接受短暂网络抖动。超过阈值后,需要自动切换Server,但要防止请求风暴。
第三:断网演练
断网场景下以合理的频率进行重试,断网结束时可以快速重连恢复。
(4)长连接选型对比
在上述的备选框架中:从功能的契合度上,RSocket比较贴切Nacos的功能性诉求。RSocket性能上比gRPC要强,社区活跃度相对gRPC要逊色很多。
7.Nacos内核设计之寻址机制
(1)寻址机制的设计抽象
(2)当前Nacos内部实现的几种寻址机制
(1)寻址机制的设计抽象
Nacos支持单机部署以及集群部署。对于单机模式,Nacos只是自己和自己通信。对于集群模式,则集群内的每个Nacos节点都需要相互通信。因此这就带来⼀个问题,应该如何管理集群内的Nacos节点信息。而这,就需要Nacos内部的寻址机制。
单机模式和集群模式的根本区别是Nacos成员节点个数是单个还是多个,而Nacos集群需要能感知节点变更情况:节点是增加了还是减少了。当前最新的成员列表信息是什么,以何种方式去管理成员列表信息,如何快速的支持新的、更优秀的成员列表管理模式等等。针对上述需求点,Nacos抽象出了⼀个MemberLookup接口如下:
public interface MemberLookup { //start.void start() throws NacosException;//Inject the ServerMemberManager property.void injectMemberManager(ServerMemberManager memberManager);//The addressing pattern finds cluster nodes.void afterLookup(Collection<Member> members);//Addressing mode closed.void destroy() throws NacosException;
}
ServerMemberManager存储着本节点所知道的所有成员节点信息、提供了针对成员节点的增删改查操作、维护了⼀个MemberLookup列表。
MemberLookup接口的核心方法是:injectMemberManager()和afterLookup()。前者的作用是将ServerMemberManager注入到MemberLookup中,以方便利用ServerMemberManager 的存储、查询能力。后者则是⼀个事件接口,当MemberLookup需要进行成员节点信息更新时,会将当前最新的成员节点信息通知给ServerMemberManager,具体的节点管理方式,则是隐藏到具体的MemberLookup实现中。
(2)当前Nacos内部实现的几种寻址机制
一.单机寻址
二.文件寻址
三.地址服务器寻址
四.未来扩展点之集群节点自动扩容缩容
一.单机寻址
com.alibaba.nacos.core.cluster.lookup.StandaloneMemberLookup
单机模式的寻址模式很简单,其实就是找到自己的IP:PORT组合信息,然后格式化为⼀个节点信息,接着调用afterLookup()将信息存储到ServerMemberManager中。
public class StandaloneMemberLookup extends AbstractMemberLookup {@Overridepublic void start() {if (start.compareAndSet(false, true)) {String url = InetUtils.getSelfIp() + ":" + ApplicationUtils.getPort();afterLookup(MemberUtils.readServerConf(Collections.singletonList(url)));}}
}
二.文件寻址
com.alibaba.nacos.core.cluster.lookup.FileConfigMemberLookup
文件寻址模式是Nacos集群模式下的默认寻址实现。文件寻址模式也很简单,就是每个Nacos节点需要维护⼀个叫cluster.conf的文件。
该文件默认只需填写每个成员节点的IP信息即可,端口会自动选择Nacos的默认端口8848。如果有特殊需求更改了Nacos的端口信息,则需要在该文件将该节点的完整网路地址信息补充完整(IP:PORT)。
//cluster.conf文件内容如下:192.168.16.101:8847
192.168.16.102
192.168.16.103
当Nacos节点启动时,会读取cluster.conf文件的内容,然后将文件内的IP解析为节点列表,接着调用afterLookup()方法将节点列表存入ServerMemberManager。
private void readClusterConfFromDisk() {Collection<Member> tmpMembers = new ArrayList<>();try {List<String> tmp = ApplicationUtils.readClusterConf();tmpMembers = MemberUtils.readServerConf(tmp);} catch (Throwable e) {Loggers.CLUSTER.error("nacos-XXXX[serverlist] failed to get serverlist from disk!, error : {}", e.getMessage());}afterLookup(tmpMembers);
}
如果集群扩容缩容,那么就要修改每个Nacos节点下的cluster.conf文件,然后Nacos内部的文件变动监听中心会自动发现文件修改,接着重新读取文件内容、加载IP列表信息、更新新增的节点。
private FileWatcher watcher = new FileWatcher() {@Overridepublic void onChange(FileChangeEvent event) {readClusterConfFromDisk();}@Overridepublic boolean interest(String context) {return StringUtils.contains(context, "cluster.conf");}
};public void start() throws NacosException {if (start.compareAndSet(false, true)) {readClusterConfFromDisk();// Use the inotify mechanism to monitor file changes and automatically trigger the reading of cluster.conftry {WatchFileCenter.registerWatcher(ApplicationUtils.getConfFilePath(), watcher);} catch (Throwable e) {Loggers.CLUSTER.error("An exception occurred in the launch file monitor : {}", e.getMessage());}}
}
但是这种默认寻址模式有⼀个缺点——运维成本较大。当新增⼀个Nacos节点时,需要手动修改每个节点下的cluster.conf文件。而且由于每个Nacos节点都存在⼀份cluster.conf文件,如果其中⼀个节点的 cluster.conf 文件修改失败,就容易造成了集群间成员节点列表数据的不⼀致性。因此,Nacos引入了新的寻址模式——地址服务器寻址模式。
三.地址服务器寻址
com.alibaba.nacos.core.cluster.lookup.AddressServerMemberLookup
地址服务器寻址模式是Nacos官方推荐的⼀种集群成员节点信息管理。该模式利用了⼀个Web服务器来管理cluster.conf文件的内容信息,这样运维人员只需要管理这⼀份集群成员节点内容即可。
每个Nacos节点,只需要定时向Web请求当前最新的集群成员节点列表信息。通过地址服务器这种模式,就可以大大简化Nacos集群节点管理的成本。同时,地址服务器是⼀个非常简单的Web程序,其稳定性能得到很好保障。
四.未来扩展点之集群节点自动扩容缩容
目前Nacos的集群节点管理,还属于人工操作。因此,未来期望能够基于寻址模式,实现集群节点自动管理的功能。
能够实现新节点上线时,只需要知道原有集群中的⼀个节点信息。这样就可以在⼀定时间内,顺利加入原有的Nacos集群中。同时也能够自行发现不存活的节点,自动将其从集群可用节点列表中剔除。这其中的逻辑实现,其实就类似Consul的Gossip协议。
8.服务注册发现模块的注册中心的设计原理
(1)服务注册发现简介
(2)数据模型
(3)数据一致性
(4)负载均衡
(5)健康检查
(6)性能与容量
(7)易用性
(8)集群扩展性
(9)用户扩展性
(1)服务注册发现简介
服务注册发现是⼀个古老的话题,当应用开始脱离单机运行和访问时,服务注册发现就诞生了。
目前的网络架构是每个主机都有⼀个独立的IP地址,那么服务注册发现基本上都是通过某种方式获取到服务所部署的IP地址。
DNS协议是最早将⼀个网络名称翻译为网络IP的协议。在最初的架构选型中,DNS + LVS + Nginx基本可满足所有服务的发现,此时服务的IP列表通常配置在Nginx或LVS。
后来出现了RPC服务,服务的上下线更加频繁,人们开始寻求⼀种能支持动态上下线 + 推送IP列表变化的注册中心产品。
互联网软件行业普遍热捧开源产品,个人开发者或中小型公司往往会将开源产品作为选型首选。Zookeeper是⼀款经典的服务注册中心产品(虽然它最初定位并不在于此),在很长⼀段时间里,它是提起RPC服务注册中心时想到的唯⼀选择,这很大程度上与Dubbo在的普及程度有关。
Consul和Eureka都出现于2014年。Consul在设计上把很多分布式服务治理上要用到的功能都包含在内,可以支持服务注册、健康检查、配置管理、Service Mesh等。Eureka借着微服务的流行,与SpringCloud生态深度结合,也曾流行起来。
Nacos则携带着阿里巴巴大规模服务生产经验,试图在服务注册和配置管理这个市场上,提供给用户⼀个新的选择。
(2)数据模型
一.服务注册中心数据模型的演进
二.Zookeeper、Eureka、Consul和Nacos的数据模型对比
三.服务实例数据的隔离模型
四.Nacos的临时服务实例和持久化服务实例
一.服务注册中心数据模型的演进
服务注册中心的核心数据是服务的名字和它对应的网络地址。
当服务注册了多个实例时,需要对不健康的实例进行过滤或针对实例的⼀些特征进行流量的分配,那么就要在服务实例上存储⼀些如健康状态、权重等属性。
随着服务规模的扩大,渐渐的又需要在整个服务级别设定⼀些权限规则,以及对所有实例都生效的⼀些开关,于是在服务级别又会设立⼀些属性。
再往后,又发现单个服务的实例又会有划分为多个子集的需求。例如⼀个服务是多机房部署的,那么可能要对每个机房的实例做不同配置,这样又需要在服务和服务实例之间再设定⼀个数据级别。
二.Zookeeper、Eureka、Consul和Nacos的数据模型对比
Zookeeper没有针对服务发现设计数据模型,它的数据以⼀种抽象的树形KV结构来组织,理论上可存储任何语义数据。
Eureka或Consul都设计了服务实例级别的数据模型,可满足大部分场景,不过无法满足大规模和多环境的服务数据存储。
Nacos经过多年经验提炼出的数据模型是⼀种服务-集群-实例的三层模型,这样基本可以满足服务在所有场景下的数据存储和管理。
Nacos的数据模型虽然相对复杂,但是它并不强制使用它里面的所有数据。在大多数场景下,可以选择忽略这些数据属性,此时可以降维成和Eureka或Consul⼀样的数据模型。
三.服务实例数据的隔离模型
另外⼀个需要考虑的是数据的隔离模型。作为⼀个共享服务型的组件,需要能够在多个用户或业务方使用的情况下,保证数据的隔离和安全,这在稍微大⼀点的业务场景中非常常见。
此外服务注册中心往往支持云上部署,此时就要求服务注册中心的数据模型能够适配云上的通用模型。
Zookeeper、Consul和Eureka都没有很明确针对服务隔离的模型。Nacos则在⼀开始就考虑到如何让用户能以多种维度进行数据隔离,同时能够平滑的迁移到阿里云上对应的商业化产品。
Nacos提供了四层的数据逻辑隔离模型。
第一层:用户账号
用户账号对应的是⼀个企业或个体,该数据⼀般不会透传到服务注册中心。
第二层:命名空间
⼀个用户账号可以新建多个命名空间。这个命名空间对应的注册中心物理集群可以根据规则进行路由,这样可以让注册中心内部的升级和迁移对用户是无感知的。同时可以根据用户的级别,为用户提供不同服务级别的物理集群。
第三层:服务分组
服务分组和服务名组成的二维服务标识,通过二维服务标识可以满足接口级别的服务隔离。
第四层:服务实例名称
每个命名空间会对应⼀个服务实例名称。
四.Nacos的临时服务实例和持久化服务实例
Nacos 1.0.0介绍的另外⼀个新特性是:临时服务实例和持久化服务实例。在定义上区分临时服务实例和持久化服务实例的关键是健康检查的方式。
临时服务实例使用客户端上报模式,持久化服务实例使用服务端反向探测模式。
临时服务实例需要能够自动摘除不健康实例,而且无需持久化存储服务实例,那么这种服务实例就适用于类Gossip的协议。
持久化服务实例使用服务端探测的健康检查方式。因为客户端不会上报心跳,那么就不能自动摘除下线的实例,所以这种实例就适用于类Raft的协议。
在大中型的公司里,这两种类型的服务往往都有。
类型一:⼀些基础的组件例如数据库、缓存等,这些往往不能上报心跳。这种类型的服务在注册时,就需要作为持久化实例注册。
类型二:一些上层的业务服务例如微服务、Dubbo服务,服务提供者支持添加汇报心跳的逻辑,此时就可作为临时实例注册。
Nacos 2.0中继续沿用了持久化及非持久化的设定,但有了⼀些调整。Nacos 1.0中持久化及非持久化属性是作为实例的元数据进行存储和识别,这导致了同⼀个服务下可同时存在持久化实例和非持久化实例。但在实际使用中,这种模式会给运维人员带来极大的困惑和运维复杂度。从系统架构看,⼀个服务同时存在持久化及非持久化实例是矛盾的,这就导致该能力事实上并未被广泛使用。
为了简化Nacos的服务数据模型,降低运维复杂度,提升Nacos的易用性,在Nacos2.0中已将是否持久化的数据,抽象至服务级别,而且不再允许⼀个服务同时存在持久化实例和非持久化实例,实例的持久化属性继承自服务的持久化属性。
(3)数据一致性
一.不同的服务注册场景使用不同的一致性协议
二.Nacos支持AP和CP两种⼀致性协议并存
一.不同的服务注册场景使用不同的一致性协议
数据⼀致性是分布式系统永恒的话题,Paxos协议的复杂更让数据⼀致性成为大牛们吹水的常见话题。
不过从协议层面上看,⼀致性的选型已经很长时间没有新的成员加入了。目前来看基本可以归为两种:⼀种是基于Leader的非对等部署的单点写⼀致性,⼀种是对等部署的多点写⼀致性。
当我们选用服务注册中心的时候,并没有⼀种协议能够覆盖所有场景。
场景一:服务节点不会定时发送心跳到注册中心
由于无法通过心跳来进行数据的补偿注册,所以第⼀次注册就必须保证数据不会丢失,从而让强⼀致协议看起来是唯⼀选择。
场景二:客户端会定时发送心跳来汇报健康状态
第⼀次注册的成功率并不是非常关键,当然也很关键,只是相对来说可以容忍数据少量的写失败,因为后续还可以通过心跳再把数据补偿回来。此时Paxos协议的单点瓶颈就不太划算,这也是Eureka为什么不采用Paxos协议而采用自定义的Renew机制的原因。可见不同的服务注册需求,会用不同的协议。
根据Dubbo对Zookeeper的处理,其实采用Eureka的Renew机制更合适。因为Dubbo服务往Zookeeper注册的就是临时节点,Dubbo服务需要定时发送心跳到Zookeeper来续约节点。当Dubbo服务下线时,需要将Zookeeper上的节点摘除。
二.Nacos支持AP和CP两种⼀致性协议并存
Zookeeper虽然用ZAB协议保证了数据的强⼀致,但它缺乏机房容灾能力,无法适应⼀些大型场景。
Nacos因为要支持多种服务类型的注册,并能够具有机房容灾、集群扩展等必不可少的能力,所以在1.0.0起正式支持AP和CP两种⼀致性协议并存。
Nacos1.0.0重构了数据的读写和同步逻辑:首先将与业务相关的CRUD与底层的⼀致性同步逻辑进行了分层隔离。然后将业务的读写(主要是写,因为读会直接使用业务层的缓存)抽象为Nacos定义的数据类型。接着调用⼀致性服务进行数据同步。在决定使用CP还是AP⼀致性时,通过代理 + 可控制的规则进行转发。
Nacos目前的⼀致性协议实现:⼀个是基于简化的Raft协议的CP⼀致性,⼀个是基于自研的Distro协议的AP⼀致性。
Raft协议,基于Leader进行写入,其CP也并不是严格的。只是能保证⼀半所见⼀致,以及数据丢失的概率较小而已。
Distro协议,则是参考了ConfigServer和开源Eureka。在不借助第三方存储的情况下,实现基本大同小异。Distro重点做了⼀些逻辑优化和性能调优。
(4)负载均衡
一.客户端进行负载均衡
二.服务端进行负载均衡
三.服务端负载均衡和客户端负载均衡的优缺点
四.Ribbon设计的客户端负载均衡
五.基于标签的负载均衡策略
六.其他
一.客户端进行负载均衡
严格来说,负载均衡并不算是传统注册中心的功能。
⼀般服务发现的完整流程应该是:客户端先从注册中心获取到服务的实例列表,然后根据自身需求来选择其中的部分实例,或者按⼀定的流量分配机制(负载均衡机制)来选择访问不同的服务提供者。
因此服务注册中心本身⼀般不限定服务消费者的访问策略。Eureka、Zookeeper、Consul,本身都没有实现可配置及可扩展的负载均衡机制。Eureka的负载均衡是由Ribbon来完成的,而Consul的负载均衡则是由Fabio来完成。
二.服务端进行负载均衡
在阿里内部,却是使用的相反的思路。服务消费者往往并不关心所访问的服务提供者的负载均衡,它们只关心如何高效和正确地访问服务提供者的服务。服务提供者则非常关注自身被访问的流量的调配。这其中的一个原因是,阿里内部服务访问流量巨大,稍有不慎就会导致流量异常压垮服务提供者的服务。因此服务提供者需要能够完全掌控服务的流量调配,并可以动态调整。
三.服务端负载均衡和客户端负载均衡的优缺点
服务端的负载均衡,给服务提供者更强的流量控制权,但是无法满足不同消费者希望使用不同负载均衡策略的需求,而不同负载均衡策略的场景却是存在的。
客户端的负载均衡则提供了这种灵活性,并对用户扩展提供更友好的支持。但客户端负载均衡策略如果配置不当,可能会导致服务提供者出现热点,或者压根就拿不到任何服务提供者。
抛开负载均衡到底是在服务提供者实现还是在服务消费者实现,目前的负载均衡有基于权重、服务提供者负载、响应时间、标签等策略。
四.Ribbon设计的客户端负载均衡
Ribbon设计的客户端负载均衡,主要是选择合适现有的IRule、ServerListFilter等接口实现,或者自己继承这些接口,实现自己的过滤逻辑。
Ribbon设计的客户端负载均衡,采用的是两步负载均衡:第⼀步先过滤掉不会采用的服务提供者实例,第二步在过滤后的服务提供者实例里实施负载均衡策略。
Ribbon内置的几种负载均衡策略功能还是比较强大的,同时又因为允许用户去扩展,这可以说是⼀种比较好的设计。
五.基于标签的负载均衡策略
基于标签的负载均衡策略可以做到非常灵活。Kubernetes和Fabio都已经将标签运用到了对资源的过滤中。使用标签几乎可以实现任意比例和权重的服务流量调配。但是标签本身需要单独的存储以及读写功能,不管是放在注册中心本身或者对接第三方的CMDB。
Nacos 0.7.0版本除了提供基于健康检查和权重的负载均衡方式外,还新提供了基于第三方CMDB的标签负载均衡器。
使用基于标签的负载均衡器,可实现同标签优先访问的流量调度策略。在实际应用场景中,可用来实现服务的就近访问。当服务部署在多个地域时,这非常有用。使用这个标签负载均衡器,可以支持非常多的场景。虽然目前Nacos支持的标签表达式不丰富,不过会逐步扩展它支持的语法。
六.其他
除此之外,Nacos定义了Selector作为负载均衡的统⼀抽象。
理想的负载均衡实现应该是什么样的呢?不同人会有不同答案。Nacos试图将服务端负载均衡与客户端负载均衡通过某种机制结合起来,提供用户扩展性,并给予用户充分的自主选择权和轻便的使用方式。
负载均衡是⼀个很大的话题,当我们关注注册中心提供的负载均衡策略时,需要注意该注册中心是否有我们需要的负载均衡方式,使用方式是否复杂。如果没有,那么是否允许我们方便地扩展来实现我们需求的负载均衡策略。
(5)健康检查
一.客户端进行健康检查—TTL机制发送心跳
二.服务端进行健康检查—TCP端口和HTTP接口探测
三.客户端健康检查和服务端健康检查的关注点
一.客户端进行健康检查—TTL机制发送心跳
Zookeeper和Eureka都实现了⼀种TTL机制。即如果客户端在⼀定时间内没有向注册中心发送心跳,则会摘除该客户端。
Eureka做的更好的⼀点在于:它允许在注册服务时,自定义检查自身状态的健康检查方法。这在服务实例能够保持心跳上报的场景下,有比较好的体验。
Nacos也支持这种TTL机制,不过这与ConfigServer的机制又有⼀些区别。Nacos目前支持临时服务实例使用心跳上报方式维持活性。临时服务实例发送心跳的周期默认是5秒。如果Nacos服务端在15秒没收到心跳,则会将该临时实例设置为不健康。如果Nacos服务端在30秒没收到心跳,则会将该临时实例摘除。
不过有⼀些服务是无法上报心跳的,但可提供⼀个检测接口由外部去探测。这样的服务广泛存在,且这些服务对服务发现和负载均衡的需求同样强烈。
二.服务端进行健康检查—TCP端口和HTTP接口探测
服务端健康检查最常见的方式是TCP端口探测和HTTP接口返回码探测,这两种探测方式因为其协议的通用性可以支持绝大多数的健康检查场景。
在其他⼀些特殊场景,可能还需要执行特殊的接口才能判断服务是否可用。例如部署了数据库的主备,数据库的主备可能会在某些情况下进行切换,需要通过服务名对外提供访问,保证当前访问的库是主库。此时的健康检查接口,可能是⼀个检查数据库是否是主库的MySQL命令。
三.客户端健康检查和服务端健康检查的关注点
客户端健康检查和服务端健康检查有不同的关注点。
客户端健康检查主要关注:客户端上报心跳的方式、服务端摘除不健康客户端的机制。
服务端健康检查主要关注:探测客户端的方式、灵敏度以及设置客户端健康状态的机制。
从实现复杂性来说,服务端探测肯定是要更加复杂的。因为服务端需要根据注册服务所配置的健康检查方式,去执行相应的接口、判断相应的返回结果、并做好重试机制和线程池管理。这与客户端探测,只需要等待心跳,然后刷新TTL是不⼀样的。
同时服务端健康检查无法摘除不健康的实例。这意味着注册过的服务实例,如果不调用接口向服务端主动进行注销,那么这些服务实例都需要服务端去维持执行健康检查的探测任务。而客户端健康检查则可以随时摘除不健康实例,减轻服务端的压力。
Nacos既支持客户端的健康检查,也支持服务端的健康检查。同⼀个服务可以切换健康检查模式。这种健康检查方式的多样性很重要,这样可以支持各种类型的服务,并且让这些服务都可以使用Nacos的负载均衡能力。
Nacos下⼀步要做的是实现健康检查方式的用户扩展机制,不管是服务端探测还是客户端探测。这样可以支持用户传入⼀条业务语义的请求,然后由Nacos去执行,做到健康检查的定制。
(6)性能与容量
一.影响性能的因素
二.Zookeeper的性能及其限制分析
三.Zookeeper、Eureka和Nacos的容量分析
一.影响性能的因素
影响读写性能的因素有:⼀致性协议、机器配置、集群规模、数据规模、数据结构、读写逻辑等。
在服务发现的场景中,读写性能是非常关键的,但是并非性能越高就越好,因为追求性能往往需要其他方面做出牺牲。
二.Zookeeper的性能及其限制分析
Zookeeper的写性能可以达上万TPS,这得益于Zookeeper精巧的设计,不过这显然是因为有⼀系列的前提存在。
首先Zookeeper的写逻辑就是进行KV写入的,内部没有聚合。其次Zookeeper舍弃了服务发现的基本功能如健康检查、友好的查询接口。它在支持这些功能时,显然需要增加⼀些逻辑,甚至弃用现有的数据结构。最后Paxos协议本身就限制了Zookeeper集群的规模,3或5个Zookeeper节点是不能应对大规模的服务订阅和查询的。
三.Zookeeper、Eureka和Nacos的容量分析
在对容量的评估时,不仅要评估现有的服务规模,也要预测未来3到5年的扩展规模。阿里的中间件在内部支撑着集团百万级别服务实例,在容量上遇到的挑战可以说不会小于任何互联网公司,这个容量不仅仅意味着整体注册的实例数,也同时包含单个服务的实例数、整体的订阅者的数目以及查询QPS等。
Nacos在淘汰Zookeeper和Eureka的过程中,容量是⼀个非常重要的因素。Zookeeper的容量,从存储实例数来说,可以达到百万级别。
但是随着容量的扩大,性能问题也会随之而来。当大量的实例上下线时,Zookeeper的表现并不稳定。同时在推送机制上的缺陷,会导致客户端的资源占用上升,从而导致客户端性能急剧下降。
Eureka在服务实例规模在5000左右时,就已经出现服务不可用的问题。甚至在压测过程中,如果并发线程数过高,就会造成Eureka崩溃。不过如果服务实例规模在1000上下,目前所有注册中心都可满足。
Nacos可以支撑的服务实例规模约为100万,服务规模可达10万+。在实际环境中,这个数字还会因为机器、网络配置、JVM参数而有所差别。下图展示了使用Nacos 1.0.0版本进行压测后的结果总结,针对容量、并发、扩展性和延时等进行了测试和统计。
(7)易用性
易用性包括多方面的工作。比如API和客户端接入是否简单,文档是否齐全,控制台界面是否完善等。对于开源产品来说,还有⼀块是社区是否活跃。
从使用的经验和调研来看:Zookeeper的易用性是比较差的,其客户端的使用也比较复杂。而且没有针对服务发现的模型设计及相应的API封装,需要自己处理。对多语言的支持也不太好,同时没有比较好用的控制台进行运维管理。
Eureka和Nacos相比较Zookeeper而言,已经改善很多。都有针对服务注册与发现的客户端,及基于Spring Cloud体系的Starter,可以帮助用户以非常低的成本无感知的做到服务注册与发现。同时还暴露标准的HTTP接口,支持多语言和跨平台访问。
Eureka和Nacos都提供官方的控制台来查询服务注册情况,不过Eureka 2.0已停止开发,而Nacos目前依然在建设中。
(8)集群扩展性
一.集群扩展性与集群容量及读写性能的关系
当使用⼀个较小的集群规模就能支撑远高于现有数量的服务注册及访问时,集群的扩展能力暂时就不会那么重要。
从协议的层面上来说:Zookeeper使用的ZAB协议,由于是单点写,在集群扩展性上不具备优势。Eureka在协议上来说理论上可以扩展到很大规模,但因为都是点对点的数据同步,Eureka集群在扩容后,性能上有很大问题。
二.集群扩展性与多地域部署和容灾支持的关系
集群扩展一:双机房容灾
如果不对基于Leader写的协议进行改造,那么是无法支持双机房容灾的。这意味着Zookeeper不能在没有人工干预的情况下做到双机房容灾。
在单机房断网情况下,使机房内服务可用并不难,难的是如何在断网恢复后做数据聚合。Zookeeper的单点写模式就会有断网恢复后的数据对账问题。
Eureka的部署模式天然支持多机房容灾。因为Eureka采用的是纯临时实例的注册模式:不持久化、所有数据都可以通过客户端心跳上报进行补偿。
由于临时实例和持久化实例都有它的应用场景,为了能够兼容这两种场景,Nacos支持两种模式的部署。⼀种是和Eureka⼀样的AP协议的部署,这种模式只支持临时实例,可以完美替代当前的Zookeeper、Eureka,并支持机房容灾。另⼀种是支持持久化实例的CP模式,这种情况下不支持双机房容灾。
集群扩展二:异地多活
很多业务组件的异地多活正是依靠服务注册中心和配置中心来实现的,这其中包含流量的调度和集群访问规则的修改等。
机房容灾是异地多活的⼀部分,但是要让业务能够在访问服务注册中心时,动态调整访问的集群节点,这需要第三方的组件来做路由。
异地多活往往是⼀个包含所有产品线的总体方案,很难说单个产品是否支持异地多活。
集群扩展三:多数据中心
多数据中心其实也算是异地多活的⼀部分。Zookeeper和Eureka都没有给出官方的多数据中心方案,Nacos则提供了采用Nacos-Sync组件来做数据中心之间的数据同步,这意味着每个数据中心的Nacos集群都会有多个数据中心的全量数据。
Nacos-Sync是Nacos生态组件里的重要⼀环,不仅承担Nacos集群与Nacos集群之间的数据同步,也承担与Eureka、Zookeeper、Kubernetes及Consul间的数据同步。
(9)用户扩展性
在框架的设计中,扩展性是⼀个重要的设计原则。Spring、Dubbo、Ribbon等框架都在用户扩展性上做了比较好的设计。这些框架的扩展性往往由面向接口及动态类加载等技术,来运行用户扩展约定的接口,实现用户自定义的逻辑。
在Server的设计中,用户扩展是比较审慎的。因为引入用户扩展代码,可能会影响原有Server服务的可用性。同时如果出问题,排查的难度也比较大。
设计良好的SPI是可能的,但由此带来的稳定性和运维风险需要仔细考虑。在开源软件中,往往通过直接贡献代码的方式来实现用户扩展。好的扩展会被很多人不停的更新和维护,这也是⼀种好的开发模式。
Zookeeper和Eureka目前Server端都不支持用户扩展。⼀个支持用户扩展的服务发现产品是CoreDNS,CoreDNS整体架构就是通过插件来串联起来的,通过将插件代码以约定的方式放到CoreDNS工程下,重新构建就可以将插件添加到CoreDNS整体功能链路的⼀环中。
那么这样的扩展性是否是有必要的呢?假如要添加⼀种新的健康检查方式,连接数据库执行⼀条MySQL命令,通常是在代码里增加MySQL类型的健康检查方法、构建、测试然后发布。但如果允许用户上传⼀个jar包放到Server部署目录下的某个位置,Server就会自动扫描并识别到这张新的健康检查方式呢?这样不仅更酷,也让整个扩展的流程与Server的代码解耦,变得非常简单。
所以对于系统的⼀些功能,如果能通过精心的设计,开放给用户进行扩展,那么其扩展性是极强的,毕竟增加扩展的支持不会让原有功能有任何损失。
所有产品都应该尽量支持用户进行扩展,这需要Server端的SPI机制设计得足够健壮和容错。
Nacos已经通过SPI机制开放了对第三方CMDB的扩展支持,后续很快会开放健康检查及负载均衡等核心功能的用户扩展,目的就是为了能够以⼀种解耦的方式支持用户各种各样的需求。