性能工具
{Back to Index}  

Table of Contents

1 FastThreadLocal

每个 FastThreadLocal 拥有一个与众不同的 index 作为其身份标识。 并且使用了 InternalThreadLocalMap 存储数据,其底层数据结构为一个数组,数组索引值就是 index 。

虽然基于 JDK 的 ThreadLocalMap 其底层也是数组,但是由于索引的计算需要用到 hashcode (key.threadLocalHashCode & (table.length - 1);) ,还要处理冲突问题,因此 FastThreadLocal 在这方面做了优化。

FastThreadLocal.png

Figure 1: FastThreadLocalThread 类结构

Netty-FastThreadLocal.png

Figure 2: FastThreadLocal 应用在 FastThreadLocalThread 和 java.lang.Thread 中的区别

2 内存池 Recycler

对象池与内存池的都是为了提高 Netty 的并发处理能力,Java 中频繁地创建和销毁对象的开销是很大的,所以会将一些通用对象缓存起来,当需要某个对象时,优先从对象池中获取对象实例。通过重用对象,不仅避免频繁地创建和销毁所带来的性能损耗,而且对 JVM GC 是友好的,这就是对象池的作用。

Netty 内存池最开始是基于 Stack,WeakOrderQueue,Link,DefaultHandle 四个组件的,后来重构过,使用了新的数据结构。

recycler-mem.png

Figure 3: 网上有张图描述了 Recycler 的内存结构(重构前)

对象使用完可以回收到当前线程缓存,也可以在别的线程的缓存中进行回收。
当从 Recycler 获取对象时,优先从当前线程缓存中查找,如果没有可用对象,会尝试从别的线程缓存中 迁移 部分对象到当前线程缓存中。

3 规避空轮训 bug

long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
    selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
    selector = selectRebuildSelector(selectCnt);
    selectCnt = 1;
    break;
}

Netty 提供了一种检测机制判断线程是否可能陷入空轮询,具体的实现方式如下:

  1. 每次执行 Select 操作之前记录当前时间 currentTimeNanos 。
  2. 如果事件轮询的持续时间大于等于 timeoutMillis ,那么说明是正常的,否则表明阻塞时间并未达到预期,可能触发了空轮询的 Bug 。
  3. 引入了计数变量 selectCnt ,正常情况下,selectCnt 会重置,否则会对 selectCnt 自增计数。当 selectCnt 达到阈值(默认512)时,触发重建 Selector 对象。异常的 Selector 中所有的 SelectionKey 会重新注册到新建的 Selector 上,重建完成之后废弃异常的 Selector 。

4 时间轮

JDK 实现定时器的方式在新增和取消任务时,其复杂度都是 \(O(logn)\) ,面对海量任务插入和删除的场景,会遇到性能瓶颈。对于性能要求较高的场景,一般都会采用时间轮算法。

java-timer.png

Figure 4: 基于 ScheduledThreadPoolExecutor 的定时器

时间轮可以理解为一种环形结构,像钟表一样被分为多个 slot 槽位。每个 slot 代表一个时间段,每个 slot 中可以存放多个任务,使用的是链表结构保存该时间段到期的所有任务。时间轮通过一个时针随着时间一个个 slot 转动,并执行 slot 中的所有到期任务。

timewheel.png

添加任务时,可以根据任务的到期时间进行取模,然后将任务分布到不同的 slot 中。如上图所示,时间轮被划分为 8 个 slot,每个 slot 代表 1s ,当前时针指向 2。假如现在需要调度一个 3s 后执行的任务,应该加入 \(2+3=5\) 的 slot 中;如果需要调度一个 12s 以后的任务,需要等待时针完整走完一圈 round 零 4 个 slot,需要放入第 \((2+12)%8=6\) 个 slot 。

为了区分每个任务是立即执行,还是等待下一圈 round ,就需要把 round 信息保存在任务中。例如图中第 6 个 slot 的链表中包含 3 个任务,第一个任务 \(round=0\) ,表示需要立即执行,第二个任务 \(round=1\) ,需要等待 8s 后执行;第三个任务 \(round=2\) ,需要等待 16s 后执行。所以当时针转动到对应 slot 时,只执行 \(round=0\) 的任务,slot 中其余任务的 round 应当减 1 ,等待下一个 round 之后执行。

可见时间轮定时器最大的优势就是,任务的新增和取消都是 \(O(1)\) 时间复杂度,而且只需要一个线程就可以驱动时间轮进行工作。

时间轮中的任务执行是 串行 的,当一个任务执行的时间过长,会影响后续任务的调度和执行,可能产生任务堆积的情况。

Netty HashedWheelTimer 的构造函数核心属性:

  • threadFactory

    线程工厂,创建了 一个 工作线程

  • tickDuration

    时针每次 tick 的时间,即间隔多久走到下一个 slot

  • ticksPerWheel

    时间轮上一共有多少个 slot ,默认 512 个

  • leakDetection

    是否开启内存泄漏检测

  • maxPendingTimeouts

    最大允许等待任务数,任务超出该阈值时会抛出异常

wheel-ds.png

Figure 6: HashedWheelTimer 数据结构

Netty 的时间轮的缺点:

  • 如果长时间没有到期任务,那么会存在时间轮 空推进 的现象
  • 只适用于处理耗时较短的任务,由于 Worker 是单线程的,如果一个任务执行的时间过长,会造成 Worker 线程阻塞

Author: Hao Ruan (ruanhao1116@gmail.com)

Created: 2020-05-17 Sun 22:28

Updated: 2022-06-10 Fri 19:56

Emacs 27.2 (Org mode 9.4.4)