Netty原理篇-EventLoop、EventLoopGroup


本文为《Netty权威指南》的读书笔记,读书过程中也伴随着一些源码阅读和其他文档阅读,所以内容和《Netty权威指南》会略有不同,请知晓。

Netty Api地址:http://netty.I/O/5.0/api/

 

一 线程模型

1. Reactor单线程模型

1) 线程模型

53f55614ccdfc67cb14b0087ee6240654dfcdb67

所有的I/O操作都在一个NIO线程上完成。NIO线程职责:

  • 作为NIO服务端,接收客户端的TCP连接。
  • 作为NIO客户端,向服务器发起TCP连接。
  • 读取对方的消息。
  • 向对方发送消息。 

由于Reactor模式使用的是异步非阻塞I/O,所有的I/O操作都不会导致阻塞,理论上一个线程可以独立处理所有的I/O操作。

通过Acceptor接收客户端的TCP连接请求消息,当链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上,进行消息解码。用户线程消息编码后通过NIO线程将消息发送给客户端。

 

2) 缺点

在小容量场景下,可以使用单线程模型,但是对一个高负载、大并发的应用会存在问题。

一个NIO线程同时处理成百上千的链路,性能存在瓶颈,无法满足海量消息的编码、解码、读取、发送。

当NIO线程负载过高以后,处理速度会降低,这会导致大量客户端连接超时,而发生超时以后往往需要进行重发,这又会加重NIO线程的负载,导致大量消息积压。

一旦NIO线程出现问题或者进入死循环,将导致不能接受和处理外部消息,整个通讯模块不可用,即出现节点故障。

 

 

2. Ractor多线程模型

1) 线程模型

6566f992dea48037385026d470851e8daea05eb4

Acceptor线程(一个专门的NIO线程)监听服务端、客户端的TCP连接。

读写操作由NIO线程池负责,负责消息的读取,编码、解码、发送。

一个NIO线程对应N个链路,但是一个链路只对应一个NIO线程。

 

2) 缺点

并发百万级的客户端连接,或者服务端需要对客户端握手进行安全认证(安全认证本身非常消耗性能),这种场景下,一个Acceptor线程可能存在性能不足的问题。

 

3. 主从Reactor多线程模型

1) 线程模型

01d6b54d837d24393cab0d15cc76014833978e25

服务端通过独立的NIO线程池接收客户端连接。Acceptor接收到客户端TCP连接请求并处理完成后(例如完成耗时的认证操作),将新创建的SocketChannel注册到I/O线程池(sub Reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编码工作。

Acceptor线程池仅仅用于客户端的登录、握手、安全认证,一旦链路建立成功,就将链路注册到sub Reactor线程池的I/O线程上,后续操作交给此I/O线程。

 

 

4. Netty线程模型

1) 线程模型

Netty的线程模型不是一成不变,它取决于用户启动参数配置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型、主从线程模型。Netty推荐使用主从Reactor线程池模式。

 

097fe2fdf51af77fe99e52360baf4c95ded7542b

 

2) 原理

服务端启动的时候创建的两个NioEventLoopGroup,就是连个独立的Reactor线程池。

a) Accetpor线程池职责

接收客户端TCP连接,初始化Channel参数。

将链路状态变更的事件通知给ChannelPipeline。

b) NIO处理IO操作的线程池职责

异步读取数据报,发送读事件到ChannelPipeline。

异步发送消息,调用ChannelPipeline的消息发送接口。

执行系统调用Task

调用定时任务Task,例如链路空闲状态监测定时任务。

c) 示例代码:
    // 配置服务端的NIO线程组
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(new ChildChannelHandler());
        // 绑定端口,同步等待成功
        ChannelFuture f = b.bind(port).sync();
 
        // 等待服务端监听端口关闭
        f.channel().closeFuture().sync();
    } finally {
        // 优雅退出,释放线程池资源
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }


二 源码分析

1. NioEventLoop

1) run

无限循环,直到收到退出指令。

首先将wakeUp设置成false,同时将之前的值存储在oldWakeUp中。

通过hasTask判断当前消息队列中是否有消息尚未处理。

如果有待处理消息,则调用selectNow方法立即进行一次select操作,此时如果有准备就绪的Channel,则返回就绪Channel的集合,否则返回0。选择完成后,再次判断用户是否调用了Selector的wakeup方法,如果调用过,则执行selector.wakeup操作。

