netty学习2:优雅的关闭

优雅的关闭

java的优雅关闭通常通过注册 jdk 的shutdownHook 来实现。

Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.out.println("shutdown start...");

                // do your work

                System.out.println("shutdown end...");
            }
        });
        
        System.exit(0);

在应用程序优雅退出的时候,netty也需要优雅退出。netty提供了 shutdownGracefully() 来实现。在 客户端的创建 中,我们可以看到netty提供的案例中,在finally里调用了shutdownGracefully()

public final class EchoClient {
    public static void main(String[] args) throws Exception {
        // 省略了部分代码
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.TCP_NODELAY, true)
             .handler(new ChannelInitializer() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     // 省略了部分代码
                     p.addLast(new EchoClientHandler());
                 }
             });
            ChannelFuture f = b.connect(HOST, PORT).sync();
            f.channel().closeFuture().sync();
        } finally {
            // Shut down the event loop to terminate all threads.
            group.shutdownGracefully();
        }
    }
}

分析其源码:
1、NioEventLoopGroup
NioEventLoopGroup其实就是 NioEventLoop线程组,它的实现就是循环调用NioEventLoop的shutdownGracefully()。(在MultithreadEventExecutorGroup 中)

@Override
    public Future shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
        for (EventExecutor l: children) {
            l.shutdownGracefully(quietPeriod, timeout, unit);
        }
        return terminationFuture();
    }

2、NioEventLoop
通过分析 NioEventLoop 的 run() 方法,可以看到 NioEventLoop运行的时候根据线程状态来判断是否需要进行退出操作。

