最近看到篇好文章《IO多路复用》,记得早期学习时,也去探索过select、poll、epoll的区别,但后来也是没有及时记录总结,也忘记了,学习似乎就是在记忆与忘记中徘徊,最后在心中留下的火种,是熄灭还是燎原就看记忆与忘记间的博弈
socket与io一对兄弟,有socket地方必然有io,io数据也大多来源于socket,回顾这两方面的知识点,大致梳理一下
socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议
除了TCP协议(三次握手、四次挥手)知识点外,再就是各阶段与java api对应的方法
三次握手关联到两个方法:服务端的listen()与客户端的connect()
两个方法的共通点:TCP三次握手都不是他们本身完成的,握手都是内核完成的,他们只是通知内核,让内核自动完成三次握手连接
不同点:connect()是阻塞的,listen()是非阻塞的
三次握手的过程细节:
•第一次握手:客户端发送 SYN 报文,并进入 SYN_SENT 状态,等待服务器的确认;•第二次握手:服务器收到 SYN 报文,需要给客户端发送 ACK 确认报文,同时服务器也要向客户端发送一个 SYN 报文,所以也就是向客户端发送 SYN + ACK 报文,此时服务器进入 SYN_RCVD 状态;•第三次握手:客户端收到 SYN + ACK 报文,向服务器发送确认包,客户端进入ESTABLISHED 状态。待服务器收到客户端发送的 ACK 包也会进入ESTABLISHED 状态,完成三次握手
io
IO中常听到的就是同步阻塞IO,同步非阻塞IO,异步非阻塞IO;也就是同步、异步、阻塞、非阻塞四个词组合体,可从名字上看就不大对,既然同步,应该都是阻塞,怎么会有同步非阻塞?不知道哪位先贤的学习总结却流传深远
还有些把non-blocking IO与NIO都混淆了
对于IO模型,最正统的应该来自Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models”
•Blocking I/O•Non-Blocking I/O•I/O Multiplexing•Asynchronous I/O
在理解这四种常见模型前,先简单说下linux的机制,可以更方便理解IO,在《堆外内存》[1]中提到linux的处理IO流程以及Zero-Copy技术,算是IO模型更深入的知识点
应用程序发起的一次IO操作实际包含两个阶段:
•1.IO调用阶段:应用程序进程向内核发起系统调用•2.IO执行阶段:内核执行IO操作并返回
•2.1. 准备数据阶段:内核等待I/O设备准备好数据•2.2. 拷贝数据阶段:将数据从内核缓冲区拷贝到用户空间缓冲区
对于阻塞与非阻塞,讲的是用户进程/线程与内核之间的切换;当内核数据没有准备好时,用户进程就得挂起
对于同步与异步,重点在于执行结果是否一起返回,IO就是指read,send是否及时获取到结果
大致分析一下,同步异步、阻塞非阻塞的两两组合其实是把宏观与微观进行了穿插,从应该程序角度获取结果是同步或异步,而IO内部再细分了阻塞与非阻塞
由上文所述:IO操作分两个阶段 1、等待数据准备好(读到内核缓存) ,2、将数据从内核读到用户空间(进程空间)。一般来说1花费的时间远远大于2。1上阻塞2上也阻塞的是同步阻塞IO;1上非阻塞2阻塞的是同步非阻塞IO,NIO,Reactor就是这种模型;1上非阻塞2上非阻塞是异步非阻塞IO,AIO,Proactor就是这种模型。
同步阻塞IO(Blocking IO)
因为用户态被阻塞,等待内核数据的完成,所以需要同步等待结果
同步非阻塞IO(Non-blocking IO)
用户态与内核不再阻塞了,但需要不停地轮询获取结果,浪费CPU,这方式还不如BIO来得痛快
IO多路复用
Reactor模式,意为事件反应,操作系统的回调/通知可以理解为一个事件,当事件发生时,进程/线程对该事件作出反应。Reactor模式也称作Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程/线程
对于IO多路复用,里面再有的细节就是一个优化过程,select,poll,epoll
AIO
Proactor模式,Reactor可理解为“来了事件我通知你,你来处理”,而Proactor是“来了事件我处理,处理完了我通知你”。这里“我”是指操作系统,“你”就是用户进程/线程
四种模型对比
对于IO模型的优化进程,一是操作系统的支持,减少系统调用,用户态与内核的切换;二是机制的变换,从命令式到响应性的转变
高性能架构
只温习Socket/IO知识太无趣了,我们要温故知新,升华一下,从架构角度谈一谈
从常规服务处理业务流程讲:request -> process -> response
站在架构师的角度,当然需要特别关注高性能架构的设计。高性能架构设计主要集中在两方面:
1.尽量提升单服务器的性能,将单服务器的性能发挥到极致。2.如果单服务器无法支撑性能,设计服务器集群方案。
除了以上两点,最终系统能否实现高性能,还和具体的实现及编码相关。但架构设计是高性能的基础,如果架构设计没有做到高性能,则后面的具体实现和编码能提升的空间是有限的。形象地说,架构设计决定了系统性能的上限,实现细节决定了系统性能的下限。
单服务器高性能的关键之一就是服务器采取的并发模型,并发模型有如下两个关键设计点:
•服务器如何管理连接•服务器如何处理请求
以上两个设计点最终都和操作系统的 I/O 模型及进程模型相关。
•I/O 模型:阻塞、非阻塞、同步、异步•进程模型:单进程、多进程、多线程
传统模式PPC&TPC
PPC,即Process Per Connection,为每个连接都创建一个进程去处理。此模式实现简单,适合服务器连接不多的场景,如数据库服务器
TPC,即Thread Per Connection,为每个连接都创建一个线程去处理。线程创建消耗少,线程间通信简单
这两种都是传统的并发模式,使用于常量连接的场景,如数据库(常量连接海量请求),企业内部(常量连接常量请求)
至于是进程还是线程,大多与语言特性相关,Java语言由于JVM是一个进程,管理线程方便,故多使用线程,如Netty。C语言进程和线程均可使用,如Nginx使用进程,Memcached使用线程。
不同并发模式的选择,还要考察三个指标,分别是响应时间(RT),并发数(Concurrency),吞吐量(TPS)。三者关系,吞吐量=并发数/平均响应时间。不同类型的系统,对这三个指标的要求不一样。
三高系统,比如秒杀、即时通信,不能使用
三低系统,比如ToB系统,运营类、管理类系统,一般可以使用
高吞吐系统,如果是内存计算为主的,一般可以使用,如果是网络IO为主的,一般不能使用。
Reactor&Proactor
对于传统方式,显示只能适合常量连接常量请求,不能适应互联网场景
如双十一场景下的海量连接海量请求;门户网站的海量连接常量请求;
引入线程池也是一种手段,但也不能根本解决,如常量连接海量请求的中间件场景,线程虽然轻量但也有得消耗资源,终有上限
Reactor,意为事件反应,操作系统的回调/通知可以理解为一个事件,当事件发生时,进程/线程对该事件作出反应。Reactor模式也称作Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程/线程。
可以看到,I/O多路复用技术是Reactor的核心,本质是将I/O操作给剥离出具体的业务进程/线程,从而能够进行统一管理,使用select/epoll去同步管理I/O连接。
Reactor模式的核心分为Reactor和处理资源池。Reactor负责监听和分配事件,池负责处理事件
如何高性能呢?就得IO多路复用,配合上进程、线程组合,就有:
•单Reactor 单进程 / 线程•单Reactor 多线程•多Reactor 单进程 / 线程(此实现方案相比“单 Reactor单进程”方案,既复杂又没有性能优势,所以很少实际应用)•多Reactor 多进程 / 线程
单Reactor单线程
在这种模式中,Reactor、Acceptor和Handler都运行在一个线程中
单 Reactor 单进程的模式优点就是很简单,没有进程间通信,没有进程竞争,全部都在同一个进程内完成。
但其缺点也是非常明显,具体表现有:
•只有一个进程,无法发挥多核 CPU 的性能;只能采取部署多个系统来利用多核 CPU,但这样会带来运维复杂度,本来只要维护一个系统,用这种方式需要在一台机器上维护多套系统。•Handler 在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈
因此,单 Reactor 单进程的方案在实践中应用场景不多,只适用于业务处理非常快速的场景,目前比较著名的开源软件中使用单 Reactor 单进程的是 Redis
在redis中如果value比较大,redis的QPS会下降得很厉害,有时一个大key就可以拖垮
现在redis6.0版本后,已经变成多线程模型,对于大value的删除性能就提高了
单Reactor多线程
在这种模式中,Reactor和Acceptor运行在同一个线程,而Handler只有在读和写阶段与Reactor和Acceptor运行在同一个线程,读写之间对数据的处理会被Reactor分发到线程池中
单Reator多线程方案能够充分利用多核多 CPU的处理能力,但同时也存在下面的问题:
•多线程数据共享和访问比较复杂。例如,子线程完成业务处理后,要把结果传递给主线程的 Reactor 进行发送,这里涉及共享数据的互斥和保护机制。以 Java 的 NIO 为例,Selector 是线程安全的,但是通过 Selector.selectKeys() 返回的键的集合是非线程安全的,对 selected keys 的处理必须单线程处理或者采取同步措施进行保护。•Reactor 承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈
多Reactor多线程
为了解决单 Reactor 多线程的问题,最直观的方法就是将单Reactor改为多Reactor
目前著名的开源系统 Nginx 采用的是多Reactor多进程,采用多Reactor多线程的实现有 Memcache 和 Netty
使用5W根因分析法(它又叫 5Why 分析法或者丰田五问法,具体是重复问五次“为什么”)检查一下对这块知识的学习程度
问题 1:为什么 Netty 网络处理性能高?
答:因为 Netty 采用了 Reactor 模式
问题 2:为什么用了 Reactor 模式性能就高?
答:因为 Reactor 模式是基于 IO 多路复用的事件驱动模式。
问题 3:为什么 IO 多路复用性能高?
答:因为 IO 多路复用既不会像阻塞 IO 那样没有数据的时候挂起工作线程,也不需要像非阻塞 IO 那样轮询判断是否有数据。
问题 4:为什么 IO 多路复用既不需要挂起工作线程,也不需要轮询?
答:因为 IO 多路复用可以在一个监控线程里面监控很多的连接,没有 IO 操作的时候只要挂起监控线程;只要其中有连接可以进行 IO 操作的时候,操作系统就会唤起监控线程进行处理。
问题 5:那还是会挂起监控线程啊,为什么这样做就性能高呢?
答:首先,如果采取阻塞工作线程的方式,对于 Web 这样的系统,并发的连接可能几万十几万,如果每个连接开一个线程的话,系统性能支撑不了;而如果用线程池的话,因为线程被阻塞的时候是不能用来处理其他连接,会出现等待线程的问题。其次,线上单个系统的工作线程数配置可以达到几百上千,这样数量的线程频繁切换会有性能问题,而单个监控线程切换的性能影响可以忽略不计。第三,工作线程没有 IO 操作的时候可以做其他事情,能够大大提升系统的整体性能。
References
[1]
《堆外内存》: http://www.zhuxingsheng.com/blog/common-sense-four-heaps-of-external-memory.html
[2]
五种IO模型透彻分析: https://www.cnblogs.com/f-ck-need-u/p/7624733.html
[3]
IO模型: https://www.cnblogs.com/sheng-jie/p/how-much-you-know-about-io-models.html
[4]
Scalable IO in Java: http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
[5]
《从零开始学架构》: http://www.zhuxingsheng.com/blog