嘉宾介绍
大家好,首先自我介绍一下,我是360 web平台-基础架构组的宋昭,负责大容量类redis存储pika的和分布式存储Bada的开发工作,这是我的github和博客地址,平时欢迎指正交流^^
我的github: https://github.com/KernelMaker
我的博客: http://kernelmaker.github.io
下面是pika的github,欢迎关注
https://github.com/Qihoo360/pika
Pika介绍
pika是360 DBA和基础架构组联合开发的类redis存储系统, 使用Redis协议,兼容redis绝大多数命令(String,Hash,List,ZSet,Set),用户不需要修改任何代码, 就可以将服务迁移至pika.
pika主要是使用持久化存储来解决redis在内存占用超过50G,80G时遇到的如启动恢复时间长,主从同步代价大,硬件成本贵等问题,并且在对外用法上尽可能做到与redis一致,用户基本上对后端是redis或pika无感知
既然pika要做到兼容redis并解决redis在大容量时的各种问题,那么首先要面对的问题便是如何从redis迁移到pika,毕竟现在redis的使用非常广泛,如果从redis迁移到pika很麻烦,那应该也不会有多少人用了
-
从redis迁移到pika需要经过几个步骤?
开发需要做的:
基本不用做任何事情
dba需要做的:
1.dba迁移redis数据到pika 2.dba将redis的数据实时同步到pika,确保redis与pika的数据始终一致 3.dba切换lvs后端ip,由pika替换redis 注:pika提供redis_to_pika工具,通过aof来将db和实时增量数据同步到pika
迁移过程中需要停业务/业务会受到影响吗:
不会
-
注
-
由于pika的数据存在硬盘上,故单线程的性能肯定不如redis, 但pika使用多线程来尽可能的弥补数据读写性能较之redis内存读写的差异, 线程数比较多的情况下, 某些数据结构的性能会优于redis
-
pika肯定不会是一个完全优于redis的方案, 和redis相比也有弊端,只是在某些场景下面更适合. 所以目前公司内部redis, pika 是共同存在的方案, DBA会根据业务的场景挑选合适的方案
-
本次分享分成6个部分
- 背景
- 为什么做pika(大容量redis遇到的问题)
- pika架构
- pika的创新及优化
- pika的优势及不足
- 总结
背景
redis提供了丰富的多数据结构的接口, 在redis之前, 比如memcache,都认为后端只需要存储kv的结构就可以, 不需要感知这个value里面的内容, 用户需要使用的话通过json_encode
, json_decode
等形式进行数据的读取就行. 但是其实redis做了一个微创新, 提供了多数据结果的支持, 让服务端写代码起来更加的方便了
因此redis在公司的使用率也是越来越广泛, 用户不知不觉把越来越多的数据存储在redis中, 随着用户的使用, DBA发现有些redis实例的大小也是越来越大, 发现在redis实例内存使用比较大的情况下, 遇到的问题也会越来越多, 因此和我们一起实现了大容量redis的解决方案
最近半年公司每天redis 的访问情况
redis 架构方案
为什么做pika(大容量redis遇到的问题)
- 恢复时间长
我们线上的redis一般同时开启rdb和aof. 我们知道aof的作用是实时的记录用户的写入操作, rdb是redis某一时刻数据的完整快照. 那么恢复的时候一般是通过rdb+aof的方式进行恢复, 根据我们线上的情况50G redis恢复时间需要差不多40~70分钟(取决于服务器性能)
- 一主多从, 主从切换代价大
redis在主库挂掉以后, 从库升级为新的主库. 那么切换主库以后, 所有的从库都需要跟新主做一次全同步, 代价非常大
- 缓冲区写满问题
为了实现部分同步,redis使用了repl_backlog
来缓存部分同步命令,repl_backlog
默认1M。 当主从之间网络有故障, 同步出现延迟了大于1M以后, slave丢失了master的同步点,就会触发全同步的过程. 如果多个从库同时触发全同步的过程, 在全同步的过程中,redis会将同步点之后的增量请求给每一个slave缓存一份,在写入量大的情况下很容易就将主库给拖死,当然你也可以把repl_backlog
调大来缓解,比如2G,不过对全内存的redis而言,这2G的内存代价也不小
- 内存太贵
我们一般线上使用的redis机器是64G, 96G. 我们只会使用80%的空间.
如果一个redis的实例是50G, 那么基本一台机器只能运行一个redis实例. 因此特别的浪费资源
总结: 可以看到在redis比较小的情况下, 这些问题都不是问题, 但是当redis容量上去以后. 很多操作需要的时间也就越来越长了
pika 整体架构
主要组成:
1. 网络模块 pink 2. 线程模型 3. 存储引擎 nemo 4. 日志模块 binlog 5. 主从同步模块
pink 网络模块
* 基础架构团队开发网络编程框架, 支持pb, redis等等协议. 提供了对thread的封装, 用户定义不同thread的行为, 使用更加清晰 * 支持单线程模型, 多线程worker模型 * github 地址: https://github.com/baotiao/pink
线程模型
pika使用的是多线程模型,使用多个工作线程来进行读写操作,线程分为11种:
PikaServer:主线程
DispatchThread:监听端口1个端口,接收用户连接请求
ClientWorker:存在多个(用户配置),每个线程里有若干个用户客户端的连接,负责接收处理用户命令并返回结果,每个线程执行写命令后,追加到binlog中
Trysync:尝试与master建立首次连接,并在以后出现故障后发起重连
ReplicaSender:存在多个(动态创建销毁,本master节点挂多少个slave节点就有多少个),每个线程根据slave节点发来的同步偏移量,从binlog指定的偏移开始实时同步命令给slave节点
ReplicaReceiver:存在1个(动态创建销毁,一个slave节点同时只能有一个master),将用户指定或当前的偏移量发送给master节点并开始接收master实时发来的同步命令,在本地使用和master完全一致的偏移量来追加binlog,然后分发给多个BinlogBGWorker中的一个来执行
BinlogBGWorker:存在多个(用户配置),ReplicaReceiver将命令按key取hash分配给其中的一个BinlogBGWorker,它负责真正执行命令
SlavePing:slave用来向master发送心跳进行存活检测
HeartBeat:master用来接收所有slave发送来的心跳并回复进行存活检测
bgsave:后台dump线程
scan:后台扫描keyspace线程
purge:后台删除binlog线程
存储引擎 nemo
pika的存储引擎是基于Rocksdb实现的. 封装了String,Hash, List, ZSet, Set等数据结构
我们知道redis是需要支持多数据结构的, 而rocksdb只是一个kv的接口, 那么我们如何实现的呢?
比如对于Hash数据结构:
对于每一个Hash存储,它包括hash键(key),hash键下的域名(field)和存储的值 (value).
nemo的存储方式是将key和field组合成为一个新的key,将这个新生成的key与所要存储的value组成最终落盘的kv键值对。同时,对于每一个hash键,nemo还为它添加了一个存储元信息的落盘kv,它保存的是对应hash键下的所有域值对的个数。
每个hash键、field、value到落盘kv的映射转换
每个hash键的元信息的落盘kv的存储格式
比如对于List 数据结构:
顾名思义,每个List结构的底层存储也是采用链表结构来完成的。对于每个List键,它的每个元素都落盘为一个kv键值对,作为一个链表的一个节点,称为元素节点。和hash一样,每个List键也拥有自己的元信息。
每个元素节点对应的落盘kv存储格式
每个元信息的落盘kv的存储格式
其他的数据结构实现的方式也类似, 通过将数据结构拆分为一个个独立的KV, 存储到rocksdb 里面去. 从而实现多数据结构的结构
日志模块 binlog
pika的主从同步是使用Binlog来完成的.
binlog 本质是顺序写文件, 通过Index + offset 进行同步点检查.
解决了同步缓冲区太小的问题
支持全同步 + 增量同步
master执行完一条写命令就将命令追加到Binlog中,ReplicaSender将这条命令从Binlog中读出来发送给slave,slave的ReplicaReceiver收到该命令,执行,并追加到自己的Binlog中.
当发生网络闪断或slave挂掉重启时, slave仅需要将自己当前的Binlog Index + offset 发送给master,master找到后从该偏移量开始同步后续命令
为了防止读文件中写错一个字节则导致整个文件不可用,所以pika采用了类似leveldb log的格式来进行存储,具体如下:
主从同步模块
上图是一个主从同步的一个过程(即根据主节点数据库的操作日志,将主节点数据库的改动同步到从节点的数据库上),从图中可以看出,每一个从节点在主节点下都有一个唯一对应的BinlogSenderThread
主要模块:
WorkerThread:接受和处理用户的命令; BinlogSenderThread:负责顺序地向对应的从节点发送在需要同步的命令; BinlogReceiverModule: 负责接受主节点发送过来的同步命令 Binglog:用于顺序的记录需要同步的命令
主要的工作过程:
1.当WorkerThread接收到客户端的命令,按照执行顺序,添加到Binlog里; 2.BinglogSenderThread判断它所负责的从节点在主节点的Binlog里是否有需要同步的命令,若有则发送给从节点; 3.BinglogReceiverModule模块则做以下三件事情: a. 接收主节点的BinlogSenderThread发送过来的同步命令; b. 把接收到的命令应用到本地的数据上; c. 把接收到的命令添加到本地Binlog里 至此,一条命令从主节点到从节点的同步过程完成
BinLogReceiverModule的工作过程:
上图是BinLogReceiverModule的组成,从图中可以看出BinlogReceiverModule由一个BinlogReceiverThread和多个BinlogBGWorker组成。
BinlogReceiverThread:负责接受由主节点传送过来的命令,并分发给各个BinlogBGWorker,若当前的节点是只读状态(不能接受客户端的同步命令),则在这个阶段写Binlog
BinlogBGWorker:负责执行同步命令;若该节点不是只读状态(还能接受客户端的同步命令),则在这个阶段写Binlog(在命令执行之前写)
BinlogReceiverThread接收到一个同步命令后,它会给这个命令赋予一个唯一的序列号(这个序列号是递增的),并把它分发给一个BinlogBGWorker;而各个BinlogBGWorker则会根据各个命令的所对应的序列号的顺序来执行各个命令,这样也就保证了命令执行的顺序和主节点执行的顺序一致了
之所以这么设计主要原因是:
-
配备多个BinlogBGWorker是可以提高主从同步的效率,减少主从同步的滞后延迟;
-
让BinlogBGWorker在执行执行之前写Binlog可以提高命令执行的并行度;
-
在当前节点是非只读状态,让BinglogReceiverThread来写Binlog,是为了让Binglog里保存的命令顺序和命令的执行顺序保持一致;
综上所述,正是因为这样的架构及实现,pika可以较好的解决上面说到redis在大数据量下的不足:
-
恢复时间长
pika的存储引擎是nemo, nemo使用的是rocksdb, rocksdb启动不需要加载全部数据, 只需要加载recover log文件就可以启动, 因此恢复时间非常快 -
一主多从, 主从切换代价大
在主从切换的时候, 新主确定以后, 从库会用当前的偏移量尝试与新主做一次部分同步, 如果部分同步不成功才做全同步. 这样尽可能的减少全同步次数 -
缓冲区写满问题
pika不是用内存buffer进行同步数据的缓存, 而是记录在本地的binlog上, binlog的大小可配,远远大于内存可以使用的上限,因此不会出现把缓冲区写满的问题,减少无用的全同步次数 -
内存昂贵问题
pika的存储引擎nemo使用的是rocksdb, rocksdb同时使用内存和磁盘减少对内存的依赖. 同时我们尽可能使用SSD盘来存放数据, 尽可能跟上redis的性能.
pika的创新及优化
多数据结构key的快速删除
以Hash为例,redis一个Hash key可能包含百万或者千万的field,对于Hash key的删除,redis首先从db dict中删除掉这个key,然后立刻返回,该key对应的hash空间采用惰性的方式来慢慢回收,而我们知道,pika的是将Hash结构转换成一个个KV来存储的,删除一个Hash Key就等于要删除其对应的千万field,此时用户的一个del操作等价于引擎千万次的del操作,当初做的时候我们有如下考量:
Solution 1:阻塞删除,这也是目前其他类似Pika项目的主要解决方案,直到把Hash key对应的所有field key全部删除才返回给用户 优点:易实现 缺点:阻塞处理,影响服务 Solution 2:删除meta key之后立刻返回,其他的field key后台起线程慢慢删除 优点:速度快 缺点:使用场景受限,如果用户删除某个Hash key之后又立刻插入这个key,则此时还未删除的field key会被无当做新key的一部分,出错
上述两种方案皆不可行,我们最终在rocksdb上做了改动,使它支持多数据结构版本的概念
最终解决方案: Hash Key的元信息增加版本,表示当前key的有效版本; 操作: Put:查询元信息,获得key的最新版本,后缀到val; Get:查询元信息,获得key的最新版本,过滤低版本的数据; Del:key的元信息版本号+1即可; Iterator: 迭代时,查询key的版本,过滤旧版本数据; Compact:数据的实际删除是在Compact过程中,根据版本信息过滤;
通过对rocksdb的修改,pika实现了对多数据结构key的秒删功能,并且将真正的删除操作交给了compact来减少显示调用引擎del造成的多次操作(插入del record及compact)
快照式备份
不同于Redis,Pika的数据主要存储在磁盘中,这就使得其在做数据备份时有天然的优势,可以直接通过文件拷贝实现
流程:
打快照:阻写,并在这个过程中或的快照内容
异步线程拷贝文件:通过修改Rocksdb提供的BackupEngine拷贝快照中文件,这个过程中会阻止文件的删除
这样的备份速度基本等同于cp的速度,降低了备份的代价
后续优化:不过目前pika正在尝试使用硬链建立checkpoint来实现数据的更快备份(秒级),并且减少备份数据的空间占用(从之前的2倍优化到不到2倍),更好的支持超大容量存储
过期支持
redis的过期是通过将需要过期的key在多存一份,记录它的过期时间,然后每次读取时进行比较来完成的,这样的实现简单,但基于内存的读写都很快不会有性能问题,目前其他类似pika的开源项目也采用这样的方式,将过期key在db多存一份,不过不同于redis,这些项目的db也是落盘,采用这样简单粗暴的方式无形中又多了一次磁盘读,影响效率,那么pika是如何解决的呢?
pika通过给修改rocksdb Set、Get接口并且新增compact filter,给每个value增加ttl后缀,并且在Get的时候来进行过滤,将真正的过期删除交给compact(基于ttl来Drop),在磁盘大容量的前提下,使用额外空间来减少磁盘读取次数,提高效率
空间回收
rocksdb 默认的compact 策略是在写放大, 读放大, 空间放大的权衡. 那么DBA同学当然希望尽可能减少空间的使用, 因此DBA希望能够随时触发compact, 而又尽可能的不影响线上的使用, 而rocksdb 默认的手动compact 策略是最高优先级的, 会阻塞线上的正常流程的合并, 因此我们修改了rocksdb compact的部分逻辑,低优先级手动compact优先级,使得自动compact可以打断手动compact,来避免level 0文件数量过多而造成的rocksdb主动停写. pika支持DBA随时compact
方便的运维
pika较之其他类似开源项目,还有一个优势就是它可以方便的运维,例如
1. pika的binlog可以配置按个数或者按天来删除,提供工具来支持不用再启实例来进行指定节点binlog的实时备份,支持binlog恢复数据到指定某一秒(正在做) 2. 支持info命令来查看后台任务的执行状态(bgsave,purgelogs,keyscan) 3. 支持monitor 4. 支持通过redis aof和monitor来迁移数据 5. 支持config set来动态修改配置项 6. 支持多用户(admin及普通用户)及命令黑名单,可以禁止掉不想让普通用户使用的命令 7. 支持不活跃客户端的自动删除,支持慢日志,client kill all,readonly开关 8. 支持快照式备份及手动compact 9. 等等...
pika在追求尽可能高的性能及稳定性的同时,还注重使用者的使用体验,一个产品即使拥有再给力的性能如果不可运维我想也不会有人想用,所以pika会不断发现并解决使用上的问题,使得它更好用
pika的优势及不足
pika相对于redis,最大的不同就是pika是持久化存储,数据存在磁盘上,而redis是内存存储,由此不同也给pika带来了相对于redis的优势和劣势
优势:
- 容量大:Pika没有Redis的内存限制, 最大使用空间等于磁盘空间的大小
- 加载db速度快:Pika 在写入的时候, 数据是落盘的, 所以即使节点挂了, 不需要rdb或者aof,pika 重启不用重新加载数据到内存而是直接使用已经持久化在磁盘上的数据, 不需要任何数据回放操作(除去少量rocksdb自身的recover),这大大降低了重启成本。
- 备份速度快:Pika备份的速度大致等同于cp的速度(拷贝数据文件后还有一个快照的恢复过程,会花费一些时间),目前已经开发完更快更省空间的秒级备份,即将投入使用,这样在对于百G大库的备份是快捷的,更快的备份速度更好的解决了主从的全同步问题
劣势:
由于Pika是基于内存和文件来存放数据, 所以性能肯定比Redis低一些, 但是我们一般使用SSD盘来存放数据, 尽可能跟上Redis的性能。
总结
如果用户的业务场景数据比较大,Redis会出现上面说到的那些问题,如果这些问题对用户来说不可容忍,那么可以考虑使用pika。
我们对pika整体进行了性能测试,结果如下:
服务端配置:
处理器:24核 Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz 内存:165157944 kB 操作系统:CentOS release 6.2 (Final) 网卡:Intel Corporation I350 Gigabit Network Connection
客户端配置:
同服务端
测试结果:
pika配置18个worker,用40个客户端;
1. 写性能: 方法:客户端依次执行set、hset、lpush、zadd、sadd接口写入数据,每个数据结构10000个key; 结果:qps 110000 2. 读性能: 方法:客户端一次执行get、hget、lindex、zscore、smembers,每个数据结构5000000个key; 结果:qps 170000
单数据结构性能用户可以依据自己的需求来测试,数据结构间的性能比较大致是:
String > Hash = Set > ZSet > List
在实际使用中,大多数场景下pika的性能大约是Redis的50%~80%,在某些特定场景下,例如range 500,pika的性能只有redis的20%,针对这些场景我们仍然在改进
在360内部使用情况:
粗略的统计如下:
实例数160个 当前每天承载的总请求量超过100亿 当前承载的数据总量约3TB
wiki
github 地址:
https://github.com/Qihoo360/pika
github wiki:
https://github.com/Qihoo360/pika/wiki/pika介绍
Q&A
Q1:元信息是跟key一起存储的吗?
A1:是的
Q2:快照的生成依赖于什么?会阻塞读写吗?
A2:快照是后台生成的,阻塞的地方只是在最开始取当前db状态和同步点信息的时候,非常短
Q3:为什么能做到基本上不阻塞?
A3:因为pika的数据本来就是存在磁盘上的,备份就等同于文件拷贝.只要在拷贝前计算一下该拷贝那些文件,然后就可以后台搞了
Q4:看hashset的实现,只记录那些信息,是怎么处理hgetall的
A4:比如hash有5个field,那么在pika存储,除了元信息之外,真正数据是这么存的:key+field1 -> value1,key+field2-> value2等等,在hgetall的时候,只需要通过rocksdb的iterator,seek到key,然后迭代便可取出所有的field了
Q5:当存储超过SSD空间后,怎么扩容呢?
A5: pika受制于底下引擎的限制,不支持扩容,如果连ssd都不够用了,就只能靠挂lvm来解决了
Q6:那新的数据如何融合到db里
A6:只需要给新的实例启动前,配置文件db路径为备份的目录,然后启动新实例即可
Q7:依赖于rocksdb_backup机制?
A7:是的,目前是基于rocksdb_backup做的,不过我们最新的秒备份已经差不多做完了,可以直接通过硬链接省去文件的拷贝,以达到更快的备份速度及更少的空间占用
Q8:dispatch 和 clientWorker 这种生产者消费者模型对性能的影响。
A8:clientworker之间是互不影响的,dispatch在把某个连接分配给其中一个worker时仅与worker有极小概率的冲突(dispatch已分发完一轮连接,又回到这个worker,并且该worker此时也在操作自己的任务队列),这样的模型是比较通用的做法(与MC类似),性能瓶颈不会出现在这里
Q9:对hash类型的读取、修改、增加、删除任意部分field的过程及性能。
A9:性能比string接口稍有下降,因为多了一个元信息的读写,过程的话无非就是先读元信息,在读写真正的数据等等,说起来比较多,感兴趣的话可以下来交流^^
Q10:binlog有对操作做可重入处理吗
A10:binlog里记录的就是用户发来的redis命令,所以不是幂等的
Q11:存放元数据的那个key如何不成为瓶颈
A11: 这样的设计元信息的key的确会成为瓶颈,不过对于比较热的元数据key,频繁更新会让他驻留在rocksdb的memtable中,这样可以弥补一下读写性能
Q12:请问讲师使用的是redis cluster吗?为何只有一个master并且50g那么大?在截图那个qps下,会遇到什么瓶颈?
A12: 目前在公司redis cluster的应用不是很广泛,所以大的业务很容易将redis内存撑到50G,截图的qps,根据上面的介绍,性能梯度是String>Hash=Set>ZSet>List,之所以可以到达10w+的qps,也是因为String,Hash,Set这样的接口操作和redis相比差不多,在worker数多的情况下,String甚至还高于redis,所以整体上看qps还不错,不过对于List这样的数据结构,实现中就是基于kv来做的list,所以性能低于前面的数据结构
Q13:在多线程情况下,对事务的处理是怎么实现的
A13:pika上层会对写操作加行锁,来确保对同一个key的写db和写binlog不被打断
Q14:hashset每次hset都要读取count值吗
A14:是的,因为版本号的原因,每一次都需要读取元信息
Q15:binlog可以理解为redis的aof文件么?存储的都是命令日志记录?
A15:是的,和aof差不多,存储的都是用户命令,不过除了做aof的功能,在主从同步中它也承担了redis中repl_backlog的功能