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

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

1 重大改进

1.1 RTC 模型

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

What

上图左侧展示的是 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 新特性

  • 给 RTC 模型增加处理开关,可以根据开关控制是否开启 RTC 模型 https://github.com/OpenAtomFoundation/pika/pull/2841
  • 用 RTC 模型处理 Pika 访问缓存部分,提升 Pika 服务的读性能 https://github.com/OpenAtomFoundation/pika/pull/2837
  • Incr、append 命令在传输 binlog 时,使用 pksetexat 命令,防止因为不正确的操作导致数据无法过期,出现脏数据 https://github.com/OpenAtomFoundation/pika/pull/2833
  • 将管理命令移出主线程,防止因为管理命令调用频繁或者耗时过高,阻塞主线程 https://github.com/OpenAtomFoundation/pika/pull/2727
  • Pika 线程整理,避免启动无用线程造成资源浪费 https://github.com/OpenAtomFoundation/pika/pull/2697
  • 添加 pika benchark 工具,旨在提升压测效率,并输出可视化的统计图表 https://github.com/OpenAtomFoundation/pika/pull/2663

2.2 bug 修复

  • 修改 Pika 自动化测试客户端链接 server 端的超时时间,避免因为长时间断开导致测试失败 https://github.com/OpenAtomFoundation/pika/pull/2683
  • 修复 kill client 命令杀连接流程不正确的 https://github.com/OpenAtomFoundation/pika/pull/2682
  • 修复 blpop/brpop 更新数据库的时候未更新缓存可能回导致数据库、缓存不一致的现象 https://github.com/OpenAtomFoundation/pika/pull/2858
  • 修复 Pika 不支持 Redis-Sentinel 的问题 https://github.com/OpenAtomFoundation/pika/pull/2854
  • 修改 flushall 的逻辑避免统一处理时,多清理一次数据 https://github.com/OpenAtomFoundation/pika/pull/2846
  • PkPatternMatchDel 命令在删除 DB 的同时删除 cache,避免出现数据库、缓存不一致的情况 https://github.com/OpenAtomFoundation/pika/pull/2839
  • 优化读锁的范围,避免因为重复消费 binlog 导致主、从数据不一致的问题 https://github.com/OpenAtomFoundation/pika/pull/2818
  • 修改 client watch 的 key,被任何人修改(包括自己的改动),都会失效的问题 https://github.com/OpenAtomFoundation/pika/pull/2815
  • 修改 slave_priority 的默认值,防止因为运维同学没有设置该值,导致主从切换失败 https://github.com/OpenAtomFoundation/pika/pull/2813
  • Multi 命令更新数据库的同时更新缓存,避免读取不到数据 https://github.com/OpenAtomFoundation/pika/pull/2810
  • 修复执行 slaveof no one 时出现异常日志输出的问题 https://github.com/OpenAtomFoundation/pika/pull/2800
  • 修复 Pika block-cache 信息计算不准确,内存使用计算不标准的问题 https://github.com/OpenAtomFoundation/pika/pull/2797
  • 修改主从复制过程中 flushdb binlog 的处理逻辑,确保按照顺序执行,避免出现主从不一致的情况 https://github.com/OpenAtomFoundation/pika/pull/2794
  • 添加标志位、时间戳和返回值机制,确保 Pika 在执行 flushdb 且处理异步删除旧目录时冲突时正确处理 https://github.com/OpenAtomFoundation/pika/pull/2790
  • 修改主从复制过程中 flushdb binlog 的处理逻辑,确保按照顺序执行,避免出现主从不一致的情况 https://github.com/OpenAtomFoundation/pika/pull/2793
  • 修复 PKPatternMatchDel 命令未删除迭代器可能会导致 RocksDB 删除数据不彻底的问题 https://github.com/OpenAtomFoundation/pika/pull/2786
  • timerTaskThread_ 重命令为 timer_task_thread_ https://github.com/OpenAtomFoundation/pika/pull/2776
  • 修复 min-blob-size 参数解析失败,导致 kv 分离使用出错的问题 https://github.com/OpenAtomFoundation/pika/pull/2767
  • 修复 sentinel 主从切换时,由于主节点和从节点数据量不一致导致主从切换状态扭转错误问题 https://github.com/OpenAtomFoundation/pika/pull/2766
  • 修复 Zverank 命令计算错误,导致返回值错误的问题 https://github.com/OpenAtomFoundation/pika/pull/2763
  • 修复 Pksetat 命令更新 DB 的同时未更新缓存,可能会存在数据库、缓存不一致的问题 https://github.com/OpenAtomFoundation/pika/pull/2759
  • 修复 Pika-port 传输数据过程中报错的问题 https://github.com/OpenAtomFoundation/pika/pull/2758
  • 修复 RsynClient 异常退出后,未做失败处理导致全量复制数据不完整的问题 https://github.com/OpenAtomFoundation/pika/pull/2756
  • 修复 Pika 不能批量扩容的问题 https://github.com/OpenAtomFoundation/pika/pull/2746
  • 修复参数未初始化导致 slotsscan 等命令不能和 bgsave 命令相互制衡的问题 https://github.com/OpenAtomFoundation/pika/pull/2745
  • 修复 SlotMigrate 返回值错误,可能会导致数据迁移中断的问题 https://github.com/OpenAtomFoundation/pika/pull/2741
  • 修复 RedisCache 层因为没有使用 storage 层解析的 ttl 可能会导致数据库、缓存不一致的问题 https://github.com/OpenAtomFoundation/pika/pull/2729
  • 修复 Stream 类型数据不能被 RocksDB compact 会导致过期数据一直存在的问题 https://github.com/OpenAtomFoundation/pika/pull/2724
  • 修复 ACL 认证可能会偶发性失败的问题 https://github.com/OpenAtomFoundation/pika/pull/2714
  • 修复 Pika cmdID 赋值在 Cmd 初始函数中,可能会导致并发构造的时候出现内存泄漏的问题 https://github.com/OpenAtomFoundation/pika/pull/2692
  • 修复 Spop 在写 binlog 时可能会出现竞态问题 https://github.com/OpenAtomFoundation/pika/pull/2674
  • 修复 server_stat 中的数据竞态问题 https://github.com/OpenAtomFoundation/pika/pull/2671
  • 修复多 DB 下全量同步超时后不重试的问题 https://github.com/OpenAtomFoundation/pika/pull/2667
  • 修复多 DB 主从超时场景下,可能会出现窗口崩溃的问题 https://github.com/OpenAtomFoundation/pika/pull/2666
  • 修复主从同步限速逻辑中重复解锁的问题 https://github.com/OpenAtomFoundation/pika/pull/2657

