pegasus项目记录

Table的管理

在Pegasus里,table相当于一个namespace,不同的table下可以有相同的(HashKey, SortKey)序对。在使用table前,需要在向MetaServer先发起建表的申请。

MetaServer在建表的时候,首先对表名以及选项做一些合法性的检查。如果检查通过,会把表的元信息持久化存储到Zookeeper上。在持久化完成后,MetaServer会为表中的每个分片都创建一条记录,叫做PartitionConfiguration。该记录里最主要的内容就是当前分片的version以及分片的composition(即Primary和Secondary分别位于哪个ReplicaServer)。

在表创建好后,一个分片的composition初始化为空。MetaServer会为空分片分配Primary和Secondary。等一个分片有一主两备后,就可以对外提供读写服务了。假如一张表所有的分片都满足一主两备份,那么这张表就是可以正常工作的。

如果用户不再需要使用一张表,可以调用删除接口对Pegasus的表进行删除。删除的信息也是先做持久化,然后再异步的将删除信息通知到各个ReplicaServer上。等所有相关ReplicaServer都得知表已经删除后,该表就变得不可访问。注意,此时数据并未作物理删除。真正的物理删除,要在一定的时间周期后发生。在此期间,假如用户想撤回删除操作,也是可以调用相关接口将表召回。这个功能称为软删除。

ReplicaGroup的管理

ReplicaGroup的管理就是上文说的对PartitionConfiguration的管理。MetaServer会对空的分片分配Primary和Secondary。随着系统中ReplicaServer的加入和移除,PartitionConfiguration中的composition也可能发生变化。其中这些变化,有可能是主动的,也可能是被动的,如:

  • Primary向Secondary发送prepare消息超时,而要求踢出某个Secondary
  • MetaServer通过心跳探测到某个ReplicaServer失联了,发起group变更
  • 因为一些负载均衡的需求,Primary可能会主动发生降级,以进行迁移
    发生ReplicaGroup成员变更的原因不一而足,这里不再一一列举。但总的来说,成员的每一次变更,都会在MetaServer这里进行记录,每次变更所引发的PartitionConfiguration变化,也都会由MetaServer进行持久化。

值得说明的是,和很多Raft系的存储系统(Kudu、TiKV)不同,Pegasus的MetaServer并非group成员变更的见证者,而是持有者。在前者的实现中,group的成员变更是由group本生发起,并先在group内部做持久化,之后再异步通知给MetaServer。

而在Pegasus中,group的状态变化都是先在MetaServer上发生的,然后再在group的成员之间得以体现。哪怕是一个Primary想要踢出一个Secondary, 也要先向MetaServer发起申请;等MetaServer“登记在案”后,这个变更才会在Primary上生效。

ReplicaServer的管理

当一台ReplicaServer上线时,它会首先向MetaServer进行注册。注册成功后,MetaServer会指定一些Replica让该Server进行服务。

在ReplicaServer和MetaServer都正常运行时,ReplicaServer会定期向MetaServer发送心跳消息,来确保在MetaServer端自己“活着”。当MetaServer检测到ReplicaServer的心跳断掉后,会把这台机器标记为下线并尝试对受影响的ReplicaGroup做调整。这一过程,我们叫做FailureDetector。

当前的FailureDetector是按照PacificA中描述的算法来实现的。主要的改动有两点:

  • PacificA中要求FailureDetector在ReplicaGroup中的Primary和Secondary之间实施,而Pegasus在MetaServer和ReplicaServer之间实施。
  • 因为MetaServer的服务是采用主备模式保证高可用的,所以我们对论文中的算法做了些强化:即FailureDetector的双方是ReplicaServer和“主备MetaServer组成的group”。这样的做法,可以使得FailureDetector可以对抗单个MetaServer的不可用。

