阅读 413

腾讯会议用户暴涨,Redis集群如何实现无缝扩容?

远程办公期间,在线会议用户需求激增,腾讯会议8天完成100万核云服务器扩展,Redis集群仅在半小时以内就高效完成了数十倍规模的扩容,单集群的扩容流程后台处理时间不超过30分钟。在这背后,腾讯云Redis是如何做到的呢?本文是伍旭飞老师在「云加社区沙龙online」的分享整理,详细阐述了腾讯云Redis无损扩容的实践和挑战。
点击此链接,查看完整直播视频回放​

一、疫情带来的挑战

今年疫情带来的挑战很明显,远程办公和在线教育用户暴涨,从1月29到2月6日,日均扩容1.5w台主机。业务7×24小时不间断服务,远程办公和在线教育要求不能停服,停服一分钟都会影响成百上千万人的学习和工作,所以这一块业务对于我们的要求非常高。

在线会议和远程办公都大量使用了redis,用户暴增的腾讯会议背后也有腾讯云Redis提供支持,同时海量请求对redis的快速扩容能力提出了要求。我们有的业务实例,从最开始的3片一天之内扩容到5片,紧接着发现还是不够,又扩到12片,第二天继续扩。

二、开源Redis扩容方案

1. 腾讯云Redis的集群版架构

腾讯云Redis跟普遍Redis有差别,我们加入了Proxy,提高了系统的易用性,这个是因为不是所有的语言都支持集群版客户端。为了兼容这部分客户,我们做了很多的兼容性处理,能够兼容更多普通客户端使用,像做自动的路由管理,切换的时候可以自由处理MOVE和ASK,增加端到端的慢查询统计等功能,Redis默认的slowlog只包含命令的运算时间,不包括网络来回的时间,不包括本地物理机卡顿导致的延时,Proxy可以做端到端的慢日志统计,更准确反应业务的真实延迟。

对于多帐户,Redis不支持,现在把这部分功能也挪到Proxy,还有读写分离,这个对于客户非常有帮助,客户无须敲写代码,只需要在平台点一下,我们在Proxy自动实现把读写派发上去。这一块功能放到Redis也是可以,那为什么做到Proxy呢?

主要是考虑安全性!因为Redis承载用户数据,如果在Redis做数据会频繁升级功能迭代,这样对于用户数据安全会产生比较大的威胁。

2. 腾讯云Redis如何扩容

腾讯云Redis怎么扩容呢?我们的扩容从三个维度出发,单个节点容量扩容, 比如说三分片,每个片4G,我们可以每节点扩到8G。单节点容量扩容,一般来说只要机器容量足够,就可以扩容上去。还有副本扩容,现在客户使用的是一主一从,有的同学开读写分离,把读全部打到从机,这种情况下,增加读qps比较简单的方法就是增加副本的数量,还增加了数据安全性。最近的实际业务,我们遇到的主要是扩分片,对于集群分片数,最主要就是CPU的处理能力,扩容分片就是相当于扩展CPU,扩容处理能力也间接扩容内存。

最早腾讯云做过一个版本,利用开源的原生版的扩容方式扩容。简单描述一下操作步骤:

首先Proxy是要做slot容量计算,否则一旦搬迁过去,容易把新分片的内存打爆。计算完每个slot内存后,按照算法分配,决定好目标分片有哪些slot。先设置目标节点slot 为importing状态 ,再设置源节点的slot为 migrating状态。

这里存在一个小坑,在正常开发中这两个设置大家感觉顺序无关紧要,但是实际上有影响。如果先设置源节点的slot为migrating,再设置目标节点的slot为importing,那么在这两条命令的执行间隙,如果有对应slot的命令打到源节点,而key又恰好不存在,则会重定向到目标节点,由于目标节点此时slot并未来得及设置为importing, 又会把这条命令重定向给源节点,这样就无限重定向了。

