TigerBeetle:世界上最快的会计数据库


TigerBeetle 是一个财务会计数据库,专为关键任务安全性和性能而设计,为金融服务的未来提供动力。

希望让其他人能够轻松构建下一代金融服务和应用程序,而无需从头开始拼凑会计或分类帐记录系统。
TigerBeetle 采用最新的研究和技术来提供前所未有的安全性、耐用性和性能,同时将运营成本降低几个数量级并提供出色的开发人员体验。

安全
TigerBeetle 的设计遵循比 MySQL 等通用关系数据库或 Redis 等内存数据库更高的安全标准:

  • 严格的一致性、CRC 和碰撞安全还不够。
  • TigerBeetle 可处理和恢复潜伏扇区错误,检测并修复固件读/写错误扇区的磁盘损坏或错误 I/O,检测并修复数据篡改。
  • 利用哈希链加密校验和检测和修复数据篡改(在少数群集上,如同非拜占庭损坏)。
  • TigerBeetle在设计上使用直接 I/O来避免EIO fsync 错误后内核页面缓存中的缓存一致性错误。
  • TigerBeetle超过了单个磁盘和单个服务器硬件的 fsync 持久性,因为磁盘固件可能包含错误并且单个服务器系统发生故障。
  • TigerBeetle作为复制状态机和 TigerBeetle 服务器集群(称为副本)提供严格的可串行性(一致性的黄金标准),以实现最佳的高可用性和分布式容错。
  • TigerBeetle使用开创性的Viewstamped Replication和共识协议对一定数量的备份 TigerBeetle 服务器执行同步复制,以实现低延迟自动领导者选举,并消除与临时手动故障转移系统相关的脑裂风险。
  • TigerBeetle 具有“故障感知”能力,可以在全球共识协议的背景下从本地存储故障中恢复,比 ZooKeeper 和 LogCabin 等复制状态机提供更高的安全性。
  • TigerBeetle 不依赖于同步系统时钟,不使用领导者租约,并执行基于领导者的时间戳,以便您的应用程序只能处理与传输超时有关的安全相对时间量。
  • 为了确保领导者的时钟在“真实时间”的安全范围内,TigerBeetle 组合了集群中的所有时钟来创建一个容错时钟,我们称之为“集群时间”。

性能
TigerBeetle 提供比 MySQL 等通用关系数据库或 Redis 等内存数据库更高的性能:

  • TigerBeetle使用小型、简单的固定大小数据结构(账户和转账)和严格范围的域。
  • TigerBeetle执行数据库中的所有余额跟踪逻辑。“会计”业务逻辑内置于 TigerBeetle 中,以便您可以保持应用程序层简单且完全无状态。
  • TigerBeetle在设计上支持批处理。
  • 一切都是一批。TigerBeetle 能够通过消除小批量引起的排队延迟成本来分摊 I/O 成本,以实现更低的延迟,即使对于相当大的批量也是如此。
  • 如果你的系统没有负载,TigerBeetle还会优化小批量的延迟。从内核的 TCP 接收缓冲区复制后(TigerBeetle 不执行用户空间 TCP),TigerBeetle执行从网络协议到磁盘,然后到状态机并返回的零复制直接 I/O ,以减少内存压力和 L1-L3缓存污染。
  • TigerBeetle使用 io_uring 进行零系统调用网络和存储 I/O。对于几千次传输,系统调用在上下文切换方面的成本会迅速增加。
  • TigerBeetle通过使用固定大小的数据结构进行零反序列化,这些数据结构针对缓存行对齐进行了优化,以最大限度地减少 L1-L3 缓存未命中。
  • TigerBeetle利用 Heidi Howard 的灵活仲裁来减少同步复制到最多一个(或两个)远程副本(除了领导者之外)的成本,并在其余跟随者之间进行异步复制。这提高了写入可用性,而不会牺牲严格的可串行性或持久性。这还可以将服务器部署成本降低多达 20%,因为具有灵活仲裁的 4 节点集群现在可以f=2为复制仲裁提供与 5 节点集群相同的保证。
  • TigerBeetle绕过短暂的灰色故障延迟峰值。例如,如果由于磁盘缓慢故障而通常需要 4 毫秒的磁盘写入开始需要 4 秒,TigerBeetle 将使用集群冗余自动掩盖灰色故障,而用户不会看到任何 4 秒的延迟峰值。这是文献中一种相对较新的性能技术,称为“尾部容差”。

