作为一家数据智能公司,个推不仅领有海量的关系型数据,也积攒了丰盛的 key-value 等非关系型数据资源。个推采纳 Codis 保留大规模的 key-value 数据,随着公司 kv 类型数据的一直减少,应用原生的 Codis 搭建的集群所破费的老本越来越高。在一些对性能响应要求不高的场景中,个推打算采纳新的存储和治理计划以无效兼顾老本与性能。通过选型,个推引入了 360 开源的存储系统 Pika 作为 Codis 的底层存储,以替换老本较高的 codis-server,治理分布式 kv 数据集群。将 Pika 接入到 Codis 的过程并非一帆风顺,为了更好地满足业务场景需要,个推进行了系列设计和革新工作。
本文是“大数据降本提效”专题的第四篇,为大家分享个推如何完满联合 Pika 和 Codis,最终节俭 90% 大数据存储老本的实战经验。
Codis 的四大组件
在理解具体的迁徙实战之前,须要先初步意识下 Codis 的根本架构。Codis 是一个分布式 Redis 解决方案,由 codis-fe、codis-dashboard、codis-proxy、codis-server 等四个组件形成。
- 其中,codis-server 是 Codis 中最外围和根底的组件。基于 Redis 3 版本,codis-server 进行了性能扩大,但其本质上还是依赖于高性能的 Redis 提供服务。codis-server 扩大了基于 slot 的 key 存储性能(为了实现 slot 这个性能,codis-server 会额定占用超出存储数据所需的内存),并可能在 Codis 集群的不同 Group 之间进行 slot 数据热迁徙。
- codis-fe 则提供对运维比拟敌对的治理界面,不便对立治理多套的 codis-dashboard。
- codis-dashboard 负责管理 slot、codis-proxy 和 ZooKeeper(或者 etcd)等组件的数据一致性,整个集群的运维状态,数据的扩容缩容和组件的高可用,相似于 k8s 的 api-server 性能。
- codis-proxy 次要提供给业务层面应用的拜访代理,负责解析申请路由并将 key 的路由信息路由到对应的后端 group 下面。此外,codis-proxy 还有一个很重要的性能,即在通过 codis-fe 进行集群的扩缩容时,codis-proxy 会依据 group 对应的 slot 的迁徙状态触发 key 迁徙的流程,可能实现在不中断业务服务的状况下热迁徙数据,以确保业务的可用性。
Pika 接入 Codis 的挑战
咱们引入 Pika 次要是用来替换 codis-server。作为 360 开源的类 Redis 存储系统,Pika 底层选用 RocksDB,它齐全兼容 Redis 协定,并且支流版本提供 Codis 的接入能力。但在引入 Pika 以及将数据迁徙到 Codis 的过程中,咱们发现 Pika 和 Codis 的联合并非设想中完满。
问题一:语法不对立
在接入之前,咱们深刻查阅并比照了 Pika 和 Codis 源码,发现 Pika 实现的命令绝对较少,将 Pika 接入到 Codis 之后有些性能还是否失常应用有待察看。
位于 pika_command.h 头文件中的 Pika (3.4.0 版本) 源码:
//Codis Slots
const std::string kCmdNameSlotsInfo = "slotsinfo";
const std::string kCmdNameSlotsHashKey = "slotshashkey";
const std::string kCmdNameSlotsMgrtTagSlotAsync = "slotsmgrttagslot-async";
const std::string kCmdNameSlotsMgrtSlotAsync = "slotsmgrtslot-async";
const std::string kCmdNameSlotsDel = "slotsdel";
const std::string kCmdNameSlotsScan = "slotsscan";
const std::string kCmdNameSlotsMgrtExecWrapper = "slotsmgrt-exec-wrapper";
const std::string kCmdNameSlotsMgrtAsyncStatus = "slotsmgrt-async-status";
const std::string kCmdNameSlotsMgrtAsyncCancel = "slotsmgrt-async-cancel";
const std::string kCmdNameSlotsMgrtSlot = "slotsmgrtslot";
const std::string kCmdNameSlotsMgrtTagSlot = "slotsmgrttagslot";
const std::string kCmdNameSlotsMgrtOne = "slotsmgrtone";
const std::string kCmdNameSlotsMgrtTagOne = "slotsmgrttagone";
codis-server 反对的命令如下:
{"slotsinfo",slotsinfoCommand,-1,"rF",0,NULL,0,0,0,0,0},
{"slotsscan",slotsscanCommand,-3,"rR",0,NULL,0,0,0,0,0},
{"slotsdel",slotsdelCommand,-2,"w",0,NULL,1,-1,1,0,0},
{"slotsmgrtslot",slotsmgrtslotCommand,5,"w",0,NULL,0,0,0,0,0},
{"slotsmgrttagslot",slotsmgrttagslotCommand,5,"w",0,NULL,0,0,0,0,0},
{"slotsmgrtone",slotsmgrtoneCommand,5,"w",0,NULL,0,0,0,0,0},
{"slotsmgrttagone",slotsmgrttagoneCommand,5,"w",0,NULL,0,0,0,0,0},
{"slotshashkey",slotshashkeyCommand,-1,"rF",0,NULL,0,0,0,0,0},
{"slotscheck",slotscheckCommand,0,"r",0,NULL,0,0,0,0,0},
{"slotsrestore",slotsrestoreCommand,-4,"wm",0,NULL,0,0,0,0,0},
{"slotsmgrtslot-async",slotsmgrtSlotAsyncCommand,8,"ws",0,NULL,0,0,0,0,0},
{"slotsmgrttagslot-async",slotsmgrtTagSlotAsyncCommand,8,"ws",0,NULL,0,0,0,0,0},
{"slotsmgrtone-async",slotsmgrtOneAsyncCommand,-7,"ws",0,NULL,0,0,0,0,0},
{"slotsmgrttagone-async",slotsmgrtTagOneAsyncCommand,-7,"ws",0,NULL,0,0,0,0,0},
{"slotsmgrtone-async-dump",slotsmgrtOneAsyncDumpCommand,-4,"rm",0,NULL,0,0,0,0,0},
{"slotsmgrttagone-async-dump",slotsmgrtTagOneAsyncDumpCommand,-4,"rm",0,NULL,0,0,0,0,0},
{"slotsmgrt-async-fence",slotsmgrtAsyncFenceCommand,0,"rs",0,NULL,0,0,0,0,0},
{"slotsmgrt-async-cancel",slotsmgrtAsyncCancelCommand,0,"F",0,NULL,0,0,0,0,0},
{"slotsmgrt-async-status",slotsmgrtAsyncStatusCommand,0,"F",0,NULL,0,0,0,0,0},
{"slotsmgrt-exec-wrapper",slotsmgrtExecWrapperCommand,-3,"wm",0,NULL,0,0,0,0,0},
{"slotsrestore-async",slotsrestoreAsyncCommand,-2,"wm",0,NULL,0,0,0,0,0},
{"slotsrestore-async-auth",slotsrestoreAsyncAuthCommand,2,"sltF",0,NULL,0,0,0,0,0},
{"slotsrestore-async-select",slotsrestoreAsyncSelectCommand,2,"lF",0,NULL,0,0,0,0,0},
{"slotsrestore-async-ack",slotsrestoreAsyncAckCommand,3,"w",0,NULL,0,0,0,0,0},
此外,codis-server 和 Pika 反对的语法也有所不同。例如,如果要查看某一节点上 slot 1 的详细信息,Codis 与 Pika 执行的命令别离如下:也就是说,咱们必须在 codis-fe 层命令调度与治理性能方面加上对 Pika 语法格局的反对。
针对此问题,咱们在 codis-dashboard 层中,通过批改局部源码逻辑,实现了对 Pika 主从同步、主从晋升等相干命令的反对,从而实现了在 codis-fe 层面的操作。
问题二:未胜利实现数据迁徙
实现了以上操作之后,咱们便开始将 kv 数据迁徙到 Pika。而后,问题来了,咱们发现尽管 codis-fe 界面上显示数据均已迁徙实现,但实际上要迁徙的数据并未被迁徙到对应的集群。在 codis-fe 界面上,咱们也未查看到显著的报错信息。
到底为何呈现此问题呢?
咱们持续查看了 Pika 无关 slot 的源码:
void SlotsMgrtSlotAsyncCmd::Do(std::shared_ptr<Partition> partition) {
int64_t moved = 0;
int64_t remained = 0;
res_.AppendArrayLen(2);
res_.AppendInteger(moved);
res_.AppendInteger(remained);
}
咱们发现,在日常的运行状况下,通过 codis-dashboard 发送给 Pika 的指令就是胜利返回,这样 codis-dashboard 在迁徙时立马就收到了胜利的信号,而后就间接将迁徙状态批改为胜利,而其实此时数据迁徙并没有被真的执行。
针对这种状况,咱们查阅了无关 Pika 的官网文档 Pika 配合 Codis 扩容案例。
从官网的文档来看,这种迁徙计划是一种可能会丢数据的有损计划,咱们须要依据本身状况来从新设计和调整迁徙计划。
1. 设计开发 Pika 迁徙工具
首先,依据 Codis 的数据扩缩容原理,咱们参考 codis-proxy 的架构设计,应用 Go 语言自行设计并开发了一套 Pika 数据迁徙工具,目标是实现以下性能需要:
- 将 Pika 迁徙工具伪装成一个 Pika 实例接入 Codis 并提供服务。
- 把 Pika 迁徙工具作为一个流量转发工具,相似于 codis-proxy,可能将对应 slot 的申请转发到指定的 Pika 实例下面,从而保障迁徙过程中的业务可用性。
- 使 Pika 迁徙工具可能感知到迁徙过程中的主从同步状况,在主从实现的状况下可主动从节点断开,并将新增数据写入新集群,从而在流量散发过程中全力保证数据一致性。
2. 应用 Pika 迁徙工具进行数据的热迁徙
依据如上需要实现 Pika 迁徙工具的设计开发后,咱们就能够应用该工具对数据进行热迁徙。
迁徙过程如下:
Step1: 集群原始状态
通过下图,能够看到,咱们须要将 801-1023 中 901-1023 区间的 slot 信息迁徙到新组件即 Group4 上,作为新实例提供服务。
Step2: 将 Pika 迁徙工具接入 Codis 提供服务
在 Pika 迁徙工具接入 Codis 之前,咱们需将 Group3 中待迁徙的 901-1023 作为 Group4 的主节点,并进行主从数据同步。此时 Group3 的 901-1023 作为主,Group4 的 901-1023 作为从。在实现该步骤之后就可将 Pika 迁徙工具接入 Codis。首先将 801-1023 的 slot 信息迁徙到 Pika 迁徙工具。此时 Pika 迁徙工具将 801-900 的读写信息写入 Group3。在 Pika 迁徙工具中,将 901-1023 的读写信息同时指向 Group4 和 Group3。而后进入下一步。
Step3: 主从同步数据并动静切换主从
此时 Pika 迁徙工具曾经实现接入,它将转发 801-1023 的 slot 申请到后端。这里须要留神,Pika 迁徙工具在解决写流量时,会查看主从同步是否实现。如果主从同步实现,Pika 迁徙工具会间接将 Group4 中 Pika 实例的从断掉,并将新数据写入到 Group4 中,否则就持续将写入的数据路由到 Group3。如果是读流量,Pika 迁徙工具会先尝试获取 Group4 的数据,如果获取到则返回,否则就去 Group3 获取数据。如果 901-1023 的 slot 中没有写流量,则无奈判断该 slot 主从同步是否实现以及是否要断开主从,那么咱们能够向 Pika 迁徙工具发送针对该 slot 的命令来执行该操作。直到 Group4 中所有 slot 的主从同步实现且主从断开,方进行下一步。
下图比拟形象地展现了 Pika 迁徙工具的作业逻辑:
Step4: 将待迁徙的 slot 迁入新的 Group
在实现步骤 3 之后,再将 Pika 迁徙工具的 slot 信息,即 801-900,迁徙回 Group3,将 901-1023 迁徙到 Group4。将 901-1023 齐全迁徙到 Group4 之后,就可将原来 Group3 中冗余的旧数据删除。
至此,咱们通过 Pika 迁徙工具实现了对 kv 集群的扩容。
这里须要阐明的是,Pika 迁徙工具的大部分性能和 codis-proxy 类似,只不过须要将对应的路由规定进行转换,并增加上反对 Pika 的语法指令。之所以可能如此设计实现,是因为在 codis-proxy 的迁徙过程中产生的都是原子性命令的操作,从而可能在 Pika 迁徙工具这一层拦挡指标端的数据,并动静地将数据写入到对应的集群中。
计划成果实测
通过以上一系列的操作之后,咱们胜利应用 Pika 替换了原有的 codis-server。那么咱们事后的兼顾老本与性能的指标是否有达成呢?
首先,在性能方面,依据线上业务方的应用反馈,以后总体的业务服务 p99 值为 250 毫秒(包含对 Codis 和 Pika 的屡次操作),可能满足以后现网对性能的需要。
再看老本方面,因为存储的 key 的数据结构相似,占用的理论物理空间基本相同。通过将 Pika 的数据转换成 codis-server 的存储量,内存应用大略为 24/482 = 480G 的内存空间。依据以后的运维教训,如果理论存储 480G 的数据,依照每个节点存储 10G 数据,单节点最大 15G,须要 48 个节点,即须要 256G 6 台机器(3 主 3 从)提供服务。
这样咱们就能够得出结论:存储等同容量的数据,应用 Pika 的破费老本仅为 Codis 的 5~10%!
真挚的选型倡议
咱们还对 Pika 的单实例与 Redis 的单实例进行了性能压测比照。
压测命令为 redis-benchmark -r 1000000000 -n 1000 -c 50 时,性能体现如下:
压测命令为 redis-benchmark -r 1000000000 -n 1000 -c 100 时,性能体现如下:
从测试环境的压测后果来看,相对而言,单实例压测状况下,Redis 体现占优;应用 Pika 的场景倡议为 kv 类型性能较好,在五种数据结构外面举荐应用 String 类型。
综合压测数据和现网状况,咱们对 Codis + codis-server 和 Codis + Pika 两种技术栈的优缺点进行了总结:
针对如上比照,咱们的选型倡议如下:
总结
以上是个推应用 Pika 替换 codis-server,以低成本实现海量 kv 数据存储与读写的实战过程。个推《大数据降本提效》专栏还将继续关注性能与老本的均衡之道,心愿咱们的实战经验能帮忙大数据从业者们更快地找到大数据降本提效的最优解。