返回

Redis 变慢?大 Key/CPU/网络/持久化全方位排查

java

Redis 响应慢?别只盯着内存,揪出延迟真凶

最近有朋友发现 Redis 响应变慢,延迟似乎增加了,想从内存信息里找找线索,看看是不是哪里设置不对,导致了性能下降。下面是他提供的 INFO MEMORY 输出:

10.32.10.112:6379> info memory
# Memory
used_memory:35113416
used_memory_human:33.49M
used_memory_rss:43671552
used_memory_rss_human:41.65M
used_memory_peak:52099136
used_memory_peak_human:49.69M
used_memory_peak_perc:67.40%
used_memory_overhead:11478317
used_memory_startup:9234208
used_memory_dataset:23635099
used_memory_dataset_perc:91.33%
allocator_allocated:35388888
allocator_active:37056512
allocator_resident:42283008
total_system_memory:67260600320
total_system_memory_human:62.64G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:34359738368
maxmemory_human:32.00G
maxmemory_policy:noeviction
allocator_frag_ratio:1.05
allocator_frag_bytes:1667624
allocator_rss_ratio:1.14
allocator_rss_bytes:5226496
rss_overhead_ratio:1.03
rss_overhead_bytes:1388544
mem_fragmentation_ratio:1.25
mem_fragmentation_bytes:8722016
mem_not_counted_for_evict:0
mem_replication_backlog:1048576
mem_clients_slaves:17186
mem_clients_normal:859459
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0

这位朋友还提到,他有一个包含大约 20 万个键的 Hash 结构,里面的值类似 virt,GGUGUYGYUGYJFF600AEE4AJHJHDD,ssd,uttj,HBHJDBJHDBBFF877779799,JHIJJJIOJ00AEE4AIUHIHID 这种逗号分隔的长字符串。

INFO MEMORY 说了啥?初步分析

我们先来看看这份内存报告的关键信息:

  • used_memory_human: 33.49M:这是 Redis 目前实际存储数据加上自身管理开销占用的内存。看着不大。
  • used_memory_rss_human: 41.65M:这是操作系统视角下,Redis 进程实际占用的物理内存(Resident Set Size)。比 used_memory 多一点,这部分差额可能来自内存碎片或者内存分配器的额外开销。
  • maxmemory_human: 32.00G:Redis 被限制最多只能使用 32G 内存。
  • used_memory / maxmemory:当前使用的 33.49M 距离 32G 的上限差得很远,连零头都不到 (大约 0.1%)。maxmemory_policy 还是 noeviction(不淘汰数据),所以,内存容量不足肯定不是这次延迟问题的原因
  • mem_fragmentation_ratio: 1.25:内存碎片率。这个值是 used_memory_rss / used_memory 算出来的。1.25 意味着操作系统分配给 Redis 的物理内存比 Redis 内部记录使用的内存多了 25%。这个值不算特别低,但也通常不至于引起严重的延迟 。大于 1.5 时需要关注,大于 2 就比较糟糕了。这里的 1.25 提示存在一定的内存碎片,占用了大约 mem_fragmentation_bytes: 8722016 (8.3MB) 的额外物理内存。
  • allocator_frag_ratio: 1.05:内存分配器(jemalloc)层面的碎片率。这个值很低,挺好。
  • total_system_memory_human: 62.64G:服务器总内存很充足。

初步结论:单看 INFO MEMORY没有发现直接能导致响应延迟显著增加的内存“硬伤” 。内存使用量远低于上限,碎片率虽然不是完美,但也还在可接受范围。那问题可能出在哪儿呢?

延迟元凶大搜查:可能的原因与解决方案

INFO MEMORY 只是冰山一角。Redis 响应变慢的原因多种多样,内存本身往往不是唯一的因素,有时甚至不是主要因素。结合朋友提到的那个 20 万 key 的大 Hash,我们来排查一下更可能的“嫌疑犯”。

1. 大 Key “重灾区”:警惕单个 Hash 过大

