关于I/O模型的文章比较多,参考多篇后理解上仍然不太满意,终需本身整理一次,也是编写高吞吐量高性能网络接口模块的基础。这里所说的主要针对网络I/O,近几年面对愈来愈大的用户请求量,如何优化这些步骤直接影响接口用户体验。css
1、前言html
I/O模型有几个名词的解释 (比较容易混淆):编程
阻塞与非阻塞:区别在于调用函数时,是否当即返回仍是让线程等待。阻塞模型须要等待操做完成,而非阻塞模型则是当即返回(未准备好则返回一个错误码)。后端
同步与非同步:区别在于网络数据从内核拷贝到用户空间时是否须要用户线程参与等待。缓存
UNIX 网络I/O模型分类有几种 (参考 UNIX Network Programming Volume.1.3rd.Ed):网络
1. Blocking I/O
2. Nonblocking I/O
3. I/O multiplexing (select and poll)
4. Signal driven I/O (SIGIO)
5. Asynchronous I/O (the POSIX aio_functions)异步
它们有两个独特的阶段差异 :socket
1. 等待数据准备好. (阻塞/非阻塞的差别)tcp
2. 数据从内核复制到用户空间. (同步/非同步的差别)ide
理解上面2个阶段,对后面的解释就很容易明白。
2、I/O 模型详解
2.1 Blocking I/O
下图以UDP服务端调用 recvfrom为例描述线程等待过程 (TCP稍微复杂):
主处理线程一直阻塞到有用户数据,而且数据从内核拷贝到用户空间,也就是说主处理线程同一时间只能处理一个用户请求:
伪代码相似以下:
while (1) { Socket clientSock = serverSock.accept(); processRequest(clientSock); }
void processRequest(Socket clientSock)
{
read(...);
write(...);
}
在复杂的网络环境中,常常出现一个“慢速”客户端。也就是说这个客户端数据到达很慢,在TCP网络编程中,若是以此方式等待足够的数据(根据协议定义),则会严重影响到其余客户端的处理等待时间。
由此进行改善的模型就是使用线程池,伪代码以下:
while (1) { Socket clientSock = serverSock.accept(); threadPool.execute(new Task(processRequest(clientSock)); } void processRequest(Socket clientSock) { read(...); write(...); }
线程池模式使得主线程能处理更多的客户端,N个客户端使用M个线程(N:M),主线程不会被一个慢客户端阻塞,可是处理能力仍然是比较有限的。
2.2 Nonblocking I/O
将socket设置为非阻塞模式,告诉内核若是操做不能当即完成则返回一个错误码而不是等待,描述图以下:
以下图,主线程一直尝试调用网络函数,直到数据准备好,该模型的缺陷就是"忙等待",CPU空转浪费系统资源。此模式极少使用,在此仅用于介绍。
2.3 I/O multiplexing (多路复用)
Linux 中提供select/poll系统调用实现,线程阻塞在此方法上,监测多个socket fd(file descriptor)是否就绪,一旦有事件发生则顺序扫描具体是哪一个就绪,但这样作会比较费时,模型以下:
在Linux kernel 2.6+提供了epoll实现,使用驱动方式替代顺序扫描,哪一个fd有事件发生就返回哪个(避免了select/poll的只要有一个发生就扫描所有找出是哪一个,新的内核也可能对这模式实现作了一些优化)。
在ORACLE JDK有以下源码对内核作判断:
package sun.nio.ch;
import ...
public class DefaultSelectorProvider {
public static SelectorProvider create() {
......
if ("Linux".equals(str1)) {
String str2 = (String)AccessController.doPrivileged(new GetPropertyAction("os.version"));
String[] arrayOfString = str2.split("\\.", 0);
if (arrayOfString.length >= 2) {
try {
int i = Integer.parseInt(arrayOfString[0]);
int j = Integer.parseInt(arrayOfString[1]);
if ((i > 2) || ((i == 2) && (j >= 6))) {
return new EPollSelectorProvider();
}
} catch (NumberFormatException localNumberFormatException) {}
}
}
return new PollSelectorProvider();
}
}
关于epoll的实现有专门文章详解,在此不作细说,另一个epoll使用mmap内存映射避免内存复制损耗。
2.4 Asynchronous I/O (异步)
异步I/O告诉内核开始某个操做,在内核完成后(包括数据从内核拷贝到用户空间)通知咱们,以下图:
在ORACLE JDK 1.7+提供异步I/O的实现AsynchronousChannel,相对于原Selector实现I/O复用模式简单不少。
在这几种模型中,只有异步I/O是用户线程不参与数据从内核拷贝到用户空间这个过程。
2.5 几种I/O模型的对比
以下图,它们在 [等待数据] 和 [从内核复制数据到用户空间] 的差别:
从上图看出,只有Asynchronous I/O不参与内核数据复制。
3、实例举例说明
3.1 针对BIO拒绝服务攻击
BIO中Boss线程处理请求后交给链接池Worker处理,但线程池有限,能轻易致使服务异常。好比针对Tomcat默认配置进行HTTP慢速攻击:
声明一个Content-Length为300的POST包,开启300个线程请求,每一个请求中每秒发送1 byte。默认状况下Tomcat的200个线程将爆满,在持续攻击的这300秒内,都没法正常处理其余正经常使用户请求,形成服务异常。
改善方法:将链接器使用NIO处理,修改默认配置protocol为Http11NioProtocol,而且增大合适的线程数,对此类攻击有必定的缓解做用。
3.2 对外接口服务应用
若是使用Java开发对外接口服务,在对响应时延和吞吐量有必定要求的话,一般能够考虑集成Netty (NIO),若是遵循Servlet标准可考虑集成Jetty使用NIO链接器处理。在协议方面,内部服务可选择高效的私有协议和高压缩比的组件(如protobuf / kryo等),对外服务可选择HTTP+JSON。
WEB服务可考虑动静分离,html/css/js/image等文件使用Nginx (sendfile提供更高效的数据传输方式)处理,业务数据才透到后端服务中,后端服务加入缓存等方式减小响应时延。
3.3 关于JDK中一些名词解析
在JDK提供的NIO API与这里介绍的NIO(Nonblocking I/O)模型是不一样的概念,JDK在1.7以前提供的Selector基于select/poll、epoll(Linux kernel > 2.6)实现I/O复用技术的非阻塞IO,JDK1.7之后提供的NIO2.0 API (如AsynchronousServerSocketChannel) 才是真正的异步I/O。
参考资料:
1. http://www.madwizard.org/programming/tutorials/netcpp/5
2. http://www.importnew.com/22019.html
3. 《UNIX Network Programming Volume.1.3rd.Edition》
4.《Netty权威指南》