Consul注销实例时候的问题

DD的博客全面升级,阅读体验更佳(尤其是系列教程),后续不再通过这里发布新文章,而是改到 www.didispace.com 发布啦,奔走相告!点击直达~

当我们在Spring Cloud应用中使用Consul来实现服务治理时,由于Consul不会自动将不可用的服务实例注销掉(deregister),这使得在实际使用过程中,可能因为一些操作失误、环境变更等原因让Consul中存在一些无效实例信息,而这些实例在Consul中会长期存在,并处于断开状态。它们虽然不会影响到正常的服务消费过程,但是它们会干扰我们的监控,所以我们可以实现一个清理接口,在确认故障实例可以清理的时候进行调用来将这些无效信息清理掉。

开始以为只要简单的调用注销接口就能轻松完成,但是实际实践的发现并非如此。因此,分享一下整个实现过程以及中间遇到的一些坑。

借鉴Spring Cloud Consul

在实现之初,先参考了Spring Cloud Consul在关闭程序时候实现的注销方法,具体如下:

public class ConsulLifecycle extends AbstractDiscoveryLifecycle {
...

private void deregister(String serviceId) {
if (!this.properties.isRegister()) {
return;
}
if (ttlScheduler != null) {
ttlScheduler.remove(serviceId);
}
log.info("Deregistering service with consul: {}", serviceId);
client.agentServiceDeregister(serviceId);
}

...
}

我们可以看到,当应用关闭时候的注销操作是通过调用client.agentServiceDeregister(serviceId)来实现的。其中client是consul-api的com.ecwid.consul.v1.ConsulClient实例。而agentServiceDeregister方法则是对/v1/agent/service/deregister/<serviceID> 接口的实现,该接口主要用来从Consul Agent中根据serviceId来注销实例。

以此实现为范例,于是开始的思路是这样的:

  • 先通过consulClient.getHealthServices(serviceId, false, null)根据serviceId来获取服务实例清单
  • 遍历实例清单中有不是PASSING状态的实例,就调用client.agentServiceDeregister(serviceId)来剔除

具体实现如下:

@RestController
public class ApiController {

@Autowired
private ConsulClient consulClient;

@RequestMapping(value = "/unregister/{id}", method = RequestMethod.POST)
public String unregisterServiceAll(@PathVariable String id) {
List<HealthService> response = consulClient.getHealthServices(id, false, null).getValue();
for(HealthService service : response) {
service.getChecks().forEach(check -> {
if(!check.getStatus().name().equals(Check.CheckStatus.PASSING.name())) {
logger.info("unregister : {}", check.getServiceId());
consulClient.agentServiceDeregister(check.getServiceId());
}
});
}
return null;
}
}

但是,在测试后发现该方法只能剔除同一个agent上的非PASSING实例。

Catalog误区

继续搜索了一下Consul的文档,发现了这个接口:/v1/catalog/deregister : Deregisters a node, service, or check。于是,尝试了用该接口来替换之前的consulClient.agentServiceDeregister(check.getServiceId());实现。

CatalogDeregistration catalogDeregistration = new CatalogDeregistration();
catalogDeregistration.setDatacenter("dc1");
catalogDeregistration.setNode(check.getNode());
catalogDeregistration.setServiceId(check.getServiceId());
catalogDeregistration.setCheckId(check.getCheckId());
consulClient.catalogDeregister(catalogDeregistration);

经过测试,该方法可以实现短暂的剔除,但是过一段时间之后这些被剔除的实例又都恢复回来了……也就是说这个接口完全没有什么卵用!

那么为什么会出现这种情况呢?我们可以在Github中找到这个维持了一年多的问题讨论:https://github.com/hashicorp/consul/issues/1188

整个讨论过程非常曲折,虽然当前该问题还依然在open状态,但是一些回复也基本够我们去理解它的原因了。比如下面这条评论:

You cannot deregister a service from the agent on a different node, service only exists on the agent you have registered with. It also exists in the catalog on all nodes, but that is not related to the agent itself. And to be honest I don’t understand why there is a catalog/deregister endpoint at all, in my opinion catalog should be a read-only service list.

从该评论中,我们可以知道一个重要信息:服务实例只能在注册的Agent上进行注销!另外,对于/v1/catalog/deregister接口,目前还是有不少争议的,因为根本没啥用。

最终实现

既然服务实例只能在注册的Agent上进行注销,那么我们的实现完全可以按照该思路来实现,方法很简单,只需要对一开始实现的内容做一些调整,依然使用client.agentServiceDeregister(serviceId)方法,只是我们需要调整client连接的agent必须是serviceId注册的agent。所以,最终的修改结果如下:

List<HealthService> response = consulClient.getHealthServices(id, false, null).getValue();
for(HealthService service : response) {
// 创建一个用来剔除无效实例的ConsulClient,连接到无效实例注册的agent
ConsulClient clearClient = new ConsulClient(service.getNode().getAddress(), 8500);
service.getChecks().forEach(check -> {
if(check.getStatus() != Check.CheckStatus.PASSING) {
logger.info("unregister : {}", check.getServiceId());
clearClient.agentServiceDeregister(check.getServiceId());
}
});
}