目录导航
前言
本节我们探究一下Netty的核心源码。
Netty共计分为六节,分别是:
- 01 Java IO 演进之路
- 02 Netty与NIO之前世今生
- 03 Netty初体验之重构RPC框架
- 04 Netty核心之Netty高性能之道
- 05 Netty核心之大动脉 Pipeline与EventLoop
- 06 Netty实战之性能调优与设计模式
本节重点:
➢ 深入了解Netty的运行机制
➢ 掌握NioEventLoop、Pipeline、ByteBuf的核心原理
➢ 掌握Netty常见的调优方案
Netty的运行机制
传统RPC调用性能差的三宗罪
- 阻塞IO不具备弹性伸缩能力,高并发导致宕机。
- Java序列化编码的性能问题。
- 传统IO线程模型过多占用CPU资源。
而我们说,体现高性能的三个主题是:
- IO模型
- 数据协议
- 线程模型
那么Netty的高性能是如何体现的?下面总结了8个优势。
异步非阻塞通信
NioEventLoop(io.netty.channel.nio.NioEventLoop
) 聚合了多路复用器Selector,可以同时并发处理成百上千个客户端Channel,由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起。
- 服务端层面
- 客户端层面
零拷贝
- 接收和发送ByteBuffer使用堆外内存直接进行Socket读写。
- 提供了组合Buffer对象,可依据和多个ByteBuffer对象。
- transferTo() 直接将文件缓冲区的数据发送到目标Channel。
内存池
- Pooled与UnPooled池化与(非池化)。
- UnSafe和非UnSafe(底层读写与应用程序读写)。
- Heap和Direct(堆内存与堆外内存)。
高效的Reactor线程模型
-
Reactor单线程模型
-
Reactor多线程模型
- 主从Reactor多线程模型
无锁化的串行设计概念
高效的并发编程
Netty的高效并发编程主要体现在如下几点:
- volatile的大量、正确使用。
- CAS和原子类的广泛使用。
- 线程安全容器的使用。
- 通过读写锁提升并发性能。
高性能的序列化框架
影响序列化性能的关键因素总结如下:
- 序列化后的码流大小(网络带宽的占用)。
- 序列化&反序列化的性能(CPU资源占用)。
- 是否支持跨语言(异构系统的对接和开发语言切换)。
灵活的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 通信模型,我总结了它的两点缺点:
-
当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些 CPU 时间
-
阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。在这种情况下非阻塞式 I/O 就有了它的应用前景。
Java NIO 是在 jdk1.4 开始使用的,它既可以说成“新 I/O”,也可以说成非阻塞式 I/O。下面是 Java NIO 的工作原理:
-
由一个专门的线程来处理所有的 IO 事件,并负责分发。
-
事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
-
线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。下面贴出我理解的 Java NIO 反应堆的工作原理图:
(注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码、发送响应。)
后记
Netty相关的全部演示代码的下载地址:
https://github.com/harrypottry/nettydemo
更多架构知识,欢迎关注本套Java系列文章:Java架构师成长之路