好在常见的客户端(比如jedis)对重定向次数是有限制的, 一旦打到上限,就会抛出错误。

(1)准备


(2)搬迁

设置完了这一步,下一步就是搬迁。从源节点来获取slot的搬迁,从源进程慢慢逐个搬迁到目标节点。此操作是同步的,什么意思呢?

在migrate命令结束之前进程不能直接处理客户请求,实际上是源端临时创建一个socket,连接目标节点,同步执行命令,确认执行成功了后,把本地的Key删掉,这个时候源端才可以继续处理客户新的请求。在搬迁过程中,整个集群仍然是可以处理请求的。

这一块开源Redis有考虑,如果这个时候有Key读请求,刚好这个slot发到源进程,进程可以判断,如果这个Key在本进程有数据,就会当正常的请求返回给它。

那如果不存在怎么办?就会发一个ASK给客户,用户收到ASK知道这个数据不在这个进程上,马上重新发一个ASKING到目标节点,紧接着把命令发到那边去。这样会有一个什么好处呢?

源端的Key的slot只会慢慢减少,不会增加,因为新增加的都发到目标节点去了。随着搬迁的持续,源端的Key会越来越少,目标端的key逐步增加,用户感知不到错误,只是多了一次转发延迟,只有零点零几毫秒,没有特别明显的感知。

(3)切换

方案到什么时候切换呢?就是slot源进程发现这个slot已经不存在数据了,说明所有数据全部搬到目标进程去了。这个时候怎么办呢?先发送set slot给目标,然后给源节点发送set slot命令,最后给集群所有其他节点发送set slot。

这是为了更快速把路由更新过去,避免通过自身集群版协议推广,降低速度。这里跟设置迁移前的准备步骤是一样,也有一个小坑需要注意。

如果先给源节点设置slot,源节点认为这个slot归属目标节点,会给客户返回move,这个是因为源节点认为Key永久归属目标进程,客户端搜到move后,就不会发ASKing给目标,目标如果没有收到ASK,就会把这个消息重新返回源进程,这样就和打乒乓球一样,来来回回无限重复,这样客户就会感觉到这里有错误。

三、 无损扩容挑战

1. 大Key问题

像这样迁移其实也没有问题,客户也不会感觉到正常的访问请求的问题。但是依然会面临一些挑战,第一就是大Key的问题。

前文提到的搬迁内部,由于这个搬迁是同步的搬迁,同步搬迁会卡住,这个卡住时间由什么决定的?主要不是网速,而是搬迁Key的大小来决定的,由于搬迁是按照key来进行的,一个list也是一个Key,一个哈希表也是Key,一个list会有上千万的数据,一个哈希表也会有很多的数据。同步搬迁容易卡非常久,同步搬迁100兆,打包有一两秒的情况,客户会觉得卡顿一两秒,所有访问都会超时,一般Redis业务设置超时大部分是200毫秒,有的是100毫秒。如果同步搬移Key超过一秒,就会有大量的超时出现,客户业务就会慢。

如果这个卡时超过15秒,这个时间包括搬迁打包时间、网络传输时间、还有loading时间。超过15秒,甚至自动触发切换,把Master判死,Redis会重新选择新的Master,由于migrating状态是不会同步给slave的,所以slave切换成master后,它身上是没有migrating状态的。然后,正在搬迁的目标节点会收到新的master节点对这个slot的所有权声明, 由于这个slot是importing的,所以它会拒绝承认新master拥有这个slot。从而在这个节点看来,slot的覆盖是不全面的, 有的slot无节点提供服务,集群状态为fail。

一旦出现这种情况,假如客户继续在写,由于没有migrating标记了,新Key会写到源节点上,这个key可能在目标节点已经有了,就算人工处理,也会出现哪一边的数据比较新, 应该用哪一边的数据, 这样的一些问题,会影响到用户的可用性和可靠性。

