暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

「腾讯云NoSQL」技术之Redis篇:精准围剿rehash时延毛刺实践方案揭秘

腾讯云数据库 2025-10-29
145

在互联网时代,数据存储与访问的效率直接决定着用户体验的好坏。Redis作为当今最流行的内存数据库之一,凭借其出色的性能表现,已成为支撑亿万用户在线服务的核心基础设施。然而,随着业务规模的快速增长,Redis内部的rehash机制却可能在关键时刻引发严重的时延毛刺,导致整个系统性能急剧下降,甚至引发大规模的服务故障。如何精准识别并有效解决rehash带来的性能问题,已成为Redis产品必须面对的重要挑战。本文将通过一个真实的线上故障案例,深入剖析rehash机制的工作原理及其时延影响,并详细介绍腾讯云NoSQL团队针对这一问题的体系化优化实践。

作者:李鸿瑞、朱彬彬




、 故障案发现场一次惊心动魄线上事故

一个寒冷冬夜,你拖着疲惫身躯回到家中盛满一杯热牛奶,躺在松软的沙发上。终于周末你一边庆祝一边郑重打开了最爱娱乐软件准备狠狠happy一波天有不测风云平时轻松登录应用软件今天竟然显示登录失败你以为是网络问题,于是熟练地刷新页面、切换网络,但那个刺眼的提示依旧顽固地占据着屏幕,直到各种方法用尽依然无法登录恍然大悟——应用服务崩了!

对于腾讯云NoSQL团队的工程师们来说,这个不平静的夜晚,始于监控屏幕上一条急剧拉升的曲线。这条曲线对应着负责应用软件身份校验服务的云Redis集群——一个存储着海量用户登录凭证的关键系统。监控图上,P99延迟从稳如磐石的18ms飙升至390ms,暴涨超过20倍,平均延迟也翻了个倍!相应地身份校验服务的成功率骤降,毫不知情的用户面对着无法登录应用,下意识地刷新、重试。这股巨大的“重试风暴”带来了海量的建连请求,如潮水般涌一个Redis分片,瞬间将其CPU打满,服务近乎瘫痪。

在紧张的故障原因调查中,一个关键线索浮出水面:业务的key总量在短短半个多月内暴涨4。而发生故障的那天,故障分片的key数量不多不少,正好增长到了6710万与2的26次方十分接近。我们将时延激增的时间点与key数量增长曲线一对齐,揪出了这次事故的“幕后黑手”——Redis的rehash机制




、 揭秘真凶rehash为何引发时延毛刺

在介绍rehash如何引发时延上升之前,我们需要先了解Redis rehash的基本原理。接下来我们Redis的基本数据结构——字典(dict)开始说起。

2.1 dict数据结构

dict数据结构图一所示Redis这样key-value数据库数据都是以key-value这样键值对形式存储dict则是存储键值对核心数据结构dict哈希表组成普通状态dict只使用table0table1为空用户写入key-valueRedis首先计算keyhashkey-value存入hash对应哈希每个哈希桶都是一个链表新来key-value插入到链表头部为例写入k3-v3Redis首先计算k3hash2然后k3-v3插入2哈希桶链表头部

图1 dict数据结构示意图

在写数据时,数据是直接写到链表头部的,因此写数据的时间复杂度可以稳定在O(1),但读数据就没有这么幸运了。当一个哈希桶里有多对key-value时,Redis会从链表头部开始遍历,依次比较每个链表节点的key与目标key是否一致:如果不一致,则比较下一个节点;如果一致,则返回该节点的value。

图2 过长哈希链表导致读性能下降

不难想到,这样的读取方式会带来下述问题:如果哈希桶中的key-value对太多,并且我们要查询的key非常不幸地位于链表尾部,那么我们就不得不遍历链表的所有节点才能读取到目标value,本次查询就会偏慢。如图二所示,如果用户执行“GET k0”命令,那么它需要进行6次比较才能完成查询。可以想象,如果哈希表永远只有4个桶,元素个数为N,那么每个桶的平均key-value对数量为N/4,每次查询平均需要做N/8次比较。在数据量N很大时,这样的查询性能是无法接受的。如何解决以上问题呢?很简单,给哈希表分配更多的哈希桶就行了。

2.2 dict扩容rehash

在上一小节我们得知,当dict的数据量较大时,为了避免读性能下降,应该给哈希表分配更多的哈希桶,这就是dict的扩容。怎样决定何时扩容呢?接着上一节的分析,记哈希表中的实际元素个数为N,哈希表的桶数为M,那么每个桶的平均链表长度为N/M。平均链表长度直接影响哈希表的读取性能,因此Redis把N/M作为决定dict何时扩容的指标,并将其称为“负载因子”(load factor)。当负载因子大于1时,dict就决定进行扩容