如果没有待处理消息,则执行select方法,由Selector多路复用器无限轮询,看是否有就绪的Channel。当出现以下几种情况时,退出无限轮询。

Channel处于就绪状态,selectedKeys不为0,说明读写事件需要处理。

oldWakeUp改false

系统或这用户调用了wakeup操作,唤醒当前的多路复用器。

消息队列中有新任务需要处理。

如果本次Selector轮询结果为空,也没有wakeup操作或者是新的消息需要处理,则说明是个空轮询。(示例代码中rebuildSelector相关逻辑主要是解决JDK bug的。)


    protected void run() {
        for (;;) {
            oldWakenUp = wakenUp.getAndSet(false);
            try {
                if (hasTasks()) {
                    selectNow();
                } else {
                    select();
                    if (wakenUp.get()) {
                        selector.wakeup();
                    }
                }
                cancelledKeys = 0;
                final long ioStartTime = System.nanoTime();
                needsToSelectAgain = false;
                if (selectedKeys != null) {
                    processSelectedKeysOptimized(selectedKeys.flip());
                } else {
                    processSelectedKeysPlain(selector.selectedKeys());
                }
                final long ioTime = System.nanoTime() - ioStartTime;
 
                final int ioRatio = this.ioRatio;
                runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
 
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        break;
                    }
                }
            } catch (Throwable t) {
                logger.warn("Unexpected exception in the selector loop.", t);
 
                // Prevent possible consecutive immediate failures that lead to
                // excessive CPU consumption.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // Ignore.
                }
            }
        }
    }
 
    void selectNow() throws IOException {
        try {
            selector.selectNow();
        } finally {
            // restore wakup state if needed
            if (wakenUp.get()) {
                selector.wakeup();
            }
        }
    }
 
    private void select() throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
            for (;;) {
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                if (timeoutMillis <= 0) {
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }
 
                int selectedKeys = selector.select(timeoutMillis);
                selectCnt ++;
 
                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks()) {
                    // Selected something,
                    // waken up by user, or
                    // the task queue has a pending task.
                    break;
                }
 
                if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    // The selector returned prematurely many times in a row.
                    // Rebuild the selector to work around the problem.
                    logger.warn(
                            "Selector.select() returned prematurely {} times in a row; rebuilding selector.",
                            selectCnt);
 
                    rebuildSelector();
                    selector = this.selector;
 
                    // Select again to populate selectedKeys.
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }
 
                currentTimeNanos = System.nanoTime();
            }
 
            if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Selector.select() returned prematurely {} times in a row.", selectCnt - 1);
                }
            }
        } catch (CancelledKeyException e) {
            if (logger.isDebugEnabled()) {
                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector - JDK bug?", e);
            }
            // Harmless exception - log anyway
        }
    }

2) processSelectedKey

首先判断SelectionKey是否可用,如果不可用通过unsafe.close方法释放资源。

然后根据网络操作位readyOps来判断那种事件(read、write、connnect),根据事件执行相应的网络操作。

    private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final NioUnsafe unsafe = ch.unsafe();
        if (!k.isValid()) {
            // close the channel if the key is not valid anymore
            unsafe.close(unsafe.voidPromise());
            return;
        }
 
        try {
            int readyOps = k.readyOps();
            // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
            // to a spin loop
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
                if (!ch.isOpen()) {
                    // Connection already closed - no need to handle write.
                    return;
                }
            }
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
                ch.unsafe().forceFlush();
            }
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
                // See https://github.com/netty/netty/issues/924
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);
 
                unsafe.finishConnect();
            }
        } catch (CancelledKeyException e) {
            unsafe.close(unsafe.voidPromise());
        }
    }

三 最佳实践

1. 建议

创建两个NioEventLoopGroup,用户逻辑隔离NIO Acceptor和NIO I/O线程。

尽量不要在ChannelHandler中启动用户线程(编码后将POJO消息发送到通信对端的线程除外)。

解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息解码。

如果没有复杂的逻辑计算,没有可能导致线程被阻塞的磁盘操作、DB操作、网络操作等,可以直接在NIO线程上完成业务逻辑,不用切换到用户线程。

如果业务逻辑复杂,不要再NIO线程中完成,以保证NIO线程尽快被释放,处理其他的I/O操作。

 

2. 推荐的线程数计算公式

线程数 = (线程总时间 / 瓶颈资源时间)x 瓶颈资源的线程并行数

QPS = 1000 / 线程总时间 x 线程数

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值