一.介绍
- pika 的单线程的性能肯定不如redis, pika是多线程的结构, 因此在线程数比较多的情况下, 某些数据结构的性能可以优于redis
- pika 肯定不是完全优于redis 的方案, 只是在某些场景下面更适合. 所以目前公司内部redis, pika 是共同存在的方案, DBA会根据业务的场景挑选合适的方案
1.1 redis大容量问题:
- 恢复时间长:我们线上的redis 一般同时开启rdb 和 aof。我们知道aof的作用是实时的记录用户的写入操作, rdb 是redis 某一时刻数据的完整快照. 那么恢复的时候一般是通过 rdb + aof 的方式进行恢复, 50G redis 恢复时间需要差不多70分钟。
- 一主多从, 主从切换代价大:redis 在主库挂掉以后, 从库升级为新的主库. 那么切换主库以后, 所有的从库都需要跟新主做一次全同步, 全量同步一次大容量的redis, 代价非常大.
- 缓冲区写满问题:为了防止同步缓冲区被复写,dba给redis设置了2G的巨大同步缓冲区,这对于内存资源来讲代价很大. 当由于机房之间网络有故障, 主从同步出现延迟了大于2G以后, 就会触发全同步的过程. 如果多个从库同时触发全同步的过程, 那么很容易就将主库给拖死
- 内存太贵:如果一个redis 的实例是50G, 那么基本一台机器只能运行一个redis 实例. 因此特别的浪费资源
1.2 pika优势
- 容量大:pika没有redis内存限制,最大使用空间等于磁盘大小。
- 加载DB快:pika在写入的时候,数据是落盘的,所以即使是节点挂了,不需要rdb和aof;pika重启也不用重新加载数据到内存,而是直接使用已经持久化在磁盘上的数据,不需要任何数据回放的操作,大大降低重启成本。
- 备份速度快:pika备份速度大致等于cp速度,这样在对于百G大库备份是快捷的,更快的备份速度更好的解决了主从全同步的问题。
- 对网络容忍度高:不会占有
二.整体架构
2.1 pink 网络模块
支持pb、redis、pb、http等等协议. 对网络编程的封装。
2.2 线程模块
pika使用的是多线程模型,使用多个工作线程来进行读写操作,由底层nemo引擎来保证线程安全,线程分为11种:
- PikaServer:主线程
- DispatchThread:监听端口1个端口,接收用户连接请求
- ClientWorker:存在多个(用户配置),每个线程里有若干个用户客户端的连接,负责接收处理用户命令并返回结果,每个线程执行写命令后,追加到binlog中
- Trysync:尝试与master建立首次连接,并在以后出现故障后发起重连
- ReplicaSender:存在多个(动态创建销毁,本master节点挂多少个slave节点就有多少个),每个线程根据slave节点发来的同步偏移量,从binlog指定的偏移开始实时同步命令给slave节点
- ReplicaReceiver:存在1个(动态创建销毁,一个slave节点同时只能有一个master),将用户指定或当前的偏移量发送给master节点并开始接收执行master实时发来的同步命令,在本地使用和master完全一致的偏移量来追加binlog
- SlavePing:slave用来向master发送心跳进行存活检测
- bgsave:后台dump线程
- HeartBeat:master用来接收所有slave发送来的心跳并回复进行存活检测
- scan:后台扫描keyspace线程
- purge:后台删除binlog线程
2.3 存储引擎 nemo
nemo本质上是对rocksdb的改造和封装,使其支持多数据结构的存储(rocksdb只支持kv存储)。总的来说,nemo支持五种数据结构类型的存储:KV键值对(为了区分,nemo的的键值对结构用大写的“KV”表示)、Hash结构、List结构、Set结构和ZSet结构。因为rocksdb的存储方式只有kv一种结构,所以以上所说的5种数据结构的存储最终都要落盘到rocksdb的kv存储方式上。
2.3.1 KV键值对的存储
KV存储没有添加额外的元信息,只是在value的结尾加上8个字节的附加信息(前4个字节表示version,后 4个字节表示ttl)作为最后落盘kv的值部分。具体如下图:
version字段用于对该键值对进行标记,以便后续的处理,如删除一个键值对时,可以在该version进行标记,后续再进行真正的删除,这样可以减少删除操作所导致的服务阻塞时间。
2.3.2 Hash结构的存储
对于每一个Hash存储,它包括hash键(key),hash键下的域名(field)和存储的值 (value)。nemo的存储方式是将key和field组合成为一个新的key,将这个新生成的key与所要存储的value组成最终落盘的kv键值对。同时,对于每一个hash键,nemo还为它添加了一个存储元信息的落盘kv,它保存的是对应hash键下的所有域值对的个数。下面的是具体的实现方式:
- 每个hash键、field、value到落盘kv的映射转换:
左面的横条对应落盘kv的键部分,从前到后,第一个字段是一个字符’h’,表示的是hash结构的key;第二个字段是hash键的字符串长度,用一个字节(uint8_t类型)来表示;第三个字段是hash键的内容,因为第二个字段是一个字节,所以这里限定hash键的最大字符串长度是254个字节;第四个字段是field的内容。右面的横条代表的是落盘kv键值对的值部分,和KV结构存储一样,它是存入的value值加上8个字节的version字段和8个字节的ttl字段得到的。
- 每个hash键的元信息的落盘kv的存储格式 :
左面的横条代表的存储每个hash键的落盘kv键值对的键部分,它有两字段组成,第一个字段是一个’H’字符,表示这存储时hash键的元信息,第二个字段是对应的hash键的字符串内容;b中后面的横条代表的该元信息的值,它表示对应的hash键中的域值对(field-value)的数量,大小为8个字节(类型是int64_t)。
2.3.3 List结构的存储
每个List结构的底层存储也是采用链表结构来完成的。对于每个List键,它的每个元素都落盘为一个kv键值对,作为一个链表的一个节点,称为元素节点。和hash一样,每个List键也拥有自己的元信息。
- 每个元素节点对应的落盘kv存储格式 :
左面横条代表的是最终落盘kv结构的键部分,总共4个字段,前面三个字符段分别为一个字符’l’(表明是List结构的结存),List键的字符串长度(1个字节)、List键的字符串内容(最多254个字节),第四个字段是该元素节点所对应的索引值,用8个字节表示(int64_t类型),对于每个元素节点,这个索引(sequence)都是唯一的,是其他元素节点访问该元素节点的唯一媒介;往一个空的List键内添加一个元素节点时,该添加的元素节点的sequence为1,下次一次添加的元素节点的sequence为2,依次顺序递增,即使中间有元素被删除了,被删除的元素的sequence也不会被之后新插入的元素节点使用,这就保证了每个元素节点的sequence都是唯一的。
- 每个元信息的落盘kv的存储格式:
左面的横条表示存储元信息的落盘kv的键部分,和前面的hash结构是类似的;右面的横条表示存储List键的元信息,它有四个字段,从前到后分别为该List键内的元素个数、最左边的元素 节点的sequence(相当于链表头)、最右边的元素节点的sequence(相当于链表尾)、下一个要插入元素节点所应该使用的sequence。
2.3.4 Set结构的存储
Set结构的存储和hash结构基本是相同的,只是Set中每个元素对应的落盘kv中,值的部分只有version和ttl,没有value字段。
- 每个元素节点对应的落盘kv存储格式 :
- 每个Set键的元信息对应的落盘kv存储格式 :
2.3.5 ZSet结构的存储
ZSet存储结构是一个有序Set,所以对于每个元素,增加了一个落盘kv,在这个增加的罗盘 kv的键部分,把该元素对应的score值整合进去,这样便于依据Score值进行排序(因为从rocksdb内拿出的数据时按键排序的),下面是落盘kv的存储形式。
- score值在value部分的落盘kv存储格式 :
- score值在key部分的落盘kv存储格式 :
- 存储元信息的落盘kv的存储格式 :
与前面的几种数据结构类似,不再赘述。b中的score是从double类型转变过来的int64_t类型,这样做是为了可以让原来的浮点型的score直接参与到字符串的排序当中(浮点型的存储格式与字符串的比较方式不兼容)。
2.4 日志模块 Binlog
binlog 本质是顺序写文件, 通过Index + offset 进行同步点检查。解决了同步缓冲区太小的问题,支持全同步 + 增量同步,master 执行完一条写命令就将命令追加到Binlog中,ReplicaSender将这条命令从Binlog中读出来发送给slave,slave的ReplicaReceiver收到该命令,执行,并追加到自己的Binlog中。当发生主从切换以后, slave仅需要将自己当前的Binlog Index + offset 发送给master,master找到后从该偏移量开始同步后续命令。
2.5 主从同步 slaveof
2.5.1 全同步
简介
- 需要进行全同步时,master会将db文件dump后发送给slave
- 通过rsync的deamon模式实现db文件的传输
实现逻辑
- slave在trysnc前启动rsync进程启动rsync服务
- master发现需要全同步时,判断是否有备份文件可用,如果没有先dump一份
- master通过rsync向slave发送dump出的文件
- slave用收到的文件替换自己的db
- slave用最新的偏移量再次发起trysnc
- 完成同步
Slave连接状态
- No Connect:不尝试成为任何其他节点的slave
- Connect:Slaveof后尝试成为某个节点的slave,发送trysnc命令和同步点
- Connecting:收到master回复可以slaveof,尝试跟master建立心跳
- Connected: 心跳建立成功
- WaitSync:不断检测是否DBSync完成,完成后更新DB并发起新的slaveof
2.5.2 增量同步
pika增量同步
Pika Binlog结构
Pika的主从同步是使用Binlog来完成的,一主多从的结构master节点也可以给多个slave复用一个Binlog,只不过不同的slave在binglog中有自己的偏移量而已,master执行完一条写命令就将命令追加到Binlog中,ReplicaSender将这条命令从Binlog中读出来发送给slave,slave的ReplicaReceiver收到该命令,执行,并追加到自己的Binlog中,由于主从偏移量一样,所以一旦发生网络或节点故障需要重连主从时,slave仅需要将自己当前的Binlog偏移量发送给master,master找到后从该偏移量开始同步后续命令,理论上将命令不做处理一条一条追加到文件中,如果这样的记录格式容错很差,如果读文件中写错一个字节则导致整个文件不可用,所以pika采用了类似leveldb log的格式来进行存储,具体如下:
在了解了Binlog格式之后,首先给出一个解释pika的主从同步的对象:
上图是一个主从同步的一个过程(即根据主节点数据库的操作日志,将主节点数据库的改变过程顺序的映射到从节点的数据库上),从图1中可以看出,每一个从节点在主节点下都有一个唯一对应的BinlogSenderThread。 (为了说明方便,我们定一个“同步命令”的概念,即会改变数据库的命令,如set,hset,lpush等,而get,hget,lindex则不是)
主要模块的功能:
- WorkerThread:接受和处理用户的命令;
- BinlogSenderThread:负责顺序地向对应的从节点发送在需要同步的命令;
- BinlogReceiverModule: 负责接受主节点发送过来的同步命令
- Binglog:用于顺序的记录需要同步的命令
主要的工作过程:
- 当WorkerThread接收到客户端的命令,按照执行顺序,添加到Binlog里;
- BinglogSenderThread判断它所负责的从节点在主节点的Binlog里是否有需要同步的命令,若有则发送给从节点;
- BinglogReceiverModule模块则做以下三件事情:接收主节点的BinlogSenderThread发送过来的同步命令;把接收到的命令应用到本地的数据上;把接收到的命令添加到本地Binlog里 至此,一条命令从主节点到从节点的同步过程完成
BinLogReceiverModule的工作过程:
上图是BinLogReceiverModule(在源代码中没有这个对象,这里是为了说明方便,抽象出来的)的组成,从图2中可以看出BinlogReceiverModule由一个BinlogReceiverThread和多个BinlogBGWorker组成。
- BinlogReceiverThread: 负责接受由主节点传送过来的命令,并分发给各个BinlogBGWorker,若当前的节点是只读状态(不能接受客户端的同步命令),则在这个阶段写Binlog
- BinlogBGWorker:负责执行同步命令;若该节点不是只读状态(还能接受客户端的同步命令),则在这个阶段写Binlog(在命令执行之前写)
BinlogReceiverThread接收到一个同步命令后,它会给这个命令赋予一个唯一的序列号(这个序列号是递增的),并把它分发给一个BinlogBGWorker;而各个BinlogBGWorker则会根据各个命令的所对应的序列号的顺序来执行各个命令,这样也就保证了命令执行的顺序和主节点执行的顺序一致了 之所以这么设计主要原因是:
- 配备多个BinlogBGWorker是可以提高主从同步的效率,减少主从同步的滞后延迟;
- 让BinlogBGWorker在执行执行之前写Binlog可以提高命令执行的并行度;
- 在当前节点是非只读状态,让BinglogReceiverThread来写Binlog,是为了让Binglog里保存的命令顺序和命令的执行顺序保持一致.