莫斯科交易所交易和清算系统架构的演变。 第2部分

莫斯科交易所交易和清算系统架构的演变。 第2部分

这是一个漫长故事的延续,讲述了我们创建一个强大的高负载系统以确保交易所运行的荆棘之路。 第一部分在这里: habr.com/en/post/444300

神秘错误

经过多次测试,更新后的交易和清算系统投入运行,我们遇到了一个可以写一个侦探神秘故事的错误。

在主服务器上启动后不久,其中一笔交易处理时出现错误。 然而,备份服务器上一切正常。 事实证明,在主服务器上计算指数的简单数学运算给出了真实参数的负结果! 我们继续研究,在 SSE2 寄存器中,我们发现了一位差异,该位负责在处理浮点数时进行舍入。

我们编写了一个简单的测试实用程序来计算带有舍入位设置的指数。 事实证明,在我们使用的 RedHat Linux 版本中,当插入不幸的位时,在使用数学函数时会出现错误。 我们向 RedHat 报告了这一情况,过了一段时间,我们收到了他们的补丁并推出了它。 错误不再出现,但不清楚这个位是从哪里来的? 该函数负责它 fesetround 来自C语言。我们仔细分析了我们的代码以寻找假定的错误:我们检查了所有可能的情况; 查看所有使用舍入的函数; 尝试重现失败的会话; 使用具有不同选项的不同编译器; 使用静态和动态分析。

无法找到错误原因。

然后他们开始检查硬件:对处理器进行负载测试; 检查内存; 我们甚至针对一个单元中出现多位错误这一极不可能的情况进行了测试。 无济于事。

最后,我们采用了高能物理领域的理论:一些高能粒子飞入我们的数据中心,刺穿机箱壁,撞击处理器并导致触发闩锁卡在其中。 这个荒谬的理论被称为“中微子”。 如果您远离粒子物理学:中微子几乎不与外界相互作用,并且肯定无法影响处理器的运行。

由于无法找到故障原因,为了以防万一,“有问题”的服务器被停止运行。

一段时间后,我们开始改进热备份系统:我们引入了所谓的“热储备”(warm)——异步副本。 他们收到了可能位于不同数据中心的交易流,但 Warms 并没有主动与其他服务器交互。

莫斯科交易所交易和清算系统架构的演变。 第2部分

为什么要这样做? 如果备份服务器发生故障,则与主服务器相连的热服务器将成为新的备份服务器。 也就是说,发生故障后,系统不会保留在一台主服务器上,直到交易时段结束。

而当新版本系统测试投入运行时,再次出现舍入位错误。 而且,随着热服务器数量的增加,该错误开始更频繁地出现。 与此同时,由于没有具体证据,供应商也没有什么可展示的。

在接下来的情况分析中,出现了一个理论:问题可能与操作系统有关。 我们编写了一个简单的程序,在无限循环中调用函数 fesetround,记住当前状态并通过睡眠检查它,这是在许多竞争线程中完成的。 选择睡眠参数和线程数后,我们在运行该实用程序约 5 分钟后开始一致地重现位故障。 然而,红帽支持无法重现它。 对我们其他服务器的测试表明,只有那些具有某些处理器的服务器才容易受到该错误的影响。 同时,切换到新内核解决了问题。 最后,我们只是简单地更换了操作系统,而导致该错误的真正原因仍不清楚。

去年突然在哈布雷上发表了一篇文章“我如何发现 Intel Skylake 处理器中的错误” 里面描述的情况和我们的非常相似,但是作者进一步调查并提出了错误出在微代码中的理论。 当Linux内核更新时,制造商也会更新微代码。

系统的进一步发展

虽然我们摆脱了错误,但这个故事迫使我们重新考虑系统架构。 毕竟,我们无法避免此类错误的重复发生。