@Override
    protected void run() {
        for (;;) {
            // 省略部分代码: NioEventLoop 运行时处理的逻辑
            
            // Always handle shutdown even if the loop processing threw an exception.
            try {
                // 通过修改线程状态判断是否关闭。修改线程状态具体实现在父类 SingleThreadEventExecutor.shutdownGracefully() 中
                // 如果正在关闭中,则执行下列代码
                if (isShuttingDown()) {
                    closeAll();
                    // 关闭完成之后,判断是否真的可以退出。关闭连接,释放资源之后,NioEventLoop 还有扫尾工作需要进行。
                    // NioEventLoop 除了IO操作,还需要负责执行定时任务,shutDownHook 的执行等.
                    // 此时如果有到期的定时任务,即使channel已经关闭也要执行,线程不能退出
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }

    
    @Override
    public boolean isShuttingDown() {
        return state >= ST_SHUTTING_DOWN;
    }

通过线程状态 state 判断线程是否正在关闭。在其父类 SingleThreadEventExecutor.shutdownGracefully() 中(正是上文 NioEventLoopGroup中循环调用的方法)进行了对线程状态的修改,将 state 改为 ST_SHUTTING_DOWN

@Override
    public Future shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
        
        if (isShuttingDown()) {
            return terminationFuture();
        }

        // 判断当前线程是否是eventLoop绑定的线程,这样的判断在 NioEventLoop各种流程中都很常见。
        // 在客户端的创建一文中可以看到一个channel绑定了一个专门的NioEventLoop来处理channel中的各种事件,一个NioEventLoop则有一个专门的线程来执行。
        // 在整个channel的生命周期内,所有的操作都将由这个线程来执行,如果当前线程不是eventLoop绑定的线程,则会提交给eventLoop绑定的队列,等待绑定线程执行。eventLoop都实现了java的ScheduledExecutorService接口
        // 一个NioEventLoop可以被多个channel同时使用。如果是一一对应,则相当于是BIO模型,一个channel对应一个线程
        boolean inEventLoop = inEventLoop();
        boolean wakeup;
        int oldState;

        // 通过自旋和 cas 的方式修改线程状态
        for (;;) {
            // 自旋中首先判断线程状态是否已经被其他线程修改
            if (isShuttingDown()) {
                return terminationFuture();
            }
            int newState;
            wakeup = true;
            oldState = state;
            if (inEventLoop) {
                newState = ST_SHUTTING_DOWN;
            } else {
                switch (oldState) {
                    case ST_NOT_STARTED:
                    case ST_STARTED:
                        newState = ST_SHUTTING_DOWN;
                        break;
                    default:
                        newState = oldState;
                        wakeup = false;
                }
            }
            // 如果没有其他线程修改 state, 则通过 cas 的方式修改 state
            if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {
                break;
            }
        }
        gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod);
        gracefulShutdownTimeout = unit.toNanos(timeout);

        if (oldState == ST_NOT_STARTED) {
            thread.start();
        }

        if (wakeup) {
            wakeup(inEventLoop);
        }

        return terminationFuture();
    }

因为 shutdownGracefully() 可能由 NIOEventLoopGroup 发起调用,也可能由多个用户线程发起调用,所以通过自旋加 cas 的方式进行并发控制。
回到上文中 NioEventLoop 的 run() 方法,可以看到当线程状态 state >= ST_SHUTTING_DOWN 时,执行了closeAll() 方法。closeAll() 执行完成之后,通过confirmShutdown()判断是否真的可以退出,其具体作用可以看代码中注释。
我们先分析 closeAll()的源码。

private void closeAll() {
        // 更新 selectionKey
        selectAgain();
        Set keys = selector.keys();
        Collection channels = new ArrayList(keys.size());
        for (SelectionKey k: keys) {
            Object a = k.attachment();
            if (a instanceof AbstractNioChannel) {
                // 将所有注册在selector 上的 channel 循环关闭
                channels.add((AbstractNioChannel) a);
            } else {
                k.cancel();
                @SuppressWarnings("unchecked")
                // 如果不是channel,则将task关联的channel状态置为 Unregistered
                NioTask task = (NioTask) a;
                invokeChannelUnregistered(task, k, null);
            }
        }

        for (AbstractNioChannel ch: channels) {
            // 具体实现在 AbstractChannel.AbstractUnsafe 中
            ch.unsafe().close(ch.unsafe().voidPromise());
        }
    }

这里为什么 不是channel 就是 NioTask,暂时还不是很清楚这方面的细节。
上述代码将 注册在selector 上的 channel循环调用,具体继续关注 AbstractChannel.AbstractUnsafe。

private void close(final ChannelPromise promise, final Throwable cause,
                           final ClosedChannelException closeCause, final boolean notify) {
            if (!promise.setUncancellable()) {
                return;
            }

            if (closeInitiated) {
                if (closeFuture.isDone()) {
                    // Closed already.
                    safeSetSuccess(promise);
                } else if (!(promise instanceof VoidChannelPromise)) { // Only needed if no VoidChannelPromise.
                    // This means close() was called before so we just register a listener and return
                    closeFuture.addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            promise.setSuccess();
                        }
                    });
                }
                return;
            }

            closeInitiated = true;

            final boolean wasActive = isActive();
            final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;

            // 将outboundBuffer 置为 null ,不再允许发送新的消息
            this.outboundBuffer = null; // Disallow adding any messages and flushes to outboundBuffer.
            Executor closeExecutor = prepareToClose();
            if (closeExecutor != null) {
                closeExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            // Execute the close.
                            doClose0(promise);
                        } finally {
                            // Call invokeLater so closeAndDeregister is executed in the EventLoop again!
                            invokeLater(new Runnable() {
                                @Override
                                public void run() {
                                    if (outboundBuffer != null) {
                                        // Fail all the queued messages
                                        outboundBuffer.failFlushed(cause, notify);

                                        // 释放发送队列中还没有发送完成的 byteBuf,等待 gc
                                        outboundBuffer.close(closeCause);
                                    }

                                    // 1、将链路上的channel 置为 inactive 状态 (channel 的生命周期: channelRegistered:channel 被注册到了eventLoop -> channelActive:channel处于活动状态,可以接收发送数据
                                    // -> channelInactive: channel没有连接到远程节点 -> channelRegistered: channel已经被创建但没有注册到eventLoop)

                                    // 2、从多路复用器上取消 selectionKey
                                    fireChannelInactiveAndDeregister(wasActive);
                                }
                            });
                        }
                    }
                });
            } else {
                try {
                    // Close the channel and fail the queued messages in all cases.
                    // 关闭 channel,将对应的channelFuture操作结果置为失败
                    doClose0(promise);
                } finally {
                    if (outboundBuffer != null) {
                        // Fail all the queued messages.
                        outboundBuffer.failFlushed(cause, notify);
                        outboundBuffer.close(closeCause);
                    }
                }

                // 判断当前链路是否有消息正在发送,如果有,则稍后再执行
                if (inFlush0) {
                    invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            fireChannelInactiveAndDeregister(wasActive);
                        }
                    });
                } else {
                    //
                    fireChannelInactiveAndDeregister(wasActive);
                }
            }
        }

这段代码较为复杂,其中主要做了这几个操作:
1、将outboundBuffer 置为 null ,不再允许发送新的消息
2、doClose0(promise); 关闭channel,将对应的channelFuture操作结果置为失败
3、outboundBuffer.close(closeCause); 释放发送队列中还没有发送完成的 byteBuf,等待 gc
4、fireChannelInactiveAndDeregister(wasActive);
4.1、将链路上的channel 置为 inactive 状态 (channel 的生命周期: channelRegistered:channel 被注册到了 eventLoop -> channelActive:channel处于活动状态,可以接收发送数据
-> channelInactive: channel没有连接到远程节点 -> channelRegistered: channel已经被创建但没有注 册到eventLoop);
4.2、从多路复用器上取消 selectionKey

