数据模型
组合键:Table + HashKey + SortKey
Table实现业务数据的隔离
HashKey决定数据在那个分片
SortKey决定数据在分片内的排序
一致性协议
使用PacificA协议,保证多副本数据的一致性。
<!-- addition -->
机器
Pegasus分布式集群至少需要准备这些机器:
MetaServer
link:Meta Server 的设计
要求:2~3台机器,无需SSD盘。
作用:用来保存表和表的分片信息。
ReplicaServer:
link:Replica Server 的设计
要求至少3台机器,建议挂SSD盘。譬如一台服务器挂着8块或者12块SSD盘。这些机器要求是同构的,即具有相同的配置。
作用:至少三台ReplicaServer。每一个ReplicaServer都是由多个Replica组成的。每一个Replica表示的是一个数据分片的Primary或Secondary。
Collector:可选角色,1台机器,无需SSD盘。该进程主要用于收集和汇总集群的统计信息,负载很小,建议放在MetaServer的其中一台机器上。
Replica
一个数据分片对应3个Replica。
Replica有两个种类:Primary和Secondary。
在一个数据分片对应的 3 个或以上的Replica中,只有1个Primary,其余的均是Secondary。
很多个Replica组成一个ReplicaServer,同一个ReplicaServer中的Replica种类不相同。
同一个ReplicaServer中的Replica不一定全是Primary,也不一定全是Secondary。
一个基本的Pegasus集群,少需要 3 个ReplicaServer。
在Pegasus中,一个Replica有如下几种状态:
Primary
Secondary
PotentialSecondary(learner):
当group中新添加一个成员时,在它补全完数据成为Secondary之前的状态
Inactive:
和MetaServer断开连接时候的状态,或者在向MetaServer请求修改group的PartitionConfiguration时的状态
Error:
当Replica发生IO或者逻辑错误时候的状态
写流程
写流程类似于两段提交:
客户端根据Key首先查询MetaServer,查询到这个Key对应的分片的对应的ReplicaServer。具体来说,客户端需要的其实是分片Primary所在的ReplicaServer。
客户端向Primary ReplicaServer发起写请求。
Primary ReplicaServer向其对应的两台或以上的Secondary ReplicaServer复制数据。
Secondary ReplicaServer将数据写入成功后,Primary ReplicaServer向客户端返回成功的响应。
可能导致ReplicaServer不能响应写请求的原因有:
ReplicaServer无法向MetaServer持续汇报心跳,自动下线。
Replica在IO上发生了一些无法恢复的异常故障,自动下线。
MetaServer将Replica的Primary进行了迁移。
Primary在和MetaServer进行group成员变更的操作,拒绝写。
当前Secondary个数太少,Replica出于安全性考虑拒绝写。
出于流控考虑而拒绝写。
读流程
只从Primary读。
宕机恢复
MetaServer和所有的ReplicaServer维持心跳。
通过心跳来实现失败检测。
宕机恢复的几种情况:
Primary Failover:如果某个分区的Primary所在的ReplicaServer宕机了,那么MetaServer 就会选择一个Secondary成为Primary。过后再添加Secondary。
Secondary Failover:如果某个分区的Secondary所在的ReplicaServer宕机了,那么暂时使用一主一副的机构继续提供服务。过后再添加Secondary。
MetaServer Failover:主MetaServer宕机了,备用的MetaServer通过zookeeper抢主成为新的主MetaServer。从zookeeper恢复状态,然后重新和所有ReplicaServer建立心跳。
宕机恢复过程中,尽量避免数据的跨节点复制。
zookeeper抢主
<!-- zookeeper抢主 -->
link:zookeeper抢主
单机存储
一个ReplicaServer包括多个Replica,Replica使用RocksDB作为存储引擎:
关闭掉了rocksdb的WAL。
PacificA对每条写请求都编了SequenceID,RocksDB对写请求也有内部的SequenceID。Pegasus对二者做了融合,来支持自定义的checkpoint的生成。
Pegasus给RocksDB添加了一些compaction filter以支持Pegasus的语义:例如某个value的TTL。
和很多一致性协议的实现一样,Pegasus中PacificA的实现也是和存储引擎解耦的。
RocksDB
<!-- RocksDB -->
link:RocksDB
数据安全
Table软删除
Table删除后,数据会保留一段时间,防止误删除
元数据恢复
Zookeeper损坏时,从各ReplicaServer收集并重建元数据
远程冷备份
数据定期备份到异地,譬如HDFS或者金山云 • 在需要的时候可快速恢复
跨机房同步
在多个机房部署集群
采用异步复制的方式同步数据
冷备份
Pegasus的冷备份功能用来将Pegasus中的数据定期生成快照文件,并备份到其他存储介质上,从而为数据容灾多提供一层保障。但由于备份的是某个时间点的数据快照文件,所以冷备份并不保证可以保留所有新的数据,也就是说,恢复的时候可能会丢失近一段时间的数据。
具体来看,冷备份过程要涉及到如下一些参数:
存储介质(backup_provider):
指其他的文件存储系统或服务,如本地文件系统或者HDFS。
数据冷备份的周期(backup_interval):
周期的长短决定了备份数据的覆盖范围。如果周期是1个月,那么恢复数据时,就可能只恢复一个月之前的数据。但如果周期设的太短,备份就会太频繁,从而使得备份开销很大。在小米内部,冷备份的周期通常是1天。
保留的冷备份个数(backup_history_count):
保留的备份个数越多,存储的空间开销就越大。在小米内部,一般保留近的3个冷备份。
进行冷备份的表的集合(backup_app_ids):
并不是所有的表都值得进行冷备份。在小米内部,对于经常重灌全量数据的表,我们是不进行冷备份的。
在Pegasus中,以上这几个参数的组合称为一个冷备份策略(backup_policy)。数据的冷备份就行按照policy为单位进行的。
跨机房同步
link:跨机房同步文档
小米内部有些业务对服务可用性有较高要求,但又不堪每年数次机房故障的烦恼,于是向 pegasus 团队寻求帮助,希望在机房故障时,服务能够切换流量至备用机房而数据不致丢失。因为成本所限,在小米内部以双机房为主。
通常解决该问题有几种思路:
由 client 将数据同步写至两机房。这种方法较为低效,容易受跨机房专线带宽影响,并且延时高,同机房 1ms 内的写延时在跨机房下通常会放大到几十毫秒,优点是一致性强,但需要 client 实现。服务端的复杂度小,客户端的复杂度大。
使用 raft/paxos 协议进行 quorum write 实现机房间同步。这种做法需要至少 3 副本分别在 3 机房部署,延时较高但提供强一致性,因为要考虑跨集群的元信息管理,这是实现难度大的一种方案。
在两机房下分别部署两个 pegasus 集群,集群间进行异步复制。机房 A 的数据可能会在 1 分钟后复制到机房 B,但 client 对此无感知,只感知机房 A。在机房 A 故障时,用户可以选择写机房 B。这种方案适合 终一致性/弱一致性 要求的场景。后面会讲解我们如何实现 “终一致性”。
基于实际业务需求考虑,我们选择方案3。
即使同样是做方案 3 的集群间异步同步,业内的做法也有不同:
各集群单副本:这种方案考虑到多集群已存在冗余的情况下,可以减少单集群内的副本数,同时既然一致性已没有保证,大可以索性脱离一致性协议,完全依赖于稳定的集群间网络,保证即使单机房宕机,损失的数据量也是仅仅几十毫秒内的请求量级。考虑机房数为 5 的时候,如果每个机房都是 3 副本,那么全量数据就是 3*5=15 副本,这时候简化为各集群单副本的方案就是几乎自然的选择。
同步工具作为外部依赖使用:跨机房同步自然是尽可能不影响服务是好,所以同步工具可以作为外部依赖部署,单纯访问节点磁盘的日志(WAL)并转发日志。这个方案对日志 GC 有前提条件,即日志不可以在同步完成前被删除,否则就丢数据了,但存储服务日志的 GC 是外部工具难以控制的。所以可以把日志强行保留一周以上,但缺点是磁盘空间的成本较大。同步工具作为外部依赖的优点在于稳定性强,不影响服务,缺点在于对服务的控制能力差,很难处理一些琐碎的一致性问题(后面会讲到),难以实现终一致性。
同步工具嵌入到服务内部:这种做法在工具稳定前会有一段阵痛期,即工具的稳定性影响服务的稳定性。但实现的灵活性肯定是强的。
初 Pegasus 的热备份方案借鉴于 HBase Replication,基本只考虑了第三种方案。而事实证明这种方案更容易保证 Pegasus 存储数据不丢的属性。
每个 replica (这里特指每个分片的 primary,注意 secondary 不负责热备份复制)独自复制自己的 private log 到远端,replica 之间互不影响。复制直接通过 pegasus client 来完成。每一条写入 A 的记录(如 set / multiset)都会通过 pegasus client 复制到 B。为了将热备份的写与常规写区别开,我们这里定义 duplicate_rpc 表示热备写。
A->B 的热备写,B 也同样会经由三副本的 PacificA 协议提交,并且写入 private log 中。这里有一个问题是,在 A,B 互相同步的场景,一份写操作将形成循环:A->B->A,同样的写会无数次地被重放。为了避免循环写,我们引入 cluster id 的概念,每条 duplicate_rpc 都会标记发送者的 cluster id。
[duplication-group]
A=1
B=2
void set(String tableName, byte[] hashKey, byte[] sortKey, byte[] value, int ttlSeconds)
直接使用pegasus优化,直接对pegasus读、写。可以替代redis缓存的架构。
读写逻辑复杂
要特意维护数据一致性----
服务可用性不高
机器成本高
问题:
读取:先读取缓存,如果缓存中不存在,那么再读取数据库中的数据。
写入:双写,既写入缓存、也要写入数据库。
原先:Redis 作为缓存 + HBase/mysql/MongoDB 作为数据库。
业务应用
HashKey | SortKey | Value | |
---|---|---|---|
map | MapId | key | value |
set | SetId | key | null |
list | ListId | index | value |
Pegasus本身不支持容器类型,但是其HashKey + SortKey的数据模型可以模拟容器。
容器支持
对HashKey或者SortKey进行字符串匹配, 只有符合条件的结果才会返回。对HashKey或者SortKey进行字符串匹配,只有符合条件的
条件过滤
同一个HashKey的数据写入同一个Replica,同一个Replica的操作,在同一个线程中串行执行。这样就避免了同步的问题。
对同一个HashKey的写操作,保证总是原子的,包括set、multiSet、del、multiDel、incr、 checkAndSet。
单行事务
支持对数据指定过期时间, 数据过期后就无法读取到。
TTL过期策略
线程安全
所有的接口都是线程安全的,不用担心多线程的问题。
并发性能
客户端底层使用异步的方式实现,可以支持大的并发,不用担心性能的问题。
Client单例
通过 getSingletonClient() 获得的Client是单例, 可以重复使用。
翻页功能
通过客户端提供的接口,能够轻松实现数据翻页功能 。
Java客户端
集群使用falcon进行监控。
集群监控
使用
热备份同时也需要容忍在 replica 主备切换下复制的进度不会丢失,例如当前 replica1 复制到日志 decree=5001,此时发生主备切换,我们不想看到 replica1 从 0 开始,所以为了能够支持 断点续传,我们引入 confirmed_decree。replica 定期向 meta 汇报当前进度(如 confirmed_decree = 5001),一旦meta将该进度持久化至 zookeeper,当replica故障恢复时即可安全地从 5001重新开始热备份。
所以当 B 重放某条 duplicate_rpc 时,发现其 cluster_id = 1,识别到这是一条发自 A 的热备写,则不会将它再发往 A。