Redis中key过期如何解决

70次阅读
没有评论

共计 3034 个字符,预计需要花费 8 分钟才能阅读完成。

这篇文章给大家介绍 Redis 中 key 过期如何解决,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。

初步调查

受影响的团队和缓存团队开始进行初步的调查。我们发现延迟增加与现在正在发生的 key 清除有关。当 Redis 收到写入请求但没有内存来保存写入时,它将停止正在执行的操作,清除 key 然后保存新 key。但是,我们仍然需要找出导致这些新清除的内存使用量增加的原因。

我们怀疑内存中充满了过期但尚未删除的 key。有人建议使用扫描,扫描的方法会读取所有的 key,并且让过期的 key 被删除。

在 Redis 中,key 有两种过期方式,主动过期和被动过期。扫描将触发 key 的被动过期,当读取 key 时,TTL 将会被检查,如果 TTL 已过期,TTL 会被删除并且不返回任何内容。Redis 文档中描述了版本 3.2 中的 key 的主动过期。key 的主动过期以一个名为 activeExpireCycle 的函数开始。它以每秒运行几次的频率,运行在一个称为 cron 的内部计时器上。activeExpireCycle 函数的作用是遍历每个密钥空间,检查具有 TTL 集的随机 kry,如果满足过期 kry 的百分比阈值,则重复此过程直到满足时间限制。

这种扫描所有 kry 的方法是有效的,当扫描完成时,内存使用量也下降了。似乎 Redis 不再有效地使 key 过期了。但是,当时的解决方案是增加集群的大小和更多的硬件,这样 key 就会分布得更多,就会有更多的可用内存。这是令人失望的,因为前面提到的升级 Redis 的项目通过提高集群的效率降低了运行这些集群的规模和成本。

Redis 版本:有什么改变?

Redis 版本 2.4 和 3.2 之间,activeExpireCycle 的实现发生了变化。在 Redis 2.4 中,每次运行时都会检查每个数据库,在 Redis3.2 中,可以检查的数据库数量达到了最大值。版本 3.2 还引入了检查数据库的快速选项。“Slow”在计时器上运行,“fast”运行在检查事件循环上的事件之前。快速到期周期将在某些条件下提前返回,并且它还具有较低的超时和退出功能阈值。时间限制也会被更频繁地检查。总共有 100 行代码被添加到此函数中。

进一步调查

最近我们有时间回过头来重新审视这个内存使用问题。我们想探索为什么会出现 regression,然后看看我们如何才能更好地实现 key expiration。我们的第一个想法是,在 Redis 中有很多的 key,只采样 20 是远远不够的。我们想研究的另一件事是 Redi 3.2 中引入数据库限制的影响。

缩放和处理 shard 的方式使得在 Twitter 上运行 Redis 是独一无二的。我们有包含数百万个 key 的 key 空间。这对于 Redis 用户来说并不常见。shard 由 key 空间表示,因此 Redis 的每个实例都可以有多个 shard。我们 Redis 的实例有很多 key 空间。Sharding 与 Twitter 的规模相结合,创建了具有大量 key 和数据库的密集后端。

过期测试的改进

每个循环上采样的数字由变量

ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP

配置。我决定测试三个值,并在其中一个有问题的集群中运行这三个值,然后进行扫描,并测量内存使用前后的差异。如果内存使用前后的差异较大,表明有大量过期数据等待收集。这项测试最初在记忆使用方面有积极的结果。

该测试有一个控件和三个测试实例,可以对更多 key 进行采样。500 和 200 是任意的。值 300 是基于统计样本大小的计算器的输出,其中总 key 数是总体大小。在上面的图表中,即使只看测试实例的初始数量,也可以清楚地看出它们的性能更好。这个与运行扫描的百分比的差异表明,过期 key 的开销约为 25%。

虽然对更多 key 进行采样有助于我们找到更多过期 key,但负延迟效应超出了我们的承受能力。

上图显示了 99.9%的延迟(以毫秒为单位)。这表明延迟与采样的 key 的增加相关。橙色代表值 500,绿色代表 300,蓝色代表 200,控制为黄色。这些线条与上表中的颜色相匹配。

在看到延迟受到样本大小影响后,我想知道是否可以根据有多少 key 过期来自动调整样本大小。当有更多的 key 过期时,延迟会受到影响,但是当没有更多的工作要做时,我们会扫描更少的 key 并更快地执行。

这个想法基本上是可行的,我们可以看到内存使用更低,延迟没有受到影响,一个度量跟踪样本量显示它随着时间的推移在增加和减少。但是,我们没有采用这种解决方案。这种解决方案引入了一些在我们的控件实例中没有出现的延迟峰值。代码也有点复杂,难以解释,也不直观。我们还必须针对每个不理想的群集进行调整,因为我们希望避免增加操作复杂性。

调查版本之间的拟合

我们还想调查 Redis 版本之间的变化。Redis 新版本引入了一个名为 CRON_DBS_PER_CALL 的变量。这个变量设置了每次运行此 cron 时要检查的最大数据库数量。为了测试这种变量的影响,我们简单地注释掉了这些行。

//if (dbs_per_call server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;

这会比较每次运行时具有限制的,和没有限制的检查所有数据库两个方法之间的效果。我们的基准测试结果十分令人兴奋。但是,我们的测试实例只有一个数据库,从逻辑上讲,这行代码在修改版本和未修改版本之间没有什么区别。变量始终都会被设置。

99.9% 的以微秒为单位。未修改的 Redis 在上面,修改的 Redis 在下面。

我们开始研究为什么注释掉这一行会产生如此巨大的差异。由于这是一个 if 语句,我们首先怀疑的是分支预测。我们利用

gcc’s__builtin_expect

来改变代码的编译方式。但是,这对性能没有任何影响。

接下来,我们查看生成的程序集,以了解究竟发生了什么。

我们将 if 语句编译成三个重要指令 mov、cmp 和 jg。Mov 将加载一些内存到寄存器中,cmp 将比较两个寄存器并根据结果设置另一个寄存器,jg 将根据另一个寄存器的值执行条件跳转。跳转到的代码将是 if 块或 else 块中的代码。我取出 if 语句并将编译后的程序集放入 Redis 中。然后我通过注释不同的行来测试每条指令的效果。我测试了 mov 指令,看看是否存在加载内存或 cpu 缓存方面的性能问题,但没有发现区别。我测试了 cmp 指令也没有发现区别。当我使用包含的 jg 指令运行测试时,延迟会回升到未修改的级别。在找到这个之后,我测试了它是否只是一个跳转,或者是一个特定的 jg 指令。我添加了非条件跳转指令 jmp,跳转然后跳回到代码运行,期间没有出现性能损失。

我们花了一些时间查看不同的性能指标,并尝试了 cpu 手册中列出的一些自定义指标。关于为什么一条指令会导致这样的性能问题,我们没有任何结论。当执行跳转时,我们有一些与指令缓存缓冲区和 cpu 行为相关的想法,但是时间不够了,可能的话,我们会在将来再回到这一点。

解析度

既然我们已经很好地理解了问题的原因,那么我们需要选择一个解决这个问题的方法。我们的决定是进行简单的修改,以便能够在启动选项中配置稳定的样本量。这样,我们就能够在延迟和内存使用之间找到一个很好的平衡点。即使删除 if 语句引起了如此大幅度的改进,如果我们不能解释清楚其原因,我们也很难做出改变。

关于 Redis 中 key 过期如何解决就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-01发表,共计3034字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)