这是整个开源Redis的核心问题,就是容易卡住,不提供服务,甚至影响数据安全。开源版如何解决这个问题呢?老规矩:惹不起就躲,如果这个slot有最大Key超过100M或者200M的阈值不搬这个slot。这个阈值很难设置,由于migrate命令一次迁移很多个key,过小的阈值会导致大部分slot迁移不了,过大的阈值还是会导致卡死,所以无论如何对客户影响都非常大,而且这个影响是不能被预知的,因为这个Key大小可以从几k到几十兆,不知道什么时候搬迁到大key就会有影响,只要搬迁未结束,客户在相当长时间都心惊胆战。

2. Lua问题

除了Key的整体搬迁有这样问题以外,我们还会有一个问题就是Lua。

假如业务启动的时候通过script load加载代码,执行的时候使用evalsha来,扩容是新加了一个进程,对于业务是透明,所以按照Redis开源版的办法搬迁Key,key搬迁到目标节点了,但是lua代码没有,只要在新节点上执行evalsha,就会出错。

这个原因挺简单,就是Key搬迁不会迁移代码,而且Redis没有命令可以把lua代码搬迁到另外一个进程(除了主从同步)。

这个问题在开源版是无解,最后业务怎么做才能够解决这个问题呢?需要业务那边改一下代码,如果发现evalsha执行出现代码不存在的错误,业务要主动执行一个script load,这样可以规避这个问题。但是对很多业务是不能接受的。因为要面临一个重新加代码然后再发布这样一个流程,业务受损时间是非常长的。

3. 多Key命令/Slave读取

还有一个挑战,就是多Key命令,这个是比较严重的问题,Mget和mset其中一个Key在源进程,另外一个Key根本不存在或者在目标进程,这个时候会直接报错,很多业务严重依赖于mget的准确性,这个时候业务是不能正常工作的。

这也是原生版redis没有办法解决的问题,因为只要是Key搬迁,很容易出现mget的一部分key在源端,一部分在目标端。

除了这个问题还有另一个问题,这个问题跟本身分片扩容无关,但是开源版本存在一个bug,就是我们这边Redis是提供了一个读写分离的功能,在Proxy提供这个功能,把所有的命令打到slave,这样可以降低master的性能压力。这个业务用得很方便,业务想起来就可以开启,发现不行就可以马上关闭。

这里比较明显的问题是:当每个分片数据比较大的时候,举一个例子20G、30G的数据量的时候,我们刚开始挂slave,slave身份推广跟主从数据是两个机制,可能slave已经被集群认可了,但是还在等master的数据,因为20G数据的打包需要几分钟(和具体数据格式有关系)。这个时候如果客户的读命令来到这个slave, 会出现读不到数据返回错误, 如果客户端请求来到的时候rdb已经传到slave了,slave正在loading, 这个时候会给客户端回loading错误。

这两个错误都是不能接受,客户会有明显的感知,扩容副本本来为了提升性能,但是结果一扩反而持续几分种到十几分钟内出现很多业务的错误。这个问题其实是跟Redis的基本机制:身份推广机制、主从数据同步机制有关,因为这两个机制是完全独立的,没有多少关系,问题的解决也需要我们修改这个状态来解决,下文会详细展开。

最后一点就是扩容速度。前文说过,Redis通过搬Key的方式对业务是有影响的,由于同步操作,速度会比较慢,业务会感受到明显的延时,这样的延时业务肯定希望越快结束越好。但是我们是搬迁Key,严重依赖Key的速度。因为搬Key不能全速搬,Redis是单线程,基本上线是8万到10万之间,如果搬太快,就占据用户CPU。用户本来因为同步搬迁卡顿导致变慢,搬迁又要占他CPU,导致雪上加霜,所以一般这种方案是不可能做特别快的搬迁。比如说每次搬一万Key,相当于占到12.5%,甚至更糟,这对于用户来说是非常难以接受的。

四、行业其他方案

