Netty设计特点:
- I/O等待:在多任务系统中,进程主动发起I/O请求,但I/O设备还没有准备好,所以会发生I/O阻塞,进程进入Wait状态。
- 时间片耗尽:在多任务分时系统中,内核分配给进程的时间片已经耗尽了,进程进入Ready状态,等待内核重新分配时间片后的执行机会。
- 硬件中断:在抢占式的多任务分时系统中,I/O设备可以在任意时刻发生中断,CPU会停下当前正在执行的进程去处理中断,因此进程进入Ready状态。
netty优化方向:
- 线程数控制:高并发下如果线程较多时,Context Switch会非常明显,超过CPU核心数的线程不会带来任何好处。不是特别耗时的操作的话,业务线程池也是有害无益的。Netty 5为我们提供了指定底层线程池的机会,这样能更好的控制整个中间件的线程数和调度策略。
- 非阻塞I/O操作:要想线程少还多做事,避免阻塞是一定要做的。
- 减少系统调用:虽然Mode Switch比Context Switch的开销要小得多,但我们还是要尽量减少频繁的syscall。
- 数据零拷贝:从内核空间的Direct Buffer拷贝到用户空间,每次透传都拷贝的话累积起来是个不小的开销。
- 共享状态保护:中间件内部的并发处理也是决定性能的关键。
private void doStartNettyServer(int port) throws InterruptedException { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(4); try { ServerBootstrap b = new ServerBootstrap() .group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .localAddress(port) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(...); } }); // Bind and start to accept incoming connections. ChannelFuture f = b.bind(port).sync(); // Wait until the server socket is closed. f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }
透传不需要完整解析消息,只需要知道消息要转发给下游哪个系统就足够了。所以透传时,我们可以只解析出部分消息,消息整体还原封不动地放在Direct Buffer里,最后直接将它写入到连接下游系统的Channel中。所以应用层的Zero Copy实现就分为两部分:Direct Buffer配置和Buffer的零拷贝传递。
ServerBootstrap b = new ServerBootstrap() .group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .localAddress(port) .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(...); } });
Netty是用引用计数的方式来判断是否回收的,所以要想继续使用ByteBuf而不让Netty释放的话,就要增加它的引用计数。只要我们在ChannelPipeline中的任意一个Handler中调用ByteBuf.retain()将引用计数加1,Netty就不会释放掉它了。我们在连接下游的客户端的Encoder中发送消息成功后再释放掉,这样就达到了零拷贝透传的效果
public class RespEncoder extends MessageToByteEncoder<Resp> { @Override protected void encode(ChannelHandlerContext ctx, Msg msg, ByteBuf out) throws Exception { // Raw in Msg is retained ByteBuf out.writeBytes(msg.getRaw(), 0, msg.getRaw().readerIndex()); msg.getRaw().release(); } }
http://laolinshi.iteye.com/blog/2341729
netty在服务器初始化的时候需要设置两个线程池,一个是用来接收客户端的连接(1),另一个是用来处理客户端的读写(一般设置成CPU核数的两倍到三倍)
为了方便系统BUG 的解决,可以为每个线程池设置一个自定义的ThreadFactory,这个factory的作用是根据线程池的类型为创建的线程设置一个特殊的名称,如boss线程的名称是NettyBoss_,工作线程的名称是NettyServerSelector_%d_%d,这样可以在以后的问题排除过程中利用这个名称识别不同的线程,分析这些线程的资源占用率,便于找到问题的根源。
Linux, 在选中worker线程池的时候可以考虑用epoll代替传统的select,前者对应netty提供的EpollEventLoopGroup,后者对应的是NioEventLoopGroup。之所以要这样做,是因为传统的select基于轮询的方式来进行事件处理,随着fd数量的增加,导致轮询的处理开销增大,实际的事件处理效率就会下降。而epoll是基于通知的方式,每当事件准备就绪的时候,epoll就会通知线程进行处理,这样可以保证及时性的同时也不会随着fd的数量的增加而降低效率。
SO_BACKLOG,这个值通常设置1024,意味着当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度。这样就可以服务器处理能力达到饱和的情况下用队列来暂存用户取得请求,等服务器空闲的时候再从队列中取出请求来处理,而不是马上拒接掉。
SO_SNDBUF和SO_RCVBUF,两者可以设置成1048576,即是128K,在性能优化是通常都设置成这样
内存池,需要内存时就从内存池中分配,使用完了再归还给内存池以便可以重复利用。这样就可以避免每次使用内存所带来的内存创建和销毁开销,而且还不容易形成内存碎片。基于这样的目的可以使用netty提供的PooledByteBufAllocator分配器,需要注意的是使用这个分配器分配的内存使用完后必须手动进行释放,否则会造成内存泄漏
WRITE_BUFFER_WATER_MARK,他的作用是限制netty向channel写数据时使用的缓冲区边界。当netty需要往channel中写入数据时会先把数据写入一个Buffer缓冲区,这个缓冲区是各个channel独占的,不共享。等到channel空闲的时候就从缓冲区中读取数据进行发送,当Buffer的数据超过高水位线时就停止写入数据,设置channel的isWritable为false。等到buffer中的数据由于被消费而低于低水位线时设置channel的isWritable为true,又可以重新接受写入的数据。所以设置了这个参数之后,对应用的要求是,每次写数据时先判断channel的isWritable,在 true时才进行写入。
考虑到具体的业务逻辑处理可能涉及到耗时的数据库操作或远程RPC调用,因而在具体的业务处理Handler中启动一个新的线程来处理相关逻辑,如上图的NettyServerHandler。这样可以让这些耗时的操作不阻塞处理Handler的线程,让线程可以及时返回来处理新的操作,同样可以起到提高服务器并发处理能力的目的
class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand> { @Override protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception { Runnable run = new Runnable() { @Override public void run() { //具体的业务逻辑处理 } } submit(run);//在线程池中提交处理任务 } }