Nacos源码分析
1.下载Nacos源码并运行
要研究Nacos源码自然不能用打包好的Nacos服务端jar包来运行,需要下载源码自己编译来运行。
1.1.下载Nacos源码
Nacos的GitHub地址:https://github.com/alibaba/nacos
课前资料中已经提供了下载好的1.4.2版本的Nacos源码:
如果需要研究其他版本的同学,也可以自行下载:
大家找到其release页面:https://github.com/alibaba/nacos/tags,找到其中的1.4.2.版本:
点击进入后,下载Source code(zip):
1.2.导入Demo工程
我们的课前资料提供了一个微服务Demo,包含了服务注册、发现等业务。
导入该项目后,查看其项目结构:
结构说明:
- cloud-source-demo:项目父目录
- cloud-demo:微服务的父工程,管理微服务依赖
- order-service:订单微服务,业务中需要访问user-service,是一个服务消费者
- user-service:用户微服务,对外暴露根据id查询用户的接口,是一个服务提供者
- cloud-demo:微服务的父工程,管理微服务依赖
1.3.导入Nacos源码
将之前下载好的Nacos源码解压到cloud-source-demo项目目录中:
然后,使用IDEA将其作为一个module来导入:
1)选择项目结构选项:
然后点击导入module:
在弹出窗口中,选择nacos源码目录:
然后选择maven模块,finish:
最后,点击OK即可:
导入后的项目结构:
1.4.proto编译
Nacos底层的数据通信会基于protobuf对数据做序列化和反序列化。并将对应的proto文件定义在了consistency这个子模块中:
我们需要先将proto文件编译为对应的Java代码。
1.4.1.什么是protobuf
protobuf的全称是Protocol Buffer,是Google提供的一种数据序列化协议,这是Google官方的定义:
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
可以简单理解为,是一种跨语言、跨平台的数据传输格式。与json的功能类似,但是无论是性能,还是数据大小都比json要好很多。
protobuf的之所以可以跨语言,就是因为数据定义的格式为.proto
格式,需要基于protoc编译为对应的语言。
1.4.2.安装protoc
Protobuf的GitHub地址:https://github.com/protocolbuffers/protobuf/releases
我们可以下载windows版本的来使用:
另外,课前资料也提供了下载好的安装包:
解压到任意非中文目录下,其中的bin目录中的protoc.exe可以帮助我们编译:
然后将这个bin目录配置到你的环境变量path中,可以参考JDK的配置方式:
1.4.3.编译proto
进入nacos-1.4.2的consistency模块下的src/main目录下:
然后打开cmd窗口,运行下面的两个命令:
protoc --java_out=./java ./proto/consistency.proto
protoc --java_out=./java ./proto/Data.proto
如图:
会在nacos的consistency模块中编译出这些java代码:
1.5.运行
nacos服务端的入口是在console模块中的Nacos类:
我们需要让它单机启动:
然后新建一个SpringBootApplication:
然后填写应用信息:
然后运行Nacos这个main函数:
将order-service和user-service服务启动后,可以查看nacos控制台:
2.服务注册
服务注册到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中。
2.1.服务注册接口
Nacos提供了服务注册的API接口,客户端只需要向该接口发送请求,即可实现服务注册。
**接口说明:**注册一个实例到Nacos服务。
请求类型:POST
请求路径:/nacos/v1/ns/instance
请求参数:
名称
类型
是否必选
描述
ip
字符串
是
服务实例IP
port
int
是
服务实例port
namespaceId
字符串
否
命名空间ID
weight
double
否
权重
enabled
boolean
否
是否上线
healthy
boolean
否
是否健康
metadata
字符串
否
扩展信息
clusterName
字符串
否
集群名
serviceName
字符串
是
服务名
groupName
字符串
否
分组名
ephemeral
boolean
否
是否临时实例
错误编码:
错误代码
描述
语义
400
Bad Request
客户端请求中的语法错误
403
Forbidden
没有权限
404
Not Found
无法找到资源
500
Internal Server Error
服务器内部错误
200
OK
正常
2.2.客户端
首先,我们需要找到服务注册的入口。
2.2.1.NacosServiceRegistryAutoConfiguration
因为Nacos的客户端是基于SpringBoot的自动装配实现的,我们可以在nacos-discovery依赖:
spring-cloud-starter-alibaba-nacos-discovery-2.2.6.RELEASE.jar
这个包中找到Nacos自动装配信息:
可以看到,有很多个自动配置类被加载了,其中跟服务注册有关的就是NacosServiceRegistryAutoConfiguration这个类,我们跟入其中。
可以看到,在NacosServiceRegistryAutoConfiguration这个类中,包含一个跟自动注册有关的Bean:
2.2.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:
2.2.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。
2.2.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 registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
instance);
// 组织请求参数
final Map<String, String> params = new HashMap<String, String>(16);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, serviceName);
params.put(CommonParams.GROUP_NAME, groupName);
params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
params.put("ip", instance.getIp());
params.put("port", String.valueOf(instance.getPort()));
params.put("weight", String.valueOf(instance.getWeight()));
params.put("enable", String.valueOf(instance.isEnabled()));
params.put("healthy", String.valueOf(instance.isHealthy()));
params.put("ephemeral", String.valueOf(instance.isEphemeral()));
params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
// 通过POST请求将上述参数,发送到 /nacos/v1/ns/instance
reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}
这里提交的信息就是Nacos服务注册接口需要的完整参数,核心参数有:
- namespace_id:环境
- service_name:服务名称
- group_name:组名称
- cluster_name:集群名称
- ip: 当前实例的ip地址
- port: 当前实例的端口
而在NacosNamingService的registerInstance方法中,有一段是与服务心跳有关的代码,我们在后续会继续学习。
2.2.5.客户端注册的流程图
如图:
2.3.服务端
在nacos-console的模块中,会引入nacos-naming这个模块:
模块结构如下: