首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

携程Redis治理演进之路

一、背景

携程 Redis 集群规模和数据规模在过去几年里快速增长,我们通过容器化解决了 Redis 集群快速部署的问题,并根据实际业务进行的一系列尝试,比如二次调度,自动化漂移等,在内存超分的情况下保证了宿主机的可靠性。

扩缩容方面,我们主要通过垂直扩缩容的方式解决 Redis 集群容量的问题,但随着集群规模扩大,这种方式逐渐遇到了瓶颈。一方面,单个 Redis 实例过大,会带来较大的运维风险和困难;另一方面,宿主机容量有上限,不能无止境的扩容。考虑到运维便利性和资源利用率的平衡,我们希望单个 Redis 实例的上限为 15GB。但实际操作中却很难做到:a. 某些业务发展很快,经常性需要给 Redis 进行扩容,导致单个实例大小远超 15GB;b. 一些业务萎缩,实际使用量远低于初始申请的量,造成资源的浪费。

如何有效控制 Redis 实例大小呢?接下来本文将带着这个问题,逐步讲解携程 Redis 治理和扩缩容方面的演进历程。

二、Redis 水平扩分拆

在携程开始使用 Redis 很长一段时间里,一直只有垂直扩缩容,原因有两点:

第一,一开始业务规模比较小,垂直扩缩容可以满足需求。垂直扩缩容对于 Redis 来说只是 Maxmemory 的配置更改,对业务透明;

第二,水平拆分/扩缩容的实现难度和成本较高。

之前文章《携程Redis治理演进之路》中已经提到,携程访问所有的 Redis 集群使用的是自主研发的 CRedis,而部署在应用端的 CRedis 通过一致性 hash 来访问实际承载数据的 Redis 实例。但一致性 hash 是无法支持直接水平扩缩容的。因为无论增加一个节点或者删除一个节点,都会导致整个 hash 环的调整。

图 1

如图所示,假设原始有 4 个分片(图 1)。当添加一个节点后,它会导致某一部分的 key 本来是写到 nodeC 上而现在会被写到 nodeE 上,也就是无法命中之前的节点。从客户端的角度来看,key 就像是丢失了。而变动的节点越多,key 丢失的也越多,假设某个集群从 10 分片直接添加到 20 分片,它直接会导致 50%的 key 丢失。删除一个节点同理,就不再赘述。

因此尽管一致性 hash 是个比较简单优秀的集群方案,但无法直接水平扩容一直困扰着运维和架构团队。为此,CRedis 团队在 2019 年提出了水平拆分的方案。

CRedis 水平分拆的思路比较朴素,因为在一致性 hash 同一个水平位置增加节点会导致数据丢失,那么不改变原来层次节点的 hash 规则,以某个节点为 hash 的起点,再来进行一次一致性 hash,演变成树的结构(图 2)。

图 2

如上图所示,将树形结构从一层拓展成二层,如果继续拆分新的叶子 Group,则可以将树形结构拓展到三层,拆分方案可以支持到十层。叶子 Group 是物理分片,直接对应的 Redis 实例,分支 Group 是虚拟分片,当 Hash 命中到分支 Group 后,并没有找不到对应的 Redis 实例,需要再继续向下寻找,直到找到叶子 Group 为止。

图 3

CRedis 水平分拆上线后,DBA 将现存的绝大部分超过 15G 的实例都拆分成更小的实例,在一段时间内缓解了大内存实例的运维治理压力。但随着 Redis 规模的快速增长,不断有大的实例集群出现,此外 CRedis 水平分拆的缺点也逐渐暴露出来:

1)持续的周期很长,对多个 Group 进行拆分的话,每个 Group 的数据需要同时复制几份同样的实例。比如 60G 的某个实例(图 3),如果想拆到 5G 一个,那么下级的 Group 必须有 12 个,而拆分要先将该实例的数据先同步为 12 个 60G 的实例,再根据 key 的命中规则清理该 12 个 60G 的实例中不会命中的 key,最终演变成 12 个 5G 的实例。一般 60G 的 group 实例拆分需要 3 个小时-6 个小时,如果一个集群的分片非常多,加上观察对业务影响的时间,可能要持续上几天或一两周,并且只能是有人值守的串行操作。

2)拆分过程中需要 2 次迁移,如上面所说的,拆分中中间态实例对于内存的要求是非常大的,拆分完成后对内存的需求会急剧下降,因此每次拆分都涉及到 2 次迁移,尽管迁移不会影响业务,但对于执行操作拆分的运维人员来说,心智负担比较大,而且一不小心也会导致线上事故。

3)拆分后无法还原回去,也就是说假设业务分拆后收缩,对 Redis 的需求变小了,但它实际拆分后的分片还在那边,所申请的空间还并没有释放掉,客观上浪费了资源,降低了 Redis 总体的利用率。

4)只支持扩容,不支持缩容,这点上面也提到了,除了一些集群过大需要分拆外,还有一些申请远超需求的实例需要缩容,而水平分拆对于这点无能为力。

5)拆分一次,就多一次的性能损耗,因为需要多计算一次 hash,虽然耗时不大,但是对于性能敏感的业务还是有影响。

由此可见,水平分拆的方案虽然解决了实例过大的问题,但不能缩容的弊端也逐渐凸现了出来。尤其是在今年因疫情影响需要降本增效的背景下,一方面资源比充足,一方面宿主机上跑的都是无法缩容的实例。那么是否有更好的解决方案呢?答案是有的。

三、Redis 水平扩缩容

3.1 设计思路

图 4

既然缩分片比较困难,我们首先想到的是业务双写集群的方法,也就是业务同时双写 2 个新老集群,新老集群的分片数是不一样的,并且大小配置也不一样。比如之前申请 4 个分片现在发现资源过剩,让业务创新申请一个新的 2 个分片的集群,由业务来控制灰度写哪个集群(图 4)。最终会迁移到新集群上,而新集群大小是满足当前业务需求的,从而达到了缩容的目的。

双写集群的方案虽然解决我们部分的问题,但对于业务的侵入比较深,此外由于双写集群引入了业务配合观察的时间,整体流程也比较长。所以,我们需要寻找更好的解决方案。

既然业务双写集群可以达到要求,基础设施如果代替业务做完这部分岂不是更好?借鉴业务双写集群的思路和云原生的不可变基础设施的理念,我们首先想到的是通过新集群替换老集群而不是原地修改集群;另外,为了在公有云上节省 Redis 成本,我们积累了 kvrocks 的实践经验,两者相结合,设计了一种高效的水平扩缩容的方案。

本方案的核心是引入了一个基于 kvrocks 改造的中间态 binlogserver,它既是一个老集群的 Slave 节点,又充当了新集群的客户端。一方面,它会从 Redis Master 复制全量和增量数据;另一方面,它又充当客户端的角色,将复制来的数据按照新集群的一致性 HASH 规则写往新的集群。大致的步骤如下,具体的步骤流程可以参考下面的图所示(图 5)。

1)根据当前 V1 集群的分片启动对应个数 binlogserver,并获取 V2 集群的一致性 HASH 规则和 group。

2)每个 binlogserver 成为 V1 集群单个分片中 Master 的 Slave,执行 salveof 后保存 V1 中 Master 传过来的 RDB 文件并解析,对于每个 RDB 文件,解析还原成 Redis 命令,并按 CRedis 的一致性 hash 规则写入到 V2 中,对于后续 V1 集群传播过来的命令,同样同步到 V2 中。

3)当这个过程都完成并且 binlog 追的差不多的时候,为了数据一致性,可以停止 V1 的写(客户端报错)后由 CRedis 推送 V2 的配置或直接推送 V2 的配置(客户端不报错但数据可能会丢或不一致),APP 端将会顺序切换到 V2 上来;此过程对用户完全透明,应用端无需做任何操作。

图 5

通过 Redis 的水平扩缩容方案,我们解决了之前的几个痛点问题:

1)持续时间大大缩短,基本上跟 V1 集群最大实例的大小正相关,因为是并发执行,跟集群分片数无关。根据实际的运维数据来看,集群单个实例为 20G,集群扩缩容在 10 分钟之内完成,而低于 10G 的,5 分钟即可完成,大大缩短了扩缩容的周期,并且业务在毫无感知的情况下即可完成扩缩容。由于可以做到秒级切换集群,即使扩缩容后对业务有影响也可以快速回退,因为回退也只是更改了集群的路由指向。

2)扩缩容过程只需要 1 次切换集群指向,0 次迁移,没有中间态,也无需通过大内存宿主机来实现分拆。

3)对于扩容的集群,很方便再来一次缩容还原回去,缩容同理。对于那些已经水平拆分过的集群,也可以通过这种方式还原回去。

4)既可以扩容也可以缩容,甚至还可以不扩容也不缩容按集群来迁移,比如《携程Cilium+BGP云原生网络实践》一文中提到的云原生网络安全控制试点项目。由于原来 Redis 集群下面的实例可能同时部署在 openstack 网络和 cilium 网络,但云原生安全只能控制 cilium 网络下的实例,这种情况下就需要迁移 Redis 实例。如果按之前的运维方式,要按分片来一组组迁移,整个工程可能持续较长时间,并且耗费较多人力,而水平扩缩容可以将一个集群一次性快速迁移到 cilium 网络,省时省力。

5)扩缩容后无性能损耗。

3.2 运维数据

水平扩缩容方案上线 4 个月来,已经成功完成了 200 多次的扩容和缩容。今年某个业务突然请求量暴增十几倍,相关集群经历了多次扩容,每次扩容大多在 10 分钟内完成,有效地支撑了业务发展。

另一方面,针对申请分片非常多而大但实际使用量非常小的集群,我们也借助水平扩缩容的能力快速地缩小了分片数和申请量。通过这些缩容,有效地提升了整体的资源利用率。

3.3 一些坑

单个 key 过大导致 key 被驱逐

在实际水平扩缩容过程中,我们发现有些集群,单个实例中可能会有巨大的 key(大于 3G),由于 V2 集群的大小是根据 V1 大小实时算出来的平均值,一旦 V1 中某个实例过大,可能会导致写到 V2 中的某个实例大小大于预期的平均值,从而引起某些 key 被驱逐。因此,针对这种情况:

1)加强大 key 的检测逻辑,对于超过 512M 的 key 会有告警邮件告知所有者。

2)V2 中所有实例的 maxmemory 在分拆之前不设置限制,统一都调到 60G,防止 V2 中 key 分配不均导致 key 驱逐。

3)水平扩缩容后,在 V1 和 V2 切换过程中,检测 V2 中的实例是否发生过驱逐,如果有则默认分拆失败,不进行切换。

mget 扩容后会导致性能下降

对于极个别的场景,我们还发现,mget 请求耗时会有明显上升,主要原因还是在于,扩容之前 mget 需要访问的实例数少,而分拆后访问的实例数变多。一般这种情况,我们建议业务控制单次 mget 的 key 的数量,或者将 string 类型改造为 hash 类型,通过 hmget 来访问数据,保证每次只会访问到一个实例,这样扩容后其吞吐量是随着分片数量线性增加,而延迟不会有增加。

四、总结和未来规划

4.1 Xpipe 支持

目前水平扩缩容和漂移以及二次调度等一系列治理工具和策略组成了一个比较完善的闭环,有效地支撑了生产几千台宿主机,几万带超分能力 Redis 实例的运维治理。

但目前受制于 xpipe 的架构,对于接入了 xpipe 的集群,必须先扩缩容后再将 DR 端的 xpipe 人工补齐,自动化程度还不足,而补齐 xpipe 的时间比较长,比如之前是就近读本机房的 Redis 集群的 APP,在扩缩容后可能一段时间里只能跨机房读取,必然导致延迟上升。而这种延迟上升又会影响我们对于水平扩缩容逻辑是否正确,是否需要回退的判断。因此后续我们会针对 xpipe 集群,也做到和普通集群一样,也就是 V2 集群在扩缩容写流量之前就是带 DR 架构的集群。

4.2 持久化 KV 存储的支持

除了 Redis 本身受业务欢迎使用广泛外,我们还发现有些业务需要相比 Redis 更可靠的 KV 存储方式,比如数据保存在磁盘上而不是保存在内存里,再比如业务需要支持一些增减库存逻辑,对某个 key 的独占访问,实现语义近似 INCRBY 操作,但实际上是对于一些字符串进行 merge 操作。此外数据可靠性要求更高,master 宕机不能丢失数据等。针对这些需求目前我们已经也有一些实践经验,将在后续文章中分享。

文章转载自:携程技术(ID:ctriptech)

原文链接:携程Redis治理演进之路

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/wHpTaleSGpGO1CeCleG8
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券