LMAX系统架构

本文转载自:LMAX系统架构 ,(很是感谢做者yfx416分享好文)php

  

  不少架构师都面临这么一个问题:如何设计一个高吞吐量,低延时的系统?面对这个问题,各位都有本身的答案。但面对这个问题,你们彷佛渐渐造成了一个共识:并发是解决之道。你们彷佛都这么认为:对于服务器而言,因为多核愈来愈广泛,所以咱们的程序必需要充分利用多线程,为了让多线程工做得更好,必须有一个与之匹配的高效的并发模型。因而各类各样的并发模型被提出来,好比Actor模型,好比SEDA模型(Actor模型的表弟),好比Software Transactional Memory模型(准确得讲,STM和其余两个模型所处在的视角是不同的,Actor和SEDA更可能是一种编程模型,而STM更相似一种思想,其实咱们经常使用到的Lock-Free机制都包含了STM的思想在里头)。html

这些模型获得了普遍讨论和应用。但这些模型都有一个讨厌之处-麻烦。这个麻烦是由多线程复杂的天性带来的,很难避免。除了麻烦,这些模型还忽视了另一个问题,因为这个问题的忽视,可能致使这些模型在解决高性能问题的道路上走到了一个错误的方向。这个问题就是JVM的伪共享问题。所谓JVM的伪共享,简单来讲,就是JVM的每个操做指令都是基于一个缓存行,同一个缓存行中的数据是不能同时被多个线程同时修改的,也就是说,若是多线程各自操做的数据位于同一个缓存行,那么这几个线程访问数据时实际上被加上了一把隐形的锁,它们实际上在顺序地访问数据。(若是你看过JDK Concurrent的实现,你能够看到有些类很奇怪得加了不少无用的padding成员,这就是为了填充缓存行,从而绕过JVM的伪共享)。因为JVM伪共享的存在,使得多线程在某些状况下成了一个摆设。这也就是说大多数状况下咱们的枪炮瞄错了方向,咱们一般认为没有充分利用多线程压榨多个CPU的能力是形成性能问题的缘由,实际上缓存问题才是性能杀手。java

因而LMAX就作了一个大胆的尝试。既然多线程在JVM中有可能成为摆设,并且又这么麻烦,那么干脆回到单线程来吧。用单线程来实现一个高吞吐量,低延时的系统?听起来很疯狂,但其实是可能的。LMAX就用单线程实现了一个吞吐量达到百万TPS的系统。程序员

这里讲LMAX是单线程,并非它彻底只有一个线程,LMAX组件仍是有用到多线程。只不过LMAX充分认识到了单线程的意义,在某些组件中大胆得采用单线程的架构,这就是LMAX所谓的单线程。LMAX决定组件是否采用单线程的依据很简单,若是某一个组件是IO密集型的,那么这个组件的设计就使用多线程。若是某一个组件是CPU密集型的,那么该组件就使用单线程的设计。这么作的理由很简单,IO密集型的组件的操做通常都很慢,每每会阻塞线程,所以使用多线程来竞争执行,有提升的余地。而CPU密集型的操做,若是采用多线程的设计,一方面可能会陷入JVM伪共享的陷阱,另外一方面多线程之间的同步会带来开发的复杂性,同时多线程会竞争某些资源,好比队列等等,这些竞争会对计算机cache命中形成扰动,并且有可能引入锁这种性能杀手,与这两点相比,多线程带来的好处至关有限,所以就采用单线程。数据库

【LMAX的原则】编程

LMAX的设计使人耳目一新,它的设计也向咱们分享了高性能计算中的几个重要经验或者说原则:缓存

1. 全部的架构师和开发人员都应该具备良好的Mechanical Sympathy(这个单词不太好翻译,“机制共鸣”?)所谓Mechanical Sympathy,实际上就是指架构设计者应该对现代操做系统,现代服务器的底层运行机制有良好的理解和认识,设计的时候充分考虑到这些机制,可以和它们产生共鸣。这很容易理解,若是一个架构师对底层机制的认识不够深刻或者还停留在过去,那么很难想象这样的架构师能设计出一个基于现代服务器的高效系统来。LMAX在文档中向咱们分享了几点对现代服务器的认识:安全

