netty的EventLoop选择策略

netty 中的 channel 在完成创建和初始化之后,需要注册到 EventLoopGroup 上,这本质上是交给 EventLoop 管理 channel 的各种事件。一个 EventLoopGroup 管理了多个 EventLoop,那么在注册 channel 时,EventLoopGroup 就需要选择一个 EventLoop,然后将其和 channel 关联起来。选择 EventLoop 是一个很高频的操作,该操作是否高效会直接影响 netty 的性能,本文就来聊下 EventLoop 的选择策略。

image-20210102175908637

netty 使用 EventExecutorChooserFactory.EventExecutorChooser 选择要使用的 EventLoop

1
2
3
4
5
6
7
8
9
10
11
12
13
private final EventExecutorChooserFactory.EventExecutorChooser chooser;

// MultithreadEventExecutorGroup
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
// 省略其他代码...
children = new EventExecutor[nThreads];
chooser = chooserFactory.newChooser(children);
}

public EventExecutor next() {
return chooser.next();
}

EventExecutorChooser 是一个接口,它只有一个 next() 方法,用于获取一个 EventExecutor(对于 EventLoopGroup 来说,就是获取一个 EventLoop

1
2
3
4
5
6
interface EventExecutorChooser {
/**
* Returns the new {@link EventExecutor} to use.
*/
EventExecutor next();
}

EventExecutorChooser 共有两种实现,分别是 GenericEventExecutorChooserPowerOfTwoEventExecutorChooser,本质上这两种策略都是最简单的轮询策略,他们的区别在于具体的实现。

GenericEventExecutorChooser

GenericEventExecutorChooser 是一个通用的选择器,它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final class GenericEventExecutorChooser implements EventExecutorChooser {
// Use a 'long' counter to avoid non-round-robin behaviour at the 32-bit overflow boundary.
// The 64-bit long solves this by placing the overflow so far into the future, that no system
// will encounter this in practice.
private final AtomicLong idx = new AtomicLong();
private final EventExecutor[] executors;

GenericEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}

@Override
public EventExecutor next() {
return executors[(int) Math.abs(idx.getAndIncrement() % executors.length)];
}
}

它内部使用一个数组保存了所有可用的 executor,使用一个 AtomicLong 类型的元素作为 counter 计数器。每次取元素时根据 counter 和数组长度取余计算数组下标,取完数据后 counter 加一。其中 getAndIncrement 保证了操作的原子性,避免了并发问题。

1
2
3
index = id % length       # 计算下标
id = id + 1 # 下标加一
return executors[index] # 返回数组元素

PowerOfTwoEventExecutorChooser

PowerOfTwoEventExecutorChooser 是另外一种选择器,它也是采用的轮询的策略。对于轮询策略来说,当数组长度是 2 的幂次方时,我们可以通过位运算计算数组下标。

1
index = id & (length - 1)

和取余操作相比,位运算是一种更高效的计算方式,这对于性能的提升有一定的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
private final AtomicInteger idx = new AtomicInteger();
private final EventExecutor[] executors;

PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
this.executors = executors;
}

@Override
public EventExecutor next() {
return executors[idx.getAndIncrement() & executors.length - 1];
}
}

PowerOfTwoEventExecutorChooser 的缺点是对数组的长度有要求,这样对于不符合要求的情况就只能使用 GenericEventExecutorChooser ,这也是 EventExecutorChooserFactory 要做的工作

EventExecutorChooserFactory

1
2
3
4
5
6
7
public interface EventExecutorChooserFactory {

/**
* Returns a new {@link EventExecutorChooser}.
*/
EventExecutorChooser newChooser(EventExecutor[] executors);
}

EventExecutorChooserFactory 用于生成 EventExecutorChooser ,它考虑的因素是 executors 数组的长度,当长度是 2 的幂次方时使用 PowerOfTwoEventExecutorChooser ,否则使用 GenericEventExecutorChooser

1
2
3
4
5
6
7
8
9
10
11
12
// DefaultEventExecutorChooserFactory.newChooser
public EventExecutorChooser newChooser(EventExecutor[] executors) {
if (isPowerOfTwo(executors.length)) {
return new PowerOfTwoEventExecutorChooser(executors);
} else {
return new GenericEventExecutorChooser(executors);
}
}

private static boolean isPowerOfTwo(int val) {
return (val & -val) == val;
}

判断是否为2的幂次方

2 的幂次方一定大于 0,当确认数字大于 0 时,可以用以下方法判断

方法一

1
return (val & -val) == val;

示例

1
2
3
16       = 00000000000000000000000000010000
-16 = 11111111111111111111111111110000
16 & -16 = 00000000000000000000000000010000

方法二

1
return (val & val - 1 == 0)

示例

1
2
3
16       = 00000000000000000000000000010000
15 = 00000000000000000000000000001111
16 & 15 = 00000000000000000000000000000000