Pika的设计及实现
Pika
pika是360奇虎公司开源的一款类redis存储系统,主要解决的是用户使用 Redis 的内存大小超过 50G、80G 等等这样的情况,会遇到启动恢复时间长,一主多从代价大,硬件成本贵,缓冲区容易写满等问题。
Pika 就是针对这些场景的一个解决方案:
- Pika 的单线程的性能肯定不如 Redis,Pika 是多线程的结构,因此在线程数比较多的情况下,某些数据结构的性能可以优于 Redis;
- Pika 肯定不是完全优于 Redis 的方案,只是在某些场景下面更适合,DBA 可以根据业务的场景挑选合适的方案。
Pika架构
主要组成
- 网络模块 pink,对网络编程的封装,用户实现一个高性能的 server 只需要实现对应的 DealMessage 函数即可,支持单线程模型、多线程 worker 模型;
- 线程模块,见下文;
- 存储引擎 nemo,基于 Rocksdb 修改,封装 Hash, List, Set, Zset 等数据结构;
- 日志模块 binlog,解决了同步缓冲区太小的问题;
线程模块
Pika 基于 pink 对线程进行封装,使用多个工作线程来进行读写操作,由底层 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 发送心跳进行存活检测
- HeartBeat:master 用来接收所有 slave 发送来的心跳并恢复进行存活检测
- bgsave:后台 dump 线程
- scan:后台扫描 keyspace 线程
- purge:后台删除 binlog 线程
nemo存储引擎
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键值对的键部分,它有两字段组成:
- 第一个字段是一个’H’字符,表示这存储时hash键的元信息;
- 第二个字段是对应的hash键的字符串内容;
后面的横条代表的该元信息的值,它表示对应的hash键中的域值对(field-value)的数量,大小为8个字节(类型是int64_t)。
每个hash键、field、value到落盘kv的映射转换:
前面的横条对应落盘kv键值对的键部分:
- 第一个字段是一个字符’h’,表示的是hash结构的key;
- 第二个字段是hash键的字符串长度,用一个字节(uint8_t类型)来表示;
- 第三个字段是hash键的内容,因为第二个字段是一个字节,所以这里限定hash键的最大字符串长度是254个字节;
- 第四个字段是field的内容。
后面的横条代表的是落盘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的连接状态:
- No Connect,不尝试成为任何其他节点的slave;
- Connect,Slaveof后尝试成为某个节点的slave,发送trysnc命令和同步点;
- Connecting,收到master回复可以slaveof,尝试跟master建立心跳;
- Connected, 心跳建立成功;
- WaitSync,不断检测是否DBSync完成,完成后更新DB并发起新的slaveof;
全同步
Pika 支持 master/slave 的复制方式,通过 slave 端的 slaveof 命令激发:
- salve 端处理 slaveof 命令,将当前状态变为 slave,改变连接状态;
- slave的trysync线程向 master 发起 trysync,同时将要同步点传给 master;
- master处理trysync命令,发起对slave的同步过程,从同步点开始顺序发送 binlog 或进行全同步;
pika同步依赖于binlog,binlog 文件会自动或手动删除,当同步点对应的 binlog 文件不存在时,需要通过全同步进行数据同步。
需要进行全同步时,master 会将 db 文件 dump 后发送给 slave(通过 rsync 的 deamon 模式实现 db 文件的传输),实现逻辑:
- slave 向 master 发送trysnc命令(此时需要开启rsync后台服务);
- master 发现需要全同步时,判断是否有备份文件可用,如果没有先 dump 一份;
- master 通过 rsync 向 slave 发送 dump 出的文件;
- slave 用收到的文件替换自己的 db;
- slave 用最新的偏移量再次发起 trysnc;
- 完成同步;
Slave 的流程:
Master 的流程:
增量同步
一主多从的结构master节点也可以给多个slave复用一个Binlog,只不过不同的slave在binglog中有自己的偏移量而已,master执行完一条写命令就将命令追加到Binlog中,ReplicaSender将这条命令从Binlog中读出来发送给slave,slave的ReplicaReceiver收到该命令,执行,并追加到自己的Binlog中。
主要模块:
- WorkerThread:接受和处理用户的命令;
- BinlogSenderThread:负责顺序地向对应的从节点发送在需要同步的命令;
- BinlogReceiverModule: 负责接受主节点发送过来的同步命令
- Binglog:用于顺序的记录需要同步的命令
主要的工作过程:
- 当WorkerThread接收到客户端的命令,按照执行顺序,添加到Binlog里;
- BinglogSenderThread判断它所负责的从节点在主节点的Binlog里是否有需要同步的命令,若有则发送给从节点;
- BinglogReceiverModule模块则做以下三件事情:
- 接收主节点的BinlogSenderThread发送过来的同步命令;
- 把接收到的命令应用到本地的数据上;
- 把接收到的命令添加到本地Binlog里 至此,一条命令从主节点到从节点的同步过程完成;
下图是BinLogReceiverModule(在源代码中没有这个对象,这里是为了说明方便,抽象出来的)的组成,从图中可以看出BinlogReceiverModule由一个BinlogReceiverThread和多个BinlogBGWorker组成。
- BinlogReceiverThread: 负责接受由主节点传送过来的命令,并分发给各个BinlogBGWorker,若当前的节点是只读状态(不能接受客户端的同步命令),则在这个阶段写Binlog
- BinlogBGWorker:负责执行同步命令;若该节点不是只读状态(还能接受客户端的同步命令),则在这个阶段写Binlog(在命令执行之前写)
BinlogReceiverThread接收到一个同步命令后,它会给这个命令赋予一个唯一的序列号(这个序列号是递增的),并把它分发给一个BinlogBGWorker;而各个BinlogBGWorker则会根据各个命令的所对应的序列号的顺序来执行各个命令,这样也就保证了命令执行的顺序和主节点执行的顺序一致了 之所以这么设计主要原因是:
- 配备多个BinlogBGWorker是可以提高主从同步的效率,减少主从同步的滞后延迟;
- 让BinlogBGWorker在执行执行之前写Binlog可以提高命令执行的并行度;
- 在当前节点是非只读状态,让BinglogReceiverThread来写Binlog,是为了让Binglog里保存的命令顺序和命令的执行顺序保持一致.
上图是一个主从同步的一个过程(即根据主节点数据库的操作日志,将主节点数据库的改变过程顺序的映射到从节点的数据库上),从图中可以看出,每一个从节点在主节点下都有一个唯一对应的BinlogSenderThread。 (为了说明方便,我们定一个“同步命令”的概念,即会改变数据库的命令,如set,hset,lpush等,而get,hget,lindex则不是)
快照式备份
不同于Redis,Pika的数据主要存储在磁盘中,这就使得其在做数据备份时有天然的优势,可以直接通过文件拷贝实现 实现
快照内容:
- 当前db的所有文件名
- manifest文件大小
- sequence_number
- 同步点
- binlog filenum
- offset
流程
- 打快照:阻写,并在这个过程中或的快照内容
- 异步线程拷贝文件:通过修改Rocksdb提供的BackupEngine拷贝快照中文件,这个过程中会阻止文件的删除
锁的应用
应用挂起指令,在挂起指令的执行中,会添加写锁,以确保,此时没有其他指令执行。其他的普通指令在会添加读锁,可以并行访问。其中挂起指令有:
- trysync
- bgsave
- flushall
- readonly
在pika系统中,对于数据库的操作都需要添加行锁,主要在应用于两个地方,在系统上层指令过程中和在数据引擎层面。在pika系统中,对于写指令(会改变数据状态,如SET,HSET)需要除了更新数据库状态,还涉及到pika的增量同步,需要在binlog中添加所执行的写指令,用于保证master和slave的数据库状态一致。故一条写指令的执行,主要有两个部分:
- 更改数据库状态
- 将指令添加到binlog中
其加锁情况,如下图:
在图中可以看到,对同一个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作为数据库引擎而不需要修改任何代码就能使用完整的数据访问功能。
参考文档:
http://toutiao.com/a6283628736621461761/
https://github.com/Qihoo360/pika/wiki
Pika的设计及实现的更多相关文章
- 高性能kv存储之Redis、Redis Cluster、Pika:如何应对4000亿的日访问量?
一.背景介绍 随着360公司业务发展,业务使用kv存储的需求越来越大.为了应对kv存储需求爆发式的增长和多使用场景的需求,360web平台部致力于打造一个全方位,适用于多场景需求的kv解决方案.目前, ...
- pika常见问题解答(FAQ)
1 编译安装 Q1: 支持的系统? A1: 目前只支持Linux环境,包括Centos,Ubuntu: 不支持Windowns, Mac Q2: 怎么编译安装? A2: 参考编译安装wiki Q3: ...
- 大容量类Redis存储--Pika介绍
嘉宾介绍 大家好,首先自我介绍一下,我是360 web平台-基础架构组的宋昭,负责大容量类redis存储pika的和分布式存储Bada的开发工作,这是我的github和博客地址,平时欢迎指正交流^^ ...
- 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...
- 如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
阅读目录 前言 建模 实现 结语 一.前言 前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念.会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是 ...
- 设计爬虫Hawk背后的故事
本文写于圣诞节北京下午慵懒的午后.本文偏技术向,不过应该大部分人能看懂. 五年之痒 2016年,能记入个人年终总结的事情没几件,其中一个便是开源了Hawk.我花不少时间优化和推广它,得到的评价还算比较 ...
- 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...
- 如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念
一.前言 DDD(领域驱动设计)的一些介绍网上资料很多,这里就不继续描述了.自己使用领域驱动设计摸滚打爬也有2年多的时间,出于对知识的总结和分享,也是对自我理解的一个公开检验,介于博客园这个平 ...
- 如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
阅读目录 前言 明确业务细节 建模 实现 结语 一.前言 上一篇我们已经确立的购买上下文和销售上下文的交互方式,传送门在此:http://www.cnblogs.com/Zachary-Fan/p/D ...
随机推荐
- Codeforces Round #109 (Div. 1) 题解 【ABC】
A - Hometask 题意:给你一个字符串,然后再给你k个禁止挨在一起的字符串,问你最少删除多少个字符串,使得不会有禁忌的字符串对挨在一起.题目保证每个字符最多出现在一个禁忌中. 题解:由于每个字 ...
- Codeforces Round #406 (Div. 1) A. Berzerk 记忆化搜索
A. Berzerk 题目连接: http://codeforces.com/contest/786/problem/A Description Rick and Morty are playing ...
- 集合(4)—Collection之Set的使用方法
定义 set接口及其实现类–HashSet Set是元素无序且不可重复的集合,被称为集. HashSet是哈希集,是Set的一个重要实现类 set中循环只能使用foreach和iterator这两个, ...
- WTL中最简单的实现窗口拖动的方法(转)
目前,很多基于对话框的应用程序中对话框都是不带框架的,也就是说对话框没有标题栏.众所周知,窗口的移动都是通过鼠标拖动窗口的标题栏来实现的,那么现在应用程序中的对话框没有了标题栏,用户如何移动对话框呢? ...
- Internet Explorer 11:不要再叫我IE
上周,Internet Explorer 11搭载Windows 8.1预览版而来,相信很多浏览迷也已经在使用中.Internet Explorer 11 Preview 改进了与 Web 标准.其他 ...
- VS2010链接TFS遇见错误:TF204017,没有访问工作区域,需要一个或者多个必须权限
最近刚刚搭建好服务器,然后准备将VSS源代码迁移到TFS源代码管理服务器上面.在我本机先用的服务器帐号来上传初始化源代码数据库,然后我又用自己的帐号进行迁出代码的时候发生的异常. 造成上述错误,主要是 ...
- python 网络工具 scapy 介绍
作者介绍,这是个万能的网络工具,除了可以查看 TCP/IP 各层的报文,还可以发送报文.可以说是一个万能工具,作者嚣张的说, “it can replace hping, 85% of nmap, a ...
- TF_Server gRPC failed, call return code:8:Received message larger than max (45129801 vs. 4194304)
tensorflow_serving 遇到错误:gRPC failed, call return code:8:Received message larger than max (45129801 v ...
- EOF多行写入文件防止变量替换
问题描述 对多个变量及多行输出到文件,存在变量自动替换,当使用cat<<EOF不想对内容进行变量替换.命令替换.参数展开等 问题解决 转义特殊字符如 $ `等 一.对 $·\ 进行转义 c ...
- 单片机成长之路(51基础篇) - 017 C51中data,idata,xdata,pdata的区别(转)
从数据存储类型来说,8051系列有片内.片外程序存储器,片内.片外数据存储器,片内程序存储器还分直接寻址区和间接寻址类型,分别对应code.data.xdata.idata以及根据51系列特点而设定的 ...