这是非常可疑 的一个点。

  • 原理剖析:

    • Redis 处理命令是单线程的(网络 IO 和一些后台操作除外)。一个命令执行慢了,会阻塞后面所有的命令。
    • 操作一个包含大量成员的 Hash、Set、ZSet 或 List 时,某些命令的复杂度会随着成员数量增加而升高。
    • 对于 Hash 结构:
      • HGETALL, HLEN, HKEYS, HVALS 这类需要遍历整个 Hash 的命令,当 Hash 包含 20 万个 field 时,耗时会非常可观 ,直接阻塞 Redis。HLEN 还好,但 HGETALL 这种要拷贝所有数据的命令简直是灾难。
      • 即使是 HGET(获取单个 field),随着 Hash 内 field 数量的增加,内部哈希表的查找也可能因为哈希冲突增多、rehash 等因素,性能略微下降(虽然理论复杂度是 O(1),但在极端规模下常量因子不可忽略)。
      • 存储过长的 Value(比如那个逗号分隔的长字符串),也会增加网络传输和内存拷贝的开销。
  • 解决方案:

    • 拆分大 Hash (Hash Splitting/Sharding):

      • 原理: 把一个巨大的 Hash 拆成多个小 Hash。比如,原来的 my_large_hashfield1field200000,可以按某种规则(例如,按 field key 的哈希值或者前缀)将其分散到 my_hash_shard_1, my_hash_shard_2, ... , my_hash_shard_N 中。

      • 操作示例 (思路):

        • 应用层逻辑修改:写入时,根据 field key 计算出目标分片 ID (e.g., shard_id = hash(field_key) % N),然后写入 HSET my_hash_shard_{shard_id} field_key value。读取时同样计算分片 ID 再 HGET
        • 如果需要遍历,不要再想着用 HGETALL 了。你需要自己遍历所有分片 (my_hash_shard_0my_hash_shard_{N-1}),对每个分片执行 HGETALL (或者更好的 HSCAN)。这显然变复杂了,所以在设计初期就应避免大 Key。
      • 命令行示例 (辅助迁移): 没有直接的命令自动拆分,通常需要写脚本。可以用 HSCAN 迭代旧 Hash,然后 HSET 到新的分片 Hash。

        # 概念性演示,实际需要脚本逻辑
        redis-cli HSCAN my_large_hash 0 COUNT 1000 | while read -r field; read -r value; do
          shard_id=$(calculate_shard_id "$field") # 假设有个函数/工具
          redis-cli HSET "my_hash_shard_$shard_id" "$field" "$value"
        done
        # 注意:HSCAN 返回的是 field 和 value 交替列表,处理起来需要配对
        # COUNT 只是建议值,不保证精确数量
        
    • 避免慢命令 (HGETALL, HKEYS),使用 HSCAN

      • 原理: HSCAN 命令用于迭代 Hash 中的 field-value 对,它使用游标分页返回结果,不会一次性加载所有数据,避免了长时间阻塞。

      • 命令行示例:

        # 从游标 0 开始扫描 my_large_hash,每次最多返回约 100 对
        redis-cli HSCAN my_large_hash 0 COUNT 100
        
        # 返回结果类似:
        # 1) "196608"  # 下一个游标
        # 2) 1) "field_abc"
        #    2) "value_for_abc"
        #    3) "field_xyz"
        #    4) "value_for_xyz"
        #    ... (更多 field-value 对)
        
        # 下一次扫描,使用上次返回的游标 "196608"
        redis-cli HSCAN my_large_hash 196608 COUNT 100
        
        # 重复扫描,直到返回的游标为 "0"
        
      • 注意: 应用代码需要循环调用 HSCAN,直到游标变回 0。

    • 数据结构优化 (可能的话):

      • 原理: 检查一下那个逗号分隔的长字符串 Value。是不是经常只需要其中的一部分?如果是,把它拆分成 Hash 内的多个 field,或者甚至拆成独立的 Redis Key,可能更高效。
      • 示例: 如果 virt,GGUGUYGYUGYJFF600AEE4AJHJHDD,ssd,uttj,... 这串东西经常需要单独访问 virtssd 对应的部分,不如存成:
        • HSET my_hash field_key:type "virt"
        • HSET my_hash field_key:id "GGUGUYGYUGYJFF600AEE4AJHJHDD"
        • HSET my_hash field_key:disk "ssd"
        • 或者干脆用顶级 Key: SET field_key:type "virt" 等。这需要权衡 Key 的数量和单个 Value 的大小/访问模式。
  • 进阶技巧:

    • Pipeline/Multi: 如果确实需要一次获取大 Hash 中的多个(但不是全部)field,使用 HMGET 或者将多个 HGET 请求通过 Pipeline 批量发送,可以减少网络 RTT (Round-Trip Time) 开销。

