排查 Redis 慢命令:性能检查清单

一份实用检查清单,教你如何通过 SLOWLOG、MONITOR、延迟工具、命令复杂度以及更安全的修复方案来定位 Redis 慢命令。

排查 Redis 慢命令:性能检查清单

Redis 慢命令通常始于那些超出预期的常规命令。当集合只有 200 个成员时,SMEMBERS 调用是无害的。当仪表盘查询只加载 50 个键时,它运行良好。一个 Lua 脚本原本很快,直到某个客户创建了远超其他人的数据规模。

关键问题不仅仅是“哪个命令慢?”,而是“什么样的数据规模导致这个命令变慢,以及为什么应用程序要求 Redis 一次性完成这么多工作?”

理解 Redis 性能

Redis 的性能通常非常出色,这得益于其内存特性。然而,有几个因素可能导致命令延迟:

  • 命令复杂度: 某些命令天生比其他命令更消耗资源(例如,在大数据集上执行 KEYS 与执行 GET)。
  • 数据大小和结构: 大型列表、集合或有序集合,以及复杂的数据结构,会影响操作它们的命令的性能。
  • 网络延迟: 虽然不直接是命令问题,但客户端和服务器之间的高网络延迟会使命令看起来很慢
  • 服务器负载: 高 CPU 使用率、内存不足或 Redis 服务器上的其他进程会降低性能。
  • 阻塞命令: 某些操作会阻塞 Redis 事件循环,影响所有后续命令。

使用 SLOWLOG 识别慢命令

SLOWLOG 命令是 Redis 内置的机制,用于记录超过指定执行时间的命令。这是你主动识别问题命令的主要工具。

SLOWLOG 的工作原理

Redis 维护一个循环缓冲区,用于存储执行时间超过配置的 slowlog-log-slower-than 阈值(微秒)的命令信息。默认阈值通常是 10 毫秒(10000 微秒)。当此缓冲区填满时,旧条目将被丢弃。

关键 SLOWLOG 子命令

  • SLOWLOG GET [count]:从慢日志中检索最后 count 个条目。如果省略 count,则检索所有条目。
  • SLOWLOG LEN:返回慢日志的当前长度(条目数)。
  • SLOWLOG RESET:清除慢日志条目。请谨慎使用此命令,因为它会永久删除记录的数据。

SLOWLOG 使用示例

假设你怀疑某些命令耗时过长。你可以按如下方式检查慢日志:

# 连接到你的 Redis 实例
redis-cli

# 获取最后 5 个慢命令
127.0.0.1:6379> SLOWLOG GET 5

输出将类似于:

1) 1) (integer) 18
   2) (integer) 1678886400
   3) (integer) 15000
   4) 1) "KEYS"
      2) "*"

2) 1) (integer) 17
   2) (integer) 1678886390
   3) (integer) 12000
   4) 1) "SMEMBERS"
      2) "my_large_set"

...

输出说明:

  1. 条目 ID:慢日志条目的唯一标识符。
  2. 时间戳:命令执行时的 Unix 时间戳。
  3. 执行时间:命令执行所花费的持续时间(微秒)。
  4. 命令和参数:命令本身及其参数。

在上面的示例中,KEYS * 花费了 15000 微秒(15 毫秒),SMEMBERS my_large_set 花费了 12000 微秒(12 毫秒)。如果你的 slowlog-log-slower-than 设置为 10000 微秒,这些将被视为慢命令。

配置 slowlog-log-slower-than

你可以使用 CONFIG SET 命令动态更改 slowlog-log-slower-than 阈值:

127.0.0.1:6379> CONFIG SET slowlog-log-slower-than 50000  # 记录慢于 50ms 的命令

要使此更改在 Redis 重启后持久化,你需要修改 redis.conf 文件并重启 Redis 服务器,或者使用 CONFIG REWRITE 将更改保存到配置文件中。

使用 MONITOR 进行实时命令监控

虽然 SLOWLOG 提供了历史视图,但 MONITOR 提供了 Redis 服务器正在执行的所有命令的实时流。这对于在特定缓慢性能期间进行调试或了解命令流量模式非常宝贵。

MONITOR 的工作原理

当你启用 MONITOR 时,Redis 会为每个接收并处理的命令向 MONITOR 客户端发送响应。这可能会产生非常大量的输出,尤其是在繁忙的 Redis 实例上。因此,通常建议仅在主动调试时谨慎使用 MONITOR

MONITOR 使用示例

在一个单独的 redis-cli 会话中,执行 MONITOR 命令:

# 在 *另一个* 终端中连接到你的 Redis 实例
redis-cli

# 开始监控
127.0.0.1:6379> MONITOR

现在,在另一个 redis-cli 会话或你的应用程序中执行的任何命令都会出现在 MONITOR 输出中。例如,如果你在另一个客户端中运行 SET mykey myvalue,你将看到:

1678887000.123456 [0 127.0.0.1:54321] "SET" "mykey" "myvalue"

使用 MONITOR 进行调试

  1. 重现问题: 当你注意到速度变慢时,立即在专用的 redis-cli 会话中启动 MONITOR
  2. 触发慢操作: 让你的应用程序执行你认为导致速度变慢的操作。
  3. 分析输出: 观察 MONITOR 流中的命令。寻找:
    • 出现时间较长的命令(虽然 MONITOR 本身不显示执行时间,但你可以通过手动计时命令或观察延迟来推断)。
    • 正在执行的异常或意外命令。
    • 可能导致服务器过载的高命令量。
  4. 停止监控:Ctrl+C 退出 MONITOR 命令。

重要提示: 不要在生产环境中长时间运行 MONITOR,因为将每个命令发送到客户端的开销会显著影响 Redis 性能。

慢命令的常见原因及修复方法

根据从 SLOWLOGMONITOR 收集的信息,以下是常见的罪魁祸首及其解决方案:

1. KEYS 命令

  • 问题: KEYS 命令会遍历整个键空间以查找匹配模式的键。在拥有数百万个键的数据库上,这可能需要很长时间,并阻塞 Redis 服务器,影响所有其他客户端。
  • 解决方案: 避免在大型生产键空间上使用 KEYS。当你需要增量键迭代时,请使用 SCANSCAN 在每次调用中返回匹配模式的键子集,这减少了长时间阻塞服务器的可能性。
      # 代替 KEYS user:*
      redis-cli -h <host> -p <port> SCAN 0 MATCH user:* COUNT 100
    
    你需要多次调用 SCAN,使用上一次调用返回的游标,直到游标返回 0。

2. 复杂脚本(Lua 脚本)

  • 问题: 通过 EVALEVALSHA 执行的长时间运行或低效的 Lua 脚本可能会阻塞服务器。虽然 Redis 原子性地执行脚本,但单个长脚本可能会独占事件循环。
  • 解决方案: 优化你的 Lua 脚本。将复杂的逻辑分解为更小、更易于管理的脚本。分析脚本性能。确保脚本中的循环高效且正确终止。对你的脚本进行基准测试,以了解其执行时间。

3. 对大型数据结构的操作

  • 问题: 在拥有数百万成员的集合上执行 SMEMBERS、在非常长的列表上执行 LRANGE 或在巨大的有序集合上执行 ZRANGE 等命令可能会很慢。
  • 解决方案: 避免获取整个大型数据结构。相反,使用迭代命令或分块处理数据:
    • 集合: 使用 SSCAN 代替 SMEMBERS
    • 列表: 使用带有较小 startstop 值的 LRANGE 来分页检索数据。
    • 有序集合: 使用带有 LIMITZRANGEZSCAN

4. 需要键迭代的命令(不太常见但可能)

  • 问题: 虽然不太常见,但如果键空间很大,那些由于其性质可能隐式迭代键的命令可能会很慢。
  • 解决方案: 查看特定命令的 Redis 命令参考,了解其复杂度。如果某个特定命令被证明是瓶颈,请考虑替代的数据结构或方法。

5. 阻塞命令(在现代 Redis 中很少见)

  • 问题: 较旧版本的 Redis 有一些可能阻塞服务器的命令。大多数这些问题已经得到解决或替换。
  • 解决方案: 确保你使用的是最新版本的 Redis。查阅 Redis 文档,了解特定版本中任何已知的阻塞操作。

首先判断是 Redis 慢还是客户端在等待

当有人说“Redis 很慢”时,他们可能指的是几种不同的事情。服务器可能在执行命令上花费了太长时间。客户端可能在网络上等待。连接池可能已耗尽。TLS 代理可能过载。大型响应可能比命令执行花费更长的传输时间。

SLOWLOG 只记录 Redis 内部的命令执行时间。它不包括网络传输时间、客户端排队时间或等待应用程序池连接的时间。这就是为什么干净的慢日志并不总能证明用户是在想象延迟。

比较三种视图:

