头图

导读:本文提出了一种将事务日志和 Raft 日志融合在一起的机制,从而实现了分布式事务和数据一致性的场景。

01 背景介绍

分布式系统是伴随着互联网的高速发展而出现的。其出现为了应对单机系统无法解决的高并发、高可用性、容错性等问题。分布式系统将传统的系统扩容模式,从 scale up 转变为了 scale out。为了实现这一目标并兼顾性价比,分布式系统采用了廉价硬件系统,在软件层面实现了多数据副本(一般至少 3 个副本以防止硬件损坏)、自动容错、自动调度等能力。 在这其中,一个最关键的问题就是如何保证分布式系统中多个数据副本之间的一致性

而事务则是数据库领域的一个关键问题,它解决的是多个数据库操作之间应该如何并发执行的问题。往往是功能、性能、业务需求等多个因素的权衡与取舍,其复杂度并不逊色于分布式副本一致性的问题。特别是当分布式系统遇上了事务,这就演变成了一个分布式事务,其复杂度呈指数级上升,这也是很多分布式系统会放弃事务能力的原因。

过去二十年,工业界和学术界对这两个问题进行了大量研究,但具体到应用实践上,仍然有大量的细节需要处理。ArcGraph 作为底层基础设施,如果不提供这两项能力,则会给上层业务系统带来极大的复杂度,所以我们没有其他选择。在 ArcGraph 中我们结合 Raft 协议与数据库事务,最终实现了 ArcGraph 完整的分布式事务能力与数据强一致性,可以极大降低上层业务的复杂性

02 ArcGraph 的一致性策略选择

常见的分布式一致性策略

首先来看一下分布式系统中副本一致性的两大理论基础,即 CAP 理论和 BASE 理论:

  • CAP 理论是指在一个分布系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者不可兼得,最多只能同时满足其中的 2 个。一致性指的是数据在多个副本之间能够保持一致的特性。可用性指的是系统提供的服务一直处于可用状态,每次请求都能获得正确的响应。分区容错性是指分布式系统在遇到任何网络分区故障时,仍然能够对外提供满足一致性和可用性的服务。

  • BASE 理论是基本可用(Basically Available)、软状态(Soft-state)和最终一致性(Eventually Consistent)的缩写, 它是对 CAP 中一致性 C 和可用性 A 权衡的结果,源于对大规模互联网系统分布式实践的总结,大大降低了对系统的要求。其核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使用系统达到最终一致性。

从上述两个理论我们可以看出,一致性问题可以划分为强一致性和弱一致性。常见的强一致性实现包括主从同步复制、多数派协议(例如 Paxos 和 Raft 等协议),一般在数据库中比较常见。弱一致性则包括最终一致性、消息补偿和 Gossip 去中心化协议等,一般在业务系统中比较常见。

ArcGraph 的一致性策略

从之前的论述,很容易得出 ArcGraph 的选择:

  • 作为图数据库的 ArcGraph,需要提供数据的强一致性访问。
  • 作为分布式系统与数据库系统的结合,需要提供完整的分布式事务能力。

鉴于此结论,我们提出了一种将事务日志和 Raft 日志融合在一起的策略,从而实现了分布式事务和数据一致性的场景。Raft 协议是目前广泛使用的一种分布式一致性协议,主要内容包括领导者(Leader)选举、日志复制、安全性和日志压缩等,其核心是通过日志同步的方式来保证各个数据副本之间的一致。而数据库事务的四个特性,即原子性、持久性、隔离性和一致性,通常也是通过日志(比如 Redo Log 与 Undo Log)的方式来实现。虽然 Raft 日志与数据库事务日志是两个不同的概念,但我们可以将它们结合在一起,以同时实现分布式事务与数据的一致性。本文着重讨论 Raft 协议与事务的结合,而不会深入到 Raft 协议与事务本身的细节。

Raft 日志的核心概念

在 Raft 中,一般每个日志包含一条命令、任期信息和日志索引。命令是具体要执行的指令,在分布事务中通常包括事务的开启、数据的插入、更新、删除以及事务的提交或回滚操作。任期编号记录了当前 Leader 的任期编号,用来判断日志是否能被 Follower 接收。索引值则是一个连续并且单调递增的整数,用来记录日志在所有日志序列中的位置。