1.1 内存服务器

衡量内存有两个指标:Bandwidth和Latency。所谓bandwidth指的是内存在单位时间内经过内存总线的数据量,它计算公式是bandwidth = 传输倍率*总线位宽*工做频率/8,单位是Bytes/s(字节每秒)。传输倍率指的是内存在一个脉冲周期内传输数据的次数,好比DDR一个脉冲周期内能够在上升沿传递一次数据,在降低沿传递一次数据,而SDRAM只能在脉冲周期的上升沿传递数据。工做频率的是内存的工做频率,好比133Mhz等等。网络

而Latency是指内存总线发出访问请求到内存总线返回数据之间的延迟时间,单位是纳秒。

这两个指标描述了内存性能的两个方面,bandwidth描述了内存能够以多快的速度来传递数据,反映了吞吐量,而latency从更底层的细节描述了内存的物理性能。一个内存的bandwidth说明的仅仅是内存在内存边界的传输的速率,而数据在内存内部的流动速度是靠latency来决定。这就像赶飞机,bandwidth就好像是T3航站楼的门的大小,门的大小决定了T3每秒可以接纳的旅客数量。而安检的速度就是latency,它也会影响你最终登上飞机的时间。Bandwidth加上latency才能彻底描述内存整个环节的性能。最终内存的性能能够用内存性能 = (bandwidth*latency)来近似描述(Little's Law)。

尽管硬件技术一日千里,但这些年来,服务器内存的延迟并无发生数量级的变化。可是内存的bandwidth仍是得到了很大的进步,所以总体而言内存的性能仍是有比较大的提升。

另外内存的容量也是愈来愈大,144G大小的内存配置也是至关广泛。

1.2 CPU

对于CPU而言,单纯的提升主频的方法已经走到尽头,Intel的主频可能会在Ghz这个量级上停留很长的一段时间。

CPU的核数是愈来愈多,24核的服务器也很广泛。

CPU的缓存机制也愈来愈强大,一方面CPU缓存变大了,另外一方面Intel又提出了Smart cache等概念,相比于传统的L1 cache, L2 cache又提升了一步。

1.3 网络

服务器本地的网络响应时间很是快,处于sub 10 microseconds这个级别。(10 ms在操做系统中通常是一个时钟滴答,sub 10 microseconds意味着小于一个时钟滴答,咱们知道Linux的延时,线程切换都是基于时钟滴答的,也就是说本地网络速度是很快的,对于大多数的应用来说,几乎能够忽略不计)。

广域网的带宽是比较便宜的。

10GigE(10Gbps的以太网卡)的服务器很是广泛。

Multi-cast技术愈来愈获得关注,应用也愈来愈多。

1.4 存储

硬盘是新一代的磁带。磁盘对于顺序访问的速度是很是快的。
对于并发的随机访问,考虑采用SSD。

SSD的接口通常都是PCI总线接口,速度更快。

2. 把工做放到内存中来

尽量把一些数据都放到内存中来,避免和磁盘的低效交互。

3. 写的代码要缓存友好。

什么样的代码是缓存友好的代码?这个一言难尽。但总的原则就是,保持访问的局部性,也就是说尽量使一段时间内的访问保持在一个狭小的内存范围内。经常使用的一个作法就是,先统一分配一个对象池,而后复用对象池中的对象,不要每次都是从新分配新的对象。

clip_image002

上图显示了各个层次的缓存的访问效率,提醒咱们要对缓存敏感。

4. 要时刻牢记,代码要干净,简练

Hotspot虚拟机喜欢短小,简练的代码;

若是CPU的分支预测不许确,那么CPU流水线会被阻断;

复杂的代码是一个危险的信号,这意味着你有可能没有正确理解问题的领域(DDD里的概念);

世界上的事情都不会很复杂,除了扣税的方法。

5. 多花点时间考虑一下你的领域模型。

记住这么几个原则:

责任单一:一个类只干一件事,一个方法也只干一件事,不要臃肿的类或方法。

了解你的数据结构和关系基数(一对一的关系?一对多?仍是多对多)

让关系来完成工做,好比“书架”和“书”之间存在一个“attach”的关系,既然如此,咱们可让“书架”有一个方法叫attach,用来处理添加书本的工做,这就是让关系来完成工做。这实际上也是DDD里面的一些设计原则。

6. 采用正确的方法来实现并发。

实现并发须要考虑两件事:

资源互斥和变化可见(让结果以一个正确的顺序出现)

并发的实现通常有两种方法:

第一个方法是用锁来保证,另外一个方法是借助于CAS进行无锁编程。

使用锁会致使内核态的切换,但总能够确保任什么时候刻总有一个进程会被执行(相比之下Lock-Free若是代码逻辑出现问题,有可能全部线程都处在自旋等待状态,没法前进),锁也增长了编程的难度。

而借助于CAS的Lock-Free则始终是运行在用户态的(节省了效率,避免了无谓的上下文切换),相比于锁,它的编程难度更加大。下面图形象地表达了Lock和Lock-Free之间的区别:

clip_image004

这些原则大部分都是老生常谈,但很容易被人忽略,总之这些原则提醒咱们:

1) 不少程序员对现代服务器的硬件有着一个错误的认识或者根本没有认识,他们根本就不知道单线程所能达到的性能高度。

