跳到主要内容

元数据设计文档

本文档介绍 Apache Doris FE 元数据的整体架构、存储结构、数据流,以及实现细节、启动流程和宕机恢复机制。

名词解释

术语说明
FEFrontend,即 Doris 的前端节点。主要负责接收和返回客户端请求、元数据以及集群管理、查询计划生成等工作。
BEBackend,即 Doris 的后端节点。主要负责数据存储与管理、查询计划执行等工作。
bdbjeOracle Berkeley DB Java Edition。在 Doris 中,使用 bdbje 完成元数据操作日志的持久化以及 FE 高可用等功能。

整体架构

Apache Doris 架构

如上图,Doris 的整体架构分为两层。多个 FE 组成第一层,提供 FE 的横向扩展和高可用;多个 BE 组成第二层,负责数据存储与管理。本文主要介绍 FE 这一层中元数据的设计与实现方式。

  1. FE 节点分为 follower 和 observer 两类。各 FE 之间通过 bdbje(BerkeleyDB Java Edition)进行 leader 选举、数据同步等工作。

  2. follower 节点通过选举产生一个 leader,负责元数据的写入操作。当 leader 节点宕机后,其他 follower 节点会重新选举出新的 leader,保证服务的高可用。

  3. observer 节点仅从 leader 同步元数据,不参与选举。可以横向扩展以提供元数据读服务的扩展性。

注:follower 和 observer 对应 bdbje 中的概念分别为 replica 和 observer。下文可能会同时使用两种名称。

元数据结构

Doris 的元数据是全内存的。每个 FE 内存中都维护一份完整的元数据镜像。在百度内部,一个包含 2500 张表、100 万个分片(300 万副本)的集群,元数据在内存中仅占用约 2 GB。(当然,查询所使用的中间对象、各种作业信息等内存开销,需要根据实际情况估算,但总体仍维持在较低的内存开销范围内。)

同时,元数据在内存中整体采用树状的层级结构存储,并通过添加辅助结构能够快速访问各层级的元数据信息。

下图是 Doris 元信息所存储的内容。

Doris 元数据内容

如上图,Doris 的元数据主要存储 4 类数据:

  1. 用户数据信息,包括数据库、表的 Schema、分片信息等。
  2. 各类作业信息,如导入作业、Clone 作业、SchemaChange 作业等。
  3. 用户及权限信息。
  4. 集群及节点信息。

数据流

Doris 元数据数据流

元数据的数据流具体过程如下:

  1. 只有 leader FE 可以对元数据进行写操作。写操作在修改 leader 的内存后,会序列化为一条 log,按照 key-value 的形式写入 bdbje。其中 key 为连续的整型,作为 log id;value 即为序列化后的操作日志。

  2. 日志写入 bdbje 后,bdbje 会根据策略(写多数 / 全写)将日志复制到其他 non-leader FE 节点。non-leader FE 节点通过对日志回放,修改自身的元数据内存镜像,完成与 leader 节点的元数据同步。

  3. 当 leader 节点的日志条数达到阈值(默认 10w 条)并且满足 checkpoint 线程执行周期(默认 60 秒)时,checkpoint 会读取已有的 image 文件及其之后的日志,在内存中重新回放出一份新的元数据镜像副本,然后将该副本写入磁盘,形成一个新的 image。之所以重新生成一份镜像副本而不是直接将已有镜像写成 image,主要是因为写 image 时加读锁会阻塞写操作;因此每次 checkpoint 会占用双倍内存空间。

  4. image 文件生成后,leader 节点会通知其他 non-leader 节点新的 image 已生成。non-leader 节点主动通过 HTTP 拉取最新的 image 文件,替换本地的旧文件。

  5. bdbje 中的日志,在 image 完成后会定期删除旧的日志。

实现细节

元数据目录

  1. 元数据目录通过 FE 的配置项 meta_dir 指定。
  2. bdb/ 目录下为 bdbje 的数据存放目录。
  3. image/ 目录下为 image 文件的存放目录。

image/ 目录下的关键文件如下:

