Netty如何高效接收网络数据?聊透ByteBuffer动态自适应扩缩 六
4.3 扩缩容逻辑
现在扩缩容索引表SIZE_TABLE已经初始化完毕了,那么当我们需要对ByteBuffer进行扩容或者缩容的时候如何根据SIZE_TABLE决定扩容多少或者缩容多少呢??
这就用到了在AdaptiveRecvByteBufAllocator类中定义的扩容步长INDEX_INCREMENT = 4,缩容步长INDEX_DECREMENT = 1了。
我们就以上面两副扩缩容容量索引表SIZE_TABLE中的容量索引展示截图为例,来介绍下扩缩容逻辑,假设我们当前ByteBuffer的容量索引为33,对应的容量为2048。
4.3.1 扩容
当对容量为2048的ByteBuffer进行扩容时,根据当前的容量索引index = 33 加上 扩容步长INDEX_INCREMENT = 4计算出扩容后的容量索引为37,那么扩缩容索引表SIZE_TABLE下标37对应的容量就是本次ByteBuffer扩容后的容量SIZE_TABLE[37] = 32768
4.3.1 缩容
同理对容量为2048的ByteBuffer进行缩容时,我们就需要用当前容量索引index = 33 减去 缩容步长INDEX_DECREMENT = 1计算出缩容后的容量索引32,那么扩缩容索引表SIZE_TABLE下标32对应的容量就是本次ByteBuffer缩容后的容量SIZE_TABLE[32] = 1024
4.4 扩缩容时机
public abstract class AbstractNioByteChannel extends AbstractNioChannel {
@Override
public final void read() {
.........省略......
try {
do {
.........省略......
} while (allocHandle.continueReading());
//根据本次read loop总共读取的字节数,决定下次是否扩容或者缩容
allocHandle.readComplete();
.........省略.........
} catch (Throwable t) {
...............省略...............
} finally {
...............省略...............
}
}
}
在每轮read loop结束之后,我们都会调用allocHandle.readComplete()来根据在allocHandle中统计的在本轮read loop中读取字节总大小,来决定在下一轮read loop中是否对DirectByteBuffer进行扩容或者缩容。
public abstract class MaxMessageHandle implements ExtendedHandle {
@Override
public void readComplete() {
//是否对recvbuf进行扩容缩容
record(totalBytesRead());
}
private void record(int actualReadBytes) {
if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
if (decreaseNow) {
index = max(index - INDEX_DECREMENT, minIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
} else {
decreaseNow = true;
}
} else if (actualReadBytes >= nextReceiveBufferSize) {
index = min(index + INDEX_INCREMENT, maxIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
}
}
}
我们以当前ByteBuffer容量为2048,容量索引index = 33为例,对allocHandle的扩容缩容规则进行说明。
扩容步长INDEX_INCREMENT = 4,缩容步长INDEX_DECREMENT = 1。
image.png
4.4.1 缩容
•如果本次OP_READ事件实际读取到的总字节数actualReadBytes在SIZE_TABLE[index - INDEX_DECREMENT]与SIZE_TABLE[index]之间的话,也就是如果本轮read loop结束之后总共读取的字节数在[1024,2048]之间。说明此时分配的ByteBuffer容量正好,不需要进行缩容也不需要进行扩容。比如本次actualReadBytes = 2000,正好处在1024与2048之间。说明2048的容量正好。
•如果actualReadBytes 小于等于 SIZE_TABLE[index - INDEX_DECREMENT],也就是如果本轮read loop结束之后总共读取的字节数小于等于1024。表示本次读取到的字节数比当前ByteBuffer容量的下一级容量还要小,说明当前ByteBuffer的容量分配的有些大了,设置缩容标识decreaseNow = true。当下次OP_READ事件继续满足缩容条件的时候,开始真正的进行缩容。缩容后的容量为SIZE_TABLE[index - INDEX_DECREMENT],但不能小于SIZE_TABLE[minIndex]。
注意需要满足两次缩容条件才会进行缩容,且缩容步长为1,缩容比较谨慎
4.4.2 扩容
如果本次OP_READ事件处理总共读取的字节数actualReadBytes 大于等于 当前ByteBuffer容量(nextReceiveBufferSize)时,说明ByteBuffer分配的容量有点小了,需要进行扩容。扩容后的容量为SIZE_TABLE[index + INDEX_INCREMENT],但不能超过SIZE_TABLE[maxIndex]。
满足一次扩容条件就进行扩容,并且扩容步长为4, 扩容比较奔放
4.5 AdaptiveRecvByteBufAllocator类的实例化
AdaptiveRecvByteBufAllocator类的实例化主要是确定ByteBuffer的初始容量,以及最小容量和最大容量在扩缩容索引表SIZE_TABLE中的下标:minIndex和maxIndex。
AdaptiveRecvByteBufAllocator定义了三个关于ByteBuffer容量的字段:
•DEFAULT_MINIMUM :表示ByteBuffer最小的容量,默认为64,也就是无论ByteBuffer在怎么缩容,容量也不会低于64。
•DEFAULT_INITIAL:表示ByteBuffer的初始化容量。默认为2048。
•DEFAULT_MAXIMUM :表示ByteBuffer的最大容量,默认为65536,也就是无论ByteBuffer在怎么扩容,容量也不会超过65536。
public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllocator {
static final int DEFAULT_MINIMUM = 64;
static final int DEFAULT_INITIAL = 2048;
static final int DEFAULT_MAXIMUM = 65536;
public AdaptiveRecvByteBufAllocator() {
this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);
}
public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
.................省略异常检查逻辑.............
//计算minIndex maxIndex
//在SIZE_TABLE中二分查找最小 >= minimum的容量索引 :3
int minIndex = getSizeTableIndex(minimum);
if (SIZE_TABLE[minIndex] < minimum) {
this.minIndex = minIndex + 1;
} else {
this.minIndex = minIndex;
}
//在SIZE_TABLE中二分查找最大 <= maximum的容量索引 :38
int maxIndex = getSizeTableIndex(maximum);
if (SIZE_TABLE[maxIndex] > maximum) {
this.maxIndex = maxIndex - 1;
} else {
this.maxIndex = maxIndex;
}
this.initial = initial;
}
}
接下来的事情就是确定最小容量DEFAULT_MINIMUM 在SIZE_TABLE中的下标minIndex,以及最大容量DEFAULT_MAXIMUM 在SIZE_TABLE中的下标maxIndex。
从AdaptiveRecvByteBufAllocator类初始化的过程中,我们可以看出SIZE_TABLE中存储的数据特征是一个有序的集合。
我们可以通过二分查找在SIZE_TABLE中找出第一个容量大于等于DEFAULT_MINIMUM的容量索引minIndex。
同理通过二分查找在SIZE_TABLE中找出最后一个容量小于等于DEFAULT_MAXIMUM的容量索引maxIndex。
根据上一小节关于SIZE_TABLE中容量数据分布的截图,我们可以看出minIndex = 3,maxIndex = 38
4.5.1 二分查找容量索引下标
private static int getSizeTableIndex(final int size) {
for (int low = 0, high = SIZE_TABLE.length - 1;;) {
if (high < low) {
return low;
}
if (high == low) {
return high;
}
int mid = low + high >>> 1;//无符号右移,高位始终补0
int a = SIZE_TABLE[mid];
int b = SIZE_TABLE[mid + 1];
if (size > b) {
low = mid + 1;
} else if (size < a) {
high = mid - 1;
} else if (size == a) {
return mid;
} else {
return mid + 1;
}
}
}
经常刷LeetCode的小伙伴肯定一眼就看出这个是二分查找的模板了。
它的目的就是根据给定容量,在扩缩容索引表SIZE_TABLE中,通过二分查找找到最贴近给定size的容量的索引下标(第一个大于等于 size的容量)
4.6 RecvByteBufAllocator.Handle
前边我们提到最终动态调整ByteBuffer容量的是由AdaptiveRecvByteBufAllocator中的Handler负责的,我们来看下这个allocHandle的创建过程。
protected abstract class AbstractUnsafe implements Unsafe {
private RecvByteBufAllocator.Handle recvHandle;
@Override
public RecvByteBufAllocator.Handle recvBufAllocHandle() {
if (recvHandle == null) {
recvHandle = config().getRecvByteBufAllocator().newHandle();
}
return recvHandle;
}
}
从allocHandle的获取过程我们看到最allocHandle的创建是由AdaptiveRecvByteBufAllocator#newHandle方法执行的。
public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllocator {
@Override
public Handle newHandle() {
return new HandleImpl(minIndex, maxIndex, initial);
}
private final class HandleImpl extends MaxMessageHandle {
//最小容量在扩缩容索引表中的index
private final int minIndex;
//最大容量在扩缩容索引表中的index
private final int maxIndex;
//当前容量在扩缩容索引表中的index 初始33 对应容量2048
private int index;
//预计下一次分配buffer的容量,初始:2048
private int nextReceiveBufferSize;
//是否缩容
private boolean decreaseNow;
HandleImpl(int minIndex, int maxIndex, int initial) {
this.minIndex = minIndex;
this.maxIndex = maxIndex;
//在扩缩容索引表中二分查找到最小大于等于initial 的容量
index = getSizeTableIndex(initial);
//2048
nextReceiveBufferSize = SIZE_TABLE[index];
}
.......................省略...................
}
}
这里我们看到Netty中用于动态调整ByteBuffer容量的allocHandle的实际类型为MaxMessageHandle。
下面我们来介绍下HandleImpl中的核心字段,它们都和ByteBuffer的容量有关:
•minIndex :最小容量在扩缩容索引表SIZE_TABE中的index。默认是3。
•maxIndex :最大容量在扩缩容索引表SIZE_TABE中的index。默认是38。
•index :当前容量在扩缩容索引表SIZE_TABE中的index。初始是33。
•nextReceiveBufferSize :预计下一次分配buffer的容量,初始为2048。在每次申请内存分配ByteBuffer的时候,采用nextReceiveBufferSize的值指定容量。
•decreaseNow : 是否需要进行缩容。