Netty是什么
Netty是一款开发便捷,并且高性能的Java网络开发框架。Netty主要用来开发tcp或udp相关的服务,常被用来开发rpc服务,或被开源框架用作底层的网络库。
其便捷体现在:
1.统一而又简单的API,隐藏了底层细节方便开发,并可以在不同的IO模型间切换,如可以直接从NIO切换到epoll
2.扩展性强并且预置了很多编解码功能,支持主流协议(如protobuf)
3.使用广泛,参考案例多(如zookeeper,elasticSearch等知名项目使用了Netty)
其高性能体现在:
1.优雅的线程模型和无阻塞的api设计(大部分方法直接返回future)
2.使用内存池来分配Buffer,减少gc和反复创建对象的开销
3.零拷贝技术
HelloWorld与主要组件
EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); // (2)
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // (3)
.childHandler(new ChannelInitializer<SocketChannel>() { // (4)
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DiscardServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128) // (5)
.childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
// Bind and start to accept incoming connections.
ChannelFuture f = b.bind(port).sync(); // (7)
// Wait until the server socket is closed.
// In this example, this does not happen, but you can do that to gracefully
// shut down your server.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
主要组件(NIO):
EventLoop : 单线程的一个处理器,负责遍历selector,并处理其中的IO事件
EventLoopGroup:EventLoop池,为新连接的请求或者连结器分配EventLoop(实际上是将新来的连接,jdk中的channel注册到eventLoop中的selector上)
Channel:封装各种IO事件的处理方式,比如read,write,connect,bind
Pipeline:每一个Channel都有自己的Pipeline,它类似与Servlet的拦截器链,及发生一个IO事件时,将这个事件和处理结果依次向下传递处理。
Handler:把Pipeline比作一个拦截器链的化,handler就是一个个拦截器,用户可以自定义Handler来处理编解码,加解密和业务处理
流程图
内存池
在网络读写时,每一次读写都需要创建Buffer来承载数据。这些buffer通常只会被用到一次,之后便等待垃圾回收器将它们回收掉。由于网络服务的读写十分频繁,将产生大量的buffer,这会给GC照成很大的压力。因此Netty实现了一套内存池机制,自己来给buffer分配释放内存。
PoolChunk
为了能够简单的操作内存,必须保证每次分配到的内存时连续的。Netty中底层的内存分配和回收管理主要由PoolChunk实现,其内部维护一棵平衡二叉树memoryMap,所有子节点管理的内存也属于其父节点。
由于是平衡二叉树,那么用数组来存刚好合适。memoryMap 就是一个int数组,它的值代表了它的可分配内存状态。比如512号节点它的值是memoryMap[511] = val
如果val等于9(512节点的层数)那么表明512节点之下的所有节点都可分配。
如果val等于n(n <= 11)那么表明该节点的子节点有被分配了的,最大能分配的节点是n层的
如果val等于12(11 + 1)那么表示该节点下的所有节点都被分配了
那么如何来寻找合适的可分配节点呢?
## req 为被分配的节点的层数
def find(node, req):
if node.val < req :
res = find(node.left,req)
if res == NOT_FOUND:
res = find(node.right,req)
return res
if node.val == req:
return node.id
return NOT_FOUND
PoolSubpage
前面说到pageSize是chunk叶子节点的大小,默认为8K,当要分配的内存大于pageSize时去poolChunk里申请内存,而当内存小于pageSize时则在PoolSubpage中申请。
为什么要有Subpage呢,因为如果很小的内存仍要占一个page,那么会造成严重的内存浪费。而如果把pageSize设的足够小,又会使得分配内存的树(PoolChunk)占据太大的内存,并且对于小内存节点的搜索耗时增加。
那么什么是Subpage呢,Subpage实际上是将Chunk的Page的切分。比如现在要分配一个2K的内存,那么我们先找到一个8K的page,然后把它分成4份,其中一份分配出去,剩下的三分等着之后有要分配2K内存时再分配出去。
PoolChunkList
PoolChunkList就是由PoolChunk组成的双向链表
PoolArena
PoolArena是Netty内存池的基本单元,由两个PoolSubpages数组和6个PoolChunkList组成
PoolSubpage用于分配小于8k的内存;
- tinySubpagePools:用于分配小于512字节的内存,默认长度为32,因为内存分配最小为16,每次增加16,直到512,区间[16,512)一共有32个不同值;
- smallSubpagePools:用于分配大于等于512字节的内存,默认长度为4;
- tinySubpagePools和smallSubpagePools中的元素都是默认subpage。
PoolChunkList用于分配大于8k的内存;
- qInit:存储内存利用率0-25%的chunk
- q000:存储内存利用率1-50%的chunk
- q025:存储内存利用率25-75%的chunk
- q050:存储内存利用率50-100%的chunk
- q075:存储内存利用率75-100%的chunk
- q100:存储内存利用率100%的chunk
为什么ChunkList要分别装在六个不同的链表里呢,其实是为了解决整体利用率的问题。使用这种方法能避免频繁的创建和删除PoolChunk
上图展示了chunk在不同的ChunkList之间的移动规则
可以看到除了qInit和q000没法相互移动,别的相邻的list都是可以相互移动的。
刚创建的PoolChunk被放在qInit里,之后如果该chunk的利用率大于25%则会被放入q000,但chunk却没法重q000回到qInit。
在qInit里的chunk即使利用率等于0也不会被释放,而别的chunkList则会直接被释放。这个特性是的刚被创造出的chunk不会被立即释放,从而避免内存抖动时创建过多的chunk。
private synchronized void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
++allocationsNormal;
if (q050.allocate(buf, reqCapacity, normCapacity)
|| q025.allocate(buf, reqCapacity, normCapacity)
|| q000.allocate(buf, reqCapacity, normCapacity)
|| qInit.allocate(buf, reqCapacity, normCapacity)
|| q075.allocate(buf, reqCapacity, normCapacity)
|| q100.allocate(buf, reqCapacity, normCapacity)) {
return;
}
// Add a new chunk.
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
long handle = c.allocate(normCapacity);
assert handle > 0;
c.initBuf(buf, handle, reqCapacity);
qInit.add(c);
}
上述代码展示了不同ChunkList的优先级,可以看见申请内存时是按照:
q050->q025->q000->qInit->q075->q100的顺序申请的。
为什么q000不是在最前面,而是q050在最前面呢?因为如果q000在最前面,那么Chunk将很难被释放掉,会导致整体利用率不高,而如果qInit在最前面的话,会导致刚创建的Chunk没过多久就被释放了,使得Chunk被频繁创建,而当q050在最前时,避免了前面的问题,并且q050的命中率比q075和q100高,是个则中的选择。
由于netty通常应用于高并发系统,不可避免的有多线程进行同时内存分配,可能会极大的影响内存分配的效率,为了缓解线程竞争,可以通过创建多个poolArena细化锁的粒度,提高并发执行的效率。