Netty in action ——— 事件循环 和 线程模式

本文是Netty文集中“Netty in action”系列的文章。主要是对Norman Maurer and Marvin Allen Wolfthal 的 《Netty in action》一书简要翻译,同时对重要点加上一些自己补充和扩展。

概要

  • 线程模式概述
  • 事件循环概念和实现
  • 定时任务
  • 实现细节

线程模型概述

一个基于线程池的模式可以描述为:

  • 从池的空闲队列中选择一个线程,并将该线程分配以运行一个提交上来的任务( 任务实现了Runnable接口 )。
  • 当任务完成,线程返回给队列,并可用于重复使用。

池和重用改善了每个任务创建和销毁一个线程的开销,但这并没有消除线程上下文切换的开销,随着线程的增加这种开销会快速显现,并且在负载较重的情况下变得严峻。

EventLoop 接口

执行一个任务用于处理一个连接生命周期期间遇到的事件,这是任何一个网络框架的基本功能。相应的网络结构经常会引用一个事件循环( event loop ),Netty采用 io.netty.channel.EventLoop 接口。

一个事件循环的基本思想通过👇的例子来展示:

Netty的EventLoop是一个协作设计的一部分,该协作设计使用了两个基本APIs:并发 和 网络。第一个是,io.netty.util.concurrent包,该包依赖于JDK java.util.concurrent包,用于提供线程执行器。第二个的类在包io.netty.channel中,扩展该类为了与Channel事件对接。

概括的说,EventLoop用于遍历依次执行所有可运行的事件和任务,那么事件和任务在哪执行了?这里就需要通过EventExecutor来为事件/任务提供的执行器。而EventExecutor底层则是依赖于JDK 的java.util.concurrent包中的Executor来实现执行器的。

一个EventLoop由一个永远不会改变的线程所驱动,并且任务( Rannable 或 Callable )能被直接提交给EventLoop实现立即或定时的执行。

Event/Task 执行的顺序:事件和任务根据FIFO( 先进先出 )的顺序被执行。这消除了数据损坏的可能性,因此保证了以正确的顺序处理字节内容。

依赖系统配置和有效核心,可以创建多个EventLoops以使资源使用最优化,并且一个EventLoop可能被分配与服务多个Channels。

Netty4 中的I/O和事件处理

I/O操作触发一个事件,该事件流经含有一个或多个ChannelHandlers实例的ChannelPipeline。传播这些事件的方法调用能被ChannelHandler拦截,并根据需要处理事件。
一个事件的本质通常决定了它该被如何处理;它可能转换数据从网络栈到你的应用中,或执行相反操作,或执行完全不同的操作。但事件处理逻辑必须是通用的并且足够灵活去处理所有情况。因此,在Netty4中所有I/O操作和事件处理都在EventLoop所在的线程上执行。
这与Netty3是不同的。

Netty3 中的I/O操作

在早前的版本的线程模式中,仅保证所有的入站事件会在I/O线程(相当于Netty 4 中的EventLoop)上执行。所有的出站事件将通过调用线程来处理,该线程可能是I/O线程也可能是其他线程。起初这看似是一个好主意,但是很快这被认为是会有问题的,因为我们需要小心出站事件在ChannelHandler的同步性 ( 👈 比如,你再不同的线程中同时调用了同一个Channel的Channel.write()方法。因此出站的ChannelHandler可以被多个线程同时方法,这就存在了同步性问题 )。总而言之,多个线程不会尝试同时访问一个出站事件,这是无法保证的。这是可能发生的,比如,你再不同的线程中同时调用了同一个Channel的Channel.write()方法,以此触发了相同的下游事件。
另一个消极的副作用发生在,当一个入站事件作为一个出站事件的结果被触发。当Channel.write()导致了一个异常,你需要产生并触发一个exceptionCaught事件。但在Netty3模式中,因为exceptionCaught是一个入站事件,你将在调用线程上去挂起执行代码的线程(即,挂起执行Channel.write的线程),然后到I/O线程上执行该异常事件,这产生了额外的线程上下文切换。
Netty4所采用的线程模式解决了该问题,通过处理所有的事情在一个给定EventLoop上的同一个线程。这提供了一个简单的执行架构并消除了ChannelHandler的同步必要性( 除了可在多个Channels共享的ChannelHandler )。

定时任务

偶尔你需要延迟处理一个任务或定时周期性处理一个任务。比如,你可能想要注册一个事件用于解除一个客户端连接,当该客户端已经连接5分钟后。一个常见的使用场景是发送一个心跳包消息到远端去检查连接是否还活着。如果没后收到回复,你就知道你能够关闭这个Channel。

JDK的定时API

尽管ScheduledExecutorService API非常的简单,但在大负载下可能会导致性能损耗。下一节,我们将看到Netty如何使用更好的性能来提供相同的功能

使用EventLoop的定时任务

ScheduledExecutorService的实现是有限制性的,比如额外的线程被创建作为池管理的一部分。如果许多任务被积极安排,这可能会遇到瓶颈。
使用JDK的定时任务线程池ScheduledExecutorService的局限性包括:
① 在IO线程中聚合了一个独立的定时任务线程池,这样在处理过程中会存在线程上下文切换问题,这就打破了Netty的串行化设计理念;
② 存在多线程并发操作问题,因为定时任务Task和IO线程NioEventLoop可能同时访问并修改同一份数据;
③ JDK的ScheduledExecutorService从性能角度看,存在性能优化空间。