TigerBeetle 为您完成所有账本验证、余额跟踪、持久性和复制;您所要做的就是使用 TigerBeetle 客户端来:

  1. 向 TigerBeetle 发送一批准备(在单个网络请求中),然后
  2. 向 TigerBeetle 发送一批提交(在单个网络请求中)。

架构
理论上,TigerBeetle 是一个复制状态机,它采用初始启动状态(账户开户余额),并按确定性顺序应用一组输入事件(转账),在首先安全地复制这些输入事件后,到达最终状态(账户期末余额)。

在实践中,TigerBeetle基于LMAX架构 并做了一些改进。

我们采取相同的三个经典 LMAX 步骤:

  1. 将传入事件安全地记录到磁盘,并复制到备份节点,然后
  2. 将这些事件应用到内存状态,然后
  3. 向客户端确认

然后我们引入一些新内容:

  1. 完全删除本地日志步骤,并且
  2. 将其替换为并行复制到 3/5 分布式副本。

然后我们的架构就变成了三个简单的步骤:

  1. 将传入事件安全地复制到分布式副本的法定数量,然后
  2. 将这些事件应用到内存状态,然后
  3. 向客户端确认

这就是 TigerBeetle 如何消除领导者本地磁盘中的灰色故障,以及 TigerBeetle 如何消除复制节点的网络链路中的灰色故障。

与 LMAX 一样,TigerBeetle 使用每核线程设计来实现最佳性能,并通过严格的单线程来强制执行单写入器原则并避免多线程协调访问数据的成本。

数据结构
为了性能和简单性,所有数据结构都是固定大小的,并且有两种主要的数据结构:事件和状态。

1、事件Event活动
事件是不可变的数据结构,可以实例化或改变状态数据结构:

  • 事件不能改变,即使是其他事件也不能改变。
  • 事件无法导出,因此必须在执行之前记录下来。
  • 事件必须按确定的顺序一个接一个地执行,以确保可重玩性。
  • 事件可能取决于过去的事件(如果他们选择)。
  • 事件不能取决于未来的事件。
  • 事件可能取决于处于确切版本的状态(如果他们选择)。
  • 事件可能成功或失败,但事件的结果永远不会存储在事件中;它存储在由事件实例化或变异的状态中。
  • 事件只能有一个不可变的版本,可以通过事件的 id 直接引用。
  • 应保留事件以供审计之用。然而,一旦在状态快照中捕获事件的影响,事件可能会被排入单独的冷存储系统,以压缩日志并缩短启动时间。

create_transfer:创建帐户之间的转账(映射到“准备”)。我们按大小降序对字段进行分组,以避免 C 实现中不必要的结构填充。

 

        create_transfer {
                      id: 16 bytes (128-bit)
        debit_account_id: 16 bytes (128-bit)
       credit_account_id: 16 bytes (128-bit)
                  amount: 16 bytes (128-bit) [required, an unsigned integer in the unit of value of the debit and credit accounts, which must be the same for both accounts]
              pending_id: 16 bytes (128-bit) [optional, required to post or void an existing but pending transfer]
           user_data_128: 16 bytes (128-bit) [optional, e.g. opaque third-party identifier to link this transfer (many-to-one) to an external entity]
            user_data_64:  8 bytes ( 64-bit) [optional, e.g. opaque third-party identifier to link this transfer (many-to-one) to an external entity]
            user_data_32:  4 bytes ( 32-bit) [optional, e.g. opaque third-party identifier to link this transfer (many-to-one) to an external entity]
                 timeout:  4 bytes ( 32-bit) [optional, required only for a pending transfer, a quantity of time, i.e. an offset in seconds from timestamp]
                  ledger:  4 bytes ( 32-bit) [required, to enforce isolation by ensuring that all transfers are between accounts of the same ledger]
                    code:  2 bytes ( 16-bit) [required, an opaque chart of accounts code describing the reason for the transfer, e.g. deposit, settlement]
                   flags:  2 bytes ( 16-bit) [optional, to modify the usage of the reserved field and for future feature expansion]
               timestamp:  8 bytes ( 64-bit) [reserved, assigned by the leader before journalling]
} = 128 bytes (2 CPU cache lines)


