关于partition split的基本概念和操作示例可以参照 administration/partition-split,这里将主要描述partition split的设计和实现细节。
Pegasus在创建table时需要指定partition个数,且该个数为2的幂次。然而,在原有设计中,表的partition个数并不会随着数据量变化而动态变化。在用户的数据量和访问QPS增加,当前partition个数无法满足需求之前,我们需要人工地增加partition数目,避免阈值达到后的服务降级。
为了简化系统的设计和实现,我们这里要求增加后的partition个数必须是之前的2倍。若原partition个数为8,split后partition个数将变成16。具体来说,原partition序号为 [0, 7],split后partition序号为 [0, 15],0号partition将分裂为0号与8号,以此类推。
下图显示了表id为1,0号partition在split前后的示意图:
+------+ +------+ +------+
| 1.0 | | 1.0 | | 1.0 |
+------+ +------+ +------+
primary secondary secondary
|
|
+------+ +------+ +------+
| 1.0 | | 1.0 | | 1.0 |
| 1.8 | | 1.8 | | 1.8 |
+------+ +------+ +------+
primary secondary secondary
|
|
+------+ +------+ +------+ +------+ +------+ +------+
| 1.0 | | 1.0 | | 1.0 | | 1.8 | | 1.8 | | 1.8 |
+------+ +------+ +------+ +------+ +------+ +------+
primary secondary secondary primary secondary secondary
为了方便描述和画示意图,我们将整体流程分为下面3个部分:
+--------+ split +------------+ partition_count*2 +-----------+
| client ----------> meta_server --------------------> zookeeper |
+--------+ +------------+ +-----------+
|
| on_config_sync
|
+--------v----------+
| primary partition |
+-------------------+
开始partition split的流程如上图所示:
partition split是指replica group中的每个replica一分为二的过程。一般来说,一个replica group会包括一个primary和两个secondary共三个replica,分裂后,会新增三个replica,并分别对应前面的一主两备。我们称之前的三个replica为parent,新增的为child。
partition split的过程与learn比较类似,但也有一定的区别。learn是potential secondary从primary上拷贝数据,它们位于两台不同的机器;而split是三个child分别从它们对应的parent复制数据,child与parent在同一台机器上,并在同一个盘上。因此,child可以:
+--------+ +-------+
| parent | | child |
+--------+ +-------+
| 4. create child |
|---------------------------------->|
| |
| 5. async learn |
|---------------------------------->|
| (2pc async) |
| |
| 6. finish async learn |
|<----------------------------------|
| (send to primary parent) |
| |
| 7. all child finish async learn |
|-----------------------------------|
| (2pc sync, wait for sync_point) |
| |
| 8. update child partition_count |
|---------------------------------->|
| |
| 9. update partition_count ack |
|<--------------------------------->|
| |
replica执行partition split的流程如上图所示:
同步点
**,当同步点mutation commit后,所有的child已拥有所需的全部数据+----------------+ 10. register child +-------------+ +-----------+
| |------------------->| | 11. update child config | |
| parent primary | | meta_server |------------------------>| zookeeper |
| |<-------------------| | | |
+----------------+ ack +-------------+ +-----------+
|
| 12. active child
+-------v---------+
| child primary |
+-----------------+
注册child的流程如上图所示:
在第13步之前,parent与child所对应的所有读写请求都由parent处理;在第13步之后,parent将拒绝child对应的请求。
我们引入**partition_version
**这个概念,来保证client读写数据的正确性,即,不要把数据写错地方,不要读到错误的数据,不要读不到数据。
partition_version是primary内存中的一个变量,一般应为partition_count – 1,在split过程中拒绝读写时候会被设置为-1
client在向server端发读写请求时,会在请求的header中带上所访问的hash_key的hash值,primary将此hash值与partition_version进行按位与操作,检查结果是否等于partitionId。 检查的过程用伪代码表示如下:
if partition_version == -1 return ERR_OBJECT_NOT_FOUND elif partition_version & hash ! = partition return ERR_PARENT_PARTITION_MISUSED return ERR_OK
client收到ERR_OBJECT_NOT_FOUND时,会从meta_server更新当前partition的信息;收到ERR_PARENT_PARTITION_MISUSED时,会更新table所有partition的信息。信息更新后,再向正确的partition重发请求
下面举一个例子来分析partition_version的作用:
假设split前,table的partition个数为4,split后为8,client需要读写hash_key的hash值为5的key-value,
partition_count(meta) = 8 ballot(replica5) = -1 partition_count(replica1) = 4 partition_version(replica1) = 4–1 = 3
partition_count(client-old) = 4
,会访问replica1partition_count(client-new) = 8
,通过hash % partition_count = 5%8 = 5
得知应该访问replica5,但是,ballot(replica5) = -1,client知道replica5暂不存在,所以根据hash % (partition_count / 2) = 1
,会访问replica1,replica1收到请求后,检查hash & partition_version(replica1) = 5&3 = 1
,正确partition_count(replica1) = partition_count(replica5) = 8 partition_version(replica1) = partition_version(replica5) = 7
hash & partition(replica1) = 5 % 8 = 5
,由于5不等于partitionId,所以拒绝访问,并通知client从meta_server更新config,client更新后,将会访问replica5,读写也正确上面描述的交互依赖于一个前提,即request header中的hash必须是希望访问的hash_key的hash值,而这个假设对于绝大部分请求都成立,除了全表scan。在full_scan时,request header中的hash是partitionId,因此可能会得到冗余数据。
因此,我们为full_scan增加一步检查操作,replica server从rocksdb中读到数据后,检查数据的hash,滤除无效数据。这样,除了在split的过程中,client不会读到无效数据。由于full_scan本身不具备原子性和一致性,想完全解决一致性问题很难,而split是一个非频繁操作,我们只要让split避开full_scan的时间段就可以了。
partition_version除了用于client的访问控制,还用于无效数据清理。 partition split结束后,历史数据会同时存在于parent和child,但实际上应该分别只保留一半数据。我们同样可以使用partition_version & hash == partitionId
把无效数据区分出来,并通过rocksdb filter回收清理这些数据。
在执行partition split时,我们需要检查partition的健康状态,我们认为只有在partition健康的情况下,才会开始split。一个典型的“不健康”场景是partition正在执行learn,或者secondary数量过少。并且,replica是通过on_config_sync检查partition_count是否翻倍来判断是否需要执行split,而on_config_sync是周期性执行的,replica完全可以等到partition健康再进行split。
在执行partition split过程中,parent的ballot不能发生变化,一旦发生变化,将抛弃这个partition所有的child,重新开始split过程。即在split过程中,如果发生replica迁移,无论是因为故障还是负载均衡的原因,我们都认为本次split失败,在之后的on_config_sync中重新split。
若在partition split过程中,meta_server发生故障,meta group会选出一个新的leader,会从zookeeper中得到新的partition_count,并通过on_config_sync开始split