Disruptor:一种高性能的、在并发线程间数据交换领域用于替换有界限队列


1 摘要.............................................................................................................................................1  2 概述.............................................................................................................................................2  3 并发的复杂性 .............................................................................................................................2  3.1 锁的代价..................................................................................................................................3  3.2 CAS .........................................................................................................................................3  3.3 内存栅栏..................................................................................................................................4  3.4 缓存行......................................................................................................................................5  3.5 队列所带来的问题..................................................................................................................5  3.6 管线和图..................................................................................................................................6  4 LMAX Disruptor的独特设计...................................................................................................6  4.1 内存分配...................................................................................................................................6  4.2 将负面的因素分离开...............................................................................................................7  4.3 使用序列号..............................................................................................................................7  4.4 批量效应..................................................................................................................................7  4.5 依赖关系..................................................................................................................................7  4.6 Disruptor的类结构图............................................................................................................7  4.7 代码示例..................................................................................................................................8  5 吞吐量性能测试 ........................................................................................................................10  Disruptor:一种高性能的、在并发线程间数据交换领域用于替换有界限队列 的方案 作者: Martin Thompson、 Dave Farley、Micheal Barker、Patricia Gee、Andrew Stewart 1 摘要 LMAX 公司被创建去构建一种高性能的金融交易平台。作为我们为达到这样的目标所做的工作的一 部分,我们论证了一些设计这个系统的方案。但是随着我们的测试,我们发现传统方案的一些根本的局限 性。 许多应用使用队列来实现在其线程间的数据交互。通过测试我们发现,非常戏剧性的——使用队列 造成的延迟与磁盘 IO 操作(RAID、SSD 磁盘)造成的延迟同样的多!如果在一个端对端操作中使用多个 队列,这将会增加数百毫秒的总延迟。显然,这是一个需要优化的领域。 进一步的研究和专注于计算机科学的学习使我们认识到,传统方案固有的合并特点导致了在多线程 实现中出现了争用现象,这意味着或许应该有更好的解决办法。 考虑下现代 CPU 的工作原理,有一种我们称之为“硬件机制共鸣”的编程优化方法,使用优化设计 方案、专注于分离问题我们最后创建了一套数据结构和对应的设计模式,我们称之为 Disruptor。 测试表明,使用 Disruptor 的三个线程管道的平均延迟要比相当的使用队列的方案低得多,而且在 同样的配置下,Disruptor 的吞吐量大约为队列的 8 倍。 这些性能上的提高,使我们开始重新思考并发编程的方式。Disruptor,这个全新的模式对于任何需 要高吞吐低延迟特性的异步事件驱动架构来说都是一个理想的借鉴基础。 在 LMAX 公司,我们在 Disruptor 模式基础上构建了次序匹配引擎、实时风险管理系统、高可用性 内存事务处理系统,这些项目都获得了巨大的成功。这些系统的性能都为业界设置了新的标杆——在我们 看来、在目前一段可以预期的时间内不会被超越!(译者注:好狂妄的老外) Disruptor 并不是只能应用于金融业的解决方案,它是一种通用的机制,以简单的实现来最大程度的 提高性能,用来解决并发编程中的复杂问题。尽管 Disruptor 中的一些概念似乎不大主流,但是以我们的成 功经验证明,使用这种模式构建系统要比使用同类机制实现起来简单得多。 Disruptor 框架显著的减少了写的争用、具有更低的系统开销和比之同类其他机制更好的缓存(译者 注:这里指 CPU 的缓存)友好特性,所有这些优点使 Disruptor 具有更加强大的吞吐性能和平稳的低延迟 处理能力。使用中等的时钟频率的处理器,我们测试得到了每秒 2500 万消息发送,上述延迟低于 50 纳秒 (译者注:1 纳秒=一秒的 10 亿分之一,神乎其技啊~~!)。这样的性能相对于我们见过的任何其他的实 现方案来说都是显著的提高。这已经非常接近现代 CPU 处理器在核心之间交换数据的理论极限了。(译者 注:再次惊叹,神乎其技啊!) 2 概述 Disruptor 是我们在 LMAX 公司构建世界上最快的高性能金融交易平台过程中的研究成果。早期,我 们的设计是基于 SEDA 派生和 Actors 模式的,使用管道来提供吞吐量。在评测了各种不同的实现之后,事 实证明在各个管道的事件排队是主要的系统消耗来源。我们也发现队列产生相当的延迟和较高的时间偏差。 我们花费了大量的精力去实现更高性能的队列,但是,事实证明队列作为一种基础的数据结构带有它的局 限性——在生产者、消费者、以及它们的数据存储之间的合并设计问题。Disruptor 就是我们在构建这样一 种能够清晰地分割这些关注问题的数据结构过程中所诞生的成果。 3 并发的复杂性 在我们这一段里我们来讨论并发。在计算机科学中,并发的意思是两个或两个以上的任务同时并行的执 行,但是也要通过争抢来接入资源。争抢的资源可能是数据库、文件系统、套接字、甚至或者说内存中的 一块区域。 并发的执行代码包括两个方面:互斥性和改变的可见性。互斥性是指线程对资源进行争用状态的改变的 管理(译者注:从后面看出,这里的争用状态主要是指写的操作要保持互斥性),而改变可见性是指控制 何时这种改变对其他线程可见。很明显,如果能消除争用就能够避免互斥性管理——如果有某种算法,能 够确保任何给定的资源同一时刻只被一个线程修改,那么互斥性就不是必要的了。读和写操作需要所有改 变对其他线程都是可见的,但是只有争用写操作需要保持互斥性。 在任何并发的环境中,争用写操作是花费最大代价的。为了支持多个并发的线程对同一块资源进行写操 作是需要花费复杂而昂贵的代价来进行协调的。最典型的解决这种协调的方法是引入某种锁的策略。 3.1 锁的代价 锁提供了互斥性并且确保改变对其他线程的可见性以一种命令式的方式发生。锁的代价消耗是难以置 信的大——因为它们当遇到争用时需要进行仲裁。这种仲裁是通过一种上下文切换到操作系统的内核,挂 起线程等待锁的释放。在这样的上下文切换过程中,也会交还控制权给操作系统——操作系统此时可能同 时决定去做其他一些请理性的工作,这样正在执行中的上下文对象将会丢失之前预读的缓存中的数据和指 令(译者注:这里的缓存指的是 CPU 缓存)。对现代 CPU 而言,这将会造成一系列的性能上的坏的影响。 可以使用快速的用户模式的锁,但是这也仅当没有争用的时候能够带来真正的益处。 我们将会举一个简单的演示例子说明锁的代价。这个实验的主要内容是调用一个函数,执行一个循环 5 亿次的增量为 64bit 的计数循环。我们用 Java 编写这个程序后在 2,4Ghz 的 Intel Westmere EP 处理器(译 者注:一款 6 核企业级应用 CPU)上单线程执行,仅仅花费大约 300 毫秒的时间。其实用什么语言对这个 实验来说并不重要,使用相同基本底层原语的语言编写的程序执行时间都差不多。 但是,一旦使用了锁来提供互斥性,那么即使当锁还未被争抢的时候,资源的消耗(译者注:这里主要 指时间上的损耗)仍会显著的提高。而当多个线程开始发生争用现象时,花费在巨大的排队操作上的工作 将会使程序对资源的消耗继续增加。这个小实验的结果如下面的表格所示: 3.2 CAS 在需要修改的内存数据仅为一个字长时,有些更有效的方案可以用来替代锁来进行内存修改。这些替代 方案是基于原子性、互锁性的现代 CPU 实现的指令的。这些指令通常被称为“CAS”(Compare And Swap) 操作,例如 x86 处理器上的"lock cmpxchg"。CAS 操作是一种特殊的机器码指令,它在一定条件下可以允 许对内存中一个字长的操作成为原子操作。在上一小节的计数器小实验中,每个线程轮流在一个循环中读 取计算器并尝试在同一个原子指令中将计算器累加为新的值。累加后的新值和累加前的旧值一起作为该指 令的参数,如果指令执行后计数器的值与指令的新值参数相同,则指令执行成功,将计数器的值置为新值; 否则,如果计数器此时与指令的新值参数不同,则该 CAS 操作失败,此时回到线程重新读取计数器增量加 到原来的新值参数上作为新的新值参数(译者注:即设指令为 f,新值参数为 B,旧值参数为 A,计数增量 为 k:由 f(A,B)改为 f(B,B+k)重新执行指令),重复上述过程,直到指令执行成功。CAS 方法比锁要有效 得多,因为它不需要切换上下文到操作系统内核去仲裁。但是,CAS 操作也并不是没有资源消耗的,处理 器必须锁定它的指令通道去确保原子性,并且要创建一个内存栅栏(memory barrier)来使得状态的变化 对其他线程可见。具体到实际的编程实现上,CAS 操作在 Java 开发中,可以使用 java.util.concurrent.Atomic.* 包中的类来实现。 如果程序的关键部分要比我们上面举的计数器例子复杂,这就需要更加复杂的使用多个 CAS 操作的机器 指令来协调线程间的争用。用锁来编写并发程序很难;用 CAS 操作和内存栅栏开发无锁算法来编写并发程 序有时候更难!而且这样的程序更加难以测试,难以确保其正确性!(译者注:好绝望啊 orz) 理想的算法应该是仅仅使用一个单线程来处理所有的对一个资源的写操作,而有多个线程执行读取处理 结果的操作。在一个多处理器或多核处理器环境下处理对资源的读操作需要内存栅栏来确保一个线程状态 改变对其他处理器上运行的线程可见。 3.3 内存栅栏 现代的处理器为了提高效率,采用无序的方式执行其指令、在内存和对应的执行单元间加载和存储数据。 处理器仅仅需要确保程序逻辑执行出正确的结果而不去关心其执行顺序。这不是单线程程序的特性。但是 当线程间彼此共享状态时为了确保数据交换的成功处理,在需要的时点,内存的改变能够按次序发生就是 很重要的了。内存栅栏是处理器用来指出代码块在哪里修改内存是需要有序的进行的。它们是硬件排序和 线程间保持彼此改变可见性的重要手段。编译器会在适当的位置设置合适的软件栅栏来确保代码按照正确 的顺序编译,处理器本身也会使用这样的软件栅栏作为硬件栅栏的一种补充。 现代的处理器要比同代的内存快得多的多。为了填补这样一个速度差距的鸿沟,CPU 使用了复杂的缓 存系统——非常快的通过硬件实现的无链哈希表。这些缓存系统通过消息传递协议与其他处理器 CPU 的缓 存系统保持协调一致。另外作为补充,处理器的“存储缓冲”可以将写操作从上述缓冲上卸载下来,在一个 写操作将要发生的时候,缓存协调协议通过这样的一个“失效队列”快速的通知失效消息。 这些对于数据来说意味着,当某个数据值的最后一个版本刚刚被写操作执行之后,将会被存储登记给一 个存储缓冲——可能是 CPU 的某一层缓存、或者是一块内存区域。如果线程想要共享这一数据,那么它需 要以一种有序的方式轮流对其他线程可见,这是通过处理器协调消息的协调来实现的。这些及时的协调消 息的生成,是又内存栅栏来控制的。 读操作内存栅栏对 CPU 的加载指令进行排序,通过失效队列来得知当前缓存的改变。这使得读操作内 存栅栏对其之前的已排序的写操作有了一个持久化的视界。 写操作内存栅栏对 CPU 的存储指令进行排序,通过存储缓冲执行,因此,通过对应的 CPU 缓存来刷 新写输出。写操作内存栅栏提供了一个在其之前的存储操作如何发生的、有序的视界。 一个完整的内存栅栏即对加载排序也对存储排序,但这只针对执行该栅栏的 CPU。 一些CPU还有上述三种元件的变体,但是介绍这三种元件已经足够来理解相关的复杂联系了。在Java 的内存模型中,对一个volatile类型成员变量的域的读和写,分别实现了读内存栅栏和写内存栅栏。(译者 注:对volatile类型的成员变量虚拟机不采用优化策略,即不在每个线程中保存其副本,每次读取和修改都 将到共享的内存域中进行)这在关于Java内存模型的一篇文章中已经有很详细的描述 (http://www.ibm.com/developerworks/library/j-jtp02244/index.html),上述这种特性已经随 着Java 5 一起发布。 3.4 缓存行 (译者注:这里缓存行实际上是单词 cache line 的拙劣翻译-_-! 意思是 CPU 缓存与物理内存间交互时所 一次发生的数据,一般为 64 个字节) 现代处理器使用缓存的方式对成功的高性能操作而言具有重要的意义。这种处理器架构在数据搅动和指 令存储方面有极大作用,反之,如果缓存出现丢失的话将对系能造成极大的影响。 我们的硬件在移动内存数据的时候不是以字节和字长为单位的,为了更加有效的工作,缓存被组织成缓 存行的形式,每个缓存行为 32-256 个字节大小,一般是 64 字节。这是缓存协调协议操作的粒度层级。这 意味着如果两个变量在同一个缓存行内,并且它们是被两个不同的线程写入的话,它们将会呈现出相同的 写争用问题,就好像它们是一个变量一样!这就是“伪共享”概念。所以为了提高性能,应该确保独立的且 并发的写操作、并且写操作的变量不在同一个缓存行内,可以使争用最小化。 当访问内存时,一个具有预读功能的 CPU 会通过预读的方式来减少访问时花费的延迟——CPU 会预 读有可能下次需要访问的数据、并在后台将其提取到缓存中。这种机制仅仅在处理器侦测到某种访问上的 模式是时候启动,就好像是本来一步一步走路的内存访问突然来了个“跳跃”一样。比如,在对一个数组中 的内容进行遍历的时候,上述的预读跳跃是会启动的,所以相应的内存数据会被提前提取到 CPU 缓存中, 最大可能的提高了访问效率。上述跳跃一般来讲是不大于 2048 个字节的,或者说 CPU 能够预测到的字节 也不大于这个数字。但是,像链表或树集这种由分散在内存空间不同位置的节点组成的数据结构,是没有 办法启动预读跳跃的。这种在内存中没有规律的存储限制了系统预读内存行,会导致内存的访问效率下降 两个数量级。 3.5 队列所带来的问题 一般来说队列是使用链表或者数组来作为其中元素的基本存储的。如果一个内存中的队列没有被限制大 小成为无界队列时,在很多种类的问题当中它可能会没有被校验的增长——直到灾难性的出现内存耗尽为 止,这种情况发生在生产者“跑”的比消费者快的时候。无界的队列在生产者确保不会跑的比消费者快的并 且内存资源比较稀缺的系统中比较有用,但是如果这种假设不成立队列变得无限制的增长的话总会有一定 的风险的。为了避免这种灾难发生,队列一般会被限制大小成为有界队列,方法是要么使用数组来实现队 列、要么实时的去跟踪队列的大小。 队列的实现可能会在队首、队尾和记录队列大小的变量上发生写操作争用。在使用过程中,由于生产 者和消费中跑的快慢不同,队列总是处于将满或者将空状态,而很少处于一种生产者快慢相当的平衡的中 间状态。这种大部分时间处于将满或将空状态的倾向导致了大量的争用和昂贵的缓存协调代价。而且问题 还在于即使用不同的并发对象(比如锁或 CAS 变量)来分别实现头尾机制,它们通常会占用同一块缓存行。 (译者注:发生伪共享,导致并行失败) 使用单个大粒度的带锁队列,生产者声明队首、消费者声明队尾、中间的存储节点用来设计并发,这 样的实现管理起来非常复杂。队列上为了 put 和 take 操作的大粒度的锁实现起来很简单,但是会导致吞吐 量上很大的瓶颈问题。如果只使用队列自有的语义模型来消除并发矛盾的话,那么除了单生产者单消费者 这种情况之外,其他情况实现起来相当复杂。 在 Java 中使用队列还有另外一个问题,队列是很容易产生垃圾的结构。首先对象会被分配并置放在队 列中,其次如果是使用链表实现的队列,那么对象需要被分配去实现链表中的节点。当不再被引用的时候, 所有这些为支持队列实现所分配的对象需要被重新声明。(译者注:实际上是说在 Java 中,队列的垃圾回 收代价很大,特别是对链表式队列而言) 3.6 管线和图 在许多种类的问题中把几个阶段处理捆绑为一个管线是个好办法,这些管线一般具有并行的路径,组 成一种图状的拓扑结构。每个阶段之间的链接一般使用队列来实现,每个阶段具有其自己的线程。 这种方案的代价可不便宜——在每个阶段我们不得不花费工作单元进队和出队的开销。当有多个目标 其路径分叉时会加倍这种开销,并且当上述分叉路径必须合并时也会遭受无法避免的争用的代价。 如果处理图状依赖拓扑结构时,能够避免在各阶段之间使用队列的开销的话,那将是非常理想的。 4 LMAX Disruptor 的独特设计 在试着定位上面几段中描述的那些个问题的时候,一个通过严格剔除像队列导致的一系列问题为目标 的设计浮现出来了。这个方案关注确保任何一块数据同一时刻只被一个线程执行写操作,因此便消除了写 争用,这便是“Disruptor”框架。之所以叫这个名字,是因为它在处理依赖性拓扑结构的时候与 Java 7 中支 持分叉合并的"Phasers"(译者注:Java7 中引入的一种新的并发特性,属于一种新型并发 barrier)有着相 似的地方。 LMAX 公司的 Disruptor 框架的设计被定位于上述问题,通过尝试最大化内存分配的效率、以缓存友好 的工作方式优化在现代硬件上的性能。 处于 Disruptor 机制中心脏地位的是一个预先分配的有界数据结构形式——环状缓冲。数据通过一个或 多个生产者添加到环状缓冲中,并通过一个或多个消费者从其中取出处理。 4.1 内存分配 环状缓冲(ring buffer)的所有内存空间是在启动的时候预先分配好的。环状缓冲既可以存储一整个数 组的指向实体的指针、或者是代表实体本身的数据结构。由于 Java 语言本身的限制意味着实体是以对象的 引用的形式存放在环状缓冲中的。每一个实体一般并不是直接存放的,而是放在一个容器里,而把容器放 在缓冲中。这种实体存放空间的预分配的形式终结了支持垃圾回收机制的语言所带来的问题,因为实体会 被重复使用并在 Disruptor 实例的生命周期中一致存在。这些实体的内存空间是在同一时刻分配好的,并且 一般来说是在内存中连续的一块地址,因此支持缓存跳跃(译者注:即前文中提到的预读跳跃)。John Rose 有一篇关于 Java 语言的建议,介绍什么样的值类型允许 Java 的数组能够像其他语言、例如 C 语言中那样 确保分配给其的内存是连续的,从而避免使用指针寻址。 在一个像 Java 这样被管理的运行时环境中开发低延迟系统时,垃圾回收可能会是个问题。分配的内存 越多,垃圾收集器的负担就越大。当对象的生命周期都极短或者对象永不销毁的情况下,垃圾收集器可以 达到最佳性能(译者注:实际上是最小的负担)。环状缓冲中的实体内存是预先分配好的,意味着它在垃圾 回收器工作的时候是永不销毁的,所以只带来很少的负担。 由于基于队列的系统在高负载时会导致执行率降低、并且导致分配的对象释放其所在空间上的延迟,所 以一代又一代的垃圾收集器都在这一点上进行不断优化。这有两层意思:第一,对象不得不在每一代之间 进行复制,这导致了不定的延迟。第二,这些对象可能会从旧代中收集,这可能会是更加消耗性的操作,可 能增加“世界停止”一般的暂停,这发生在零碎的内存空间被重新压实的时候。在大内存堆中这会导致每 GB 数秒的暂停。 4.2 将负面的因素分离开 4.3 使用序列号 4.4 批量效应 当消费者等待环状缓冲中最新可用的游标序列号时会有一定几率发生一个在队列中不会发生的有趣的 现象:如果消费者发现与它上次检查的时候相比,环状缓冲的游标已经向前走了许多步的话,它可以直接 处理到那个最新的序列号而不必纠缠于并发机制。这样的结果是本来落后的生产者会重新赢得与之前突然 爆发的生产者的赛跑比赛,重新平衡了系统。这种批量效应增加了处理吞吐量并减少和平稳了延迟。根据 我们的观察,在内存子系统饱和之前,不管负载多大,这种效应的延迟始终接近一个时间常量,对应的变 化曲线是线性的并遵循利特尔法则,这与我们使用队列时在负载不断增加时延迟呈指数级增长得到的 J 形 曲线是截然不同的。 4.5 依赖关系 队列代表着一个生产者与消费者之间的简单单步管线依赖。如果消费者之间形成了某种链状或图状依 赖关系的话,在图状依赖的每个阶段就都需要一个队列。在图的各个依赖阶段之间导致大量的队列固定时 间消耗。在我们设计 LMAX 公司的金融交易平台的时候,我们的研究表明,基于队列的方案在事务处理中 的执行延迟大量是花费在排队上了(译者注:大量花费在排队上而不是事务本身的处理逻辑)。 因为使用 Disruptor 模式分离了生产者与消费者矛盾,使得我们可以仅仅使用核心的环状缓冲来表示复 杂的多个消费者之间的依赖关系。这减少了大量的执行上的固定消耗,并增加了吞吐处理能力、减少了延 迟。 一个环状缓冲可以用来存储表示一整个工作流的复杂数据结构的实体。在设计这样的数据结构的时候 必须注意,在被独立的不同消费者写入的时候要避免导致缓存行的伪共享。 4.6 Disruptor 的类结构图 下面的类图描述了 Disruptor 框架的核心关系。如图中所示,易于使用的类可以简化编程模型。在建立 了依赖关系之后,编程模式变得很简单。生产者通过 ProducerBarrier 使用序列号声明实体,在声明好的实 体中写入改变,然后通过 ProducerBarrier 将实体提交回来并使其可以被消费者使用。而消费者仅仅需要提 供一个 BatchHandler 的实现即可,该实现负责接收当一个新的实体可用时的回调请求。这种结果驱动编程 模型是基于事件的,与 Actor 模型有很多相似的地方。 在分离了使用队列实现所带来的问题之后,便可以实现更灵活的设计。Disruptor 模式的核心 ——RingBuffer,可以提供存储使得数据的交换在不发生争用的情况下进行。经由生产者、消费者与 RingBuffer 的交互中分离出了传统并发所带来的问题。ProducerBarrier 负责管理所有在环状缓冲中声明序 列位置的并发问题,并跟踪各个消费者以确保这个环不会缠绕。(译者注:在 RingBuffer 这个环状的跑道 上,最快的生产者超过了最慢的消费者,即为环的缠绕。)ConsumerBarrier 用来提醒消费者是否有新的 实体可用,这样消费者便可以构造图状的依赖关系用来表示一个处理关系中多个阶段。 4.7 代码示例 下面的代码是一个单生产者单消费者的例子,使用了方便的 BatchHandler 接口来实现消费者。消费者 使用单独的线程来接收那些可用的实体。 //Callback handler which can be implemented by consumers finalBatchHandler batchHandler = newBatchHandler() { public void onAvailable(final ValueEntryentry) throws Exception { // process a new entry as it becomesavailable. } public void onEndOfBatch() throws Exception { // useful for flushing results to an IOdevice if necessary. } public void onCompletion() { // do any necessary clean up beforeshutdown } }; RingBufferringBuffer = newRingBuffer(ValueEntry.ENTRY_FACTORY, SIZE, ClaimStrategy.Option.SINGLE_THREADED, WaitStrategy.Option.YIELDING); ConsumerBarrierconsumerBarrier = ringBuffer.createConsumerBarrier(); BatchConsumerbatchConsumer = newBatchConsumer(consumerBarrier, batchHandler); ProducerBarrierproducerBarrier = ringBuffer.createProducerBarrier(batchConsumer); //Each consumer can run on a separate thread EXECUTOR.submit(batchConsumer); //Producers claim entries in sequence ValueEntryentry = producerBarrier.nextEntry(); //copy data into the entry container //make the entry available to consumers producerBarrier.commit(entry); 5 吞吐量性能测试 作为对比,我们选取了 Doug Lea 的优秀数据结构 java.util.concurrent.ArrayBlockingQueue 来作为参 照,这种队列在我们测试中在所有的有界队列中是性能最好的。这些测试设置成一个阻塞的编程用来匹配 Disruptor。这些个测试的详细代码已经包含在 Disruptor 的开源项目里了。注意:想运行这些测试的话需要 能够并行运行四个线程能力的硬件环境。(译者注:恩,没错,这个 Disruptor 是个白富美框架,想护到她 的,实力低于 i5 双核 4 线程的就赶紧去升级 CPU 吧。。。) 在上面的配置中,每条数据流的弧是用 ArrayBlockingQueue 来实现的,与之对比 Disruptor 使用的是一种 内存栅栏。下面的表格列出了每秒操作数的性能测试的结果,使用 Java 1.6.0_25 64-bit Sun JVM,Wi ndows 7,Intel Core i7 860 @2.8GHz 不带超线程,另一组使用 Intel Core i7-2720QM,Ubuntu 11.0 4,取 3 组处理 5 亿次消息测试中最好的一组作为测试成绩。测试在不同的 JVM 上执行都得到类似的结果, 并且如下数据上的差距并不是我们测试观察中最大的。 6 延迟性能测试 为了测量延迟,我们准备了一个三个阶段的管线,并生成少于饱和的事件量。这通过在注入一个事件后 等待 1 毫秒然后在注入另一个事件来实现,上述动作执行 5 千万次。要做到这如此精密的情况下计时,有 必要使用 CPU 的时间戳计数器了。我们选择带有不变 TSC 值的 CPU,因为旧型号的 CPU 为了节省电源 休眠状态通常切换比较频繁,Intel Nehalem 和其之后的处理器使用了不变的 TSC,可以在 Ubuntu 11.04 上用最新的 Oracle JVM 访问。这个测试没有使用 CPU 绑定。 为了对比,我们再一次使用了 ArrayBlockingQueue,我们本可以用 ConcurrentLinkedQueue 的,这样 可能会得到更好的测试成绩,但是想使用一种有界队列的实现来避免产生更多后台压力使生产者比消费者 跑得快。下面的结果是基于 2.2GHz Core i7-2720QM,Java 1.6.0_25 64-bit,Ubuntu 11.04。 每次循环的平均延迟,Disruptor 为 52 纳秒,ArrayBlockingQueue 是 32757 纳秒。研究表明,锁和通 过条件变量传递信号是 ArrayBlockingQueue 主要的延迟来源。   7 结论 Disruptor 在一个许多应用都要考虑的重要方面,迈出了重要一步,——即如何在一个可预估的延迟范围内, 提高并发执行的上下文吞吐量和减少延迟。我们的测试表明它是具有出色性能的进程间数据交互方案。我 们相信对这种数据交互来说,Disruptor 是最高性能的机制。通过专注于准确的分离线程间数据交互带来的 问题、通过避免写争用和最少化的读争用、通过保证程序代码能够适应现代处理器的缓存模式,我们创造 了一种高效的在应用的线程间交换数据的机制。 Disruptor 特有的批量效应允许多个消费者在没有任何争用抢占的情况下、在一个确定的起始点下处理 输入,这为高性能系统引入了一个新的特性。对于大多数的系统来说,随着负载的不断加大、争用抢占的 情况不断增多,系统的延迟会呈指数级增加——典型的“J”形曲线(译者注:以负载为 x 轴,延迟为 y 轴,由 压测数据制作的曲线)。而对于 Disruptor,随着负载的增加,延迟在系统的内存饱和之前几乎是保持平稳的, 一条几乎水平的曲线。 我们相信,Disruptor 为高性能计算建立了一个新的基准,也(译者注:在 Java 应用代码层面)为更 有效的利用现代处理器和计算机结构提供了新的参考意义。 
还剩12页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 5 人已下载

下载pdf