redis-cli --latency -h <host> -p <port>
redis-cli --latency-history -h <host> -p <port>
redis-cli SLOWLOG GET 10

如果延迟很高但 SLOWLOG 为空,请检查网络、客户端池、服务器 CPU 饱和、fork 活动、持久化或大型回复。如果 SLOWLOG 显示重复的昂贵命令,请从命令和数据结构设计入手。

在应用程序中,在客户端边界添加围绕 Redis 调用的计时。记录命令族、键模式、经过的时间以及客户端是否等待了池连接。不要记录机密或完整的有效负载。少量结构化的计时通常可以回答延迟是在 Redis 内部还是命令到达之前。

将命令复杂度用作气味测试

当 Redis 命令处理少量、有界的数据时,它们很快。当它们扫描大型键空间、返回大量集合或执行与大型值成比例的工作时,它们就变得危险。

在责怪硬件之前,请检查你版本 Redis 命令参考中的命令复杂度。你不需要记住每个复杂度标签,但数据形状很重要:

  • GET user:123 受限于一个值的大小。
  • HGET profile:123 email 受限于一次哈希查找。
  • SMEMBERS followers:celebrity 返回整个集合。
  • KEYS * 扫描整个键空间。
  • LRANGE queue 0 -1 返回整个列表。
  • ZREMRANGEBYSCORE 可能删除大量有序集合成员。

危险的模式通常是“给我一切”。它可能工作数月,然后在一个集合从数百个成员增长到数百万个时失败。Redis 并没有突然变慢;数据跨越了无界命令变得可见的点。

常见慢模式的更安全替代方案

用增量模式替换整个键空间和整个集合的命令。

对于键发现,使用 SCAN

redis-cli --scan --pattern 'user:*'

对于集合,使用 SSCAN

SSCAN active_users 0 COUNT 500

对于哈希,使用 HSCAN

HSCAN user:123:settings 0 COUNT 200

对于有序集合,优先使用带有显式边界和限制的范围:

ZRANGE leaderboard 0 99 WITHSCORES
ZRANGEBYSCORE events 1716600000 1716686400 LIMIT 0 500

对于列表,使用有界范围进行分页:

LRANGE recent_jobs 0 99

SCAN 是增量的,但它不是神奇的免费操作。它可能返回重复项,并且在键发生变化时不会提供完全一致的快照。它适用于维护、迁移和后台发现。它通常不是需要精确实时列表的用户面向请求路径的正确原语。

大型回复可能是真正的成本

一个命令可以快速执行,但如果返回太多数据,仍然会损害你的应用程序。在巨大集合上的 SMEMBERS、在大型哈希上的 HGETALL 或对数千个大值的 MGET 可能会花费时间序列化回复并通过网络发送。这个成本可能不会单独作为命令执行时间清晰地显示出来。

在慢操作期间观察网络输出和客户端内存。如果单个请求返回数十或数百兆字节,请重新设计访问模式。单独存储摘要数据。对结果进行分页。使用有序集合索引并仅获取可见切片。当应用程序通常只需要一个字段时,避免在 Redis 中放置大型文档。

一个实际的例子:如果一个仪表盘显示最新的 50 个作业,不要将每个作业 ID 存储在一个列表中,并在应用程序中切片之前调用 LRANGE jobs 0 -1。以最新优先的顺序存储列表,并仅请求页面所需的内容:

LRANGE jobs:recent 0 49

这个小改变可以消除令人惊讶的延迟和内存压力。

MONITOR 是手术刀,不是仪表盘

当你需要确切地看到客户端发送了什么命令时,MONITOR 很有用,尤其是当你怀疑应用程序正在做的事情与代码审查所暗示的不同时。但在繁忙的 Redis 服务器上,MONITOR 会产生开销并产生大量输出。

在短时间、受控的窗口内使用它:

redis-cli MONITOR | head -n 200

然后停止它。在生产环境中,尽可能优先从应用程序日志、Redis 命令统计信息或短暂的维护窗口中进行采样。

INFO commandstats 通常对于广泛视图更安全:

redis-cli INFO commandstats

它显示每个命令的调用计数和累积微秒。它不会告诉你哪个键很慢,但它可以揭示应用程序正在发出比预期多得多的 HGETALLKEYSEVAL 调用。

Lua 脚本需要边界

Lua 脚本很强大,因为它们原子性地在 Redis 内部运行。同样的原子行为意味着一个长脚本在运行时阻塞其他命令。慢脚本通常来自对大型集合的循环、无界键发现或从一个小助手成长为迷你应用程序的逻辑。

