证明交易的基于云的测序流消息传递架构

2021-06-17 23:27:22

这是一个技术深度潜在的帖子,描述了为云中的校正算法交易平台提供幂的自定义消息传递中间件。对于我们系统的高级概述,请参阅此帖子。

如概述帖子中所述,我们的系统使用“测序流”架构。

以下是对此工作方式的快速描述:系统中的每个输入都被称为定序器称为定序器的中央组件,分配全局唯一的单调序列号和时间戳。该测序的事件流被传播给系统中的所有节点/应用程序,该节点/应用程序仅在这些序列的输入上运行,而不是在尚未排序的任何其他外部输入上运行。在其他应用程序或外部世界可以消耗之前,还必须首先对来自应用程序的任何输出进行排序。由于分布式系统中的所有节点具有完全相同的事件序列,因此它们相对简单地向每个事件到达相同的逻辑状态,而不会产生与节点间通信相关的任何开销或问题。

我们随便提到“传播到所有节点”,好像这是一个容易的壮举。这实际上是一个巨大的挑战,有时称为“扇出问题” - 挑战是,序列人正在执行排序的关键任务,也不能通过了解所有客户的任务并主动传输排序消息的任务负担每个客户都。

通常,使用UDP组播解决了此问题,其中生产者生成消息一次以及所有感兴趣的消费者都订阅了一个已知的多播组以消耗邮件。在此解决方案中,网络交换机对多个消费者进行数据的“扇出”,有时使用分层组合。这实际上很好,尽管没有陷阱并非没有陷阱,但鉴于UDP是不可靠的,并且很难进行良好的多播设置。我们不能在云中使用多播(否,虚假的多播覆盖无效),我们使用自定义消息层解决了这个问题,由Aeron启发。

我们没有直接使用Aeron,因为即使它呼叫自己是留言传输,它还配备了整个模块和服务的生态系统。我们不想采用所有不同的框架件,因为这会限制我们的建筑选择。此外,我们并不熟悉Aeron,并不知道所有的Gotchas。相反,我们决定建立自己的信息巴士,从最佳和最相关的陨石中获取灵感。

是的,是的,你是对的 - 尝试在这一天和年龄的年龄创建自己的消息中间件有点疯狂。我的意思是,谈谈重新发明轮子。但它确实有意义,当你想要做的时候不一定是一个具有更多功能的更好的轮子,而是一个只有非常具体的东西(并且快速且可靠地实现它们)的最小专业轮。我们不了解所有启用云的中间件,每秒具有数百万条消息的吞吐量,在一致的子毫秒延迟(除了它之外,我们还没有测试)。

让我们回忆起我们试图解决的问题:我们有一条消息,我们需要以最快的方式从生产者发送到一个或多个远程消费者。

我们不想为个人消费者承担任何东西:它可能正在与生产者同时处理数据,或者它可能会缓慢且几个小时后面。

我们不希望消费者错过在他们下降时产生的消息(可靠的交付)。

我们不希望消费者影响生产者 - 例如,如果我们有一个缓慢的消费者,我们不希望它以某种方式“推回”生产者。

我们不希望消费者的数量来影响生产者或其他消费者 - 例如,如果我们串联向大量消费者传输消息,那么随着消费者的数量上升,“最后”消费者会增加“最后”消费者的延迟而且整体吞吐量会减少。

此外,在我们希望支持具有不同性能特征的消费者的同时,我们希望为常见案例提供优化的快速路径:能够跟上的最新消费者,并且正在实时处理数据因为它可以收到它。

我们提出的解决方案是创建一个磁盘备份队列,它使用固定到专用CPU内核的线程在服务器之间复制。请继续阅读,看看如何满足以上所有需求。

