netty源码分析笔记

参考资料:
[1]. netty源码分析之揭开reactor线程的面纱(二)
[2]. Netty 源码分析之 一 揭开 Bootstrap 神秘的红盖头 (服务器端)
[3]. netty源码分析之揭开reactor线程的面纱(三)
[4]. netty源码分析之揭开reactor线程的面纱(一)
[5]. netty源码分析之pipeline(二)
[6]. Netty中的装饰者模式
[7]. 深入浅出Netty内存管理 PoolChunk
[8]. 禁用Netty对象缓存机制
[9]. ThreadLocal与弱引用WeakReference
[10]. ByteBuf继承结构
[11]. Netty 轻量级对象池Recycler分析
[12]. How to use io.netty.util.Recycler in multiple thread, will it cause a memory leak?
[13]. Netty源码笔记-第八章 内存管理
[14]. netty学习系列四:读操作
[15]. Netty-第八章 内存管理-1.基本概念

  • SelectedKeys优化
    在[1]中,最令我震撼的是netty为了提高效率,用反射selector的SelectedKeys从set的实现改为另外一个用数组实现的set
private SelectedSelectionKeySet selectedKeys;

private Selector NioEventLoop.openSelector() {
    //...
    final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
    // selectorImplClass -> sun.nio.ch.SelectorImpl
    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
    selectedKeysField.setAccessible(true);
    publicSelectedKeysField.setAccessible(true);
    selectedKeysField.set(selector, selectedKeySet);
    publicSelectedKeysField.set(selector, selectedKeySet);
    //...
    selectedKeys = selectedKeySet;
}
  • handler 与 childHandler 的区别与联系
    在配置ServerBootstrap的时候有两个handler可以配置,handler 与 childHandler,前者在建立连接的时候在bossgroup起作用,后者连接之后在wokergroup起作用。
    下面内容引用自[2]

最后我们来总结一下服务器端的 handler 与 childHandler 的区别与联系:
在服务器 NioServerSocketChannel 的 pipeline 中添加的是 handler 与 ServerBootstrapAcceptor.
当有新的客户端连接请求时, ServerBootstrapAcceptor.channelRead 中负责新建此连接的 NioSocketChannel 并添加 childHandler 到 NioSocketChannel 对应的 pipeline 中, 并将此 channel 绑定到 workerGroup 中的某个 eventLoop 中.
handler 是在 accept 阶段起作用, 它处理客户端的连接请求.
childHandler 是在客户端连接建立以后起作用, 它负责客户端连接的 IO 交互.

  • reactor线程的机制
    socket连接之后,要有worker线程对socket进行数据的读写和处理,线程的主要工作如下图所示,大致就是先读写事件,然后再运行一些数据处理的任务。

    [3] 主要讲的是上图run tasks这块。


    线程的父类SingleThreadEventExecutor中有taskQueue,创建的时候是调用NioEventLoop重写的newTaskQueue,这个mpsc是多生产者,单一消费者的线程,根据[3]中分析,其他的线程调用channel.write(...)之类的可以将要执行的任务加入到对应线程的队列中。
    @Override
    protected Queue newTaskQueue(int maxPendingTasks) {
        // This event loop never calls takeTask()
        return PlatformDependent.newMpscQueue(maxPendingTasks);
    }

[3]中作者有个问题问的很好:schedule为什么要使用优先级队列,而不需要考虑多线程的并发?
因为schedule任务的加入都是自己的线程完成的。

对此,netty的处理是,如果是在外部线程调用schedule,netty将添加定时任务的逻辑封装成一个普通的task,这个task的任务是添加[添加定时任务]的任务,而不是添加定时任务,其实也就是第二种场景,这样,对 PriorityQueue的访问就变成单线程,即只有reactor线程

最终结合[3]和[4],我们可以知道NioEventLoop其实结合了定时器任务队列和资源等待队列,后者用来等待socket注册的事件,每次阻塞之前都会处理所有的任务,处理完调用select等待,等待的超时事件为定时器第一个到期任务。

  • 异常的传递
    参考[5],pipline在这里就可以体现出好处,可以将异常很方便的进行传递。
    在in的过程中,会调用每个handler都有的exceptionCaught方法,如果没有复写,则fireExceptionCaught,这样就会一直传递下去,如果自定义的handler都没有处理,最后如果有ChannelHandlerContext在它的exceptionCaught就有进行处理。
    在out的过程也是一直向后传播,这里有点疑惑,为什么out的过程是向后传播而不是向前传播?单纯为了找ChannelHandlerContext?那out向前的那些handler的异常处理都没有调用到?

异常传播只会往后传播,而且不分inbound还是outbound节点,不像outBound事件一样会往前传播

  • NioEventLoop 继承体系