算法的细节不再展开,这里简述下算法所蕴含的几个设计原则:

  1. 所有的ReplicaServer无条件服从MetaServer
    当MetaServer认为ReplicaServer不可用时,并不会再借助其他外界信息来做进一步确认。为了更进一步说明问题,考虑以下情况:

    上图给出了一种比较诡异的网络分区情况:即网络中所有其他的组件都可以正常连通,只有MetaServer和一台ReplicaServer发生了网络分区。在这种情况下,仅仅把ReplicaServer的生死交给MetaServer来仲裁可能略显武断。但考虑到这种情况其实极其罕见,并且就简化系统设计出发,我们认为这样处理并无不妥。而且假如我们不开上帝视角的话,判断一个“crash”是不是“真的crash”本身就是非常困难的事情。
    与此相对应的是另外一种情况:假如ReplicaServer因为一些原因发生了写流程的阻塞(磁盘阻塞,写线程死锁),而心跳则由于在另外的线程中得以向MetaServer正常发送。这种情况当前Pegasus是无法处理的。一般来说,应对这种问题的方法还是要在server的写线程里引入心跳,后续Pegasus可以在这方面跟进。
  2. Pefect Failure Detector
    当MetaServer声称一个ReplicaServer不可用时,该ReplicaServer一定要处于不可服务的状态。这一点是由算法本身来保障的。之所以要有这一要求,是为了防止系统中某个ReplicaGroup可能会出现双主的局面。
    Pegasus使用基于租约的心跳机制来进行失败检测,其原理如下(以下的worker对应ReplicaServer, master对应MetaServer):

    说明:
  • beacon总是从worker发送给master,发送间隔为beacon_interval
  • 对于worker,超时时间为lease_period
  • 对于master,超时时间为grace_period
  • 通常来说:grace_period > lease_period > beacon_interval * 2
    以上租约机制还可以用租房子来进行比喻:
  • 在租房过程中涉及到两种角色:租户和房东。租户的目标就是成为房子的primary(获得对房子的使用权);房东的原则是保证同一时刻只有一个租户拥有对房子的使用权(避免一房多租)。
  • 租户定期向房东交租金,以获取对房子的使用权。如果要一直住下去,就要不停地续租。租户交租金有个习惯,就是每次总是交到距离交租金当天以后固定天数(lease period)为止。但是由于一些原因,并不是每次都能成功将租金交给房东(譬如找不到房东了或者转账失败了)。租户从最后一次成功交租金的那天(last send time with ack)开始算时间,当发现租金所覆盖的天数达到了(lease timeout),就知道房子到期了,会自觉搬出去。
  • 房东从最后一次成功收到租户交来的租金那天开始算时间,当发现房子到期了却还没有收到续租的租金,就会考虑新找租户了。当然房东人比较好,会给租户几天宽限期(grace period)。如果从上次收到租金时间(last beacon receive time)到现在超过了宽限期,就会让新的租户搬进去。由于此时租户已经自觉搬出去了,就不会出现两个租户同时去住一个房子的尴尬情况。
  • 所以上面两个时间:lease period和grace period,后者总是大于前者。

集群的负载均衡

磁盘上的replica与一主两备

为了提高数据的可靠性,在 Pegasus 项目中,每个 Partition 都会有多个副本(默认一主两备),并且它们会被分布到不同的 ReplicaServer 上,以保证数据在某个 ReplicaServer 或磁盘出现故障时也能够被访问和恢复。同时,Pegasus 会通过一些算法来确定每个 Partition 的副本数量和分布策略,以提高数据的负载均衡和容错性。

在数据的复制和高可用性方案中,每个 Partition 都会有一个 Primary replica文件 和多个 Secondary replica文件。其中,Primary 负责接收来自客户端的更新请求,然后将这些更新请求分发给所有的 Secondary 进行备份,确保数据的一致性。当 Primary 出现宕机等故障情况时,会从多个 Secondary 中选举一个新的 Primary 提供服务,保障系统的正常运行。

在识别 Primary 和 Secondary 时,Pegasus 使用了一种基于 Paxos 一致性算法的实现。具体而言,每个 ReplicaServer 作为一个 Paxos Group 的一部分,负责处理和协调 Group 中的 Primary 选举和 Leader 并发控制等操作,以确保数据的一致性和正确性。

关于partition

Pegasus 将整个数据集分成若干个 partition,每个 partition 都包含一部分数据。为了实现数据的高可用性和负载均衡,每个 partition 可以有多个副本,这些副本分布在不同的 ReplicaServer 上,以保证数据不会因为某个副本所在的节点出现故障而导致不可用。

