前言

本节我们探究一下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阻塞导致的线程挂起。
分布式专题-NIO框架之Netty04 - Netty核心之高性能之道_多线程

  • 服务端层面
    分布式专题-NIO框架之Netty04 - Netty核心之高性能之道_java_02
  • 客户端层面
    分布式专题-NIO框架之Netty04 - Netty核心之高性能之道_channel_03

零拷贝

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

内存池

  1. Pooled与UnPooled池化与(非池化)。
  2. UnSafe和非UnSafe(底层读写与应用程序读写)。
  3. Heap和Direct(堆内存与堆外内存)。

高效的Reactor线程模型

  1. Reactor单线程模型
    分布式专题-NIO框架之Netty04 - Netty核心之高性能之道_java_04

  2. Reactor多线程模型

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

  1. 主从Reactor多线程模型
    分布式专题-NIO框架之Netty04 - Netty核心之高性能之道_多线程_06

无锁化的串行设计概念

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

高效的并发编程

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

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

高性能的序列化框架

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

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

灵活的TCP参数配置能力

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

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 的通信模型示意图如下:
分布式专题-NIO框架之Netty04 - Netty核心之高性能之道_java_09
如果你细细分析,一定会发现阻塞 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 反应堆的工作原理图:
    分布式专题-NIO框架之Netty04 - Netty核心之高性能之道_netty_10
    (注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码、发送响应。)

后记

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

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