扩容的时机定好了,那么扩容的大小怎么定呢?首先,Redis中dict哈希表的桶数都是2的整数次幂(初始为4)。每次扩容时,新表的桶数是“第一个大于或等于当前元素个数两倍的2的整数次幂”。例如当前哈希表的桶数为4,元素个数为4,那么再写入一个新key时,负载因子大于1,会触发扩容,扩容出的新表大小是第一个大于或等于5的2的整数次幂,也就是8。

dict扩容的机制使得当key的数量增长至2的整数次幂时,dict就会进行扩容还记得我们在文章开头提到的定位问题的关键线索——故障分片的key数量不多不少,正好增长到了2的26次方现在我们便明白了这个数字说明故障分当时正好进行了一次扩容问题指向了扩容后续rehash

扩容的大小也定好了,那么新表在哪里分配呢?诶,还记得第一章节图一中空出来的table 1吗?它就是新表分配的地点。

最后,将新表分配出来后,还需要将旧表的元素搬到新表中,新表才能正式生效。将元素从旧表搬迁到新表的过程就是本文的主角——rehash。当元素数量较多时,rehash是一个耗时较大的任务,但Redis的运行又是单线程的,如果等到rehash完成后才执行下一步指令,那么用户的读写等请求会长时间得不到响应,这肯定是不能接受的。对此,Redis的解决方案是渐进式rehash

想象你是一个管理大师,正在经营一家图书馆。随着图书馆的书籍(键值对)越来越多,馆内的书架(哈希桶)变得十分拥挤,顾客们需要找很久才能在书架上找到自己想看的书(查询请求处理缓慢)。深知“用户为本”理念的你为了解决顾客的痛点,决定新建一个更大的图书馆。新馆建造完毕后,你还需要把旧图书馆的大量书籍搬到新馆(rehash)。粗暴的方式是闭馆一周,搬完所有书籍后再开放,但这势必会影响顾客体验,等到书搬完后,黄花菜都凉了。但聪明的你立马想到了一个更好的方式:新图书馆和旧图书馆同时保持开放,在此期间,所有新到的书籍都直接放入新馆,而查找书籍时则会先看旧馆,再看新馆,确保万无一失。在图书馆开放期间,也要时不时把书从旧馆搬往新馆,当所有书都转移完毕后,关闭旧馆。

Redis的渐进式rehash就是遵循这样的思路。table 1分配出来后,同时使用table 0和table 1两个哈希表。新写入的数据直接写入table 1,查询数据则会先查询table 0,再查询table 1。在此期间将元素渐进式地从table 0转移到table 1,转移完毕后释放掉空的table 0,然后将table 1设置为新的table 0。

综上所述可以哈希表扩容全流程总结为以下几个步骤

(1)初始dict所示此时dict负载因子已经1扩容一触即发

图3 即将扩容dict

2用户写入元素k4-v4导致负载因子大于1扩容触发dicttable1创建新表分配8哈希桶新到来k4-v4直接写到table1如图所示

图4 分配新表

3开始渐进式rehash逐渐元素table0搬迁table1期间正常处理读写请求显示搬迁全部完成场景rehash期间用户写入新数据k5-v5直接插入到了table1

图5 rehash完成

4rehash完成后释放table0释放table0拥有完整数据table1交换一下可以理解交换名字扩容完成扩容完成dict图六所示

图6 释放旧表

这里补充说明Redisrehash最小单位一个哈希桶一次rehash至少一个哈希桶所有元素全部新表Redisdict维护一个索引计数器变量rehashidx用于记录渐进式rehash进行到了哪个位置或者说rehashidx就是下一个要被迁移rehashidx0开始每次搬完一个1图七所示table00最先迁移1空桶被跳过目前rehash进行到了2下一次迁移时2两个元素都会被迁移

图7 单步rehash过程

2.3 渐进rehash进行方式

在上一我们讲到Redisrehash不是一次性完成而是正常处理读写请求渐进完成那么Redis具体如何元素旧表搬到新表本小回答这个问题

Redis渐进式rehash分为被动rehash主动rehash方式

2.3.1被动rehash

在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定操作还会顺带进行单步rehashtable0rehashidx索引所有元素rehashtable1一个被动rehash巧妙化整为零一个整体rehash分摊多个增删查改操作上从而避免了一次性rehash庞大工作量

2.3.2主动rehash