1、NioEventLoop 可以被追溯到 java.util.concurrent.ExecutorService 接口。这个信息表明 NioEventLoop 能够执行 Runnable 任务,从而保证了这些要执行的任务是在 NioEventLoop 的线程中执行,而非外部线程来执行。NioEventLoop 内置的一些 Runnable 任务包括了对 channel 的 register、系统缓冲区满后而推迟的 flush 任务等。

  • ByteBuf
    ByteBuf分类:
    内存的来源:堆内存(Heap)与直接(本地)内存(Direct)
    是否使用池化技术:Unpooled

性能增强

UnpooledUnsafeDirectByteBuf和UnpooledUnsafeNoCleanerDirectByteBuf,内部使用了Unsafe进行高性能的get/set操作,为何高性能这里涉及Unsafe操作本地方法,NoCleaner涉及操作直接内存(非堆)时使用Unsafe本地方法创建和释放DirectByteBuffer不使用JDK内部实现的DirectByteBuffer创建和释放,因为JDK内部的DirectByteBuffer内部实现了Cleaner机制来释放内存,所以这里直接操作内存叫NoCleaner,这样做有两个好处:1.屏蔽了不同版本JDK的实现差异,但违背了跨平台特性。2.高性能直接操作内存,适配高并发场景。

ByteBuf提供了一些较为丰富的实现类,逻辑上主要分为两种:HeapByteBuf和DirectByteBuf,实现机制则分为两种:PooledByteBuf和UnpooledByteBuf,除了这些之外,Netty还实现了一些衍生ByteBuf(DerivedByteBuf),如:ReadOnlyByteBuf、DuplicatedByteBuf以及SlicedByteBuf。

UnpooledHeapByteBuf

  • SimpleChannelInboundHandler内存释放
    如果我们在构建SimpleChannelInboundHandler的时候将autoRelease设置为真,也就是说,SimpleChannelInboundHandler内部帮忙释放内存的话,从下面的逻辑我们可以知道内存是在最后一个Handler里面释放的,前面的Handler如果调用输入的类型是I,那么会调用当前的channelRead0,然后释放内存;如果不是当前需要的类型,继续向下传递,由后面的Handler自己释放内存,这里不帮忙释放内存。
    我们记住谁拉屎谁擦屁股就好,谁用到I类型的对象,谁自己去释放。
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        boolean release = true;
        try {
            if (acceptInboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                I imsg = (I) msg;
                channelRead0(ctx, imsg);
            } else {
                release = false;
                ctx.fireChannelRead(msg);
            }
        } finally {
            if (autoRelease && release) {
                ReferenceCountUtil.release(msg);
            }
        }
    }

