小菜鸡的探索:netty的I/O模式

167 阅读8分钟

1. 啥是I/O模式?

对于I/O模式,以下馆子做一个类比。下馆子时,我们可能会遇到3种模式:

  • 类似于学校饭堂,需要排队,排队点餐结账,打好饭后,找位置干饭;
  • 类似于美食广场,点好餐后,餐馆开始做菜(此时你可以做其他事情,没必要等着),当菜做好了,会喊你的号,你要过去店门口,拿好自己的餐,才找位置干饭;
  • 类似于餐厅包厢,点菜后,此时你可以继续做其他事情,当菜做好后,菜会直接端上桌;

上面的下馆子的类比关系:

  • 馆子类比提供服务的服务器
  • 饭菜类比数据
  • 饭菜可以吃了类比数据就绪
  • 送饭菜类比数据的读取
  • 客人类比等待数据的线程

经典的I/O模式有3种,BIO(阻塞IO)、NIO(非阻塞IO)、AIO(异步IO)。

  • BIO:under JDK 1.4 类似于饭堂打饭的模式,阻塞、同步。当数据未就绪(菜没做好),线程发生阻塞,直到数据就绪为止。数据就绪后,唤醒当前线程(同步)读取数据(自己端菜);
  • NIO:JDK 1.4 java.nio 类似于美食广场的模式,非阻塞、同步。当数据未就绪时(菜没做好),线程不会发生阻塞,可以执行其他任务。数据就绪后,当前线程(同步)进行数据读取(自己端菜);
  • AIO:JDK 1.7 类似于餐厅包厢,非阻塞、异步。当数据未就绪时(菜没做好),线程不会发生阻塞,可以执行其他任务。数据就绪后,由程序的其他线程(异步)进行数据读取(服务员端菜);

2. netty 对IO模式的支持

BIO(OIO deprecate)NIO(common)NIO(linux)NIO(macOS)AIO(deprecate)
ThreadPerChannelEventLoopGroupNioEventLoopGroupEpollEventLoopGroupKQueueEventLoopGroupAioEventLoopGroup
ThreadPerChannelEventLoopNioEventLoopEpollEventLoopKQueueEventLoopAioEventLoop
OioServerSocketChannelNioServerSocketChannelEpollServerSocketChannelKQueueServerSocketChannelAioServerSocketChannel
OioSocketChannelNioSocketChannelEpollSocketChannelKQueueSocketChannelAioSocketChannel

2.1. 为啥BIO不建议使用(deprecate)?

假若连接数在较高时,太多线程处于阻塞状态,线程阻塞会被认为是一种浪费CPU资源的方式,线程太多时,CPU上下文切换成本较高,效率低下。 但是BIO也不是一无是处,假若连接数不高,并发度不高时,BIO不输NIO。

2.2. 为啥会删掉已经做好的AIO?

AIO在windows实现较为成熟,但是在linux实现不够成熟,且linux又常用作于服务器。

linux下,AIO相比NIO的性能提高并不理想。

但是,最近5.0版本又重新开始更新了,5.0版本和4.1版本同时更新,4.1必要的特性会合并到5.0版本。

image.png

image.png

3. netty对Reactor模式的支持

netty 的NIO线程模式是基于Reactor开发模式实现的,核心流程:

  • 注册感兴趣的事件
  • 扫描是否有感兴趣的事件发生
  • 事件发生后做出对应的处理

channel感兴趣的事件:

typechannel typeOP_ACCEPTOP_CONNECTOP_WRITEOP_READ
clientSocketChannel
serverServerSocketChannel
serverSocketChannel

3.1. Reactor的3种版本

  1. Reactor 单线程模式

image.png

