Nacos的服务注册表结构是怎样的?
了解Nacos的服务注册表结构
需要从两方面入手:一是Nacos的分级存储模型,二是Nacos的服务端源码
Nacos源码分析
一、下载Nacos源码并运行
需要下载源码自己编译来运行。
1.下载Nacos源码
Nacos的GitHub地址:https://github.com/alibaba/nacos
找到其release页面:https://github.com/alibaba/nacos/tags,找到其中的1.4.2.版本:
点击进入后,下载Source code(zip):
2.导入Demo工程
微服务Demo(cloud-source-demo),包含了服务注册、发现等业务。
导入该项目后,查看其项目结构:
结构说明:
cloud-source-demo:项目父目录
cloud-demo:微服务的父工程,管理微服务依赖
order-service:订单微服务,业务中需要访问user-service,是一个服务消费者
user-service:用户微服务,对外暴露根据id查询用户的接口,是一个服务提供者
3.导入Nacos源码
将之前下载好的Nacos源码解压到cloud-source-demo项目目录中:
然后,使用IDEA将其作为一个module来导入:
1)选择项目结构选项:
然后点击导入module:
在弹出窗口中,选择nacos源码目录:
然后选择maven模块,finish:
最后,点击OK即可:
导入后的项目结构:
4.proto编译
Nacos底层的数据通信会基于protobuf对数据做序列化和反序列化。并将对应的proto文件定义在了consistency这个子模块中:
我们需要先将proto文件编译为对应的Java代码。
1)什么是protobuf
protobuf的全称是Protocol Buffer,是Google提供的一种数据序列化协议,这是Google官方的定义:
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
可以简单理解为,是一种跨语言、跨平台的数据传输格式。与json的功能类似,但是无论是性能,还是数据大小都比json要好很多。
protobuf的之所以可以跨语言,就是因为数据定义的格式为 .proto 格式,需要基于protoc编译为对应的语言。
2)安装protoc
Protobuf的GitHub地址:https://github.com/protocolbuffers/protobuf/releases
可以下载windows版本的来使用:
解压到任意非中文目录下,其中的bin目录中的protoc.exe可以帮助我们编译:
然后将这个bin目录配置到你的环境变量path中,可以参考JDK的配置方式:
3)编译proto
进入nacos-1.4.2的consistency模块下的src/main目录下:
然后打开cmd窗口,运行下面的两个命令:
#powershell
protoc --java_out=./java ./proto/consistency.proto
protoc --java_out=./java ./proto/Data.proto
如图:
会在nacos的consistency模块中编译出这些java代码:
5.运行
nacos服务端的入口是在console模块中的Nacos类:
我们需要让它单机启动:
然后新建一个SpringBootApplication:
然后填写应用信息:
然后运行Nacos这个main函数:
将order-service和user-service服务启动后,可以查看nacos控制台:
二、服务注册
服务注册到Nacos以后,会保存在一个本地注册表中,其结构如下:
首先最外层是一个Map,结构为:`Map<String, Map<String, Service>>`:
- key:是namespace_id,起到环境隔离的作用。namespace下可以有多个group
- value:又是一个`Map<String, Service>`,代表分组及组内的服务。一个组内可以有多个服务
- key:代表group分组,不过作为key时格式是group_name:service_name
- value:分组下的某一个服务,例如userservice,用户服务。类型为`Service`,内部也包含一个`Map<String,Cluster>`,一个服务下可以有多个集群
- key:集群名称
- value:`Cluster`类型,包含集群的具体信息。一个集群中可能包含多个实例,也就是具体的节点信息,其中包含一个`Set<Instance>`,就是该集群下的实例的集合
- Instance:实例信息,包含实例的IP、Port、健康状态、权重等等信息
每一个服务去注册到Nacos时,就会把信息组织并存入这个Map中。
1.服务注册接口
Nacos提供了服务注册的API接口,客户端只需要向该接口发送请求,即可实现服务注册。
接口说明:注册一个实例到Nacos服务。
请求类型:POST
请求路径:/nacos/v1/ns/instance
请求参数:
错误编码:
2.客户端
首先,我们需要找到服务注册的入口。
1)NacosServiceRegistryAutoConfiguration
因为Nacos的客户端是基于SpringBoot的自动装配实现的,我们可以在nacos-discovery依赖:
spring-cloud-starter-alibaba-nacos-discovery-2.2.6.RELEASE.jar
这个包中找到Nacos自动装配信息:
可以看到,有很多个自动配置类被加载了,其中跟服务注册有关的就是NacosServiceRegistryAutoConfiguration这个类,我们跟入其中。
可以看到,在NacosServiceRegistryAutoConfiguration这个类中,包含一个跟自动注册有关的Bean:
2)NacosAutoServiceRegistration
NacosAutoServiceRegistration源码如图:
可以看到在初始化时,其父类AbstractAutoServiceRegistration也被初始化了。
AbstractAutoServiceRegistration如图:
可以看到它实现了`ApplicationListener`接口,监听Spring容器启动过程中的事件。
在监听到 WebServerInitializedEvent(web服务初始化完成)的事件后,执行了`bind` 方法。
其中的bind方法如下:
public void bind(WebServerInitializedEvent event) {
// 获取 ApplicationContext
ApplicationContext context = event.getApplicationContext();
// 判断服务的 namespace,一般都是null
if (context instanceof ConfigurableWebServerApplicationContext) {
if ("management".equals(((ConfigurableWebServerApplicationContext) context)
.getServerNamespace())) {
return;
}
}
// 记录当前 web 服务的端口
this.port.compareAndSet(0, event.getWebServer().getPort());
// 启动当前服务注册流程
this.start();
}
其中的start方法流程:
public void start() {
if (!isEnabled()) {
if (logger.isDebugEnabled()) {
logger.debug("Discovery Lifecycle disabled. Not starting");
}
return;
}
// 当前服务处于未运行状态时,才进行初始化
if (!this.running.get()) {
// 发布服务开始注册的事件
this.context.publishEvent(
new InstancePreRegisteredEvent(this, getRegistration()));
// ☆☆☆☆开始注册☆☆☆☆
register();
if (shouldRegisterManagement()) {
registerManagement();
}
// 发布注册完成事件
this.context.publishEvent(
new InstanceRegisteredEvent<>(this, getConfiguration()));
// 服务状态设置为运行状态,基于AtomicBoolean
this.running.compareAndSet(false, true);
}
}
其中最关键的register()方法就是完成服务注册的关键,代码如下:
protected void register() {
this.serviceRegistry.register(getRegistration());
}
此处的this.serviceRegistry就是NacosServiceRegistry:
3)NacosServiceRegistry
NacosServiceRegistry是Spring的ServiceRegistry接口的实现类,而ServiceRegistry接口是服务注册、发现的规约接口,定义了register、deregister等方法的声明。
而NacosServiceRegistry对register的实现如下:
@Override
public void register(Registration registration) {
// 判断serviceId是否为空,也就是spring.application.name不能为空
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}
// 获取Nacos的命名服务,其实就是注册中心服务
NamingService namingService = namingService();
// 获取 serviceId 和 Group
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
// 封装服务实例的基本信息,如 cluster-name、是否为临时实例、权重、IP、端口等
Instance instance = getNacosInstanceFromRegistration(registration);
try {
// 开始注册服务
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}
catch (Exception e) {
if (nacosDiscoveryProperties.isFailFast()) {
log.error("nacos registry, {} register failed...{},", serviceId,
registration.toString(), e);
rethrowRuntimeException(e);
}
else {
log.warn("Failfast is false. {} register failed...{},", serviceId,
registration.toString(), e);
}
}
}
可以看到方法中最终是调用NamingService的registerInstance方法实现注册的。
而NamingService接口的默认实现就是NacosNamingService。
4)NacosNamingService
NacosNamingService提供了服务注册、订阅等功能。
其中registerInstance就是注册服务实例,源码如下:
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
// 检查超时参数是否异常。心跳超时时间(默认15秒)必须大于心跳周期(默认5秒)
NamingUtils.checkInstanceIsLegal(instance);
// 拼接得到新的服务名,格式为:groupName@@serviceId
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
// 判断是否为临时实例,默认为 true。
if (instance.isEphemeral()) {
// 如果是临时实例,需要定时向 Nacos 服务发送心跳
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
// 发送注册服务实例的请求
serverProxy.registerService(groupedServiceName, groupName, instance);
}
最终,由NacosProxy的registerService方法,完成服务注册。
public void