Netty学习笔记05-EventLoop和线程模型

Netty权威指南学习笔记
Netty实战
Scalable IO in Java

java多线程概述

大多数的现代应用程序都利用了复杂的多线程处理技术以有效地利用系统资源。
在早期的 Java 语言中,我们使用多线程处理的主要方式无非是按需创建和启动新的 Thread 来执行并发的任务单元——一种在高负载下工作得很差的原始方式。 Java 5 随后引入了 Executor API,其线程池通过缓存和重用Thread 极大地提高了性能。
java基本的线程池化模式可以描述为:

  • 从池的空闲线程列表中选择一个 Thread, 并且指派它去运行一个已提交的任务(一个
    Runnable 的实现);
  • 当任务完成时, 将该 Thread 返回给该列表, 使其可被重用。
public static void test01() {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), 20, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(100));
        IntStream.range(0, 100).forEach(i -> {
            pool.execute(() -> {
                System.out.println("start-->" + i);

                try {
                    Thread.sleep(11);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("end-->" + i);
            });
        });

        pool.shutdown();
    }

Netty学习笔记05-EventLoop和线程模型_第1张图片

Netty线程模型

学习Netty线程模型时,首先会想到Reactor线程模型。

Reactor单线程模型
Reactor单线程模型,是指所有I/O操作都在同一个I/O线程上完成的。I/O线程职责如下:

  • 作为NIO服务器端,接收客户端的TCP连接;
  • 作为NIO客户端,向服务端发起TCP连接请求
  • 读取通信对端的请求或者应答消息
  • 向通信对端发送请求或者应答消息
    Reactor单线程模型如下图:
    Netty学习笔记05-EventLoop和线程模型_第2张图片
    由于Reactor模式使用的异步非阻塞操作,所有的I/O操作都不会导致阻塞,理论上,一个线程可以处理所有I/O相关操作。从架构层看,一个NIO线程确实可以完成其承担的职责。
    在一些小容量应用场景下,可以使用单线程模型。但是对于高负载,大并发的应用场景却不合适。

Reactor多线程模型
Netty学习笔记05-EventLoop和线程模型_第3张图片
Reactor多线程模型的特点:

  • 有一个专门的NIO线程—Acceptor用于监听服务端,接收客户端的TCP连接请求
  • 网络I/O操作–读、写等由一个专门的NIO线程池负责,线程池的线程负责消息的读取,解码,编码
  • 一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题

在绝大多数情况下,Reactor多线程模型 可以满足需求。但是,在个别特殊场景,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户连接。

主从Reactor多线程模型
Netty学习笔记05-EventLoop和线程模型_第4张图片
主从Reactor多线程模型的特点:

 - 服务端用于接收客户端连接请求的不在是一个单独的NIO线程,而是一个独立的线程池
     - Acceptor Pool 仅仅用来处理客户端的登录,握手和安全认证,一旦链路创建成功,就将链路注册到Sub Reactor Thread Pool(I/O Pool)上
 - Sub Reactor Thread Pool(I/O Pool) :负责SocketChannel的读写和编码工作。

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

Netty学习笔记05-EventLoop和线程模型_第5张图片

private void bind(int port) throws InterruptedException {
        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)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                        }
                    });
            //绑定端口,同步等待成功
            ChannelFuture f = b.bind(8080).sync();

            //监听端口关闭
            f.channel().closeFuture().sync();
        } finally {
            //优雅退出,释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();

        }
    }

用于接收客户端请求的线程池(bossGroup)作用:
- 接收客户端TCP连接,初始化Channel参数
- 将链路状态变更事件通知给ChannelPipeline

处理I/O线程池(workerGroup)作用:

  • 异步读取通信对端的数据报,发送读事件到ChannelPipeline
  • 异步发送消息到通信对端,调用ChannelPipeline的消息发送接口
  • 执行系统调用Task;
  • 执行定时任务Task。 例如:链路空闲状态监测定时任务。

最佳实践

  • 创建两个NioEventLoopGroup,隔离NIO Acceptor和NIO的IO线程。
  • 尽量不要在ChannelHandler中启动用户线程(解码之后,将POJO消息派发到后端的业务线程池除外)。
  • 解码要放在NIO线程调用的Handler中,不要切换到用户线程处理。
  • 如果IO操作非常简单,不涉及复杂的业务逻辑计算,没有可能导致阻塞的磁盘操作、数据库操作、网络操作等,可以再NIO线程调用的Handler中完成业务逻辑,不需要切换到用户线程。
  • 如果IO业务操作比较复杂,就不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程处理,以保证NIO线程尽快的被释放,处理其他的IO操作。

推荐的线程数量计算公式:

  1. 公式一:线程数量 = (线程总时间/瓶颈资源时间) * 瓶颈资源的线程并行数
  2. 公式二:QPS=1000/线程总时间*线程数

EventLoop接口

Netty 4 中的 I/O 和事件处理
I/O 操作触发的事件将流经安装了一个或者多个ChannelHandler 的 ChannelPipeline。传播这些事件的方法调用可以随后被 ChannelHandler 所拦截, 并且可以按需地处理事件。事件的性质通常决定了它将被如何处理;它可能将数据从网络栈中传递到你的应用程序中,或者进行逆向操作,或者 执行一些截然不同的操作。
在Netty 4 中(Netty 5亦然), 所有的I/O操作和事件都由已经被分配给了EventLoop的那个Thread来处理。

