如果您对Nacos工作流程和原理还不是很清楚的话,建议从前面的文章开始看:
1、nacos功能简介
2、Nacos服务注册-客户端自动注册流程
一、主流程
上篇讲到了com.alibaba.cloud.nacos.registry.NacosServiceRegistry#register方法,现在就以这个方法开始,看下这个方法代码:
@Override
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}
//构造NamingService
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
//获取将要注册的客户端实例信息
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) {
log.error("nacos registry, {} register failed...{},", serviceId,
registration.toString(), e);
// rethrow a RuntimeException if the registration is failed.
// issue : https://github.com/alibaba/spring-cloud-alibaba/issues/1132
rethrowRuntimeException(e);
}
}
其实这个方法就干了一件事:调用Nacos提供的客户端API进行服务注册。Nacos提供了一个非常核心的接口:NamingService,该接口提供了实例注册、实例注销、服务发现、服务订阅和取消订阅等等一系列的API供客户端调用。接着会根据registration对象构造实例信息,如图:
接着就是调用NacosNamingService.registerInstance方法,这是在nacos-client包中,代码:
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
NamingUtils.checkInstanceIsLegal(instance);
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
//如果是临时实例,发送心跳
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
//HTTP调用nacos-server端服务注册接口
serverProxy.registerService(groupedServiceName, groupName, instance);
}
可以看到这里有两个关键点:发送心跳和服务注册,我们先来看下serverProxy.registerService方法,这个方法往下一直跟会发现最终其实是通过NamingProxy.callServer方法发起了一个对地址/nacos/v1/ns/instance的HTTP调用,/nacos/v1/ns/instance就是nacos-server端提供的服务注册的接口地址,nacos-server端逻辑会在下一篇讲。这里有一个地方需要讲一下,就是如果nacos-server是集群部署的话,其实客户端仍然只会发送请求到其中一个集群节点上进行注册,并不会每个集群节点都去注册一遍(可以先想一下为什么这样做,会在后面讲到),具体实现是在NamingProxy.reqApi方法:
public String reqApi(String api, Map<String, String> params, Map<String, String> body, List<String> servers,
String method) throws NacosException {
//非关键代码略
if (StringUtils.isNotBlank(nacosDomain)) {
for (int i = 0; i < maxRetry; i++) {
try {
return callServer(api, params, body, nacosDomain, method);
} catch (NacosException e) {
exception = e;
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("request {} failed.", nacosDomain, e);
}
}
}
} else {
Random random = new Random(System.currentTimeMillis());
int index = random.nextInt(servers.size());
for (int i = 0; i < servers.size(); i++) {
String server = servers.get(index);
try {
return callServer(api, params, body, server, method);
} catch (NacosException e) {
exception = e;
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("request {} failed.", server, e);
}
}
index = (index + 1) % servers.size();
}
}
//非关键代码略
}
以上代码的意思其实很简单,通过nacosDomain进行判断,nacosDomain是在构造方法中赋值的:
serverList表示的就是服务端节点列表,对应于配置文件中所配置的server地址,用逗号隔开,需要注意的是,如果server-addr配置的是nacos-server集群的vip,比如nginx地址,那么在这里也是走的“单机”部署(其实并不一定是单机部署,我这里为了更容易与集群部署的代码分支区分,所以下文都称为单机部署)的流程,只不过负载由nginx负责了。
如果serverList中只有一个值,说明就是nacos-server是单机部署,那么就把serverList赋值给nacosDoman属性,所以如果nacosDoman不为空,那就说明是单机部署,单机部署时,如果客户端通过HTTP远程调用服务端注册接口失败时,会进行maxRetry次重试(默认3次),重试成功即返回;如果nacosDomain不为空即集群部署时,则通过index = (index + 1) % servers.size()算法选出一个权威节点来进行调用,如果调用失败,就重新再随机选出一个下标再进行上述运算,一直到所有节点都尝试了一遍为止。所以说nacos-server以集群方式部署时,每个客户端每次注册也只是往其中一个节点发送服务注册请求。
需要注意的一点就是,AP模式下所有集群节点都是平等的,即没有主从节点的概念,所有节点
都有可能接受客户端的注册请求,然后会将注册的实例数据同步到其它集群节点。
二、心跳机制
现在再回到前面说到的发送心跳的地方:
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
//构造心跳信息
public BeatInfo buildBeatInfo(String groupedServiceName, Instance instance) {
BeatInfo beatInfo = new BeatInfo();
beatInfo.setServiceName(groupedServiceName);
beatInfo.setIp(instance.getIp());
beatInfo.setPort(instance.getPort());
beatInfo.setCluster(instance.getClusterName());
beatInfo.setWeight(instance.getWeight());
beatInfo.setMetadata(instance.getMetadata());
beatInfo.setScheduled(false);
//心跳周期,默认5秒一次
beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
return beatInfo;
}
//发送心跳
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
BeatInfo existBeat = null;
//fix #1733
if ((existBeat = dom2Beat.remove(key)) != null) {
existBeat.setStopped(true);
}
dom2Beat.put(key, beatInfo);
//延迟任务,客户端启动后,延迟5秒发送心跳,之后每5秒发送一次心跳
executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}
以上代码很简单,就不作详细解释了,主要看下心跳任务的核心实现BeatTask:
class BeatTask implements Runnable {
BeatInfo beatInfo;
public BeatTask(BeatInfo beatInfo) {
this.beatInfo = beatInfo;
}
@Override
public void run() {
if (beatInfo.isStopped()) {
return;
}
long nextTime = beatInfo.getPeriod();
try {
//发送心跳
JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
long interval = result.get("clientBeatInterval").asLong();
boolean lightBeatEnabled = false;
if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0) {
nextTime = interval;
}
int code = NamingResponseCode.OK;
if (result.has(CommonParams.CODE)) {
code = result.get(CommonParams.CODE).asInt();
}
//如果服务端响应码是20404(服务端不存在该实例并且心跳信息为空),
//则重新发起注册请求
if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
Instance instance = new Instance();
instance.setPort(beatInfo.getPort());
instance.setIp(beatInfo.getIp());
instance.setWeight(beatInfo.getWeight());
instance.setMetadata(beatInfo.getMetadata());
instance.setClusterName(beatInfo.getCluster());
instance.setServiceName(beatInfo.getServiceName());
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(true);
try {
serverProxy.registerService(beatInfo.getServiceName(),
NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
} catch (Exception ignore) {
}
}
} catch (NacosException ex) {
NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());
}
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}
BeatTask是一个线程,先整体看一下run()方法的主体结构:前面部分是心跳的处理逻辑;最后一句代码又是开启了一个延迟任务:executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS),我觉得这是对定时任务的一个比较灵活的使用方式,它并没有按照传统的定时任务(如Spring的scheduler)的方式指定一个时间间隔,固定以该时间间隔进行循环调用,而是手动以类似递归的方式开启一个延迟任务,这样做的好处是不需要受固定的时间间隔的约束,因为该任务中包含了HTTP远程调用,所以无法准确预估任务的执行时间,这个时间间隔也就不是那么容易确定,也就是说万一由于网络问题或者其它未知因素导致HTTP调用耗时超过了5秒,也没关系,那就等本次心跳任务执行完成后再启动下一次的心跳任务。nacos中存在大量这种用法,后面还会讲到。
发送心跳的关键代码:serverProxy.sendBeat(beatInfo,BeatReactor.this.lightBeatEnabled):
public JsonNode sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
}
Map<String, String> params = new HashMap<String, String>(8);
Map<String, String> bodyMap = new HashMap<String, String>(2);
if (!lightBeatEnabled) {
bodyMap.put("beat", JacksonUtils.toJson(beatInfo));
}
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
params.put("ip", beatInfo.getIp());
params.put("port", String.valueOf(beatInfo.getPort()));
String result = reqApi(UtilAndComs.nacosUrlBase + "/instance/beat", params, bodyMap, HttpMethod.PUT);
return JacksonUtils.toObj(result);
}
所以实际上也是通过HTTP调用nacos-server端的接口:nacos/v1/ns/instance/beat。
其中一个很重要的一点就是,如果收到的服务端的响应码为20404时(服务端不存在该实例并且心跳信息为空),则重新发起注册请求,这个需要结合服务端一起来看才能完整的理解这个心跳机制,服务端还有个健康检查任务,与客户端心跳密切相关,因此心跳逻辑将会在服务端流程讲完之后我会另开一篇单独来讲。
本文主要介绍了客户端发送服务注册请求到服务端以及心跳相关流程,下篇开始讲服务端处理客户端注册请求的流程。