socket与IO高性能

最近看到篇好文章《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,在《堆外内存》中提到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 操作的时候可以做其他事情,能够大大提升系统的整体性能。

Reference

五种IO模型透彻分析

IO模型

Scalable IO in Java

《从零开始学架构》

公众号:码农戏码
欢迎关注微信公众号『码农戏码』