2. CPU 到顶了吗?检查计算资源

  • 原理剖析: Redis 的命令处理主要靠单个 CPU 核心。如果 Redis Server 所在机器的某个 CPU 核心被长时间占满(接近 100%),那么所有客户端请求的响应都会变慢。高 QPS、复杂命令(即使不是针对大 Key 的,比如 SORTLUA 脚本)都可能导致 CPU 瓶颈。

  • 解决方案:

    • 监控 CPU 使用率:

      • Redis 命令:

        redis-cli INFO CPU
        # 关注 `used_cpu_sys` 和 `used_cpu_user` 两项的总和,它们代表 Redis Server 进程消耗的 CPU 时间(秒)。
        # 多次执行,观察其增长率。高增长率意味着高 CPU 消耗。
        # 还可以看 `used_cpu_sys_children` 和 `used_cpu_user_children`,了解后台进程(如 BGSAVE/AOF rewrite)的 CPU 消耗。
        
      • 操作系统工具:

        # 找到 redis-server 进程的 PID
        ps aux | grep redis-server
        # 使用 top 或 htop 实时监控指定 PID 的 CPU%
        top -p <redis_pid>
        htop -p <redis_pid>
        

        观察 %CPU 列是否经常达到或接近 100%。

    • 定位耗时命令:

      • 慢查询日志 (Slow Log): Redis 可以记录执行时间超过阈值的命令。

        # 查看当前慢查询日志配置
        redis-cli CONFIG GET slowlog-log-slower-than
        redis-cli CONFIG GET slowlog-max-len
        
        # 设置记录执行时间超过 10 毫秒 (10000 微秒) 的命令
        redis-cli CONFIG SET slowlog-log-slower-than 10000
        
        # 获取最近 10 条慢查询记录
        redis-cli SLOWLOG GET 10
        
        # 清空慢查询日志
        redis-cli SLOWLOG RESET
        

        检查慢查询日志,看看是不是有意外的命令(可能是操作大 Hash 的命令)耗时过长。

      • MONITOR 命令 (慎用): 实时打印 Redis 执行的所有命令。

        redis-cli MONITOR
        

        安全建议: MONITOR显著影响 Redis 性能 ,因为它需要把所有命令都转发一份出来。只在调试时短时间使用,绝不能在生产环境长期运行

  • 进阶技巧:

    • 使用 redis-cli --latency 相关工具 检测 Redis 命令处理的基础延迟。
    • 如果 CPU 确实是瓶颈,且优化命令无效(比如业务 QPS 实在太高),可以考虑:
      • Redis 集群 (Cluster) 分散压力。
      • 读写分离(主从架构,读请求打到从库)。
      • 升级服务器硬件。

3. 网络抖动:不一定是 Redis 的锅

  • 原理剖析: 客户端和 Redis 服务器之间的网络延迟、丢包或带宽不足,都会直接体现在应用的感知层面,让人觉得 Redis “慢”了。

  • 解决方案:

    • 基础连通性和延迟测试:
      • 应用服务器 ping Redis 服务器 IP。

        ping <redis_server_ip>
        

        关注 time= 值,看是否有异常升高或不稳定的情况。

      • 使用 traceroutemtr 跟踪路由,看网络路径中哪一跳延迟高。

        traceroute <redis_server_ip>
        mtr <redis_server_ip>
        
    • Redis 延迟测试工具:
      • redis-cli 自带的延迟测试工具,测量 PING 命令的 RTT。

        redis-cli --latency -h <redis_server_ip> -p <redis_port>
        # 持续测试
        redis-cli --latency-history -h <redis_server_ip> -p <redis_port>
        

        这个工具可以反映网络 + Redis 命令处理(PING 命令本身很快)的综合延迟。如果这个延迟都很高,网络问题可能性大。

    • 检查服务器网络流量和状态:
      • 在 Redis 服务器上使用 iftop, nload 查看网络带宽使用情况。
      • 使用 netstat -sss -s 查看 TCP 连接统计,关注重传 (retransmits)、错误 (errors) 等指标。
  • 安全建议: 无特定安全建议,主要是网络排查思路。

