上一篇:Netty-发送队列积压导致内存泄漏
业务调用write后,经过ChannelPipeline职责链处理,消息被投递到消息发送缓冲区待发送,调用flush之后会执行真正的发送操作,底层通过调用Java NIO的SocketChannel进行非阻塞write操作,将消息发送到网络上。
WriteAndFlushTask原理和源码分析
ChannelOutboundBuffer原理和源码分析
消息发送源码分析
消息发送高低位水位控制
为了尽可能地提升性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行化操作,避免多线程竞争导致性能下降。从表面看,串行化设计的CPU利用率似乎不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部的串行线程设计相比“一个队列多个工作线程”模型性能更忧。传送门:《Netty-什么是串行无锁化》
当用户发起write操作时,Netty会进行判断,如果发现不是NioEventLoop(I/O线程),则将发送消息封装成WriteTask,放入NIoEventLoop的任务队列由NioEventLoop线程执行,代码如下(AbstractChannelHandlerContext类):
private void write(Object msg, boolean flush, ChannelPromise promise){
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor;
if(executor.inEventLoop()){
if(flush){
next.invokeWriteAndFlush(m, promise);
}else{
next.invokeWrite(m, promise);
}
}else{
AbstractWriteTask task;
if(flush){
task = WriteAndFlushTask.newInstance(next, m, promise);
}else{
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}
Netty的NioEventLoop线程内部维护了一个Queue taskQueue,除了处理网络I/O读写操作,同时还负责执行网络读写相关的Task(包含用户自定义的Task),代码如下(SingleThreadEventExecutor类):
public void execute(Runnable task){
if(task == null){
throw new NullPointerException("task");
}
boolean inEventLoop = inEventLoop();
addTask(task);
if(!inEventLoop){ //不是当前线程,任务队列相关操作
startThread();
if(isShutDown() && removeTask(task)){
reject()
}
}
if(!addTaskWakesUp && wakesUpForTask(task)){
wakeup(inEventLoop);
}
}
NioEventLoop遍历taskQueue,执行消息发送任务,代码如下(AbstractWriteTask类):
public final void run(){
try{
if(ESTIMATE_TASK_SIZE_ON_SUBMIT){
ctx.pipeline.decrementPendingOutboundBytes(size);
}
write(ctx, msg, promise);
}finally{
}
}
经过一系列系统处理操作,最终会调用ChannelOutboundBuffer的addMessage方法,将发送的消息加入发送队列。
通过上述分析,得知:
ChannelOutboundBuffer是Netty的发送缓冲队列,是基于链表来管理待发送的消息,定义如下(ChannelOutboundBuffer类):
// 缓存链表中被刷新的第一个元素
private Entry flushedEntry;
// 缓存链表中第一个未刷新的元素
private Entry unFlushedEntry;
// 缓存链表中的尾元素
private Entry tailEntry;
// 刷新但还没有写入到 socket 中的数量
private int flushed;
static final Entry {
private final Handle<Entry> handle;
Entry next;
Object msg;
ByteBuffer[] bufs;
ByteBuffer[] buf;
ChannelPromise promise;
}
在消息发送时会调用ChannelOutboundBuffer的addMessage方法,修改链表指针,将新加入的消息放到尾部,同时更新上一个尾部消息的next指针,指向新加入的消息,代码如下:
private void addMessage(Object msg, int size, ChannelPromise promise){
//1.创建一个新的entry
Entry entry = Entry.newInstance(msg, size, total(msg), promise);
if(tailEntry == null){
//2.判断tailEntry是否为null,如果为null说明链表为空,把flushedEntry置为null
flushedEntry = null;
}else{
//3.如果tailEntry不为空,则把新添加的Entry添加到tailEntry后面
Entry tail = tailEntry;
tail.next = entry;
}
//4.将新添加的Entry设置为链表tailEntry
tailEntry = entry;
if(unflushedEntry == null){
//5.如果unflushedEntry为空,说明没有被刷新的元素。新添加的Entry肯定是未被刷新的,
把点前Entry设置为unflushedEntry(缓存链表中第一个未刷新的元素)
unflushedEntry = entry;
}
}
addMessage成功添加进ChannelOutboundBuffer后,就需要flush刷新到Socket中去,但此方法并不是做刷新到Socket的操作,而是将unflushedEntry的引用转移到flushEntry引用中,表示即将刷新这个flushedEntry。因为Netty提供promise,这个对象可以做取消操作,所以在write后,flush之前需要告诉promise不能做取消操作了。代码如下:
public void addFlush(){
//1.通过 unflushedEntry 获取未被刷新元素 entry
//2.如果entry为null表示没有待刷新的元素,不执行任何操作
Entry entry = unflushedEntry;
//3.如果entry不为null,说明有需要被刷新的元素
if(entry != null){
//4.如果flushedEntry为空说明当前没有正在刷新的任务,把entry设置为flushedEntry刷新的起点
if(flushedEntry == null){
flushedEntry = entry;
}
//5.循环设置Entry, 设置这些Entry的状态为非取消状态,如果设置失败,则把这些entry节点取消并使totalPendingSize减去这个节点的字节大小。
do{
flushed++;
if(!entry.promise.setUncancellable()){
int pending = entry.cancel();
decrementPendingOutboundBytes(pending, false, true);
}
entry = entry.next;
}while(entry != null);
unflushedEntry = null;
}
}
调用完addFlush方法后,Channel会调用flush0方法做真正的刷新。
在消息发送时,调用current方法,获取待发送的原始信息(flushedEntry):
public Object current(){
Entry entry = flushedEntry;
if(entry == null){
return null;
}
return entry.msg;
}
消息发送成功,则调用ChannelOutboundBuffer的remove方法,将已发送消息从链表中删除,同时更新待发送的消息,代码如下:
private void removeEntry(Entry e){
//1.如果flushed为0表示链表中所有flush数据已经发送到socket中,把flushedEntry置为null
if(--flushed == 0){
flushedEntry = null;
if(e == tailEntry){
//说明链表为空,则把tailEntry和unflushedEntry都置为空
tailEntry = null;
unflushedEntry = null;
}
}else{
//把flushedEntry置为下一个节点
flushedEntry = e.next;
}
}
将已发送的消息从链表中删除后,释放ByteBuf资源,如果是基于内存池分配的ByteBuf,则重新返回池中重用:如果是非池模式,则清空相关资源,等待GC回收,代码如下(ChannelOutboundBuffer):
public boolean remove(){
if(!e.cancelled){
ReferenceCountUtil.safeRelease(msg);
safeSuccess(promise);
}
}
protected void doWrite(ChannelOutboundBuffer in) throws Exception{
SocketChannel ch = javaChannel();
//获取自旋的次数,默认16
int writeSpinCount = config().getWriteSpinCount();
do{
//消息发送代码
} while(writeSpinCount > 0){
//如果自旋16次还没完成flush,则创建一个任务放进队列中执行
incompleteWrite(writeSpinCount < 0);
}
}
protected final void incompleteWrite(boolean setOpWrite) {
if(setOpWrite){
this.setOpWrite();
}else {
this.clearOpWrite();
this.eventLoop().execute(this.flushTask);
}
}
for(;;){
ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
int nioBufferCnt = in.nioBufferCount();
switch(nioBufferCnt){
case 1: {
ByteBuffer buffer = nioBuffers[0];
int attemptedBytes = buffer.remaining();
final int localWrittenBytes = ch.write(buffer);
if(localWrittenBytes <= 0){
incompleteWrite(true);
return;
}
adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite);
in.removeBytes(localWrittenBytes);
--writeSpinCount;
break;
}
}
}
(2)如果待发送消息的ByteBuffer数大于1,则调用SocketChannel的批量发送接口,将nioBuffers数组写入TCP发送缓冲区,代码如下:
default:{
long attemptedBytes = in.nioBufferSize();
final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
if(localWrittenBytes <= 0){
incompleteWrite(true);
return;
}
adjustMaxBytesPerGatheringWrite((int)attemptedBytes, (int)localWrittenBytes, maxBytesPerGatheringWrite);
in.removeBytes(localWrittenBytes);
--writeSpinCount;
break;
}
(3)如果待发送的消息包含的JDK原生ByteBuffer数为0,则调用父类AbstractNioByteChannel的doWrite0方法,将Netty的ByteBuffer发送套TCP缓冲区,代码如下(AbstractNioByteChannel类):
private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception{
if(msg instanceof ByteBuf){
ByteBuf buf = (ByteBuf) msg;
if(!buf.isReadable()){
in.remove();
return 0;
}
final int localFlushedAmount = doWriteBytes(buf);
if(localFlushedAmount > 0){
in.progress(localFlushedAmount);
if(!buf.isReadable()){
in.remove();
}
return 1;
}
}
}
public void removeBytes(long writtenBytes){
while(true){
//点前flushedEntry节点
Object msg = current();
if(!(msg instanceof ByteBuf)){
assert writtenBytes == 0;
}
final ByteBuf buf = (ByteBuf) msg;
final int readerIndex = buf.readerIndex();
final int readableBytes = buf.writerIndex() -readerIndex;
//比较可读字节数和发送的总字节数,如果发送的字节数大于可读的字节数,说明当前ByteBuffer已经被完全发送出去,
//flushedEntry写完
if(readableBytes <= writtenBytes ){
if(writtenBytes != 0){
//更新进度
progress(readableBytes);
writtenBytes -= readableBytes;
}
//删除flushedEntry指向的节点,向后移动flushedEntry
remove();
}else{
//该flushedEntry没有写完,只更新进度
if(writtenBytes != 0){
buf.readerIndex(readerIndex + (int) writtenBytes);
progress(writtenBytes);
}
break;
}
}
clearNioBuffers();
}
(2)如果发送的对象是Netty的ByteBuf,则通过判断当前ByteBuf的isReadable来获取消息发送结果,如果发送完成,则调用ChannelOutboundBuffer的remove方法删除并释放ByteBuf,代码如下(AbstractNioByteChannel类doWriteInternal):
final int localFlushedAmount = doWriteBytes(buf);
if(localFlushedAmount > 0){
in.progress(localFlushedAmount);
if(!buf.isReadable()){
in.remove();
}
return 1;
}
if(localWrittenBytes <= 0){
incomplete(true);
return;
}
注册SelectionKey.OP_WRITE相关代码如下(AbstractNioByteChannel类setOpWrite方法):
protected final void setOpWrite(){
final SelectionKey key = selectionKey();
if(!key.isValid()){
return;
}
final int interestOps = key.interestOps();
if((interestOps & SelectionKey.OP_WRITE) == 0){
key.interestOps(interestOps | SelectionKey.OP_WRITE);
}
}
为了对发送速度和消息积压数进行控制,Netty提供了高低水位机制,当消息队列中积压的待发送消息总字节数到达高水位时,修改Channel的状态为不可写,代码如下(ChannelOutboundBuffer类):
private void incrementPendingOutboundBytes(long size, boolean invokeLater){
if(size == 0){
return;
}
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
if(newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()){
setUnwrite(invokeLater);
}
}
修改Channel状态后,调用ChannelPipeline发送通知事件,业务可以监听该事件及时获取链路可写状态,代码如下(ChannelOutboundBuffer类):
private void fireChannelWritabilityChanged(boolean invokeLater){
final ChannelPipeline pipeline = channel.pipeline();
if(invokeLater){
Runnable task = fireChannelWritabilityChangedTask;
if(task == null){
fireChannelWritabilityChangedTask = task = new Runnable(){
@Override
public void run(){
pipeline.fireChannelWritabilityChanged();
}
};
}
channel.eventLoop().execute(task);
} else{
pipeline.fireChannelWritabilityChaned();
}
}
当消息发送完成后,对低水位进行判断,如果当前积压待发送字节数到达或低于低水位,则修改Channel状态为可写,并发送通知事件,代码如下:
private void decreamentPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability){
if(size == 0){
return;
}
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
if(notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark()){
setWritable(invokeLater);
}
}
利用高低水位机制,可以防止在发送队列处于高水位时继续发送消息,导致积压更严重。