完整代码在gitee: rengerpc: rrpc远程方法调用框架开发
一、概述
心跳检测实际上就是一个调用方对服务方的没有payLoad的请求,用来检测服务节点是否持续存在
事实上netty框架本身可以实现探活,可以自动感知channel的连接状态。
其核心原理十分简单,就是定期向所有的channel发送一个简单的请求即可,如果能得到回应说明连接是正常的。 其中我们要在心跳探测的过程中完成以下几项工作:
1、如果可以正常访问,记录响应时间,以备后用。
2、如果不能正常访问,则进行重试,重试三次依旧不能访问,则从服务列表中剔除,以后的访问不会使用该连接。
注意:重试的等待时间我们选取一个合适范围内的随机时间,这样可以避免局域网络问题导致的大面积同时重试, 产生重试风暴。
动态上下线检测,主要还是利用zk的watcher机制,如果注册中心的服务列表发生改变,watcher会通知调用方,然后调用方重新向注册中心拉取服务列表并缓存,然后进行重新的负载均衡
当有服务提供方动态上下线,我们如何进行感知呢?
服务上线,首先会在注册中心进行注册,调用方是无法实时感 知的,合理的方式只有两种: 1、调用方定时的去主动的拉。
2、注册中心主动的推送。
在我们当前的项目中zk提供了watcher机制,我们正好可以利用他来实现动态上下线,具体步骤如下:
1、调用方拉取服务列表时,注册一个watcher关注该服务节点的变化。
2、当服务提供方上线或线下时会触发watcher机制(节点发生了变化)。
3、通知调用方,执行动态上下线的操作。
实现效果
1.多个节点的负载均衡心跳检测(负载均衡看我主页)
2.动态感知节点上下线,并重新对节点进行连接上线
二、具体实现
1、HeartbeatDetector 心跳检测器(心跳检测的具体实现代码)
注:心跳检测器由服务调用方的reference发布执行,是对服务节点的动态感知
@Slf4j
public class HeartbeatDetector {
public static void detectorHeartbeat(String serviceName) {
//1.从注册中心拉取服务列表并连接
Registry registry = RrpcBootstrap.getInstance().getRegistry();
List<InetSocketAddress> addresses = registry.lookup(serviceName);
//2.将连接进行缓存
for (InetSocketAddress address : addresses) {
try {
//如果没有就缓存
if (!RrpcBootstrap.CHANNEL_CACHE.containsKey(address)) {
//通过同步的方式拿到缓存
Channel channel = NettyBootStrapInitializer.getBootstrap().connect(address).sync().channel();
RrpcBootstrap.CHANNEL_CACHE.put(address, channel);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//3.定时发送任务
//使用守护线程也被称之为后台线程、服务线程或精灵线程程值
Thread thread = new Thread(() ->
new Timer().scheduleAtFixedRate(new MyTimerTask(), 0, 2000)
, "rrpc-HeartbeatDetector-thread");
//设置为守护线程,主线程没了守护线程自动消失
thread.setDaemon(true);
thread.start();
log.info("当前线程:【{}】,正在运行", thread.getName());
}
private static class MyTimerTask extends TimerTask {
@Override
public void run() {
//每次心跳,将响应时长的map清空
RrpcBootstrap.ANSWER_TIME_CHANNEL_CACHE.clear();
//遍历所有的channel
Map<InetSocketAddress, Channel> cache = RrpcBootstrap.CHANNEL_CACHE;
//遍历map
for (Map.Entry<InetSocketAddress, Channel> entry : cache.entrySet()) {
//定义重试的次数
int tryTimes = 3;
while (tryTimes > 0) {
Channel channel = entry.getValue();
long start = System.currentTimeMillis();
//创建一个心跳请求
RrpcRequest rrpcRequest = RrpcRequest.builder()
.requestId(RrpcBootstrap.ID_GENERATOR.getId())
.requestType(RequestType.HEART_BEAT.getId())
.serializeType(SerializerFactory.getSerializer(RrpcBootstrap.SERIALIZE_TYPE).getCode())
.compressType(CompressorFactory.getCompressor(RrpcBootstrap.COMPRESS_TYPE).getCode())
// .timeStamp(start)
.build();
// 需要将CompletableFuture暴露并挂起
CompletableFuture<Object> completableFuture = new CompletableFuture<>();
RrpcBootstrap.PENDING_REQUEST.put(rrpcRequest.getRequestId(), completableFuture);
//writeAndFlush 写出一个请求 请求的实例加入pipeline执行出战的一系列操作 将请求转化成二进制报文
channel.writeAndFlush(rrpcRequest).addListener((ChannelFutureListener) promise -> {
if (!promise.isSuccess()) {
completableFuture.completeExceptionally(promise.cause());
}
});
//
Long endTime = 0L;
try {
//如果得不到结果就会一直阻塞,只等一秒
completableFuture.get(1, TimeUnit.SECONDS);
endTime = System.currentTimeMillis();
} catch (InterruptedException | ExecutionException | TimeoutException e) {
tryTimes--;
log.error("和地址为【{}】的主机连接发生异常,正在进行第【{}】次重试", channel.remoteAddress(), 3 - tryTimes);
//重试的机会用尽,将失效的地址移除服务列表
if (tryTimes == 0) {
//将失效的地址移除服务列表
RrpcBootstrap.CHANNEL_CACHE.remove(entry.getKey());
}
//尝试等待一段时间后重试
try {
Thread.sleep(10 * (new Random().nextInt(5)));
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
continue;
}
//获取时间戳
Long time = endTime - start;
//使用treeMap进行缓存
RrpcBootstrap.ANSWER_TIME_CHANNEL_CACHE.put(time, channel);
log.debug("和【{}】服务器的响应时间是[{}]", entry.getKey(), time);
break;
}
}
log.info("-------------------响应时间的treeMap------------------");
for (Map.Entry<Long, Channel> entry : RrpcBootstrap.ANSWER_TIME_CHANNEL_CACHE.entrySet()) {
if (log.isDebugEnabled()) {
log.debug("[{}]------>[{}]", entry.getKey(), entry.getValue().id());
}
}
}
}
}
2、UpAndDownWatcher继承了watcher是实现动态上下线感知的主要代码
@Slf4j
public class UpAndDownWatcher implements Watcher {
@Override
public void process(WatchedEvent event) {
//当前节点是否发生了变化
if (event.getType() == Event.EventType.NodeChildrenChanged) {
if (log.isDebugEnabled()) {
log.debug("检测到服务【{}】下有节点上/下线:将重新拉取服务列表......", event.getPath());
}
String serviceName = getServiceName(event.getPath());
Registry registry = RrpcBootstrap.getInstance().getRegistry();
List<InetSocketAddress> addresses = registry.lookup(serviceName);
//处理节点上线
for (InetSocketAddress address : addresses) {
//此时有两种情况
//1.新增的节点 会在address 不在 CHANNEL_CACHE
//2.下线的节点 可能会在CHANNEL_CACHE 不在address
if (!RrpcBootstrap.CHANNEL_CACHE.containsKey(address)){
Channel channel = null;
try {
channel = NettyBootStrapInitializer.getBootstrap()
.connect(address).sync().channel();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
RrpcBootstrap.CHANNEL_CACHE.put(address,channel);
}
}
//处理下线的节点,也可通过心跳检测
//遍历channel 下线的节点可能在CHANNEL_CACHE 不在address
for (Map.Entry<InetSocketAddress,Channel> entry : RrpcBootstrap.CHANNEL_CACHE.entrySet()){
if (!addresses.contains(entry.getKey())){
//如果节点不在address 就remove
RrpcBootstrap.CHANNEL_CACHE.remove(entry.getKey());
}
}
//获取负载均衡器,进行重新的loadBalance
LoadBalancer loadBalancer = RrpcBootstrap.LOAD_BALANCER;
loadBalancer.reLoadBalance(serviceName,addresses);
}
}
private String getServiceName(String path) {
String[] split=path.split("/");
return split[split.length - 1];
}
}