简介
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。
对应的版本依赖
主要功能
- 服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
- 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
- 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
- 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。。
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所Worker(schedulerx-client)上执行。
- 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
组件
[Sentinel] :阿里巴巴源产品,把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
[Nacos] :一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
[RocketMQ] :一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
[Dubbo] :Apache Dubbo™ 是一款高性能 Java RPC 框架。
[Seata] :阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
[Alibaba Cloud OSS] : 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
[Alibaba Cloud SchedulerX]: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
[Alibaba Cloud SMS] : 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
Nacos介绍
Nacos(Naming Configuration Service) 是一个易于使用的动态服务发现、配置和服务管理平台,用于构建云原生应用程序
服务发现是微服务架构中的关键组件之一。Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
什么是Nacos
- Nacos = 注册中心+配置中心组合
- Nacos支持几乎所有主流类型的“服务”的发现、配置和管理,常见的服务如下:
Kubernetes Service
gRPC & Dubbo RPC Service
Spring Cloud RESTful Service
Nacos下载和安装
官网网址:https://nacos.io/zh-cn/index.html
官网文档网址:https://nacos.io/zh-cn/docs/quick-start.html
下载地址:https://github.com/alibaba/nacos/releases
安装过程
执行指令
Linux/Unix/Mac
启动命令(standalone代表着单机模式运行,非集群模式):
sh startup.sh -m standalone
Windows
启动命令(standalone代表着单机模式运行,非集群模式):
startup.cmd -m standalone
默认的账号密码是:nacos/nacos
Nacos代替Eureka
Nacos可以直接提供注册中心(Eureka)+配置中心(Config),所以它的好处显而易见,我们在上节课成功安装和启动了Nacos以后就可以发现Nacos本身就是一个小平台,它要比之前的Eureka更加方便,不需要我们在自己做配置。
Nacos服务注册中心
服务发现是微服务架构中的关键组件之一。在这样的架构中,手动为每个客户端配置服务列表可能是一项艰巨的任务,并且使得动态扩展极其困难。Nacos Discovery 帮助您自动将您的服务注册到 Nacos 服务器,Nacos 服务器会跟踪服务并动态刷新服务列表。此外,Nacos Discovery 将服务实例的一些元数据,如主机、端口、健康检查 URL、主页等注册到 Nacos。
学习任何知识我们都需要从它的官方文档入手,所以我们直接来看官网给我们提供的文档:https://spring.io/projects/spring-cloud-alibaba#learn
各种服务注册中心对比
服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
---|---|---|---|
Eureka | AP | 支持 | 低 |
Zookeeper | CP | 不支持 | 中 |
Consul | CP | 支持 | 高 |
Nacos | AP/CP | 支持 | 高 |
CAP模型
一致性(Consistency): 同一时刻的同一请求的实例返回的结果相同,所有的数据要求具有强一致性(Strong Consistency)
可用性(Availablity): 所有实例的读写请求在一定时间内可用得到正确的响应
分区容错性(Partition tolerance): 在网络异常的情况下,系统仍能通过正常的服务
以上三个特点就是CAP原则(又称CAP定理),但是三个特性不可能同时满足,所以分布式系统设计要考虑的是在满足P(分区容错性)的前提下选择C(一致性)还是A(可用性),即:CP或AP
CP原则: 一致性 + 分区容错性原则
CP 原则属于强一致性原则,要求所有节点可以查询的数据随时都要保持一直(同步中的数据不可查询),即:若干个节点形成一个逻辑的共享区域,某一个节点更新的数据都会立即同步到其他数据节点之中,当数据同步完成后才能返回成功的结果,但是在实际的运行过程中网络故障在所难免,如果此时若干个服务节点之间无法通讯时就会出现错误,从而牺牲了以可用性原则(A),例如关系型数据库中的事务。
AP原则:可用性原则 + 分区容错性原则
AP原则属于弱一致性原则,在集群中只要有存活的节点那么所发送来的所有请求都可以得到正确的响应,在进行数据同步处理操作中即便某些节点没有成功的实现数据同步也返回成功,这样就牺牲一致性原则(C 原则)。
使用场景:对于数据的同步一定会发出指令,但是最终的节点是否真的实现了同步,并不保证,可是却可以及时的得到数据更新成功的响应,可以应用在网络环境不是很好的场景中。
Nacos支持CP和AP
Nacos无缝支持一些主流的开源生态,同时再阿里进行Nacos设计的时候重复的考虑到了市场化的运作(市面上大多都是以单一的实现形式为主,例如:Zookeeper使用的是 CP、而 Eureka采用的是AP),在Nacos中提供了两种模式的动态切换。
Nacos 何时选择切换模式
- 一般来说,如果不需要储存服务界别的信息且服务实例通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。如Spring Cloud 和 Dubbo,都适用于AP模式,AP模式为了服务的可用性减弱了一致性,因此AP模式下只支持注册临时实例。
- 如果需要在服务级别编辑或者储存配置信息,那么CP是必须的,K8S服务和DNS服务则是用于CP模式。CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
- 切换命令(默认是AP):
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
Nacos之服务配置中心
Nacos不仅仅可以作为注册中心来使用,同时它支持作为配置中心
pom文件
这里我们主要要引入的是此依赖,这个依赖依据在官网上可以找到:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_an_example_of_using_nacos_discovery_for_service_registrationdiscovery_and_call
<dependency>
<groupId> com.alibaba.cloud </groupId>
<artifactId> spring-cloud-starter-alibaba-nacos-config </artifactId>
</dependency>
YML配置
要注意的是这里我们要配置两个,因为Nacos同SpringCloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
分别要配置的是,这里bootstrap.yml配置好了以后,作用是两个,第一个让3377这个服务注册到Nacos中,第二个作用就是去Nacos中去读取指定后缀为yaml的配置文件:
bootstrap.yml
# nacos配置
server:
port: 3377
spring:
profiles:
active: dev
application:
name: nacos-config-client # 微服务名称
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #Nacos服务注册中心地址
config:
server-addr: 127.0.0.1:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
group: DEV_GROUP #分组名
namespace: 6182321e-7fb1-40b7-9799-11ecfbab2192 #命名空间ID
# ${spring.application.name}-${spring.profiles.active}.${file-extension}
# nacos-config-client-dev.yaml
# 微服务名称-当前环境-文件格式
业务类
这里的 @RefreshScope 实现配置自动更新,意思为如果想要使配置文件中的配置修改后不用重启项目即生效,可以使用 @RefreshScope 配置来实现
@RestController
@RefreshScope //支持Nacos的动态刷新功能
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo(){
return configInfo;
}
}
Nacos配置规则
1. `prefix` 默认为 `spring.application.name` 的值,也可以通过配置项 `spring.cloud.nacos.config.prefix`来配置。
2. `spring.profiles.active` 即为当前环境对应的 profile,注意:**当 `spring.profiles.active` 为空时,对应的连接符 `-` 也将不存在,dataId 的拼接格式变成 `${prefix}.${file-extension}`**(不能删除)
3. `file-exetension` 为配置内容的数据格式,可以通过配置项 `spring.cloud.nacos.config.file-extension` 来配置。目前只支持 `properties` 和 `yaml` 类型。
4. 通过 Spring Cloud 原生注解 `@RefreshScope` 实现配置自动更新:
5. 所以根据官方给出的规则我们最终需要在Nacos配置中心添加的配置文件的名字规则和名字为:
# ${spring.application.name}-${spring.profiles.active}.${file-extension}
# nacos-config-client-dev.yaml
# 微服务名称-当前环境-文件格式
Nacos命名空间分组和DataID三者关系
命名空间(Namespace)
用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等。
配置分组(Group)
Nacos 中的一组配置集,是组织配置的维度之一。通过一个有意义的字符串(如 Buy 或 Trade )对配置集进行分组,从而区分 Data ID 相同的配置集。当您在 Nacos 上创建一个配置时,如果未填写配置分组的名称,则配置分组的名称默认采用 DEFAULT_GROUP 。配置分组的常见场景:不同的应用或组件使用了相同的配置类型,如 database_url 配置和 MQ_topic 配置。
配置集 ID(Data ID)
Nacos 中的某个配置集的 ID。配置集 ID 是组织划分配置的维度之一。Data ID 通常用于组织划分系统的配置集。一个系统或者应用可以包含多个配置集,每个配置集都可以被一个有意义的名称标识。Data ID 通常采用类 Java 包(如 com.taobao.tc.refund.log.level)的命名规则保证全局唯一性。此命名规则非强制。
配置集:一组相关或者不相关的配置项的集合称为配置集。在系统中,一个配置文件通常就是一个配置集,包含了系统各个方面的配置。例如,一个配置集可能包含了数据源、线程池、日志级别等配置项。
三者关系
Nacos源码
Nacos核心功能点
服务注册(重点):Nacos Client会通过发送Rest请求的方式向Nacos Server 注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接受到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中
服务心跳:在服务注册后,Nacos Client 会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳
服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30s没收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
服务发现(重点):服务消费者(Nacos Client) 在调用服务提供者的服务时,会发生一个rest请求给Nacos Server,获取上面注册的服务清单,并且缓存Nacos Client本地,同时会在Nacos Client本地开启一个定时任务拉取服务端最新的注册表信息并更新到本地缓存
(消费端发请求给服务端,获取对应的服务信息,并开启一个定时调度任务获取最新的服务信息)
服务同步:Nacos Server集群之间会相互同步服务实例,用来保证服务信息的一致性
Nacos源码模块图
服务信息注册
我们从Nacos-Client开始说起,那么说到客户端就涉及到服务注册,我们先了解一下Nacos客户端都会将什么信息传递给服务器,我们直接从Nacos Client项目的NamingTest说起
public void testServiceList() throws Exception {
//Nacos Server连接信息
Properties properties = new Properties();
//Nacos服务器地址,属性的key为serverAddr;
properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
//连接Nacos服务的用户名,属性key为username,默认值为nacos;
properties.put(PropertyKeyConst.USERNAME, "nacos");
//连接Nacos服务的密码,属性key为password,默认值为nacos;
properties.put(PropertyKeyConst.PASSWORD, "nacos");
//创建实例
Instance instance = new Instance();
//设置实例的信息
instance.setIp("1.1.1.1");//ip地址
instance.setPort(800);//端口号
instance.setWeight(2);//权重
Map<String, String> map = new HashMap<String, String>();
//元数据信息
map.put("netType", "external");//网络类型 external外网
map.put("version", "2.0");//版本号
instance.setMetadata(map);
//核心 nacos工厂创建服务
NamingService namingService = NacosFactory.createNamingService(properties);
//注册
namingService.registerInstance("nacos.test.1", instance);
ThreadUtils.sleep(5000L);
List<Instance> list = namingService.getAllInstances("nacos.test.1");
System.out.println(list);
ThreadUtils.sleep(30000L);
// ExpressionSelector expressionSelector = new ExpressionSelector();
// expressionSelector.setExpression("INSTANCE.metadata.registerSource = 'dubbo'");
// ListView<String> serviceList = namingService.getServicesOfServer(1, 10, expressionSelector);
}
Instance类核心部分
//注册实例信息用Instance对象承载,注册的实例信息又分两部分:实例基础信息和元数据。
public class Instance implements Serializable {
private static final long serialVersionUID = -742906310567291979L;
/**
* unique id of this instance.
* 实例的唯一ID;
*/
private String instanceId;
/**
* instance ip.
* 实例IP,提供给消费者进行通信的地址;
*/
private String ip;
/**
* instance port.
* 端口,提供给消费者访问的端口;
*/
private int port;
/**
* instance weight.
* 权重,当前实例的权限,浮点类型(默认1.0D);
*/
private double weight = 1.0D;
/**
* instance health status.
* 健康状况,默认true;
*/
private boolean healthy = true;
/**
* If instance is enabled to accept request.
* 实例是否准备好接收请求,默认true;
*/
private boolean enabled = true;
/**
* If instance is ephemeral.
*
* @since 1.0.0
* 实例是否为瞬时的,默认为true;
*/
private boolean ephemeral = true;
/**
* cluster information of instance.
* 实例所属的集群名称;
*/
private String clusterName;
/**
* Service information of instance.
* 实例的服务信息;
*/
private String serviceName;
/**
* user extended attributes.
* 存储元数据的信息
* netType:顾名思义,网络类型,这里的值为external,也就是外网的意思;
* version:版本,Nacos的版本,这里是2.0这个大版本。
*/
private Map<String, String> metadata = new HashMap<String, String>();
/**
* add meta data.
*
* @param key meta data key
* @param value meta data value
*/
public void addMetadata(final String key, final String value) {
if (metadata == null) {
metadata = new HashMap<String, String>(4);
}
metadata.put(key, value);
}
public String toInetAddr() {
return ip + ":" + port;
}
/**
* 获得心跳间隙
* 心跳间隙的key,默认为5s,也就是默认5秒进行一次心跳;
* @return
*/
public long getInstanceHeartBeatInterval() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,
Constants.DEFAULT_HEART_BEAT_INTERVAL);
}
/**
* 获得心跳超时
* 心跳超时的key,默认为15s,也就是默认15秒收不到心跳,实例将会标记为不健康;
* @return
*/
public long getInstanceHeartBeatTimeOut() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_TIMEOUT,
Constants.DEFAULT_HEART_BEAT_TIMEOUT);
}
/**
* 删除实例
* @return
* 实例IP被删除的key,默认为30s,也就是30秒收不到心跳,实例将会被移除;
*/
public long getIpDeleteTimeout() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.IP_DELETE_TIMEOUT,
Constants.DEFAULT_IP_DELETE_TIMEOUT);
}
/**
* 实例id生成器
* 实例ID生成器key,默认为simple;
* @return
*/
public String getInstanceIdGenerator() {
return getMetaDataByKeyWithDefault(PreservedMetadataKeys.INSTANCE_ID_GENERATOR,
Constants.DEFAULT_INSTANCE_ID_GENERATOR);
}
/**
* Returns {@code true} if this metadata contains the specified key.
*
* @param key metadata key
* @return {@code true} if this metadata contains the specified key
*/
public boolean containsMetadata(final String key) {
if (getMetadata() == null || getMetadata().isEmpty()) {
return false;
}
return getMetadata().containsKey(key);
}
private long getMetaDataByKeyWithDefault(final String key, final long defaultValue) {
if (getMetadata() == null || getMetadata().isEmpty()) {
return defaultValue;
}
final String value = getMetadata().get(key);
if (!StringUtils.isEmpty(value) && value.matches(NUMBER_PATTERN)) {
return Long.parseLong(value);
}
return defaultValue;
}
private String getMetaDataByKeyWithDefault(final String key, final String defaultValue) {
if (getMetadata() == null || getMetadata().isEmpty()) {
return defaultValue;
}
return getMetadata().get(key);
}
}
public static NamingService createNamingService(Properties properties) throws NacosException {
try {
//利用反射机制创建 NamingService
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
NamingService vendorImpl = (NamingService) constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
}
}
@Override
public void registerInstance(String serviceName, Instance instance) throws NacosException {
registerInstance(serviceName, Constants.DEFAULT_GROUP, instance);
}
/**
*
* @param serviceName name of service
* @param groupName group of service 分组名
* @param instance instance to register
* @throws NacosException
*/
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//检查心跳时间是否正确
NamingUtils.checkInstanceIsLegal(instance);
//通过代理注册服务实例
clientProxy.registerService(serviceName, groupName, instance);
}
检查心跳时间是否正确
/**
* <p>Check instance param about keep alive.</p>
*
* <pre>
* heart beat timeout must > heart beat interval
* ip delete timeout must > heart beat interval
* </pre>
*
* @param instance need checked instance
* @throws NacosException if check failed, throw exception
*/
public static void checkInstanceIsLegal(Instance instance) throws NacosException {
//间隙时间大于心跳超时时间 //间隙时间大于实例移除时间
if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()
|| instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) {
throw new NacosException(NacosException.INVALID_PARAM,
"Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'.");
}
}
通过代理注册服务实例
@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}
private NamingClientProxy getExecuteClientProxy(Instance instance) {
//实例是否为瞬时的,默认为true
return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
}
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName, instance);
//缓存数据 ConcurrentMap进行缓存
redoService.cacheInstanceForRedo(serviceName, groupName, instance);
//基于gRPC进行服务的调用
doRegisterService(serviceName, groupName, instance);
}
//缓存数据
/**
* Cache registered instance for redo.
* @param serviceName service name
* @param groupName group name
* @param instance registered instance
*/
public void cacheInstanceForRedo(String serviceName, String groupName, Instance instance) {
String key = NamingUtils.getGroupedName(serviceName, groupName);
InstanceRedoData redoData = InstanceRedoData.build(serviceName, groupName, instance);
synchronized (registeredInstances) {
registeredInstances.put(key, redoData);
}
}
//基于gRPC进行服务的调用
/**
* Execute register operation.
* @param serviceName name of service
* @param groupName group of service
* @param instance instance to register
* @throws NacosException nacos exception
*/
public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {
InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,
NamingRemoteConstants.REGISTER_INSTANCE, instance);
requestToServer(request, Response.class);
redoService.instanceRegistered(serviceName, groupName);
}
总结nacos流程
封装服务信息Instance
实例化NamingService
选择通信方式 Grpc还是http
请求nacos注册实例(GRPC调用服务端实例注册)
依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
找到SpringBoot自动装配文件META-INF/spring.factories文件
最核心的是“NacosAutoServiceRegistration”
NacosAutoServiceRegistration这个类就是注册的核心
继承关系图
NacosAutoServiceRegistration继承了AbstractAutoServiceRegistration而这个类型实现了ApplicationListener接口,所以我们由此得出一般实现ApplicationListener接口的类型都会实现一个方法"onApplicationEvent",这个方法会在项目启动的时候触发
public void onApplicationEvent(WebServerInitializedEvent event) {
this.bind(event);
}
/** @deprecated */
@Deprecated
public void bind(WebServerInitializedEvent event) {
ApplicationContext context = event.getApplicationContext();
if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
this.port.compareAndSet(0, event.getWebServer().getPort());
this.start();//开启注册
}
}
/*
* 在start()方法中调用register()方法来注册服务
*/
public void start() {
if (!this.isEnabled()) {
if (logger.isDebugEnabled()) {
logger.debug("Discovery Lifecycle disabled. Not starting");
}
} else {
if (!this.running.get()) {
this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
this.register();
if (this.shouldRegisterManagement()) {
this.registerManagement();
}
this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
this.running.compareAndSet(false, true);
}
}
}
serviceRegistry.register
protected void register() {
this.serviceRegistry.register(getRegistration());
}
serviceRegistry接口的具体实现类NacosServiceRegistry:
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
} else {
NamingService namingService = this.namingService();
String serviceId = registration.getServiceId();
String group = this.nacosDiscoveryProperties.getGroup();
//构建instance实例
Instance instance = this.getNacosInstanceFromRegistration(registration);
try {
//向服务端注册此服务
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", new Object[]{group, serviceId, instance.getIp(), instance.getPort()});
} catch (Exception var7) {
log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var7});
ReflectionUtils.rethrowRuntimeException(var7);
}
}
}
调用接口注册
namingService.registerInstance(serviceId, group, instance);
总结
Nacos服务端服务注册源码
NamingService.registerInstance这个方法是客户端来完成实例的注册
@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + UtilsAndCommons.NACOS_NAMING_INSTANCE_CONTEXT)
public class InstanceController {
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
//还原成instance信息
final Instance instance = HttpRequestInstanceBuilder.newBuilder()
.setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();
//注册服务实例
getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
return "ok";
}
}
getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
private InstanceOperator getInstanceOperator() {//判断是否是grpc协议
return upgradeJudgement.isUseGrpcFeatures() ? instanceServiceV2 : instanceServiceV1;
}
服务注册
instanceServiceV2.registerInstance
@Override
public void registerInstance(String namespaceId, String serviceName, Instance instance) {
//判断是否为瞬时对象(临时客户端)
boolean ephemeral = instance.isEphemeral();
//获取客户端ID
String clientId = IpPortBasedClient.getClientId(instance.toInetAddr(), ephemeral);
//通过客户端ID创建客户端连接
createIpPortClientIfAbsent(clientId);
//获取服务
Service service = getService(namespaceId, serviceName, ephemeral);
//具体注册服务
clientOperationService.registerInstance(service, instance, clientId);
}
Nacos2.0以后新增Client模型。一个客户端gRPC长连接对应一个Client,每个Client有自己唯一的id(clientId)。Client负责管理一个客户端的服务实例注册Publish和服务订阅Subscribe。
EphemeralClientOperationServiceImpl.registerInstance 具体注册服务
@Override
public void registerInstance(Service service, Instance instance, String clientId) {
//确保Service单例存在
Service singleton = ServiceManager.getInstance().getSingleton(service);
if (!singleton.isEphemeral()) {
throw new NacosRuntimeException(NacosException.INVALID_PARAM,
String.format("Current service %s is persistent service, can't register ephemeral instance.",
singleton.getGroupedServiceName()));
}
//根据客户端id,找到客户端Client
Client client = clientManager.getClient(clientId);
if (!clientIsLegal(client, clientId)) {
return;
}
//客户端Instance模型,转换为服务端Instance模型
InstancePublishInfo instanceInfo = getPublishInfo(instance);
//将Instance储存到Client里
//对于单个客户端来说,同一个服务只能注册一个实例
client.addServiceInstance(singleton, instanceInfo);
client.setLastUpdatedTime();
//建立Service与ClientId的关系
NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
NotifyCenter.publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), false));
}
确保Service单例存在
public class ServiceManager {
private static final ServiceManager INSTANCE = new ServiceManager();
//单例Service,可以查看Service的equals和hasCode方法
private final ConcurrentHashMap<Service, Service> singletonRepository;
//namespace下的所有service
private final ConcurrentHashMap<String, Set<Service>> namespaceSingletonMaps;
/**
* Get singleton service. Put to manager if no singleton.
* 通过Map储存单例的Service
* @param service new service
* @return if service is exist, return exist service, otherwise return new service
*/
public Service getSingleton(Service service) {
singletonRepository.putIfAbsent(service, service);
Service result = singletonRepository.get(service);
namespaceSingletonMaps.computeIfAbsent(result.getNamespace(), (namespace) -> new ConcurrentHashSet<>());
namespaceSingletonMaps.get(result.getNamespace()).add(result);
return result;
}
}
将Instance储存到Client里client.addServiceInstance(singleton, instanceInfo);
@Override
public boolean addServiceInstance(Service service, InstancePublishInfo instancePublishInfo) {
if (null == publishers.put(service, instancePublishInfo)) {
MetricsMonitor.incrementInstanceCount();
}
NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(this));
Loggers.SRV_LOG.info("Client change for service {}, {}", service, getClientId());
return true;
}
建立Service与ClientId的关系
NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
private void handleClientOperation(ClientOperationEvent event) {
Service service = event.getService();
String clientId = event.getClientId();
if (event instanceof ClientOperationEvent.ClientRegisterServiceEvent) {
//建立Service与发布Client的关系
addPublisherIndexes(service, clientId);
} else if (event instanceof ClientOperationEvent.ClientDeregisterServiceEvent) {
removePublisherIndexes(service, clientId);
} else if (event instanceof ClientOperationEvent.ClientSubscribeServiceEvent) {
addSubscriberIndexes(service, clientId);
} else if (event instanceof ClientOperationEvent.ClientUnsubscribeServiceEvent) {
removeSubscriberIndexes(service, clientId);
}
}
/**
* 建立Service与发布Client的关系
* @param service
* @param clientId
*/
private void addPublisherIndexes(Service service, String clientId) {
publisherIndexes.computeIfAbsent(service, (key) -> new ConcurrentHashSet<>());
publisherIndexes.get(service).add(clientId);
NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true));
}
总结服务端流程
1.获取服务Service,并校验是否存在对应的单例Service集群,不存在的话,创建concurrentHashSet存储,反之添加进set中
2.获取客户端ID,通过客户端ID创建客户端连接
3.客户端Instance模型,转换为服务端Instance模型,储存到Client里
4.建立Service与ClientId的关系
(核心就是service,client,instance建立联系)
Nacos服务端健康检查
长连接,指在一个连接上可以连续发送多个数据包 在连接保持期间,如果没有数据包发送,需要双方发链路检测包
注册中心Nacos客户端2.0之后使用gRPC代替http,会与服务端建立长连接,但仍然保留了对旧http客户端的支持。
健康检查
在1.x的版本中临时实例走Distro写内存存储,客户端向注册中心发送心跳来维持自身的healthy状态,持久化实例走raft写持久化存储,服务端定时与客户端建立tcp连接做健康检查。
2.0版本后持久化实例没什么变化,但是2.0临时实例不在使用心跳,而是通过长连接是否存活来判断是否健康
ConnectionManager负责管理所有客户端的长连接
每3s检测所有超过20s没发生过通讯的客户端,向客户端发起ClientDetectionRequest探测请求,如果在1s内客户端成功响应,则检测通过,否则执行unregister方法移除connection
(超过20s没有通讯的客户端会被移除)
如果客户端持续与服务端通讯,服务端是不需要主动探活的
Nacos客户端服务发现
public class NamingTest {
@Test
public void testServiceList() throws Exception {
......
//getAllInstances 服务发现入口
List<Instance> list = namingService.getAllInstances("nacos.test.1");
System.out.println(list);
}
}
这里的方法比入口多出了几个参数,这里不仅有服务名称,还有分组名、集群列表、是否订阅,重载方法中的其他参数已经在各种重载方法的调用过程中设置了默认值,比如:
分组名称默认:DEFAULT_GROUOP
集群列表:默认为空数组
是否订阅:订阅
@Override
public List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters,
boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
String clusterString = StringUtils.join(clusters, ",");
// 是否是订阅模式 默认是true
if (subscribe) {
// 先从客户端缓存获取服务信息
serviceInfo = serviceInfoHolder.getServiceInfo(serviceName, groupName, clusterString);
if (null == serviceInfo || !clientProxy.isSubscribed(serviceName, groupName, clusterString)) {
// 如果未命中,则进行订阅 首次订阅
serviceInfo = clientProxy.subscribe(serviceName, groupName, clusterString);
}
} else {
// 如果未订阅服务信息,则直接从服务器进行查询
serviceInfo = clientProxy.queryInstancesOfService(serviceName, groupName, clusterString, 0, false);
}
// 从服务信息中获取实例列表
List<Instance> list;
if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) {
return new ArrayList<Instance>();
}
return list;
}
流程的基本逻辑
如果是订阅模式,则直接从客户端本地缓存中获取服务信息,如果服务信息为空,则进行订阅,
如果是非订阅模式,那就直接通过rpc请求服务端,获取服务信息,缓存至客户端缓存中
@Override
public ServiceInfo subscribe(String serviceName, String groupName, String clusters) throws NacosException {
NAMING_LOGGER.info("[SUBSCRIBE-SERVICE] service:{}, group:{}, clusters:{} ", serviceName, groupName, clusters);
String serviceNameWithGroup = NamingUtils.getGroupedName(serviceName, groupName);
String serviceKey = ServiceInfo.getKey(serviceNameWithGroup, clusters);
// 定时调度UpdateTask
serviceInfoUpdateService.scheduleUpdateIfAbsent(serviceName, groupName, clusters);
// 获取缓存中的ServiceInfo
ServiceInfo result = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
if (null == result || !isSubscribed(serviceName, groupName, clusters)) {
// 如果为null,则进行订阅逻辑处理,基于gRPC协议
result = grpcClientProxy.subscribe(serviceName, groupName, clusters);
}
// ServiceInfo本地缓存处理
serviceInfoHolder.processServiceInfo(result);
return result;
}
订阅流程
1.开启定时调度任务
2.判断本地缓存是否存在,存在serviceinfo直接返回。反之基于gRPC协议进行订阅,向服务端器发送订阅请求,并返回ServiceInfo
3.将服务信息缓存至本地客户端
服务发现大致流程图
Nacos服务订阅机制的核心流程
Nacos订阅机制
Nacos客户端通过一个定时任务,每6秒从注册中心获取实例列表,当发现实例变化时,发布变更事件,调用者进行业务处理(更新实例,更改本地缓存)。(观察者模式)
UpdateTask
@Override
public void run() {
long delayTime = DEFAULT_DELAY;
try {
// 判断是服务是否订阅 如果订阅过直接返回
if (!changeNotifier.isSubscribed(groupName, serviceName, clusters) && !futureMap.containsKey(
serviceKey)) {
NAMING_LOGGER.info("update task is stopped, service:{}, clusters:{}", groupedServiceName, clusters);
isCancel = true;
return;
}
// 获取缓存的service信息
ServiceInfo serviceObj = serviceInfoHolder.getServiceInfoMap().get(serviceKey);
// 如果为空
if (serviceObj == null) {
// 根据serviceName从注册中心服务端获取Service信息
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
// 处理本地缓存
serviceInfoHolder.processServiceInfo(serviceObj);
lastRefTime = serviceObj.getLastRefTime();
return;
}
// 过期服务,服务的最新更新时间小于等于缓存刷新
// (最后一次拉取数据的时间)时间,从注册中心重新查询
/*是否过期*/
if (serviceObj.getLastRefTime() <= lastRefTime) {
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
// 处理本地缓存
serviceInfoHolder.processServiceInfo(serviceObj);
}
//刷新更新时间
lastRefTime = serviceObj.getLastRefTime();
if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
incFailCount();
return;
}
// 下次更新缓存时间设置,默认6秒
// TODO multiple time can be configured.
delayTime = serviceObj.getCacheMillis() * DEFAULT_UPDATE_CACHE_TIME_MULTIPLE;
// 重置失败数量为0(可能会出现失败情况,没有ServiceInfo,连接失败)
resetFailCount();
} catch (Throwable e) {
incFailCount();
NAMING_LOGGER.warn("[NA] failed to update serviceName: {}", groupedServiceName, e);
} finally {
if (!isCancel) {
// 下次调度刷新时间,下次执行的时间与failCount有关,failCount=0,则下次调度时间为6秒,最长为1分钟
// 即当无异常情况下缓存实例的刷新时间是6秒
executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60),
TimeUnit.MILLISECONDS);
}
}
}
订阅机制大致流程
1.是否开启过定时任务?开启过直接结束返回
2.查询缓存是否为空?为空的话通过servicename查询注册中心获取服务信息(serviceInfo),缓存至本地
3.服务信息是否过期?过期的话与缓存为空过程一致
4.刷新服务信息过期时间
5.计算下次任务时间
6.重置
Nacos客户端本地缓存及故障转移
ServiceInfoHolder
服务信息的持有者,包含了服务名称、分组名称、集群信息、实例列表信息,上次更新时间等。
serviceInfoHolder类有持有ServiceInfo,通过ConcurrentHashMap来存储
当实例发生变动是,向缓存中put最新数据即可。当实例使用时,根据ke进行get操作即可。
本地缓存目录
private void initCacheDir(String namespace, Properties properties) {
String jmSnapshotPath = System.getProperty(JM_SNAPSHOT_PATH_PROPERTY);
String namingCacheRegistryDir = "";
if (properties.getProperty(PropertyKeyConst.NAMING_CACHE_REGISTRY_DIR) != null) {
namingCacheRegistryDir = File.separator + properties.getProperty(PropertyKeyConst.NAMING_CACHE_REGISTRY_DIR);
}
if (!StringUtils.isBlank(jmSnapshotPath)) {
cacheDir = jmSnapshotPath + File.separator + FILE_PATH_NACOS + namingCacheRegistryDir
+ File.separator + FILE_PATH_NAMING + File.separator + namespace;
} else {
cacheDir = System.getProperty(USER_HOME_PROPERTY) + File.separator + FILE_PATH_NACOS + namingCacheRegistryDir
+ File.separator + FILE_PATH_NAMING + File.separator + namespace;
}
}
故障转移
在ServiceInfoHolder的构造方法中,还会初始化一个FailoverReactor类。FailoverReactor的作用便是用来处理故障转移的。
FailoverFileReader
Nacos集群数据同步
当有服务进行注册后,会写入注册信息同时会触发ClientChangeEvent事件,通过这个事件,会开始进行nacos的集群数据同步,当然只有一个nacos节点来处理对应的客户端请求,这其中还涉及到一个负责节点和非负责节点
负责节点
distroProtocol会循环所有其他nacos节点,提交一个异步任务,这个异步任务会延迟1s
delete事件
change事件
非负责节点
此时Client实现类ConnectionBasedClient,只不过它的isNative属性为false,这是非负责节点和负责节点的主要区别。
判断当前nacos节点是否为负责节点的依据就是这个isNative属性,如果是客户端直接注册在这个nacos节点上的ConnectionBasedClient,它的isNative属性为true;如果是由Distro协议,同步到这个nacos节点上的ConnectionBasedClient,它的isNative属性为false
nacos 2.x的版本以后使用了长连接,所以通过长连接建立在哪个节点上,哪个节点就是责任节点,客户端也只会向这个责任节点发送请求
Distro协议负责集群数据统一
Distro(阿里自定义的协议)为了确保集群间数据一致,不仅仅依赖于数据发生改变时的实时同步,后台有定时任务做数据同步。
在1.x版本中,责任节点每5s同步所有Service的Instance列表的摘要(md5)给非责任节点,非责任节点用对端传来的服务md5比对本地服务的md5,如果发生改变,需要反查责任节点。
在2.x版本中,对这个流程做了改造,责任节点会发送Client全量数据,非责任节点定时检测同步过来的Client是否过期,减少1.x版本中的反查。
责任节点每5s向其他节点发送DataOperation=VERIFY类型的DistroData,来维持非责任节点的Client数据不过期。
非责任节点每5s扫描isNative=false的client,如果client30s内没有被VERIFY的DistroData更新过续租时间,会删除这个同步过来的Client数据。