2.3 提升改进项

  • 优化主从复制,确保 Master 端的 SlaveNode 在提交 bgsave 任务前进入 DBSync 状态,防止 bgsave 执行时的 binlog 在极端情况下被清除 https://github.com/OpenAtomFoundation/pika/pull/2798
  • 优化 Apply binlog 时锁机制,减少不必要的锁竞争 https://github.com/OpenAtomFoundation/pika/pull/2773
  • 添加 Geo 数据类型的 TCL 测试,并修复测试过程中遇到的 bug https://github.com/OpenAtomFoundation/pika/pull/2753
  • 更新 Pika Docker Readme, 可以按照 Readme 在 Docker 中部署 Pika 服务 https://github.com/OpenAtomFoundation/pika/pull/2743
  • 支持对更多的 RocksDB 参数进行动态调整,用户根据不同的业务使用场景调整参数提升 Pika 的读写性能 https://github.com/OpenAtomFoundation/pika/pull/2728
  • Pkpatternmatchdel 命令支持删除 Redis Stream 数据类型 https://github.com/OpenAtomFoundation/pika/pull/2723
  • 重构主从复制模式 slave 节点的主从同步线程模型,尽可能减少 binlog 消费阻塞问题 https://github.com/OpenAtomFoundation/pika/pull/2638
  • 增加主从复制状态指标 repl_connect_status,方便运维人员清晰明确的判断当前的主从复制状态 https://github.com/OpenAtomFoundation/pika/pull/2656
  • 新增 RocksDB Compaction 策略动态调整参数,用户可以根据业务调整 Compaction 策略,降低 Compaction 操作对服务性能的损耗 https://github.com/OpenAtomFoundation/pika/pull/2538
  • 对事务新增 TCL 测试并修复测试中遇到的 bug https://github.com/OpenAtomFoundation/pika/pull/2844

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