Pegasus 中的 Partition 使用了一种哈希算法来进行分区,并且与数据的 key 相关联。具体而言,Pegasus 会根据 key 的值对 Partition 进行哈希,并将哈希的结果映射到某个 Partition 上。一个 Partition 所包含的数据范围是连续的,这样可以方便地实现数据的扫描和切片等操作。

具体概念

在Pegasus中,负载均衡主要包括以下几个方面的内容:

  • 如果某个partition分片不满足一主两备,要选择一个机器将缺失的分片补全。这个过程在Pegasus中叫做cure。
    1. 如果一个ReplicaGroup中缺少Primary, MetaServer会选择一个Secondary提名为新的Primary;
    2. 如果ReplicaGroup中缺Secondary,MetaServer会根据负载选一个合适的Secondary;
    3. 如果备份太多,MetaServer会根据负载选一个删除。
  • 当所有的分片都满足一主两备份后,对集群各个replica server上分片的个数做调整,尽量让每个机器上服务的分片数都维持在一个相近的水平上。这个过程在Pegasus中叫做balance。
    1. 每个ReplicaServer的各个磁盘上的Replica的个数
    2. Primary和Secondary分开考虑
    3. 各个表分开考虑
    4. 如果可以通过做Primary切换来调匀,则优先做Primary切换。
  • 如果一个replica server上挂载了多个磁盘,并且通过配置文件data_dirs提供给Pegasus使用。replica server要尽量让每个磁盘上分片的数量都维持在一个相近的水平上。

Pegasus为Partition定义了几种健康状况:

  • 【fully healthy】: 健康的,完全满足一主两备
  • 【unreadable】: 分片不可读了。指的是分片缺少primary, 但有一个或两个secondary。
  • 【readable but unwritable】: 分片可读但是不可写。指的是只剩下了一个primary,两个secondary副本全部丢失
  • 【readable and writable but unhealthy】: 分片可读可写,但仍旧不健康。指的是三副本里面少了一个secondary
  • 【dead】: partition的所有副本全不可用了,又称之为DDD状态。

相关操作

可以通过pegasus的shell客户端来观察系统的Partition情况:

1
2
3
4
5
6
>>> nodes -d
address status replica_count primary_count secondary_count
10.132.5.1:32801 ALIVE 54 18 36
10.132.5.2:32801 ALIVE 54 18 36
10.132.5.3:32801 ALIVE 54 18 36
10.132.5.5:32801 ALIVE 54 18 36

如果节点间的partition个数分布差异太大,可以采用”set_meta_level lively”的命令来进行调整。
可以用来某张表的所有partition的分布情况:可以观察到某个具体partition的组成,也可以汇总每个节点服务该表的partition个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
>>> app temp -d
[Parameters]
app_name: temp
detailed: true

[Result]
app_name : temp
app_id : 14
partition_count : 8
max_replica_count : 3
details :
pidx ballot replica_count primary secondaries
0 22344 3/3 10.132.5.2:32801 [10.132.5.3:32801,10.132.5.5:32801]
1 20525 3/3 10.132.5.3:32801 [10.132.5.2:32801,10.132.5.5:32801]
2 19539 3/3 10.132.5.1:32801 [10.132.5.3:32801,10.132.5.5:32801]
3 18819 3/3 10.132.5.5:32801 [10.132.5.3:32801,10.132.5.1:32801]
4 18275 3/3 10.132.5.5:32801 [10.132.5.2:32801,10.132.5.1:32801]
5 18079 3/3 10.132.5.3:32801 [10.132.5.2:32801,10.132.5.1:32801]
6 17913 3/3 10.132.5.2:32801 [10.132.5.1:32801,10.132.5.5:32801]
7 17692 3/3 10.132.5.1:32801 [10.132.5.3:32801,10.132.5.2:32801]

node primary secondary total
10.132.5.1:32801 2 4 6
10.132.5.2:32801 2 4 6
10.132.5.3:32801 2 4 6
10.132.5.5:32801 2 4 6
8 16 24

fully_healthy_partition_count : 8
unhealthy_partition_count : 0
write_unhealthy_partition_count : 0
read_unhealthy_partition_count : 0

list app temp succeed

