美文网首页
Netty分享

Netty分享

作者: ssochi | 来源:发表于2020-06-06 11:45 被阅读0次

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

    相关文章

      网友评论

          本文标题:Netty分享

          本文链接:https://www.haomeiwen.com/subject/fepzzhtx.html