EventLoop的类图

Netty学习笔记05-EventLoop和线程模型_第6张图片
由类图可以看见,EventLoop 实现了:ExecutorService,ScheduledExecutorService等juc包下接口。

//使用 EventLoop 执行任务
ch.eventLoop().execute(()->{
    //execute sth
});

//使用 EventLoop 调度任务
ch.eventLoop().schedule(() -> {
    //schedule sth
}, 60, TimeUnit.SECONDS);

//使用 EventLoop 周期性调度任务
ScheduledFuture future =    ch.eventLoop().scheduleAtFixedRate(() -> {
    //schedule sth -- cyclical
}, 60, 60, TimeUnit.SECONDS);
//取消任务,防止它再次运行
future.cancel(false);

EventLoop执行逻辑
每个 EventLoop 都有它自已的任务队列,独立于任何其他的 EventLoop。
如果(当前)调用线程正是支撑 EventLoop 的线程, 那么所提交的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当 EventLoop下次处理它的事件时, 它会执行队列中的那些任务/事件。
Netty学习笔记05-EventLoop和线程模型_第7张图片

再次重申:“永远不要将一个长时间运行的任务放入到执行队列中,因为它将阻塞需要在同一线程上执行的任何其他任务。”

EventLoop的线程分配
服务于 Channel 的 I/O 和事件的 EventLoop 包含在 EventLoopGroup 中。根据不同的传输实现, EventLoop 的创建和分配方式也不同。

  • 异步传输
    异步实现使用只有少数 EventLoop(和Threads)共享于 Channel 之间 。这使得可以通过尽可能少量的 Thread 来支撑大量的 Channel,而不是每个 Channel 分配一个 Thread。
    Netty学习笔记05-EventLoop和线程模型_第8张图片

  • 阻塞传输
    用于像 OIO(旧的阻塞 I/O) 这样的其他传输的设计略有不同,这里每一个 Channel 都将被分配给一个 EventLoop(以及它的 Thread)。
    Netty学习笔记05-EventLoop和线程模型_第9张图片


NioEventLoop源码分析

NioEventLoop类图
Netty学习笔记05-EventLoop和线程模型_第10张图片
NioEventLoop源码简析

作为NIO的Reactor线程,NIOEventLoop需要处理网络的I/O读写事件,因此它需要有一个多路复用器对象(Selector)

//多路复用器
Selector selector;
private SelectedSelectionKeySet selectedKeys;

private final SelectorProvider provider;

//开关是否启用优化项
private static final boolean DISABLE_KEYSET_OPTIMIZATION =
            SystemPropertyUtil.getBoolean("io.netty.noKeySetOptimization", false);

 private Selector openSelector() {
        final Selector selector;
        try {
            //通过provider.openSelector()创建并打开多路复用器
            selector = provider.openSelector();
        } catch (IOException e) {
            throw new ChannelException("failed to open a new selector", e);
        }

        if (DISABLE_KEYSET_OPTIMIZATION) {
            //若没有开启优化开关,就立刻返回
            return selector;
        }

        try {
            SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();

            Class selectorImplClass =
                    Class.forName("sun.nio.ch.SelectorImpl", false, ClassLoader.getSystemClassLoader());

            // 判断selector是否为selectorImplClass的超类or吵接口
            if (!selectorImplClass.isAssignableFrom(selector.getClass())) {
                return selector;
            }

            Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
            Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");

            selectedKeysField.setAccessible(true);
            publicSelectedKeysField.setAccessible(true);

            //通过反射将"selectedKeys","publicSelectedKeys"设置为selectedKeySet,将原JDK的selectedKeys替换掉
            selectedKeysField.set(selector, selectedKeySet);
            publicSelectedKeysField.set(selector, selectedKeySet);

            selectedKeys = selectedKeySet;
        } catch (Throwable t) {
            //...略
        }

        return selector;
    }

下面着重看 run方法

private final Queue taskQueue;
private final AtomicBoolean wakenUp = new AtomicBoolean();
private boolean oldWakenUp;

//是否存在任务 
protected boolean hasTasks() {
    assert inEventLoop();
    return !taskQueue.isEmpty();
}

@Override
protected void run() {
    for (;;) {
        //首先要将wakeUp还原为false,并将原先值赋给oldWakeUp
        oldWakenUp = wakenUp.getAndSet(false);
        try {
            //当前消息队列中是否存在未完成的任务
            if (hasTasks()) {
                //调用selector.selectNow();立即执行一次select操作,看是否存在准备就绪的Channel需要处理
                selectNow();
            } else {
                //selector轮询,看是否有准备就绪的Channel
                select();
                if (wakenUp.get()) {
                    selector.wakeup();
                }
            }
         }
    }

    //.....

    //执行所有task
     runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
 }



    private void select() throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            //下一个将要触发定时任务的执行时间
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
            for (;;) {
                //到任务超时时刻的剩余时间, +0.5毫秒调整值
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                if (timeoutMillis <= 0) {
                    if (selectCnt == 0) {
                        //如果超时,则立即执行,退出当前循环
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }
                //将到任务超时时刻的剩余时间作为参数,执行select();
                int selectedKeys = selector.select(timeoutMillis);
                selectCnt ++;

                //存在以下任一条件,退出循环
                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks()) {
                    break;
                }
//...............略.....................
            }
        }

    }

你可能感兴趣的:(#,netty,网络)