控制集群的负载均衡

  1. set_meta_level
    这个命令用来控制meta的运行level,支持以下几种level:
  • freezed:meta server会停止unhealthy partition的cure工作,一般在集群出现较多节点宕机或极其不稳定的情况下使用,另外如果集群的节点数掉到一个数量或者比例以下(通过配置文件min_live_node_count_for_unfreeze和node_live_percentage_threshold_for_update控制)就会自动变为freezed,等待人工介入。
  • steady:meta server的默认level, 只做cure,不做balance。
  • lively:meta server会调整分片数,力求均衡。
    可以使用cluster_info或者get_meta_level查看当前集群的运行level。
    关于调整的一些建议:
  • 先用shell的nodes -d命令查看集群是否均衡,当不均衡时再进行调整。通常在以下几种情况发生后,需要开启lively进行调整:
    • 新创建了表,这个时候分片数目可能不均匀。
    • 集群上线、下线、升级了节点,这时候分片数目也可能不均匀。
    • 有节点宕机,一些replica迁移到了别的节点上。
  • 调整过程会触发replica迁移,影响集群可用度,虽然影响不大,但是如果对可用度要求很高,并且调整需求不紧急,建议在低峰时段进行调整。
  • 调整完成后通过set_meta_level steady将level重置为steady状态,避免在平时进行不必要的replica迁移,减少集群抖动。

MetaServer的高可用

为了保证MetaServer本身不会成为系统的单点,MetaServer依赖Zookeeper做了高可用。在具体的实现上,我们主要使用了Zookeeper节点的ephemeral和sequence特性来封装了一个分布式锁。该锁可以保证同一时刻只有一个MetaServer作为leader而提供服务;如果leader不可用,某个follower会收到通知而成为新的leader。

为了保证MetaServer的leader和follower能拥有一致的集群元数据,元数据的持久化我们也是通过Zookeeper来完成的。

zookeeper分布式锁

  1. 创建一个 ZooKeeper 的客户端连接

    1
    2
    // 其中 callback_func 是回调函数,在本例中可以为空,timeout 是连接超时时间。
    zhandle_t *zkhandle = zookeeper_init("localhost:2181", callback_func, timeout, 0, 0, 0);
  2. 创建一个用于锁定的父节点,这个节点是 ephemeral 节点,所有的锁都是在这个节点下创建的

    1
    2
    3
    char lockNodeName[256];
    memset(lockNodeName, 0, sizeof(lockNodeName));
    zoo_create(zkhandle, lockParentPath "/lock-", "", 0, &ZOO_OPEN_ACL_UNSAFE, ZOO_EPHEMERAL | ZOO_SEQUENCE, lockNodeName, sizeof(lockNodeName));
  3. 当需要获取锁的时候,创建一个 sequence 节点,将这个节点作为锁定的节点

    1
    2
    3
    4
    // 创建成功后,lockNodeName 将返回锁的全路径名,例如 /lock/lock-0000000001。
    char lockNodeName[256];
    memset(lockNodeName, 0, sizeof(lockNodeName));
    zoo_create(zkhandle, lockParentPath "/lock-", "", 0, &ZOO_OPEN_ACL_UNSAFE, ZOO_EPHEMERAL | ZOO_SEQUENCE, lockNodeName, sizeof(lockNodeName));
  4. 获取锁前,先获取锁定节点的子节点列表,如果锁定节点下只有当前请求锁的这个节点,则说明锁没有被其他客户端占用,此时获取到锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct String_vector children_list;
    zoo_get_children(zkhandle, lockParentPath, 0, &children_list);
    std::sort(children_list.data, children_list.data + children_list.count, [](const char* a, const char* b) {
    return std::strcmp(a, b) < 0;
    });

    char shortest_lock_node_name[256];
    memset(shortest_lock_node_name, 0, sizeof(shortest_lock_node_name));
    std::snprintf(shortest_lock_node_name, sizeof(shortest_lock_node_name), "%s/%s", lockParentPath, children_list.data[0]);

    if (std::strcmp(lockNodeName, shortest_lock_node_name) == 0) {
    // 获取到锁
    }
  5. 如果获取不到锁,则针对当前节点下一个次小的节点添加事件监听器,等待当前节点被删除

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    char last_open_lock[256];
    memset(last_open_lock, 0, sizeof(last_open_lock));
    std::snprintf(last_open_lock, sizeof(last_open_lock), "%s/%s", lockParentPath, children_list.data[std::lower_bound(children_list.data, children_list.data + children_list.count, lockNodeName,
    [](const char* a, const char* b) {
    return std::strcmp(a, b) < 0;
    }) - children_list.data - 1]);

    int rc = zoo_wexists(zhandle, last_open_lock, watcher_func, watcher_ctx, &last_open_lock_stat);
    if (rc == ZOK) {
    // 等待锁
    pthread_cond_wait(&watcher_cond, &watcher_mutex);
    }
    // 其中,watcher_func 是事件监听器,在本例中可以为空,watcher_ctx 是传递给事件监听器的上下文参数,watcher_cond 和 watcher_mutex 分别是线程条件变量和互斥锁。
  6. 当锁被释放时,删除锁定节点,其他等待锁的节点通过监听器获取通知,检查是否可以获取到锁

    1
    zoo_delete(zhandle, lockNodeName, -1);

