简易RPC框架实现——6、实现服务自动注销,引入心跳机制

本文详细介绍了如何使用Netty实现服务端与客户端的心跳保活机制,确保连接有效性,并通过配置IdleStateHandler监听心跳事件。同时,通过在JVM关闭时注册钩子函数,实现在服务下线时自动注销Nacos中的服务,避免无效服务调用,提高了系统效率。测试验证了心跳机制和服务注销功能的正确性。
摘要由CSDN通过智能技术生成

本章在前篇引入nacos的基础上,继续实现了对于服务下线时,服务自动注销的功能,而且基于Netty心跳机制,来实现服务端与客户端通道的保活。本章的commit为1b44d89

心跳机制

在客户端与服务端的通信中,为了保证连接的有效可靠性,需要由客户端定时向服务端发送心跳包以证明自己的连接仍然有效。

而在ChannelOption中我们选择开启了TCP的Keepalive机制。但是该机制默认的心跳间隔时两小时,且依赖操作系统的实现不够灵活。所以我们除了开启TCP的心跳机制之外,还需要引入Netty为我们提供的IdleStateHandler:

    public IdleStateHandler(
            int readerIdleTimeSeconds,
            int writerIdleTimeSeconds,
            int allIdleTimeSeconds) {

        this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds,
             TimeUnit.SECONDS);
    }

第一个参数表示规定时间未收到读事件,则会向下一个入站处理器的userEventTriggered方法发送一个状态为IdleState.Reader_IDLE的消息,服务端可以根据此来断定客户端是否断开连接。

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof IdleStateEvent){
            IdleState state = ((IdleStateEvent) evt).state();
            if(state == IdleState.READER_IDLE){
                logger.info("长时间未收到心跳包,断开连接");
                ctx.close();
            }
        }
        else{
            super.userEventTriggered(ctx, evt);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) throws Exception {
        threadPool.execute(() -> {
            try {
                if(msg.getHeartbeatMessage()){
                    logger.info("收到客服端的心跳包。。。");
                    return;
                }
                logger.info("服务器接收到请求:{}",msg);
                Object service = serverPublisher.getService(msg.getInterfaceName());
                Object res = requestHandler.handle(msg, service);
                ChannelFuture future = ctx.writeAndFlush(RpcResponse.success(res, msg.getRequestId()));
                //future.addListener(ChannelFutureListener.CLOSE);
            } finally {
                ReferenceCountUtil.release(msg);
            }
        });
    }

第二个参数表示规定时间未发出写事件,则会向下一个入站处理器的userEventTriggered方法发送一个状态为IdleState.WRITER_IDLE的消息,可以在userEventTriggered方法中写下向服务端发送心跳包的代码。

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof IdleStateEvent){
            IdleState state = ((IdleStateEvent) evt).state();
            if(state == IdleState.WRITER_IDLE){
                RpcRequest rpcRequest = new RpcRequest();
                rpcRequest.setHeartbeatMessage(true);
                logger.info("向服务端:{}发送心跳包:{}",ctx.channel().remoteAddress(),rpcRequest);
                ctx.writeAndFlush(rpcRequest);
            }
        }else {
            super.userEventTriggered(ctx, evt);
        }
    }

完成了客户端如果没有向服务端发送事件,就会采用心跳机制定时向服务端发送心跳包保活。

利用钩子函数实现服务自动注销

在引入Nacos作注册中心,实现了服务发现以及服务注册的功能,但同时也会带来一些新的问题。虽然Nacos有自己的心跳机制来监测每一个实例是否存活,但是在大量服务调用的情况下,如果服务端突然下架,仍然可能存在Nacos未及时删除下架服务,导致客户端调用到不存在的服务,导致效率降低。

因此我们可以采用钩子函数,来实现在jvm关闭时完成,所有服务的注销,以及线程池的关闭。

首先我们需要在NacosUtils中新建一个删除此服务端所有已经注册过的服务:

    public static void clearRegistry(){
        if(!serviceNameSet.isEmpty() && address != null){
            Iterator<String> iterator = serviceNameSet.iterator();
            while(iterator.hasNext()){
                String serviceName = iterator.next();
                try {
                    namingService.deregisterInstance(serviceName,address.getHostName(),address.getPort());
                    logger.info("成功注销{}服务",serviceName);
                } catch (NacosException e) {
                    logger.error("注销服务{}失败",serviceName);
                }
            }
        }
    }

之后可以用饿汉式的方式创建一个钩子实例,里边存放在jvm关闭时我们需要完成的逻辑:

public class ShutdownHook {
    private static final Logger logger = LoggerFactory.getLogger(ShutdownHook.class);
    private static final ExecutorService threadPool = ThreadPoolFactory.creatDefaultThreadPool("shutdown-hook-pool");
    private static final ShutdownHook shutdownHook = new ShutdownHook();

    public static ShutdownHook getShutdownHook(){
        return shutdownHook;
    }