既然开源版有这么多问题,为什么不改呢?不改的原因这个问题比较多。可能改起来不容易,也确实不太容易。

关于搬迁分片扩容是Redis的难点,很多人反馈过,但是目前而言没有得到作者的反馈,也没有一个明显的解决的趋势,行业内最常见就是DTS方案。

DTS方案可以通过下图来了解,首先通过DTS建立同步,DTS同步跟Redis-port是类似,会伪装一个slave,通过sync或者是psync命令从源端slave发起一次全量同步,全量之后再增量,DTS接到这个数据把rdb翻译成命令再写入目标端的实例上,这样就不要求目标和源实例的分片数目一致,dts在中间把这个活给干了。

等到DTS完全迁移稳定之后,就可以一直同步增量数据,不停从源端push目标端,这时候可以考虑切换。

切换首先观察是不是所有DTS延迟都在阈值内,这个延迟指的是从这边Master到那边Master的中间延迟。如果小于一定的数据量,就可以断连客户端,等待一定时间,等目标实例完全追上来了,再把LB指向新实例,再把源实例删除了。一次扩容就完全实现了,这是行业比较常见的一种方案。

DTS方案解决什么问题呢?大Key问题得到了解决。因为DTS是通过源进程slave的一个进程同步的。Lua问题有没有解决?这个问题也解决了,DTS收到RDB的时候就有lua信息了,可以翻译成script load命令。多Key命令也得到了解决,正常用户访问不受影响,在切换之前对用户来说无感知。迁移速度也能够得到比较好的改善。迁移速度本身是因为原实例通过rdb翻译,翻译之后并发写入目标实例,这样速度可以很快,可以全速写。这个速度一定比开源版key搬迁更快,因为目标实例在切换前不对外工作,可以全速写入,迁移速度也是得到保证。迁移中的HA和可用性和可靠性也都还可以。当然中间可用性要断连30秒到1分钟,这个时间用户不可用,非常小的时间影响用户的可用性。

DTS有没有缺点?有!首先是其复杂度,这个迁移方案依赖于DTS组件,需要外部组件才能实现,这个组件比较复杂,容易出错。其次是可用性,前文提到步骤里面有一个踢掉客户端的情况,30秒到1分钟这是一般的经验可用性影响,完全不可访问。还有成本问题,迁移过程中需要保证全量的2份资源,这个资源量保证在迁移量比较大的情况下,是非常大的。如果所有的客户同时扩容1分片,需要整个仓库2倍的资源, 否则很多客户会失败,这个问题很致命,意味着我要理论上要空置一半的资源来保证扩容的成功, 对云服务商来说是不可接受的,基于以上原因我们最后没有采用DTS方案。

五、腾讯云Redis扩容方案

我们采用方案是这样的,我们的目标是首先不依赖第三方组件,通过命令行也可以迁。第二是我们资源不要像DTS那样迁移前和迁移后两份资源都要保留,这个对于我们有相当大的压力。最后用的是通过slot搬迁的方案。具体步骤如下:

首先还是计算各slot内存大小,需要计算具体搬迁多少slot。分配完slot之后,还要计算可分配到目标节点的slot。跟开源版不一样,不需要设置源进程的migrating状态,源进程设置migrating是希望新Key自动写入到目标进程,但是我们这个方案是不需要这样做。

再就是在目标进程发起slot命令,这个命令执行后,目标节点根据slot区间自动找到进程,然后对它发起sync命令(带slot的sync),源进程收到这个sync命令,执行一个fork,将所有同步的slot区间所有的数据生成rdb,同步给目标进程。

每一个slot有哪一些Key在源进程是有记录的,这里遍历将每一个slot的key生成rdb传输给目标进程,目标进程接受rdb开始loading,然后接受aof,这个aof也是接受跟slot相关的区间数据,源进程也不会把不属于这个slot的数据给目标进程。

