Bootstrap

分布式专题-NIO框架之Netty04 - Netty核心之高性能之道

前言

本节我们探究一下Netty的核心源码。

Netty共计分为六节,分别是:

本节重点:

➢ 深入了解Netty的运行机制
➢ 掌握NioEventLoop、Pipeline、ByteBuf的核心原理
➢ 掌握Netty常见的调优方案

Netty的运行机制

传统RPC调用性能差的三宗罪

  1. 阻塞IO不具备弹性伸缩能力,高并发导致宕机。
  2. Java序列化编码的性能问题。
  3. 传统IO线程模型过多占用CPU资源。

而我们说,体现高性能的三个主题是:

  1. IO模型
  2. 数据协议
  3. 线程模型

那么Netty的高性能是如何体现的?下面总结了8个优势。

异步非阻塞通信

NioEventLoop(io.netty.channel.nio.NioEventLoop) 聚合了多路复用器Selector,可以同时并发处理成百上千个客户端Channel,由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起。
在这里插入图片描述

  • 服务端层面
    在这里插入图片描述
  • 客户端层面
    在这里插入图片描述

零拷贝

  1. 接收和发送ByteBuffer使用堆外内存直接进行Socket读写。
  2. 提供了组合Buffer对象,可依据和多个ByteBuffer对象。
  3. transferTo() 直接将文件缓冲区的数据发送到目标Channel。

内存池

  1. Pooled与UnPooled池化与(非池化)。
  2. UnSafe和非UnSafe(底层读写与应用程序读写)。
  3. Heap和Direct(堆内存与堆外内存)。
高效的Reactor线程模型
  1. Reactor单线程模型
    在这里插入图片描述

  2. Reactor多线程模型

在这里插入图片描述

  1. 主从Reactor多线程模型
    在这里插入图片描述

无锁化的串行设计概念

在这里插入图片描述

高效的并发编程

Netty的高效并发编程主要体现在如下几点:

  1. volatile的大量、正确使用。
  2. CAS和原子类的广泛使用。
  3. 线程安全容器的使用。
  4. 通过读写锁提升并发性能。

高性能的序列化框架

影响序列化性能的关键因素总结如下:

  1. 序列化后的码流大小(网络带宽的占用)。
  2. 序列化&反序列化的性能(CPU资源占用)。
  3. 是否支持跨语言(异构系统的对接和开发语言切换)。

灵活的TCP参数配置能力

在这里插入图片描述

NIO 源码初探

说到源码先得从 Selector 的 open 方法开始看起,java.nio.channels.Selector

   public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

看看 SelectorProvider.provider()做了什么:

 public static SelectorProvider provider() {
        synchronized (lock) {
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            if (loadProviderFromProperty())
                                return provider;
                            if (loadProviderAsService())
                                return provider;
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }

其中 provider = sun.nio.ch.DefaultSelectorProvider.create();会根据操作系统来返回不同的实现类,windows 平台就返回WindowsSelectorProvider;而 if (provider != null) return provider;保证了整个 server 程序中只有一个 WindowsSelectorProvider 对象;

再看看 WindowsSelectorProvider. openSelector():

   public AbstractSelector openSelector() throws IOException {
        return new WindowsSelectorImpl(this);
    }

new WindowsSelectorImpl(SelectorProvider)代码:

WindowsSelectorImpl(SelectorProvider sp) throws IOException { 
	super(sp);
	pollWrapper = new PollArrayWrapper(INIT_CAP);
	wakeupPipe = Pipe.open();
	wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal();
	SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink();
	(sink.sc).socket().setTcpNoDelay(true);		     
	wakeupSinkFd = ((SelChImpl)sink).getFDVal();
	pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
}

其中 Pipe.open()是关键,这个方法的调用过程是:

public static Pipe open() throws IOException {
	return SelectorProvider.provider().openPipe();
}

SelectorProvider 中:

public Pipe openPipe() throws IOException {
	return new PipeImpl(this);
}

再看看怎么 new PipeImpl()的:

PipeImpl(SelectorProvider sp) {
	long pipeFds = IOUtil.makePipe(true);
	int readFd = (int) (pipeFds >>> 32);
	int writeFd = (int) pipeFds;
	FileDescriptor sourcefd = new FileDescriptor();
	IOUtil.setfdVal(sourcefd, readFd);
	source = new SourceChannelImpl(sp, sourcefd);
	FileDescriptor sinkfd = new FileDescriptor();
	IOUtil.setfdVal(sinkfd, writeFd);
	sink = new SinkChannelImpl(sp, sinkfd);
}

其中 IOUtil.makePipe(true)是个 native 方法:

staticnativelong makePipe(boolean blocking);

具体实现:

JNIEXPORT jlong JNICALL
Java_sun_nio_ch_IOUtil_makePipe(JNIEnv *env, jobject this, jboolean blocking)
{

int fd[2];

if (pipe(fd) < 0) {
	JNU_ThrowIOExceptionWithLastError(env, "Pipe failed");
	return 0;
}
if (blocking == JNI_FALSE) {
	if ((configureBlocking(fd[0], JNI_FALSE) < 0)
		|| (configureBlocking(fd[1], JNI_FALSE) < 0)) {
		JNU_ThrowIOExceptionWithLastError(env, "Configure blocking failed");
		close(fd[0]);
		close(fd[1]);
		return 0;
}
}
return ((jlong) fd[0] << 32) | (jlong) fd[1];
}
static int
configureBlocking(int fd, jboolean blocking)
{
	int flags = fcntl(fd, F_GETFL);
	int newflags = blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK);
	return (flags == newflags) ? 0 : fcntl(fd, F_SETFL, newflags); }