    public void addClearHook(){
        logger.info("关闭后将自动注销所有服务。。。");
        Runtime.getRuntime().addShutdownHook(new Thread(()->{
            NacosUtils.clearRegistry();
            ThreadPoolFactory.shutdownAll();
        }));
    }
}

之后我们只需要将此钩子挂在服务端启动代码中,这样的话,在服务端关闭时,就会自动执行钩子中的代码:

    @Override
    public void start() {
        ShutdownHook.getShutdownHook().addClearHook();
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boss,worker)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .option(ChannelOption.SO_BACKLOG,256)
                    .option(ChannelOption.SO_KEEPALIVE,true)
                    .childOption(ChannelOption.TCP_NODELAY,true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new IdleStateHandler(30,0,0, TimeUnit.SECONDS))
                                    .addLast(new MyEncoder(serializer))
                                    .addLast(new MyDecoder())
                                    .addLast(new NettyServerHandler(serverPublisher));
                        }
                    });
            ChannelFuture future = serverBootstrap.bind(host,port).sync();
            future.channel().closeFuture().sync();
        }catch (InterruptedException e){
            logger.error("启动服务时发生错误");
        }
        finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

测试

服务端

[main] INFO cn.fzzfrjf.core.DefaultServerPublisher - 向接口:interface cn.fzzfrjf.entity.HelloService注册服务:cn.fzzfrjf.service.HelloServiceImpl
[main] WARN com.alibaba.nacos.client.utils.LogUtils - Load Log4j Configuration of Nacos fail, message: org/apache/logging/log4j/LogManager
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Property :null
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Environment :null
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Property :null
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - [settings] [req-serv] nacos-server port:8848
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - [settings] [http-client] connect timeout:1000
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - NACOS_CLIENT_VERSION: ${project.version}
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - PER_TASK_CONFIG_SIZE: 3000.0
[main] INFO com.alibaba.nacos.client.naming - [BEAT] adding beat: BeatInfo{port=9999, ip='activate.navicat.com', weight=1.0, serviceName='DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService', cluster='DEFAULT', metadata={}, scheduled=false, period=5000, stopped=false} to beat map.
[main] INFO com.alibaba.nacos.client.naming - [REGISTER-SERVICE] public registering service DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService with instance: Instance{instanceId='null', ip='activate.navicat.com', port=9999, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='null', metadata={}}
[main] INFO com.alibaba.nacos.client.identify.CredentialWatcher - null No credential found
[main] INFO cn.fzzfrjf.core.DefaultServerPublisher - 向接口:interface cn.fzzfrjf.entity.ByeService注册服务:cn.fzzfrjf.service.ByeServiceImpl
[main] INFO com.alibaba.nacos.client.naming - [BEAT] adding beat: BeatInfo{port=9999, ip='activate.navicat.com', weight=1.0, serviceName='DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService', cluster='DEFAULT', metadata={}, scheduled=false, period=5000, stopped=false} to beat map.
[main] INFO com.alibaba.nacos.client.naming - [REGISTER-SERVICE] public registering service DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService with instance: Instance{instanceId='null', ip='activate.navicat.com', port=9999, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='null', metadata={}}
[main] INFO cn.fzzfrjf.utils.ShutdownHook - 关闭后将自动注销所有服务。。。
[main] WARN io.netty.bootstrap.ServerBootstrap - Unknown channel option 'SO_KEEPALIVE' for channel '[id: 0xc7530b51]'
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xc7530b51] REGISTERED
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xc7530b51] BIND: /127.0.0.1:9999
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xc7530b51, L:/127.0.0.1:9999] ACTIVE
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xc7530b51, L:/127.0.0.1:9999] READ: [id: 0xd5b30982, L:/127.0.0.1:9999 - R:/127.0.0.1:52023]
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0xc7530b51, L:/127.0.0.1:9999] READ COMPLETE
[netty-server-handler-0] INFO cn.fzzfrjf.core.NettyServerHandler - 服务器接收到请求:cn.fzzfrjf.entity.RpcRequest@4aa63002
[netty-server-handler-1] INFO cn.fzzfrjf.core.NettyServerHandler - 服务器接收到请求:cn.fzzfrjf.entity.RpcRequest@2ef4bf57
[netty-server-handler-2] INFO cn.fzzfrjf.core.NettyServerHandler - 收到客服端的心跳包。。。
[netty-server-handler-3] INFO cn.fzzfrjf.core.NettyServerHandler - 收到客服端的心跳包。。。
[Thread-7] INFO com.alibaba.nacos.client.naming - [BEAT] removing beat: DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService:activate.navicat.com:9999 from beat map.
[Thread-7] INFO com.alibaba.nacos.client.naming - [DEREGISTER-SERVICE] public deregistering service DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService with instance: Instance{instanceId='null', ip='activate.navicat.com', port=9999, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='null', metadata={}}
[Thread-7] INFO cn.fzzfrjf.utils.NacosUtils - 成功注销cn.fzzfrjf.entity.ByeService服务
[Thread-7] INFO com.alibaba.nacos.client.naming - [BEAT] removing beat: DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService:activate.navicat.com:9999 from beat map.
[Thread-7] INFO com.alibaba.nacos.client.naming - [DEREGISTER-SERVICE] public deregistering service DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService with instance: Instance{instanceId='null', ip='activate.navicat.com', port=9999, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='null', metadata={}}
[Thread-7] INFO cn.fzzfrjf.utils.NacosUtils - 成功注销cn.fzzfrjf.entity.HelloService服务
[Thread-7] INFO cn.fzzfrjf.utils.ThreadPoolFactory - 关闭所有线程。。。。
[Thread-7] INFO cn.fzzfrjf.utils.ThreadPoolFactory - 关闭线程池[shutdown-hook-pool][true]
[Thread-7] INFO cn.fzzfrjf.utils.ThreadPoolFactory - 关闭线程池[netty-server-handler][true]

