Netty分享

2020-06-06  本文已影响0人  ssochi

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的内存;

PoolChunkList用于分配大于8k的内存;

为什么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细化锁的粒度,提高并发执行的效率。

上一篇下一篇

猜你喜欢

热点阅读