一个目标进程可以从一两个源点建立这样的连接,一旦全部建立连接,并且同步状态正常后,当offset足够小的时候,就可以发起failover操作。和Redis官方主动failover机制一样。在failover之前,目标节点是不提供服务的,这个和开源版有巨大的差别。

通过这个方案,大Key问题得到了解决。因为我们是通过fork进程解决的,而不是源节点搬迁key。切换前不对外提供服务,所以loading一两分钟没有关系,客户感知不到这个节点在loading。

还有就是Lua问题也解决了,新节点接受的是rdb数据,rdb包含了Lua信息在里面。还有多Key命令也是一样,因为我们完全不影响客户正常访问,多Key的命令以前怎么访问现在还是怎么访问。迁移速度因为是批量slot打包成rdb方式,一定比单个Key传输速度快很多。

关于HA的影响,迁移中有一个节点挂了会不会有影响?开源版会有影响,如果migrating节点挂了集群会有一个节点是不能够对外提供服务。但我们的方案不存在这个问题,切换完了依然可以提供服务。因为我们本来目标节点在切换之前就是不提供服务的。

还有可用性问题,我们方案不用断客户端连接,客户端从头到尾没有受到任何影响,只是切换瞬间有小影响,毫秒级的影响。成本问题有没有解决?这个也得到解决,因为扩容过程中,只创建最终需要的节点,不会创建中间节点,零损耗。

六、Q&A

Q:Cluster数量会改变槽位的数量吗?

A:不会改变槽位数量,一直是16814,这个跟开源版是一致的。

Q:迁移之前怎么评估新slot接触数据不会溢出?

A:根据前文所述,我们有一个准备阶段,计算所有slot各自内存大小,怎么计算我们会在slave直接执行一次扫描计算,基本上能够计算比较准确。这里没有用前一天的备份数据,而是采用slave实时计算,CPU里有相应控制,这样可以计算出slot总量大小。我们会预定提前量(1.3倍),用户还在写,保证目标不会迁移中被写爆,万一写爆了也只是流程失败,用户不会受到影响。

Q:对于 redis 扩容操作会发生的问题,你们有采用什么备用方案,和紧急措施吗?

A:新的扩容方案,如果出现问题,不会影响客户的使用,只会存在多余的资源,目前这块依赖工具来处理。数据的安全性本身除了主从,还依赖每日备份来保证。

Q:请问老师,高峰期需要扩容,但总有回归正常请求量的时候,此时的扩容显得有些冗余,怎么样让Redis集群能够既能够快速回收多余容量,同时又能方便下一次高峰请求的再次扩容呢?

A:可能你想了解的是serverless,按需付费自动扩展的模式,目前的数据库大多是提供的PAAS服务,PASS层的数据库将过去的资源手动管理变成了半自动化,扩缩容还是需要运维参与。Serverless形式的服务会是下一步的形态,但是就算是serverless可能也会面临着需要资源手动扩展的问题,特别是对超大规模的运算服务。

Q:slot来源于多个分片,同时和多个节点同步进行吗?

A:是,确实会这样做的。

Q:slot产生过程中产生新数据怎么同步?

A:类似aof概念的机制同步到目标进程。这个aof跟普通aof传输到slave有区别,只会将跟目标slot相关的数据同步过去,而不会同步别的。

Q:还在招人吗?

A:腾讯云Redis还在招人,欢迎大家投简历,简历请投至邮箱:feiwu@tencent.com。大家还可以选择在官网上投递简历,或者搜索关注腾讯云数据库的公众号,查看21日的推送文章,我们贴上了招聘职位的链接,也可以@社群小助手,小助手会收集上来发到我这边。

讲师简介

伍旭飞,腾讯云高级工程师,腾讯云Redis技术负责人,有多年和游戏和数据库开发应用实践经验, 聚焦于游戏开发和NOSQL数据库在各个领域的应用实践。

关注云加社区公众号,回复“线上沙龙”,即可获取老师演讲PPT~