正如这段注释所描述的:

/**
*Returns two file descriptors for a pipe encoded in a long.
*The read end of the pipe is returned in the high 32 bits,
*while the write end is returned in the low 32 bits.
*/

High32 位存放的是通道 read 端的文件描述符 FD(file descriptor),low 32 bits 存放的是 write 端的文件描述符。所以取到 makepipe()返回值后要做移位处理。

pollWrapper.addWakeupSocket(wakeupSourceFd, 0);

这行代码把返回的 pipe 的 write 端的 FD 放在了 pollWrapper 中(后面会发现,这么做是为了实现 selector 的 wakeup())

ServerSocketChannel.open()的实现:

public static ServerSocketChannel open() throws IOException { return 			  SelectorProvider.provider().openServerSocketChannel();
}

SelectorProvider:

public ServerSocketChannel openServerSocketChannel() throws IOException { return new ServerSocketChannelImpl(this);

}

可见创建的 ServerSocketChannelImpl 也有 WindowsSelectorImpl 的引用。

public ServerSocketChannelImpl(SelectorProvider sp) throws IOException { 
	super(sp);
	this.fd =	Net.serverSocket(true);
	this.fdVal = IOUtil.fdVal(fd);
	this.state = ST_INUSE;
}

然后通过 serverChannel1.register(selector, SelectionKey.OP_ACCEPT);把 selector 和 channel 绑定在一起,也就是把 new ServerSocketChannel 时创建的 FD 与 selector 绑定在了一起。

到此,server 端已启动完成了,主要创建了以下对象:

  • WindowsSelectorProvider:单例

  • WindowsSelectorImpl 中包含:

    pollWrapper:保存 selector 上注册的 FD,包括 pipe 的 write 端 FD 和 ServerSocketChannel 所用的 FD
    wakeupPipe:通道(其实就是两个 FD,一个 read,一个 write)

再到 Server 中的 run():
selector.select();主要调用了 WindowsSelectorImpl 中的这个方法:


protected int doSelect(long timeout) throws IOException {
	if (channelArray == null)
		throw new ClosedSelectorException();
	this.timeout = timeout; // set selector timeout
	processDeregisterQueue();
	if (interruptTriggered) {
		resetWakeupSocket();
		return 0;
	}
	//Calculate number of helper threads needed for poll. If necessary
	//threads are created here and start waiting on startLock adjustThreadsCount();
	finishLock.reset(); // reset finishLock
	//Wakeup helper threads, waiting on startLock, so they start polling.
	//Redundant threads will exit here after wakeup. startLock.startThreads();
	//do polling in the main thread. Main thread is responsible for
	//first MAX_SELECTABLE_FDS entries in pollArray.
	try {
		begin();
		try {
			subSelector.poll();
		} catch (IOException e) {
			finishLock.setException(e); // Save this exception
		}
		//Main thread is out of poll(). Wakeup others and wait for them if (threads.size() > 0)
			finishLock.waitForHelperThreads(); } 
		finally {
			end();
		}
		//Done with poll(). Set wakeupSocket to nonsignaled for the next run. 			finishLock.checkForException();
		processDeregisterQueue();
		int updated = updateSelectedKeys();
		//Done with poll(). Set wakeupSocket to nonsignaled for the next run. resetWakeupSocket();
		return updated;
}