客户端

[main] WARN com.alibaba.nacos.client.utils.LogUtils - Load Log4j Configuration of Nacos fail, message: org/apache/logging/log4j/LogManager
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Property :null
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Environment :null
[main] INFO com.alibaba.nacos.client.naming - initializer namespace from System Property :null
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - [settings] [req-serv] nacos-server port:8848
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - [settings] [http-client] connect timeout:1000
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - NACOS_CLIENT_VERSION: ${project.version}
[main] INFO com.alibaba.nacos.client.utils.ParamUtil - PER_TASK_CONFIG_SIZE: 3000.0
[main] INFO com.alibaba.nacos.client.identify.CredentialWatcher - null No credential found
[main] INFO com.alibaba.nacos.client.naming - new ips(1) service: DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService -> [{"instanceId":"activate.navicat.com#9999#DEFAULT#DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService","ip":"activate.navicat.com","port":9999,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService","metadata":{},"instanceHeartBeatInterval":5000,"ipDeleteTimeout":30000,"instanceIdGenerator":"simple","instanceHeartBeatTimeOut":15000}]
[main] INFO com.alibaba.nacos.client.naming - current ips:(1) service: DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService -> [{"instanceId":"activate.navicat.com#9999#DEFAULT#DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService","ip":"activate.navicat.com","port":9999,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@cn.fzzfrjf.entity.HelloService","metadata":{},"instanceHeartBeatInterval":5000,"ipDeleteTimeout":30000,"instanceIdGenerator":"simple","instanceHeartBeatTimeOut":15000}]
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.ChannelProvider - 获取channel连接成功,连接到服务器activate.navicat.com:9999
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClient - 成功向服务器发送请求:cn.fzzfrjf.entity.RpcRequest@6a94a844
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClientHandler - 客户端获取到服务端返回的信息:RpcResponse(code=200, requestId=11a6e351-9808-4142-8c8e-911fe36e8fdb, data=这是id为:2发送的:This is NettyClient!)
[main] INFO com.alibaba.nacos.client.naming - new ips(1) service: DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService -> [{"instanceId":"activate.navicat.com#9999#DEFAULT#DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService","ip":"activate.navicat.com","port":9999,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService","metadata":{},"instanceHeartBeatInterval":5000,"ipDeleteTimeout":30000,"instanceIdGenerator":"simple","instanceHeartBeatTimeOut":15000}]
[main] INFO com.alibaba.nacos.client.naming - current ips:(1) service: DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService -> [{"instanceId":"activate.navicat.com#9999#DEFAULT#DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService","ip":"activate.navicat.com","port":9999,"weight":1.0,"healthy":true,"enabled":true,"ephemeral":true,"clusterName":"DEFAULT","serviceName":"DEFAULT_GROUP@@cn.fzzfrjf.entity.ByeService","metadata":{},"instanceHeartBeatInterval":5000,"ipDeleteTimeout":30000,"instanceIdGenerator":"simple","instanceHeartBeatTimeOut":15000}]
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClient - 成功向服务器发送请求:cn.fzzfrjf.entity.RpcRequest@4db64a90
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClientHandler - 客户端获取到服务端返回的信息:RpcResponse(code=200, requestId=6670516c-b80d-4428-887f-f6f3b75ca85c, data=(This is NettyClient!),bye!)
这是id为:2发送的:This is NettyClient!
(This is NettyClient!),bye!
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClientHandler - 向服务端:activate.navicat.com/127.0.0.1:9999发送心跳包:cn.fzzfrjf.entity.RpcRequest@192655c0
[nioEventLoopGroup-2-1] INFO cn.fzzfrjf.core.NettyClientHandler - 向服务端:activate.navicat.com/127.0.0.1:9999发送心跳包:cn.fzzfrjf.entity.RpcRequest@1e8c9c24

成功引入了心跳机制并且实现了服务的自动注销。
未完。。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值