读写流程

ReplicaServer由一个个的Replica的组成,每个Replica表示一个数据分片的Primary或者Secondary。真正的读写流程,则是由这些Replica来完成的。

前面说过,当客户端有一个写请求时,会根据MetaServer的记录查询到分片对应的ReplicaServer。具体来说,客户端需要的其实是分片Primary所在的ReplicaServer。当获取到这一信息后,客户端会构造一条请求发送给ReplicaServer。请求除数据本身外,最主要的就包含了分片的编号,在Pegasus里,这个编号叫Gpid(global partition id)。

ReplicaServer在收到写请求时,会检查自己是不是能对请求做响应,如果可以的话,相应的写请求会进入写流程。具体的写流程不再赘述,大体过程就是先prepare再commit的两阶段提交。

读流程比写流程简单些,直接由Primary进行读请求的响应。
除此之外,Pegasus还提供了两种scan的,允许用户对写入的数据进行遍历:

  • HashScan: 可以对同一个HashKey下的所有(SorkKey, Value)序对进行扫描,扫描结果按SortKey排序输出。该操作在对应Primary上完成。
  • table全局scan: 可以对一个表中的所有数据进行遍历。该操作在实现上会获取一个表中所有的Partition,然后逐个对Primary进行HashScan。

SharedLog和PrivateLog

  1. 所有的写请求先合着写一个WAL,叫做SharedLog;
  2. 同时,对于每个Replica, 所有的请求都有一个内存cache, 然后以批量的方式写各自的WAL,叫做PrivateLog;
  3. 在进程重启的时候,PrivateLog缺失的部分可以在重放SharedLog时补全;
  4. 添加PotentialSecondary时,直接使用PrivateLog。

在Pegasus中,一个Replica有如下几种状态:

  • Primary
  • Secondary
  • PotentialSecondary(learner):当group中新添加一个成员时,在它补全完数据成为Secondary之前的状态
  • Inactive:和MetaServer断开连接时候的状态,或者在向MetaServer请求修改group的PartitionConfiguration时的状态
  • Error:当Replica发生IO或者逻辑错误时候的状态

添加Learner

添加Learner是整个一致性协议部分中最复杂的一个环节,这里概述以下其过程:

  • MetaServer向Primary发起add_secondary的提议,把一个新的Replica添加到某台机器上。这一过程不会修改PartitionConfiguration。
  • Primary定期向对应机器发起添加Learner的邀请
  • Leaner在收到Primary的邀请后,开始向Primary拷贝数据。整个拷贝数据的过程比较复杂,要根据Learner当前的数据量决定是拷贝Primary的数据库镜像、PrivateLog、还是内存中对写请求的缓存。
  • Leaner在拷贝到Primary的全部数据后,会通知Primary拷贝完成
  • Primary向MetaServer发起修改PartitionConfiguration的请求。请求期间同样拒绝写,并且仍旧是MetaServer持久化完成后Primary才会修改本地视图。

关于流控

为什么要做流量控制?主要是减小集群压力,提升稳定性。如果集群的写流量太大,就会消耗大量的系统资源(CPU、IO等),从而影响读请求的延迟。有些业务对读性能要求比较高,如果对写流量不加控制,就无法保证服务质量。

