Pika:基于SSD实现大容量Redis
参考:
# 1 Pika:如何基于 SSD 实现大容量 Redis?
在应用 Redis 时,随着业务数据的增加,会需要 Redis 保存更多的数据,这时会想到使用 Redis 切片集群,从而把数据分散保存到多个实例上,但这样会导致集群的实例规模增加,让集群的运维变得复杂。
还有一种方法是增加 Redis 单个实例的内存容量,从而减少所需的 Redis 实例的个数。这是个好主意,但并不完美:基于大内存的大容量实例在实例恢复、主从同步过程中会引起一系列潜在问题,例如恢复时间增长、主从切换开销大、缓冲区易溢出。
这该怎么办呢?我推荐你使用固态硬盘(SSD),它成本很低(每 GB 的成本约是内存的十分之一),且容量大,读写速度快,我们可以基于SSD来实现大容量的Redis实例。360公司DBA和基础架构组联合开发的 Pika (opens new window) 键值数据库,正好实现了这一需求。
Pika 在刚开始设计时有两个目标:
- 单实例可以保存大容量数据,同时避免了实例恢复和主从同步时的潜在问题;
- 和 Redis 数据类型保持兼容,可以支持使用 Redis 的应用平滑地迁移到 Pika 上。
所以,如果你一直在使用 Redis,并且想使用 SSD 来扩展单实例容量,Pika 就是一个很好的选择。
本节将先介绍基于大内存实现大容量 Redis 实例的潜在问题,然后介绍 Pika 的相关设计。
# 1.1 大内存 Redis 实例的潜在问题
Redis 使用内存保存数据,内存容量增加后,就会带来两方面的潜在问题:
- 内存快照 RDB 生成和恢复效率低:这会导致 Redis 实例阻塞过久;
- 主从节点全量同步时长增加:如果 RDB 文件很大,会导致全量同步的时长增加,效率不高,而且还可能会导致复制缓冲区溢出。一旦缓冲区溢出了,主从节点间就会又开始全量同步,影响业务应用的正常使用。如果我们增加复制缓冲区的容量,这又会消耗宝贵的内存资源。
那么,Pika 是如何解决这两方面的问题呢?这就要提到 Pika 中的关键模块 RocksDB、binlog 机制和 Nemo 了,这些模块都是 Pika 架构中的重要组成部分。
# 1.2 Pika 的整体架构
Pika 键值数据库的整体架构中包括了五部分,分别是网络框架、Pika 线程模块、Nemo 存储模块、RocksDB 和 binlog 机制,如下图所示:
这五个部分分别实现了不同的功能,下面我一个个来介绍下。
首先,网络框架主要负责底层网络请求的接收和发送。Pika的网络框架是对操作系统底层的网络函数进行了封装。Pika在进行网络通信时,可以直接调用网络框架封装好的函数。
其次,Pika 线程模块采用了多线程模型来具体处理客户端请求,包括一个请求分发线程(DispatchThread)、一组工作线程(WorkerThread)以及一个线程池(ThreadPool)。
请求分发线程专门监听网络端口,一旦接收到客户端的连接请求后,就和客户端建立连接,并把连接交由工作线程处理。工作线程负责接收客户端连接上发送的具体命令请求,并把命令请求封装成Task,再交给线程池中的线程,由这些线程进行实际的数据存取处理,如下图所示:
在实际应用Pika的时候,我们可以通过增加工作线程数和线程池中的线程数,来提升Pika的请求处理吞吐率,进而满足业务层对数据处理性能的需求。
Nemo 模块很容易理解,它实现了 Pika 和 Redis 的数据类型兼容。这样一来,当我们把 Redis 服务迁移到 Pika 时,不用修改业务应用中操作 Redis 的代码,而且还可以继续应用运维 Redis 的经验,这使得 Pika 的学习成本就较低。Nemo 模块对数据类型的具体转换机制是我们要重点关心的,下面我会具体介绍。
最后,我们再来看看 RocksDB 提供的基于SSD保存数据的功能。它使得 Pika 可以不用大容量的内存,就能保存更多数据,还避免了使用内存快照。而且,Pika使用 binlog 机制记录写命令,用于主从节点的命令同步,避免了刚刚所说的大内存实例在主从同步过程中的潜在问题。
接下来,我们就来具体了解下,Pika 是如何使用 RocksDB 和 binlog 机制的。
# 1.3 Pika 如何基于 SSD 保存更多数据?
# 1.3.1 使用 RocksDB 来存储数据
为了把数据保存到 SSD,Pika 使用了持久化 KV 数据库 RocksDB (opens new window)。这里只需要了解 RocksDB 的基本数据读写机制就可以:
- Memtable 是一个小内存空间,大约几MB或几十MB。两个 Memtable 来交替缓存写入数据。
根据上图可以看到,RocksDB 会先用 Memtable 缓存数据,再将数据快速写入SSD,即使数据量再大,所有数据也都能保存到SSD中。而且,Memtable本身容量不大,即使RocksDB使用了两个Memtable,也不会占用过多的内存,这样一来,Pika在保存大容量数据时,也不用占据太大的内存空间了。
数据查找方法:当Pika需要读取数据的时候,RocksDB会先在Memtable中查询是否有要读取的数据。这是因为,最新的数据都是先写入到Memtable中的。如果Memtable中没有要读取的数据,RocksDB会再查询保存在SSD上的数据文件,如下图所示:
到这里,你就了解了,当使用了RocksDB保存数据后,Pika就可以把大量数据保存到大容量的SSD上了,实现了大容量实例。
不过,我刚才向你介绍过,当使用大内存实例保存大量数据时,Redis会面临RDB生成和恢复的效率问题,以及主从同步时的效率和缓冲区溢出问题。那么,当Pika保存大量数据时,还会面临相同的问题吗?其实不会了,接下来分析一下。
- 一方面,Pika基于RocksDB保存了数据文件,直接读取数据文件就能恢复,不需要再通过内存快照进行恢复了。而且,Pika从库在进行全量同步时,可以直接从主库拷贝数据文件,不需要使用内存快照,这样一来,Pika就避免了大内存快照生成效率低的问题。
- 另一方面,Pika使用了binlog机制实现增量命令同步,既节省了内存,还避免了缓冲区溢出的问题。binlog是保存在SSD上的文件,Pika接收到写命令后,在把数据写入Memtable时,也会把命令操作写到binlog文件中。和Redis类似,当全量同步结束后,从库会从binlog中把尚未同步的命令读取过来,这样就可以和主库的数据保持一致。当进行增量同步时,从库也是把自己已经复制的偏移量发给主库,主库把尚未同步的命令发给从库,来保持主从库的数据一致。
和Redis使用缓冲区相比,使用binlog好处是非常明显的:binlog是保存在SSD上的文件,文件大小不像缓冲区,会受到内存容量的较多限制。而且,当binlog文件增大后,还可以通过轮替操作,生成新的binlog文件,再把旧的binlog文件独立保存。这样一来,即使Pika实例保存了大量的数据,在同步过程中也不会出现缓冲区溢出的问题了。
简单小结下:Pika 使用 RocksDB 把大量数据保存到了 SSD,同时避免了内存快照的生成和恢复问题。而且,Pika 使用 binlog 机制进行主从同步,避免大内存时的影响,Pika 的第一个设计目标就实现了。
接下来,我们再来看 Pika 是如何实现第二个设计目标的,也就是如何和 Redis 兼容。毕竟,如果不兼容的话,原来使用 Redis 的业务就无法平滑迁移到 Pika 上使用了,也就没办法利用 Pika 保存大容量数据的优势了。
# 1.4 Pika 如何实现 Redis 数据类型兼容?
Pika 的底层存储使用了 RocksDB 来保存数据,但是,RocksDB 只提供了单值的键值对类型,而 Redis 键值对中的值却可以是集合类型。这样,对于集合类型就无法直接存储在 RocksDB 中,需要 Pika 的 Nemo 模块来完成将 Redis 集合类型 -> 单值 kv 的转换。
简单来说,可以将 Redis 的集合类型分成两类:
- 一类是 List 和 Set 类型,它们的集合中每个元素都是单值;
- 另一类是 Hash 和 Sorted Set 类型,它们的集合中的元素都是 pair,其中,Hash 集合元素是 field-value 类型,而 Sorted Set 集合元素是 member-score 类型。
下面看一下 Nemo 模块是如何做的这种转换。
# 1.4.1 List 类型
在Pika中,List集合的key被嵌入到了单值键值对的键当中,用key字段表示;而List集合的元素值,则被嵌入到单值键值对的值当中,用value字段表示。因为List集合中的元素是有序的,所以,Nemo模块还在单值键值对的key后面增加了sequence字段,表示当前元素在List中的顺序,同时,还在value的前面增加了previous sequence和next sequence这两个字段,分别表示当前元素的前一个元素和后一个元素。
此外,在单值键值对的key前面,Nemo模块还增加了一个值“l”,表示当前数据是List类型,以及增加了一个1字节的size字段,表示List集合key的大小。在单值键值对的value后面,Nemo模块还增加了version和ttl字段,分别表示当前数据的版本号和剩余存活时间(用来支持过期key功能),如下图所示:
# 1.4.2 Set 类型
Set集合的key和元素member值,都被嵌入到了Pika单值键值对的键当中,分别用key和member字段表示。同时,和List集合类似,单值键值对的key前面有值“s”,用来表示数据是Set类型,同时还有size字段,用来表示key的大小。Pika单值键值对的值只保存了数据的版本信息和剩余存活时间,如下图所示:
# 1.4.3 Hash 类型
对于Hash类型来说,Hash集合的key被嵌入到单值键值对的键当中,用key字段表示,而Hash集合元素的field也被嵌入到单值键值对的键当中,紧接着key字段,用field字段表示。Hash集合元素的value则是嵌入到单值键值对的值当中,并且也带有版本信息和剩余存活时间,如下图所示:
# 1.4.4 Sorted Set 类型
,对于Sorted Set类型来说,该类型是需要能够按照集合元素的score值排序的,而RocksDB只支持按照单值键值对的键来排序。所以,Nemo模块在转换数据时,就把Sorted Set集合key、元素的score和member值都嵌入到了单值键值对的键当中,此时,单值键值对中的值只保存了数据的版本信息和剩余存活时间,如下图所示:
采用了上面的转换方式之后,Pika不仅能兼容支持Redis的数据类型,而且还保留了这些数据类型的特征,例如List的元素保序、Sorted Set的元素按score排序。了解了Pika的转换机制后,你就会明白,如果你有业务应用计划从使用Redis切换到使用Pika,就不用担心面临因为操作接口不兼容而要修改业务应用的问题了。
前面分析了 Pika 的两大优势:
- 基于 SSD 保存大量数据
- 与 Redis 兼容
接下来看一下 Pika 的其他优势和潜在不足。
# 1.5 Pika 的其他优势与不足
跟Redis相比,Pika最大的特点就是使用了SSD来保存数据,这个特点能带来的最直接好处就是,Pika单实例能保存更多的数据了,实现了实例数据扩容。除此之外,Pika 还有两个额外优势:
- 实例重启快。Pika的数据在写入数据库时,是会保存到SSD上的。当Pika实例重启时,可以直接从SSD上的数据文件中读取数据,不需要像Redis一样,从RDB文件全部重新加载数据或是从AOF文件中全部回放操作,这极大地提高了Pika实例的重启速度,可以快速处理业务应用请求。
- 主从库重新执行全量同步的风险低。Pika通过binlog机制实现写命令的增量同步,不再受内存缓冲区大小的限制,所以,即使在数据量很大导致主从库同步耗时很长的情况下,Pika也不用担心缓冲区溢出而触发的主从库重新全量同步。
但 Pika 也有一些不足:
虽然它保持了Redis操作接口,也能实现数据库扩容,但当把数据保存到SSD上后,会降低数据的访问性能。这是因为,数据操作毕竟不能在内存中直接执行了,而是要在底层的SSD中进行存取,这肯定会影响,Pika的性能。而且,我们还需要把binlog机制记录的写命令同步到SSD上,这会降低Pika的写性能。
不过,Pika的多线程模型,可以同时使用多个线程进行数据读写,这在一定程度上弥补了从SSD存取数据造成的性能损失。当然,你也可以使用高配的SSD来提升访问性能,进而减少读写SSD对Pika性能的影响。
为了帮助你更直观地了解Pika的性能情况,我再给你提供一张表,这是Pika 官网 (opens new window) 上提供的测试数据:
这些数据是在Pika 3.2版本中,String和Hash类型在多线程情况下的基本操作性能结果。从表中可以看到,在不写binlog时,Pika的SET/GET、HSET/HGET的性能都能达到200K OPS以上,而一旦增加了写binlog操作,SET和HSET操作性能大约下降了41%,只有约120K OPS。
所以,我们在使用Pika时,需要在单实例扩容的必要性和可能的性能损失间做个权衡。如果保存大容量数据是我们的首要需求,那么,Pika是一个不错的解决方案。
# 1.6 小结
这节课,我们学习了基于SSD给Redis单实例进行扩容的技术方案Pika。跟Redis相比,Pika的好处非常明显:既支持Redis操作接口,又能支持保存大容量的数据。如果你原来就在应用Redis,现在想进行扩容,那么,Pika无疑是一个很好的选择,无论是代码迁移还是运维管理,Pika基本不需要额外的工作量。
不过,Pika毕竟是把数据保存到了SSD上,数据访问要读写SSD,所以,读写性能要弱于Redis。针对这一点,我给你提供两个降低读写SSD对Pika的性能影响的小建议:
- 利用Pika的多线程模型,增加线程数量,提升Pika的并发请求处理能力;
- 为Pika配置高配的SSD,提升SSD自身的访问性能。
最后,我想再给你一个小提示。Pika本身提供了很多工具,可以帮助我们把Redis数据迁移到Pika,或者是把Redis请求转发给Pika。比如说,我们使用aof_to_pika命令,并且指定Redis的AOF文件以及Pika的连接信息,就可以把Redis数据迁移到Pika中了,如下所示:
aof_to_pika -i [Redis AOF文件] -h [Pika IP] -p [Pika port] -a [认证信息]
关于这些工具的信息,你都可以直接在Pika的 GitHub (opens new window) 上找到。而且,Pika本身也还在迭代开发中,我也建议你多去看看GitHub,进一步地了解它。这样,你就可以获得Pika的最新进展,也能更好地把它应用到你的业务实践中。