其中NioSocketChannel 的 doClose0源码实现如下

@Override
    protected void doClose() throws Exception {
        super.doClose();
        // 关闭 java channel
        javaChannel().close();
    }

其父类 AbstractNioChannel 的实现
@Override
    protected void doClose() throws Exception {
        ChannelPromise promise = connectPromise;
        if (promise != null) {
            // Use tryFailure() instead of setFailure() to avoid the race against cancel().
            promise.tryFailure(DO_CLOSE_CLOSED_CHANNEL_EXCEPTION);
            connectPromise = null;
        }

        // 只有当设置了connection timeout 的时候,在connect 的时候会创建 connectTimeoutFuture
        ScheduledFuture future = connectTimeoutFuture;
        if (future != null) {
            // 
            future.cancel(false);
            connectTimeoutFuture = null;
        }
    }
    
// 具体实现在DefaultPromise类
@Override
    public boolean tryFailure(Throwable cause) {
        if (setFailure0(cause)) {
            notifyListeners();
            return true;
        }
        return false;
    }

其中setFailure0(cause) 最终调用
private boolean setValue0(Object objResult) {
        if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||
            RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {
            checkNotifyWaiters();
            return true;
        }
        return false;
    }

channelFuture,继承java 的 future接口,是异步操作的结果,其中用一个 result 表示异步操作的状态。channelPromise继承自channelFuture,可以通过 setSuccess(), setFailure(),trySuccess(), tryFailure()等方法标记future的result并通知注册的监听器。其中try 和 set 的区别是 try 会返回操作是否成功,失败返回false,而 set 失败则抛异常。

doClose0源码分析完成,接下来分析 fireChannelInactiveAndDeregister(wasActive) 的源码。里面主要调用了

private void deregister(final ChannelPromise promise, final boolean fireChannelInactive) {
            // 省略部分代码
            invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 从多路复用器上取消 selectionKey,具体实现在 AbstractNioChannel
                        doDeregister();
                    } catch (Throwable t) {
                        logger.warn("Unexpected exception occurred while deregistering a channel.", t);
                    } finally {
                        if (fireChannelInactive) {
                            // 将channel的状态置为 inactive,触发链路关闭通知事件,触发链路上所有 channel 的对应channelHandler的对应方法
                            pipeline.fireChannelInactive();
                        }
                        // Some transports like local and AIO does not allow the deregistration of
                        // an open channel.  Their doDeregister() calls close(). Consequently,
                        // close() calls deregister() again - no need to fire channelUnregistered, so check
                        // if it was registered.
                        if (registered) {
                            registered = false;

                            // 将channel的状态置为 Unregistered
                            pipeline.fireChannelUnregistered();
                        }
                        safeSetSuccess(promise);
                    }
                }
            });
        }

    
    @Override
    protected void doDeregister() throws Exception {
        // 从多路复用器上取消 selectionKey,具体实现调用的是 java nio的selectionKey的cancel(),这里不做展开
        eventLoop().cancel(selectionKey());
    }

至此,closeAll() 方法分析完成。让我们回到 NioEventLoop 的 run() 方法,接下来看下 confirmShutdown()的源码

protected boolean confirmShutdown() {
        if (!isShuttingDown()) {
            return false;
        }

        if (!inEventLoop()) {
            throw new IllegalStateException("must be invoked from an event loop");
        }

        // 取消scheduledTaskQueue中的所有定时任务 cancelWithoutRemove
        cancelScheduledTasks();

        if (gracefulShutdownStartTime == 0) {
            gracefulShutdownStartTime = ScheduledFutureTask.nanoTime();
        }

        // runAllTasks 执行在 taskQueue中排队的task(会将 scheduledTaskQueue 中执行时间满足条件的定时任务也添加到 taskQueue中), 只有至少一个任务成功执行才返回true
        // runShutdownHooks 执行注册到 NioEventLoop 中的 shutdownHook。shutdownHook是 jdk 优雅退出的一个钩子,在jvm 因为某些情况退出的时候可以被调用,用于我们在关闭时处理自己的逻辑
        // 在 doRegistered 的时候就会 addShutdownHook
        if (runAllTasks() || runShutdownHooks()) {
            if (isShutdown()) {
                // Executor shut down - no new tasks anymore.
                return true;
            }

            // There were tasks in the queue. Wait a little bit more until no tasks are queued for the quiet period or
            // terminate if the quiet period is 0.
            // See https://github.com/netty/netty/issues/4241
            if (gracefulShutdownQuietPeriod == 0) {
                return true;
            }
            wakeup(true);
            return false;
        }

        final long nanoTime = ScheduledFutureTask.nanoTime();

        // 判断是否到达优雅关闭指定的超时时间
        if (isShutdown() || nanoTime - gracefulShutdownStartTime > gracefulShutdownTimeout) {
            return true;
        }

        // 如果没有到达超时时间,则暂时不关闭,每隔100ms检测是否有新的任务加入,有则执行
        if (nanoTime - lastExecutionTime <= gracefulShutdownQuietPeriod) {
            wakeup(true);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // Ignore
            }

            return false;
        }

        // No tasks were added for last quiet period - hopefully safe to shut down.
        // (Hopefully because we really cannot make a guarantee that there will be no execute() calls by a user.)
        // 不能完全确保所有消息都能够处理完成
        return true;
    }