Netty通过使用channel的EventLoop调度来解决这个问题,如下例子:

Netty的EventLoop继承了ScheduledExecutorService,所以它通过对JDK的实现提供所有方法的可用性,包括schedule()和scheduleAtFixedRate()。

为了取消或检查一个执行的状态,可以使用ScheduledFuture,每个异步操作都将返回ScheduledFuture。



实现细节

线程管理

Netty线程模式的优越性取决于确定当前执行线程的一致性;也就是,它是否一个分配给当前Channel以及EventLoop的线程。(EventLoop负责处理一个Channel的所有事件在这个Channel的生命周期期间)
如果调用线程就是EventLoop所在线程的话,那么执行该代码块。否则,EventLoop安排一个任务用于随后执行并将该任务放到一个内部队列中。当EventLoop下一次处理它的事件时,EventLoop将执行队列中的任务。这解释了为什么多个线程能够通过Channel直接交互而不用在ChannelHandler中进行同步操作。
注意,每个EventLoop都有它自己的任务队列,是独立于其他EventLoop的。


我们早前提到过很重要的一点:不要阻塞当前的I/0线程。我们将从另一个方式说明这点:不要将一个耗时的任务放到执行队列中,因为这将阻塞同一线程其他任务。如果你必须使用一个阻塞调用或者执行一个耗时任务,我们建议使用一个专门的EventExecutor。

这里我们根据Netty源码来更加明确的阐述上面的观点,这里我用NioEventLoop的源码进行描述:

① 当执行注册Channel的操作在EventLoop所在线程时,则执行执行该操作

② 当执行注册Channel的操作不在EventLoop所在线程时,则将要执行的操作通过EventLoop.execute进行提交。
③ 而通过EventLoop.execute提交的任务通过添加的到EventLoop内部的一个任务队列中。而任务队列的任务会在NioEventLoop的run方法中得以调用。
// NioEventLoop的run()方法
    protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));

                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                        // fall through
                    default:
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        runAllTasks();
                    }
                } else {
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        final long ioTime = System.nanoTime() - ioStartTime;
                        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
            // Always handle shutdown even if the loop processing threw an exception.
            try {
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }

该方法通过Java NIO Selector的多路复用来实现对多个Channel的监控,该方法还对epoll CPU空轮询bug进行了解决,并且在处理完Selector返回的可执行事件后,会处理taskQueue中的任务。

EventLoop/thread 分配

服务I/O和Channels事件的EventLoops包含在一个EventLoopGroup里。EventLoops被创建和分配的方式是根据传输实现而有所不同。

  • 异步传输
    异步实现只使用少量的EventLoops(以及和它们关联的线程),在并发模式它们能被多个Channels共享。这允许通过尽可能最少的线程数服务大量Channels,而不是每个Channel分配一个线程。

    在创建EventLoopGroup的时候会直接分配EventLoops,这将使它们可用在需要的时候。
    EventLoopGroup负责去分配一个EventLoop到每一个新创建的Channel。在当前的实现中,使用一个轮询方式以得到一个均衡分布,并且同一个EventLoop可能被分配给多个Channels。

    从EventLoopGroup中以轮询的方式获取EventLoop:
    一旦一个Channel被分配给一个EventLoop,那么它将使用这个EventLoop( 以及对应的线程 )在它的整个生命周期里。记住这一点,因为它将使你不用为你的ChannelHandler的线程安全性和同步性而担心。
    另外,请注意EventLoop分配对ThreadLocal使用的影响。因为你EventLoop通常为多个Channels所使用,所以该EventLoop下所有的Channels将对应同一个ThreadLocal。这使得通过ThreadLocal实现一个状态功能将是一个糟糕的选择。然而,在无状态的环境下,在多通道间共享巨大或昂贵的对象甚至是事件仍然是有用的。

  • 阻塞传输

    一个EventLoop( 以及对应的线程 ) 被分配给一个Channel,即一个EventLoop只对应一个Channel。
    但是就像以前一样,它保证每个Channel的I/O事件只会在一个线程上执行——该线程为Channel的EventLoop提供支持。这是Netty设计一致性的另一个例子,并且为Netty可靠性和易用性提供了强大的贡献。

在NIO模式下,EventLoop在EventLoopGroup创建的时候就分配好了,EventLoop的个数是固定的了;而OIO模式下EventLoop是在创建Channel的时候才会同时创建一个EventLoop并分配给这个新创建的Channel,EventLoop的个数随着Channel的增加而增加。可以见在NIO模式下,EventLoop和Channel是一对多的关系;而在OIO模式下,EventLoop和Channel是一对一的关系。并且在NIO模式下,EventLoop的个数是可知的;而在OIO模式下EventLoop的个数是不可知的,它随着Channel的增加而增加。

后记

本文主要对Netty的事件循环和线程模式进行了介绍,其中事件循环是Netty中非常重要的一部分,也涉及到了很多的知识点,也是Netty设计一致性的例子之一。在以后的文章中,还会对EventLoop中涉及到的重要知识点进行详细的分析。
若文章有任何错误,望大家不吝指教:)

参考

《Netty in action》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容