心跳检测
给注册的结点信息一个倒计时,让结点定期的续期,重置倒计时,如果节点宕机了,一直不续期,etcd就会进行key的过期删除
- 服务者想etcd注册自己的服务信息,在注册时设置过期时间
- etcd收到服务方的信息后一直维持这个过期时间,并在过期后删除key
- 服务方定期向etcd发起请求续签自己的注册信息(续期时间小于过期时间)
java
// 创建一个 30 秒的租约
long leaseId = leaseClient.grant(30).get().getID();
// 设置要存储的键值对
String registerKey = ETCD_ROOT_PATH + serviceMetaInfo.getServiceNodeKey();
ByteSequence key = ByteSequence.from(registerKey, StandardCharsets.UTF_8);
ByteSequence value = ByteSequence.from(JSONUtil.toJsonStr(serviceMetaInfo), StandardCharsets.UTF_8);
// 将键值对与租约关联起来,并设置过期时间
PutOption putOption = PutOption.builder().withLeaseId(leaseId).build();
kvClient.put(key, value, putOption).get();
实现心跳检测
java
for(String key : loaclRegistryNodeKeySet) {
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);
}
}
服务节点下线机制
- 主动下线:关闭了provider的链接
- 被动下线:服务者出现异常后,将该节点剔除
我们需要完善的是主动下线机制,利用jvm中的shutdownHook实现,允许开发者在jvm关闭前完成一些必要操作,例如数据库的关闭,释放资源等
java
@Override
public void destroy() {
System.out.println("当前节点下线");
// 下线节点,遍历所有的key
for(String key: loaclRegistryNodeKeySet) {
try{
kvClient.delete(ByteSequence.from(key, StandardCharsets.UTF_8));
}catch (Exception e){
throw new RuntimeException(key+"下线失败");
}
}
// 释放资源
if (kvClient != null) {
kvClient.close();
}
if (client != null) {
client.close();
}
}
消费者服务缓存
由于服务节点的更新频率不是很高,所以在注册后完完全可以缓存在本地,下次就不需要在注册中心获取了,此时我们通过本地缓存实现
java
public class RegistryServiceCache {
/**
* 服务缓存
*/
List<ServiceMetaInfo> serviceCache;
/**
* 写缓存
*
* @param newServiceCache
* @return
*/
void writeCache(List<ServiceMetaInfo> newServiceCache) {
this.serviceCache = newServiceCache;
}
/**
* 读缓存
*
* @return
*/
List<ServiceMetaInfo> readCache() {
return this.serviceCache;
}
/**
* 清空缓存
*/
void clearCache() {
this.serviceCache = null;
}
}
修改我们EtcdRegistry的判断逻辑
java
// 优先从缓存获取服务
List<ServiceMetaInfo> cachedServiceMetaInfoList = registryServiceCache.readCache();
if (cachedServiceMetaInfoList != null) {
return cachedServiceMetaInfoList;
}
......
// 写入服务缓存
registryServiceCache.writeCache(serviceMetaInfoList);
服务端缓存更新机制
当注册信息发生改变后(例如节点下线),需要及时更新消费缓存,此时采用etcd的watch机制,当监听到某个key发生变化后,触发事件
java
/**
* 只监听首次加入到监听集合中的key,防止重复
* @param serviceNodeKey
*/
@Override
public void watch(String serviceNodeKey) {
Watch watchClient = client.getWatchClient();
// 开启监听
boolean newWatch = watchKeySet.add(serviceNodeKey);
if (newWatch) {
watchClient.watch(ByteSequence.from(serviceNodeKey,StandardCharsets.UTF_8),response->{
for(WatchEvent event : response.getEvents()){
switch (event.getEventType()){
// key 删除时触发
case DELETE:
// 清除缓存
registryServiceCache.clearCache();
break;
case PUT:
default:
break;
}
}
});
}
}
tip:ETCD集群的高可用保障
- 多节点部署,确保集群中有足够的健康节点
- raft一致性算法:通过选举和复制机制,确保集群中节点状态保持一致
- 自动选举和复制
- 负载均衡:分发请求到每个节点中,确保了高可用
- 监控和告警
- 通过WAL机制进行日志记录,即使在系统崩溃后也能进行数据恢复