Netty-消息发送工作机制

上一篇:Netty-发送队列积压导致内存泄漏


业务调用write后,经过ChannelPipeline职责链处理,消息被投递到消息发送缓冲区待发送,调用flush之后会执行真正的发送操作,底层通过调用Java NIO的SocketChannel进行非阻塞write操作,将消息发送到网络上。


   WriteAndFlushTask原理和源码分析
   ChannelOutboundBuffer原理和源码分析
   消息发送源码分析
   消息发送高低位水位控制


WriteAndFlushTask原理和源码分析

     为了尽可能地提升性能,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方法,将发送的消息加入发送队列。
     通过上述分析,得知:

  • 多个业务线程并发调用write相关方法是线程安全的,Netty会将发送消息封装成Task,由I/O线程异步执行。
  • 由于单个Channel由其对应的NioEventLoop线程执行,如果并行调用某个Channel的write操作超过NioEventLoop线程的执行能力,会导致WriteTask积压。
  • NioEventLoop线程需要处理网络读写操作,以及注册到NioEventLoop上的各种Task,两者相互影响,如果网络读写任务较重,或者注册的Task过多,都会导致对方延迟执行,引发性能问题。

ChannelOutboundBuffer原理和源码分析

     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);
	}
}

消息发送源码分析

  • 发送次数限制
         当SocketChannel无法一次将所有待发送的ByteBuf/ByteBuffer写入网络时,需要决定是注册SelectionKey.OP_WRITE在下一次Selector轮询时继续发送,还是在当前位置循环发送,等到所有消息都发送完成再返回。
         如果频繁的注册SelectionKey.OP_WRITE并wakeup Selector会影响性能;但如果TCP的发送缓冲区已满,TCP处于KEEP-ALIVE状态,消息无法发送出去,如果不对循环发送次数进行控制,就会长时间处于发送状态,Reactor线程无法及时读取其他消息和执行排队的Task。Netty采取折中方式,如果本次发送的字节数大于0,但是消息尚未发送完,则循环发送,一旦发现write字节数为0,说明TCP缓冲区已满,此时继续发送没有意义,注册SelectionKey.OP_WRITE并退出循环,在下一个SelectionKey轮询周期继续发送,代码如下(NioSocketChannel类):
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);
    }
}
  • 不同的消息发送策略
         (1)如果待发送消息(ChannelOutboundBuffer)的ByteBuffer数等于1,则通过nioBuffers[0]获取待发送消息的ByteBuffer,通过调用JDK的SocketChannel直接完成消息发送,代码如下(NioSocketChannel类doWrite方法):
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;
		}
	}
}
  • 已发送消息内存释放
         如果消息被发送成功,Netty会释放已发送消息的内存,发送的对象不同,释放策略也不同。
         (1)如果发送对象是JDK的ByteBuffer,则根据发送的字节数计算需要被释放的发送对象个数,代码如下(ChannelOutboundBuffer类):
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;
}
  • 写半包
         如果一次无法将待发送的消息全部写入TCP缓冲区,循环writeSpinCount次仍然未发送完,或者在发送过程中出现了TCP零滑窗(写入的字节数为0),则进入"写半包"模式(目的是在消息发送慢时不要死循环发送,这回阻塞NioEventLoop线程),注册SelectionKey.OP_WRITE到对应的Selector,退出循环,在下一次Selector轮询过程中继续执行write操作,代码如下(NioSocketChannel类doWrite方法):
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);
	}
}

     利用高低水位机制,可以防止在发送队列处于高水位时继续发送消息,导致积压更严重。

你可能感兴趣的:(Netty)