- 服务实例在启动时注册到服务注册表,并在关闭时注销
- 服务消费者查询服务注册表,获得可用实例
- 服务注册中心需要调用服务实例的健康检查API来验证它是否能够处理请求
8.1 服务注册
spring-cloud-alibaba-nacos-discovery这个jar包下的spring.factories文件引入了
一个关键的配置文件NacosDiscoveryAutoConfiguration实现了ApplicationListener接口。在spring容器启动的时候,会调用其重写的onApplicationEvent方法,因此我们进入此方法。
public void onApplicationEvent(WebServerInitializedEvent event) {
this.bind(event);
}
/** @deprecated */
@Deprecated
public void bind(WebServerInitializedEvent event) {
ApplicationContext context = event.getApplicationContext();
if (!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
this.port.compareAndSet(0, event.getWebServer().getPort());
this.start();
}
}
public void start() {
if (!this.isEnabled()) {
if (logger.isDebugEnabled()) {
logger.debug("Discovery Lifecycle disabled. Not starting");
}
} else {
if (!this.running.get()) {
this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
//开始注册
this.register();
if (this.shouldRegisterManagement()) {
this.registerManagement();
}
this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
this.running.compareAndSet(false, true);
}
}
}
//register方法,开始注册服务。进入此方法
public void register(Registration registration) {
if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}
//获取服务标识
String serviceId = registration.getServiceId();
//获取服务所在的group
String group = nacosDiscoveryProperties.getGroup();
//获取服务的instance,就是把配置文件中配置的服务信息(服务名、IP、Port等等)封装到instance对象中
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);
}
}
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
//判断是否是临时实例。一般来说为了性能,都用的临时实例
if (instance.isEphemeral()) {
//以下就是将instance中的信息封装到beatInfo中去
BeatInfo beatInfo = new BeatInfo();
beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
beatInfo.setIp(instance.getIp());
beatInfo.setPort(instance.getPort());
beatInfo.setCluster(instance.getClusterName());
beatInfo.setWeight(instance.getWeight());
beatInfo.setMetadata(instance.getMetadata());
beatInfo.setScheduled(false);
long instanceInterval = instance.getInstanceHeartBeatInterval();
beatInfo.setPeriod(instanceInterval == 0 ? DEFAULT_HEART_BEAT_INTERVAL : instanceInterval);
//发送心跳
beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
}
//注册服务
serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
}
因此,在spring容器启动的时候,nacos客户端会进行两步操作。
1 向nacos服务端发送心跳
2 向nacos服务端注册当前服务
8.2 服务心跳
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
// key的格式:服务名#ip#port
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);
//向线程池中提交发送心跳的任务,实现异步发送
executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}
将心跳的信息包装为一个BeatTask任务,放到线程池中异步执行。我们主要看BeatTask的run方法。
class BeatTask implements Runnable {
BeatInfo beatInfo;
public BeatTask(BeatInfo beatInfo) {
this.beatInfo = beatInfo;
}
public void run() {
if (beatInfo.isStopped()) {
return;
}
// 获取返回的下一次执行的间隔时间。0的话,就取默认的,默认是5s
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();
}
//发送心跳失败,重新注册
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());
} catch (Exception unknownEx) {
NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, unknown exception msg: {}",
JacksonUtils.toJson(beatInfo), unknownEx.getMessage(), unknownEx);
} finally {
executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
}
}
public long sendBeat(BeatInfo beatInfo) {
try {
if (NAMING_LOGGER.isDebugEnabled()) {
NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
}
//以下就是我们常见的调用Http接口。下面是进行接口参数封装。
Map<String, String> params = new HashMap<String, String>(4);
params.put("beat", JSON.toJSONString(beatInfo));
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
// 发起心跳接口Http调用。心跳接口url是/nacos/v1/ns/instance/beat
String result = reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat", params, HttpMethod.PUT);
JSONObject jsonObject = JSON.parseObject(result);
if (jsonObject != null) {
return jsonObject.getLong("clientBeatInterval");
}
} catch (Exception e) {
NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: " + JSON.toJSONString(beatInfo), e);
}
return 0L;
}
我们可以看到,客户端默认会每隔5s发送一次心跳。心跳发送过程就是简单的Http接口调用。
接下来进入服务端的心跳接口代码。也就是查找url为/nacos/v1/ns/instance/beat的接口。我们在naming工程下的InstanceController中找到接口方法。
@CanDistro
@PutMapping("/beat")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public ObjectNode beat(HttpServletRequest request) throws Exception {
ObjectNode result = JacksonUtils.createEmptyJsonNode();
result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());
String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);
RsInfo clientBeat = null;
if (StringUtils.isNotBlank(beat)) {
clientBeat = JacksonUtils.toObj(beat, RsInfo.class);
}
//判断传过来的服务有没有指定cluster名字,如果没有就设置默认值DEFAULT
String clusterName = WebUtils
.optional(request, CommonParams.CLUSTER_NAME, UtilsAndCommons.DEFAULT_CLUSTER_NAME);
String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);
int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));
if (clientBeat != null) {
if (StringUtils.isNotBlank(clientBeat.getCluster())) {
clusterName = clientBeat.getCluster();
} else {
// fix #2533
clientBeat.setCluster(clusterName);
}
ip = clientBeat.getIp();
port = clientBeat.getPort();
}
String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
NamingUtils.checkServiceNameFormat(serviceName);
Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}", clientBeat, serviceName);
// 真正处理心跳请求
int resultCode = getInstanceOperator().handleBeat(namespaceId, serviceName, ip, port, clusterName, clientBeat);
result.put(CommonParams.CODE, resultCode);
result.put(SwitchEntry.CLIENT_BEAT_INTERVAL,
getInstanceOperator().getHeartBeatInterval(namespaceId, serviceName, ip, port, clusterName));
result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
return result;
}
@Override
public int handleBeat(String namespaceId, String serviceName, String ip, int port, String cluster,
RsInfo clientBeat) throws NacosException {
//获取服务对应的instance。第一次注册,这里肯定是null
com.alibaba.nacos.naming.core.Instance instance = serviceManager
.getInstance(namespaceId, serviceName, cluster, ip, port);
if (instance == null) {
if (clientBeat == null) {
return NamingResponseCode.RESOURCE_NOT_FOUND;
}
Loggers.SRV_LOG.warn("[CLIENT-BEAT] The instance has been removed for health mechanism, "
+ "perform data compensation operations, beat: {}, serviceName: {}", clientBeat, serviceName);
// 创建instance实例。将客户端传过来的服务信息设置到instance中
instance = new com.alibaba.nacos.naming.core.Instance();
instance.setPort(clientBeat.getPort());
instance.setIp(clientBeat.getIp());
instance.setWeight(clientBeat.getWeight());
instance.setMetadata(clientBeat.getMetadata());
instance.setClusterName(cluster);
instance.setServiceName(serviceName);
instance.setInstanceId(instance.getInstanceId());
instance.setEphemeral(clientBeat.isEphemeral());
// 注册服务。如果是第一次,那么会在发送心跳的时候完成服务注册
serviceManager.registerInstance(namespaceId, serviceName, instance);
}
Service service = serviceManager.getService(namespaceId, serviceName);
if (service == null) {
throw new NacosException(NacosException.SERVER_ERROR,
"service not found: " + serviceName + "@" + namespaceId);
}
if (clientBeat == null) {
clientBeat = new RsInfo();
clientBeat.setIp(ip);
clientBeat.setPort(port);
clientBeat.setCluster(cluster);
}
//真正处理心跳
service.processClientBeat(clientBeat);
return NamingResponseCode.OK;
}
public void processClientBeat(final RsInfo rsInfo) {
ClientBeatProcessor clientBeatProcessor = new ClientBeatProcessor();
clientBeatProcessor.setService(this);
clientBeatProcessor.setRsInfo(rsInfo);
//向健康检测的线程池中提交一个处理心跳请求的任务,并且立刻执行。
HealthCheckReactor.scheduleNow(clientBeatProcessor);
}
//立即安排客户端心跳检查任务。
public static ScheduledFuture<?> scheduleNow(Runnable task) {
//delay值是0,因此立刻执行。
return EXECUTOR.schedule(task, 0, TimeUnit.MILLISECONDS);
}
我们主要看ClientBeatProcessor的run方法,查看处理心跳的逻辑。
@Override
public void run() {
Service service = this.service;
if (Loggers.EVT_LOG.isDebugEnabled()) {
Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString());
}
String ip = rsInfo.getIp();
String clusterName = rsInfo.getCluster();
int port = rsInfo.getPort();
Cluster cluster = service.getClusterMap().get(clusterName);
// 获取当前cluster下的所有服务列表
List<Instance> instances = cluster.allIPs(true);
// 遍历服务列表
for (Instance instance : instances) {
// 从服务列表中找客户端发送心跳的那个服务
if (instance.getIp().equals(ip) && instance.getPort() == port) {
if (Loggers.EVT_LOG.isDebugEnabled()) {
Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo.toString());
}
// 找到了,说明之前注册过了,那就这里更新下这个服务的最近心跳时间。后面健康检测就是根据这个心跳时间来的。
instance.setLastBeat(System.currentTimeMillis());
if (!instance.isMarked()) {
if (!instance.isHealthy()) {
// 将服务的健康状态置为true
instance.setHealthy(true);
Loggers.EVT_LOG.info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok",
cluster.getService().getName(), ip, port, cluster.getName(), UtilsAndCommons.LOCALHOST_SITE);
// 服务发生变化,发布推送事件,触发服务端向客户端的推送任务
getPushService().serviceChanged(service);
}
}
}
}
}
这里我们可以看出,心跳的目的就是更新这个服务的最后心跳时间。而服务端判定这个服务是否掉线,就是根据这个时间来判定的,如果最后心跳时间与当前时间差超过15s就会设置为false,也就是掉线。时间差超过30s就会将此服务踢出服务列表。
服务心跳过程总结:
客户端在启动的时候,会开启一个心跳线程,每隔5s调用一次服务端的心跳接口(Http调用),服务端将心跳请求封装成一个task,放到线程池中。由服务端的线程池执行task,更新对应服务的最后心跳时间。
客户端发送服务注册接口
@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
instance);
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
//判断是否是临时实例。
if (instance.isEphemeral()) {
//以下就是将instance中的信息封装到beatInfo中去
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
//发送心跳
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
final Map<String, String> params = new HashMap<String, String>(16);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
params.put(CommonParams.SERVICE_NAME, groupedServiceName);
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()));
//注册服务
reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}
服务注册就是客户端启动的时候,发起Http接口调用。
接下来进入服务端的服务注册接口代码。也就是查找url为/nacos/v1/ns/instance的接口。我们在naming工程下的InstanceController中找到接口方法。
@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance")
public class InstanceController {
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
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);
getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
return "ok";
}
- 从请求参数汇总获得serviceName(服务名)和namespaceId(命名空间Id)
- 调用registerInstance注册实例
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException { createEmptyService(namespaceId, serviceName, instance.isEphemeral()); 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); }
- 创建一个控服务(在Nacos控制台“服务列表”中展示的服务信息),实际上是初始化一个serviceMap,它是一个ConcurrentHashMap集合
- getService,从serviceMap中根据namespaceId和serviceName得到一个服务对象
- 调用addInstance添加服务实例
public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException { createServiceIfAbsent(namespaceId, serviceName, local, null); } public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster) throws NacosException { // 获取service。从serviceMap中获取。 Service service = getService(namespaceId, serviceName); // 如果是第一次注册,那么这里肯定获取不到,会进入下面if分支 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)); // 现在验证服务。如果失败,将抛出异常 service.setLastModifiedMillis(System.currentTimeMillis()); service.recalculateChecksum(); if (cluster != null) { cluster.setService(service); service.getClusterMap().put(cluster.getName(), cluster); } //判断服务是否有效。 service.validate(); // 将service放到serviceMap中,并初始化service putServiceAndInit(service); if (!local) { addOrReplaceService(service); } } } //通过putService()方法将服务缓存到内存 //service.init()建立心跳机制 //consistencyService.listen实现数据一致性监听 private void putServiceAndInit(Service service) throws NacosException { // 把服务放到serviceMap中 putService(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()); } public void putService(Service service) { //将一个空的服务放到serviceMap中。这里使用了双重校验加同步锁。 //因为在判断此服务不存在之后,放入的时候可能恰好其他线程放入了一样的service。 //所以需要加锁,并再次校验。类似单例模式创建的双重检测加锁。 if (!serviceMap.containsKey(service.getNamespaceId())) { synchronized (putServiceLock) { if (!serviceMap.containsKey(service.getNamespaceId())) { serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>()); } } } serviceMap.get(service.getNamespaceId()).putIfAbsent(service.getName(), service); }
调用addInstance方法把当前注册的服务实例保存到Service中
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) throws NacosException { // key格式:com.alibaba.nacos.naming.iplist.ephemera.namespaceId##serviceName String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral); // 获取之前创建的service Service service = getService(namespaceId, serviceName); synchronized (service) { // 获取当前service下的所有ip列表。因为如果客户端做集群,这里就是集群中的多个服务ip信息 List<Instance> instanceList = addIpAddresses(service, ephemeral, ips); Instances instances = new Instances(); instances.setInstanceList(instanceList); // 服务注册 consistencyService.put(key, instances); } } @Override public void put(String key, Record value) throws NacosException { //将服务注册的通知放到阻塞队列中。//服务端有一个线程专门从阻塞队列中获取通知。处理通知。 onPut(key, value); // If upgrade to 2.0.X, do not sync for v1. if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) { return; } //异步向nacos服务端集群中的其他节点同步服务 distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE, DistroConfig.getInstance().getSyncDelayMillis()); } public void onPut(String key, Record value) { if (KeyBuilder.matchEphemeralInstanceListKey(key)) { Datum<Instances> datum = new Datum<>(); datum.value = (Instances) value; datum.key = key; datum.timestamp.incrementAndGet(); // Instances信息放到dataStore中 dataStore.put(key, datum); } if (!listeners.containsKey(key)) { return; } // 发出服务列表修改的通知,notifier是一个实现Runnable接口的类,内部有一个阻塞队列,addTask方法就是往阻塞队列里添加服务变更的通知。 notifier.addTask(key, ApplyAction.CHANGE); }
notifier是一个实现Runnable接口的类,内部有一个阻塞队列,addTask方法就是往阻塞队列里添加服务变更的通知。
/** * Map<namespace, Map<group::serviceName, Service>> * serverMap: * key是namespace * value是Map<group::serviceName, Service> * 而Map<group::serviceName, Service>: * key是组名::服务名 * value是service对象 * 而service中存了一个clusterMap,是一个hashmap。 * Map<String, Cluster> clusterMap = new HashMap<>(); * key是clustername * value是cluster对象 */ private Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
服务注册的主要过程就是,将服务列表更新的通知放入阻塞队列。 服务端启动时候会开启一个线程,专门从这个阻塞队列中获取通知,拿到最新的服务列表,并更新到service中的clusterMap中去。也就是更新最底层Cluster中的ephemeralInstances变量,此变量就是存放当前cluster下的所有服务列表。
8.3健康检查
service.init ( ) 方法,它主要通过定时任务不断检测当前服务下所有实例最后发送心跳包的时间。如果超时,则设置healthy为false表示服务不健康,并且发送服务变更事件。
public void init() { // 开启服务健康检测。向线程池中提交健康检测任务。 HealthCheckReactor.scheduleCheck(clientBeatCheckTask); for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) { entry.getValue().setService(this); entry.getValue().init(); } } //clientBeatCheckTask 的run @Override public void run() { try { // 判断当前服务器是否需要对此服务执行健康检查 if (!getDistroMapper().responsible(service.getName())) { return; } // 获取当前service下的所有服务列表 List<Instance> instances = service.allIPs(true); // 第一次检查 for (Instance instance : instances) { // 判断最后一次心跳时间与当前时间差是否超过了15s。超过了就设置健康状态为false,表示服务已掉线 if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) { if (!instance.isMarked()) { if (instance.isHealthy()) { instance.setHealthy(false); Loggers.EVT_LOG.info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}", instance.getIp(), instance.getPort(), instance.getClusterName(), service.getName(), UtilsAndCommons.LOCALHOST_SITE, instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat()); // 服务发生变化,发布推送事件,触发服务端向客户端的推送任务 getPushService().serviceChanged(service); SpringContext.getAppContext().publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance)); } } } } if (!getGlobalConfig().isExpireInstance()) { return; } // 第二次检查 for (Instance instance : instances) { if (instance.isMarked()) { continue; } // 判断最后一次心跳时间与当前时间差是否超过了30s。超过了就踢出该服务 if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) { // delete instance Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(), JSON.toJSONString(instance)); // 踢出服务 deleteIP(instance); } } } catch (Exception e) { Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e); } } public boolean responsible(String serviceName) { if (!switchDomain.isDistroEnabled() || SystemUtils.STANDALONE_MODE) { return true; } if (CollectionUtils.isEmpty(healthyList)) { // means distro config is not ready yet return false; } int index = healthyList.indexOf(NetUtils.localServer()); int lastIndex = healthyList.lastIndexOf(NetUtils.localServer()); if (lastIndex < 0 || index < 0) { return true; } // 计算服务的hash值与集群长度取模 int target = distroHash(serviceName) % healthyList.size(); // 如果是当前服务器在集群中的index,就返回false,也就是执行下面的健康检查 return target >= index && target <= lastIndex; }
检查的方法就是:
如果最后心跳时间与当前时间差超过15s就会设置为false,也就是掉线。时间差超过30s就会将此服务踢出服务列表。 踢出服务就是调用删除服务的接口。
@CanDistro @DeleteMapping @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE) public String deregister(HttpServletRequest request) throws Exception { Instance instance = getIpAddress(request); String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); NamingUtils.checkServiceNameFormat(serviceName); getInstanceOperator().removeInstance(namespaceId, serviceName, instance); return "ok"; }
总结:
- Nacos客户端通过Open API的形式发送服务注册请求
- Nacos服务端收到请求后,做以下三件事:
-
- 构建一个Service对象保存到ConcurrentHashMap集合中
- 使用定时任务对当前服务下的所有实例建立心跳检测机制
- 基于数据一致性协议服务数据进行同步