Boost application performance using asynchronous I/O把同步阻塞、同步非阻塞、异步阻塞、异步非阻塞的模型讲得很清楚。
event-driven模型派(异步模型):
有人对于event-driven模型有一些批判,认为多线程模型(同步阻塞模型)不比事件模型差:
两种模型也不是说水火不容,SEDA提出了可以将两种模型结合起来,构建更具弹性的系统。10年之后该作者写了篇回顾文章A Retrospective on SEDA。
SEDA提出了几个很具有见地的意见:
什么是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,下面是几篇文章:
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的性能差别,不过看下来发现文章的压测方式存在偏心:
Thread.sleep()
。不过当我尝试在vert.x中使用sleep则发生了大量报错,应该是我的使用问题,后面就没有深究了。我写的测试可以在这里看到。
看了前面这么多文章其实总结下来就这么几点:
Jeff Darcy's notes on high-performance server design提到了高性能服务器的几个性能因素:
仔细看看有些因素不就是事件驱动模型和多线程模型都面临的问题吗?而又有一些因素则是两种模型提出的当时所各自存在的短板吗?而某些短板现在不是就已经解决了吗?
上面说的有点虚,下面讲点实在的。
如果你有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我之前写了一篇文章如何估算吞吐量以及线程池大小可以看看。
如何用满网络带宽没有什么经验,这里就不说了。