其中 subSelector.poll()是核心,也就是轮训 pollWrapper 中保存的 FD;具体实现是调用 native 方法 poll0:


private int poll() throws IOException{ // poll for the main thread return poll0(pollWrapper.pollArrayAddress,
	Math.min(totalChannels, MAX_SELECTABLE_FDS),
	readFds, writeFds, exceptFds, timeout);
}

private native int poll0(long pollAddress, int numfds,
	int[] readFds, int[] writeFds, int[] exceptFds, long timeout);
	//These arrays will hold result of native select().
	//The first element of each array is the number of selected sockets.
	//Other elements are file descriptors of selected sockets.

	private final int[] readFds = new int [MAX_SELECTABLE_FDS + 1];//保存发生 read 的 FD 
	private final int[] writeFds = new int [MAX_SELECTABLE_FDS + 1]; //保存发生 write 的 FD 
	private final int[] exceptFds = new int [MAX_SELECTABLE_FDS + 1]; //保存发生 except 的 FD

这个 poll0()会监听 pollWrapper 中的 FD 有没有数据进出,这会造成 IO 阻塞,直到有数据读写事件发生。比如,由于pollWrapper 中保存的也有 ServerSocketChannel 的 FD,所以只要 ClientSocket 发一份数据到 ServerSocket,那么 poll0()就会返回;又由于 pollWrapper 中保存的也有 pipe 的 write 端的 FD,所以只要 pipe 的 write 端向 FD 发一份数据,也会造成 poll0()返回;如果这两种情况都没有发生,那么 poll0()就一直阻塞,也就是 selector.select()会一直阻塞;如果有任何一种情况发生,那么 selector.select()就会返回,所有在 OperationServer 的 run()里要用 while (true) {,这样就可以保证在selector 接收到数据并处理完后继续监听 poll();这时再来看看 WindowsSelectorImpl. Wakeup():

public Selector wakeup() {
	synchronized (interruptLock) {
		if (!interruptTriggered) {
			setWakeupSocket();
			interruptTriggered = true;
		}
	}
	return this;
}
//Sets Windows wakeup socket to a signaled state. 
private void setWakeupSocket() {
	setWakeupSocket0(wakeupSinkFd);
}
private native void setWakeupSocket0(int wakeupSinkFd);
JNIEXPORT void JNICALL
Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass this, jint scoutFd)
{
	/* Write one byte into the pipe */
	const char byte = 1;
	send(scoutFd, &byte, 1, 0);
}

可见 wakeup()是通过 pipe 的 write 端 send(scoutFd, &byte, 1, 0),发生一个字节 1,来唤醒 poll()。所以在需要的时候就可以调用 selector.wakeup()来唤醒 selector。

反应堆 Reactor

现在我们已经对阻塞 I/O 已有了一定了解,我们知道阻塞 I/O 在调用 InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;同样,在调用 ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。阻塞 I/O 的通信模型示意图如下:
在这里插入图片描述
如果你细细分析,一定会发现阻塞 I/O 存在一些缺点。根据阻塞 I/O 通信模型,我总结了它的两点缺点:

  1. 当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些 CPU 时间

  2. 阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。在这种情况下非阻塞式 I/O 就有了它的应用前景。

Java NIO 是在 jdk1.4 开始使用的,它既可以说成“新 I/O”,也可以说成非阻塞式 I/O。下面是 Java NIO 的工作原理:

  1. 由一个专门的线程来处理所有的 IO 事件,并负责分发。

  2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。

  3. 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。下面贴出我理解的 Java NIO 反应堆的工作原理图:
    在这里插入图片描述
    (注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码、发送响应。)

后记

Netty相关的全部演示代码的下载地址:
https://github.com/harrypottry/nettydemo

更多架构知识,欢迎关注本套Java系列文章Java架构师成长之路

;