文章

系统设计 | 利用心跳机制实现自动续期 —— 看门狗机制在注册中心中的应用

这几天在学习手写 RPC 项目,一开始服务注册的实现我只是使用 ConcurrentHashMap 在本地完成,随着功能的逐步完善,我对框架进行了扩展,抽象出了统一的 Registry 接口,作为注册中心的统一规范。后续只需要实现这个接口,即可灵活地接入不同的注册中心实现。

在实践过程中,我选择使用了 etcd 作为注册中心中间件。不过,在接入使用的过程中,我注意到一个关键问题:

etcd 和其他键值对存储系统一样,采用租约机制(Lease)来限制键值对的生存时间。换句话说,一个服务注册信息如果没有被续租(即延长过期时间),就会被自动删除。这种机制虽然可以避免脏数据,但也带来两个挑战:

  1. 如果服务节点已经下线,那么注册中心需要及时删除该节点的注册信息,防止客户端请求到不可用的节点;

  2. 如果服务节点仍然可用,则必须定期续租,保持注册状态不失效。

仅仅依靠 etcd 的过期时间设置并不能很好地实现这两个目标,因为 etcd 本身无法感知某个服务节点是否宕机或失联。

常见思路分析

为了解决上述问题,我调研并总结了以下两种思路:

  • 方案一:轮询探活

    对注册中心内所有节点定期进行探活检查,判断其是否仍然存活。如果不响应,则主动移除。这种方式的问题在于节点的注册时间和过期时间可能不同,导致轮询的时间间隔难以控制;并且如果节点量多,开销也会非常大。

  • 方案二:引入看门狗机制(Watchdog)

    参考 Redisson 分布式锁的设计思路,Redisson 为了避免锁被提前释放,会在客户端内部单独启动一个定时任务线程,如果业务还在执行中,就会自动向 Redis 续租该锁。

    类似地,我们也可以为服务节点设置一个心跳机制:服务提供者定期向 etcd 注册中心发送心跳请求,更新其注册状态,从而实现自动续期。

通过进一步调研可以发现,方案二(心跳续期)已经成为大多数注册中心实现中的主流做法。

实现过程

下面是我在现有 RPC 框架基础上对注册中心进行的具体改造。

1. 改造 Registry 接口,添加续期方法

我们首先在统一的 Registry 接口中添加 heartBeat 方法:

/**
 * Registry interface
 */
public interface Registry {

    void init(RegistryConfig registryConfig);

    void register(ServiceMetaInfo serviceMetaInfo) throws Exception;

    void unRegister(ServiceMetaInfo serviceMetaInfo);

    List<ServiceMetaInfo> serviceDiscovery(String serviceKey);

    void destroy();

    /**
     * Heartbeat for lease renewal
     */
    void heartBeat();
}

2. 存储本地注册的节点 Key,用于续期

在 etcd 的具体实现类中,我们使用一个 HashSet<String> 来记录当前节点注册的所有 key,以便后续定时对其进行续期:

@Slf4j
public class EtcdRegistry implements Registry {
    // ...
    /**
     * 本机注册的节点 key 集合(用于续期)
     */
    private final HashSet<String> localRegisterNodeKeySet = new HashSet<>();
    // ...
}

这样一来,不仅注册服务时我们能记住注册的 key,在节点下线或者定时续期时,也能准确找到所有本地注册的节点。

3. 实现心跳续期逻辑

@Override
public void heartBeat() {
    // 每 10 秒执行一次心跳续期
    CronUtil.schedule("*/10 * * * * *", (Task) () -> {
        for (String key : localRegisterNodeKeySet) {
            try {
                List<KeyValue> keyValues = kvClient.get(ByteSequence.from(key, StandardCharsets.UTF_8))
                    .get()
                    .getKvs();

                // 节点已过期,不进行续期(可考虑重启注册逻辑)
                if (CollUtil.isEmpty(keyValues)) {
                    continue;
                }

                // 节点未过期,执行重新注册(续签)
                KeyValue keyValue = keyValues.get(0);
                String value = keyValue.getValue().toString(StandardCharsets.UTF_8);
                ServiceMetaInfo serviceMetaInfo = JSONUtil.toBean(value, ServiceMetaInfo.class);
                register(serviceMetaInfo);

            } catch (Exception e) {
                throw new RuntimeException(key + "续签失败", e);
            }
        }
    });
    CronUtil.setMatchSecond(true);
    CronUtil.start();
}

⚠️ 注意:续期间隔应小于 etcd 的租约过期时间,以避免在下一次心跳前租约已失效。

4. 修改原有方法逻辑,集成心跳机制

为了让心跳机制生效,我们需要对原有代码进行适配:

初始化阶段启动心跳:

@Override
public void init(RegistryConfig registryConfig) {
    // 初始化操作...
    heartBeat(); // 启动心跳定时任务
}

注册和下线时维护本地节点集合:

@Override
public void register(ServiceMetaInfo serviceMetaInfo) throws ExecutionException, InterruptedException {
    // 注册逻辑...
    localRegisterNodeKeySet.add(registerKey); // 添加到本地节点集合
}

@Override
public void unRegister(ServiceMetaInfo serviceMetaInfo) {
    // 注销逻辑...
    localRegisterNodeKeySet.remove(registerKey); // 从集合移除
}

节点销毁时,释放资源前手动注销:

@Override
public void destroy() {
    log.info("当前节点下线");

    // 主动删除注册信息
    for (String key : localRegisterNodeKeySet) {
        try {
            kvClient.delete(ByteSequence.from(key, StandardCharsets.UTF_8)).get();
        } catch (ExecutionException | InterruptedException e) {
            log.error("{} 节点下线失败", key, e);
        }
    }

    // 关闭客户端连接
    if (kvClient != null) {
        kvClient.close();
    }
    if (client != null) {
        client.close();
    }
}

总结

引入看门狗机制(Watchdog)是解决服务注册状态自动维护问题的一种经典方式。通过服务节点主动发送心跳,实现对 etcd 注册信息的自动续期,不仅可以确保服务长期可用时注册信息不会失效,还可以在节点宕机时通过租约过期机制自动清理。

相比被动探活,心跳机制更加主动、低成本、易于实现,也符合当前微服务注册中心普遍采用的设计模式。

如果你在实现过程中也遇到服务注册信息过期、节点状态更新不及时等问题,不妨考虑用「心跳 + 看门狗」的思路来简化实现逻辑、提升系统健壮性。

🛠️ 下一步,我计划将心跳机制进一步抽象并集成到框架初始化流程中,同时支持自定义心跳间隔、租期配置等能力,让整个注册中心实现更通用和灵活。

License:  CC BY 4.0