2) 对于现代处理器,缓存丢失才是性能的最大杀手。

3)架构设计时,把并发放到infrastructure层里去考虑,这样一方面使得应用层的编写避免了并发编程的复杂性,另外一方面因为并发放在了相对单纯的infrastructure层,避免了来自应用层的乱七八糟的干扰,更容易优化。

4) 牢记上述3条原则,一旦你实现了这3条,那恭喜你,你已经进入了理想王国:

单线程;

全部的一切都在内存中;

优雅的模式;

易于测试的代码;

不用担忧infrastructure和集成的问题。

太完美了!

【LMAX的架构

接下来就来谈谈LMAX具体的架构,LMAX正是基于上述原则下的产物。

clip_image006

LMAX的整体架构就如上图,它分为三个部分:

l Business logic processor

l Input disruptor

l Output disruptors

Business logic processor是一个单线程的java进程,用来处理方法调用,产生输出事件。因为它是一个单线程的简单的java程序,所以它除了JVM自己以外不依赖于任何其余的framework,这就使得咱们很容易把它放入一个测试环境进行测试,这就是所谓的“易于测试的代码”。

Input Disruptor是用来处理输入消息的,输入消息从网络中接收,须要进行反序列化(unmarshaled),须要进行replicated避免单点故障,须要journaled来记录消息日志从而可以进行故障恢复。

Output Disruptors用来处理输出消息,这些消息须要进行序列化以便于网络传输。

Input Disruptor和output disruptor都是多线程的,由于他们设计到大量的IO操做,这些IO操做很慢并且相互独立。

Business logic processor

Business logic processor的整个处理都是放在内存中的,这样带来的好处不少:

l 速度快,一切尽在内存,没有缓慢的磁盘IO

l 简单,由于所处理的都是java对象模型,不须要进行数据库和java对象之间的映射

因为一切尽在内存中,所以一个须要认真考虑的问题是若是Business logic processor发生了crash怎么办?Lmax解决这个问题的思路很直接,它在input distruptor上运行了一个按期的任务,该任务(journal任务)的职责就是在一个合适的时间(好比天天的夜里12点)生成一个输入信息的快照,只要有了这些输入信息的快照,processor在crash以后只要一被重启,它会从新再处理一遍这些输入信息,处理以后它就可以把整个系统带回到crash以前的最新状态。

