尊敬的社区朋友及 PikiwiDB(Pika) 用户们:

非常高兴地宣布 — PikiwiDB(Pika)【下文简称 Pika】长期维护版本 v3.5的最新版本 — v3.5.5 今天正式发布。在这个版本中,除了修复所有已知的主从复制 bug 外,通过引入 RTC 特性其性能又有了大幅度提升,且主从 failover 机制兼容了 Redis-Sentinel。

1 重大改进

1.1 RTC 模型

在 v3.5.5 版本中我们引入了 RTC 模型,旨在提高缓存的访问效率,具体修改流程如下所示:

上图左侧展示的是 Pika 服务请求的 IO 处理路径:请求依次经过网络线程、请求队列、工作线程,最终到达内存或硬盘引擎进行处理。这种设计的不足之处在于,请求在各个线程间传递会导致频繁的 CPU 上下文切换以及 CPU 高速缓存的 Cache Miss 问题,从而增加 CPU 资源的消耗。在高并发场景下,这种额外的 CPU 消耗尤为显著。

年初(2024年3 月)美团发布了 《美团大规模 KV 存储挑战与架构实践》一文,给出了一个名为 RTC(Run-to-Completion) 的技术方案,网络线程被赋予了直接处理读请求的能力,特别是那些能直接从内存引擎中命中的读请求。

当时 Pika 已经实现了混合存储:在内存 RedisCache 中缓存热数据,在磁盘中存储全量数据。在这个基础之上,恰好看到这个 RTC 解决方案,我们觉得这个方案甚好,可以优化 Pika 的读写流程,提高吞吐,降低延时。引入 RTC 方案后,新读写流程如上图右侧:

  1. 当网络线程接收到请求后,Pika 首先判断该请求是否为读操作请求。
  2. 如果是读请求,并且能在 RedisCache 中找到所需的数据(RedisCache 命中读请求),则网络线程直接返回结果给客户端。
  3. 对于写请求或 RedisCache 未命中的读请求,则依旧沿用原有的处理路径,访问 DB 硬盘引擎进行读写。

RTC 在网络线程内部就能实现 Redis 读命中的闭环处理,避免了由于请求在不同线程间的流转而带来的 CPU 资源开销。具体而言,Pika 的多个 Worker 线程争抢同一个 TaskQueue,且 Worker 在等待任务队列时,没有任何轻量级等待策略,直接使用了“很重” 的cv.wait(std::condition\_variable::wait),如果不使用 RedisCache,这或许不构成瓶颈,因为读写请求都打在 RocksDB 上,此时 Pika 工作线程需要等待 RocksDB 的读写流程。但如果打开了 Pika RedisCache 开关且缓存命中率超过 60% 时,热点数据集中度较高,在读操作远多于写操作的高并发场景下,这个线程池中转的过程就会构成性能瓶颈。

通过压测发现,打开 RedisCache 和 RTC 模型开关,且在 Pika 内嵌 Redis Cache 缓存命中率达到 60% 以上的高并发情况下,Get QPS 能提高到原本的 1.5 倍(从 18.4w 到 27.2w),P99 延时降低 47.5% (2.4ms 以下)。尽管此模型仅针对读缓存命中的读多写少请求,但鉴于实际应用场景中读操作远多于写操作,并且热点数据集中度较高,因此在多数业务集群部署后均实现了显著的性能提升。

关键 ISSUE/PR:

Improve the RTC process of Read/Write model <https://github.com/OpenAtomFoundation/pika/issues/2542> Pika 使用 RTC 模型访问 Rediscache <https://github.com/OpenAtomFoundation/pika/pull/2837> Pika RTC 模型开关 <https://github.com/OpenAtomFoundation/pika/pull/2841>

1.2 通过 Redis-Sentinel 对 Pika 主从实例进行故障自愈

自一年前 (2023年7月)发布 v3.5.0 版本以来,Pika 无法兼容 Redis-Sentinel 进行 auto failover,主要有以下几个原因:

  1. 事务冲突

Redis-Sentinel 选举以后,进行主从切换操作时会给 slave 发送 "slaveof no one" 等一批 事务形式的命令:

multisalveof no oneconfig rewriteclient kill type pubsubclient kill type normalexec

当时 Pika 不支持 "client kill type pubsub" 与 "client kill type normal" 命令,整个事务会 abort 不执行,导致实际的 slaveof 命令没有执行,所以无法完成新主切换。针对这个问题,在PR 2854 中支持了这两个命令:"client kill type pubsub" 杀掉所有 pubsub client 连接,"client kill type normal" 命令杀掉除 pubsub 外的其他 client 连接。

  1. 指标不兼容

