A Design Framework for Highly Concurrent Systems (待翻译)

A Design Framework for Highly Concurrent Systems (待翻译)

本文翻译自加州大学伯克利分校的一篇论文:《A Design Framework for Highly Concurrent Systems》,原文链接如下:
https://www.eecs.harvard.edu/~mdw/papers/events.pdf

一、概要

构建高并发系统,例如大规模的互联网服务,需要同时管理许多信息流,并且在需求超出资源可用性时维护峰值吞吐量。并且,任何支持互联网服务的平台都必须提供高可用性,并能够处理突发性负载。人们提出了很多构建并发系统的方法,一般都分为线程(threads)和事件(event-driven)编程两大类。我们认为线程和事件实际是设计谱的末端,高并发应用的设计策略存在于两者之间。
我们基于三个设计组件——任务(tasks),队列(queues)和线程池(thread pools),为构建高并发系统提出了一个通用目的的设计框架。它封装了并发,性能,错误隔离以及线程和事件在软件工程上的益处。我们提供了一系列设计模式,可帮助使用这些组件构建应用。同时,我们还提供了一些使用我们的框架构建的系统的分析数据,以证明我们框架构建并发应用的优势。

二、介绍

大型互联网服务必须处理空前规模的并发。每天互联网站上的并发会话数和点击量(hit)都将转化成更高数量的I/O和网络请求,为底层资源施加巨大的请求。微软的web网站一天接收410万用户的3亿点击;Lycos每天有100万多的用户和820万多的页面访问。随着对互联网服务的要求的增长,对它们的功能的要求也在增长,必须采用新的系统设计技术来管理这些负载。
除了高并发,还必须通过互联网服务的其他三个属性:突发性( burstiness)、持续需求(continuous demand)以及人类尺度( human-scale)的访问延迟,来重新审视这些系统是如何设计的。负载的突发性是互联网的基本原则,必须在系统设计开始时就处理过载状态。互联网服务还必须具备非常高的可用性,要求一年的故障时间不多于几分钟。最后,因为用户访问互联网服务有自身的访问延迟,同时受制于WAN和调制解调器的访问时间,要做的一个重要的权衡即朝着高吞吐量而优化,而非为了低延时。
高并发系统天生就很难构建。现行的编程模型并没有好的结构化代码来实现高吞吐量。虽然线程常被用来实现并发,但是很多线程实现的高资源使用率以及扩展性限制,导致很多开发者更偏好事件驱动方案。然而,这些事件驱动系统大多为了特殊应用从头构建,并且依赖于一些并未被大多数语言和操作系统支持的机制。而且,使用事件驱动编程实现并发,会比线程更难开发和调试。
线程和事件常被视为设计谱上相反的两端;开发高并发系统的关键是在这个设计谱的中间操作。事件驱动技术可以有效地实现高并发,但是在构建真实系统时,线程更有利于充分挖掘多处理器的并行性以及处理阻塞I/O。大部分开发者都知道这个设计谱是存在的,同时使用线程和事件驱动方法来实现并发。但是,目前并没有很好的理解这个设计谱的维度。
我们提出了一个构建高并发系统的通用设计框架。隐藏在我们框架背后的思想,即为使用事件驱动程序实现高吞吐量,但是利用线程(受限的数量)实现并行以及简化编程。另外,我们的框架还解决这些应用的其他需求:高吞吐负载下的高可用性以及可维护性。前者通过在应用的组件间引入故障边界来实现;后者通过调节系统资源上的负载来实现。
这个框架还提供一种手段从整体上推出系统的结构和性能特性。我们分析了几个使用本框架实现的不同的系统,包括一个分布式持久化存储(distributed persistent store)和一个可扩展的互联网服务平台。这些分析将证明我们的设计框架提供了一个构建和探究并发系统的有效模型。

三、动机:强健的吞吐量

为了探索不同的并发编程风格,设想一个假想的服务器(如图1所示),
图1

图1:并发服务器模型:服务器每秒接收A个任务,处理每个任务需要L秒,并且每秒返回S个响应。这个系统是闭环的:每个服务响应都导致另一个任务被注入到服务器,也就是说,稳定情况下,S = A。

每秒从一系列客户端接收A个任务,在返回给每个任务响应前,在服务器端有L秒/任务的延时,并且尽可能多的重叠各个任务。设服务器端的任务完成速率是S。这种服务器的一个具体的例子为一个web代理缓存;如果到该缓存的请求未命中,则页面需要从被代理的服务器获取,因此会有一个大的延时。但是在此期间,任务并没有消费任何CPU周期。客户端收到各自的响应后,会马上发出另一个任务到服务器,因此这是一个闭环系统(closed-loop system)。
在现代系统中,有两个流行的策略来处理并发:线程和事件。线程允许程序员编写线性的代码,并依赖操作系统通过透明的切换线程来并行化cpu计算和I/O操作。而事件则允许程序员通过将代码组织为单线程的handler,并对各种事件(例如非阻塞I/O完成事件,特定的消息或者定时器事件)做出反应,来明确地管理并发。我们将依次探究这些策略,然后总结出一个健壮的混合设计模式,也就是我们所提出的通用的设计框架。

1. 多线程服务器(Threaded Servers)

图2

图2:多线程服务器:针对服务器接收到的每个人物,要么从一个静态创建的池里面分派一个线程,要么新创建一个线程来处理它。在任意给定时间,有总共T个线程并发执行,并且 T = A X L。

