1. 百万级并发场景

实际场景下,百万级并发请求是较多的,比如电商的促销、12306抢票、健康码查询等等,这些场景要求应用服务稳定,能最大化资源利用。所以实际应用程序设计时,会对QPS并发做预估处理,通过QPS选择合适的设计实现和对应的服务器资源数。一般会做如下要求:

  • 选择占用尽可能少的服务器资源,减少成本投入;
  • 服务器数量少,运维和维护难度降低,减少人力成本投入;
  • 优秀的应用设计会降低实现复杂度,提升可用性。        

2. 为什么选择netty做高并发

面临高并发场景诉求,解决办法有三个主题:

  • I/O传输模型:用什么样的通道将数据发送给对方,是BIO、NIO还是AIO,I/O传输模型在很大程度上决定了框架的性能。
  • 数据协议:采用什么样的通信协议,协议的选择不同,性能模型也就不同。
  • 线程模型:线程模型涉及读取数据包,读包之后的编解码,编解码后消息如何派发等,线程模型设计得不同,对性能也会产生非常大的影响。

2.1 IO传输模型

IO在计算机中指Input/Output,也就是输入和输出。计算机运行时数据是在内存中驻留,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。

LINUX中进程无法直接操作I/O设备,必须通过系统调用请求kernel来协助完成I/O动作。内核会为每个I/O设备维护一个缓冲区。IO输入时,应用进程请求内核,内核会先看缓冲区中有没有相应的缓存数据,有数据则直接复制到进程空间,没有的话再到设备中读取。通常用户进程中的一个完整IO分为两阶段:用户进程空间<-->内核空间、内核空间<- ->设备空间。

由于CPU和内存的速度远远高于外设的速度,所以在IO编程中,就存在速度严重不匹配的问题,所以有了同步/异步,阻塞和非阻塞IO之分。

IO模型分为:BIO、NIO、IO多路复用、信号驱动IO和AIO。

  • BIO:进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程,操作成功则进程获取到数据。
  • NIO:非阻塞IO模型在内核数据没准备好,需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞;
  • IO多路复用:多个的进程的IO可以注册到一个复用器(selector)上,然后用一个进程调用该select,,select会监听所有注册进来的IO。
  • 信号驱动IO:当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。
  • AIO:当进程发起一个IO操作,进程返回(不阻塞),但也不能返回结果。内核把整个IO处理完后,会通知进程结果,如果IO操作成功则进程直接获取到数据。

异步IO是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待,所以只有AIon才是真正的一步IO。

2.2 线程模型

先了解一下Reactor模式,

在Reactor模式中,有5种角色:

  • Initiation Dispatcher: 初始分发器,一旦事件被触发后,Initiation Dispatcher首先会分离出每一个事件,然后调用事件处理器,最后调用相关的回调方法来处理这些事件。
  • Synchronous Event Demultiplexer: 同步事件分发器,调用方在调用它的时候会被阻塞,一直阻塞到同步事件分离器上有事件产生为止。对于Linux来说,同步事件分离器指的就是常用的I/O多路复用机制,比如说select、poll、epoll等。
  • Event Handler:事件处理器,本身由多个回调方法构成,这些回调方法构成了与应用相关的对于某个事件的反馈机制。
  • Concrete Event Handler:具体事件处理器,是事件处理器的实现。它本身实现了事件处理器所提供的各种回调方法,从而实现了特定于业务的逻辑。
  • Handle:句柄,本质上表示一种资源(比如说文件描述符,或是针对网络编程中的socket描述符),是由操作系统提供的;该资源用于表示一个个的事件,事件既可以来自于外部,也可以来自于内部;外部事件比如说客户端的连接请求,客户端发送过来的数据等;内部事件比如说操作系统产生的定时事件等。它本质上就是一个文件描述符,Handle是事件产生的发源地。

Reactor 单线程模型:

优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
缺点:性能问题,单线程无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈 。

多线程Reactor

优点:可以充分的利用多核cpu 的处理能力
缺点:多线程数据共享和访问比较复杂,但是reactor处理所有的事件的监听和响应,在单线程运行, 在高并发场景容易出现性能瓶颈。

主从Reactor

优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
缺点:编程复杂度较高

2.3 零拷贝

  • Netty接收和发送ByteBuffer采用DirectBuffer,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。
  • Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便地对组合Buffer进行操作,避免了传统的通过内存拷贝的方式将几个小Buffer合并成一个大Buffer的烦琐操作。
  • Netty中文件传输采用了transferTo()方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write()方式导致的内存拷贝问题。

2.4 内存池

        对于缓冲区来说,尤其是对于堆外直接内存的分配和回收,是一种耗时的操作。为了尽量重复利用缓冲区内存,Netty设计了一套基于内存池的缓冲区重用机制。

2.5 无锁化的串行设计理念

        大多数应用场景下,并行多线程处理可以提升系统的并发性能。但如果共享资源竞争激烈,就会造成严重的锁竞争,导致系统性能的下降。为了尽可能避免锁竞争带来的性能损耗,可以通过串行化设计来避免多线程竞争和同步锁,即消息的处理尽可能在同一个线程内完成,不进行线程切换。

2.6 高效的并发编程

Netty的高效并发编程主要体现在如下几点:

  1. volatile关键字的大量且正确的使用。

  2. CAS和原子类的广泛使用。

  3. 线程安全容器的使用。

  4. 灵活的TCP参数配置能力。

2.7 对高性能的序列化框架的支持

影响序列化性能的关键因素有:序列化后的码流大小(网络带宽的占用);序列化/反序列化的性能(CPU资源占用)和是否支持跨语言。Netty默认提供了对Google Protobuf的支持,用户也可以通过扩展Netty的编解码接口接入其他高性能的序列化框架进行编解码,例如Thrift的压缩二进制编解码框架。

3. netty的使用姿势

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
     ServerBootstrap bootstrap = new ServerBootstrap();
     bootstrap.option(ChannelOption.SO_REUSEADDR, true).option(ChannelOption.SO_BACKLOG, 1024)
    .option(ChannelOption.SO_RCVBUF, 1024 * 128)
    .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

    bootstrap.childOption(ChannelOption.TCP_NODELAY, true)
             .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
             .childOption(ChannelOption.SO_KEEPALIVE, true);

    bootstrap.group(bossGroup,workerGroup) 
             .channel(NioServerSocketChannel.class) 
             .localAddress(new InetSocketAddress(port)) 
             .childHandler(new ChannelInitializer<SocketChannel>() { 
                        EventExecutorGroup logicGroup = new DefaultEventExecutorGroup(16);
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(logicGroup,new EchoServerWithExecutorHandler());
                        }
                    });

            ChannelFuture f = bootstrap.bind().sync(); 
            System.out.println(App.class.getName() + " started and listen on " + f.channel().localAddress());
            f.channel().closeFuture().sync(); 
        } finally {
            bossGroup.shutdownGracefully().sync(); 
            workerGroup.shutdownGracefully().sync();
        }

参考:Netty(二) 从线程模型的角度看 Netty 为什么是高性能的?-阿里云开发者社区

Netty 为什么有如此高的性能? - 墨天轮

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