Redis-Sentinel 通过 "info" 命令的指标 "slave\_repl\_offset" 作为选主依据,这个指标反映了某个 slave 实例从 master 接收的主从同步字节数的总量(多 DB 场景下这个值就是多 DB 同步字节数的总和),而 Pika 的主从体系下每个 DB 各自独立,每个 DB 都有一个 binlog 偏移量,由 (filenum, offset) 这样二维坐标形式的两个数字构成,没有提供 "slave\_repl\_offset" 指标供 Redis-Sentinel 做判断。Pika v3.3.6 版本没有提供这个指标,但通过 fake 数据方式兼容了 Redis-Sentinel, 但这样的选主其实是不可靠的。

在 PR 2854 中,Pika 通过算式 "slave\_repl\_offset = filenum \ filesize + offset*" 给出了 "info" 命令的 "slave\_repl\_offset" 指标。

  1. slave-priority 默认参数的变更

Pika Slave "info" 命令的 "slave-priority" 指标默认值为 0,这个值导致 Pika Slave 不会被 Redis-Sentinel 列入候选 master。

在 PR 2854 中,Pika 将这个默认值改为 100,Redis-Sentinel 会将 Pika Slave 实例列入候选 master。

  1. 进程 coredump

在修复以上问题后,在测试过程中 发现 Redis-Sentinel 连接 Pika 实例并发出 "shutdown" 命令 Pika 实例会 coredump。追查后发现:Pika 退出时的资源释放流程中,网络线程关闭的时机不对(关的太晚了),有些重要资源对象已经析构了,但网络线程还能收发包,这就导致 Pika 实例收到 "shutdown" 命令时使用已经析构的资源对象进行命令处理,最终导致 Pika 实例 coredump。

在 PR 2854 中,修改了网络线程 Stop 函数的位置,提前终止线程 pika\_dispatcher\thread\,停止 dispatcher 进行网络任务分配。

  1. 一个意外收获

在测试过程中,还发现了一个资源退出时的主从同步相关的问题:Pika Slave 消费 Binlog 时,先将 Binlog 落盘,然后异步提交 WriteDB 任务进行数据的物化(重放至 RocksDB),Pika 退出时不会等待这些异步 WriteDB 任务,这就可能导致 Slave 丢数据。

在 PR 2854 中,Pika 退出时,会等到 WriteDBWorker TaskQueue 为空才继续往下释放资源,退出进程。

关键PR:

Pika 兼容 Redis-Sentinel 进行主从切换 <https://github.com/OpenAtomFoundation/pika/pull/2854>

1.3 主从复制

Pika v3.3.6 有很多主从复制的缺陷,如丢数据、数据污染、死锁等问题。v3.5.5 版本对 Pika 全量复制及增量复制进行了大量优化和 bug 修复,取得了非常好的效果。

例如,PR 2638 在 "info" 命令中输出了 "repl\_connect\_status" 指标,以方便用户更加清晰看到当前 Pika 实例的主从复制状态。

关键 PR:

优化主从复制,确保 Master 端的 SlaveNode 在提交 bgsave 任务前进入 DBSync 状态,防止bgsave执行时的 binlog 在极端情况下被清除 <https://github.com/OpenAtomFoundation/pika/pull/2798>

修改主从复制过程中 flushdb binlog 的处理逻辑,确保按照顺序执行,避免出现主从不一致的情况 <https://github.com/OpenAtomFoundation/pika/pull/2790>

优化 Apply binlog 时锁机制,减少不必要的锁竞争 <https://github.com/OpenAtomFoundation/pika/pull/2773>

修复批量扩容时,多个 slave 同时连接 master,短时间多次 bgsave 导致部分从节点数据不完整的问题 <https://github.com/OpenAtomFoundation/pika/pull/2746>

修复 Spop 在写 binlog 时可能会出现竞态问题 <https://github.com/OpenAtomFoundation/pika/pull/2647>

修复多 DB 下全量同步超时后不重试的问题 <https://github.com/OpenAtomFoundation/pika/pull/2667>

修复多 DB 主从超时场景下,可能会出现窗口崩溃的问题 <https://github.com/OpenAtomFoundation/pika/pull/2666>

修复主从同步限速逻辑中重复解锁的问题 <https://github.com/OpenAtomFoundation/pika/pull/2657>