以下原则构成了预订系统下一步改进的基础:

  • 你不能相信任何人。 服务器可能无法正常运行。
  • 多数保留。
  • 确保达成共识。 作为对多数保留的逻辑补充。
  • 双重失败是可能的。
  • 活力。 新的双机热备方案应该不会比之前的方案差。 交易应不间断地进行,直到最后一个服务器。
  • 延迟略有增加。 任何停机都会造成巨大的经济损失。
  • 最少的网络交互以尽可能降低延迟。
  • 在几秒钟内选择新的主服务器。

市场上现有的解决方案都不适合我们,而且 Raft 协议仍处于起步阶段,因此我们创建了自己的解决方案。

莫斯科交易所交易和清算系统架构的演变。 第2部分

联网

除了预订系统之外,我们还开始实现网络交互的现代化。 I/O子系统由许多进程组成,对抖动和延迟的影响最严重。 由于有数百个进程处理 TCP 连接,我们被迫不断地在它们之间切换,而在微秒级别上,这是一个相当耗时的操作。 但最糟糕的是,当进程收到要处理的数据包时,它会将其发送到一个 SystemV 队列,然后等待来自另一个 SystemV 队列的事件。 然而,当存在大量节点时,一个进程中新 TCP 数据包的到达和另一进程中队列中数据的接收对于操作系统来说是两个相互竞争的事件。 在这种情况下,如果没有可用于这两个任务的物理处理器,则将处理一个任务,并将第二个任务放入等待队列中。 无法预测后果。

在这种情况下,可以使用动态进程优先级控制,但这将需要使用资源密集型系统调用。 于是,我们改用经典的epoll的单线程,这大大提高了速度并减少了事务处理时间。 我们还摆脱了单独的网络通信进程和通过SystemV进行的通信,显着减少了系统调用的数量并开始控制操作的优先级。 仅在 I/O 子系统上,就可以节省大约 8-17 微秒,具体取决于具体情况。 从那时起,这种单线程方案就一直沿用不变;一个带有余量的 epoll 线程足以为所有连接提供服务。

事务处理

我们系统上不断增长的负载需要升级几乎所有组件。 但不幸的是,近年来处理器时钟速度增长的停滞已经不再能够正面扩展流程。 因此,我们决定将引擎流程分为三个级别,其中最繁忙的是风险检查系统,该系统评估账户中资金的可用性并自行创建交易。 但资金可以是不同的货币,因此有必要弄清楚应在什么基础上划分请求的处理。

合理的解决方案是将其按货币划分:一台服务器以美元进行交易,另一台服务器以英镑进行交易,第三台服务器以欧元进行交易。 但如果采用这样的方案,发送两笔交易来购买不同的货币,那么就会出现钱包不同步的问题。 但同步既困难又昂贵。 因此,按钱包和工具分别分片是正确的。 顺便说一句,大多数西方交易所没有像我们那样严格检查风险的任务,因此大多数情况下都是在线下完成的。 我们需要实施在线验证。

让我们用一个例子来解释一下。 交易者想要购买 30 美元,请求进入交易验证:我们检查该交易者是否被允许使用这种交易模式以及他是否拥有必要的权限。 如果一切正常,请求将发送至风险验证系统,即检查资金是否足以完成交易。 有一条说明,所需金额目前已被冻结。 然后请求被转发到交易系统,交易系统批准或拒绝交易。 假设交易获得批准,那么风险验证系统就会标记资金已解锁,卢布就会变成美元。

一般来说,风险检查系统包含复杂的算法并执行大量非常消耗资源的计算,并且并不像乍一看那样简单地检查“帐户余额”。

当我们开始将Engine流程分层时,我们遇到了一个问题:当时可用的代码在验证和验证阶段主动使用相同的数据数组,这需要重写整个代码库。 因此,我们借鉴了现代处理器的一种处理指令的技术:每个指令都被分为小阶段,并且在一个周期内并行执行多个操作。

莫斯科交易所交易和清算系统架构的演变。 第2部分

