vlambda博客
学习文章列表

换个角度聊聊Netty

优质文章,及时送达

Netty3


Netty3出现了太多的内存垃圾,创建了过多对象,在大的服务端压力下会表现比较糟糕,做了太多的内存拷贝,在堆上创建对象,堆缓冲区,当往socket写内容时就需要做内存拷贝,拷贝到直接内存,然后交给socket所以做了太多内存拷贝。


Netty3也没有一个好的内存池,写一个高性能的网络程序,大多时候会想使用本地内存,比如直接内存,创建和销毁直接内存是很耗性能的。


Netty3没有对linux进行优化,java对nio抽象接口这样可以在windows和osx上可以工作,但是在制定操作系统上做一些优化是比较难的,因为它是基于java的nio实现的。


Netty3的线程模型也不合理,每次要从socket中读取数据时,由于使用的是异步模式,所以有一个线程运行在一个eventloop中,当一些socket或文件描述符就绪时,我们从中读取数据然后传递到pipline,在pipline中可以进行数据处理和转换操作,当写数据时会从pipline再走一遍,直到走到socket,然后经过系统网络进入网络层,再到内核空间等。这里的问题是inbound数据处理是在一个eventloop里,一直是同一个线程。但outbound上写数据却不再这样,outbound处理数据始终处于调用线程里,这样难以理解到底是哪个线程在操作哪个数据和流程。


Netty4


  • Netty4产生更少的内存垃圾,意味着垃圾回收不必频繁工作

  • Netty4对linux传输层进行了优化使用了jni实现

  • Netty4还有一个高性能的Buffer Pool,用于直接内存


Netty4还有一个好用的线程池模型,inbound和outbound都发生在同一个线程内,这样就不用关心同步问题。


Channel


Channel是对socket的抽象,是双向操作的可以做一个写操作数据会走到socket,然后调用write系统操作把数据发送出去,如果使用TCP channel就相当于一个连接,每个channel对应一个channelpipline,channelpipline是一个包含不同channelhandler的双向链表,channelhandler可以处理inbound和outbound事件。


在Inbound每次读取一些数据,一个新的链接被接受,从pipline头部开始每个channelhandler可以对事件做一些处理,比如记日志,byte转pojo。

在做outbound时比如写操作,从pipline尾部开始,做系统调用之前你可以做一些拦截器模式实现。


Netty中使用链式过滤器,一个http编解码器其实是一个channelinboundhandler+channeloutboundhandler



Netty3中每个网络事件都是一个POJO对象,这样看起来比较简单,只需要传递POJO对象,通过instanceOf判断消息事件是连接事件还是读写事件,问题是在每次网络事件都建立一个POJO对象在加上一些数据转换操作,在更多连接建立时,这些都成为了负担。


同时也妨碍了JVM进行JIT优化,因为总是调用同一个方法来处理不同的POJO,做各种instanceof检查,这些对象可能有着复杂的结构,这是不友好的。


Netty4为了解决这个问题,引入了一个非常轻量级的对象池,除非真正需要,否则不要把所有对象都池化。但在Netty中有大量一直被重复使用的对象,这些对象被限制在同一个线程里使用,可以将他们缓存起来之后在重用他。


之前Netty3中进行POJO操作所调用的方法,都替换为直接方法调用减少了对象传递。


在从网络读取数据时 channelRead方法被调用,每次新连接建立时 channelActive方法被调用,当文件描述符被关闭时,channelInactive方法调用,这样可以节省一小部分对象。



EventloopGroup类似一个线程池,有很多线程用来分配给处理不同的channel上的事件。


如果想让系统处理更多的socket,只使用一个线程会成为性能瓶颈,因为你不能快的接受连接,然后SO_BACKLOG队列会变长直到超时,我们可以起多个线程在同一端口处理不同连接。


Buffer




JDK的ByteBuffer中,如果想把多个ByteBuffer组合起来,只能把多个ByteBuffer传递到一个ByteBuffer数组里,需要自己遍历。


Netty中提供了CompositeByteBuf类,允许把多个ByteBuffer组合到一起。


如果想要往Socket里写数据,需要先有一个非堆内存的Direct buffer,这个Direct buffer时需要释放的,否则就会造成内存不足,我们无法保证能够及时对直接内存进行回收。


在申请创建直接内存时,通过一个静态同步计数方法,在超出配置的最大大小的时候会抛出内存不足异常或错误。因为是静态同步方法,如果很多线程一起创建直接内存,就会产生大量阻塞,这就很糟糕。程序会暂停100毫秒在创建对象,调用system.gc告诉垃圾回收器该运行了,也就是说有了100毫秒的停顿,对于延迟敏感的程序很糟糕。


在Netty中创建一个bytebuffer,计数器就会加1,当结束的时候调用release方法,计数器就会减1。当计数器为0时,代表可以销毁它了。


解决这种问题Netty使用了内存池,在计数器为0的时候,将它放回到内存池,这样就不会再进入到静态同步方法了,但仍可以在内存池中保留它。创建池化的直接内存比创建非池化的直接内存快3倍。


Netty中提供了内存溢出检测器,比如在每次创建和释放内存的地方进行检测。


线程模型


Netty4中,一个channel被绑定到一个IO Thread上后绑定关系不再改变,这样的好处是所有操作会一直处于同一个线程内。


这样省去了类似volatile等线程可见性机制,可以不关心同步问题。IO线程驱动了inbound handler和outbound handler里的事件。如果一个handler是被共享的,需要保证线程安全了。如果handler只被一个channel使用就不需要关系同步问题了。


Eventloop线程其实就是java里的executor,netty会确保执行write/read线程是否是当前的eventloop线程。


在Netty3中,每次调用channelWrite都会调用socket write操作,但每次channel write的时候数据通常都比较小,这样可能调用多次write方法,系统调用是昂贵的,这样会很糟糕,因为java会调用JNI的C语言逻辑,C会进行系统调用,之后从用户空间进入到内核空间。


在Netty4对write和flus进行了拆分,对于outputstream每次调用write不会进入到socket,你需要调用flush才可以将outbound缓冲区所有数据写入到socket,当数据进来之后,调用channelWrite进行回应,在channel没有数据可读时调用channelReadComplete方法,这个方法触发channelFlush方法,之前写入缓存的数据会在一个系统调用里完成发送。


为解决每次调用Flush造成占用内存过多的情况,引入了背压机制对应的是channelWriteAblilityChange事件。


每次channel从可写变成不可写或反过来时,都会发送一个事件,这样可以检查channel是否可写了。可写则继续写,不可写了调用flush,这样可以对pipline连在一起的两个socket建立背压机制。


Netty3里,每次读事件来都会调用read,在Netty4中,每次有请求读取对象时,就调用channelRead方法,在订阅者无法在接受数据时停止读取。这也是基于Netty搞得Reactive Stream实现的方式。


IO线程


是用来做IO操作和事件处理的。他的抽象是eventloop。


有的时候我们需要调用一些不支持非阻塞API,比如JDBC或者文件系统等。在建立一个pipline之后里面会有多个handler处理不同任务。


内存伪共享



因为不同线程在使用相同cache line,他们之间通过ping-pong机制来更新值,因为你需要刷新内存里的值,这对性能影响很大。