Netty ObjectPool

写在前面

对象池通过复用不再使用的对象来减少新对象创建, 背后的潜规则是对象创建和释放成本高于对象复用成本。这种成本体现在空间成本(对象的创建和释放需要做内存的分配和释放)和时间成本上(对象的构造过程比较长, 比如一条TCP链接需要经历三次握手)。工程案例如Spring减少空间成本, 连接池则更偏重于时间成本。总之, 对象池是一种工程上做空间或者时间优化的方法。(类似方法如buffer, cache)

一、常规实现

a. 主体结构: 线程安全容器+复用对象;
b. 复用对象: 提供保鲜接口给对象池, 比如数据库链接中的select 1;
c. 对象池: 对复用对象做必要的容量控制、并发控制和保鲜控制。其中并发控制解决并发环境下的对象获取问题, 避免一个对象分配给两个线程; 容量控制则基于过期时间或空间大小将多余的对象释放掉; 保鲜控制保持对象的可用性(如TCP链接长时间无心跳导致断开);
总之, 常规对象池通常是单例的, 全局共享。

二、Netty实现

1. 特别之处

a. 对象池的作用域是线程, 与对象获取线程绑定, 1对1关系, 尽可能降低并发竞争;
b. 使用jcTools MpscQueue库, 通过字节填充避免伪共享问题, CPU缓存友好, 因此效率更高;

2. 对象复用状态处理 Handle

  1. 常规对象池中对象的复用状态基于是否存放在容器中标识,Netty通过状态标识,更新成本更低。具体到io.netty.util.Recycler$DefaultHandle实现, 可以看到其内部包含2种状态,CLAIMED,AVAILABLE,和目标对象的引用value;
  2. 调用handle.recyle()时,对象会基于CAS被标记为AVAILABLE,表示可复用;
  3. 调用handle.claim时, 会基于CAS被标记为CALIM,表示已使用;
  4. Netty中的默认实现有两个为DefaultHandle和NOOP Handle。后者显然在不复用的使用使用,毕竟Netty的许多功能都可以通过配置实现关闭;

3. 对象池 LocalPool

实际对象存储容器为MessagePassingQueue, 具体实现基于jcTools中的MpscChunkedArrayQueue或者MpscChunkedAtomicArrayQueue(通过生产者消费者的双指针,和缓存行填充来降低并发成本,进而提升并发环境下performance的一种集合)。

3.1 对象获取

  1. 尝试从容器中获取,如果成功则返回;这里多啰嗦几句,获取过程包含Handle出队+Handle状态更新2个步骤,。而常规的出队通过poll来完成,Netty此处使用的是relaxedPoll,这里说明下2者的区别。
我们知道MpscQueue中有2个指针,无论是P还是C都有limit和index两个指针,其实还包含了一个就是index对应的element。在MpscQueue中,其保证limit 和 index的更新是原子的,但是并没有保证index更新与element的写入是原子可见的。存在C先看到index更新,而后看到element的情况。MpscQueue对外的poll方法内部通过自旋等待直到看到element才返回;

Netty则是为了性能,把自旋等待的这步给省略掉了。因为,作为一个对象池我们其实也不需要保证顺序消费,即一定要复用最近一次放入的对象,复用其他上上次放入的对象也是OK的。因此,调用的是relaxedPoll,只要获取结果为NULL,直接结束,大不了创建新的元素。
  1. 如果失败则新建对象并加入pool;
  2. 返回目标对象;

3.2 对象归还

  1. 更新Handle状态为available;
  2. 调用relaxedOffer归还到队列中,虽然是relaxedOffer但其内部还是offer,也确实没啥优化。

4. 线程绑定

4.1 如何绑定

显然此处默认会基于FastThreadLocal实现,当然对象池的实现与线程绑定方式的实现么有关系,因此使用ThreadLocal来搞也是没有问题的。

4.2 其他配置

问题 解决
ObjectPool中应该Cache多少个对象 启动参数 io.netty.recycler.maxCapacityPerThread, 默认至多 4096个
Cache对象的创建速度调控 启动io.netty.recycler.ratio ,默认每8个对象保留缓存1个

5. 再调整

由于Netty中对象池的实现需要解决配置问题,线程绑定,以及对象池本身的逻辑问题,全部使用ObjectPool中非常不够单一职责。因此使用了Recycler,作为顶层接口,内部解决配置问题和线程绑定问题,具体的对象池逻辑沉淀在LocalPool中。

总结

本篇咱们讨论了对象池的作用是复用对象,基于并发集合+保鲜控制这种全局实例的常规实现,最终重点介绍了Netty中基于线程颗粒度的MpscQueue+Handle的特别实现,希望帮助你建立起Netty对象池的全局视角。

你可能感兴趣的:(Netty,java,开发语言,Netty)