业务高并发离不开,Netty\Redis\Zookeeper等分布式高性能工具,涉及到高并发肯定离不开频繁的IO读写操作,IO底层原理是这些高并发工具的一个基石;
当我们聊IO底层原理的时候我们应该想到哪些关键问题节点?
基于这些问题go on;
首先我们应该明白一点就是在进行一次read/write(后续rw简写)系统调用或socket套接字调用,并不是直接进行物理设备和内存之间的数据置换;他们都是通过一层缓冲区进行数据置换的;
调用操作系统的read,是把数据从系统内核缓冲区复制到进程缓冲区;而w调用是把数据从进程缓冲区复制到内核缓存区;
为何要设置这么多的缓存区呢?
缓存区的目的是为了减少频繁的与设备之间的物理交换,我们清楚外部物理设备的直接读写,涉及到操作系统的中断。发生中断时要保留进程的数据和状态信息,而结束中断后要恢复。为了减少这种中断带来底层系统的时间和性能损耗,于是出现了内存缓冲区;
有了内核缓冲区,操作系统会对内核缓存区进行监控,等待缓存区数据达到一定大小时,才进行IO设备的中断处理,集中执行物理设备的IO操作,这种机制提升系统性能;用户进程缓存区,则是将用户进程的IO操作集中处理;用户进程的IO操作实际上是用户进程缓存区和内核缓冲区之间的数据置换;
read一个socket套接字的流程如下:
同步阻塞IO(Blocking IO)
优点:程序开发简单,阻塞等待数据期间,用户线程挂起;阻塞期间,用户线程基本不会占用CPU;
缺点:每个连接会开启一个独立线程,维护一个连接的IO;并发量高的情况下,维护大量的网络连接,内存、线程切换开销巨大;不适合高并发。
同步非租塞IO(None Blocking IO)
优点:每次发起IO系统调用,在内核等待数据过程中可以立即返回。用户线程并不会阻塞,实时性好;
缺点:用户进程要想拿到数据,需要不断轮询,这将占用大量cpu;这里的同步非租塞IO不是java的NIO(NIO采用的IO多路复用);
IO多路复用(IO Multiplexing)
查询IO数据就绪状态的轮询过程交给了select/epoll调用完成,通过这种调用方式,一个进程可以监视多个文件描述符(文件句柄),一旦某个文件描述符数据准备就绪(内核缓存区可读、写),内核就将就绪的状态返回给用户进程,随后用户程序进行就绪状态数据的IO rw系统调用;与NIO模型相比,IO多路复用模型涉及两种系统调用,一种是select/epoll(就绪查询),另一种是IO操作;NIO只有IO操作;
流程:
优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接。系统不必创建大量的线程,也不必维护这些线程,大大减小系统开销;
缺点:本质上select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,这个读写过程是阻塞的。
异步IO(Asynchronous IO)
用户线程通过系统调用,向内核注册某个IO操作。内核在整个IO操作(从网卡读取数据到内核缓冲区、数据复制到用户缓冲区)完成后,通知用户程序,用户程序执行后续业务操作;内核的整个IO操作过程中用户程序不阻塞;
优点:在内核等待数据和复制两个阶段,用户线程都不阻塞。用户线程接收内核IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。吞吐量最大;
缺点:应用程序需要进行事件注册和接收,其余工作交给系统内核,底层系统内核需要支持;Linux 2.6版本才引入异步IO,目前不完善。
之前就说了java NIO是基础IO多路复用模型进行通信的,New IO也有人称为非阻塞IO(Non-Block IO),弥补了老式IO(OIO)面向流阻塞的不足,它是面向缓冲区的;Java NIO被应用到文件读写和网络通信中;
提供了三个核心组件:
与Java OIO相比,java NIO网络通信编程特点大致如下:
(1)、在NIO中,服务器接收新连接的工作,是异步进行的。不像java的OIO那样,服务器监听连接是同步、阻塞的。NIO可以通过选择器(多路复用器)查看IO事件的就绪状态,后续只需不断进行轮询选择器中的选择键集合,选择新到来的连接;
(2)、NIO中SocketChannel通道的读写操作都是异步的。如果没有可读写的数据,负责IO的线程不会阻塞同步等待,线程可以处理其他的连接通道;
(3)、NIO中选择器可以同时处理成千上万个客户端连接,性能会随着客户端的增加而线性下降;
现状:高并发通信中间件Netty、Nginx、Redis等都是基于Reactor反应器模式实现的;
背景:在Reactor反应器模式出现之前;Java 原始的OIO编程,网络服务器程序,是通过一个while循环,不断的监听端口是否有新的连接。如果有才调用处理函数处理。示例代码
while(true){
socket=accept(); //阻塞,接收连接
hadler(socket); //读取数据,业务处理,写入结果
}
这种方法问题在一个连接handler(socket) 没有处理完,那么后面的连接请求没法被接收,于是后面的请求统统会被阻塞住,服务器的吞吐量就太低了。
进而引入了Connection Per Thread(一线程处理一连接)模式。代码逻辑如下:
class ConnectionPerThread implements Runnable{
public void run(){
try{
ServerSocket serverSocket=new ServerSocket(SOCKET_SERVER_PORT);
while(!Thread.interrupted()){
Socket socket = serverSocket.accept();
//接收一个连接后,为socket连接,新建一个专属的处理器对象
Handler handler=new Handler(socket);
//创建新线程专门处理这个连接
new Thread(handler).start();
}
}catch(Exception e){
//异常处理
}
}
}
static class Handler implements Runnable{
final Socket socket;
Handler(Socket s){
this.socket=s;
}
while(true){
try{
byte[] input=new byte[1024];
//读取数据源
socket.getInputStream().read(input);
//业务逻辑处理
....
byte[] output=null;
//写入结果
socket.getOutputStream().write(outPut);
}catch(Exception e){
//异常处理
}
}
}
解决了前面新连接被严重阻塞的问题,在一定程度上,极大提高了服务器的吞吐量;但是对于成千上万的连接,需要耗费大量线程资源(创建、销毁、线程切换代价太高);所以引入了Reactor反应器模式,对线程数据量进行控制,做到一个线程可以处理大量的连接;
反应器模式Reactor由反应器线程、Handler处理器两大角色组成:
单线程的Reactor反应器模式
Reactor选择器需要用到SelectionKey提供的两个方法:
通信服务端的Reactor反应器和处理器在同一个线程中:
public class Reactor implements Runnable {
private final ServerSocketChannel ssc;
private final Selector selector;
public TCPReactor(int port) throws IOException {
selector = Selector.open();
ssc = ServerSocketChannel.open();
InetSocketAddress addr = new InetSocketAddress(port);
// 在ServerSocketChannel绑定监听端口
ssc.socket().bind(addr);
// 设置ServerSocketChannel为非阻塞
ssc.configureBlocking(false);
// ServerSocketChannel向selector注册一个OP_ACCEPT事件,然后返回通道的key
SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT);
// 核心1:给选择key绑定一个附加的Acceptor处理器
sk.attach(new Acceptor(selector, ssc));
}
@Override
public void run() {
// 在线程被中断前持续运行
while (!Thread.interrupted()) {
System.out.println("Waiting for new event on port:"+ssc.socket().getLocalPort() + "...");
try {
// 若沒有事件就就绪则不往下执行
if (selector.select() == 0)
continue;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 取得所有已就绪事件的key集合
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
// 根據事件的key進行調度
dispatch((SelectionKey) (it.next()));
// 处理完后从就绪事件列表中移除
it.remove();
}
}
}
/*
* name: dispatch(SelectionKey key)
* description: 調度方法,根據事件绑定的对象新开线程
*/
private void dispatch(SelectionKey key) {
// 核心2:根据事件之key绑定的对象新开线程
Runnable r = (Runnable) (key.attachment());
if (r != null)
r.run();
}
class AcceptorHandler implements Runnable {
private final ServerSocketChannel ssc;
private final Selector selector;
// 核心3:使用同一个选择器注册其他事件
public Acceptor(Selector selector, ServerSocketChannel ssc) {
this.ssc=ssc;
this.selector=selector;
}
@Override
public void run() {
try {
// 接受client连接请求
SocketChannel sc= ssc.accept();
System.out.println(sc.socket().getRemoteSocketAddress().toString() + " is connected.");
if(sc!=null) {
// 设置为非阻塞
sc.configureBlocking(false);
// SocketChannel向selector注册一个OP_READ事件,然后返回该通道的key
SelectionKey sk = sc.register(selector, SelectionKey.OP_READ);
// 使一个阻塞住的selector操作立即返回
selector.wakeup();
// 给定key一个附加的IOHandler处理象
sk.attach(new IOHandler(sk, sc));
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class IOHandler implements Runnable {
private final SelectionKey sk;
private final SocketChannel sc;
int state;
public TCPHandler(SelectionKey sk, SocketChannel sc) {
this.sk = sk;
this.sc = sc;
state = 0; // 初始狀態設定為READING
}
@Override
public void run() {
try {
if (state == 0)
read(); // 讀取網絡數據
else
send(); // 發送網絡數據
} catch (IOException e) {
System.out.println("[Warning!] A client has been closed.");
closeChannel();
}
}
private void closeChannel() {
try {
sk.cancel();
sc.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
private synchronized void read() throws IOException {
// non-blocking下不可用Readers,因為Readers不支援non-blocking
byte[] arr = new byte[1024];
ByteBuffer buf = ByteBuffer.wrap(arr);
int numBytes = sc.read(buf); // 讀取字符串
if(numBytes == -1)
{
System.out.println("[Warning!] A client has been closed.");
closeChannel();
return;
}
String str = new String(arr); // 將讀取到的byte內容轉為字符串型態
if ((str != null) && !str.equals(" ")) {
process(str); // 邏輯處理
System.out.println(sc.socket().getRemoteSocketAddress().toString()
+ " > " + str);
state = 1; // 改變狀態
sk.interestOps(SelectionKey.OP_WRITE); // 通過key改變通道註冊的事件
sk.selector().wakeup(); // 使一個阻塞住的selector操作立即返回
}
}
private void send() throws IOException {
// get message from message queue
String str = "Your message has sent to "
+ sc.socket().getLocalSocketAddress().toString() + "\r\n";
ByteBuffer buf = ByteBuffer.wrap(str.getBytes()); // wrap自動把buf的position設為0,所以不需要再flip()
while (buf.hasRemaining()) {
sc.write(buf); // 回傳給client回應字符串,發送buf的position位置 到limit位置為止之間的內容
}
state = 0; // 改變狀態
sk.interestOps(SelectionKey.OP_READ); // 通過key改變通道註冊的事件
sk.selector().wakeup(); // 使一個阻塞住的selector操作立即返回
}
void process(String str) {
// do process(decode, logically process, encode)..
// ..
}
}
}
通信服务端示例代码的关键:
优点:相对于传统的一连接一线程,是基于Java NIO实现,反应器模式不用再启动成千上万的线程了。
缺点:当其中某个handler处理器阻塞时,也会导致其他所有handler无法执行;handler不仅仅负责输入、输出业务处理,还包括建 立连接监听的AcceptorHandler处理器,问题很严重;其次也不能充分利用多核心资源;
多线程的Reactor反应器模式
多线程池的Reactor反应器演进在两个方面:
总体设计思路如下:
实例代码参考https://www.cnblogs.com/crazymakercircle/p/9833847.html 5.3. 多线程Reactor的参考代码