pika是360奇虎公司开源的一款类redis存储系统,主要解决的是用户使用 Redis 的内存大小超过 50G、80G 等等这样的情况,会遇到启动恢复时间长,一主多从代价大,硬件成本贵,缓冲区容易写满等问题。
Pika 就是针对这些场景的一个解决方案:
下图是Pika的实现框架:
主要组成
Pika 基于 pink 对线程进行封装,使用多个工作线程来进行读写操作,由底层 nemo 引擎来保证线程安全,线程分为 11 种:
nemo本质上是对rocksdb的改造和封装,使其支持多数据结构的存储(rocksdb只支持kv存储)。总的来说,nemo支持五种数据结构类型的存储:KV键值对、Hash结构、List结构、Set结构和ZSet结构。因为rocksdb的存储方式只有kv一种结构,所以以上所说的5种数据结构的存储最终都要落盘到rocksdb的kv存储方式上。
1、KV键值对
KV存储没有添加额外的元信息,只是在value的结尾加上8个字节的附加信息(前4个字节表示version,后 4个字节表示ttl)作为最后落盘kv的值部分。具体如下图:
version字段用于对该键值对进行标记,以便后续的处理,如删除一个键值对时,可以在该version进行标记,后续再进行真正的删除,这样可以减少删除操作所导致的服务阻塞时间。
2、Hash结构
对于每一个Hash存储,它包括hash键(key),hash键下的域名(field)和存储的值 (value)。nemo的存储方式是将key和field组合成为一个新的key,将这个新生成的key与所要存储的value组成最终落盘的kv键值对。同时,对于每一个hash键,nemo还为它添加了一个存储元信息的落盘kv,它保存的是对应hash键下的所有域值对的个数。下面的是具体的实现方式:
每个hash键的元信息的落盘kv的存储格式:
前面的横条代表的存储每个hash键的落盘kv键值对的键部分,它有两字段组成:
后面的横条代表的该元信息的值,它表示对应的hash键中的域值对(field-value)的数量,大小为8个字节(类型是int64_t)。
每个hash键、field、value到落盘kv的映射转换:
前面的横条对应落盘kv键值对的键部分:
后面的横条代表的是落盘kv键值对的值部分,和KV结构存储一样,它是存入的value值加上8个字节的version字段和8个字节的ttl字段得到的。
3、List结构
每个List结构的底层存储也是采用链表结构来完成的。对于每个List键,它的每个元素都落盘为一个kv键值对,作为一个链表的一个节点,称为元素节点。和hash一样,每个List键也拥有自己的元信息。
每个元信息的落盘kv的存储格式
前面横条表示存储元信息的落盘kv的键部分,和前面的hash结构是类似的;
后面的横条表示存储List键的元信息,它有四个字段,从前到后分别为该List键内的元素个数、最左边的元素节点的sequence(相当于链表头)、最右边的元素节点的sequence(相当于链表尾)、下一个要插入元素节点所应该使用的sequence。
每个元素节点对应的落盘kv存储格式
前面横条代表的是最终落盘kv结构的键部分,总共4个字段,前面三个字符段分别为一个字符’l’(表明是List结构的结存),List键的字符串长度(1个字节)、List键的字符串内容(最多254个字节),第四个字段是该元素节点所对应的索引值,用8个字节表示(int64_t类型),对于每个元素节点,这个索引(sequence)都是唯一的,是其他元素节点访问该元素节点的唯一媒介;往一个空的List键内添加一个元素节点时,该添加的元素节点的sequence为1,下次一次添加的元素节点的sequence为2,依次顺序递增,即使中间有元素被删除了,被删除的元素的sequence也不会被之后新插入的元素节点使用,这就保证了每个元素节点的sequence都是唯一的。
后面的横条代表的是具体落盘kv结构的值,它有5个字段,后面的三个字段分别为存入的value值、version、ttl,这和前面的hash结构存储是类似的;前两个字段分别表示的是前一个元素节点的sequence、和后一个元素节点的sequence、通过这两个sequence,就可以知道前一个元素节点和后一个元素节点的罗盘kv的键内容,从而实现了一个双向链表的结构。
4、Set结构
每个Set键的元信息对应的落盘kv存储格式
每个元素节点对应的落盘kv存储格式
值的部分只有version和ttl,没有value字段。
5、ZSet结构
ZSet存储结构是一个有序Set,所以对于每个元素,增加了一个落盘kv,在这个增加的罗盘 kv的键部分,把该元素对应的score值整合进去,这样便于依据Score值进行排序(因为从rocksdb内拿出的数据时按键排序的),下面是落盘kv的存储形式。
存储元信息的落盘kv的存储格式
score值在value部分的落盘kv存储格式
score值在key部分的落盘kv存储格式
score是从double类型转变过来的int64_t类型,这样做是为了可以让原来的浮点型的score直接参与到字符串的排序当中(浮点型的存储格式与字符串的比较方式不兼容)。
Pika 的主从同步是使用 Binlog 来完成的:master 执行完一条写命令就将命令追加到 Binlog 中,ReplicaSender 将这条命令从 Binlog 中读出来发送给 slave,slave 的 ReplicaReceiver 收到该命令,执行,并追加到自己的 Binlog 中。
binlog 本质是顺序写文件,通过 Index + offset 进行同步点检查,支持全同步 + 增量同步;
当发生主从切换以后,slave 仅需要将自己当前的 Binlog Index + offset 发送给 master,master 找到后从该偏移量开始同步后续命令。
为了防止读文件中写错一个字节则导致整个文件不可用,所以Pika采用了类似 leveldb log 的格式来进行存储,具体如下:
先说下slave的连接状态:
Pika 支持 master/slave 的复制方式,通过 slave 端的 slaveof 命令激发:
pika同步依赖于binlog,binlog 文件会自动或手动删除,当同步点对应的 binlog 文件不存在时,需要通过全同步进行数据同步。
需要进行全同步时,master 会将 db 文件 dump 后发送给 slave(通过 rsync 的 deamon 模式实现 db 文件的传输),实现逻辑:
Slave 的流程:
Master 的流程:
一主多从的结构master节点也可以给多个slave复用一个Binlog,只不过不同的slave在binglog中有自己的偏移量而已,master执行完一条写命令就将命令追加到Binlog中,ReplicaSender将这条命令从Binlog中读出来发送给slave,slave的ReplicaReceiver收到该命令,执行,并追加到自己的Binlog中。
主要模块:
主要的工作过程:
下图是BinLogReceiverModule(在源代码中没有这个对象,这里是为了说明方便,抽象出来的)的组成,从图中可以看出BinlogReceiverModule由一个BinlogReceiverThread和多个BinlogBGWorker组成。
BinlogReceiverThread接收到一个同步命令后,它会给这个命令赋予一个唯一的序列号(这个序列号是递增的),并把它分发给一个BinlogBGWorker;而各个BinlogBGWorker则会根据各个命令的所对应的序列号的顺序来执行各个命令,这样也就保证了命令执行的顺序和主节点执行的顺序一致了 之所以这么设计主要原因是:
上图是一个主从同步的一个过程(即根据主节点数据库的操作日志,将主节点数据库的改变过程顺序的映射到从节点的数据库上),从图中可以看出,每一个从节点在主节点下都有一个唯一对应的BinlogSenderThread。 (为了说明方便,我们定一个“同步命令”的概念,即会改变数据库的命令,如set,hset,lpush等,而get,hget,lindex则不是)
不同于Redis,Pika的数据主要存储在磁盘中,这就使得其在做数据备份时有天然的优势,可以直接通过文件拷贝实现 实现
应用挂起指令,在挂起指令的执行中,会添加写锁,以确保,此时没有其他指令执行。其他的普通指令在会添加读锁,可以并行访问。其中挂起指令有:
在pika系统中,对于数据库的操作都需要添加行锁,主要在应用于两个地方,在系统上层指令过程中和在数据引擎层面。在pika系统中,对于写指令(会改变数据状态,如SET,HSET)需要除了更新数据库状态,还涉及到pika的增量同步,需要在binlog中添加所执行的写指令,用于保证master和slave的数据库状态一致。故一条写指令的执行,主要有两个部分:
其加锁情况,如下图:
在图中可以看到,对同一个key,加了两次行锁,在实际应用中,pika上所加的锁就已经能够保证数据访问的正确性。如果只是为了pika所需要的业务,nemo层面使用行锁是多余的,但是nemo的设计初衷就是通过对rocksdb的改造和封装提供一套完整的类redis数据访问的解决方案,而不仅仅是为pika提供数据库引擎。这种设计思路也是秉承了Unix中的设计原则:Write programs that do one thing and do it well。
这样设计大大降低了pika与nemo之间的耦合,也使得nemo可以被单独拿出来测试和使用,在pika中的数据迁移工具就是完全使用nemo来完成,不必依赖任何pika相关的东西。另外对于nemo感兴趣或者有需求的团队也可以直接将nemo作为数据库引擎而不需要修改任何代码就能使用完整的数据访问功能。