增加主从复制状态指标 repl\_connect\_status,方便运维人员清晰明确的判断当前的主从复制状态 <https://github.com/OpenAtomFoundation/pika/pull/2656>

重构主从复制模式 slave 节点的主从同步线程模型,尽可能减少 binlog 消费阻塞问题 <https://github.com/OpenAtomFoundation/pika/pull/2638>

1.4 RocksDB Compaction

Pika 的底层磁盘存储引擎 RocksDB 在进行 compaction 时会显著影响 Pika 的读写性能。因此,控制好 compaction 是优化 Pika 读写性能的关键。

v3.5.5 使用了 v8.7.3 版本的 RocksDB,开放了更多 RocksDB 参数,以方便用户优化 RocksDB 性能:

  1. min-write-buffer-number-to-merge: 默认值为 1,如果将此值设置得更大,意味着需要更多的写缓冲区被填满后才进行 flush。这样可以减少 flush 的频率,增加数据在内存中的累积量,从而可能提高写入吞吐量。
  2. level0-stop-writes-trigger: 默认值为 36,定义了 L0 层中 sst 文件的最大数量,一旦达到这个数量,RocksDB 将会采取 暂停写入、强制 compaction 等措施来防止写入操作继续累积,以避免 L0 层变得过于庞大,进而可能导致写入放大、查询性能下降等问题。
  3. level0-slowdown-writes-trigger:默认值为 20,用于控制当 Level 0 的 SST 文件数量达到这个阈值时,触发写减速(write slowdown),防止 Level 0 的文件数量过多,导致后续 compaction 操作的压力过大。
  4. level0-file-num-compaction-trigger:默认值为 4,当 Level 0 的 SST 文件数量达到这个参数设定的阈值时,RocksDB 会开始执行 compaction 操作,将 Level 0 的文件合并到 Level 1,以减少 Level 0 的文件数量,降低读取延迟,并优化存储空间的利用率。
  5. max-subcompactions:默认值为 1,用于控制 RocksDB 中并发执行的 sub-compaction 任务数量,其值为 1 表示关闭 sub-compaction。如果系统资源充足,建议提升该参数以优化 compaction 效率。
  6. max-bytes-for-level-base:指定了 L1 SST 文件总的大小。这个大小是 RocksDB 进行数据分层管理和 compaction 决策的重要依据:如果 L1 层的大小设置得太小,可能会导致 L0 层的 compaction 过于频繁,进而影响写性能。反之,如果设置得太大,可能会占用较多的磁盘空间,并且影响读取性能,因为读取操作可能需要跨越更多的层级。Pika 没有在 pika.conf 中开放此参数给用户配置,而是使用其他参数(level0-file-num-compaction-trigger 和 write-buffer-size)计算后的结果。
storage_options_.options.max_bytes_for_level_base = g_pika_conf->level0_file_num_compaction_trigger() * g_pika_conf->write_buffer_size()

关键 PR:

新增 RocksDB Compaction 策略动态调整参数,用户可以根据业务调整 Compaction 策略,降低 Compaction 操作对服务性能的损耗 <https://github.com/OpenAtomFoundation/pika/pull/2538>

1.5 测试集

Pika 测试集由 gTest 单测集、Redis TCL 测试集和 Go 测试集组成。v3.5.5 丰富了诸多特性的 go test 功能,并进一步完善了基本数据类型的 TCL 测试。

关键 PR:

Pika Geo 数据类型增加 TCL 测试,并修复测试过程中遇到的缺陷 <https://github.com/OpenAtomFoundation/pika/pull/2753>

2 改进列表

下面详细列出了本次发版的主要功能升级和改进。

2.1 新特性

2.2 bug 修复

2.3 提升改进项

2.4 发版 tag

<https://github.com/OpenAtomFoundation/pika/releases/tag/v3.5.5>

3 社区

感谢所有为 v3.5.5 做出贡献的社区成员,包括 issue/PR 提交者、代码 reviewer 【排名不分先后,依据字母顺序】:

  • baerwang
  • baixin01
  • bigdaronlee163
  • cheniujh
  • chejinge
  • haiyang426
  • lqxhub
  • luky116
  • Mixficsol
  • QlQlqiqi
  • saz97
  • VanessaXWGUO
  • wangshao1
  • XiaoLiang2333

PikiwiDB (Pika) 开源社区热烈欢迎您的参与和支持。如果您有任何问题、意见或建议,请添加 PikiwiDB 小助手【微信号: PikiwiDB】为好友,它会拉您加入官方微信群。