Pika 新存储结构 #2052
Replies: 7 comments 9 replies
-
Floyd 一定要保证 zscore 中字段的顺序。 |
Beta Was this translation helpful? Give feedback.
-
张智清:数据类型通过增加前缀进行区分,如:'kv':表示 string 类型。'li':表示 list 类型,'se': 表示 set 类型等等,可能导致多种类型共用同一个 key 名称,这个在 Blackwidow 的问题在 Floyd 中依旧存在 |
Beta Was this translation helpful? Give feedback.
-
一哥:兼容 Redis 协议,只能在整个数据类型上设置过期时间,所以还是需要保留 meta value。RedRocks 把复合数据类型聚合成一个 value,可能导致热点 key。 |
Beta Was this translation helpful? Give feedback.
-
使用多 RocksDB 的理由: ref: https://mp.weixin.qq.com/s/83D_AQYnNHwWfVsaV9LksQ 从上图可得出的结论是:需要控制多 RocksDB 的数据规模 |
Beta Was this translation helpful? Give feedback.
-
/202311 pika存储结构整体架构新的存储架构中,pika实例存储引擎包括内存缓存redis和硬盘持久存储RocksDB。每个pika实例由一个redis和多个RocksDB实例构成。 pika当前是将不同的数据类型放在不同的RocksDB实例中,线上使用过程中发现,同一个业务服务使用的数据类型一般集中在一两个数据类型中,无法发挥多RocksDB实例的优势。因此,pika新版本中计划不再按照数据类型区分RocksDB实例,而是通过column-family区分。单个pika节点的RocksDB实例个数根据物理机硬件配置决定,每个RocksDB实例使用独立的compaction线程池和flush线程池,初次之外每个RocksDB实例使用一个后台线程,该后台线程用来发起manual compaction以及对RocksDB中存储的数据进行定期的统计和巡检。 每个节点在启动时获取到当前节点持有的分片(目前不支持,需要进行代码开发),将分片排序并等分为RocksDB实例个数,保证每个分片持有的RocksDB实例个数近似相同。 数据格式为了兼容redis协议,即为同一个数据类型的数据设置统一的过期时间值,复合数据类型中的meta信息还是需要保留,否则ttl/expire接口操作性能耗时增加。不同的数据类型混合使用RocksDB实例,通过column family中进行区分。为了方便后期remote Compaction,参考todis的数据存储格式,对userkey进行编码后存储,保证datakey和metakey参与排序的前缀相同。除此之外,数据存储格式与之前的blackwidow基本相同,只是key,value增加一些字段。 对于key来讲,前缀增加8字节的reserve保留字段,后缀增加16字节的保留字段。 对于value来讲,在value最后统一增加:16字节的保留字段,8字节的数据的写入时间cdate,8字节的数据过期时间。 string结构 key格式
| reserve1 | key | reserve2 |
| 8B | | 16B |
value格式
| value | reserve | cdate | timestamp |
| | 16B | 8B | 8B |
hash结构 meta数据格式 key格式
| reserve1 | key | reserve2 |
| 8B | | 16B |
value格式
| hash_size | version | reserve | cdate | timestamp |
| 4B | 8B | 16B | 8B | 8B |
data数据格式 key格式
| reserve1 | key | version | field | reserve2 |
| 8B | | 8B | | 16B |
value格式
| hash value | reserved | cdate |
| | 16B | 8B | List结构 meta数据格式 key格式
| reserve1 | key | reserve2 |
| 8B | | 16B |
value格式
| list_size | version | left index | right index | reserve | cdate | timestamp |
| 4B | 8B | 8B | 8B | 16B | 8B | 8B | data数据格式 key格式
| reserve1 | key | version | index | reserve2 |
| 8B | | 8B | 8B | 16B |
value格式
| value | reserve | cdate |
| | 16B | 8B | set结构 meta数据格式 key格式
| reserve1 | key | Reserved2 |
| 8B | | 16B |
value格式
| set_size | version | reserve | cdate | timestamp |
| 4B | 8B | 16B | 8B | 8B |
data数据格式 key格式
| reserve1 | key | Version | member | reserve2 |
| 8B | | 8B | | 16B |
value格式
| reserve | cdate |
| 16B | 8B | zset结构 meta数据格式 key格式
| reserve1 | key | reserve2 |
| 8B | | 16B |
value格式
| zset_size | version | reserved | cdate | timestamp |
| 4B | 8B | 16B | 8B | 8B |
member to score数据格式 key格式
| reserve1 | key | version | Field | reserve2 |
| 8B | | 8B | | 16B |
value格式
| score value | reserve | cdate |
| 8B | 16B | 8B | score to member数据格式 key格式
| reserve1 | key | version | score | member | reserve2 |
| 8B | | 8B | 8B | | 16B |
value格式
| reserve | cdate |
| 16B | 8B | RocksDB使用优化blobdb使用优化RocksDB支持了key-value分离的实现,即通过将大value存储到blob文件中,在sst文件中存储大value在blob文件的索引信息,从而减少写写放大,有效提升大value场景下的写入性能。pika依赖自定义的compactionFilter实现过期数据的处理,ttl存储在value中,因此在compaction过程中不可避免导致额外的blob文件IO。一种方法是修改sst文件中存储的blobindex,在blobindex的相同offset位置存储value的ttl值,这样compaction过程中对过期数据的清理的逻辑,就不需要查询blob文件,减少额外的磁盘IO。对于非string类型的数据,通过实现FilterByBlobKey的方式,省掉额外的BLOB文件IO开销。 dealslowkey参考新浪微博的经验,当pika上层代码发现一个慢查询key时,发起一次manual compaction,compaction的范围即对应的key前缀对应的数据范围。性能待验证。 compact老的sst文件参考新浪微博的经验,定期对最老的sst文件进行compaction可明显提升集群性能。看官方文档,貌似类似的功能RocksDB已经支持,链接如下:https://github.com/facebook/rocksdb/wiki/Leveled-Compaction#ttl。计划使用RocksDB官方的实现。 新技术探索主要是包括了RocksDB的异步IO,协程,remote compaction等新技术的测试和落地。 |
Beta Was this translation helpful? Give feedback.
-
Floyd 是 Pika 新的数据存储格式,同之前的 Blackwidow 一样,Floyd 基于 RocksDB 进行封装,支持了 String 结构(实际上就是存储 key, value), Hash 结构,List 结构,Set 结构和 ZSet 结构。在Floyd存储格式中,不再按照数据类型区分 RocksDB 实例,用户可以根据机器配置指定一个 Pika 实例中 RocksDB 实例个数,存储的数据根据 key 的 hash 值计算出存储的 RocksDB 实例,不同的数据类型通过 Column-Family 进行隔离。 下边将依次介绍 Floyd 数据存储格式和 RocksDB 使用优化以及兼容性问题。 存储格式Floyd 中数据的存储格式包括数据字段和保留字段两部分,其中数据字段的存储格式参考了 Todis 中的数据格式实现。具体来讲,即 key在存储时去掉之前的 size 前缀,将 key 中的 '\u0000' 字符编码为 "\u0000\u0001",并在编码后追加 "\u0000\u0000" 的分隔符。这样做可以保证复杂数据类型的 data key 前缀与 meta key 前缀相同,在对 data key 进行 Compaction 时,可以根据参与 Compaction 的 key 区间获取这些 key 的元信息,从而在 Compaction 过程中不需要再查元数据。在后期的 Remote Compaction 工作中,生成 Compaction Job 时可以将元信息一同打包,Compaction 在远程执行时不需要进行额外的网络 IO 查询元数据。保留字段的存储是在有效数据字段的前后分别设置了8字节和16字节的保留字段。不同的数据类型以及复杂数据类型的元信息和数据信息分别占用一个 Column-Family,如下图所示。 String 结构的存储String 类型数据存储在第一个 Column-Family,在 RocksDB 中的存储格式如下图所示: key 由 8 字节前缀保留字段,编码后的 user key 以及 16 字节后缀保留字段构成,value 由 User Value,8 字节保留字段,8 字节的数据写入时间以及 8 字节的数据过期时间构成。未设置过期时间的 value的expire time为0。 Hash 结构的存储Hash 类型数据结构由两部分构成,元数据(meta_key,meta_value)和普通数据(data_key,data_value)。每个 Hash 类型数据对应一条元数据,每个 Filed 对应一条普通数据。元数据存储在 Column-Family 1 中,普通数据存储在 Column-Family 2 中,具体格式如下图所示。 元数据中的 key 由前缀保留字段,编码后的 user key 以及后缀保留字段构成,value 中记录了 hash 中元素个数,最新版本号,保留字段,数据写入时间以及数据过期时间,version 字段用于实现秒删功能。 普通数据主要就是指的同一个 hash 表中一一对应的 field 和 value,普通数据的 key 字段依次拼接了保留字段,编码后的 user key,元数据中的最新 version,filed 字段以及后缀保留字段。value 则拼接了保留字段以及 field 写入时间。 当执行读写以及删除单个 field 操作时,首先获取元信息,之后解析出 version,再将主 key,version 和 field字段拼接得到最终的 data key,最后读写 data key。 当执行 HgetAll 时,首先获取元信息,接下来将主 key 与解析出的 version 拼接构成 data key 前缀,生成 RocksDB 迭代器遍历 Data Key。 当执行 Del 操作时,由于 Version 字段支持了秒删功能,因此生成一个新 Version 并将新的元信息写回到 RocksDB 中即可。无效的数据会随着 RocksDB 的 Compaction 逐渐被物理删除。 Set 结构的存储set 结构与 hash 类型的存储格式基本相同,也是由元数据和普通数据两部分构成。不同的是,set 类型由于没有 value字段,所以其 data value 中只需要记录保留字段和数据写入时间即可。具体格式如下所示: List 结构的存储与 Blackwidow 中存储方式类似,Floyd 中的 list 由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value)。 元数据中存储的主要是 list 链表的一些信息, 比如说当前 list 链表结点的的数量以及当前 list 链表的版本号和过期时间(用做秒删功能), 还有当前 list 链表的左右边界, 普通数据实际上就是指的 list 中每一个结点中的数据,作为具体最后 RocksDB 落盘的 KV 格式,具体格式如下所示: 元数据中记录了一个 list 的信息,包括 list 元素个数,左右边界 Index,最新 version以及数据写入时间和过期时间。普通数据的 key 拼接了 list key 和 Index,value 记录用户写入数据以及写入时间。 向 list 插入数据时,首先获取元数据解析得到 list 的左右边界,根据 list push 方向获取新插入元素的 Index,之后更新元数据中的边界,最后构造一个 WriteBatch 将元数据和普通数据写入 RocksDB。 当遍历 list 类型数据时,首先获取元数据,解析出左右边界,与 list key 拼接后得到 data key 前缀,之后构造迭代器遍历 RocksDB 中数据。 ZSets 结构的存储Floyd 中的 zset 由两部分构成,元数据(meta_key, meta_value), 普通数据(data_key, data_value)。元数据中存储的主要是 zset 集合的一些信息, 比如说当前 zset 集合中 member 的数量以及当前 zset 集合的版本号和过期时间(用做秒删功能), 而普通数据就是指的 zset 中每个 member 以及对应的 score。由于 zset 这种数据结构比较特殊,需要按照 memer 进行排序,也需要按照 score 进行排序,所以我们对于每一个 zset 我们会按照不同的格式存储两份普通数据, 在这里我们称为 member to score 和 score to member,作为具体最后 RocksDB 落盘的 KV 格式,具体格式如下: Meta KV 记录的是一个 zset 的元信息,包括集合元素个数,最新版本号,数据写入时间以及数据过期时间。对 zset 类型数据的读写删除操作都需要先获取元数据。 Member To Score KV 记录的是 zset 元素与分值的映射关系,key 部分拼接了保留字段,编码后的 user key,最新版本号,member,根据从 Meta KV 中获取的信息,即可拼接出 key 前缀。value 部分记录了数据的写入时间和分值。 Score To Member KV 的作用是实现 zset 数据类型按照分值排序的功能,如 zrank 接口。同样也是利用了 RocksDB 存储数据有序性的特点,拼接 key 时先拼接 score,然后拼接 member,保证了优先按照分值排序,分值相同按照 member 字典序排序的功能。 RocksDB 使用优化Blob 使用优化RocksDB 支持了 key-value 分离的实现,即通过将大 value 存储到单独的 blob 文件中,只在 sst 文件中存储大 value 在 blob 文件的索引信息,从而减少写放大,有效提升大 value 场景下的写入性能。Pika 依赖自定义的 CompactionFilter 实现过期数据的处理,TTL 存储在 value 中,因此在 compaction 过程中不可避免导致额外的 blob 文件 IO。 Floyd 中规避这些额外 IO 的方式是修改 sst 文件中存储的 blobindex 的结构,在 BlobIndex 存储 value 的 ttl 值,数据写入 Blob 时,将 TTL 在 BlobIndex 中保留一份。同时,RocksDB 的 CompactionFilter 中本身提供了 FilterBlobByKey 这样的函数针对 blob 类型的 value 使用过滤,因此修改 FilterBlobByKey 函数接口,传入从 BlobIndex 中解析出的 TTL 字段,Pika 在实现自己的 CompactionFilter 时,重写 FilterBlobByKey 函数即可,根据传入的 TTL 即可判断 blob value是否需要删除,无需额外读取 blob文件。对于非 String 类型,在 FilterBlobByKey 函数中读取元信息,并根据元信息的 version和 TTL 判断数据是否需要保留,同样不会导致blob文件IO。 过期数据清理优化RocksDB 中的数据在 Compaction 到高 level之后,再次触发 Compaction 概率更低,因此对于设置了过期时间或者是业务重复写的场景,无效数据无法及时被清理掉,占用磁盘空间,同时对遍历操作有性能影响。RocksDB 官方支持了对指定时间没有进行 Compaction 的 sst 文件主动触发一次 Compaction,Floyd 中支持了相关配置项的设置使用。 兼容性Floyd 兼容 Pika 之前的所有 Redis API 接口,但由于数据存储格式发生了变更,因此不支持原地升级。同时,由于 DB struct 发生了变化,因此主从的全量复制也不支持跨版本进行数据同步。 |
Beta Was this translation helpful? Give feedback.
-
以上讨论是 Floyd 的第一阶段,已经在 PR https://github.com/OpenAtomFoundation/pika/pull/2413/files 中实现。 为了进一步解决 Blackwidow 中存在的 "每个类型使用独立的 RocksDB 实例” 导致的 “允许多种类型存在同名称 key" 的问题,我们决心进行第二阶段工作 ”合并 Floyd 的 list meta/set meta/zset meta/hash meta 到 string 中“,以进一步兼容 Redis 达到 ”Redis 同一个 DB 内不能有两个相同名称但类型不同的 key“。 目前 PR #2609 已经提交。 第二阶段的工作可能导致的问题就是: |
Beta Was this translation helpful? Give feedback.
-
说明
Pika 有给存储结构取名代号的传统,譬如第一代取名 Nemo,第二代取名 Blackwidow, 当前版本我们决定取名 Floyd,没有什么特殊意义,一个代号而已。
目前 Pika 的数据层级架构是:DB -> Blackwidow。
当前,一个 DB 下使用一个 Blackwidow。Blackwidow 支持的数据类型 String/List/Set/Zset/Hashtable,每种数据类型使用一个 RocksDB 实例,所以一个 DB 下共有五个 Blackwidow。
考虑点
2. 整体架构
新的存储架构中,Pika 实例存储引擎包括内存缓存和硬盘持久存储 RocksDB。每个 Pika 实例由一个内存缓存和多个 RocksDB 实例构成,每个数据分片对应一个 RocksDB 实例。同一个Pika实例的多个 RocksDB 实例共享同一个 compaction 和 flush 线程池。
每个数据分片对应一个 RocksDB 实例的好处是:
3. 数据格式
为了方便后续的数据格式兼容问题,4.0 的数据存储时考虑使用 protobuf 序列化之后再存如 RocksDB 。但序列化之后存入 RocksDB ,需要考虑序列化/反序列化导致的 CPU 性能开销。待测试验证。
目前 blackwidow 中不同的数据类型存储在不同的 RocksDB 实例中,业务的实际使用场景中,可能会更集中在某一个数据类型中,因此相当于是单个 RocksDB 实例在承担Pika节点上所有的流量。因此考虑不再按照数据类型区分 RocksDB 实例。为了防止数据冲突,目前想到有两种解决方法:
为了兼容 Redis 协议,即为同一个数据类型的数据设置统一的过期时间值,复合数据类型中的meta信息还是需要保留,否则 ttl/expire 接口操作性能耗时增加。增加meta信息导导致的数据写入过程中产生的查询开销,计划通过增加内存 cache 的方式进行缓解,即读 meta 时也是优先读内存缓存 cache,读不到再查硬盘。
4. 性能优化
4.1 dealslowkey
参考新浪微博的经验,当Pika上层代码发现一个慢查询key时,发起一次manual compaction,compaction的范围即对应的key前缀对应的数据范围。性能待验证。
4.2 新技术探索
主要是包括了 RocksDB 的异步IO,协程,remote compaction等新技术的测试和落地
Beta Was this translation helpful? Give feedback.
All reactions