Business logic processor还有一个优点就是易于诊断,好比团队发现了在线的产品上的一些问题,一个简单的作法是利用replicated任务产生的副本,把它放到一个安全的测试机器上,而后从新运行processor进行debug,这不会影响产品。也就是说business logic processor好像一台录像机,只要记录了拷贝,它能够在任何地方进行回放。这个特性很是吸引人,这也是为何近些年来event sourcing这样的方法获得重视的缘由。

为了达到性能最大化,Lmax甚至实现了本身的数据结构,好比本身的List等等。

Business logic processor的一个重要特色就是不和任何外部服务进行交互。由于调用外部服务的速度会比较慢,processor又是单线程的,所以外部服务会拖慢整个processor的速度。Processor只和event交互,要么接受一个event,要么产生一个event。怎么理解呢?举个例子就明白了,好比电商网站经过信用卡来订购商品。普通青年的作法就很直接,先获取订单信息,经过银联的外部服务来验证信用卡信息是否有效(这意味着信用卡号若是有问题,根本就不会生成订单),而后生成订单信息入库,这两步放在一个操做里。因为信用卡验证服务是一个外部服务,所以操做每每会被阻塞较长的一段时间。

Lmax则另辟蹊径,它把整个操做分为两个,第一个操做是获取用户填写的订单。这个操做的结果是产生一个“信用卡验证请求”的事件。第二个操做是当它接受一个“信用卡验证成功响应”的事件,生成订单入库。Processor在完成第一个操做以后会接下来执行另外其余的事件,直到“信用卡验证成功响应”事件被插入input disruptor并被processor选取。至于lmax如何根据“信用卡验证请求”输出事件生成另一个输入事件-“信用卡验证成功响应”,这则是经过output disruptor的多线程来完成的。所以能够看出lmax青睐单线程的态度并不执拗,而是有本身的原则:IO密集型操做用多线程,CPU密集型用单线程。

这样异步的工做方式带来一个问题是如何实现事务?相比传统作法,lmax须要作更多的工做。

Input and Output Disruptor

business logic processor是单线程工做的,在processor能够正常进行工做以前仍是有不少任务须要作的。Processor的输入本质上是网络消息,为了便于business logic processor处理,这些网络消息在送达processor以前须要进行反序列化(unmarshaled)。Event Sourcing的工做依赖于记录输入事件,所以输入消息的日志须要被持久化。

clip_image007

Figure 2: The activities done by the input disruptor (using UML activity diagram notation)

如上图,因为replicator和journaler涉及到大量的IO,所以速度相对比较慢。而business logic processor的中心思想就是避免任何IO。这三个任务相对比较独立,它们必须在business logic processor处理消息以前完成,所以这三个任务很适合并发。

为了处理这个并发,lmax开发了一个特殊的并发组件 – disruptor。

粗略理解,你能够把disruptor看做一组队列,生产者向某些队列中放入对象,这些对象会被广播发送到若干个独立分开的下行队列中,消费者就会并行得从这些下行队列中获取对象进行处理。若是你进一步深刻到disruptor的内部,你会发现实际上并非一组队列,而只是一个单独的数据结构-ring buffer。

clip_image009

Figure 3: The input disruptor coordinates one producer and four consumers

如上图,每一个生产者/消费者都拥有一个序号,这个序号表示该生产者/消费者正在处理ring buffer的那个slot。每一个生产者/消费者都只能拥有本身序号的写权限,对于其它消费者/生产者的序号只能读取而不能更改。基于这种方法,生产者能够不断读取其它消费者的序号来检查生产者想要写入的slot是否被占用,这种方法实际上就是的lock-free,避免了加锁。相似的,一个消费者也能够经过观察其余消费者的序号来确保不会重复处理某些消息。

Output disruptor和input disruptor是相似的,只不过output disruptor的两个消费者marshaller和publisher必须是顺序执行的,也就是说ring buffer里的消息必须通过marshaller处理以后才能由publisher公布出去。Publisher发布出去的事件被组织成了若干个topics,每一个事件只会被转发到订阅了该主题的receivers。

clip_image011

Figure 4: The LMAX architecture with the disruptors expanded