用同样的问题审查脚本:

  • 这可以触及多少个键?
  • 这可以循环遍历多少个元素?
  • 当输入键有一百万个成员时会发生什么?
  • 工作可以分成更小的块吗?
  • 脚本是否返回大型有效负载?

如果一个脚本出现在 SLOWLOG 中,抵制只提高 slowlog-log-slower-than 的诱惑。日志告诉你,一个原子块耗时足够长,以至于影响其他客户端。

持久化、Fork 和作为症状的“慢命令”

有时命令很慢是因为 Redis 忙于后台工作。RDB 快照和 AOF 重写操作会增加 CPU、内存压力和磁盘 I/O。在 Linux 上,fork 一个大型 Redis 进程也可能造成延迟峰值,尤其是在涉及内存超额使用、大页面或慢速存储时。

检查:

redis-cli INFO persistence
redis-cli INFO stats
redis-cli INFO memory
redis-cli LATENCY LATEST

如果延迟峰值与后台保存或 AOF 重写一致,请仔细调整持久化。你可能需要更快的存储、调整后的保存策略、AOF 重写阈值或内存设置。不要仅仅为了让基准测试看起来更好而禁用持久化,除非 Redis 纯粹是一个可丢弃的缓存,并且业务接受丢失数据。

客户端行为可以在没有一个坏命令的情况下使 Redis 过载

一个 Redis 服务器可能被数百万次微小的低效调用所伤害,就像被一个明显缓慢的命令所伤害一样。一个发出 200 次顺序 GET 调用的页面会感觉很慢,即使每个单独的 GET 都很快。

当应用程序需要许多独立命令并且可以容忍一起接收回复时,使用管道:

GET user:1
GET user:2
GET user:3

作为管道发送,避免了每个命令的往返。管道不能替代良好的数据建模,如果批次太大,它可能会增加内存使用。从适中的批次大小开始并进行测量。

还要检查连接池。如果应用程序日志显示 Redis 调用需要 500 毫秒,但 Redis 没有看到慢命令,则应用程序可能在等待空闲连接。只有在检查现有连接为何繁忙之后,才增加池。更大的池可以掩盖症状,同时增加 Redis 的压力。

实用的事件检查清单

当 Redis 延迟损害用户时,按此顺序收集事实:

date -u
redis-cli PING
redis-cli --latency -i 1
redis-cli SLOWLOG GET 20
redis-cli INFO commandstats
redis-cli INFO clients
redis-cli INFO memory
redis-cli INFO persistence
redis-cli LATENCY LATEST

然后询问发生了什么变化:部署、新端点、数据增长事件、批处理作业、迁移、仪表盘查询、新的缓存键模式或持久化重写。Redis 速度变慢通常与某个变得流行的访问模式或某个变得比预期大得多的键有关。

对于每个慢命令,记下键模式和所有者。“慢 SMEMBERS”是不够的。“推荐服务在可以无限增长的集合上调用 SMEMBERS product:123:viewers”才是可操作的。

性能调优检查清单总结

  1. 启用并监控 SLOWLOG:定期查看 SLOWLOG GET 以识别重复出现的慢命令。如有必要,调整 slowlog-log-slower-than
  2. 谨慎使用 MONITOR:用于怀疑速度变慢时的实时调试,但之后立即禁用它。
  3. 在大型生产键空间上避免 KEYS:当真正需要键发现时,使用 SCAN 进行增量迭代。
  4. 优化 Lua 脚本:确保 EVALEVALSHA 脚本高效且不会运行过长时间。
  5. 迭代处理大型数据结构:使用 SSCANZSCAN、带限制的 LRANGESCAN,而不是获取整个集合。
  6. 分析命令参数:确保传递给命令的参数不会导致意外行为(例如,非常大的计数、复杂的模式)。
  7. 监控服务器资源:密切关注 Redis 服务器的 CPU、内存和网络使用情况。慢命令有时可能是服务器负载过重的症状。
  8. 客户端优化:验证你的应用程序没有过快地发送命令或以低效的批次发送。在适当的情况下,考虑对多个命令使用管道。

最终检查

使用 SLOWLOG 查找 Redis 内部缓慢的命令,使用延迟工具捕获服务器端峰值,使用应用程序计时捕获客户端等待。然后修复访问模式,而不仅仅是阈值。有界命令、较小的回复、合理的批处理以及大型键的清晰所有权,比追逐一次性的调优更改更能提高 Redis 性能。