文件说明
image.[logid]最新的 image 文件,后缀 logid 表明该 image 所包含的最后一条日志的 id。
image.ckpt正在写入的 image 文件。写入成功后会重命名为 image.[logid],并替换掉旧的 image 文件。
VERSION记录 cluster_id,唯一标识一个 Doris 集群。cluster_id 是 leader 第一次启动时随机生成的 32 位整型,也可以通过 FE 配置项 cluster_id 指定。
ROLE记录 FE 自身的角色,只有 FOLLOWEROBSERVER 两种。FOLLOWER 表示该 FE 是一个可选举的节点(注意:即使是 leader 节点,其角色也为 FOLLOWER)。

启动流程

  1. FE 第一次启动,如果启动脚本不加任何参数,则会尝试以 leader 的身份启动。FE 启动日志中最终会看到 transfer from UNKNOWN to MASTER

  2. FE 第一次启动,如果启动脚本中指定了 --helper 参数,并且指向了正确的 leader FE 节点,那么该 FE 首先会通过 HTTP 向 leader 节点询问自身的角色(即 ROLE)和 cluster_id,然后拉取最新的 image 文件。读取 image 文件、生成元数据镜像后,启动 bdbje 进行日志同步。同步完成后开始回放 bdbje 中 image 文件之后的日志,完成最终的元数据镜像生成。

    注 1:使用 --helper 参数启动时,需要首先通过 mysql 命令在 leader 上添加该 FE,否则启动时会报错。

    注 2:--helper 可以指向任何一个 follower 节点,即使它不是 leader。

    注 3:bdbje 在同步日志过程中,FE 日志会显示 xxx detached,此时正在进行日志拉取,属于正常现象。

  3. FE 非第一次启动,如果启动脚本不加任何参数,则会根据本地存储的 ROLE 信息确定自己的身份;同时根据本地 bdbje 中存储的集群信息获取 leader 的信息,然后读取本地的 image 文件以及 bdbje 中的日志,完成元数据镜像生成。(如果本地 ROLE 中记录的角色和 bdbje 中记录的不一致,则会报错。)

  4. FE 非第一次启动,且启动脚本中指定了 --helper 参数,则和第一次启动的流程一样,也会先去询问 leader 角色;但会和自身存储的 ROLE 进行比较,如果不一致则会报错。

元数据读写与同步

  1. 用户可以使用 mysql 连接任意一个 FE 节点进行元数据的读写访问。如果连接的是 non-leader 节点,则该节点会将写操作转发给 leader 节点。leader 写成功后,会返回当前最新的 log id。之后,non-leader 节点会等待自身回放的 log id 大于回传的 log id 后,才将命令成功的消息返回给客户端。该方式保证了任意 FE 节点的 Read-Your-Write 语义。

    注:一些非写操作也会转发给 leader 执行,例如 SHOW LOAD。因为这些命令通常需要读取一些作业的中间状态,而这些中间状态是不写 bdbje 的,因此 non-leader 节点的内存中并不存在这些中间状态。(FE 之间的元数据同步完全依赖 bdbje 的日志回放,如果一个元数据修改操作不写 bdbje 日志,则在其他 non-leader 节点中无法看到该操作修改后的结果。)

  2. leader 节点会启动一个 TimePrinter 线程,该线程会定期向 bdbje 中写入一个当前时间的 key-value 条目。其余 non-leader 节点通过回放这条日志,读取日志中记录的时间并与本地时间比较;如果本地时间的落后大于指定的阈值(配置项:meta_delay_toleration_second,写入间隔为该配置项的一半),则该节点会处于不可读的状态。此机制解决了 non-leader 节点在长时间和 leader 失联后,仍然提供过期元数据服务的问题。

  3. 各个 FE 的元数据只保证最终一致性。正常情况下,不一致的窗口期仅为毫秒级。我们保证同一 session 中元数据访问的单调一致性;但是如果同一 client 连接不同 FE,则可能出现元数据回退的现象。(对于批量更新系统,该问题影响很小。)

宕机恢复

场景行为
leader 节点宕机其余 follower 会立即选举出一个新的 leader 节点提供服务。
多数 follower 节点宕机元数据不可写入;若此时发生写请求,目前的处理流程是 FE 进程直接退出。后续会优化为在不可写状态下依然提供读服务。
observer 节点宕机不会影响任何其他节点的状态,也不会影响其他节点上元数据的读写。