在上一篇文章里我们介绍了 tomcat io 主要包含那些 items,在这里我们主要介绍tomcat io 的基础-多路复用。tomcat 服务器(tomcat7以上)默认使用 java NIO 模型,NIO 不仅仅需要 java 语言上的支持,同时还离不开各种操作系统对于多路复用的支持(linux,windows,mac 等等),所以 tomcat的NIO 是建立在操作系统基础之上的。
对于 linux 操作系统,IO 多路复用使用的是 epoll 方式,对于 windows 操作系统中 IO 多路复用使用的是 iocp 方式,对于 mac 操作系统 IO 多路复用使用的是 kqueue 方式。由于对于 tomcat 服务器来说基本主要部署在 linux 操作系统上,所以我们主要介绍 linux 的 epoll 模型。epoll 是 event poll 的简称,在 linux 内核版本 2.6 开始支持,所以如果你的 tomcat 服务器如果希望默认使用 NIO,除了自己版本在 tomcat7 以上之外,还需要部署在 linux 内核版本大于 2.6 的操作系统之上。
在介绍 epoll 多路复用之前,我们先简单描述一下传统 IO,也就是 BIO(block IO),从而和 epoll IO 有一个大致的对比。在 tomcat6 和之前的版本默认都是使用的 BIO 模型,从 linux 操作系统的角度看,并没有利用 epoll 模型,BIO 模型大致如下:
从 linux 操作系统角度看有一个 socket 监听在某个端口,等待客户端的连接请求,我们称运行监听 socket 的线程为 acceptor thread 。
有多个客户端的连接请求过来,每个请求经过3次握手,监听线程 accept 请求,为每个连接请求在 server 端创建 socket 。
对于服务端的 socket 会尝试读取客户端发送的数据,如果客户端不发送数据,那么这个读取操作会一直阻塞,一直到有数据发送过来。
从操作系统的角度看,当客户端没有数据发送的时候,服务端这个读取数据的线程或进程就会进入 TASK_INTERRUPTIBLE 状态,也就是平时常用的 top 命令中的 S 状态。在操作系统的等待队列里,等待有客户端数据到来,然后唤醒这个读取线程或者进程来读取数据。
基于以上,一般对于每个连接请求的服务端 socket 都会创建一个线程来读取并操作数据。所以连接请求,服务端 socket ,服务端线程是一一对应的关系。
对于上述模型,在并发连接比较少的情况下没有问题。如果并发连接数量巨大,那么意味着操作系统要创建巨大数量的线程来支持并发,同时也需要对这些线程进行调度和上下文切换。这些大量多线带来的工作量对于操作系统来说都是巨大的负担,所以这种 IO 模型很难支持大量的并发。
为了解决传统 IO 模型带来的问题,linux 内核(2.6版本及以上)提供了 epoll 模型,epoll 是event poll ,这种 IO 模型是基于事件的非阻塞 IO 。从 linux 操作系统的角度看,epoll 模型大致如下:
从 linux 操作系统角度看有一个 socket 监听在某个端口,等待客户端的连接请求,我们称运行监听 socket 的线程为 acceptor thread 。
有多个客户端的连接请求过来,每个请求经过3次握手,监听线程 accept 请求,为每个连接请求在 server 端创建 socket 。
对于服务端的 socket 来说,linux 操作系统会为其注册一系列感兴趣的事件(例如读事件,当数据就绪可读的时候触发。写事件,当 buffer 有缓冲,可以写数据的时候触发)。这样所有的服务端 socket 可以形成一个 intreast list 。
对于 individual 的 server 端 socket 来说,如果客户端发送了数据,linux 操作系统会触发注册的读事件,然后会把这个 socket 加入一个就绪列表中,我们称之为 ready list。对于 ready list 之中的 socket 是一定可以读到数据的,因为已经触发了读事件,即数据就绪可读。
一般我们会有一个用户空间的线程或者进程来运行 java NIO 的 API ,在这个线程里通过来轮询 ready list ,如果 list 里有 socket 则进行读取数据和操作数据。如果 ready list 没有触发事件的 socket ,对于操作系统来说,该线程会进入 TASK_INTERRUPTIBLE 状态( top 命令中的 S 状态),在操作系统的等待队列里,等待 ready list 有数据,然后唤醒这个读取线程读取并操作数据。
基于上述,epoll 用少量的线程就可以支持大量的连接请求,从而避免了传统 IO 的问题。
综合上述的传统 IO 和 epoll 模式下的 IO ,我们总结如下:
传统 IO 对于每个连接都需要操作系统分配一个单独的线程(或者进程),在高并发下的大量线程(或者进程)会给操作系统带来巨大负担,所以传统 IO 对高并发支持不友好。
epoll 模型下的 IO 会对 socket 注册感兴趣的事件(读写事件等),当事件发生的时候把就绪的 socket 放到 ready list 里,这个列表里的 socket 一定可以读写数据。对于检查事件是否发生,把 socket 放入就绪列表里等任务都是由操作系统内核完成的,不由用户空间的应用程序做,最大限度的利用了操作系统。
用户空间的线程利用包装好的 java NIO API 发起对 ready list 轮询的系统调用,这样少量的用户空间线程就可以完成对大量连接请求的支持,所以 epoll 模式下的 IO 对高并发的支持是友好的。
对于户空间线程我们一般称之为事件轮询线程,tomcat NIO 中一般叫 poller thread 。当 poller thread 发现有可用 socket 的时候一般不会自己处理读取操作数据,而是把 socket 给预先定义好的线程池中的线程来读取数据,操作数据。
对于 tomcat 来说,上面的线程池就是 io 线程池,也就是我们平时配置的 tomcat 线程池,这里面的线程读取数据,运行 servlet 。
对于 epoll 下的 tomcat io 线程池来说,数据的读取是同步的。从操作系统的角度来说,NIO API 发起读数据的系统调用,这个线程会一直等到数据读完返回。只是这个时候一定有数据可读,不必等待过长的时间,所以 tomcat NIO 是同步非阻塞 IO。
目前先写到这里,下一篇文章里我们继续介绍 tomcat NIO 中主要涉及的类和这些类的作用。
本文分享自微信公众号 - TA码字()。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。