目录
nacos和eureka注册中心对比 and CAP定律理解
nacos
概念
命名空间:用于进行租户粒度的配置隔离,常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
配置:需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。通常以 param-key=param-value 的形式存在。
配置集:一组相关的配置项的集合称。一个配置文件通常就是一个配置集,一个系统或者应用可以包含多个配置集。常用于不同的应用或组件使用了相同的配置类型,如 database_url 配置和 MQ_topic 配置。
服务注册中心:存储服务实例和服务负载均衡策略的数据库。通过服务名可以唯一确定其指代的服务。不同的服务可以归类到同一分组。
服务发现:(通常使用服务名)对服务下的实例的地址和元数据进行探测,并以预先定义的接口提供给客户端进行查询。
元信息:Nacos配置和服务描述信息,如服务版本、权重、策略。
权重:实例级别的配置。权重为浮点数。权重越大,分配给该实例的流量越大。
健康检查:检查实例是否能提供服务,不健康的实例不会返回给客户端。
健康保护阈值:为了防止因过多实例不健康导致流量全部流向健康实例,继而造成流量压力把健健康实例压垮并形成雪崩效应,应将健康保护阈值定义为一个 0 到 1 之间的浮点数。当域名健康实例占总服务实例的比例小于该值时,无论实例是否健康,都会将这个实例返回给客户端。这样做虽然损失了一部分流量,但是保证了集群的剩余健康实例能正常工作。
这张图很重要。表述了namespace、group和service/dataId的包含关系。
nacos和eureka注册中心对比 and CAP定律理解
CAP原则
CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。一般分布式系统中,肯定是优先办证P,剩下的就是C和A的取舍。
分布式系统的CAP理论:理论首先把分布式系统中的三个特性进行了如下归纳:
一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
分区容错性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
nacos支持CP和AP两种模式,eureka只支持AP模式。
CP模式主要保障数据一致性,支持C数据一致性和P分区容错
AP模式主要保障可用性,支持A可用性和P分区容错
初步结论为:使用Nacos代替Eureka,主要理由为:
相比与Eureka:
(1)Nacos具备服务优雅上下线和流量管理(API+后台管理页面),而Eureka的后台页面仅供展示,需要使用api操作上下线且不具备流量管理功能。
(2)从部署来看,Nacos整合了注册中心、配置中心功能,把原来两套集群整合成一套,简化了部署维护
(3)从长远来看,Eureka开源工作已停止,后续不再有更新和维护,而Nacos在以后的版本会支持SpringCLoud+Kubernetes的组合,填补 2 者的鸿沟,在两套体系下可以采用同一套服务发现和配置管理的解决方案,这将大大的简化使用和维护的成本。同时来说,Nacos 计划实现 Service Mesh,是未来微服务的趋势
(4)从伸缩性和扩展性来看Nacos支持跨注册中心同步,而Eureka不支持,且在伸缩扩容方面,Nacos比Eureka更优(nacos支持大数量级的集群)。
(5)Nacos具有分组隔离功能,一套Nacos集群可以支撑多项目、多环境。
nacos服务注册源码分析
我们以spring cloud 整合nocos为例子,当我们引入nacos start依赖后,查看spring.factories,通过配置的自动加载配置,实现去执行NacosServiceRegistry进行服务注册
NacosServiceRegistry实现了ServiceRegistry,这个ServiceRegistry接口是SpringCloud提供的服务注册的标准,集成到SpringCloud中实现服务注册的组件,都需要实现这个接口。
那么对于Nacos而言,该接口的实现类是NacosServiceRegistry
spring.factories
主要是包含了自动装配的配置信息,如图:
在spring.factories中配置EnableAutoConfiguration的内容后,项目在启动的时候,会导入相应的自动配置类,那么也就允许对该类的相关属性进行一个自动装配。
NacosDiscoveryAutoConfiguration发现当中对三个对象进行了装载
- NacosServiceRegistry:完成服务注册,实现ServiceRegistry,这样才能被调用,因为AbstractAutoServiceRegistration中的serviceRegistry属性就是接口ServiceRegistry类型,必须实现它
- NacosRegistration:用来注册时存储nacos服务端的相关信息
- NacosAutoServiceRegistration 继承spring中的AbstractAutoServiceRegistration,装载NacosAutoServiceRegistration的时候,会调用构造方法设置AbstractAutoServiceRegistration中的serviceRegistry属性为NacosServiceRegistry,AbstractAutoServiceRegistration继承ApplicationListener<WebServerInitializedEvent>,通过事件监听来发起服务注册,调用serviceRegistry属性进行注册,到时候就会调用NacosServiceRegistry.register(registration)
整个调用链路:
第一步:springboot在启动main方法时调用到spring的核心方法refresh
第二步: spring的refresh方法会在最后的流程中finishRefresh(),而由于是Springboot的方式启动时初始化的applictionContext是AnnotationConfigServletWebServerApplicationContext,并且该类是继承于ServletWebServerApplicationContext,ServletWebServerApplicationContext重写了finishRefresh方法,Spring在调用finishRefresh最终会调到子类的ServletWebServerApplicationContext当中的finishRefresh方法。
第三步:ServletWebServerApplicationContext在处理完父类的finishRefresh的时候会去发布一个ServletWebServerInitializedEvent事件,该事件继承于WebServerInitializedEvent,而最终nacos就是去监听该事件来进行注册的
第四步:spring在发送事件之后最终由AbstractApplicationContext将该事件交给ApplicationEventMulticaster将事件广播到适定的监听器。
第五步:AbstractApplicationContext的实现类SimpleApplicationEventMulticaster当中的multicastEvent中会去获取当前处理该事件的所有的监听器,并执行最终的事件处理
第六步:最后根据注册的事件,该事件的对应的监听器会实现onApplicationEvent方法,调到对应的实现上,AbstractAutoServiceRegistration会去监听前面所提到的WebServerInitializedEvent事件,最终会调到 AbstractAutoServiceRegistration中的onApplicationEvent方法
第七步:AbstractAutoServiceRegistration经过一系列的跳转调用最终调用到ServiceRegistry中的register方法。而ServiceRegistry.register实际上就是spring cloud为各个注册中心所制定的标准,要想使服务注册,那么须各个注册中心的客户端去实现该方法
第八步:在这里nacos的注册流程就完全的清楚了,由于使用了唯一的注册中心nacos,而恰巧nacos的注册流程是通过NacosServiceRegistry实现的,这里最终就调用到了nacos的注册流程。
第九步:nacos的注册流程已经走完了,其实还有个疑问,那么spring cloud中AbstractAutoServiceRegistration又是怎么初始话的呢,翻开spring cloud common的/META-INFO/spring.factories中配置AutoServiceRegistrationAutoConfiguration,而通过该类对AbstractAutoServiceRegistration进行了初始化。
服务注册处理
当nacos进行服务注册的时候,NacosServiceRegistry.class会调用register()方法进行服务注册,该方法中调用了namingService.registerInstance()方法进行服务注册的逻辑。
serviceId对应当前应用的application.name,instance表示服务实例信息
NacosNamingService实现了NamingService的接口;然后在namingService.registerInstance()方法中,会做两件事情:
- 如果当前注册的是临时节点,则构建心跳信息,通过beat反应堆来构建心跳任务
- 调用registerService发起服务注册
心跳机制先不看,后面在看,然后调用 NamingProxy 的注册方法进行注册,代码逻辑很简单,构建请求参数,发起请求。
然后我们直接往下走,可以看到服务注册的时候会轮询配置的注册中心的地址,进行注册,
最后通过 callServer(api, params, server, method) 发起调用,这里通过 JSK自带的 HttpURLConnection 进行发起调用。我们可以通过断点的方式来看到这里的请求参数:
期间可能会有多个 GET的请求获取服务列表,是正常的,会发现有如上的一个请求,会调用 /nacos/v1/ns/instance 这个地址。那么接下去就是Nacos Server 接受到服务端的注册请求的处理流程。
nacos注册中心处理
需要下载Nacos Server 源码,服务端提供了一个InstanceController类,在这个类中提供了服务注册相关的API,而服务端发起注册时,调用的接口是: [post]: /nacos/v1/ns/instance ,serviceName: 代表客户端的项目名称 ,namespace: nacos 的namespace。
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
// 从请求中解析出instance实例
final Instance instance = parseInstance(request);
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
然后调用 ServiceManager 进行服务的注册
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
//创建一个空服务,在Nacos控制台服务列表展示的服务信息,实际上是初始化一个serviceMap,它是一个ConcurrentHashMap集合
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
//从serviceMap中,根据namespaceId和serviceName得到一个服务对象
Service service = getService(namespaceId, serviceName);
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
//调用addInstance创建一个服务实例
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
在创建空的服务实例的时候我们发现了存储实例的map:
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
//从serviceMap中获取服务对象
Service service = getService(namespaceId, serviceName);
if (service == null) {//如果为空。则初始化
Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
service = new Service();
service.setName(serviceName);
service.setNamespaceId(namespaceId);
service.setGroupName(NamingUtils.getGroupName(serviceName));
// now validate the service. if failed, exception will be thrown
service.setLastModifiedMillis(System.currentTimeMillis());
service.recalculateChecksum();
if (cluster != null) {
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
service.validate();
putServiceAndInit(service);
if (!local) {
addOrReplaceService(service);
}
}
在 getService 方法中我们发现了Map:
/**
* Map(namespace, Map(group::serviceName, Service)).
*/
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
通过注释我们可以知道,Nacos是通过不同的 namespace 来维护服务的,而每个namespace下有不同的group,不同的group下才有对应的Service ,再通过这个 serviceName 来确定服务实例。
第一次进来则会进入初始化,初始化完会调用 putServiceAndInit
private void putServiceAndInit(Service service) throws NacosException {
putService(service);//把服务信息保存到serviceMap集合
service.init();//建立心跳检测机制
//实现数据一致性监听,ephemeral(标识服务是否为临时服务,默认是持久化的,也就是true)=true表示采用raft协议,false表示采用Distro
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
consistencyService
.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}
获取到服务以后把服务实例添加到集合中,然后基于一致性协议进行数据的同步。然后调用 addInstance
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
// 组装key
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);
// 也就是上一步实现监听的类里添加注册服务
consistencyService.put(key, instances);
}
}
然后给服务注册方发送注册成功的响应。结束服务注册流程。
看下流程图
心跳机制
nacos客户端
只有NACOS服务与所注册的Instance之间才会有直接的心跳维持机制,换言之,这是一种典型的集中式管理机制
在client这一侧是心跳的发起源,进入NacosNamingService,可以发现,注册服务实例的时候才会构造心跳包,前面分析服务注册的时候已经看到在哪里了。
没有特殊情况,目前ephemeral都是true。BeatReactor维护了一个Map对象,记录了需要发送心跳的BeatInfo,构造了一个心跳包后,BeatReactor.addBeatInfo方法将BeatInfo放入Map中。
BeatReactor的构造函数中创建了一个ScheduledExecutorService线程操作对象,在里面执行了一个线程操作,BeatTask线程,然后在BeatTask线程中调用了sendBeat()方法,将心跳包作为参数;
BeatTask线程操作:调用sendBeat()方法
在sendBeat()方法中,通过http服务,/instance/beat,调用了服务端InstanceController.beat()方法。
nacos服务端
在服务端的心跳接口,InstanceController.beat内,会判断实例是否存在,如果不存在,会重新注册。(如网络不通导致实例在服务端被下线,或服务端重启临时实例丢失)
然后,执行service.processClientBeat(clientBeat)方法,调用一个线程任务
在该任务中,将上次的心跳时间,设置为当前时间
至此,nacos发送心跳的过程就到此结束。
这个最后的心跳时间有什么用呢,有个定时任务每5s会检查是不是太久没心跳,如果没心跳就下线掉这个实例。
调用流程: