Redis 变慢?大 Key/CPU/网络/持久化全方位排查
2025-04-15 04:39:10
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_hash
有field1
到field200000
,可以按某种规则(例如,按 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_0
到my_hash_shard_{N-1}
),对每个分片执行HGETALL
(或者更好的HSCAN
)。这显然变复杂了,所以在设计初期就应避免大 Key。
- 应用层逻辑修改:写入时,根据 field key 计算出目标分片 ID (e.g.,
-
命令行示例 (辅助迁移): 没有直接的命令自动拆分,通常需要写脚本。可以用
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,...
这串东西经常需要单独访问virt
或ssd
对应的部分,不如存成: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) 开销。
- Pipeline/Multi: 如果确实需要一次获取大 Hash 中的多个(但不是全部)field,使用
2. CPU 到顶了吗?检查计算资源
-
原理剖析: Redis 的命令处理主要靠单个 CPU 核心。如果 Redis Server 所在机器的某个 CPU 核心被长时间占满(接近 100%),那么所有客户端请求的响应都会变慢。高 QPS、复杂命令(即使不是针对大 Key 的,比如
SORT
、LUA 脚本
)都可能导致 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=
值,看是否有异常升高或不稳定的情况。 -
使用
traceroute
或mtr
跟踪路由,看网络路径中哪一跳延迟高。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 -s
或ss -s
查看 TCP 连接统计,关注重传 (retransmits)、错误 (errors) 等指标。
- 在 Redis 服务器上使用
- 基础连通性和延迟测试:
-
安全建议: 无特定安全建议,主要是网络排查思路。
4. 内存碎片:小麻烦也可能滚雪球
-
原理剖析: 前面提到
mem_fragmentation_ratio: 1.25
不算非常高。但内存碎片过多(例如 ratio > 1.5)时,会浪费物理内存,降低内存使用效率。极端情况下,如果碎片导致 Redis 实际需要的物理内存(RSS)触碰到系统内存上限(即使used_memory
远低于maxmemory
),可能引发系统层面的性能问题(比如,如果系统开始使用 swap,虽然这里看起来不太可能)。碎片整理本身也可能带来微小的性能影响。同时,高碎片率会使得BGSAVE
或AOF rewrite
时fork()
操作的 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_ratio
和mem_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-percentage
和auto-aof-rewrite-min-size
,避免在高峰期自动触发重写。可以考虑手动在低峰期执行BGREWRITEAOF
。
- RDB: 如果
-
操作系统优化 (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 -h
或vmstat
检查。
- 过多连接数: 每个连接都会消耗一定的内存和文件符资源。非常大量的连接(即使很多是空闲的)也可能给 Redis 的事件循环带来压力。查看
下一步怎么查?
基于以上分析,排查 Redis 延迟问题的建议步骤:
- 重点怀疑大 Hash: 这是目前信息中最可疑的点。检查应用代码中所有操作这个大 Hash 的地方,特别是
HGETALL
,HKEYS
等。优先使用HSCAN
替代,或者考虑拆分 Hash。用SLOWLOG GET
确认是不是这些命令惹的祸。 - 检查 CPU: 使用
INFO CPU
和top
/htop
监控 Redis 进程的 CPU 使用率。如果 CPU 饱和,结合SLOWLOG
找到耗 CPU 的命令并优化。 - 检查网络: 从客户端到服务器进行
ping
,mtr
,redis-cli --latency
测试,排除网络本身的问题。 - 检查持久化影响: 查看
latest_fork_usec
,如果耗时过长,考虑调整持久化策略和禁用 THP。 - 评估内存碎片:
mem_fragmentation_ratio
为 1.25,可以暂时放缓,但如果其他问题都排除了,或者该比率未来升高,可以考虑主动整理或计划重启。 - 收集更多信息: 需要时,获取
INFO ALL
可以提供更全面的状态。redis-cli LATENCY LATEST
和redis-cli LATENCY HISTORY command_name
可以查看特定命令的延迟分布。
解决 Redis 延迟问题通常需要综合分析,INFO MEMORY
只是起点,往往需要结合 SLOWLOG
、CPU/网络监控、甚至应用端的调用日志,才能揪出真正的“元凶”。