pika 是什么呢?简单的说:pika 是 360 Web 平台部 DBA 与基础架构组合作开发的大容量类 Redis 存储,pika 的出现并不是为了替代 Redis,而是 Redis 的场景补充。pika 力求在完全兼容 Redis 协议、继承 Redis 便捷运维设计的前提下通过持久化存储的方式解决 Redis 在大容量场景下的问题,如恢复时间慢、主从同步代价高、单线程相对脆弱、承载数据较有限、内存成本高昂等。
pika 诞生背景pika 诞生是因为 Redis 使用中的一些痛点。硬件成本:
横轴是时间,纵轴是价格。
通过上图可以看到内存价格短时间内上涨一倍,即在所使用的 Redis 内存情况没有波动的情况下,成本莫名涨了一倍;这意味着,这个成本上涨的锅需要由使用者你来背。
采购的同学更加痛苦,之前可以买一台机器的预算现在内存容量必须打五折,内存容量不打折老板就把采购腿打折,如果采购不能打折开发就麻烦了,明明开心的计划要在 Redis 里装 60G 数据现在只能装 30G 了,那少了的 30G 明明找谁说理去?开发同学只能独自一人默默的加班加点改代码压缩数据量。(画外音:本来周末约了姑娘出去散心看电影什么的,现在全废了!)
综上所述,Redis 的内存是一把双刃剑(高性能 / 高成本),成本提高最终伤害到的是开发同学的代码!因此我们真的很需要一个性能“别太差”的“廉价 Redis”!
运维的代价,使用 Redis 集群的问题不知道有没有同学运维过 50G 的 Redis 集群?如果有类似场景运维经验的同学,非常希望可以和你们就如何处理主库宕机故障进行交流。这里,我首先来讲讲我们是和处理的吧。
360 的 Redis 集群结构非常简单,1 主 1 从(从库容灾)或 1 主多从(读写分离),如下图:
通常在一个 Redis 实例能轻松应对的场景下我们会使用 1 主 1 从结构,主库通过 LVS 把自己“藏起来”,而从库是纯粹的容灾功能并不提供任何服务,所以对用户是透明的,这种情况下的主库宕机灾难太好解决了:将从库升级为主库,再用这个新的主库替换掉 LVS 下的故障主库即可完成整个容灾过程,LVS 做 rs 的切换是很快的,正常整个主库切换过程不会超过 1 分钟。而对于一些压力很大的场景,我们常见的做法是读写分离(图 2),为主库挂载多个从库,主库在 LVS 后承载写入请求,而读取请求则由另一个 LVS 后端的多个从库共同支撑。在该结构中主库挂了又该怎么办呢?
要知道 Redis 集群主库发生变更后剩余从库数据要完全重传,一个 50G 的 Redis 重灌需要大概 70 分钟(硬件、网络环境好会快一点),而在重灌过程中实例是不可读写的,也就是无法提供读取服务了,那么唯一的办法就是在这个过程中将所有的读请求切至主库的 LVS 即可。但是等一下,好象哪里不对?是的!我们最初为什么要把读请求分到另一个 LVS 上?因为主库撑不住啊,这下切过来刚刚换好的新主库也因为撑不住那么大的读取请求而立即挂了,多么尴尬?
所以特别大的 Redis 在运维中简直是灾难,不挂还好一挂不得了,在这种灾难场景中,多数时间运维同学只能无助的死等从库恢复什么都做不了,而此时背后可能站着 3 个开发 2 个测试 1 个产品,压力多么的大?
这里讲一件很有趣的事:
曾经我们为了合理降低开发同学在 Redis 中存放的数据体积想了无数办法,但最终效果都很差,后来我们考虑从“源头”控制。在 360 的 HULK 云平台 2.0 版本中,最早可以申请的 Redis 最大套餐为 20G,这个套餐已经不小了,然而我们的开发同学习惯性的直接申请最大套餐(20G),后来我们没办法只能关闭了 20G 套餐的自动部署改为人工审核,结果就是我们不仅被投诉耽误开发上线时间还得乖乖通过审核。
实在没办法我们做了这样一个小改进:在申请 Redis 套餐大于 15G 的时候会弹出一个框,申请人需要在这个框里填写这个 Redis 的容量是怎么计算出来的(例如多少 key 每个 key 大概多大),之后我们发现所有的套餐申请都变成了 14G,多么奇特。
于是我们再接再厉,把这个弹框改到了套餐大于等于 8G 时出现,结果非常有趣的,之后的 90% 的套餐申请都为 7G,并且没有任何投诉我们!这心理战效果拔群。
可见在 Redis 的内存体积这一问题上,开发同学和运维同学是存在“并不尖锐”的矛盾的,大内存 Redis 可以省很多事,存更多的数据、不必太在乎过期、不用代码层拆分,而对于运维同学而言,体积越大,运维代价则越大。
如果有一个能存放巨量数据(动辄 T 级)的 Redis,无论切主还是重启都不需要重新装填数据到内存,这个矛盾点是不是就不存在了?
工作价值的体现,开发廉价的 Redis这是非业务线的开发、运维和 DBA 比较关心的事。为什么呢?因为我们这些人与那些挣钱的业务线部门相比存在感是相对较低的,甚至部分公司老板、非技术岗位的同事都不知道我们是做什么的!
这并不是歧视,因为每个公司的高层最关注的都一定会是赚钱的那几个部门 / 项目,而非业务线开发以及技术支持部门因为无法赚钱而不被关注是很正常的事,所以我们是最默默无闻的重要角色!
但是没关系,我们不能挣钱但我们可以帮公司节约成本!如果价值一定要与钱挂钩,那么省钱可能是我们最能够体现工作价值的事(之一)!有些公司每个季度都要计算运营成本,去年一季度服务器增加 200 台,而今年因为 xxx 项目的改进成功,一季度服务器 0 采购并腾出 100 台给新项目使用,这难道不惊人吗?
Redis 这么贵而我们又用了这么多。看起来这是个很不错的切入点,如果有一个很廉价的 Redis 且不用改代码(千万不要给开发同学添乱)即可接入,那简直太好了!
pika 特点 1. pika 完全兼容 Redis 协议通俗的说:pika 没有自己的客户端,连接 pika 使用 Redis 的 driver 即可,从 Redis 迁移到 pika 几乎不需要修改代码,要知道对于一个稳定的业务开发同学最讨厌的就是:“这么稳定的业务你居然叫我改代码?!”我们在做一件事的时候尽量不要给别人增加负担,如果可以的话尽量让大家都成为受益者,这是最完美的状态也就是互相成就。
2. 持久化存储 - 真对于 Redis rdb(快照式内存落地)和 aof(日志式记录)的持久化方式,pika 是真正意义上的持久化,数据自动落盘无需额外设置,所以如果服务器断电了数据怎么办?什么怎么办?数据都在硬盘上老实躺着呢,你把实例起来呗。
3. pika 非常廉价pika 本身使用磁盘作为存储介质,并自带多种压缩方式供你选择(默认是 snappy)。在实际测试中 Redis 的数据导入 pika 后只有原体积的 1/3~1/8,另外 SSD 本来也比内存便宜多了,下图是 250G SSD 的价格变化曲线,周期和之前的内存完全一样:
是不是价格变化比内存小多了?那么单 G 成本如何呢? SSD : 2.5 元 /G,而内存则达到了 60 元 /G,再加上 pika 的自带压缩特性(最少三倍),综合计算 pika 的成本只有之前的 1/72!
4. 多线程设计我相信肯定有同学遇到过一条命令搞死 Redis 的情况,例如:keys *(建议大家直接禁用该命令并用 scan 替代)、对一个巨大的 hash key 执行 hget all、执行一个巨型的 set bit 等等,因为 Redis 是单线程的,在出现这些请求的时候由于它们执行速度极其缓慢而 Redis 的每秒请求量又比较大,因此在这些请求执行完毕前其它所有请求都会被阻塞住,这个 Redis 就好像 hang 住了,此时这个 Redis 基本上就等于挂了。
而 pika 可配置式(自行设置线程数)的多线程设计可以大大降低此场景的故障率,在极端场景下 pika 存活率更高,例如一个可以让 Redis 完全 hang 住的 keys * 命令在 12 线程的 pika 中你需要连续至少发送 12 次 keys * 才可达到一样的 hang 住效果。
5. 便捷轻松pika 继承了 Redis 的便捷运维设计,同时我们对部分运维相关功能进行了一些改进,如果你有 Redis 运维经历那么 pika 可轻松上手,例如:
a) 和 Redis 完全一样的 slaveof 一键创建主从结构
b) 同样支持 Redis 的级联集群模式
c) 重启无需重新导入数据,数据秒级全自动加载
d) 同步使用二进制日志为媒介,这种持久化日志的同步方案使从库不再有 Redis 从库的“重启重灌”、“断网太久重灌”等令人头疼的问题,从库灾难恢复后可以通过灾难前的 binlog 位置继续进行同步
e) 故障切主后从库无需重灌数据,从库到新主库秒级挂载,同步通过记录的偏移量自动续传,大幅度提高故障后整个集群的恢复时间
f) 无阻塞快照式全量热备,在备份速度极快(毫秒即可完成)的同时备份对空间只有很小的占用,同时我们提供了增量备份工具
g) pika 提供多用户(管理员 / 普通用户)以及多用户“命令黑名单”功能,可以通过该功能对用户屏蔽风险命令,而管理员账户则不受限制
h)增强的管理员功能,我们认为能抢救回来就尽量不要重启,所以我们对管理员预留了连接,这使得管理员在 pika 连接数被用户用完时仍可通过 127.0.0.1 登录并对问题进行处理,例如直接执行 client kill all 命令杀死当前所有连接。
6. 监控 pika 十分容易为了不给大家添麻烦,我们使 pika 的状态参数尽量靠近 Redis,因此现有的部分 Redis 监控可以直接用于 pika,另外我们也提供了例如 qps、连接数、内存占用、磁盘占用、日志占用、与 Redis 完全一样的 monitor 命令以及可配置的慢请求记录等功能让管理者对 pika 的工作状态完全掌控。
7. 迁移到 pika 很简单我们在 tools 目录下提供了三个工具:redis_to_pika:将 Redis 的数据迁移到 pika, 它基于 aof,能够全量 + 自动增量将 aof 中的数据发送到 pika,无论是 aof 中的老数据还是后续写入的新数据,一条都不会丢。
ssdb_to_pika:将 SSDB 中的数据迁移到 pika(目前还不支持增量同步)
pika_to_redis:迁移到 pika 后请求量迅速增长,逐渐的 pika 撑不住了怎么办?没关系,可以用这个工具方便的将 pika 中的数据迁移到 Redis,同样支持增量同步!
其实我们还有很多细小的改进,但时间关系就不在本文详述,大家可以关注我们的 github,上面有所有的 change log,大家可以看看我们是多么关注 pika 的使用体验,记得 star(上周我们刚刚发布了 2.2.1,该版本增加了几个我们自己用了都觉得开心的功能):https://github.com/Qihoo360/pika
pika 核心部件:nemo 与 pink Pika 整体架构图 主要组成:网络模块 pink
线程模块
存储引擎 nemo
日志模块 binlog
基础架构团队开发的网络编程框架, 支持 pb, Redis,http 等协议.提供了对 thread 的封装, 用户能够定义不同 thread 的行为, 使用更加清晰,支持单线程模型以及多线程 worker 模型。是它让 pika 完全兼容 Redis 协议,使不改代码迁移到 pika 成为可能,大家的项目如果有需要可以直接把 pink 拿去用,它也是开源的。
github 地址:https://github.com/Qihoo360/pink
存储引擎 nemo它是 pika 的存储引擎, 基于 Rocksdb 实现. 封装 Hash, List, Set ,Zset 等数据结构。
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 的存储格式
其他的数据结构实现的方式也类似,通过将 hash_filed 拼成一个 key,存储到支持 kv 的 rocksdb 里面去, 从而实现多数据结构的结构。
更详细的说明请参考:https://github.com/Qihoo360/pika/wiki
在这里插一个 nemo 的小话题:大家都知道在 Redis 中如果删除一个巨型多数据结构(例如一个 6G 的 hash key)是很危险的操作,Redis 会立即进行内存释放并因此阻塞很久,而此时同步缓冲区一般就溢出了(除非你设置超过 6G,但这样成本高昂),直接代价是从库*重传,为了解决这个问题 pika 实现了“1ms”删除含有 100 亿个元素的多数据结构的方案。
以 hash 为例,它分为 meta_key 和 data_key,一个 hash 对应一个 meta_key,里面会记录这个 hash 的 size、version 和 ttl 信息,version 就是用来秒删的关键。
当删除一个 hash 时,只需要将这个 meta_key 对应的 version+1,这样这个 hash 原来对应的所有的 data_key 都不可用,因为他们里面保留的还是老的 version,而在每次读取时,pika 会用最新的 meta_key 对应的 version 来取拼凑并读取相应的 data_key,所以有用老 version 的 data_key 是不会被读取出来的,这样就实现了秒删。
这些老 version 的 data_key 的真正删除是交给了后台的 compaction 过程,我们定义了特定的 compactfilter,它会保证在 compaction 的过程中将老 version 或者过期的 data_key 从磁盘中缓慢彻底删除,整个过程用户是感觉不到的。
操作:Pika 从零开始 1. 搭建一个单点非常简单,编译安装完毕之后执行./pika -c pika.conf 即可启动,配置文件中的参数请参考 conf 目录下的 conf 说明文档
我们将这个节点简称为 m
2. 别忘了集群化单点是不行的,因为无法容灾,挂了就真的挂了,所以将 pika 集群化非常必要。
创建一个 pika 集群(主从模式)也很简单,在第一步的基础上再搭建一个 pika 节点(称之为 s),登录 s 后执行 slaveof m-ip m-port 即可。
s 会将 m 中的数据 clone 到本地并根据当时记录的同步偏移量进行续传,这一些都是自动的。
3. 关注 pika 的运行状态我们提供了不少运行状态信息,例如 qps、连接数、内存占用、磁盘占用、慢日志记录、monitor 命令等,大部分运行状态信息用 info 命令即可查看(注意 pika 的慢日志是记录在 pika.ERROR 日志文件中的,不会出现 Redis 的新旧覆盖问题也便于分析统计,所以 pika 没有 slow 命令)。大家在使用 pika 的时候记得多多关注这些信息,做到心中有数。
4. 一定要有备份得益于 rocksdb 的实现原理,pika 的备份类型为“无阻塞快照式”,500G 的 pika 需要多久才能备份完毕呢?大概是毫秒级!
pika 在收到 bgsave 命令之后会立即生成一个快照目录,之后将该目录拷走即可,同时该目录中的 info 文件记录了快照瞬间的同步偏移量以方便之后恢复中的同步续传工作。
而恢复一个 pika 更加简单,将该备份目录改名为 db 放至 pika 目录下直接启动即可,如果是恢复一个从库,那么在 slaveof 时准确的偏移量可参考备份目录下的 info 文件。
5. 故障处理pika 的故障处理和 Redis 有相同的地方也有不同的地方,在这里分几个场景,我做了一个简单的表格来进行对比:
6. 一些 pika 的使用心得a. pika 的性能非常依赖所部属的磁盘性能,因此我们建议将其部署在 SSD 上,虽然 SSD 是比 HDD 贵多了,但也比内存便宜多了。
b. pika 并不能替代 Redis,我们将它定义为“Redis 的场景补充”,从 Redis 迁移到 pika 我们节约了成本,但性能上也需要一些妥协,pika 的 kv 性能非常不错,我们有 8 万 qps 的支撑案例可以说无需妥协!但 pika 多数据结构由于实现方式所致,在一些巨型多数据结构 key 上(例如含有上亿元素的 hash key)时间复杂度较高的接口性能比 Redis 要差很多,大概只有 1/10。
c. 虽然 pika 是多线程的不容易被搞死,但 keys * 仍旧伤神,建议别这样用。
d. pika 的存储引擎是 rocksdb,所以有大量的 sst 文件,如果你的 pika 计划存放大量数据,记得为它配置一个较大的 open files limit。
pika 现状360 内部最新数据实例数量:已达 600+承载请求:500 亿次 / 天总数据量:5T(此处为单份数据未记入冗余部分,例如一个主从集群 2 个实例每个实例 20G 共 40G,此处只按一个实例的数据量 20G 计算),换算到 Redis 大致相当于 25T 内存
社区用户:目前已知的部分:新浪微博,garena 游戏,apus,万达飞凡网,美团网,学而思教育,小米,环信,58 同城,迅雷,高伟达,第一弹社区,亿玛科技等。用户 QQ 群:294254078
pika 下一步计划:pika-cluster?pika 帮助我们突破了 Redis 的容量瓶颈,但随着使用的增多又出现了另一个瓶颈:pika 无法突破本地磁盘容量上限(我们遇到有社区用户希望用 PIKA 存储 10T 数据,可是哪有这么大的 SSD)
为了解决这个问题社区帮助 pika 兼容了 codis,目前我们发现使用 pika-codis 的用户还真不少,所以我们可能会开发一个 pika-cluster。
它不一定是 Redis-cluster 的实现方式,但我们的目的很明确:突破 pika 的本地磁盘上限瓶颈,方案还在讨论中,请大家拭目以待!
作者介绍
杨艳杰,主业 360web 平台部资深 DBA,负责公司 kv 存储、数据仓库以及 360 HULK 云平台 DB 相关服务建设,副业 pika 产品经理。