经过对代码的小修改,我们创建了一个用于并行事务处理的管道,其中事务被分为管道的 4 个阶段:网络交互、验证、执行和结果发布

莫斯科交易所交易和清算系统架构的演变。 第2部分

让我们看一个例子。 我们有两个处理系统,串行和并行。 第一个交易到达并发送到两个系统中进行验证。 第二个事务立即到达:在并行系统中,它立即开始工作,而在顺序系统中,它被放入队列中,等待第一个事务完成当前处理阶段。 也就是说,管道处理的主要优点是我们可以更快地处理事务队列。

这就是我们提出 ASTS+ 系统的原因。

确实,传送带也并非一切都那么顺利。 假设我们有一个交易影响相邻交易中的数据数组;这是交换的典型情况。 这样的事务不能在管道中执行,因为它可能会影响其他事务。 这种情况称为数据危险,此类事务只需单独处理:当队列中的“快”事务用完时,管道停止,系统处理“慢”事务,然后再次启动管道。 幸运的是,此类事务在整体流量中所占的比例很小,因此管道很少停止,不会影响整体性能。

莫斯科交易所交易和清算系统架构的演变。 第2部分

然后我们开始解决同步三个线程执行的问题。 结果是一个基于具有固定大小单元的环形缓冲区的系统。 在这个系统中,一切都取决于处理速度;数据不会被复制。

  • 所有传入的网络数据包都进入分配阶段。
  • 我们将它们放入一个数组中,并将它们标记为可用于第 1 阶段。
  • 第二笔交易已到达,再次可用于第一阶段。
  • 第一个处理线程查看可用事务,处理它们,并将它们移动到第二个处理线程的下一个阶段。
  • 然后,它处理第一个事务并标记相应的单元 deleted - 现在可以用于新用途。

整个队列都是这样处理的。

莫斯科交易所交易和清算系统架构的演变。 第2部分

每个阶段的处理需要单位或几十微秒。 如果我们使用标准操作系统同步方案,那么我们将在同步本身上损失更多时间。 这就是我们开始使用自旋锁的原因。 然而,这在实时系统中是非常糟糕的形式,并且RedHat严格不建议这样做,因此我们应用自旋锁100毫秒,然后切换到信号量模式以消除死锁的可能性。

结果,我们实现了每秒约 8 万笔交易的性能。 两个月后 文章 关于 LMAX Disruptor,我们看到了具有相同功能的电路的描述。

莫斯科交易所交易和清算系统架构的演变。 第2部分

现在,一个阶段可能有多个执行线程。 所有交易均按照收到的顺序一一处理。 结果,峰值性能从每秒 18 笔交易增加到 50 笔交易。

交易所风险管理系统

完美无极限,很快我们再次开始现代化:在ASTS+的框架内,我们开始将风险管理和结算运营系统转移到自治组件中。 我们开发了灵活的现代架构和新的分层风险模型,并尝试尽可能使用该类 fixed_point 而不是 double.

但一个问题立刻就出现了:如何将运行多年的业务逻辑全部同步并转移到新系统上? 结果,新系统原型的第一个版本不得不被放弃。 目前正在生产中的第二个版本基于相同的代码,适用于交易和风险部分。 在开发过程中,最难做的事情就是两个版本之间的 git merge。 我们的同事 Evgeniy Mazurenok 每周都会进行这项手术,每次他都会咒骂很长时间。

当选择一个新系统时,我们立即要解决交互的问题。 选择数据总线时,需要确保稳定的抖动和最小的延迟。 InfiniBand RDMA 网络最适合这种情况:平均处理时间比 4 G 以太网少 10 倍。 但真正让我们着迷的是百分位数的差异 - 99 和 99,9。

当然,InfiniBand 也面临着挑战。 首先,不同的 API - ibverbs 而不是套接字。 其次,几乎没有广泛可用的开源消息传递解决方案。 我们尝试制作自己的原型,但结果证明非常困难,因此我们选择了商业解决方案 - Confinity Low Latency Messaging(以前称为 IBM MQ LLM)。