4. 内存碎片:小麻烦也可能滚雪球

  • 原理剖析: 前面提到 mem_fragmentation_ratio: 1.25 不算非常高。但内存碎片过多(例如 ratio > 1.5)时,会浪费物理内存,降低内存使用效率。极端情况下,如果碎片导致 Redis 实际需要的物理内存(RSS)触碰到系统内存上限(即使 used_memory 远低于 maxmemory),可能引发系统层面的性能问题(比如,如果系统开始使用 swap,虽然这里看起来不太可能)。碎片整理本身也可能带来微小的性能影响。同时,高碎片率会使得 BGSAVEAOF rewritefork() 操作的 Copy-on-Write (CoW) 成本增加,因为更多内存页可能被拷贝,加剧 fork() 期间的延迟。

  • 解决方案:

    • 主动碎片整理 (Redis 4.0+):

      • 原理: Redis 可以在运行时尝试整理内存碎片,将分散的数据聚合到连续的内存块中,然后将空闲的内存块还给操作系统。

      • 操作指令:

        # 开启主动碎片整理 (需要 redis.conf 配置或动态修改)
        redis-cli CONFIG SET activedefrag yes
        # 可以在 redis.conf 文件中设置 activedefrag yes 使其持久化
        
        # 查看整理状态
        redis-cli INFO MEMORY # 里面的 active_defrag_running 等相关字段
        
      • 注意: 主动碎片整理会消耗一定的 CPU 资源。可以通过调整 active-defrag-ignore-bytes, active-defrag-threshold-lower, active-defrag-threshold-upper, active-defrag-cycle-min, active-defrag-cycle-max 等参数来控制其行为和资源消耗。

    • 重启 Redis 服务 (终极手段):

      • 原理: 重启后,Redis 从持久化文件加载数据,内存是重新分配的,自然就没有碎片了。
      • 操作: 安全地关闭 Redis (SHUTDOWN SAVE 或等待 BGSAVE 完成后 SHUTDOWN NOSAVE),然后重新启动。
      • 建议: 这是最有效的清理碎片的方法,但会导致服务中断(除非有高可用架构)。最好安排在业务低峰期进行。
  • 进阶技巧:

    • 监控 mem_fragmentation_ratiomem_fragmentation_bytes 的趋势。如果持续增长且达到较高水平(比如 > 1.5),考虑上述整理或重启方案。
    • 选择合适的内存分配器 (jemalloc 通常是默认且较好的选择) 并考虑其版本。