了解了被动rehash之后,我们会想,如果Redis太空闲了,长时间都没有收到增删查改的请求,那么rehash岂不是就停滞了吗?这时候就要靠主动rehash了。Redis设置了一个服务器定时任务serverCron,它会在Redis运行期间周期性执行,每次serverCron执行都会分配出1ms的时间片专门用于字典rehash。

被动和主动两种rehash方式的结合,使得整个字典的rehash是分摊到每一次对字典的读写请求中,并且在被动模式低效时(客户端读写请求少)也可以通过主动rehash最终将字典rehash完成。

2.4 Rehash时延影响

通过前面的分析,我们已经了解了Redis rehash的基本原理和执行方式。现在让我们回到文章开头那个惊心动魄的故障现场:P99延迟从18ms暴涨到390ms,平均延迟翻倍,海量用户无法正常使用服务。究竟是rehash过程中的哪些环节导致了如此严重的时延毛刺?让我们深入剖析rehash过程中的三个时延"杀手"。

2.4.被动rehash影响

还记得被动rehash的工作原理吗?每当用户执行一次增删查改操作时,Redis不仅要完成用户请求的操作,还要"顺便"搬迁一个哈希桶的数据。这就像你去图书馆,本来只是借个书,但工作人员说:"顺便帮我们把这个书架上的书搬到我们的新馆去吧。"

这种"顺便"的代价可不小。被动rehash虽然将迁移成本分摊每个用户请求,但也意味着用户的每一次读写请求都会被强行增加一个额外的rehash步骤在高QPS下,这会显著拉低系统整体的吞吐量和性能表现。正如文章开头提到线上事故Redis分片平均时延翻倍这其中离不开被动rehash贡献

糟糕如果某个哈希桶中恰好存储了大量的key-value对,那么搬迁这个桶就需要花费的时间如图所示图中rehash进行到了一个元素数量很多此时恰好有一个用户Redis发送了读请求请求必须等待rehashidx指向所有元素完成迁移才能继续执行从而导致响应延迟显著增加

图8 被动rehash搬迁元素数量较多会造成较高时延

我们无法确定某个请求“顺带”的被动rehash需要搬迁的元素数量是多还是少,从而无法控制每次被动rehash带来的额外耗时。从这个角度来看,被动rehash是不可控的,且当QPS越大时,被动rehash对整体时延的影响就会越大。

2.4.主动rehash影响

主动rehash的问题在于其固定的时间分配策略。Redis的serverCron定时任务每次执行时,都会固定分配1ms的时间来进行字典rehash。这听起来似乎不多,但如果serverCron任务运行频率考虑在内主动rehash也可能占用较大比例CPU时间

在Redis中,周期性任务serverCron的执行频率由系统配置server.hz决定,server.hz是serverCron在1s内的执行次数。server.hz默认配置为10,也就是说serverCron默认每秒执行10次。在默认配置下,rehash的CPU使用率是,这的确是一个不高的值。但是,server.hz配置是可以更改的,若server.hz配置为100,那么rehash的CPU使用率就上升到了。如果珍贵的CPU资源有10%都用于rehash,那么用于处理读写请求的CPU资源就会减少,从而不可避免地导致读写请求的响应时延上升。

2.4.旧表释放致命一击

经过读写请求顺带被动rehashserverCron定期主动rehash旧表终于搬空释放旧表最后一步dict扩容大功告成但就是最后一个看似不起眼步骤可能会给系统致命一击

释放旧表实际就是释放旧表中的每个哈希桶,每个哈希桶指向的空间大小为8个byte,因此释放一个大小为的哈希表一共需要释放bytes的空间。当旧表很大时,释放内存需要花费较长的时间,在这期间Redis的主线程被完全占用,无法处理任何用户请求。

回到我们开头的故障案例,当故障分片的key数量达到6710万时触发了rehash最后释放空间大小将是512MB监控数据显示,正是在旧表释放的瞬间,P99延迟从18ms暴涨到到390ms。

被动rehash主动rehash释放旧表这三种操作带来影响相互交织,共同增加rehash期间Redis响应时间被动rehash带来了不可控的请求延迟,主动rehash消耗了宝贵的CPU资源,而旧表释放则在关键时刻给出了"致命一击"。理解了这些问题的根源,我们就可以有针对性地制定解决方案了。






三、 驯服怪兽腾讯云Redis针对rehash的体系化优化实践

针对本次故障暴露的rehash问题,腾讯云NoSQL团队从全局视角出发,成功将rehash这一不可控的"性能怪兽"纳入可控范围,实现了rehash过程的体系化管理。

3.1 被动rehash开关