一般说来,一条 Raft 日志从 Leader 复制到 Follower 并被 Apply 后,该日志就可以被清理掉了,在 Raft 中该行为被称作 Compact。但是在遇到事务日志时,因为还牵涉到事务的提交与回滚,所以在发现存在未提交的事务时,这些日志是不允许被 Compact 的,只有那些真正被提交或者回滚的日志才允许被清理掉。将事务日志和 Raft 日志融合到一起,需要解决下面几个问题:

  • 事务提交与回滚。
  • 数据写入(Checkpoint)。
  • 数据恢复(Recovery)。

接下来我们会详细阐述上述问题在 ArcGraph 中的解决方案。

03 ArcGraph 的解决方案

整体架构

ArcGraph 是 ArcNeural 多模态智能引擎中的图引擎,是 Fabarta 自主设计和研发的一款分布式、云原生的高性能图 HTAP 数据库,是一款同时支持图查询和图分析的存查分析一体化的融合性图数据库引擎。 上图是 ArcGraph 的整体架构,核心模块包括了查询层、缓存层、事务管理层、存储层、Meta 服务和 Partition Raft 层。本文主要涉及到的是事务管理层和 Partition Raft 层。事务管理层主要用于控制事务的提交与回滚,以及内存中数据的变更与回退,同时将事务日志发送给 Partition Raft 中的 Log Buffer。Partition Raft 按分区来完成一个分区内的 Raft 一致性逻辑,通过 Checkpoint 和 Recovery 将日志同步到底层 存储层来实现数据变更并保证数据一致性。

整体流程

Raft 主要包括三个大步骤:

  • 选主。
  • 日志复制。
  • 日志压缩。

选主是前提,日志复制是从 Leader 将日志复制到 Follower 并完成日志的 Commit,最后需要对已经提交的日志进行压缩和快照生成。将事务跟 Raft 一致性融合到一起,需要解决的最大难题是日志复制到 Follower 后,并不能立即执行业务数据的变更,而且是需要延迟判断事务的类型(Commit 还是 Rollback)来决定最后的数据是生效还是回滚到之前状态,相应在日志压缩时也不能直接把日志删除,只有那些已经被提交的事务的日志才能被删除。接下来我们再来详细看下每个步骤的具体逻辑。

选举过程

  • 节点全部重启时或者 Leader 宕机时,需要先选出新的 Leader,然后进行数据加载:

    • 首先获取每个分区对应的机器列表。
    • 投票,通过比较候选人的 lastLogIndex 和 lastLogTerm 与当前节点的日志,检测候选人的日志至少比当前节点的日志新时才投票,确保新选举出来的 Leader 不会丢失已经提交的日志。

数据恢复(Recovery)

Recovery 是在机器宕机后,Raft 如何将数据恢复到宕机前的状态,主要包括下面几个过程:

  • 从 Meta 中获取到所有 Raft Group(Partition)列表,并行初始化 Raft Group,选出 Leader。
  • Raft Leader 按分区从底层存储加载截止到 checkpoint lsn 的数据。
  • 分析 min Txn start lsn->checkpoint lsn ->last commit lsn 这一区间的日志。
  • 待恢复完成后,重新上报 commit txn。
  • 对于那些未发现提交或者回滚日志的操作日志,在这一阶段被认为是脏日志直接清理了。

日志复制

ArcGraph 会走标准的 Raft 日志复制流程。同时提供了 Performane 和 Security 模式,Performane 模式会在 append log 后就直接返回,适合在性能优先的场景下使用。而 Security 模式则会在 leader commit 后才返回给客户端,安全性更强。

Leader 首先将日志加入到自己的日志列表并记录日志的序号(lsn)和 term,然后同时将日志发送给 Follower,在收到 Follower 的回应后,根据事务指定来执行 Commit 还是 Rollback,并将 Active txn start lsn 从日志列表中删除。

Follower 同样需要执行上述流程。为了实现图之间日志的隔离,每个的图的分区会持有各自的目录句柄,类似 /log/\<graph\_id>/\<partition\_id>/,日志也采用比较标准的 KV 形式进行存储在 sled 中,具体格式见下图:

这里的事务类型包括,事务开启、事务提交、事务回滚、插入点、更新点、删除点、插入边、更新边和删除边共 9 种类型。

除此之后,sled 会保存 raft 相关的辅助信息,如当前 term 编号、hard 和 conf state、最后达成一致的日志编号,以及跟事务日志相关的最后一次 checkpoint 对应的日志编号。

