1.简介
1.Nacos是阿里的一个开源产品,它是针对微服务架构中的服务发现、配置管理、服务治理的综合型解决方案;
2.官网:https://nacos.io
Nacos致力于帮助您发现、配置和管理微服务.Nacos提供了一组简单易用的特性集,帮助您实现
动态服务发现、服务配置管理、服务及流量管理;
Nacos帮助您更敏捷和容易地构建、交付和管理微服务平台;
2.特性
2.1.服务发现与服务健康检查
Nacos使服务更容易注册,并通过DNS或HTTP接口发现其他服务,Nacos还提供服务的实时健康检查,以防止向不健康的主机或服务实例发送请求;
2.2.动态配置管理
动态配置服务允许您在所有环境中以集中和动态的方式管理所有服务的配置.Nacos消除了在更新配置时重新部署应用程序,这使配置的更改更加高效和灵活;
这里动态配置管理的特性说明了Naocs的配置管理能力;
2.3.动态DNS服务
Nacos提供基于DNS 协议的服务发现能力,旨在支持异构语言的服务发现,支持将注册在Nacos上的服务以域名的方式暴露端点,让三方应用方便的查阅及发现;
2.4.服务和元数据管理
Nacos能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略;
3.下载Nacos源码并运行
3.1.下载源码
Nacos的GitHub地址:https://github.com/alibaba/nacos
本例中我们下载1.4.2版本的Nacos源码: https://github.com/alibaba/nacos/tags
点击进入后,下载Source code(zip):
3.2.导入Demo工程
我们准备了一个微服务Demo,包含了服务注册、发现
等业务;
结构说明:
cloud-source-demo:项目父目录
- cloud-demo:微服务的父工程,管理微服务依赖
- order-service:订单微服务,业务中需要访问user-service,是一个服务消费者
- user-service:用户微服务,对外暴露根据id查询用户的接口,是一个服务提供者
3.3.导入Nacos源码
1.将之前下载好的Nacos源码解压到cloud-source-demo项目目录中:
2.使用IDEA将其作为一个module来导入
①.选择项目结构选项:
②.点击导入module
③.在弹出窗口中,选择nacos源码目录:
④.选择maven模块,finish
⑤.最后,点击OK即可
⑥.导入后的项目结构
3.4.proto编译
Nacos底层的数据通信会基于protobuf对数据做序列化和反序列化
,并将对应的proto文件定义在了consistency这个子模块中
我们需要先将proto文件编译为对应的Java代码!
3.4.1.什么是protobuf?
1.protobuf的全称是Protocol Buffer,是Google提供的一种数据序列化协议
,这是Google官方的定义:
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,很适合做数据存储或 RPC 数据交换格式,它可用于通讯协议,数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式;
2.可以简单理解为,protobuf是一种跨语言、跨平台的数据传输格式.与json的功能类似,但是无论是性能,还是数据大小都比json要好很多;
3.protobuf的之所以可以跨语言,就是因为数据定义的格式为.proto
格式,需要基于protoc编译为对应的语言;
3.4.2.安装protoc
Protobuf的GitHub地址:https://github.com/protocolbuffers/protobuf/releases
1.我们可以下载windows版本的来使用:
2.解压到任意非中文目录下,其中的bin目录中的protoc.exe可以帮助我们编译:
3.为了方便使用,我们将这个bin目录配置到环境变量path中,可以参考JDK的配置方式
3.4.3.编译proto文件
1.进入nacos-1.4.2的consistency模块下的src/main目录下:
2.然后打开cmd窗口,运行下面的两个命令
protoc --java_out=./java ./proto/consistency.proto
protoc --java_out=./java ./proto/Data.proto
3.会在nacos的consistency模块中编译出这些java代码:
3.5.运行nacos
1.nacos服务端的入口是在console模块中的Nacos类
2.我们需要让它单机启动:
3.新建一个SpringBootApplication
4.填写信息
5.运行Nacos这个main函数
3.6.运行Demo程序
order-service和user-service服务启动后,可以查看nacos控制台:
4.服务注册
服务注册到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中;
4.1.服务注册接口
Nacos提供了服务注册的API接口(https://nacos.io/zh-cn/docs/open-api.html),客户端只需要向该接口发送请求,即可实现服务注册;
接口说明: 注册一个实例到Nacos服务
请求类型: POST
请求路径: /nacos/v1/ns/instance
请求参数:
错误编码:
4.2.客户端
首先,我们需要找到服务注册的入口
4.2.1.NacosServiceRegistryAutoConfiguration
因为Nacos的客户端是基于SpringBoot的自动装配实现的,我们可以在nacos-discovery依赖spring-cloud-starter-alibaba-nacos-discovery-2.2.6.RELEASE.jar
这个包中找到Nacos自动装配信息:
可以看到,这里有很多个自动配置类被加载了,其中跟服务注册有关的就是NacosServiceRegistryAutoConfiguration
这个类,我们跟入其中;
可以看到,在NacosServiceRegistryAutoConfiguration这个类中,包含一个跟自动注册有关的Bean:
4.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
4.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;
4.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: 当前实例的端口
4.2.5.客户端注册流程图
4.3.服务端
在nacos-console的模块中,会引入nacos-naming这个模块:
模块结构如下:
其中的com.alibaba.nacos.naming.controllers包下就有服务注册、发现等相关的各种接口,其中的服务注册是在InstanceController
类中:
4.3.1.InstanceController
进入InstanceController类,可以看到一个register方法,就是服务注册的方法了:
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
// 尝试获取namespaceId
final String namespaceId = WebUtils
.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
// 尝试获取serviceName,其格式为group_name@@service_name
final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
// 解析出实例信息,封装为Instance对象
final Instance instance = parseInstance(request);
// 注册实例
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
进入到注册实例serviceManager.registerInstance()方法中:
4.3.2.ServiceManager
ServiceManager就是Nacos中管理服务、实例信息的核心API
,其中就包含Nacos的服务注册表:
其中的registerInstance方法
就是注册服务实例的方法:
/**
* Register an instance to a service in AP mode.
*
* <p>This method creates service or cluster silently if they don't exist.
*
* @param namespaceId id of namespace
* @param serviceName service name
* @param instance instance to register
* @throws Exception any error occurred in the process
*/
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
// 创建一个空的service(如果是第一次来注册实例,要先创建一个空service出来,放入注册表)
// 此时不包含实例信息
createEmptyService(namespaceId, serviceName, instance.isEphemeral());
// 拿到创建好的service
Service service = getService(namespaceId, serviceName);
// 拿不到则抛异常
if (service == null) {
throw new NacosException(NacosException.INVALID_PARAM,
"service not found, namespace: " + namespaceId + ", service: " + serviceName);
}
// 添加要注册的实例到service中
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
服务创建好之后,就要添加实例到服务中:
/**
* Add instance to service.
*
* @param namespaceId namespace
* @param serviceName service name
* @param ephemeral whether instance is ephemeral
* @param ips instances
* @throws NacosException nacos exception
*/
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
// 监听服务列表用到的key,服务唯一标识,例如:
com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@order-service
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
// 获取服务
Service service = getService(namespaceId, serviceName);
// 同步锁,避免并发修改的安全问题
synchronized (service) {
// 1)获取要更新的实例列表
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
// 2)封装实例列表到Instances对象
Instances instances = new Instances();
instances.setInstanceList(instanceList);
// 3)完成注册表更新以及Nacos集群的数据同步
consistencyService.put(key, instances);
}
}
该方法中对修改服务列表的动作加锁处理,确保线程安全.而在同步代码块中.包含下面几步:
①.获取需要更新的实例列表,
addIpAddresses(service, ephemeral, ips);
②.将更新后的数据封装到
Instances
对象中,后面更新注册表时使用;③.调用
consistencyService.put()
方法完成Nacos集群的数据同步,保证集群一致性;
注意:在第1步的addIPAddress中,会拷贝旧的实例列表,添加新实例到列表中,在第3步中,完成对实例状态更新后,则会用新列表直接覆盖旧实例列表.而在更新过程中,旧实例列表不受影响,用户依然可以读取;这样在更新列表状态过程中,无需阻塞用户的读操作,也不会导致用户读取到脏数据,性能比较好.这种方案称为CopyOnWrite方案;
4.3.2.1.更新服务列表
实例列表的更新,对应的方法是addIpAddresses(service, ephemeral, ips)
private List<Instance> addIpAddresses(Service service, boolean ephemeral, Instance... ips) throws NacosException {
return updateIpAddresses(service, UtilsAndCommons.UPDATE_INSTANCE_ACTION_ADD, ephemeral, ips);
}
进入updateIpAddresses
方法:
public List<Instance> updateIpAddresses(Service service, String action, boolean ephemeral, Instance... ips)
throws NacosException {
// 根据namespaceId、serviceName获取当前服务的实例列表,返回值是Datum
// 第一次来,肯定是null
Datum datum = consistencyService
.get(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), ephemeral));
// 得到服务中现有的实例列表
List<Instance> currentIPs = service.allIPs(ephemeral);
// 创建map,保存实例列表,key为ip地址,value是Instance对象
Map<String, Instance> currentInstances = new HashMap<>(currentIPs.size());
// 创建Set集合,保存实例的instanceId
Set<String> currentInstanceIds = Sets.newHashSet();
// 遍历要现有的实例列表
for (Instance instance : currentIPs) {
// 添加到map中
currentInstances.put(instance.toIpAddr(), instance);
// 添加instanceId到set中
currentInstanceIds.add(instance.getInstanceId());
}
// 创建map,用来保存更新后的实例列表
Map<String, Instance> instanceMap;
if (datum != null && null != datum.value) {
// 如果服务中已经有旧的数据,则先保存旧的实例列表
instanceMap = setValid(((Instances) datum.value).getInstanceList(), currentInstances);
} else {
// 如果没有旧数据,则直接创建新的map
instanceMap = new HashMap<>(ips.length);
}
// 遍历实例列表
for (Instance instance : ips) {
// 判断服务中是否包含要注册的实例的cluster信息
if (!service.getClusterMap().containsKey(instance.getClusterName())) {
// 如果不包含,创建新的cluster
Cluster cluster = new Cluster(instance.getClusterName(), service);
cluster.init();
// 将集群放入service的注册表
service.getClusterMap().put(instance.getClusterName(), cluster);
Loggers.SRV_LOG
.warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
instance.getClusterName(), instance.toJson());
}
// 删除实例 or 新增实例?
if (UtilsAndCommons.UPDATE_INSTANCE_ACTION_REMOVE.equals(action))