rrpc:实现心跳检测和服务节点动态上下线

 完整代码在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];
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北方569

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值