随着业务数据的增加(比如电商业务中,随着用户规模和商品数量的增加),就需要 Redis 能保存更多的数据。你可能会想到使用 Redis 切片集群,把数据分散保存到不同的实例上。但是这样做的话,如果要保存的数据总量很大,但是每个实例保存的数据量较小的话,就会导致集群的实例规模增加,这会让集群的运维管理变的复杂,增加开销。
可能你又想到,可以增加 Redis 单实例的内存容量,形成大内存实例,每个实例就可以保存更多的数据,这样一来,在保存相同的数据总量时,所需要的大内存实例的个数就会减少,就可以节省开支。但是,基于大内存的大容量实例在实例恢复、主从同步过程中会引起一系列潜在问题,例如回复时间增长、主从切换开销大、缓冲区易溢出。
那该怎么办呢? 可以使用固态硬盘。他的成本很低(每 GB 的成本约是内存的十分之一),而且容量大,读写速度快,我们可以基于 SSD 来实现大容量的 “Redis” 实例。360 公司的 Pika,正好实现了这一需求。
Pika 的设计目标:
所以,如果你一直在使用 Redis,并且想使用 SSD 来扩展单实例容量,Pika 是一个不错的选择。
Pika 官网安装教程。
Redis 使用内存保存数据,内容容量增加后,就会带来两方面的潜在问题,分别是内存快照 RB 生成和恢复效率低,以及主从节点全量同步时长增加、缓冲区溢出。
实例内存和内存快照 RDB 的关系是非常直接的:实例内存容量大,RDB 文件也会相应增加,那么,RDB 文件生成时的 fork
市场就会增加,这会导致 Redis 实例阻塞。而且,RDB 文件增大后,使用 RDB 进行恢复的时长也会增加,会导致 Redis 较长时间无法对外提供服务。
主从节点的同步的第一步就是要做全量同步。全量同步是主节点生成 RDB 文件,并传给从节点,从节点再加载。想一下,如果 RDB 文件很大,肯定会导致同步时长增加,效率不高,而且还可能会导致复制缓冲区溢出。一旦缓冲区溢出了,主从节点间就会又开始全量同步,影响业务的正常使用。如果我们增加复制缓冲区的容量,又会消耗宝贵的内存资源。
此外,如果主库发生了故障,进行主从切换后,其他从库都需要和新主库进行一次全量同步。如果 RDB 文件很大,会导致主从切换过程的耗时增加,同样会影响业务的可用性。
Pika 键值数据库的整体架构中包括了五部分,分别是 网络框架、Pika 线程模块、Nemo 存储模块、RocksDB 和 binlog 机制,如下图所示:
首先,网络框架
主要负责底层网络请求的接收和发送。Pika 的网络框架是对操作系统底层的网络函数进行了封装。Pika 在进行网络通信时,可以直接调用网络封装好的函数。
其次,Pika 线程模块
采用了多线程模型来具体处理客户端请求,包括一个请求分发线程(DispatchThread)、一组工作线程(WorkerThread)以及一个线程池(ThreadPool)。
Nemo 模块
很容易理解,它实现了 Pika 和 Redis 的数据类型兼容。这样一来,当我们把 Redis 服务迁移到 Pika 时,不用修改业务应用中的 Redis 的代码,而且还可以继续应用运行 Redis 的经验,这使得 Pika 的学习成本很低。Nemo 模块对数据类型的具体转换机制,下面会进行介绍。
最后,RocksDB
提供基于 SSD 保存数据的功能。它使得 Pika 可以不用大容量的内存,就能保存更多数据,还避免了使用内存快照。而且,Pika 使用 binlog 机制记录写命令,用于主从节点的命令同步,避免了刚刚所说的大内存实例在主从同步过程中的潜在问题。
为了把数据保存到 SSD,Pika 使用了持久化数据库 RocksDB。RocksDB 本身的实现机制较为复杂,你只要记住 RocksDB 的基本数据读写机制,对于学习了解 Pika 来说就已经足够了。下面解释下这个基本读写机制。
用一张图片来介绍下 RocksDB 写入数据的基本流程。
当 RocksDB 需要保存数据的时候,RocksDB 会使用两小块内存空间(Memtable1 和 Memtable2)来交替缓存写入数据。Memtable 的大小可设置,一个 Memtable 的大小一般为几 MB 或几十 MB。
这么一分析我们就知道了,RocksDB 会先用 Memtable 缓存数据,再将数据快速写入 SSD,及时数据量再大,所有数据也都能保存到 SSD 中。而且 Memtable 本身容量不大,即使 RocksDB 使用了两个 Memtable,也不会占用过多的内存,这样一来,Pika 在保存大容量数据的同时,也不用占据太大的内存空间。
当 RocksDB 需要读数据时,RocksDB 会现在 Memtable 中查询是否有要读取的数据。因为,最新的数据都是先写入到 MemTable 中的。如果 Memtable 中没有要读取的数据,RocksDB 会再查询保存在 SSD 上的数据文件。
其实,Pika 中是没有这些问题的。
简单小结下:Pika 使用 RocksDB 把大量数据保存到了 SSD,同时避免了内存快照的生成和恢复问题。而且,Pika 使用 binlog 机制进行主从同步,避免了大内存时的影响,Pika 的第一个设计目标就实现了。
Pika 的底层使用了 RocksDB 来保存数据,但是 RocksDB 只提供了 单值的键值对类型,而 Redis 键值对中的值还可以是集合类型。Pika 的第二个设计目标(如何和 Redis 兼容)是如何实现的呢?
Pika 中的 Nemo 模块就负责把 Redis 的集合类型转换成单值的键值对。简单来说,我们可以把 Redis 的集合类型分成两类:
对于 Sorted Set 来说,该类型是需要能够按照元素的 socre 值排序的,而 RocksDB 只支持按照单键值对的键来排序。所以,Nemo 在转换数据时,就把 Sorted Set 集合 key、元素的 score 和 member 值都嵌入到了 单键值对的键当中。
Pika 单键值对的值只保存了数据的版本信息和剩余存活时间。
和 Redis 相比,Pika 最大的特点就是使用 SSD 来保持数据,这个特点能带来的最直接好处就是,Pika 单实例能保存更多的数据了,实现了实例数据扩容。
此外,Pika 使用 SSD 来保持数据,还有额外两个优势。
但是,Pika 也有自身的一些不足。
虽然它保持了 Redis 操作接口,也能实现数据扩容,但是,当把数据保存到 SSD 上后,会降低数据的访问性能。这是因为,数据库操作毕竟在内存中直接执行了,而是要在底层的 SSD 中进行存取,这肯定会影响性能。而且,我们还需要把 binlog 机制记录的写命令同步到 SSD 上,者会降低 Pika 的写性能。
不过,Pika 的多线程模型,可以同时使用多个线程进行数据读写,这在一定程度上弥补了从 SSD 存取数据造成的性能损失。当然,你也可以使用高配的 SSD 来提升访问性能,进而减少读写 SSD 对 Pika 的性能影响。
为了更加了解 Pika 的性能情况,我从 Pika 官网 上扒出来的一种测试数据表。
操作性能 (OPS) |
写binlog | 不写binlog |
---|---|---|
SET | 124K | 211K |
GET | 284K | 292K |
HSET | 122K | 214K |
HGET | 284K | 290K |
从上表的结果中,可以看出,在不写 binlog 时,Pika 的 SET/GET、HSET/HGET 的性能都能达到 200K OPS 以上,而一旦增加了 binlog 操作,SET/GET、HSET/HGET 的性能大约下降了 41%,只有约 120K OPS。
所以,在使用 Pika 时,需要在单实例扩容的必要性和可能的性能损失间做个权衡。如果保存大容量数据使我们的首要要求,那么 Pika 是一个不错的解决方案。
我们学习了基于 SSD 给 Redis 单实例进行扩容的技术方案 Pika。跟 Redis 相比,Pika 的好处非常明显:既支持 Redis 操作接口,又能保持大容量的数据。如果你原来就在应用 Redis,现在想进行扩容,那么 Pika 是一个很好的选择,无论是代码迁移还是运维管理,Pika 基本上不需要额外的工作量。
不过,Pika 比较是将数据保存到 SSD 上,数据访问要读写 SSD ,所以读写性能要弱于 Redis。针对这一点,有两个小建议:
最后,Pika 本身提供了很多工具,可以帮我我们把 Redis 数据迁移到 Pika,或者把 Redis 请求转发给 Pika。比如,aof_to_pika 命令,并且制定 Redis 的 AOF 文件以及 Pika 的连接信息,就可以把 Redis 数据迁移到 Pika 中了,如下所示:
aof_to_pika -i [Redis AOF文件] -h [Pika IP] -p [Pika 端口] -a [认证信息]
可以直接在 Pika 的官网上找到哦啊。并且,Pika 本身也还在迭代开发,你可以多去 GitHub 看看,进一步了解它,这样你可以获得 Pika 的最新进制,以便能更好地把它应用到你的业务实践中。