从流控的作用位置来看,可分为:

  • 客户端流控:从源头掐住流量。优点是避免不必要的网络传输;缺点是需要在客户端增加逻辑,且因为无法掌控用户的使用方式造成流控难以准确。
  • 服务端流控:在ReplicaServer节点上进行流控。优点是对客户端透明,流控集中容易掌控;缺点是只能通过增大延迟或者拒绝请求的方式来流控,不够直接,另外可能无法避免不必要的网络传输。

从流控的粒度来看,可分为:

  • 表级流控:只控制单个表的流控,粒度较细。
  • 节点级流控:针对ReplicaServer节点进行的流控,不区分具体的表。

集群部署

Pegasus分布式集群至少需要准备这些机器:

  • MetaServer:2~3台机器,无需SSD盘。
  • ReplicaServer:至少3台机器,建议挂SSD盘。譬如一台服务器挂着8块或者12块SSD盘。这些机器要求是同构的,即具有相同的配置。
  • Collector:可选角色,1台机器,无需SSD盘。该进程主要用于收集和汇总集群的统计信息,负载很小,建议放在MetaServer的其中一台机器上。

Pegasus集群依赖Zookeeper进行元数据存储和MetaServer抢锁,因此需要一个Zookeeper服务:

  • 如果在公司内部维护着Zookeeper集群,直接使用该集群就可以了。
  • 如果没有,就自己搭建一个Zookeeper集群,建议在Pegasus集群机器所在的同机房搭建。

集群升级

集群升级的重要目标在于平稳,即不停服,并且对可用性的影响降至最低。为了达到这个目标,我们先看看在升级过程中哪些地方可能会影响可用性:

  • replica server进程被kill后,该进程服务的replica无法提供服务:
    1. 对于primary replica:因为直接向客户端提供读写服务,所以进程kill后肯定会影响读写,需要等metaserver重新分派新的primary replica后才能恢复。meta server通过心跳感知replica server的存活状态,failure detection的时间延迟取决于配置参数fd_grace_seconds,通常配置为10秒,即最多需要经过10秒,meta server才能知道replica server挂了,然后重新分派新的primary replica。通过migrate_node命令,将replica server上的primary replica都迁走。通过shell的nodes -d命令查看该节点的服务replica情况,等待primary replica的个数变为0;如果长时间不变为0,重新执行上面命令。
    2. 对于secondary replica:由于不服务读,所以理论上对读无影响。但是会影响写,因为一致性协议要求一主两备都写成功,写操作才能提交。进程kill后,primary replica在执行写操作过程中会发现该secondary replica已失联,然后通知meta server将其踢掉,经过reconfiguration阶段后变成一主一备,继续提供写服务。在切换过程中尚未完成的写操作,即使有reconciliation阶段重新执行,但客户端那边大概率已经超时了,对可用性有一定影响。但是这个影响相对小些,因为reconfiguration的速度是比较快的,通常在1秒以内就能完成。
  • 升级meta server:升级meta server对可用度的影响几乎可以忽略不计,因为客户端会在本地缓存各partition的服务节点信息,通常情况下并不需要向meta server查询,因此meta server重启过程中的短暂失联对客户端基本没有影响。不过考虑到meta server需要与replica server维持心跳,所以要避免连续kill meta server进程,造成replica server心跳失联的风险。
  • 升级collector:升级collector对可用度没有影响。但是可用度统计是在collector上进行的,所以可能会对统计数据有轻微影响。

