本文首发于 点击转到原文
周末向往常一样睡了一上午,惆怅了一个中午,下午学了会习,梳理了下Netty的线程模型是如何体现Reactor模式的。继上一篇对一些通信底层IO的C函数学习,这一篇主要是总结Java里对底层IO不同层次的抽象,每一层都为了解决什么问题?为什么Reator模型使得现在Netty处理网络IO时如此高效?带着问题,我们一起来学习。
让我们先回忆一下传统的服务器端同步阻塞I/O处理(也就是BIO,Blocking I/O)的经典编程模型:
class Server {
public static void main() {
ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(8088);
while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
Socket socket = serverSocket.accept();
executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
}
}
static class ConnectIOnHandler implements Runnable {
private Socket socket;
public ConnectIOnHandler(Socket socket){
this.socket = socket;
}
public void run(){
while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){
String someThing = socket.read();//读取数据
if(someThing!=null){
......//处理数据
socket.write()....//写数据
}
}
}
}
}
基本上所有的网络处理程序都有以下基本的处理过程:
其实比我们读大学的时候学的高级多了,起码用到juc的线程池了,简化了使用上多线程的维护成本,降低了线程频繁创建和回收的开销(1.线程池创建线程时有个很大的全局锁。2. 线程堆栈内存分配代价),还有另外一个原因,不过不是多线程,对于socket的accept,read,write都是阻塞操作,单线程处理大量的连接必然,很容易导致此线程挂死在非CPU的IO上,进而CPU资源空闲,这也是合理的使用线程池的意义,通过多线程充分利用多核资源。之所以从上图对代码逻辑的抽象,除了编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求,菜逼容易上手外,很容易总结出BIO模型的不足之处:
所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
以socket.read()为例子:
一个连接里完整的网络处理过程一般分为accept、read、decode、process、encode、send这几步。
由于底层socket进行等待就绪的时候是阻塞的,不知道什么时候函数能返回,就只能“另起炉灶”再去创建线程处理新的连接,我们上一篇介绍过select/poll函数,使得读写操作可以立刻返回,这时候我们可以利用一个线程来轮询注册在seletor上的channel事件,我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。一次select调用会返回所有channel的就绪事件,我们的程序就可以根据不同的事件调用不同的处理器。
在NIO的支撑下基于事件驱动的Reactor应运而生,包括三种模型:
Reactor:负责响应事件,将事件分发给绑定了该事件的Handler处理;
Handler:事件处理器,绑定了某类事件,负责执行对应事件的Task对事件进行处理;
Acceptor:Handler的一种,绑定了connect事件。当客户端发起connect请求时,Reactor会将accept事件分发给Acceptor处理。
dog lea在ppt中展示的代码:
class Reactor implements Runnable {
final Selector selector;
final ServerSocketChannel serverSocket;
Reactor(int port) throws IOException { //Reactor初始化
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(port));
serverSocket.configureBlocking(false); //非阻塞
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT); //分步处理,第一步,接收accept事件
sk.attach(new Acceptor()); //attach callback object, Acceptor
}
public void run() {
try {
while (!Thread.interrupted()) {
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext())
dispatch((SelectionKey)(it.next()); //Reactor负责dispatch收到的事件
selected.clear();
}
} catch (IOException ex) { /* ... */ }
}
void dispatch(SelectionKey k) {
Runnable r = (Runnable)(k.attachment()); //调用之前注册的callback对象
if (r != null)
r.run();
}
class Acceptor implements Runnable { // inner
public void run() {
try {
SocketChannel c = serverSocket.accept();
if (c != null)
new Handler(selector, c);
}
catch(IOException ex) { /* ... */ }
}
}
}
final class Handler implements Runnable {
final SocketChannel socket;
final SelectionKey sk;
ByteBuffer input = ByteBuffer.allocate(MAXIN);
ByteBuffer output = ByteBuffer.allocate(MAXOUT);
static final int READING = 0, SENDING = 1;
int state = READING;
Handler(Selector sel, SocketChannel c) throws IOException {
socket = c; c.configureBlocking(false);
// Optionally try first read now
sk = socket.register(sel, 0);
sk.attach(this); //将Handler作为callback对象
sk.interestOps(SelectionKey.OP_READ); //第二步,接收Read事件
sel.wakeup();
}
boolean inputIsComplete() { /* ... */ }
boolean outputIsComplete() { /* ... */ }
void process() { /* ... */ }
public void run() {
try {
if (state == READING) read();
else if (state == SENDING) send();
} catch (IOException ex) { /* ... */ }
}
void read() throws IOException {
socket.read(input);
if (inputIsComplete()) {
process();
state = SENDING;
// Normally also do first write now
sk.interestOps(SelectionKey.OP_WRITE); //第三步,接收write事件
}
}
void send() throws IOException {
socket.write(output);
if (outputIsComplete()) sk.cancel(); //write完就结束了, 关闭select key
}
}
一个线程需要执行处理所有的accept、read、decode、process、encode、send事件,处理成百上千的链路时性能上无法支撑;
一旦reactor线程意外跑飞或者进入死循环,会导致整个系统通信模块不可用。
当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
代码示例:
Selector[] selectors; // 一个selector对应一个线程
int next = 0;
class Acceptor {
public synchronized void run() { ...
Socket connection = serverSocket.accept();
if (connection != null)
new Handler(selectors[next], connection);
if (++next == selectors.length) next = 0;
}
}
在极个别特殊场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个 Acceptor 线程可能会存在性能不足问题,为了解决性能问题,产生了第三种 Reactor 线程模型 - 主从 Reactor 多线程模型。
我们以服务端线程模型为例,一种比较流行的做法是服务端监听线程和 IO 线程分离,类似于 Reactor 的多线程模型。服务端监听线程来自BossGroup线程池,真正的IO事件的处理由WorkerGroup线程池里的线程负责。
NioEventLoop 是 Netty 的 Reactor 线程,它的职责如下:
Netty 可以在ServerBootstrap类里通过初始化不同线程数的NioEventLoopGroup,可以分别构造出上面三种不同的reactor线程模型
以前只知道上面每一小节的知识点, 一直缺乏把知识梳理成线,现在更加理解了某些东西演变的渊源。很多问题上我们要做到知其然知其所以然,多想想这样做的价值。
http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
Java NIO浅析
Java-NIO-Reactor
netty学习系列二:NIO Reactor模型 & Netty线程模型
Netty 系列之 Netty 线程模型