给我10分钟,无感知搞定Redis集群扩缩容

一、背景
携程Redis集群规模和数据规模在过去几年里快速增长 , 我们通过容器化解决了Redis集群快速部署的问题 , 并根据实际业务进行的一系列尝试 , 比如二次调度、自动化漂移等 , 在内存超分的情况下保证了宿主机的可靠性 。
【给我10分钟,无感知搞定Redis集群扩缩容】扩缩容方面 , 我们主要通过垂直扩缩容的方式解决Redis集群容量的问题 , 但随着集群规模扩大 , 这种方式逐渐遇到了瓶颈 。 一方面 , 单个Redis实例过大 , 会带来较大的运维风险和困难;另一方面 , 宿主机容量有上限 , 不能无止境的扩容 。 考虑到运维便利性和资源利用率的平衡 , 我们希望单个Redis实例的上限为15GB 。
但实际操作中却很难做到:某些业务发展很快 , 经常性需要给Redis进行扩容 , 导致单个实例大小远超15GB;一些业务萎缩 , 实际使用量远低于初始申请的量 , 造成资源的浪费 。
如何有效控制Redis实例大小呢?接下来本文将带着这个问题 , 逐步讲解携程Redis治理和扩缩容方面的演进历程 。
二、Redis水平扩分拆
在携程开始使用Redis很长一段时间里 , 一直只有垂直扩缩容 , 原因有两点:
第一 , 一开始业务规模比较小 , 垂直扩缩容可以满足需求 。 垂直扩缩容对于Redis来说只是Maxmemory的配置更改 , 对业务透明 。
第二 , 水平拆分/扩缩容的实现难度和成本较高 。
之前文章《携程Redis治理演进之路》中已经提到 , 携程访问所有的Redis集群使用的是自主研发的CRedis , 而部署在应用端的CRedis通过一致性hash来访问实际承载数据的Redis实例 。 但一致性hash是无法支持直接水平扩缩容的 。 因为无论增加一个节点或者删除一个节点 , 都会导致整个hash环的调整 。

给我10分钟,无感知搞定Redis集群扩缩容
文章图片
图1
如图所示 , 假设原始有4个分片(图1) 。 当添加一个节点后 , 它会导致某一部分的key本来是写到nodeC上而现在会被写到nodeE上 , 也就是无法命中之前的节点 。 从客户端的角度来看 , key就像是丢失了 。 而变动的节点越多 , key丢失的也越多 , 假设某个集群从10分片直接添加到20分片 , 它直接会导致50%的key丢失 。 删除一个节点同理 , 就不再赘述 。
因此尽管一致性hash是个比较简单优秀的集群方案 , 但无法直接水平扩容一直困扰着运维和架构团队 。 为此 , CRedis团队在2019年提出了水平拆分的方案 。
CRedis水平分拆的思路比较朴素 , 因为在一致性hash同一个水平位置增加节点会导致数据丢失 , 那么不改变原来层次节点的hash规则 , 以某个节点为hash的起点 , 再来进行一次一致性hash , 演变成树的结构(图2) 。

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

给我10分钟,无感知搞定Redis集群扩缩容
文章图片
图3
CRedis水平分拆上线后 , DBA将现存的绝大部分超过15G的实例都拆分成更小的实例 , 在一段时间内缓解了大内存实例的运维治理压力 。 但随着Redis规模的快速增长 , 不断有大的实例集群出现 , 此外CRedis水平分拆的缺点也逐渐暴露出来:
持续的周期很长 , 对多个Group进行拆分的话 , 每个Group的数据需要同时复制几份同样的实例 。 比如60G的某个实例(图3) , 如果想拆到5G一个 , 那么下级的Group必须有12个 , 而拆分要先将该实例的数据先同步为12个60G的实例 , 再根据key的命中规则清理该12个60G的实例中不会命中的key , 最终演变成12个5G的实例 。 一般60G的group实例拆分需要3个小时-6个小时 , 如果一个集群的分片非常多 , 加上观察对业务影响的时间 , 可能要持续上几天或一两周 , 并且只能是有人值守的串行操作 。 拆分过程中需要2次迁移 , 如上面所说的 , 拆分中中间态实例对于内存的要求是非常大的 , 拆分完成后对内存的需求会急剧下降 , 因此每次拆分都涉及到2次迁移 , 尽管迁移不会影响业务 , 但对于执行操作拆分的运维人员来说 , 心智负担比较大 , 而且一不小心也会导致线上事故 。 拆分后无法还原回去 , 也就是说假设业务分拆后收缩 , 对Redis的需求变小了 , 但它实际拆分后的分片还在那边 , 所申请的空间还并没有释放掉 , 客观上浪费了资源 , 降低了Redis总体的利用率 。 只支持扩容 , 不支持缩容 , 这点上面也提到了 , 除了一些集群过大需要分拆外 , 还有一些申请远超需求的实例需要缩容 , 而水平分拆对于这点无能为力 。 拆分一次 , 就多一次的性能损耗 , 因为需要多计算一次hash , 虽然耗时不大 , 但是对于性能敏感的业务还是有影响 。由此可见 , 水平分拆的方案虽然解决了实例过大的问题 , 但不能缩容的弊端也逐渐凸现了出来 。 尤其是在今年因疫情影响需要降本增效的背景下 , 一方面资源比充足 , 一方面宿主机上跑的都是无法缩容的实例 。 那么是否有更好的解决方案呢?答案是有的 。