这里我们猜测,如果没有人用到这个变量,最后在TailContext中会释放内存,果不其然,channelRead->onUnhandledInboundMessage最后会释放内存。

    protected void onUnhandledInboundMessage(Object msg) {
        try {
            logger.debug(
                    "Discarded inbound message {} that reached at the tail of the pipeline. " +
                            "Please check your pipeline configuration.", msg);
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
  • PoolArena

Netty采用了jemalloc的思想,这是FreeBSD实现的一种并发malloc的算法。jemalloc依赖多个Arena来分配内存,运行中的应用都有固定数量的多个Arena,默认的数量与处理器的个数有关。系统中有多个Arena的原因是由于各个线程进行内存分配时竞争不可避免,这可能会极大的影响内存分配的效率,为了缓解高并发时的线程竞争,Netty允许使用者创建多个分配器(Arena)来分离锁,提高内存分配效率,当然是以内存来作为代价的。

  • PoolChunk与PoolSubpage
    chunk=2048page
    16MB=2048
    8KB
    参考[7],屏蔽细节的说法,一共有2048个page,每个page的大小一样,将所有的page当做树的叶子,用一棵完全二叉树维系了page的所有权,最上面的节点拥有全部的page,其他节点也拥有对应叶子个数的page。
    如果分配的内存大于page,向上取整分配2次幂的page,二叉树每个节点都维系着其page(叶子)的使用情况——全部未使用,子节点拥有的最大完整page个数(2^N),全部使用完了。
    如果分配的内存小于page,会使用PoolSubpage,PoolSubpage是将一个page切分为后面其中一个的等分——16/32/64.../512(tiny,32种)或者512/1024/2048/4096(small,4种),由第一次分配内存的时候进行切分,会找到对应的PoolSubpage,不会分配多块内存,比如分配31个字节,直接去找以32字节进行切分的PoolSubpage,不会去以16字节进行切分的PoolSubpage找两块连续的16拼起来。([])
    运用这个数据结构的特点是:能够快速找到2次幂的page。

  • PoolThreadCache
    PoolThreadCache来减少分配内存时的锁的争用,但如果使用这个机制,获取和释放内存的要在同一个线程,如果在IO线程中调用分配,在业务线程中调用释放,那么缓存的内存都放到业务线程中去,下次IO线程调用还得重新分配内存,它并没有缓存到内存。(参考[8])

  • FastThreadLocal怎么Fast?
    ThreadLocal可以看下[9]。
    简单的来说FastThreadLocal就是将ThreadLocal的哈希表的映射去掉,每次都分配一个新的index给变量,根据这个变量在数据中心存取变量。

  • Recycler
    Recycler是一个用来回收经常重复创建和丢弃的对象,Recycler是属于某个线程的,从其主要用来回收的容器——一个netty自己实现的内部类Stack的定义就可以看出来,如下代码所示。

    private final FastThreadLocal> threadLocal = new FastThreadLocal>() {
        @Override
        protected Stack initialValue() {
            return new Stack(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor,
                    ratioMask, maxDelayedQueuesPerThread);
        }
    };

Recycler的用法如下所示,引用自[11]:
为什么回收的时候知道是否是Recycler的线程创建的?因为对象要使用Recycler进行回收,会有一定的使用规则,会传一个句柄跟对象绑定到一起,这个句柄绑定到Recycler和线程,回收的时候使用句柄进行回收,就可以知道。实际上Recycler.Handle使用起来还可以更简洁点,可以再封装作为一个类让对象来继承,实现装饰器模式,anyway,下面这样还是用起来了。Recycler.Handle是我们放进栈或者队列中的一个元素,其中的value域就是我们的对象。

public class RecyclerTest {

    private static final Recycler RECYCLER = new Recycler() {
        @Override
        protected User newObject(Handle handle) {
            return new User(handle);
        }
    };

    public static class User {
        private Recycler.Handle handle;

        public User(Recycler.Handle handle) {
            this.handle = handle;
        }

        public void recycle() {
            handle.recycle(this);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        User user = RECYCLER.get();

        user.recycle(); // 同线程回收对象

// new FastThreadLocalThread(() -> {
// user.recycle();
// }).start(); // 异线程回收对象

        System.out.println(user == RECYCLER.get()); // true
    }

}

当我们使用完将对象回去回去后,Recycler内部的结构用了一个栈来回收自己线程创建的对象,多个WeakOrderQueue(其他线程每个对应一个队列)回收非自己线程创建的对象。从Recycler中获取对象的时候,首先从栈中获取,如果获取不到就从WeakOrderQueue从获取,如果还是获取不到最后从调用上面复写的抽象方法newObject获取对象。其实我不知道WeakOrderQueue用来干嘛?貌似是防止其他线程放入对象和自己线程取对象发生冲突,但是却不知道为什么WeakOrderQueue可以做到这点,因为获取的时候也可能到WeakOrderQueue进行获取?github上面[12]
有人提问,作者也回答了,但是看不懂。

  • PooledByteBufAllocator实现
    PoolThreadLocalCache和PoolThreadCache的名字相差了一个Local,这个Local代表了PoolThreadLocalCache是专属某个线程的一个PoolThreadCache缓冲池,可以从下面PoolThreadLocalCache类的定义代码中看出,其继承自FastThreadLocal,关于FastThreadLocal可以参考上面的内容,可以简单看做ThreadLocal的另外一个实现。
    PoolThreadCache内部保存了tiny/small/normal的堆内存和直接内存的MemoryRegionCache数组。
    final class PoolThreadLocalCache extends FastThreadLocal {

        @Override
        protected synchronized PoolThreadCache initialValue() {
            final PoolArena heapArena = leastUsedArena(heapArenas);
            final PoolArena directArena = leastUsedArena(directArenas);

            return new PoolThreadCache(
                    heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize,
                    DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL);
        }

        @Override
        protected void onRemoval(PoolThreadCache threadCache) {
            threadCache.free();
        }

        private  PoolArena leastUsedArena(PoolArena[] arenas) {
            if (arenas == null || arenas.length == 0) {
                return null;
            }

            PoolArena minArena = arenas[0];
            for (int i = 1; i < arenas.length; i++) {
                PoolArena arena = arenas[i];
                if (arena.numThreadCaches.get() < minArena.numThreadCaches.get()) {
                    minArena = arena;
                }
            }

            return minArena;
        }
    }
  • RecvByteBufAllocator([15])
    RecvByteBufAllocator内部定义了一个Handle,用在底层读数据的时候实现高效分配内存,它实际上不进行具体的分配,只是根据前面分配的分配内存大小计算好下次分配需要的内存大小,然后根据传入的ByteBufAllocator分配内存。
    底层写操作
    https://www.cnblogs.com/stateis0/p/9062154.html

你可能感兴趣的:(netty源码分析笔记)