复制商店是我们用于促进制作人和一个或许多消费者之间的沟通的一般构造。 Producer App将其消息流写入存储,该商店被实现为内存映射文件。使用内存映射的文件允许存储文件的最近内容保留在机器的虚拟内存中。复制服务器的一个或多个线程在同一主机上运行,​​映射了相同的存储文件。每个消费者服务器有一个Replication Server线程,如果商店文件的最后偏移(长度)已更改,则在专用核心上繁忙 - 旋转紧密循环。检测到更改后,将立即读取新记录并通过TCP传输到复制客户端,该复制客户端将它们写入远程服务器上的内存映射文件。然后,消费者应用程序读取商店的这种复制副本,再次可能在专用核心紧密旋转,但这不是强制性的,取决于每个消费者应用程序的个人需求。

这种设计的最终结果是,如果消费者与生产者具有相当合理的电流,则复制过程完全在内存中发生,直接从生产者的内存到消费者内存,没有涉及磁盘访问。一旦生产者写入记录,生产者存储文件的大小/偏移量在内存中递增,新的偏移和记录可作为微秒或两个中的Replication Server线程的原子更新。记录可以通过TCP传输到〜50μs(内核/网络延迟)中的消费者服务器,然后在几个微秒内到消费者应用程序。 (在某些时候,操作系统会将商店文件的映射部分刷新到磁盘,这可能导致延迟,但有些方法可以调整操作系统的时间和方式。

这一非常简单的解决方案的美丽是生产者从缓慢或远方消费者彻底解耦。即使消费者应用程序在一天中间开始,Replication Server也能够通过从第一条消息读取商店文件(这将涉及磁盘访问,这没问题,因为性能不是主要考虑这个用例)。另一个相关效果是,个人消费者是独立的 - 一个消费者的相对速度不会影响另一个消费者的相对速度。此外,消费者没有以串行方式发送消息,因此没有“第一”或“最后”消费者在延迟方面具有优缺点或不利地位。

我们将承认,这些设计昂贵,因为每个消费者服务器都需要生产的服务器上的专用核心(请参阅下面标题为“硬件效率注释”的部分)。如果我们需要这缩小到更多的消费者服务器,而不是我们有核心,我们将使用分层分发模式。现在,我们还没有必要这样做。

系统使用多个复制存储器组成。拼图的最后一块是iodaemon,因为名称暗示,负责在服务器上执行I / O.这个想法是,如果在服务器上运行多个应用程序,则可以通过从semencer - iodaemon作为复制客户端来通过单个传输获取它们的顺序流。 iodaemon还负责将应用程序输出消息(AKA“未激活的”消息)传输到序列仪以进行排序,以通过UDP单播进行排序。

我们不会进入TCP与UDP的任何深度技术细节,但它是一个值得触摸的主题。你什么时候选择一个,另一个?

TCP是一种面向连接的协议,主要的好处是,通过TCP连接发送的所有数据包都将可靠地从发件人到接收器的方式,并按正确的顺序传递。此外,TCP包含拥塞控制算法,如果使用者无法跟上发送的数据,则返回发件人。所有这些功能都可以有用,但它们确实使协议在处理方面有点沉重。此外,您必须处理建立和维护连接,并从破损的连接中恢复。

如果您有一个情况,您对UDP的有损性质没有太烦扰,或者您可以从丢弃的数据包中恢复轻松的方法,您可以决定放弃这些功能,有利于使用更轻的UDP协议。像TCP一样的UDP坐在IP之上,但只有“删除”数据包(数据报),希望路由器将知道将它们引导到接收器的位置。没有连接建立,无法保证数据包将到达目的地。在实践中,它可以合理地运作,数据丢失很小。使用UDP为接收器使用UDP的另一个不同的益处是OS可以多路复用来自多个发送器的流,而不是必须手动组合超过多个TCP连接的数据(其中需要处理公平和饥饿问题)。

在我们的情况下,我们使用UDP单播将来自iodaemon的消息传输到定序器,我们实际上关注消息丢失。这不会让我们烦恼太多,因为我们有一种相对简单的方法来检测数据包滴。向定序器发送未驱换消息的iodaemon也在读取顺序流。序列流最终应包含iodaemon发送的所有这些消息。如果在预定义的时间(例如500ms)内未在排序的流上看到一组给定消息,则iodaemon可以假设邮件丢失,并且可以重新发送这些消息。如果消息实际上丢失,重新发送可能会成功,事情恢复正常;如果消息未丢失但只需延迟,则重新发送将导致序列器读取重复的消息,这智能足以丢弃重复项。最终结果是我们为有损失的UDP传输添加了可靠的交付。

为了传播测序的流,我们使用TCP,因为没有自然的方法来恢复序列仪中的错过的UDP报文。因此,如上所述,消费者通过复制商店掌握了所有这些情况的不同性能特征和TCP扇出。

使用顺序流架构创建交易系统的一种方法是真正将系统上的所有输入序列序列单个顺序流。然而,出于各种原因,特别是当不同类型的输入的比率非常大时,这可能会难以置信。例如,我们可能在系统中有1千个相关的消息,但是10亿市场数据事件。虽然将它们一起排序到单个测序的流和能力方面是完全合理的,但是系统将能够处理它们,只有icky才能咀嚼十亿+消息以便处理1000订单相关的消息。

为了简化此用法,我们将市场数据序列到从邮件的其余部分单独的测序流。这有助于我们的订单管理系统(OMS)显着,因为它不需要处理市场数据,并且能够使用更轻微的流。但是,这为我们的ILGO引擎创造了一个问题,这需要订单消息和市场数据。因此,我们实际上从两个组件流创建了一个复合流,并将其馈送到ILGO引擎中。将该复合流记录到磁盘,如果需要,也可用于将其流式传输到远程服务器。结果图像看起来如下。

关于这些流消息的内容的单词。从我们的先前经验中,我们知道我们希望为我们的内部通信使用二进制编码,优选地,一个具有固定位置的字段的二进制编码(用于便于随机访问)。在查看几个选项后,我们降落在简单的二进制编码(SBE)上,实际上是一个修复标准。

SBE快速燃烧,支持直接现场访问,支持版本控制,并提供可以生成Java Encoder /解码器类的消息处理器。我们并没有真正烘焙,并且鉴于SBE是一个修复标准,我们有点朝着它绘制。使用它一段时间后,我会说性能是恒星,但我被特征集所做的,特别是支持不断发展的架构。我们已经扩展了SBE来做我们需要做的事情,但如果我不得不这样做,我会看看Cap'n Proto或FlinBuffers,我们将来可能会这样做。

你们中的一些人“正常”工程师可能会在所有这些地方使用专用核心困惑。首先,我们从未说过这是在硬件使用方面有效的!其次,专用核心上的固定线程是一种常见的,通常是实现低延迟的唯一方式。

这些线程在紧密的循环中运行,即使没有任何可能的流程,也不会屈服于OS,这在正常的软件架构中是相当不礼貌,但在低延迟的世界中完全被接受!操作系统有围绕此方法 - 例如,如果线程在一段时间内每次发出系统调用(例如,从网络接收消息),则内核将掌握在这些呼叫期间拦截线程必要的。要遍及这个,低延迟开发人员将使用内核旁路完全从这些过程中剪掉内核(例如,DPDK,VMA,OpenOnload)。兔子洞与FPGA,实时内核和其他这样的腹部更深。

我们意识到本文中描述的设计不是实现这些结果的唯一方法,实际上我们不确定是否有其他人这么做。我们简单地记录了我们在研究后降落的内容,这仍然是一项过程。

作为结束思想,我会在这里重复我在此时在这里所说的 - 如果你认为这很酷或者你可以为这项工作做出贡献,请致电@ProokTrading.com。如果你是技术学家,你擅长你所做的事情,并希望帮助建立一个现代平台并产生影响,有可能为您的证明作用。为了向我们的员工展示我们关心和我们欣赏,我们使他们成为真正的伙伴,拥有英俊的股票赠款,可能比你在职业生涯中看到的任何东西都大。

如果您有疑问,请访问我的推特:@preaksanghvi,或在[email protected]上联系我们。