因此,在集群升级过程要提高可用性,需要考虑如下几点:

  1. 一次只能升级一个进程,且在该进程重启并完全恢复进入服务状态后,才能升级下一个进程。
    • 因为如果升级一个进程后,集群没有恢复到完全健康状态,有的partition还只有一主一备,这时再kill一个replica server的话,很可能进入只有一主的状态,无法提供写服务。
    • 另外,等待集群所有partition都恢复三备份后再继续升级下一个进程,也能有效降低数据丢失的风险。
  2. 尽量主动迁移replica,而不是被动迁移replica,避免failure detection的时间延迟影响可用度。
    • 被动迁移需要等待failure detection来感知节点失联,而主动迁移就是在kill掉replica server之前,先将这个进程服务的primary replica都迁移到其他节点上,这个reconfiguration过程是很快的,基本1秒以内完成。
    • 更进一步,还可以在kill掉replica server之前,将这个进程服务的secondary replica手动降级,将reconfiguration过程由“写失败被动触发”变为“主动触发”,也能降低对可用度的影响。通过downgrade_node命令,将replica server上的secondary replica都降级为INACTIVE。通过shell的nodes -d命令查看该节点的服务replica情况,等待secondary replica的个数变为0;如果长时间不变为0,重新执行上面命令。
  3. 尽量减少进程重启时恢复过程的工作量,缩短进程重启时间。
    • replica server在重启时需要replay log来恢复数据。如果直接kill掉,需要replay的数据量可能很大。但是如果在kill之前,先主动触发memtable的flush操作,让内存数据先落地,在重启时需要replay的数据量就会大大减少,重启时间会缩短很多,而整个集群升级所需的时间也能大大缩短。通过shell向replica server发送远程命令,将所有replica都关闭,以触发flush操作,将数据都落地。
  4. 尽量减少不必要的节点间数据拷贝,避免因为增加CPU/网络/IO负载影响可用度。
    • replica server挂掉后,部分partition进入一主一备的状态。如果meta server立即在其他replica server上补充备份,会带来大量的跨节点数据拷贝,增加CPU/网络/IO负载压力,影响集群稳定性。Pegasus解决这个问题的办法是,允许在一段时间内维持一主一备状态,给原来的replica server进行恢复的机会。如果长时间没有恢复,才会在新的replica server上补充备份。这样兼顾了数据的安全性和集群的稳定性。可以通过配置参数replica_assign_delay_ms_for_dropouts控制等待时间,默认为10分钟,使用shell工具将集群的meta level设置为steady,关闭负载均衡功能,避免不必要的replica迁移。通过shell向meta server发送远程命令,禁掉add_secondary操作。

跨机房同步

在 pegasus 中,跨机房同步又被称为 热备份,或 duplication,简称 dup。这一功能的主要目的是保证 数据中心级别的可用性。当业务需要保证服务与数据能够容忍机房故障时,可以考虑使用此功能。

1
2
3
4
5
6
7
8
9
client               client               client
+ + +
+---------v-------+ +--------v--------+ +------v-----------+
| | | | | |
| pegasus-beijing <---> pegasus-tianjin <---> pegasus-shanghai |
| | | | | |
+----------^------+ +-----------------+ +---------^--------+
| |
+------------------------------------------+

热备份使用日志异步复制的方式来实现跨集群的同步,可与 mysql 的 binlog 复制和 hbase replication 类比。

1
2
3
4
5
6
7
8
9
10
11
>>> ls
app_id status app_name
12 AVAILABLE account_xiaomi

>>> add_dup account_xiaomi tjsrv-account
Success for adding duplication [appid: 12, dupid: 1535008534]

>>> query_dup account_xiaomi
duplications of app [account_xiaomi] are listed as below:
| dup_id | status | remote cluster | create time |
| 1535008534 | DS_START | tjsrv-account | 2018-08-23 15:15:34 |

通过 add_dup 命令,bjsrv-account 集群的表 account_xiaomi 将会近实时地把数据复制到 tjsrv-account 上,这意味着,每一条在北京机房的写入,最终都一定会复制到天津机房,tjsrv-account的ip地址是在配置里面配好的。

有时一个线上表可能在设计之初未考虑到跨机房同步的需求,而在服务一段时间后,才决定进行热备份。此时我们需要将源集群已有的全部数据复制到目的集群。因为是线上表,我们要求拷贝过程中:

  • 不可以停止服务
  • 拷贝过程中的写增量数据不能丢失

面对这个需求,我们的操作思路是:

  • 首先源集群保留从此刻开始的所有写增量(即WAL日志)
  • 将源集群的全量快照(冷备份)上传至 HDFS / xiaomi-FDS 等备份存储上。
  • 然后恢复到目标集群。
  • 此后源集群开启热备份,并复制此前堆积的写增量,复制到远端目标集群。
nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!