那么合理划分风险体系的任务就出现了。 如果您只是删除风险引擎并且不创建中间节点,那么来自两个来源的交易可以混合。

莫斯科交易所交易和清算系统架构的演变。 第2部分

所谓的超低延迟解决方案具有重新排序模式:来自两个来源的交易可以在收到时按所需的顺序排列;这是通过使用单独的通道来交换有关订单的信息来实现的。 但我们还没有使用这种模式:它使整个过程变得复杂,并且在许多解决方案中根本不支持它。 此外,每个交易都必须分配相应的时间戳,在我们的方案中,这种机制很难正确实现。 因此,我们使用了带有消息代理的经典方案,即带有在风险引擎之间分发消息的调度程序。

第二个问题与客户端访问有关:如果有多个风险网关,客户端需要连接到每个风险网关,这将需要对客户端层进行更改。 我们希望在现阶段摆脱这种情况,因此当前的风险网关设计会处理整个数据流。 这极大地限制了最大吞吐量,但极大地简化了系统集成。

复制

我们的系统不应该出现单点故障,即所有组件都必须是重复的,包括消息代理。 我们使用CLLM系统解决了这个问题:它包含一个RCMS集群,其中两个调度程序可以主从模式工作,当一个调度程序出现故障时,系统自动切换到另一个调度程序。

使用备份数据中心

InfiniBand 针对本地网络的运行进行了优化,即用于连接机架安装设备,并且 InfiniBand 网络不能铺设在两个地理上分散的数据中心之间。 因此,我们实现了一个桥接器/调度器,它通过常规以太网连接到消息存储,并将所有事务中继到第二个 IB 网络。 当我们需要从数据中心迁移时,我们可以选择现在使用哪个数据中心。

结果

上述所有工作都不是一次性完成的;开发新架构需要多次迭代。 我们在一个月内创建了原型,但花了两年多的时间才使其进入工作状态。 我们试图在增加事务处理时间和提高系统可靠性之间实现最佳折衷。

由于系统进行了大量更新,我们从两个独立来源实施了数据恢复。 如果消息存储由于某种原因无法正常运行,您可以从第二个来源(风险引擎)获取事务日志。 整个系统都遵循这一原则。

除此之外,我们能够保留客户端 API,这样经纪人或其他任何人都不需要为新架构进行大量返工。 我们必须改变一些界面,但不需要对运营模型进行重大改变。

我们将平台的当前版本称为 Rebus - 作为架构中两个最引人注目的创新的缩写,Risk Engine 和 BUS。

莫斯科交易所交易和清算系统架构的演变。 第2部分

最初我们只想分配清算部分,但结果是一个庞大的分布式系统。 客户现在可以与贸易网关、清算网关或两者进行交互。

我们最终取得的成果:

莫斯科交易所交易和清算系统架构的演变。 第2部分

降低了延迟水平。 在交易量较小的情况下,系统的工作方式与之前的版本相同,但同时可以承受更高的负载。

峰值性能从每秒 50 万笔交易增加到 180 万笔交易。 进一步的增长受到唯一的订单匹配流的阻碍。

有两种进一步改进的方法:并行化匹配和改变它与 Gateway 的工作方式。 现在所有网关都根据复制方案运行,在这种负载下,复制方案将停止正常运行。

最后,我可以给那些正在敲定企业系统的人一些建议:

  • 时刻做好最坏情况的准备。 问题总是在不经意间出现。
  • 快速改造架构通常是不可能的。 特别是当您需要在多个指标上实现最大可靠性时。 节点越多,需要支持的资源就越多。
  • 所有定制和专有解决方案都需要额外的资源来进行研究、支持和维护。
  • 不要拖延解决系统可靠性和故障后恢复的问题;在初始设计阶段就将它们考虑在内。

来源: habr.com

添加评论