LMAX 架构
LMAX 是一个新的零售金融交易平台。因此,它必须以低延迟处理大量交易。该系统构建在 JVM 平台之上,以一个可以处理每秒 600 万个订单的单线程业务逻辑处理器为中心。业务逻辑处理器完全在内存中运行,使用事件溯源。业务逻辑处理器周围环绕着中断器 - 一个并发组件,它实现了一个无需锁即可运行的队列网络。在设计过程中,团队得出结论,使用队列的最新高性能并发模型方向与现代 CPU 设计存在根本冲突。
2011 年 7 月 12 日
在过去几年中,我们一直在听到“免费午餐结束了”[1] - 我们不能指望单个 CPU 速度的提高。因此,要编写快速代码,我们需要显式地使用具有并发软件的多个处理器。这不是好消息 - 编写并发代码非常困难。锁和信号量很难推理,也很难测试 - 这意味着我们花费更多的时间担心满足计算机,而不是解决领域问题。各种并发模型,例如 Actor 和软件事务内存,旨在简化此过程 - 但仍然存在引入错误和复杂性的负担。
因此,我非常着迷地听到去年 3 月在伦敦 QCon 上关于 LMAX 的演讲。LMAX 是一个新的零售金融交易平台。它的业务创新在于它是一个零售平台 - 允许任何人交易各种金融衍生产品[2]。这样的交易平台需要非常低的延迟 - 交易必须快速处理,因为市场正在快速变化。零售平台增加了复杂性,因为它必须为很多人做到这一点。因此,结果是更多用户,有大量交易,所有这些都需要快速处理[3]。
鉴于向多核思维的转变,这种苛刻的性能自然会建议一个显式并发编程模型 - 事实上,这也是他们的起点。但让 QCon 上的人们关注的是,这并不是他们最终的结果。事实上,他们最终通过对平台的所有业务逻辑进行了处理:来自所有客户的所有交易,在所有市场 - 在单个线程上。一个使用商品硬件每秒处理 600 万个订单的线程[4]。
处理大量交易,具有低延迟,并且没有并发代码的复杂性 - 我怎么能抗拒深入研究呢?幸运的是,LMAX 与其他金融公司不同的一点是,他们很乐意谈论他们的技术决策。因此,现在 LMAX 已经投入生产一段时间了,现在是时候探索他们迷人的设计了。
整体结构
图 1:LMAX 的架构,分为三个部分
在顶层,架构有三个部分
- 业务逻辑处理器[5]
- 输入中断器
- 输出中断器
顾名思义,业务逻辑处理器处理应用程序中的所有业务逻辑。正如我上面提到的,它作为一个单线程 Java 程序执行此操作,该程序对方法调用做出反应并生成输出事件。因此,它是一个简单的 Java 程序,不需要任何其他平台框架来运行,除了 JVM 本身,这使得它可以轻松地在测试环境中运行。
虽然业务逻辑处理器可以在简单的环境中运行以进行测试,但要使其在生产环境中运行,还需要更多复杂的编排。输入消息需要从网络网关中取出并取消序列化、复制和记录。输出消息需要为网络序列化。这些任务由输入和输出中断器处理。与业务逻辑处理器不同,这些是并发组件,因为它们涉及既慢又独立的 IO 操作。它们是专门为 LMAX 设计和构建的,但它们(与整体架构一样)适用于其他地方。
业务逻辑处理器
将所有内容保存在内存中
业务逻辑处理器按顺序接收输入消息(以方法调用的形式),对其运行业务逻辑,并发出输出事件。它完全在内存中运行,没有数据库或其他持久存储。将所有数据保存在内存中具有两个重要优势。首先,它很快 - 没有数据库提供缓慢的 IO 来访问,也没有任何事务行为需要执行,因为所有处理都是按顺序完成的。第二个优势是它简化了编程 - 没有对象/关系映射需要做。所有代码都可以使用 Java 的对象模型编写,无需为映射到数据库做出任何妥协。
使用内存中结构有一个重要的后果 - 如果一切都崩溃了怎么办?即使是最有弹性的系统也容易受到有人拔掉电源的影响。处理这个问题的核心是事件溯源 - 这意味着业务逻辑处理器的当前状态完全可以通过处理输入事件来推导出。只要输入事件流保存在持久存储中(这是输入中断器的工作之一),你就可以通过重放事件来重建业务逻辑引擎的当前状态。
理解这一点的一个好方法是考虑版本控制系统。版本控制系统是一系列提交,在任何时候,你都可以通过应用这些提交来构建一个工作副本。VCS 比业务逻辑处理器更复杂,因为它们必须支持分支,而业务逻辑处理器是一个简单的序列。
因此,理论上,你始终可以通过重新处理所有事件来重建业务逻辑处理器的状态。然而,在实践中,如果你需要启动一个新的处理器,这将花费太长时间。因此,与版本控制系统一样,LMAX 可以创建业务逻辑处理器状态的快照,并从快照中恢复。他们在低活动期间每晚都会拍摄快照。重新启动业务逻辑处理器很快,完全重新启动 - 包括重新启动 JVM、加载最近的快照以及重放一天的日志 - 不到一分钟。
快照使启动新的业务逻辑处理器更快,但如果业务逻辑处理器在下午 2 点崩溃,则不够快。因此,LMAX 一直保持多个业务逻辑处理器运行[6]。每个输入事件都由多个处理器处理,但除了一个处理器之外,其他所有处理器的输出都被忽略。如果实时处理器发生故障,系统将切换到另一个处理器。这种处理故障转移的能力是使用事件溯源的另一个好处。
通过将事件溯源到副本中,他们可以在几微秒内在处理器之间切换。除了每晚拍摄快照之外,他们还每晚重新启动业务逻辑处理器。复制使他们能够在没有停机的情况下做到这一点,因此他们可以继续 24/7 处理交易。
事件溯源很有价值,因为它允许处理器完全在内存中运行,但它对诊断还有另一个相当大的优势。如果发生一些意外行为,团队会将事件序列复制到他们的开发环境中并在那里重放。这使他们能够比大多数环境中更容易地检查发生的事情。
这种诊断功能扩展到业务诊断。某些业务任务,例如风险管理,需要大量的计算,而这些计算对于处理订单来说是不必要的。例如,根据客户当前的交易头寸,获取按风险概况排名前 20 位的客户列表。团队通过启动一个复制的领域模型并在那里执行计算来处理这个问题,这样就不会干扰核心订单处理。这些分析领域模型可以具有不同的数据模型,在内存中保留不同的数据集,并在不同的机器上运行。
调整性能
到目前为止,我已经解释了业务逻辑处理器速度的关键在于按顺序、在内存中完成所有操作。仅仅做到这一点(以及不做任何愚蠢的事情)就可以让开发人员编写可以处理 10K TPS[7] 的代码。然后他们发现,专注于良好代码的简单元素可以将此提升到 100K TPS 范围。这只需要结构良好的代码和小型方法 - 本质上,这允许 Hotspot 更好地优化,并使 CPU 在运行代码时更有效地缓存代码。
要再提高一个数量级,需要更多的聪明才智。LMAX 团队发现了几件事有助于他们实现这一目标。其中之一是编写自定义的 Java 集合实现,这些实现旨在对缓存友好,并且对垃圾[8] 谨慎。一个例子是使用原始 Java 长整型作为哈希映射键,并使用专门编写的数组支持的 Map 实现(LongToObjectHashMap
)。总的来说,他们发现数据结构的选择通常会产生很大的影响。大多数程序员只是抓取他们上次使用的任何 List,而不是考虑哪种实现是适合当前上下文的实现[9]。
达到最高性能水平的另一种技术是关注性能测试。我早就注意到,人们谈论了很多提高性能的技术,但真正能产生影响的是测试它。即使是优秀的程序员也很擅长构建最终被证明是错误的性能论据,因此最好的程序员更喜欢使用分析器和测试用例而不是猜测[10]。LMAX 团队还发现,先编写测试对于性能测试来说是一种非常有效的纪律。
编程模型
这种处理风格确实对编写和组织业务逻辑的方式引入了一些约束。第一个约束是,你必须找出与外部服务的任何交互。外部服务调用将很慢,并且在单线程情况下会停止整个订单处理机器。因此,你不能在业务逻辑中调用外部服务。相反,你需要用一个输出事件来完成该交互,并等待另一个输入事件来重新拾取它。
我将使用一个简单的非 LMAX 示例来说明。假设你正在用信用卡订购果冻豆。一个简单的零售系统会获取你的订单信息,使用信用卡验证服务来检查你的信用卡号码,然后确认你的订单 - 所有这些都在一个操作中完成。处理你订单的线程会在等待信用卡被检查时阻塞,但对于用户来说,这种阻塞不会很长,并且服务器始终可以在等待时在处理器上运行另一个线程。
在 LMAX 架构中,你会将此操作分成两个部分。第一个操作会捕获订单信息,并通过向信用卡公司输出一个事件(请求信用卡验证)来结束。然后,业务逻辑处理器会继续处理其他客户的事件,直到它在输入事件流中接收到一个信用卡验证事件。在处理该事件时,它会执行该订单的确认任务。
在这种事件驱动、异步风格下工作,有点不寻常——尽管使用异步性来提高应用程序的响应能力是一种熟悉的技术。它还有助于业务流程更具弹性,因为您必须更明确地思考远程应用程序可能发生的各种情况。
编程模型的第二个特点在于错误处理。传统的会话和数据库事务模型提供了一个有用的错误处理功能。如果出现任何问题,很容易丢弃到目前为止交互中发生的所有内容。会话数据是短暂的,可以丢弃,代价是如果用户在执行复杂操作时,可能会感到一些困扰。如果数据库端出现错误,您可以回滚事务。
LMAX 的内存中结构在输入事件中是持久存在的,因此如果出现错误,重要的是不要将该内存置于不一致的状态。但是,没有自动回滚机制。因此,LMAX 团队非常重视确保输入事件在对内存中持久状态进行任何修改之前完全有效。他们发现,测试是解决这些问题并在投入生产之前将其清除的关键工具。
输入和输出中断器
虽然业务逻辑发生在一个线程中,但在调用业务对象方法之前,还有很多任务需要完成。处理的原始输入以消息的形式从网络中获取,此消息需要被反序列化为业务逻辑处理器方便使用的形式。事件溯源依赖于保存所有输入事件的持久日志,因此每个输入消息都需要被记录到持久存储中。最后,该架构依赖于业务逻辑处理器的集群,因此我们必须将输入消息复制到整个集群中。类似地,在输出端,输出事件需要被序列化以通过网络传输。
图 2:输入中断器执行的活动(使用 UML 活动图符号)
复制器和日志记录器涉及 IO,因此速度相对较慢。毕竟,业务逻辑处理器的核心思想是避免进行任何 IO。此外,这三个任务相对独立,它们都需要在业务逻辑处理器处理消息之前完成,但它们可以按任何顺序完成。因此,与业务逻辑处理器不同,每个交易都会改变后续交易的市场,这里自然适合并发。
为了处理这种并发,LMAX 团队开发了一个特殊的并发组件,他们称之为 **中断器**[11]。
在粗略的层面上,您可以将中断器视为队列的多播图,生产者将对象放入其中,这些对象被发送到所有消费者,以便通过单独的下游队列并行消费。当您查看内部时,您会发现这个队列网络实际上是一个单一的数据结构——环形缓冲区。每个生产者和消费者都有一个序列计数器,用于指示它当前正在处理的缓冲区中的哪个槽。每个生产者/消费者都写入自己的序列计数器,但可以读取其他计数器。这样,生产者可以读取消费者的计数器,以确保它想要写入的槽可用,而无需对计数器进行任何锁定。类似地,消费者可以通过观察计数器来确保它只处理其他消费者完成的消息。
图 3:输入中断器协调一个生产者和四个消费者
输出中断器类似,但它们只有两个顺序消费者用于序列化和输出。[12] 输出事件被组织成多个主题,以便消息可以仅发送给对它们感兴趣的接收者。每个主题都有自己的中断器。
我描述的中断器以一个生产者和多个消费者的方式使用,但这并不是中断器设计上的限制。中断器也可以与多个生产者一起工作,在这种情况下,它仍然不需要锁定。[13]
中断器设计的一个好处是,如果消费者遇到问题而落后,它更容易快速赶上。如果反序列化器在处理槽 15 时遇到问题,并在接收器位于槽 31 时返回,它可以一次性读取槽 16-30 中的数据以赶上。这种从中断器中批量读取数据的方式使落后的消费者更容易快速赶上,从而减少了总体延迟。
我在这里描述了日志记录器、复制器和反序列化器各一个——这确实是 LMAX 所做的。但该设计允许这些组件中的多个运行。如果您运行两个日志记录器,那么一个将占用偶数槽,另一个日志记录器将占用奇数槽。如果需要,这将允许这些 IO 操作进一步并发。
环形缓冲区很大:输入缓冲区为 2000 万个槽,每个输出缓冲区为 400 万个槽。序列计数器是 64 位长整数,即使环形槽包裹,它们也会单调递增。[14] 缓冲区的大小设置为 2 的幂,因此编译器可以执行有效的模运算,以将序列计数器编号映射到槽编号。与系统其他部分一样,中断器在夜间被重启。这种重启主要是为了清除内存,以便在交易期间减少昂贵的垃圾回收事件的可能性。(我也认为定期重启是一个好习惯,这样您就可以练习如何在紧急情况下进行重启。)
日志记录器的任务是将所有事件存储在持久形式中,以便在出现任何问题时可以重播它们。LMAX 不使用数据库来完成此操作,只使用文件系统。他们将事件流式传输到磁盘。用现代术语来说,机械磁盘对于随机访问来说速度非常慢,但对于流式传输来说速度非常快——因此有了“磁盘是新的磁带”的口号。[15]
之前我提到过 LMAX 在集群中运行其系统的多个副本,以支持快速故障转移。复制器使这些节点保持同步。LMAX 中的所有通信都使用 IP 多播,因此客户端不需要知道哪个 IP 地址是领导节点。只有领导节点直接监听输入事件并运行复制器。复制器将输入事件广播到跟随节点。如果领导节点出现故障,它将无法发送心跳,另一个节点将成为领导节点,开始处理输入事件,并启动其复制器。每个节点都有自己的输入中断器,因此有自己的日志并执行自己的反序列化。
即使使用 IP 多播,仍然需要复制,因为 IP 消息可能在不同节点上以不同的顺序到达。领导节点为其余处理提供确定性序列。
反序列化器将来自网络的事件数据转换为可以用于在业务逻辑处理器上调用行为的 Java 对象。因此,与其他消费者不同,它需要修改环形缓冲区中的数据,以便它可以存储此反序列化的对象。这里的规则是,消费者被允许写入环形缓冲区,但每个可写字段只能有一个并行消费者被允许写入它。这保留了只有一个写入者的原则。[16]
图 4:扩展了中断器的 LMAX 架构
中断器是一个通用组件,可以在 LMAX 系统之外使用。通常,金融公司对自己的系统非常保密,即使对于与业务无关的项目也保持沉默。LMAX 不仅公开其整体架构,还开源了中断器代码——这让我非常高兴。这不仅将允许其他组织使用中断器,还将允许对它的并发特性进行更多测试。
队列及其缺乏机械同情
LMAX 架构引起了人们的注意,因为它是一种与大多数人正在考虑的高性能系统截然不同的方法。到目前为止,我已经讨论了它的工作原理,但没有深入探讨为什么以这种方式开发它。这个故事本身很有趣,因为这种架构并非凭空出现。在团队确定使用这种架构之前,他们花了很长时间尝试了更传统的替代方案,并认识到它们的缺陷。
如今,大多数业务系统都具有一个核心架构,该架构依赖于通过事务性数据库协调的多个活动会话。LMAX 团队熟悉这种方法,并确信它不适用于 LMAX。这种评估是基于 Betfair 的经验——Betfair 是创建 LMAX 的母公司。Betfair 是一个博彩网站,允许人们对体育赛事进行下注。它处理非常高的流量,并且存在大量争用——体育博彩往往会在特定事件周围爆发。为了使它正常工作,他们拥有最热门的数据库安装之一,并且不得不做很多不自然的事情才能使其正常工作。基于这种经验,他们知道维护 Betfair 的性能有多困难,并且确信这种架构不适合交易网站所需的极低延迟。因此,他们不得不寻找不同的方法。
他们最初的方法是遵循如今许多人都在说的话——要获得高性能,您需要使用显式并发。对于这种情况,这意味着允许订单由多个线程并行处理。但是,正如并发中经常出现的情况一样,困难在于这些线程必须相互通信。处理订单会改变市场状况,而这些状况需要被传达。
他们早期探索的方法是 Actor 模型及其近亲 SEDA。Actor 模型依赖于独立的、活动的、拥有自己线程的对象,它们通过队列相互通信。许多人发现这种并发模型比尝试基于锁定原语做一些事情更容易处理。
该团队使用 Actor 模型构建了一个原型交易所,并对其进行了性能测试。他们发现,处理器花费更多的时间来管理队列,而不是执行应用程序的实际逻辑。队列访问是一个瓶颈。
当像这样推动性能时,开始变得重要,要考虑现代硬件的构建方式。马丁·汤普森喜欢使用的短语是“机械同情”。这个词来自赛车,它反映了车手对汽车的本能感觉,因此他们能够感觉到如何充分发挥汽车的潜力。许多程序员,我承认我属于这个阵营,对编程如何与硬件交互没有多少机械同情。更糟糕的是,许多程序员认为他们有机械同情,但它建立在关于硬件过去如何工作的观念之上,而这些观念现在已经过时很多年了。
现代 CPU 影响延迟的主要因素之一是 CPU 如何与内存交互。如今,访问主内存对于 CPU 来说是一个非常慢的操作。CPU 有多个级别的缓存,每个缓存都比前一个快得多。因此,为了提高速度,您希望将代码和数据放入这些缓存中。
在某种程度上,Actor 模型在这里有所帮助。您可以将 Actor 视为一个独立的对象,它将代码和数据聚集成一个集群,这是一个自然的缓存单元。但 Actor 需要通信,它们通过队列进行通信——而 LMAX 团队观察到,正是队列干扰了缓存。
解释如下:为了将一些数据放入队列,您需要写入该队列。类似地,要从队列中取出数据,您需要写入队列以执行删除操作。这就是写入争用——多个客户端可能需要写入相同的数据结构。为了处理写入争用,队列通常使用锁。但是,如果使用锁,则会导致上下文切换到内核。发生这种情况时,涉及的处理器可能会丢失其缓存中的数据。
他们得出的结论是,为了获得最佳的缓存行为,你需要一个设计,该设计只有一个核心写入任何内存位置[17]。多个读取器是可以的,处理器通常在它们的缓存之间使用特殊的高速链接。但是队列不符合单写入原则。
这项分析让 LMAX 团队得出了一些结论。首先,它导致了 disruptor 的设计,该设计坚定地遵循单写入约束。其次,它导致了探索单线程业务逻辑方法的想法,并提出了一个问题:如果一个线程摆脱了并发管理,它能有多快?
在单线程上工作的本质是确保你有一个线程在一个核心上运行,缓存预热,并且尽可能多的内存访问都去缓存而不是主内存。这意味着代码和工作集数据都需要尽可能一致地访问。此外,将小对象与代码和数据放在一起,可以使它们作为一个单元在缓存之间交换,简化缓存管理,并再次提高性能。
通往 LMAX 架构的关键部分是使用性能测试。对基于 Actor 的方法的考虑和放弃来自构建和性能测试原型。同样,改进各种组件性能的许多步骤都是由性能测试实现的。机械同情非常有价值——它有助于形成关于你可以进行哪些改进的假设,并引导你向前迈进而不是后退——但最终,测试会给你令人信服的证据。
然而,这种风格的性能测试并不是一个被很好理解的话题。LMAX 团队经常强调,想出有意义的性能测试通常比开发生产代码更难。同样,机械同情对于开发正确的测试很重要。除非你考虑了 CPU 的缓存行为,否则测试低级并发组件是没有意义的。
一个特别的教训是,编写针对空组件的测试的重要性,以确保性能测试足够快,能够真正衡量真实组件的行为。编写快速的测试代码并不比编写快速的生产代码容易,而且很容易得到错误的结果,因为测试没有组件快,它试图衡量。
你应该使用这种架构吗?
乍一看,这种架构似乎只适用于一个非常小的利基市场。毕竟,导致它的驱动因素是能够以非常低的延迟运行大量复杂的事务——大多数应用程序不需要以 600 万 TPS 的速度运行。
但让我着迷的是这个应用程序,他们最终设计了一个架构,它消除了困扰许多软件项目的许多编程复杂性。围绕事务数据库的传统并发会话模型并非没有麻烦。通常,与数据库的关系需要付出相当大的努力。对象/关系映射工具可以帮助解决处理数据库的大部分痛苦,但它并不能解决所有问题。大多数企业应用程序的性能调整都涉及围绕 SQL 调整。
如今,你可以在你的服务器中获得比我们这些老家伙所能获得的磁盘空间更多的主内存。越来越多的应用程序能够将它们的所有工作集放在主内存中——从而消除了复杂性和迟缓的来源。事件溯源提供了一种为内存系统解决持久性问题的方法,在单线程中运行所有内容解决了并发问题。LMAX 的经验表明,只要你的 TPS 不超过几百万,你将拥有足够的性能空间。
这里与人们对 CQRS 的兴趣日益增长有相当大的重叠。基于事件的内存处理器是 CQRS 系统命令端的自然选择。(尽管 LMAX 团队目前没有使用 CQRS。)
那么,什么表明你不应该走这条路呢?对于像这样的鲜为人知的技术来说,这始终是一个棘手的问题,因为该行业需要更多时间来探索其边界。然而,一个起点是考虑鼓励这种架构的特征。
一个特征是,这是一个连接的领域,其中处理一个事务总是可能改变后续事务的处理方式。对于彼此更加独立的事务,协调的必要性较小,因此使用并行运行的独立处理器变得更有吸引力。
LMAX 专注于弄清楚事件如何改变世界的后果。许多网站更多的是关于获取现有的信息存储并将其各种组合呈现给尽可能多的眼球——例如,想想任何媒体网站。在这里,架构挑战通常集中在正确地获取缓存。
LMAX 的另一个特征是,这是一个后端系统,因此考虑它是否适用于以交互模式运行的系统是合理的。越来越多的 Web 应用程序帮助我们习惯了对请求做出反应的服务器系统,这方面与这种架构非常吻合。这种架构比大多数此类系统更进一步的是它绝对使用异步通信,导致我之前概述的编程模型的更改。
对于大多数团队来说,这些变化需要一些时间来适应。大多数人倾向于以同步的方式思考编程,并且不习惯处理异步。然而,长期以来,异步通信一直是响应能力的必不可少的工具。有趣的是,看看 JavaScript 世界中异步通信的更广泛使用,包括 AJAX 和 node.js,是否会鼓励更多人研究这种风格。LMAX 团队发现,虽然适应异步风格需要一些时间,但它很快变得自然,而且通常更容易。特别是,在这种方法下,错误处理更容易处理。
LMAX 团队肯定认为,协调事务数据库的时代已经屈指可数。能够使用这种架构更轻松地编写软件,并且它运行得更快,消除了对传统中央数据库的大部分理由。
就我而言,我发现这是一个非常激动人心的故事。我的许多目标都是专注于对复杂领域进行建模的软件。像这样的架构提供了良好的关注点分离,允许人们专注于领域驱动设计,并将大部分平台复杂性很好地分离。领域对象和数据库之间的紧密耦合一直是一个令人恼火的问题——像这样的方法提供了一种摆脱困境的方法。
脚注
1: 免费午餐结束了
这是 Herb Sutter 的一篇 著名文章 的标题。他将“免费午餐”描述为处理器不断增加的时钟速度,它定期为我们提供每年更多的 CPU 性能。他的观点是,这种时钟周期增加将不再发生,相反,性能提升将来自多个核心。但要利用多个核心,你需要能够并发工作的软件——因此,如果没有编程风格的转变,人们将不再免费获得性能午餐。
2: 我将对我对这项创新的价值的看法保持沉默
3: 用户群
所有交易系统都需要低延迟,因为一次交易会影响以后的交易,并且存在大量基于快速反应的竞争。大多数交易平台都是为专业人士——银行、经纪人等——设计的,通常有数百名用户。零售系统有可能拥有更多用户,Betfair 有数百万用户,LMAX 是为这种规模设计的。(LMAX 团队不允许透露其实际交易量。)
事实证明,虽然零售系统有很多用户,但大多数活动来自 做市商。在波动时期,一种工具每秒可以获得数百次更新,在单个微秒内出现数百次交易的异常微爆发。
4: 硬件
600 万 TPS 基准是在一台配备 32GB RAM 的 3Ghz 双插槽四核 Nehalem 基于戴尔的服务器上测量的。
5: 该团队没有使用“业务逻辑处理器”这个名称,事实上,他们没有为该组件命名,只是将其称为业务逻辑或核心服务。我给它起了个名字,以便在这篇文章中更容易谈论它。
6: 目前,LMAX 在其主要数据中心运行两个业务逻辑处理器,并在灾难恢复站点运行第三个。所有三个都处理输入事件。
7: 交易中有什么
当人们谈论交易时间时,其中一个问题是交易中究竟有什么。在某些情况下,它只不过是在数据库中插入一条新记录。LMAX 的交易相当复杂,比典型的零售销售更复杂。
在交易所下订单涉及
- 检查目标市场是否开放接受订单
- 检查订单对于该市场是否有效
- 为订单类型选择正确的匹配策略
- 对订单进行排序,以便每个订单都以最佳价格匹配,并与正确的流动性匹配
- 创建和公布由于匹配而产生的交易
- 根据新交易更新价格
8: 在这个延迟规模下,你必须注意垃圾收集器。对于如今几乎所有系统来说,现代 GC 压缩不会对性能产生任何明显的影响。但是,当你试图以最小的抖动每秒处理数百万个事务时,GC 暂停就会成为问题。要记住的是,短命对象是可以的,因为它们会很快被收集。永久对象也是如此,因为它们将永远存在。有问题的对象是那些会被提升到旧一代,但最终会死亡的对象。当这会使旧一代区域变得碎片化时,它将触发压缩。
9: 我很少考虑使用哪个集合实现。当你不在性能关键代码中时,这是完全合理的。不同的上下文表明不同的行为。
10: 一个有趣的旁注。虽然 LMAX 团队与当前对函数式编程的兴趣有很多共同之处,但他们认为,面向对象的方法为这种问题提供了一种更好的方法。他们注意到,当他们努力编写更快的代码时,他们会从函数式风格转向面向对象风格。部分原因是函数式风格为了保持不变性而需要复制数据。但这也是因为对象提供了更复杂的域的更好模型,并提供了更丰富的选择的数据结构。
11: “disruptor”这个名字的灵感来自几个来源。一个是 LMAX 团队将这个组件视为颠覆当前并发思维的东西。另一个是对 Java 引入 phaser 的回应,因此自然地也包括 disruptor。
12: 也可以记录输出事件。这将具有不需要重新计算它们的优势,如果需要为下游服务重播它们。然而,在实践中,这并不值得。业务逻辑是确定性的,而且非常快,因此从存储结果中没有收益。
13: 虽然在这种情况下它确实需要使用 CAS 指令。有关更多信息,请参阅 disruptor 技术论文。
14: 这意味着,如果他们每秒处理十亿个事务,计数器将在 292 年后回绕,导致一些地狱般的混乱。他们认为修复这个问题并不优先。
15: SSD 在随机访问方面更好,但类似磁盘的 IO 系统会减慢它们的速度。
16: 在写入字段时,另一个复杂之处是,你必须确保要写入的任何字段都分离到不同的缓存行中。
17: 确保对内存位置进行单写入
遵循单一写入原则的一个复杂之处在于,处理器不会一次只获取一个内存位置。相反,它们会一次性将多个连续的位置(称为缓存行)收集到缓存中。以缓存行块的形式访问内存显然效率更高,但也意味着您必须确保该缓存行中没有由不同核心写入的位置。因此,例如,Disruptor 的序列计数器被填充以确保它们出现在不同的缓存行中。
致谢
金融机构通常对他们的技术工作保密,通常没有充分的理由。这是一个问题,因为它阻碍了该行业从经验中学习的能力。因此,我特别感谢 LMAX 在讨论他们的经验方面的开放性——无论是本文还是他们的其他材料。
Disruptor 的主要创建者是 Martin Thompson、Mike Barker 和 Dave Farley。
Martin Thompson 和 Dave Farley 为我详细介绍了 LMAX 架构,该架构是本文的基础。他们还迅速回复了电子邮件问题,以改进我的早期草稿。
并发编程是一个棘手的领域,需要大量的关注才能胜任——而我还没有付出这种努力。因此,我对并发理解完全依赖于他人,并感谢他们的耐心指导。
进一步阅读
如果您想从 LMAX 团队成员那里获得 LMAX 架构的视频描述,最好的选择是 QCon 演示,该演示由 Martin Thompson 和 Michael Barker 于 2010 年在旧金山发表。
您可以获得 Disruptor 的源代码,它是开源的。还有一个很好的 技术论文 (pdf),它更深入地介绍了该主题,以及一系列关于它的 博客和文章。
LMAX 团队的各个成员都有自己的博客:Martin Thompson、Michael Barker 和 Trisha Gee。
重大修订
2011 年 7 月 12 日:首次出版
2011 年 6 月 22 日:开始起草