如上图,深绿色的模块表示生产者,深蓝色的模块表示消费者,output disruptor实际上包含了若干个ring buffer,每一个ring buffer对应一个topic,output disruptors 中的publisher和input disruptor中的receiver构成了一个典型的pub/sub系统,这个系统并无在图中显式注明。

上图中描述看起来整个系统彷佛都是单生产者+多消费者的模式,但实际上disruptor也可配置成多生产者+多消费者的模式,在这种模式下,input disruptor/output disruptor上的消息接收组件能够有若干个实例(每一个实例也有多是多线程的),即便在多生产者+多消费者模式下,disruptor依然不须要锁。

Lmax的这种设计带来一个好处,若是某个消费者发生了问题从而成为其它消费者的拖累,它也可以很快遇上来。仍是看Figure 3中的例子,假设un-marshaller在处理slot 15时产生了问题,速度特别慢,但一旦un-marshaller处理完了slot 15从其中脱身,那么下一次执行,它就会一次性读取slot 16到slot 31之间全部的数据从而加快消息的消费速度(至于消息的实际处理速度则是另一回事,一旦消息被读走了,它至少在ring buffer中再也不是拖累者)。

在Figure 3的例子中,journaler,replicator和un-marshaller各自只有一个实例,lmax在默认设置下的确是这样,可是lmax也能够运行多个组件实例,好比journaller组件能够运行两个实例,一个处理奇数slot,一个处理偶数slot。是否运行多个实例取决于IO操做的独立性和IO的阻塞时间。

Ring buffer是很大的,input ring buffer拥有20 million个slot,每一个output ring buffer也拥有4 million个slot。序号是一个64位的长整形。Ring Buffer的大小为2的整数次方,这样有利于作取余运算(sequence number % buffer size)把序号映射成slot号码。像不少其它的系统同样,disruptors天天深夜作按期的重启,这么作的主要缘由是回收内存,尽量下降在繁忙时段的昂贵的垃圾回收的可能性。

Journaler的主要工做就是持久化存储全部的事件,这样便于当系统出现故障时能够从日志进行恢复。Lmax没有用数据库来做为持久化存储,而只是采用文件系统。它们把事件流写入磁盘,因为现代磁盘对于顺序存储的速度很快,而对随机存储的速度很慢,所以lmax的这种作法的性能并不会不好,即便没有用数据库。

前面我提到lmax会运行多个实例节点组成一个cluster来支持快速failover。Replicator用来保持这些实例节点的同步。Lmax节点之间的全部通信采用的IP广播,所以备用节点不须要知道主节点的IP地址。只有主节点运行一个replicator并侦听输入事件。Replicator负责广播这些input event给备用节点。一旦主节点发生宕机,主节点的心跳信号就会丢失,那么另外一个备用节点就会变成主节点,接着这个新的主节点就会开始侦听输入事件,并启动本身的replcator。每一个节点是一个完整的lmax实例,有本身的disruptor,本身的journaler,本身的un-marshaller。

因为IP广播消息并不能确保消息的到达顺序。主节点负责决定广播消息的顺序。

Un-marshaller用于把网络上的事件顺序转化成business logic processor能够调用的java对象。和其它的消费者有所不一样,un-marshaller须要改变ring buffer中的数据。这里写(更改数据)时须要遵照一个原则,那就是每一个对象的writable field只能容许众多并行消费者(也就是un-marshaller)之中的一个来写,这个原则的目的就是为了不jvm的伪共享。

Disruptor能够做为一个单独的组件被使用,而不仅是用在lmax中,如今lmax已经开源了这个组件。做为一件金融交易软件公司,lmax的行为的确使人称道,也但愿更多的公司愿意交流或分享本身的架构,毕竟技术是在交流中促进的。回过头来看,乐意开源或者愿意分享的公司(好比在infoQ中分享)每每技术上都比较领先。从我的来说,技术人员也应该愿意进行分享,毕竟这是一个在业界创建本身声誉的好机会。

 

延伸阅读:Disruptor:High performance alternative to bounded queues for exchanging data between concurrent threads