服务注册和心跳检测的实现:
- 服务注册:其实就是调用HttpClient发送一个http请求实现的服务注册。信息中包含了命名空间,服务名,组名,集群名,ip,端口,权重,健康状况等信息。
- 心跳监测:通过将心跳信息封装成一个BeatTask线程任务,然后交给定时线程池执行。如果长时间服务端无法接收到客户端的心跳,此时服务端认为你的这个服务实例出问题了,这样服务端就会主动从服务注册表中剔除该服务实例,该服务实例就不存在服务端了。因此监测返回的状态码会为20404,之后会重新将该服务实例注册到服务端。并且这里的心跳检测采用了变频的方式,也就是定时任务执行的间隔是不固定的,这次任务的执行才决定下一次任务执行的时间,好处是如果有的任务频繁失败,可以将下一次执行的任务时间拉长,减少资源的浪费。
服务注册
registerInstance
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
if (instance.isEphemeral()) {
//声明一个心跳任务
BeatInfo beatInfo = new BeatInfo();
//将服务名和组名根据一定规则拼成一个字符串
beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
//设置服务的ip 端口 集群 权重等。。。
beatInfo.setIp(instance.getIp());
beatInfo.setPort(instance.getPort());
beatInfo.setCluster(instance.getClusterName());
beatInfo.setWeight(instance.getWeight());
beatInfo.setMetadata(instance.getMetadata());
beatInfo.setScheduled(false);
//设置心跳间隔 默认是5秒
//DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5L);
//将心跳任务与服务的实例绑定
beatInfo.setPeriod
(instance.getInstanceHeartBeatInterval());
this.beatReactor.addBeatInfo
(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
//将服务实例注册到服务端
this.serverProxy.registerService
(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}
registerService
//将服务的所有信息封装成一个map集合,然后调用reqAPI方法注册
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});
Map<String, String> params = new HashMap(9);
params.put("namespaceId", this.namespaceId);
params.put("serviceName", serviceName);
params.put("groupName", groupName);
params.put("clusterName", 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", JSON.toJSONString(instance.getMetadata()));
//VERSION = "Nacos-Java-Client:v" + VersionUtils.VERSION;
//WEB_CONTEXT = "/nacos";
//NACOS_URL_BASE = WEB_CONTEXT + "/v1/ns";
//NACOS_URL_INSTANCE = NACOS_URL_BASE + "/instance";
//NACOS_URL_SERVICE = NACOS_URL_BASE + "/service";
this.reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, "POST");
}
reqAPI
body,默认是空
servers,就是我们配置的nacos服务端所在服务的ip和端口的集合,因为我们可能配置多个。
public String reqAPI(String api, Map<String, String> params, String body, List<String> servers, String method) throws NacosException {
params.put("namespaceId", this.getNamespaceId());
//如果服务段为空则抛出异常
if (CollectionUtils.isEmpty(servers) && StringUtils.isEmpty(this.nacosDomain)) {
throw new NacosException(400, "no server available");
} else {
//
NacosException exception = new NacosException();
if (servers != null && !servers.isEmpty()) {
Random random = new Random(System.currentTimeMillis());
int index = random.nextInt(servers.size());
int i = 0;
while(i < servers.size()) {
//随机选择一个服务端调用
String server = (String)servers.get(index);
try {
//调用成功则返回这个服务端
return this.callServer(api, params, body, server, method);
} catch (NacosException var13) {
//调用失败则重新选择新的服务器
exception = var13;
if(LogUtils.NAMING_LOGGER.isDebugEnabled())
{
LogUtils.NAMING_LOGGER.debug("request {} failed.", server, var13);
}
index = (index + 1) % servers.size();
++i;
}
}
}
if (StringUtils.isNotBlank(this.nacosDomain)) {
int i = 0;
while(i < 3) {
try {
return this.callServer(api, params, body, this.nacosDomain, method);
} catch (NacosException var12) {
exception = var12;
if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
LogUtils.NAMING_LOGGER.debug("request {} failed.", this.nacosDomain, var12);
}
++i;
}
}
}
LogUtils.NAMING_LOGGER.error("request: {} failed, servers: {}, code: {}, msg: {}", new Object[]{api, servers, exception.getErrCode(), exception.getErrMsg()});
throw new NacosException(exception.getErrCode(), "failed to req API:/api/" + api + " after all servers(" + servers + ") tried: " + exception.getMessage());
}
}
callServer
//将地址和请求路径名( /nacos/v1/ns/instance )进行拼接,然后发送http请求进行服务注册,然后接收客户端的响应。
public String callServer(String api, Map<String, String> params, String body, String curServer, String method) throws NacosException {
long start = System.currentTimeMillis();
long end = 0L;
this.injectSecurityInfo(params);
List<String> headers = this.builderHeaders();
String url;
if (!curServer.startsWith("https://") && !curServer.startsWith("http://")) {
if (!curServer.contains(":")) {
curServer = curServer + ":" + this.serverPort;
}
url = HttpClient.getPrefix() + curServer + api;
} else {
url = curServer + api;
}
HttpResult result = HttpClient.request(url, headers, params, body, "UTF-8", method);
end = System.currentTimeMillis();
MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(result.code)).observe((double)(end - start));
if (200 == result.code) {
return result.content;
} else if (304 == result.code) {
return "";
} else {
throw new NacosException(result.code, result.content);
}
}
心跳机制
addBeatInfo
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
LogUtils.NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
//通过当前服务名,ip,端口生成一个key
String key = this.buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
BeatInfo existBeat = null;
//如果当前key已经存在了心跳那么就将它从dom2Beat集合中移除并停止心跳
if ((existBeat = (BeatInfo)this.dom2Beat.remove(key)) != null) {
existBeat.setStopped(true);
}
//将key与心跳信息绑定然后放到dom2Beat集合中
this.dom2Beat.put(key, beatInfo);
//根据心跳信息生成一个BeatTask线程任务
//将该任务丢到定时线程池中 等待beatInfo.getPeriod()默认为5秒后开始执行
this.executorService.schedule(new BeatReactor.BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
//设置一个监听器监听
MetricsMonitor.getDom2BeatSizeMonitor()
.set((double)this.dom2Beat.size());
}
*定时任务的run()
首先就是通过serverProxy发送一个http请求到服务端,服务端进行响应,其实这个过程就完成了跟服务端的心跳,接下来就是解析服务端的响应的数据了。根据服务端返回的不同的状态码进行判断,进行不同的操作。
code==20404
的状态码:
这段代码其实就是重新向服务端进行服务的注册。有人肯定会好奇,为什么发送心跳的时候,服务端会告诉客户端,该服务实例在服务端找不到?不是只有注册服务的时候才会去构建心跳么,按道理应该存在的?是的,这是正常的情况下,在正常情况下,客户端发送心跳,服务实例应该存在于服务端的,但是有些情况,比如说网络抖动的时候,因为网络的问题,客户端无法给服务端发送心跳,长时间服务端无法接收到客户端的心跳,此时服务端认为你的这个服务实例出问题了,这样服务端就会主动从服务注册表中剔除该服务实例,该服务实例就不存在服务端了。当客户端网络好了的话,那么此时会恢复跟服务端的心跳机制,就会出现服务找不到的现象,这种情况下只要重新注册一下就行了。
BeatReactor.this.executorService.schedule(BeatReactor.this.new BeatTask(this.beatInfo), nextTime, TimeUnit.MILLISECONDS);
验证完这次心跳之后会重新构建一个新的任务然后丢到定时线程池中执行。以此来实现的定时发送心跳的机制。这个定时机制实现了变频的功能,所谓的变频就是定时任务执行的间隔是不固定的,这次任务的执行才决定下一次任务执行的时间,这样有一个好处就是在一些场景中,如果有的任务频繁失败,那么是不是可以考虑让下一次执行的任务时间拉长,减少资源的浪费,nacos在定时更新本地服务实例列表的缓存也使用到了这个机制。这种机制其实在很多框架,中间件都有使用。
public void run() {
if (!this.beatInfo.isStopped()) {
long nextTime = this.beatInfo.getPeriod();
try {
JSONObject result = BeatReactor.this.serverProxy.sendBeat(this.beatInfo, BeatReactor.this.lightBeatEnabled);
long interval = (long)result.getIntValue("clientBeatInterval");
boolean lightBeatEnabled = false;
if (result.containsKey("lightBeatEnabled")) {
lightBeatEnabled = result.getBooleanValue("lightBeatEnabled");
}
BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
if (interval > 0L) {
nextTime = interval;
}
int code = 10200;
if (result.containsKey("code")) {
code = result.getIntValue("code");
}
if (code == 20404) {
Instance instance = new Instance();
instance.setPort(this.beatInfo.getPort());
instance.setIp(this.beatInfo.getIp());
instance.setWeight(this.beatInfo.getWeight());
instance.setMetadata(this.beatInfo.getMetadata());
instance.setClusterName(this.beatInfo.getCluster());
instance.setServiceName(this.beatInfo.getServiceName());
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(true);
try {
BeatReactor.this.serverProxy.registerService(this.beatInfo.getServiceName(), NamingUtils.getGroupName(this.beatInfo.getServiceName()), instance);
} catch (Exception var10) {
}
}
} catch (NacosException var11) {
LogUtils.NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}", new Object[]{JSON.toJSONString(this.beatInfo), var11.getErrCode(), var11.getErrMsg()});
}
BeatReactor.this.executorService.schedule(BeatReactor.this.new BeatTask(this.beatInfo), nextTime, TimeUnit.MILLISECONDS);
}