主要要点
Binance 的 Ledger 存储账户余额和交易,同时还支持服务进行交易。
它为高吞吐量、全天候可用性和位级数据准确性创造了必要的条件。
Binance Ledger 在幕后发挥的作用使其成为 Binance 最重要的技术之一。在此了解它的工作原理以及它在全球最大的加密货币交易所运营中解决的问题。
有没有想过币安的运作原理是什么?币安需要每天处理庞大用户群的数百万笔交易,因此值得我们了解一下币安的运作原理。
币安的技术运作以账本为基础。账本存储账户余额和交易,同时支持服务进行交易。
币安对账本的要求很高
可以想象,为了满足大量用户的需求,对 Ledger 的要求很高。需要考虑三个主要点:
高吞吐量,在高峰时段能够处理大量 TPS(每秒交易数)。
全天候可用,无停机时间。
位级数据准确性,不会出现资金损失或交易错误。
让我们看一个账本上基本条目的示例。这是一笔常见的交易,其中账户 1 将 1 BTC 转移到账户 2。
交易前余额:
帐户ID | 资产 | 平衡 |
1 | 比特币 | 10 |
2 | 比特币 | 0.1 |
表格1
交易后余额:
帐户ID | 资产 | 平衡 |
1 | 比特币 | 9 |
2 | 比特币 | 1.1 |
表 2
在这个事务中,有两个命令:
账户 1 -1 BTC
账户 2 +1 BTC
当交易完成时,将存储两个余额日志以供审计和对账。
帐户ID | 资产 | 三角洲 | TX_ID | 时间 |
100001 | 比特币 | -1 | TX-001 | 2022-01-01 01:02:03 |
100002 | 比特币 | +1 | TX-001 | 2022-01-01 01:02:03 |
表3
标准行业解决方案
一个标准的行业 Ledger 解决方案是基于关系数据库的。回到前面的例子,交易的两个命令可以转换成两个 SQL 语句并在数据库事务中执行(表 4)。
开始交易; |
更新余额_1 设置余额 = 余额 - 1 其中 account_id=1 且 asset = ‘BTC’ 且 balance - 1 >= 0; 如果 0 行受到影响则回滚; |
更新 balance_2 设置 balance = balance + 1 其中 account_id=2 且 asset = ‘BTC’ 且 balance + 1 >= 0; 如果 0 行受到影响则回滚; |
犯罪; |
表 4
解决方案的优势
实现起来非常简单。
可以轻松应用常见的数据库调整技术(例如读/写分割和分片)来提高性能。
对于 DevOps 来说,从故障转移中恢复以及监控和维护商业数据库并不难。
该解决方案的缺点
当由于行锁而出现竞争条件时,TPS 将急剧下降。
很难水平扩展以提高性能。
热门账户问题
不幸的是,对于币安来说,上面演示的行业解决方案不能满足其高要求。当发生交易时,它必须持有所涉及的每一行的行锁。虽然有些账户需要处理的交易相对较少,但当然也有一些繁忙的账户有很多并发交易。在这种情况下,只有一个交易能够持有账户的行锁。
其他事务则只能等待锁被释放。我们称这种情况为热账户问题,内部测试表明,在这种情况下 TPS 至少会下降 10 倍。您可以在下面的表 5 中看到这个问题。
热门账户示例:
Tx 1(从账户 1 转移 1 BTC 到账户 2) | Tx 2(从账户 1 转移 2 BTC 到账户 3) |
开始交易; | |
开始交易; | |
更新余额设置余额=余额-1 其中 account_id=1 且 asset = ‘BTC’ 且 balance - 1 >= 0; (行锁定:account_id=1 和 asset = ‘BTC’) 如果 0 行受到影响则回滚; | 更新余额设置余额=余额-2 其中 account_id=1 且 asset = ‘BTC’ 且余额 - 2 >= 0; 如果 0 行受到影响则回滚; |
更新余额设置余额 = 余额 + 1 其中 account_id=2 且 asset = ‘BTC’ 且 balance + 1 >= 0; 如果 0 行受到影响则回滚; | 等待锁 |
犯罪; | 等待锁 |
获取锁,执行 | |
更新余额设置余额 = 余额 + 2 其中 account_id=3 且 asset = ‘BTC’ 且 balance + 1 >= 0; 如果 0 行受到影响则回滚; | |
犯罪; |
表 5
币安的账本解决方案
热点账号问题该如何解决?
解决我们这个问题的一个可能方案是创新性地将多线程模型转换为单线程模式。这样就避免了竞争条件问题,从而不会出现热账户问题。
新的线程模型
基于消息的通信
实现我们的新线程模型后,需要解决一个通信问题。状态机层是单线程的,而网络层是多线程的,那么我们如何在两者之间有效地通信呢?
下一步是 Disruptor [1]。它基于环形缓冲区设计,创建无锁、高性能队列。
高可用性
到目前为止,我们已经通过使用内存模型和 RocksDB [2] 本地存储实现了高性能。但是,又出现了一个新的挑战。现在我们需要考虑高数据可用性。
为了确保节点之间的数据一致性,我们使用 Raft 共识算法 [3]。这意味着数据备份的数量等于存在的非领导节点的数量。该算法还确保系统在至少一半节点健康的情况下仍能正常工作,以帮助提供高服务可用性。
Raft 域角色:
Leader。Leader 处理所有客户端请求,并将操作复制到所有跟随者。
追随者。追随者在所有操作中都跟随领导者。如果领导者发生故障,其中一位追随者将被选为新的领导者。
学习者。学习者是无投票权的追随者,他们将每个幂等/交易变更记录发送给其他服务。
Raft 域角色
CQRS(命令查询职责分离)
另一个我们要保证的关键标准是 Ledger 有更高的写入性能和更多样化的查询条件。为此,我们需要创建不同的域。raft 域基于 rocksdb+raft 提供更高效的写入,view 域监听 raft 域的消息并保存到关系型数据库中,以供外部查询。我们还可以在架构层面实现命令查询职责分离。
账本架构
总体架构
Raft 和 Ledger 之间的术语:
筏 | 总帐 |
复制状态机 | 账本节点 |
状态 | 平衡 |
命令 | 交易 |
表 6
查看域角色
Raft 账本中心
使用学习者生成的消息并将交易和余额数据存储在 MySQL 中以供查询。
请求处理
一笔交易请求会先经过网络层、账本层(请求处理器)、raft 层(raft log sync),然后再回到账本层(状态机)、网络层(响应处理器),最后将响应返回给客户端。
数据通过两层之间的队列传递。
网络层 – 反序列化rpc请求并放入请求队列。
账本层——从队列中获取请求并准备上下文。然后将请求元数据放入 Raft 队列中。
Raft 层 – 从 Raft 队列中获取请求元数据,并在所有追随者之间进行同步。然后将结果放入应用队列。
账本层 – 从应用队列中获取数据并更新状态机。然后将结果放入响应队列。
网络层 – 从响应队列中获取结果,并在返回给客户端之前构建和序列化响应数据。
请求处理
数据恢复
每个 Ledger 节点会根据一个时间段触发通用快照。此外,我们还实现了一致性快照。每个节点在相同的 raft 日志索引处触发,以确保每个节点触发快照时的状态机完全相同。然后快照将上传到 S3 以供 Checker 验证并作为冷备份。
当 Ledger 重启时,会读取本地快照,重建状态机,然后重放本地的 raft log,并从 leader 同步最新日志,直到追上最新索引,如果本地快照或者 raft log 不存在,则会从 leader 获取。
快照与恢复
容灾
为了提高可用性和容错能力,Ledger 节点部署在不同的可用区,只要超过一半的节点是健康的,数据就不会丢失,故障转移将在一秒内完成。
即使整个集群发生故障,这种概率非常低,我们仍然可以通过存储在 Amazon S3 中的一致性快照恢复集群,并通过下游系统检索最新丢失的数据。
容错
表现
下表显示了性能测试的硬件规格
成分 | 实例类型 | 网络带宽(Gbps) | EBS 带宽 (Gbps) | EBS 存储类型 |
领导者/追随者 | M6i.4xlarge 16c64克 | 最多 12.5 | 高达10 | 2T GP3 * 3 IOPS6000 625MB/s |
学习者 | M6i.4xlarge 16c64克 | 最多 12.5 | 高达10 | 2T GP3 * 3 IOPS6000 625MB/s |
长椅 | C5.4xlarge 16c32克 | 高达10 | 4.750 | 仅根卷 |
内部测试证明,一个 4 节点集群(一个领导者,两个追随者和一个学习者)可以处理超过 10,000 TPS。根据设计,集群逐一处理所有交易。完全没有锁定和竞争条件。因此在热账户场景中,TPS 与正常场景一样高。
热门账户TPS
下图展示了每个事务的延迟,大部分事务可以在 10ms 内完成,较慢的事务可以在 25ms 内完成。
延迟毫秒
使用币安账本支持我们的服务
正如您所见,传统行业对热账户问题的回答并不能满足币安及其客户的需求。通过使用专为币安基础设施设计的方法,我们最终获得了最顺畅的交易和产品体验之一。我们很高兴现在与您分享我们的经验,并希望您能更好地理解如何让币安这样的服务发挥作用。
阅读以下文章以获取有关我们的技术基础设施的更多信息:
(币安博客)使用 MLOps 构建实时端到端机器学习管道
(币安博客)会见首席技术官:Rohit 回顾加密货币、区块链、Web3 以及他在币安的第一个月
参考
[1] LMAX 破坏者
[2] RocksDB
[3] Raft 共识算法