create_account:创建账户。

  • 我们使用贷方和借方而不是 "credit 应付 "或 "应收debit",因为贷方余额的含义取决于账户是资产、负债还是权益、收入还是支出。
  • posted过账金额指通过转账过账的金额。
  • pending挂账金额指的是尚未通过两阶段转账挂账的账内金额,即转账仍处于挂账状态,转账超时尚未触发。换句话说,转账金额已保留在待转账账户余额中(以避免重复支出),但尚未记入已过账余额。如果转账最终失败,预留金额将回滚。默认情况下,转账会自动过账,但将金额保留为待处理,然后再过账有时会很方便,例如在转换信用卡付款时。
  • 账户的借方余额由 debits_posted 加上 debits_pending 得出,同样,账户的贷方余额也由 debits_posted 加上 debits_pending 得出。
  • 用借方余额减去贷方余额,即可得出账户的总余额。
  • 我们将分类账的两边(借方和贷方)分开,以避免处理带符号的数字,并保留更多有关账户性质的信息。例如,两个账户的余额可能都是 0,但其中一个账户的账面余额可能都是 1,000,000 单位,而另一个账户的账面余额可能都是 1 单位,两者的余额都是 0。
  • 账户一经创建,只能通过转账事件进行更改,以保持不可更改的书面记录,便于审计。

     

      create_account {
                      id: 16 bytes (128-bit)
          debits_pending: 16 bytes (128-bit)
           debits_posted: 16 bytes (128-bit)
         credits_pending: 16 bytes (128-bit)
          credits_posted: 16 bytes (128-bit)                      
           user_data_128: 16 bytes (128-bit) [optional, opaque third-party identifier to link this account (many-to-one) to an external entity]
            user_data_64:  8 bytes ( 64-bit) [optional, opaque third-party identifier to link this account (many-to-one) to an external entity]
            user_data_32:  4 bytes ( 32-bit) [optional, opaque third-party identifier to link this account (many-to-one) to an external entity]                      
                reserved:  4 bytes ( 32-bit) [reserved for future accounting policy primitives]
                  ledger:  4 bytes ( 32-bit) [required, to enforce isolation by ensuring that all transfers are between accounts of the same ledger]
                    code:  2 bytes ( 16-bit) [required, an opaque chart of accounts code describing the reason for the transfer, e.g. deposit, settlement]
                   flags:  2 bytes ( 16-bit) [optional, net balance limits: e.g. debits_must_not_exceed_credits or credits_must_not_exceed_debits]
               timestamp:  8 bytes ( 64-bit) [reserved]
} = 128 bytes (2 CPU cache lines)

2、状态
状态是捕捉事件结果的数据结构:

  • 状态总是可以通过重放所有事件得到。

TigerBeetle 恰好提供了一种状态数据结构:
  • Account账户:一个显示所有传输效果的账户。

为了简化、减少内存拷贝并尽可能重用事件数据结构的线格式,我们重用 create_account 事件数据结构来实例化相应的状态数据结构。


深入的讨论

  • - OLTP和OLAP工作负载
  • - 为什么SQL不是最好的数据格式语言?
  • - 当涉及到高规模事务处理时,传统OLGP数据库存在什么问题?
  • - 为什么内存会成为拥塞瓶颈?
  • - TigerBeetle的安全性和性能特点。
  • - LSM,它的挑战以及TigerBeetle如何确保可预测的性能。
  • - 128字节如何足以使用财务会计建模和表示任何类型的业务?