其中,首先cancelScheduledTasks()

 protected void cancelScheduledTasks() {
        assert inEventLoop();
        Queue> scheduledTaskQueue = this.scheduledTaskQueue;
        if (isNullOrEmpty(scheduledTaskQueue)) {
            return;
        }

        final ScheduledFutureTask[] scheduledTasks =
                scheduledTaskQueue.toArray(new ScheduledFutureTask[scheduledTaskQueue.size()]);

        for (ScheduledFutureTask task: scheduledTasks) {
            // 将future 的result置为 cancel,并通知监听器
            task.cancelWithoutRemove(false);
        }

        // 这里清空了scheduledTaskQueue里的元素
        scheduledTaskQueue.clear();
    }

然后, runAllTasks(),将任务从 scheduledTaskQueue中取出,添加到 taskQueue中。如果taskQueue内存不足,则再放回scheduledTaskQueue,在之后的循环中再取出。

protected boolean runAllTasks() {
        boolean fetchedAll;
        do {
            fetchedAll = fetchFromScheduledTaskQueue();
            Runnable task = pollTask();
            if (task == null) {
                return false;
            }

            for (;;) {
                try {
                    task.run();
                } catch (Throwable t) {
                    logger.warn("A task raised an exception.", t);
                }

                task = pollTask();
                if (task == null) {
                    break;
                }
            }
        } while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.

        lastExecutionTime = ScheduledFutureTask.nanoTime();
        return true;
    }

// 将任务从 scheduledTaskQueue中取出,offer 到 taskQueue中
private boolean fetchFromScheduledTaskQueue() {
        long nanoTime = AbstractScheduledEventExecutor.nanoTime();
        Runnable scheduledTask  = pollScheduledTask(nanoTime);
        while (scheduledTask != null) {
            if (!taskQueue.offer(scheduledTask)) {
                // No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.
                // taskQueue offer 失败,说明没有足够的空间添加,则将任务放回 scheduledTaskQueue, 在之后的循环会再次从 scheduledTaskQueue 中取出这个任务
                scheduledTaskQueue().add((ScheduledFutureTask) scheduledTask);
                return false;
            }
            scheduledTask  = pollScheduledTask(nanoTime);
        }
        return true;
    }

// 只会取出执行时间在彻底关闭之前的任务
protected final Runnable pollScheduledTask(long nanoTime) {
        assert inEventLoop();

        Queue> scheduledTaskQueue = this.scheduledTaskQueue;
        ScheduledFutureTask scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek();
        if (scheduledTask == null) {
            return null;
        }

        // 只会取出执行时间在彻底关闭之前的任务
        if (scheduledTask.deadlineNanos() <= nanoTime) {
            scheduledTaskQueue.remove();
            return scheduledTask;
        }
        return null;
    }

这里有一点一直没想明白。在 cancelScheduledTasks() 中已经调用 scheduledTaskQueue.clear(); 在这里却又从scheduledTaskQueue中取出 task 来执行。

最后,runShutdownHooks()

private boolean runShutdownHooks() {
        boolean ran = false;
        // Note shutdown hooks can add / remove shutdown hooks.
        while (!shutdownHooks.isEmpty()) {
            List copy = new ArrayList(shutdownHooks);
            shutdownHooks.clear();
            for (Runnable task: copy) {
                try {
                    task.run();
                } catch (Throwable t) {
                    logger.warn("Shutdown hook raised an exception.", t);
                } finally {
                    ran = true;
                }
            }
        }

        if (ran) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
        }

        return ran;
    }

循环执行我们事先添加的 shutdownHook。可以通过下面的方式添加自己需要的shutdownHook

((SingleThreadEventExecutor)eventLoop()).addShutdownHook(new Runnable());

===============================================
真正的菜鸟一枚,边学习边整理中,可能有错漏

你可能感兴趣的:(netty学习2:优雅的关闭)