如图2所示,多线程服务器的一个简单的实现,使用一个单独的线程专门服务于网络连接,并将收到的任务传递给各自的任务处理线程,这些处理线程将贯穿任务处理的始终。每个任务创建一个处理线程。此类简单模式的一种优化是事先创建一个线程池,并从池里分发线程,从而分摊线程创建和销毁的较高消耗。稳定状态下,服务器中并发执行的线程数T = S x L。如果每个任务的处理时间变长,要维持吞吐量不变的话,需要增加更多的并发线程来消化掉增加的时间,同样的,若想在不同吞吐量下都保持响应时间不变,也需要线性的增加线程数。
线程已成为实现并发的主要手段。大多数操作系统上,线程支持都已标准化,且被像Java一样的现代语言封装的很好。使用线程的顺序编程风格,程序员会感觉很舒适,并且线程工具也相对较成熟。同时,在SMP系统(对称多处理器系统)上,线程可以随着处理器数量的增长来扩张应用,这是因为操作系统可以调度线程在不同的处理器上并发执行。
线程编程带来了一些正确性和调优挑战。同步原语(如锁,互斥信号量, 或者条件变量)常为代码带来bug。随着竞争同一个锁的线程数的增长,锁竞争会导致严重的性能退化。
无论多线程服务器被实现的有多精巧,随着系统中线程数的增长,操作系统的开销(调度和内存占用)也会增长,从而导致整个系统性能的下降。每个给定系统都有个典型的最大的线程数T’,超过这个线程数将导致性能退化。这个现象可以在图3中很清晰的观察到。
图3

图3:多线程服务器吞吐量退化:这个基准测试有一个很快的客户端,经过一个TCP连接触发很多150字节的并发任务,到一个图2所示的多线程服务器上。该服务器运行在167MHz的Ultra-SPARC Solaris 5.6系统上,并以L = 50ms的速率处理任务。任务的到达速率决定了并发线程的数量;线程为负载事先分派好。随着并发线程数T的增加,吞吐率逐渐增长,知道T大于等于T’,在此之后系统吞吐量大幅退化。

在这幅图中,虽然最大线程数T’可能对通用目的的分时作业而言已经足够大了,但是它可能并不能胜任互联网服务所需的巨大的并发要求。

2. 事件驱动服务器(Event-Driven Servers)

一种事件驱动服务器的实现,使用一个单独的线程和I/O子系统上的非阻塞接口或者定时器工具来兼顾各个并发任务,如图4所示。
图4

图4:事件驱动服务器:到达服务器的每个任务都被放置到一个主时间队列。专门服务于这个队列的线程为每个任务设置一个L秒的定时器;此定时器被实现为队列,并且被另一个线程处理。当一个定时器触发时,一个定时器时间被放到住时间队列中,导致主服务器线程生成一个响应。

事件驱动系统多被时限为一个持续循环的线程程序,处理一个队列中的不同类型的事件。此线程要么阻塞,要么轮询(poll)这个队列来等待新事件。
事件驱动编程有其固有的挑战。每个任务的顺序流不再被一个单独的线程处理;反而使用一个线程在各任务的互斥阶段处理所有任务。这导致难于调试,因为栈追踪不再表示处理某个任务时的控制流。而且,任务状态必须被绑定到任务自身,而非像多线程系统一样存储到本地变量或者栈上面。事件驱动库或包并没有标准化,也缺少相应的调试工具。然而, 事件驱动编程避免了很多同步带来的bug,例如竞争条件和死锁。
事件通常无法利用SMP系统的优势来提升性能,除非使用多个事件处理线程。并且,无论使用哪种I/O机制,事件处理线程都可能会阻塞。页缺失和垃圾收集是较为常见的导致线程挂起的原因,而且通常无法避免。而且,也不大可能让所有应用代码都是非阻塞的;通常,标准库组件和第三方代码暴露阻塞接口。在此情况下,线程就因它能在阻塞接口上获取并发而显得更有价值。
事件驱动系统面对负载时,健壮性更好。随着负载的增加,直到其超过系统所能hold住之前,都不大会发生性能退化。如果事件处理和任务状态的绑定比较有效的化,吞吐量峰值可以达到很高。图5展示了把图4中的网络服务换成事件驱动实现以后所能达到的吞吐量。吞吐量比多线程服务器要高,但更重要的是随着并发的增加,并没有发生吞吐量退化的问题。虽然任务数的增长,服务器的吞吐量一直增加,直到管道被塞满,瓶颈(此例中是CPU)变饱和。如果管道中的任务数量继续增加,这些过量的任务会被系统的队列消化,要么是被服务器的主时间队列,要么是被客户端/服务器传输连接所关联的网络栈队列。在此情况下,尽管每个任务的延迟增加了,但服务器的吞吐量维持不变。
图5

图5:事件驱动服务器吞吐量:使用同图3一样的基准设置。本图展示事件驱动服务器的吞吐量受管道中的任务数影响。事件驱动服务器用1个线程接收所有任务,还有另一个线程处理定时器事件。当系统饱和时,其吞吐量要高过线程服务器很多,并且吞吐量并没有随着并发负载的增加而退化。

事件驱动方法中,并发是显式的,并且直接与队列相关。通过将不同处于不同阶段下的多个任务放到一个或多个队列中,程序员可以使用应用特定的知识来重新排序事件的处理顺序,从而实现任务的优先级排序或者提高处理效率。

3. 线程和事件谱

To Be Continued

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论