复制

概述

对于分布式存储的复制,业界普遍采用的是基于 Paxos、Raft、ZAB 等一致性协议,这些协议的过半确认会牺牲一定性能来严格保证数据的一致性。

LinDB 面临的场景是写入量巨大,对数据一致性要求不是很高,也允许短暂的数据不一致,但最终一致,因此 LinDB 采用写入 Leader 成功即写入成功的策略。

写入 Leader 成功即成功有如下好处:

  • 可以提高写入性能;
  • 集群可部署任意台机,而 Paxos、Raft、ZAB 等则要求至少 3 台;

写入 Leader 成功即成功存在如下问题:

  • 数据一致性的问题:一旦 Leader 挂了,选举出新的 Leader ,新 Leader 复制新的数据可能会覆盖老 Leader 已写入的数据;
  • 数据的丢失问题:一旦 Leader 挂了,而此时正好未复制给 Follower 的数据块也损坏了,则这部分数据会被丢失;

为了解决上述问题,尽量保证数据不丢失, LinDB 采用多通道复制方案,如下图:

replication

以数据分片 Shard 3 为例:

  • 当 Node 1 作为 Leader 时,启用 1-WAL 写入通道, Node 1 接收新的数据写入到 1-WAL 通道中,并把 1-wal 通道中的数据复制到 Node 2 和 Node 4 的 1-WAL 通道中;
  • 当 Node 1 挂了,选举出 Node 2 作为新的 Leader 时,启用 2-WAL 写入通道,Node 2 接收新的数据写入到 2-WAL 通道中,并把 2-WAL 通道中的数据复制到 Node 4 的 2-WAL 通道中;
  • 当 Node 1 启动后,Node 1 会把 1-WAL 通道中还未复制给 Node 2 和 Node 4 的数据继续复制到它们对应的 1-WAL 通道中。 Node 2 会把 2-WAL 通道中数据补到 Node 1 的 2-WAL 通道中;

即以 Leader 为起点开启写入通道,如 1-WAL 通道永远是 Node 1 向 Node 2 和 Node 4 复制通道,2-WAL 通道永远是 Node 2 向 Node 1 和 Node 4 复制通道,4-WAL 通道永远是 Node 4 向 Node 1 和 Node 2 复制通道,以此类推,

提示

Node 1 对应在集群中唯一的标识为 1,其他节点以此类推。

这样的复制方式解决了如下问题:

  • 避免了数据不一致的问题:每个通道的数据仅仅有一个节点负责数据的权威,所以不会有冲突问题;
  • 尽量避免数据丢失问题:只要老 Leader 中还未复制的数据没有丢,再次起来后还是会复制到其他副本中。如果老 Leader 中还未复制的数据丢失了则这部分数据会被丢失;

可以采用多通道复制的前置条件:

  • 允许数据乱序写入:数据写入了多通道后就失去了顺序性,LinDB 面临的时序数据场景并不需要保证事务所以是可以乱序写入的。

提示

由于时序数据一大特点是时间相关性,所以 LinDB 在存储数据的时候也根据这一特点按时间进行分片,每个时间分片为一个存储单元,因此实际的复制道通也跟存储单元一一对应。

本地复制

local replication

整个本地复制写入过程如下:

  • 系统会为每一个 Shard 复制通道启一个写协程,该协程负责这个通道的所有写操作,包括作为 Leader 或者 Follower 时都有该协程来完成写操作;
  • 首先把 namespace/metric name/tags/fields 对应的数据写入数据库的索引文件,并生成相应的 metric id/time series id/field id,主要完成 string->int 的转换;这样的好处是所有的数据存储都以数据类型来存储,以减少整体存储大小,因为对于每个数据点,namespace/metric name/tags/fields 这样元数据所占用,如 cpu{host=1.1.1.1} load=1 1514214168614, 其实转换成 id 之后,存储将由 cpu => 1(metric id), host=1.1.1.1 => 1(time series id), load => 1(field id),简化为 1 1 1514214168614=>1,具体的索引结构请查看索引
  • 如果写索引失败,认为本次写入失败,失败分为 2 种;
    1. 数据写入格式有问题,这类失败直接标示失败;
    2. 由于内部问题,这时写入失败需要重试;
  • 通过索引得到 ID 后,再将数据写入到存储单元中,类似 LSM 先写内存数据库,直接写内存以达到高吞吐量的要求,内存数据到达内存限制之后,会触发 Flush 操作,具体请查看存储格式内存数据库

