关于同步/异步、阻塞/非阻塞IO的摘要

42次阅读

共计 3848 个字符,预计需要花费 10 分钟才能阅读完成。

四种 IO 模型
Boost application performance using asynchronous I/ O 把同步阻塞、同步非阻塞、异步阻塞、异步非阻塞的模型讲得很清楚。
处理大量连接的问题
event-driven 模型派(异步模型):

Dan Kegal’s C10K problem
延伸阅读:如何解决 C10M 问题 The Secret To 10 Million Concurrent Connections -The Kernel Is The Problem, Not The Solution 这个 presentation 主要讲了如何消除内核 network stack 的瓶颈,没有特别提到采用哪种模型。

有人对于 event-driven 模型有一些批判,认为多线程模型(同步阻塞模型)不比事件模型差:

Thousands of Threads and Blocking I/O,讲了 C10K 提到的多线程模型的性能瓶颈在如今的内核里已经不存在了,而多线程模型开发起来更简单。

Why Events are a Bad Idea(for high concurrency servers) Rob von Behren,讲了多线程模型的性能瓶颈基本上是因为内核支持的不好、多线程类库有缺陷造成的。认为可以通过编译器的优化、修复内核、修复多线程类库来达到和事件驱动模型相当的结果。且认为事件驱动模型的开发比较复杂。

两种模型也不是说水火不容,SEDA 提出了可以将两种模型结合起来,构建更具弹性的系统。10 年之后该作者写了篇回顾文章 A Retrospective on SEDA。
SEDA 提出了几个很具有见地的意见:

应用程序的各个 stage 的压力应该是可观测和可调节的。
应用程序应该是 well-conditioned。

什么是 Well-conditioned service?
Intuitively, a service is well-conditioned if it behaves like a simple pipeline, where the depth of the pipeline is determined by the path through the network and the processing stages within the service itself. As the offered load increases, the delivered throughput increases proportionally until the pipeline is full and the throughput saturates; additional load should not degrade throughput. Similarly, the response time exhibited by the service is roughly constant at light load, because it is dominated by the depth of the pipeline. As load approaches saturation, the queueing delay dominates. In the closed-loop scenario typical of many services, where each client waits for a response before delivering the next request, response time should increase linearly with the number of clients. The key property of a well-conditioned service is graceful degradation: as offered load exceeds capacity, the service maintains high throughput with a linear response-time penalty that impacts all clients equally, or at least predictably according to some service-specific policy. Note that this is not the typical Web experience; rather, as load increases, throughput decreases and response time increases dramatically, creating the impression that the service has crashed.

简单来说当负载超过一个应用的容量时,其性能表现要满足以下两点:

吞吐量依然保持稳定,可以稍有下跌但绝不会断崖式下跌
随着负载的增加其延迟线性增长,绝不会出现尖刺

Reactor pattern
事件驱动模型到最后就变成了 Reactor Pattern,下面是几篇文章:
Scalable IO in Java 介绍了如何使用 NIO,其中很重要的一点是 handler 用来处理 non-blocking 的 task,如果 task 是 blocking 的,那么要交给其他线程处理。这不就是简化版的 SEDA 吗?
Reactor Pattern 的老祖宗论文:Reactor Pattern,TL;DR。Understanding Reactor Pattern: Thread-Based and Event-Driven 帮助你快速理解什么是 Reactor Pattern,文中提到如果要处理 10K 个长连接,Tomcat 是开不了那么多线程的。对此有一个疑问,Tomcat 可以采用 NIO/NIO2 的 Connector,为啥不能算作是 Reactor 呢?这是因为 Tomcat 不是事件驱动的,所以算不上。
The reactor pattern and non-blocking IO 对比了 Tomcat 和 vert.x 的性能差别,不过看下来发现文章的压测方式存在偏心:

文中给 Tomcat 的线程少了(只给了 500),只利用了 40% 左右的 CPU,而 vert.x 的测试的 CPU 利用率为 100%。我把的 Tomcat 的线程设到 2000,测试结果就和 vert.x 差不多了(验证了多线程模型派的观点)。
vert.x 的测试代码和 Tomcat 的测试代码不等价,没有使用 Thread.sleep()。不过当我尝试在 vert.x 中使用 sleep 则发生了大量报错,应该是我的使用问题,后面就没有深究了。

总结
看了前面这么多文章其实总结下来就这么几点:

选择事件驱动模型还是多线程模型要根据具体情况来(不过这是一句废话,; )
推崇、反对某个模型的文章 / 论文都是在当时的历史情况下写出来的,说白了就是存在历史局限性,因此一定要自己验证,当时正确的论断对现在来讲未必正确,事情是会发生变化的。
看测试报告的时候一定要自己试验,有些测试可能本身设计的就有问题,导致结果存在偏见。对于大多数性能测试来说,我觉得只要抓住一点就行了,就是 CPU 一定要用足。
我们真正应该关注的是不变的东西。

Jeff Darcy’s notes on high-performance server design 提到了高性能服务器的几个性能因素:

data copy,问题依然存在,需要程序员去优化。
context switch,这个问题已经没有了(见多线程派的几篇文章),现代操作系统不论有多少 thread,开销不会有显著增加。
memory allocation,这个要看看,不过在 Java 里似乎和 JVM GC 有关。
lock contention,这个问题依然存在,应该尽量使用 lock-free/non-blocking 的数据结构。
另外补充:在 C10M 里提到 kernel 和内核的 network stack 也是瓶颈。

仔细看看有些因素不就是事件驱动模型和多线程模型都面临的问题吗?而又有一些因素则是两种模型提出的当时所各自存在的短板吗?而某些短板现在不是就已经解决了吗?
上面说的有点虚,下面讲点实在的。
如果你有 10K 个长连接,每个连接大部分时间不使用 CPU(处于 Idle 状态或者 blocking 状态),那么为每个连接创建一个单独的线程就显得不划算。因为这样做会占用大量内存,而 CPU 的利用率却很低,因为大多数时间线程都闲着。
事件驱动模型解决的是 C10K 问题,注意 C 是 Connection,解决的是用更少的硬件资源处理更多的连接的问题,它不解决让请求更快速的问题(这是程序员 / 算法的问题)。
要不要采用事件驱动模型取决于 Task 的 CPU 运算时间与 Blocking 时间的比例,如果比例很低,那么用事件驱动模型。对于长连接来说,比如 websocket,这个比例就很小,甚至可近似认为是 0,这个时候用事件驱动模型比较好。如果比例比较高,用多线程模型也可以,它的编程复杂度很低。
不论是采用哪种模型,都要用足硬件资源,这个资源可以是 CPU 也可以是网络带宽,如果发生资源闲置那你的吞吐量就上不去。
对于多线程模型来说开多少线程合适呢?Thousands of Threads and Blocking I/ O 里讲得很对,当能够使系统饱和的时候就够了。比如 CPU 到 100% 了、网络带宽满了。如果内存用满了但是这两个都没用满,那么一般来说是出现 BUG 了。
对于事件驱动模型来说也有 CPU 用满的问题,现实中总会存在一些阻塞操作会造成 CPU 闲置,这也就是为什么 SEDA 和 Scalable IO in Java 都提到了要额外开线程来处理这些阻塞操作。关于如何用满 CPU 我之前写了一篇文章如何估算吞吐量以及线程池大小可以看看。
如何用满网络带宽没有什么经验,这里就不说了。

正文完
 0