Reactor 单线程模式下,以下公共皆在同一个线程下执行:

  • 接入连接(ServerSocketChannel接受连接SocketChannel)
  • 接受请求(SocketChannel#read)
  • 请求解码(decode)
  • 请求处理(handle)
  • 处理结果编码(encode)
  • 请求结果写回(SocketChannel#write)
  • 断开连接(SocketChannel#close)

就好比,刚起步的餐馆,一人身兼多职:

  • 迎客(接入连接)
  • 点菜(接受请求)
  • 做菜(请求处理)
  • 上菜(请求结果写回)
  • 送客(断开连接)
  1. Reactor 多线程模式

image.png

本模式下是Reactor 单线程模式的多线程版本。

就好比,餐馆业务繁忙后,自己一个忙不过来,需要请多几个人帮忙,每个人的工作内容都是一样的

  • 迎客(接入连接)
  • 点菜(接受请求)
  • 做菜(请求处理)
  • 上菜(请求结果写回)
  • 送客(断开连接)
  1. Reactor 主从模式

image.png

Reactor 主从模式,拆分成俩部分:

  • 主模式(boss线程池)
    • 接入连接(ServerSocketChannel接受连接SocketChannel)
  • 从模式(worker线程池)
    • 接受请求(SocketChannel#read)
    • 请求解码(decode)
    • 请求处理(handle)
    • 处理结果编码(encode)
    • 请求结果写回(SocketChannel#write)

就好比,餐馆为了提升服务品质,需要专业的人才服务对应的岗位:

  • 迎客(接入连接)需要形象更好的人
  • 点菜(接受请求)需要记性比较好的、推荐菜品、口才、情商比较好的人
  • 做菜(请求处理)需要大厨师炒上几手好菜
  • 上菜(请求结果写回)需要细心的人
  • 送客(断开连接)需要形象更好的人

3.2. netty是怎么使用这3种Reactor模式的呢?

Reactor 单线程模式:

public class SingleThreadReactor {

    public static void main(String[] args) throws InterruptedException {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.option(NioChannelOption.SO_BACKLOG, 1024);
        serverBootstrap.childOption(NioChannelOption.TCP_NODELAY, true);

        // 单线程
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        try {
            // 注册使用的线程池
            serverBootstrap.group(bossGroup);
            serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();

                    pipeline.addLast("frameDecoder", new OrderFrameDecoder());
                    pipeline.addLast("frameEncoder", new OrderFrameEncoder());

                    pipeline.addLast("protocolDecoder", new OrderProtocolDecoder());
                    pipeline.addLast("protocolEncoder", new OrderProtocolEncoder());

                    pipeline.addLast(new OrderServerProcessHandler());
                }
            });

            ChannelFuture channelFuture = serverBootstrap.bind(8090).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
        }
    }
}

Reactor 多线程模式:

public class MultiThreadReactor {

    public static void main(String[] args) throws InterruptedException {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.option(NioChannelOption.SO_BACKLOG, 1024);
        serverBootstrap.childOption(NioChannelOption.TCP_NODELAY, true);

        // 多线程
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(16);
        try {
            // 注册使用的线程池
            serverBootstrap.group(bossGroup);
            serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();

                    pipeline.addLast("frameDecoder", new OrderFrameDecoder());
                    pipeline.addLast("frameEncoder", new OrderFrameEncoder());

                    pipeline.addLast("protocolDecoder", new OrderProtocolDecoder());
                    pipeline.addLast("protocolEncoder", new OrderProtocolEncoder());

                    pipeline.addLast(new OrderServerProcessHandler());
                }
            });

            ChannelFuture channelFuture = serverBootstrap.bind(8090).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
        }
    }
}

Reactor 主从模式:

public class MasterSlaveReactor {

    public static void main(String[] args) throws InterruptedException {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.option(NioChannelOption.SO_BACKLOG, 1024);
        serverBootstrap.childOption(NioChannelOption.TCP_NODELAY, true);

        // 主线程池
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 从线程池
        NioEventLoopGroup workerGroup = new NioEventLoopGroup(16);
        try {
            // 注册使用的线程池
            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();

                    pipeline.addLast("frameDecoder", new OrderFrameDecoder());
                    pipeline.addLast("frameEncoder", new OrderFrameEncoder());

                    pipeline.addLast("protocolDecoder", new OrderProtocolDecoder());
                    pipeline.addLast("protocolEncoder", new OrderProtocolEncoder());

                    pipeline.addLast(new OrderServerProcessHandler());
                }
            });

            ChannelFuture channelFuture = serverBootstrap.bind(8090).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

4. netty对Reactor支持的灵魂N问

4.1. 为什么主reactor大多数情况下,只会用到一个线程,并不需要一个线程池?

主reactor的主要任务是接入连接(ServerSocketChannel#accept)。

一般情况下,我们去构建一个服务,只会提供一个端口port,其他网络服务需要与本服务通信,连上此端口即可。

EventLoop可以理解为包含Selector的事件循环处理器(包含Selector的单线程线程池)。

ServerSocketChannel要想接入连接,需要注册到单个Selector即可,同时需要一个执行线程,这俩个条件单个EventLoop即可满足,因此ServerSocketChannel绑定EventLoop,注册到EventLoop内的Selector,监听OP_ACCEPT即可。

io.netty.channel.AbstractChannel.AbstractUnsafe#register

@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    // 省略部分代码...
    // 绑定EventLoop
    AbstractChannel.this.eventLoop = eventLoop;

    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            safeSetFailure(promise, t);
        }
    }
}

因此,boss线程组只需要构建一个EventLoopGroup即可。

NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 省略部分代码...

serverBootstrap.group(bossGroup, workerGroup);

当然,特殊情况下,也是允许绑定多个端口的。如果不同端口获取的数据格式一致,其实没必要绑定多个端口;如果不同端口获取的数据格式不一致,在从reactor中,对不同类型的Message,分发到不同的Handler处理,逻辑上不太清晰,也不建议这么做。

4.2. Netty是如何为channel分配NioEventLoop(即处理channel编码、解码、处理等等业务的线程)的?

ServerSocketChannel接入连接SocketChannel后,SocketChannel在ServerSocketChannel的pipeline中传播,当传播到ServerBootstrapAcceptor时,SocketChannel会进行一系列的初始化,包括option、attribute、handler的绑定、EventLoop的绑定

io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead

@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;
    // 绑定SocketChannel的handler
    child.pipeline().addLast(childHandler);
    // 设置SocketChannel的Option,如TCP_NODELAY
    setChannelOptions(child, childOptions, logger);
    // 设置SocketChannel的属性,辅助作用
    setAttributes(child, childAttrs);

    try {
        // 从Reactor中选择EventLoop,绑定
        // 注册到EventLoop的Selector
        // 绑定后,监听OP_READ事件
        childGroup.register(child).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    forceClose(child, future.cause());
                }
            }
        });
    } catch (Throwable t) {
        forceClose(child, t);
    }
}

怎么还没聊到分配NioEventLoop?来啦来啦

EventLoopGroup在注册SocketChannel时,会进行EventLoop的分配,具体分配代码,我们定位到这里:

io.netty.channel.MultithreadEventLoopGroup#register(io.netty.channel.Channel)

@Override
public ChannelFuture register(Channel channel) {
    return next().register(channel);
}

next方法的内部实现,可定位到这里:

io.netty.util.concurrent.DefaultEventExecutorChooserFactory

public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory {

    public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory();

    private DefaultEventExecutorChooserFactory() { }

    @SuppressWarnings("unchecked")
    @Override
    public EventExecutorChooser newChooser(EventExecutor[] executors) {
        //依据executor的大小(EventLoopGroup的构造器参数可设置大小)是否为2^n,选择不同的EventLoop分配器
        if (isPowerOfTwo(executors.length)) {
            return new PowerOfTwoEventExecutorChooser(executors);
        } else {
            return new GenericEventExecutorChooser(executors);
        }
    }

    private static boolean isPowerOfTwo(int val) {
        return (val & -val) == val;
    }

    private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        //executors总数是2^n(2,4,8...)才会用, &运算效率更高, 同时当idx累加成最大值之后,更公平。为啥?可看下GenericEventExecutorChooser的实现
        public EventExecutor next() {
            return executors[idx.getAndIncrement() & executors.length - 1];
        }
    }

    private static final class GenericEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        GenericEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        public EventExecutor next() {
            // 递增、取模,取正值,不然可能是负数
            // 存在缺点:当idx累加成最大值后,有短暂的不公平:
            // 假如executors总数为7
            // 1, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6, 5,...
            // 当idx达到最大值时为1,继续递增时,就变成2,1,公平性会短暂性缺失
            return executors[Math.abs(idx.getAndIncrement() % executors.length)];
        }
    }
}

4.3. netty是怎么实现多路复用器selector的跨平台?

io.netty.channel.nio.NioEventLoopGroup#NioEventLoopGroup(int, java.util.concurrent.ThreadFactory)

public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory) {
    this(nThreads, threadFactory, SelectorProvider.provider());
}

SelectorProvider是rt.jar包下的,会依据不同环境下的jdk,选择对应的Selector。

windows环境下:

    package sun.nio.ch; 
   import java.nio.channels.spi.SelectorProvider; 
     /** 
     * Creates this platform's default SelectorProvider 
     */ 
       public class DefaultSelectorProvider { 
       /** 
         * Prevent instantiation. 
         */ 
     private DefaultSelectorProvider() { } 
       /** 
         * Returns the default SelectorProvider. 
         */ 
     public static SelectorProvider create() { 
         return new sun.nio.ch.WindowsSelectorProvider(); 
     } 
   } 

macOS环境下:

    package sun.nio.ch; 
   import java.nio.channels.spi.SelectorProvider; 
   /** 
     * Creates this platform's default SelectorProvider 
     */ 
       public class DefaultSelectorProvider { 
       /** 
     * Prevent instantiation. 
     */ 
     private DefaultSelectorProvider() { } 
       /** 
     * Returns the default SelectorProvider. 
     */ 
     public static SelectorProvider create() { 
     return new sun.nio.ch.KQueueSelectorProvider(); 
     } 
   } 


linux环境下:

    package sun.nio.ch; 
       import java.nio.channels.spi.SelectorProvider; 
     import java.security.AccessController; 
     import sun.security.action.GetPropertyAction; 
     
   /** 
     * Creates this platform's default SelectorProvider 
     */ 
   public class DefaultSelectorProvider { 
       /** 
     * Prevent instantiation. 
     */ 
     private DefaultSelectorProvider() { } 
 
       @SuppressWarnings("unchecked") 
     private static SelectorProvider createProvider(String cn) { 
         Class<SelectorProvider> c; 
         try { 
             c = (Class<SelectorProvider>)Class.forName(cn); 
         } catch (ClassNotFoundException x) { 
             throw new AssertionError(x); 
         } 
         try { 
             return c.newInstance(); 
         } catch (IllegalAccessException | InstantiationException x) { 
             throw new AssertionError(x); 
         } 
       } 
   
     /** 
     * Returns the default SelectorProvider. 
     */ 
     public static SelectorProvider create() { 
     String osname = AccessController.doPrivileged(new GetPropertyAction("os.name")); 
     if (osname.equals("SunOS")) 
         return createProvider("sun.nio.ch.DevPollSelectorProvider"); 
     if (osname.equals("Linux")) 
         return createProvider("sun.nio.ch.EPollSelectorProvider"); 
     return new sun.nio.ch.PollSelectorProvider(); 
     } 
   }