对于写操作,需要注意如下几个复制 Index:

  • Consume(Replica) Index: 写协程已经处理到哪个位置,每写操作都会先验证该 Index 是否合法;
  • Ack Index: 该通道中哪些数据已经写入成功并持久化到存储中;

这里需要注意如下几点:

  1. 写协程会按顺序从 WAL 通道中消费写入的数据,因此 Replica Index 是一个有序的指针,因此比较容易与已经持久化的 Index 作校验,以验证数据是否已经被写入;
  2. 由 Flush 协程来同步 Ack Index 通知哪些数据已经写入成功;
  3. 由于所有的写入操作先写内存,再由内存中的数据持久化到相应文件中,在这个过程中如果系统 Crash,由于没有 Ack Index,即使内存中数据丢失,但每次写协程启动的时候都以 Ack Index 为当前 Replica Index 来消费 WAL 通道中的数据,所以做到了在 Crash 之后也可以恢复丢失的数据;

远程复制

以上图 Node 1 为 Leader,3 个副本来复制 1-WAL 举例来说明:

当前 Node 1 为该数据分片 Shard 的 Leader 接受 Broker 的写入,Node 2 和 Node 4 都是 Follower 接受 Node 1 的复制请求,此时 1-WAL 通道作为当前的数据写入通道。

remote replication

Index 基本概念说明:

  • 每个通道的 Append Index,表示该通道写入数据的位置;
  • 每个通道保留着每个 Follower Consumer(Replica) Index 和 Ack Index,分别表示每个 Follower 已消费的位置(已消费但是请求还在路上该位置还未确认消费成功)和消费已成功的位置;
  • 每个通道的 Tail Index:表示所有 Follower Ack Index 的最小值,该 Index 之前的 WAL 可以被删除了;

整个复制流程如下所示:

  • Leader 会为每个 Follower 的复制启动一个独立的协程进行处理,从 Leader 的 WAL 中获取该 Follower Consumer(Replica) Index 的数据发给 Follower ;
  • Follower 接收到数据后对比本地 WAL 的 Append Index 是否等于 Consumer(Replica) Index;
    • 如果等于则写入并将 Consumer Index 作为 Ack Index 返回给 Leader(正常情况下);
    • 如果不等于,则将自己本地 WAL 的 Append Index 返回给 Leader;
  • Leader 接收到响应之后,判断 Follower 返回的 Ack Index 是否等于 Consumer(Replica) Index;
    • 如果等于则更新该 Follower 的 Ack Index(正常情况下);
    • 如果不等于,则分如下几种情况:
      • 当 Follower Ack Index 小于 Leader WAL 的 Tail Index ,说明 Follower 本地的 Append Index 过小,Leader 已无该位置的数据了,所以需要 Follower 重置下自己的 Append Index 为 Leader 的 Tail Index ;
      • 当 Follower Ack Index 大于 Leader WAL 的 Append Index ,说明 Follower 本地的 Append Index 过大,Leader 已无该位置的数据了,需要将 Leader WAL 的 Append Index 提高到 Follower Ack Index ,同时将 Follower Consumer(Replica) Index 提高到 Follower Ack Index;
      • 当 Follower 在 Leader WAL 的 Tail Index 和 Append Index 之间,则需要将 Follower Consumer(Replica) Index 重置为 Follower Ack Index 即可;

Leader 与 Follower 初始化复制通道的过程类似 TCP 三次握手。

顺序性

多通道复制总体来说是不保证整体数据的顺序性,绝大多情况下只使用其中的某一个通道,需保证该通道的复制的顺序性,有了顺序性更容易知道哪些数据已经被复制了哪些数据还未被复制。

要想保证复制过程的顺序性,需要保证以下几个环节的顺序性:

  • Leader 复制请求的顺序性发送;
  • Follower 端对复制请求的处理是顺序性;
  • Leader 端对复制请求的响应需要保证是顺序性;

由于整个写入与复制过程都是基于 GRPC Stream 长连接加单协程处理机制,因此基本可以保证上述几个条件。

参考
  1. bigqueueopen in new window