数据写入(Checkpoint)

ArcGraph 借鉴了 MySQL 的 Redo Log 机制,并结合 Checkpoint 技术实现了事务日志到存储的持久化。ArcGraph Checkpoint 流程会遍历每个 Log Entry,以下图中的 T1、T2 和 T3 事务为例,在 Start Txn 时,事务会被暂存到一个 Map 中,事务 Rollback 时(T2),则会将该事务从 Map 中移除,事务 Commit 时(T1),则将暂存在 Map 中的日志解析并写入存储层然后将其从 Map 中移除。待 Log Entry 遍历完成后,Map 中剩余的即为活跃事务,对 Key 排序后得到最小的活跃事务号为 T3 Start Txn,在此之前的日志都可以被 Compact,这个操作会周期性进行,详细的处理逻辑见下图:

日志复制性能优化

上述的日志复制逻辑,需要保证事务日志与 Raft 日志一一对应,这是比较低效的。出于性能的考虑,ArcGraph 提出了 LogBuffer 和 Batch LogEntry 来提升整体吞吐效率,其核心思路是将事务操作日志进行缓存,事务提交或者缓存区满时再批量提交到 Raft Group 进行 Raft 日志的同步,具体设计见下图。

  • LogBuffer:用于缓存前端发起的事务日志,只有当事务真正提交时,才会把日志提交给 Raft Group 走日志复制的流程,如果事务回滚,可以直接删除 LogBuffer 中的日志。这里的一个问题是如果是大事务可能会有非常多的事务日志,LogBuffer 不能无限缓存前端的事务日志,因此我们给 LogBuffer 设置了容量限制,达到容量上限时日志也会刷到 Raft Group。
  • Batch LogEntry:LogBuffer 往 Raft Group 同步日志时,原本会被拆成一条条 Log Entry,但这会极大的影响 Checkpoint 的性能,因此 ArcGraph 直接把 LogBuffer 的日志列表序列化为一个 Batch Log Entry,在 Checkpoint 时再解析成真正的 KV 数据。如果每个 Batch Log Entry 只包含一个事务的提交,在 Checkpoint 后该日志可以被正常 Compact,但由于 LogBuffer 有容量上限, Batch Log Entry 可能只包含了一个事务的开始,所以在做 Compact 的时候,需要判断这条日志能否正常 Compact。每次 Checkpoint 完成后,我们会记录当前日志的"Compact Offset",用来表示该 Log Entry 之前的日志不需要再做解析以避免重复解析日志。当"Compact Offset"与 Batch Log Entry 的长度相同时,表明这整个日志可以被 Compact。

03 总结与展望

ArcGraph 通过创新性地将事务与一致性协议融合到了一起,利用 Raft 一致性协议和 Checkpoint 技术将事务日志同步到集群的多数节点,从而简化分布式图数据库的事务逻辑。同时,ArcGraph 引入了 LogBuffer 和 Batch LogEntry 机制,极大地提升了事务的吞吐率和写入性能。

在当前实现中,我们优先考虑了数据的强一致性和写入性能,Follower 并不参与读流量的分担。未来计划考虑引入 Follower Reader 机制,从而提升整个集群的吞吐能力并降低 Leader 的负载。此外还将考虑把 Raft 的 Learner 机制引入,基于 Raft 日志同步机制,将 Learner 节点上的数据组织形式转换成更贴近业务逻辑的数据结构,例如列存、JSON、向量化等,从而更高效地实现各类业务场景。

如对我们的产品或技术感兴趣,欢迎通过 business\@fabarta.com 与我们联系,了解更多我们的产品与解决方案信息。

本文作者

俞超

Fabarta 资深技术专家

具备丰富的系统架构和业务建模经验,先后负责过超大规模云原生基础系统、AI 模型全生命周期管理平台和第一代图数据库存储引擎,在分布式任务调度和分布式数据一致性领域具有非常丰富的经验。目前负责 Fabarta 新一代图数据库内核的设计和研发工作。


Fabarta
1 声望1 粉丝

Fabarta 致力于解决在大规模增长的多源异构数据环境下的图智能难题,帮助企业客户和业务合作伙伴更方便地在图智能分析平台上沉淀业务价值,整理和管理数据资产,帮助企业快速高效地构建图智能应用。