5. 持久化 BGSAVE / AOF 重写:后台也可能添堵

  • 原理剖析: Redis 的 RDB 快照 (BGSAVE) 和 AOF 文件重写 (BGREWRITEAOF) 都是通过 fork() 子进程来完成的。fork() 操作会创建一个父进程的副本。现代操作系统使用 Copy-on-Write (CoW) 技术来优化 fork(),即父子进程共享内存页,只有当一方尝试写入共享页时,该页才会被复制一份。
    • 问题:fork() 发生的瞬间,需要复制父进程的页表,这个过程本身会消耗时间,导致 Redis 短暂阻塞(通常是毫秒级,但可能更长)。
    • 问题: 在子进程进行持久化期间,如果父进程接收到写操作,修改了数据,那么被修改的内存页就需要被复制(CoW),这会增加父进程的内存开销,并可能因为频繁的页面复制操作导致父进程(即服务客户端请求的进程)处理延迟增加。即使 used_memory 不大,但如果在 fork 期间有大量写入,CoW 的影响也会存在。
  • 解决方案:
    • 检查 fork() 耗时:

      redis-cli INFO STATS | grep latest_fork_usec
      # 这个值记录了最后一次 fork() 操作消耗的微秒数。
      # 如果这个值很大 (比如超过 1 秒,即 1,000,000 微秒),说明 fork() 操作本身就是一个潜在的延迟源头。
      
    • 调整持久化策略:

      • RDB: 如果 save 配置过于频繁(例如 save 60 1000),考虑放宽条件,或者主要依赖 AOF,只在低峰期手动 BGSAVE 或使用较长的时间间隔。
      • AOF: 调整 auto-aof-rewrite-percentageauto-aof-rewrite-min-size,避免在高峰期自动触发重写。可以考虑手动在低峰期执行 BGREWRITEAOF
    • 操作系统优化 (Transparent Huge Pages - THP):

      • 原理: THP 是 Linux 内核的一个特性,试图自动使用 2MB 大小的内存页代替默认的 4KB 页,以减少 TLB (Translation Lookaside Buffer) Miss。但在使用 fork() + CoW 的场景下 (Redis 持久化就是典型),THP 可能导致更严重的延迟和内存膨胀,因为即使只修改一个字节,也需要复制整个 2MB 的大页。

      • 建议: 大多数 Redis 部署场景建议禁用 THP

      • 操作指令:

        # 检查 THP 状态 (通常是 always, madvise, never)
        cat /sys/kernel/mm/transparent_hugepage/enabled
        cat /sys/kernel/mm/transparent_hugepage/defrag
        
        # 临时禁用 THP (重启后失效)
        echo never > /sys/kernel/mm/transparent_hugepage/enabled
        echo never > /sys/kernel/mm/transparent_hugepage/defrag
        
        # 永久禁用:需要修改 /etc/rc.local 或使用 systemd 服务配置,具体方法取决于操作系统。
        
      • 安全建议: 禁用 THP 是系统级别的改动,可能影响其他依赖 THP 获得性能提升的应用。务必评估影响范围,最好在测试环境验证。

6. 其他潜在因素:连接数、慢查询等

  • 原理剖析:
    • 过多连接数: 每个连接都会消耗一定的内存和文件符资源。非常大量的连接(即使很多是空闲的)也可能给 Redis 的事件循环带来压力。查看 INFO CLIENTS 中的 connected_clients
    • 其他慢命令: 除了操作大 Key 的命令,还有像 KEYS * (绝对禁止在生产环境使用)、对大集合执行 SORT、复杂的 LUA 脚本等也可能引起阻塞。用 SLOWLOG GET 排查。
    • Swap 使用: 虽然从 INFO MEMORY 看 Redis 本身没用多少内存,但需要确认操作系统层面完全没有 使用 Swap 交换空间。如果系统因为其他进程的压力而使用了 Swap,Redis 即使自身内存不大,也可能因为内存页被换出/换入而性能骤降。用 free -hvmstat 检查。

下一步怎么查?

基于以上分析,排查 Redis 延迟问题的建议步骤:

  1. 重点怀疑大 Hash: 这是目前信息中最可疑的点。检查应用代码中所有操作这个大 Hash 的地方,特别是 HGETALL, HKEYS 等。优先使用 HSCAN 替代,或者考虑拆分 Hash。用 SLOWLOG GET 确认是不是这些命令惹的祸。
  2. 检查 CPU: 使用 INFO CPUtop/htop 监控 Redis 进程的 CPU 使用率。如果 CPU 饱和,结合 SLOWLOG 找到耗 CPU 的命令并优化。
  3. 检查网络: 从客户端到服务器进行 ping, mtr, redis-cli --latency 测试,排除网络本身的问题。
  4. 检查持久化影响: 查看 latest_fork_usec,如果耗时过长,考虑调整持久化策略和禁用 THP。
  5. 评估内存碎片: mem_fragmentation_ratio 为 1.25,可以暂时放缓,但如果其他问题都排除了,或者该比率未来升高,可以考虑主动整理或计划重启。
  6. 收集更多信息: 需要时,获取 INFO ALL 可以提供更全面的状态。redis-cli LATENCY LATESTredis-cli LATENCY HISTORY command_name 可以查看特定命令的延迟分布。

解决 Redis 延迟问题通常需要综合分析,INFO MEMORY 只是起点,往往需要结合 SLOWLOG、CPU/网络监控、甚至应用端的调用日志,才能揪出真正的“元凶”。