前言
nacos具有两大核心功能,配置管理功能和服务管理功能。从今天开始,基于1.4.3版本的源码进行分析实现这两个核心功能的细节。
服务注册中心管理服务的相关信息,如IP,端口,服务名称,服务的状态,服务集群名称等,服务注册中心的功能包括服务注册,服务注销,服务订阅,服务退订等。nacos也实现注册中心管理功能,因此也包括服务注册、服务注销,服务订阅和服务退订功能。
使用案例
在nacos中,NamingService类是服务注册中心管理功能的实现类。nacos提供了NamingService的服务注册的案例,代码如下:
public class NamingExample {
public static void main(String[] args) throws NacosException {
Properties properties = new Properties();
//nacos服务端的地址
properties.setProperty("serverAddr", "127.0.0.1:8848");
//命名空间
properties.setProperty("namespace","public");
//使用命名工厂类创建NamingService
NamingService naming = NamingFactory.createNamingService(properties);
//往nacos服务端注册服务:服务名称、ip、端口、集群名称
naming.registerInstance("nacos.test.3", "11.11.11.11", 8888, "TEST1");
//往nacos服务端注册服务
naming.registerInstance("nacos.test.3", "2.2.2.2", 9999, "DEFAULT");
//省略代码
//....
}
根据nacos服务端的地址,命名空间,使用NamingFactory工厂类创建NamingService类,然后调用NamingService类的registerInstance方法进行服务注册,该方法的参数包括服务的名称、ip、端口以及集群的名称。这样就把服务相关的信息注册到nacos服务端了。
源码分析
客户端服务注册
//源码位置:com.alibaba.nacos.client.naming.NacosNamingService#registerInstance
public void registerInstance(String serviceName, String groupName, String ip, int port, String clusterName)
throws NacosException {
//设置ip、端口、权重、集群名称
Instance instance = new Instance();
instance.setIp(ip);
instance.setPort(port);
instance.setWeight(1.0);
instance.setClusterName(clusterName);
registerInstance(serviceName, groupName, instance);
}
registerInstance的groupName参数默认为DEFAULT_GROUP,创建Instance,并设置ip、端口、权重、集群名称,调用registerInstance重载方法进行服务注册。
Instance表示实例,用于承载服务的相关信息,属性主要包括ip、端口、权重、服务名称、集群名称、元数据等。Instance的属性如下:
public class Instance implements Serializable {
private static final long serialVersionUID = -742906310567291979L;
/**
* unique id of this instance.
*/
private String instanceId;
/**
* instance ip.
*/
private String ip;
/**
* instance port.
*/
private int port;
/**
* instance weight.
*/
private double weight = 1.0D;
/**
* instance health status.
*/
private boolean healthy = true;
/**
* If instance is enabled to accept request.
*/
private boolean enabled = true;
/**
* If instance is ephemeral.
*
* @since 1.0.0
*/
private boolean ephemeral = true;
/**
* cluster information of instance.
*/
private String clusterName;
/**
* Service information of instance.
*/
private String serviceName;
/**
* user extended attributes.
*/
private Map<String, String> metadata = new HashMap<String, String>();
//省略代码
}
metadata表示元数据,承载服务的一些变化的相关信息,有利于拓展。
//代码位置:com.alibaba.nacos.client.naming.NacosNamingService#registerInstance
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//检验参数是否合法
NamingUtils.checkInstanceIsLegal(instance);
//创建 groupedServiceName
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
//如果实例是临时的
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
//调用http往nacos服务端进行服务注册
serverProxy.registerService(groupedServiceName, groupName, instance);
}
上述代码的逻辑如下:
- checkInstanceIsLegal方法检验实例心跳超时时间是否小于实例心跳间隔,.
- NamingUtils.getGroupedName方法检验serviceName和groupName是否为空,如果为空,则抛出异常,并将返回groupName@@serviceName的拼接字符串作为groupedServiceName。
- 如果实例是临时的,则创建心跳信息,并且将新创建的心跳信息添加到队列。心跳相关的分析,将会在另外的文章进行分析,这里只需要知道向nacos客户端向nacos服务端发送心跳信息。
- 调用http请求向nacos服务端进行服务注册。
//代码位置:com.alibaba.nacos.client.naming.NacosNamingService#registerInstance
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
instance);
/**
封装http请求参数,包括命名空间、服务名称、组名称、集群名称、ip、端口
权重、服务健康状态、服务是否是临时的、元数据。
**/
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);
}
既然通过http向nacos服务端发送服务注册请求,那么需要构建封装请求参数以及选择请求的方法,上面代码将Instance的相关属性封装为post请求的请求参数,包括命名空间、服务名称、组名称、集群名称、ip、端口 权重、服务健康状态、服务是否是临时的、元数据。请求参数构建好了,nacos提供了一套http请求模板,通过该模板nacos将服务注册请求发送给nacos服务端/nacos/v1/ns/instance的接口。
这样,客户端的服务注册请求到这里分析完成了,http请求模板的源码不具体分析了,如果感兴趣可以看看。
服务端服务注册
客户端服务注册,将请求发送给nacos服务的/nacos/v1/ns/instance的接口,该接口如下:
//代码位置:com.alibaba.nacos.naming.controllers.InstanceController#register
public String register(HttpServletRequest request) throws Exception {
//从请求中获取namespaceId
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);
//从请求参数中解析实例
final Instance instance = parseInstance(request);
//服务注册
serviceManager.registerInstance(namespaceId, serviceName, instance);
return "ok";
}
register接口承接客户端服务注册的请求,主要逻辑如下:
-
从请求中解析请求参数。
- 解析namespaceId,如果该参数为空,则默认为public。
- 解析serviceName,判断该参数是否为空,如果为空,则抛出异常。checkServiceNameFormat方法检验serviceName是否是groupName@@serviceName形式,如果不是,则抛出异常。
- 解析Instance,在nacos客户端中,将Instance的属性封装为相关请求参数,在nacos服务端,将请求参数解析为Instance。
-
调用serviceManager的registerInstance方法进行服务端注册。
//代码位置:com.alibaba.nacos.naming.core.ServiceManager#registerInstance
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
//创建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);
}
//添加实例
addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
- 调用createEmptyService方法创建Service。
- 根据namespaceId和serviceName获取Service。
- 添加Instance,实际就是保存服务相关信息。
接下来详情分析createEmptyService、getService和addInstance方法。
getService
//代码位置:com.alibaba.nacos.naming.core.ServiceManager#getService
private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
public Service getService(String namespaceId, String serviceName) {
if (serviceMap.get(namespaceId) == null) {
return null;
}
return chooseServiceMap(namespaceId).get(serviceName);
}
serviceMap根据namespaceId保存着Service,getServices从serviceMap和serviceName获取到对应的Service。
createEmptyService
createEmptyService方法最终调用了createServiceIfAbsent方法:
//代码位置:com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
throws NacosException {
//根据namespaceId和serviceName获取Service
Service service = getService(namespaceId, serviceName);
if (service == null) {
Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
//创建Service
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());
//计算检验和,将实例的所有的ip进行排序拼接并生成md5
service.recalculateChecksum();
if (cluster != null) {
cluster.setService(service);
service.getClusterMap().put(cluster.getName(), cluster);
}
//服务名称、集群名称合法性的校验
service.validate();
//服务的初始化和添加监听器
putServiceAndInit(service);
//如果非临时数据,则进行nacos集群间同步服务数据
if (!local) {
addOrReplaceService(service);
}
}
}
createServiceIfAbsent的逻辑如下:
- 根据namespaceId和serviceName获取Service
- service等于null,创建Service,并计算检验和,检验服务名称和集群名称的合法性。
- putServiceAndInit方法进行服务的初始化以及添加监听器,监听数据的改变。
- 如果不是临时数据(非本地数据),则进行nacos集群间同步服务数据。
createServiceIfAbsent方法中,有两个比较重要的方法:putServiceAndInit和addOrReplaceService。
putServiceAndInit方法是对服务进行初始化,并添加监听器监听数据的改变。addOrReplaceService方法是nacos集群间同步服务数据。
private void putServiceAndInit(Service service) throws NacosException {
//将service添加到serviceMap中缓存
putService(service);
//根据namespaceId和serviceName获取Service
service = getService(service.getNamespaceId(), service.getName());
//服务初始化
service.init();
//添加服务监听器
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());
}
service的init方法如下:
//代码位置:com.alibaba.nacos.naming.core.Service#init
public void init() {
//检查service的健康状态
HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
//遍历服务的集群,并检查集群的健康状态
for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
entry.getValue().setService(this);
entry.getValue().init();
}
}
service的init方法的作用就是检查service和该service的集群的健康状态,这里就点到为止,后续有文章会进行分析。
addInstance
//代码位置:com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent#addInstance
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
throws NacosException {
//创建key
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
//获取service
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);
}
}
addInstance方法的逻辑如下
- 首先根据buildInstanceListKey方法生成key,ephemeral参数为ture,则生成com.alibaba.nacos.naming.iplist.ephemeral.namespaceId##serviceName,否则生成com.alibaba.nacos.naming.iplist.namespaceId##serviceName。
- 获取service
- 获取服务的所有的实例,并且调用consistencyService的put方法进行集群间的数据同步。addIpAddresses方法调用了updateIpAddresses方法,并且action参数为add,表示添加。
分析到这里,客户端和服务端的服务注册已经完成,但是还有很多细节没有进行深入分析。如下:
- 当服务注册的实例是临时的,nacos客户端与nacos服务端之间的心跳同步。
- service健康状态和集群间的健康状态的检查。
- 集群间的数据一致性是如何保证的。
这些细节虽然没有分析,但是服务注册的主体流程还是比较清晰的。nacos客户端组装Instance,通过http请求将Instance发送给nacos服务端,nacos服务端则进行解析请求参数,并且生产Instance,然后尝试获取service,如果没有service则创建,创建service的过程中,对service进行初始化以及集群的初始化,最后调用nacos一致性服务进行集群间数据的同步。
\