在 pegasus 中,跨机房同步又被成为 热备份,或 duplication,简称 dup。这一功能的主要目的是保证 数据中心级别的可用性。当业务需要保证服务与数据能够容忍机房故障时,可以考虑使用此功能。
此外,当 Pegasus 客户端在多机房分布时,时常会遇到跨机房访问 Pegasus 服务带来的高延时问题,这时我们可以将 Pegasus 的服务与客户端部署在相同的机房内,客户端可以只读写本地机房的服务,然后由热备份功能将写同步到各个机房上。这种做法既能保证各个机房都有完整数据,又能避免跨机房的延时开销。
client client client
+ + +
+---------v-------+ +--------v--------+ +------v-----------+
| | | | | |
| pegasus-beijing <---> pegasus-tianjin <---> pegasus-shanghai |
| | | | | |
+----------^------+ +-----------------+ +---------^--------+
| |
+------------------------------------------+
我们能够做到一主一备(single-master),也能提供多机房多主(multi-master),用户可以根据需要进行配置。
这里需要注意的是,跨机房同步是异步的数据复制,并非完全实时。与单机房不同,我们不提供跨机房 read-after-write 的一致性保证。目前在跨机房网络健康的环境下,数据延时大概在 10s 左右,即 A 机房的写数据大概在 10s 后会写入 B 机房。
Get Started
假设我们有两个 pegasus 集群 bjsrv-account 和 tjsrv-account 分别位于北京与天津的两个机房内,表 account_xiaomi
由于存储了极其关键的用户帐号数据,需要能够在双集群保证可用,所以我们为它实施热备份:
./run.sh shell -n bjsrv-account
Type "help" for more information.
Type "Ctrl-D" or "Ctrl-C" to exit the shell.
The cluster name is: bjsrv-account
The cluster meta list is: ***
>>> 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 上,这意味着,每一条在北京机房的写入,最终都一定会复制到天津机房。
热备份使用日志异步复制的方式来实现跨集群的同步,可与 mysql 的 binlog 复制和 hbase replication 类比。每个 replica 单独发送自己的日志到远端集群上,replica 之间互不干扰,保证了。
热备份的两集群的表名需要保持一致,但 partition 的个数不需要相同。例如用户可以建表如下:
>>> cc bjsrv-account
>>> create account_xiaomi -p 128
>>>
>>> cc tjsrv-account
>>> create account_xiaomi -p 32
线上表开启热备
有时一个线上表可能在设计之初未考虑到跨机房同步的需求,而在服务一段时间后,才决定进行热备份。此时我们需要进行全量数据拷贝,例如将 bj 已有的全部数据复制到 tj。因为是线上表,拷贝过程中不可以停止服务,同时拷贝过程中的写增量数据也不能丢。
面对这个需求,我们的思路是:首先 bj 保留从此刻开始的所有写增量(即 wal),将 bj 的全量快照(冷备份)上传至 HDFS / xiaomi-FDS 上,然后恢复到 tj。此后 bj 开启热备份,并重放此前堆积的写增量,复制到远端 tj 机房。
如何保留写增量?pegasus 如此进行操作:
- 首先使用
add_dup [—freezed/-f]
表示不进行日志复制,它的原理就是阻止当前日志 GC(log compaction)。该操作 必须最先执行,否则无法保证数据完整性。
>>> cc bjsrv-account
>>> add_dup account_xiaomi tjsrv-account --freezed
- 虽然不进行复制,但每个分片都会记录当前确认点(confirmed_decree)(初始为
private_log->max_committed_decree
),并持久化到 meta server 上。注意需等待所有的 replica 都将当前确认点更新至 meta server 后,才可进行下一步操作,这是该功能正确性的前提。 confirme_decree 值为 -1 即表示该分片的确认点尚未同步。
>>> query_dup_detail account_xiaomi 1535008534
>>> {"dupid":1548442533,"status":"DS_START","remote":"c4srv-feedhistory","create_ts":1548442533763,"progress":[{"pid":0,"confirmed":-1},{"pid":1,"confirmed":276444333},{"pid":2,"confirmed":-1},{"pid":3,"confirmed":-1},{"pid":4,"confirmed":-1},{"pid":5,"confirmed":-1},{"pid":6,"confirmed":-1},{"pid":7,"confirmed":279069949},{"pid":8,"confirmed":-1}}
>>> query_dup_detail account_xiaomi 1535008534
>>> {"dupid":1548442533,"status":"DS_START","remote":"c4srv-feedhistory","create_ts":1548442533763,"progress":[{"pid":0,"confirmed":276444111},{"pid":1,"confirmed":276444333},{"pid":2,"confirmed":276444332},{"pid":3,"confirmed":276444222},{"pid":4,"confirmed":276444111},{"pid":5,"confirmed":276444377},{"pid":6,"confirmed":276444388},{"pid":7,"confirmed":279069949},{"pid":8,"confirmed":276444399}}
- 使用冷备份将数据快照上传至远端存储,再使用恢复功能在 tjsrv-account 恢复该表。示例命令如下:
# 立刻对表(app_id = 12)进行冷备
./run.sh shell -n bjsrv-account
>>> add_backup_policy -p dup_transfer -b fds_wq -a 12 -i 86400 -s 12:01 -c 1
# 耐心等待备份生成
>>> query_backup_policy -p dup_transfer
policy_info:
name : dup_transfer
backup_provider_type : fds_wq
backup_interval : 86400s
app_ids : {12}
start_time : 12:01
status : enabled
backup_history_count : 1
backup_infos:
[1]
id : 1541649698875
start_time : 2018-11-08 12:01:38
end_time : 2018-11-08 12:03:51
app_ids : {60}
# 在天津机房恢复表
./run.sh shell -n tjsrv-account
>>> restore_app -c bjsrv-account -p dup_transfer -a account_xiaomi -i 12 -t 1541649698875 -b fds_wq
# 开启日志复制
>>> start_dup account_xiaomi <dupid>
# 至此热备份已经完全可用。
当 start_dup
时,热备份任务会从之前的确认点开始复制,这样我们就保证了写增量的完整性。
另外需注意的是,由于写增量的长时间堆积,一时可能有大量日志复制,热备份流量会突增,从而导致服务不稳定。因此,我们需要在远端机房设置限流(write throttling)。
>>> get_app_envs
get app envs succeed, count = 7
=================================
replica.write_throttling = 30000*delay*100,40000*reject*200
=================================
元信息存储
热备份的元信息会经由 meta server 持久化于 zookeeper 上,其存储路径如下:
<cluster_root> <app_id> <dupid>
| | |
| | |
[zk: 127.0.0.1:22181(CONNECTED) 0] get /pegasus/bjsrv-account/0.0.x.x/apps/1/duplication/1537336970
{"remote":"tjsrv-account","status":"DS_START","create_timestamp_ms":1537336970483}
配置项
duplication_enabled
:如果遇到紧急情况想要手动关闭热备份,可以将该项设置为 false,默认为 true。开启热备份的集群必须配置远端机房的具体地址
[pegasus.clusters]
tjsrv-account = 127.0.0.1:51601,127.0.0.1:51601
热备份要求
allow_non_idempotent_write
必须为 false(默认值)。因为开启热备份的集群 不支持 “幂等操作” 如 CHECK_AND_MUTATE, CHECK_AND_SET,INCR 等。如果有多机房同时写的需求,配置
verify_timetag
需修改为 true :
[pegasus.server]
verify_timetag = true
- 热备份的集群需要登记其 cluster_id:
# Configuration for cluster_id.
# This is required for every cluster that enables duplication.
[duplication-group]
tjsrv-account = 1
bjsrv-account = 2
我们在每条数据前都会加上 timestamp+cluster_id
的前缀,timestamp 即数据写到 pegasus 的时间戳,cluster_id 即上面 duplication-group 中所配置的,tjsrv 的 cluster_id 为 1,bjsrv 的 cluster_id 为 2。
cluster_id 具有两个作用:
一旦出现写冲突,例如 tjsrv 和 bjsrv 同时写 key "user_1"
,系统首先会检查两次写的时间戳,以时间戳大的为最终值。当极罕见地遇到时间戳相同的情况时,以 cluster_id 大的为最终值。使用这种机制我们可以保证两集群的最终值一定相同。
cluster_id 的另一个作用就是保证热备份不会
相关监控项
监控项 | 描述 |
---|---|
dup.log_read_in_bytes_rate | |
dup.log_mutations_read_rate | |
dup.shipped_size_in_bytes_rate | |
dup.time_lag(ms) | |
dup.shipped_ops | |
dup.failed_shipping_ops | |
duplicated_put_qps | |
duplicated_remove_qps | |
duplicated_multi_put_qps | |
duplicated_multi_remove_qps |
Known Limitations
- 热备份暂时不建议两机房同时写一份数据。在我们的业务经验看来,通常这是可以接受的。用户可以将数据均分在 tjsrv 和 bjsrv 两机房内,热备份能保证当任一机房宕机,只有数秒的数据丢失(假设机房之间网络稳定)。