2.4.1小节我们讲到被动rehash会给每一请求强加一个额外rehash步骤并且由于哈希桶大小具有随机性因此过程实际不可控我们在想能否通过主动rehash方式搬迁元素为此我们做了如下实验

我们使用Redis5线上环境进行测试关闭dynamic-hz使server.hz始终保持固定10观察没有读写流量情况下redis-server单纯依靠主动rehash搬迁效率具体实验步骤如下

    # 关闭动态 hz 调整,减少变量
    127.0.0.1:6379> config set dynamic-hz no
    OK
    # 根据测试条件调整 hz
    127.0.0.1:6379> config set hz 10
    OK
    # 每次实验构造 33554432 个键,即 2 ** 25 个,在插入一个键之后会触发字典 rehash 扩容
    127.0.0.1:6379> debug populate 33554432
    OK
    # 触发rehash
    127.0.0.1:6379> set foo bar

    实例监控图像可以看到触发 rehash 后,CPU 资源平均只会消耗 2% 左右3300W 个key搬迁耗时是 15 分钟,平均 1 分钟可以 rehash 搬迁220W 个key

    图9 主动rehash期间CPU使用率

    经过上述分析,我们认为主动rehash的效率实际上已经足够了,因此我们加入了一个开关来控制被动rehash。具体而言,我们在Redis中增加了配置项passive-rehash-enabled来控制被动rehash是否开启,且该配置默认为no

    3.2 被动rehash开关

    2.4.2小节中提到,主动rehash的CPU使用率会根据server.hz的变化而变化,这一点不利于rehash期间系统读写性能的稳定。为了解决这一问题我们将Redis中每次serverCron中花费在rehash的时间,从固定的1ms更改为根据server.hz动态修正

    具体而言我们增加了配置项active-rehash-cycle表示serverCronrehashCPU使用率配置默认值1

    用于rehash时间根据server.hzserver.active-rehash-cycle实时计算每次用时


    例如:

     hz = 10, active-rehash-cycle = 1,上面计算结果为 1000us 即 1ms

     hz = 10, active-rehash-cycle = 10,上面计算结果为 10000us 即 10ms

     hz = 100, active-rehash-cycle = 1,上面计算结果为 100us 即 0.1ms

    上述方式有效固定主动rehashCPU使用率根据server.hz动态修正每次主动rehash用时消除server.hz主动rehashCPU使用率影响使得主动rehash期间系统读写性能更加稳定

    3.3空间异步释放

    正如2.4.3小节所讲,当dict大小已经很大时,rehash结束时的旧表释放会造成非常致命的系统阻塞,所有请求需要等待旧表释放完成才可继续进行。其实这个问题的解决方法十分简单,只需要将旧表释放异步进行,使其不干扰主线程的正常运行即可。

    Redis内存分配释放使用jemalloc分配器经过我们调研jemalloc5已经支持backgroundthread异步释放内存因此我们消除Redis释放大块内存抖动方式非常直接将Redis使用的jemalloc分配器升级到版本5

    3.rehash维护时间窗口

    前文许多分析我们都能感受到尽管我们通过各种方式rehash控制得更加稳定但其始终一个成本较大操作rehash或多或少都会对用户请求时延产生负面影响尤其是在QPS较高场景下这一影响更加显著

    面对这一问题我们视角系统内部转移业务场景采取一个更加无感策略——为rehash设置一个时间窗口,只有在该时间窗口内才允许dict正常扩容

    我们Redis新增了maintence-time配置项一个时间区间

     维护时间窗口内:按照社区逻辑正常走,即当添加元素时发现负载因子 >= 1 时,正常进行扩容。

     维护时间窗口外:禁止扩容。但为了避免哈希冲突带来的性能损失,会设置一个最大负载因子添加元素时发现负载因子 >= 1.618,即使不在维护时间窗口dict依旧正常进行扩容。

    maintence-time可以依据实际业务需求自由配置,通常设置为业务QPS较低的时间段,例如凌晨某时间段rehash深夜悄悄进行





    总结

    从文章开头的故障现场出发,我们剖析了Redis rehash引发时延毛刺的三大原因:被动rehash的额外负担、主动rehash的CPU占用不可控,以及旧表释放引起线程阻塞。针对这些问题,腾讯云NoSQL团队构建了完整的rehash优化体系:关闭被动rehash,主动rehash时间控制让CPU使用率可控,空间异步释放消除内存释放阻塞,维护时间窗口让rehash在业务低峰期进行。这套优化体系成功驯服了Redis这头"怪兽",持续保障线上用户体验稳定流畅

    TencentDB  




     


    往期推荐





    文章转载自腾讯云数据库,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

    评论