Staged Event-Driven Architecture

Wed 13 July 2011
  • 手艺 tags:
  • Architecture published: true comments: true

按照传统的编写应用程序的思路,当server接到请求,包装完成之后,分配到线程池中交给一个线程完成,返回。Java的servlet容器就是这么设计的,这么多年大部分的应用程序也是在这种模式下编写的。当任务的代价比较大,比如多次和数据库交互,并且可能和后端其他服务通信,这是线程被任务占用的时间就相对较长。如果请求的并发量很大,容器的线程池耗尽,新的请求就无法被处理,导致并发性无法提高,吞吐量也无法提高。

实际上在大部分应用程序里,所谓代价很高的任务,往往都是I/O Bound。涉及网络通信,磁盘读写,线程被迫wait,CPU却是空闲的。所以,I/O Bound的程序理论上都还有优化的空间。于是有了这种Staged Event-Driven Architecture,即SEDA。

SEDA的思路是将原先由一个线程完成的任务,分割为相对独立的多个阶段。每个阶段由专用的一组线程负责执行,阶段之间用过队列交互。又是上次提到的老话:If you cannot split it, you cannot scale it.

例如,我们的业务逻辑是读取请求,操作数据库,与后端通信,操作数据库,写回结果。如果采用NIO的方式,网络通信可以认为是非阻塞的。而目前与数据库的交互,通常还是阻塞式的风格。这样这五个阶段分别是非阻塞、阻塞、非阻塞、阻塞、非阻塞。组塞操作容易成为性能瓶颈,CPU时间用于等待,占用率不高,所以对组塞操作可以分配相对多的线程;非阻塞操作速度较快,为了避免快速的切换导致sy升高,采用和CPU核数一样多的线程即可。

这样,原本一个线程反复等待的阻塞操作,变成了部分线程等待的同时,其他线程仍然在处理自己的业务,有效使用CPU。利用的多核的优势,提高了CPU的占用,即提高了系统的处理能力。

而对外部接口来说,当读取数据的线程在读取完毕之后,将任务dispatch到相应队列即返回,前端可以保证很高的处理速度,并发性也可以保证。任务被积压在阻塞操作的队列中,而消费阻塞操作的线程要多于提供者,在一定程度上也保证了处理速度。再退一步说,当阻塞操作的速度却是无法消费大量任务时,前端可以根据队列的大小判断当前系统的load,拒绝服务。

当然,采用这种方式,只有在并发量提高到一定程度,并发成为系统瓶颈时才能体现价值。就单个操作而言,由于队列的传递,他的latency一定是有所上升的。

关于SEDA,可以参考Matt Welsh的相关论文:The Staged Event-Driven Architecture for Highly-Concurrent Server Applications。