Redis 容器 CPU 飙高排查实战
1. 问题背景介绍
在容器化部署环境中,Redis 作为高性能的内存数据库被广泛应用于缓存、会话存储、消息队列等场景。然而,在生产环境中,我们经常会遇到 Redis 容器 CPU 使用率突然飙高的情况,这不仅会影响 Redis 自身的性能,还可能对同一节点上的其他应用造成资源争抵,甚至导致整个系统的不稳定。
近期,我们的生产环境中一个 Redis 容器出现了 CPU 使用率持续高达 90%以上的异常情况,与平时 20%左右的使用率形成鲜明对比。更为严重的是,这导致了业务接口响应时间增加、超时率上升,亟需排查解决。
2. 排查准备工作
2.1 监控数据收集
首先,我们需要收集必要的监控数据,为后续排查提供依据:
# 查看容器CPU使用情况
docker stats redis-container --no-stream
# 或者Kubernetes环境下
kubectl top pod redis-pod -n namespace
2.2 容器环境基本信息获取
# 获取容器详细信息
docker inspect redis-container
# 进入容器内部
docker exec -it redis-container bash
# Kubernetes环境下
kubectl exec -it redis-pod -n namespace -- bash
2.3 排查工具准备
确保以下工具可用:
# 安装基本工具
apt-get update && apt-get install -y procps sysstat net-tools
# 检查Redis命令行工具是否可用
redis-cli -v
3. 排查步骤与过程分析
3.1 容器资源使用情况分析
首先确认容器的 CPU 使用情况,并与资源限制进行对比:
# 查看容器CPU限制
docker inspect redis-container | grep -i cpu
# 输出示例
"CpuShares": 1024,
"NanoCpus": 2000000000, # 表示限制为2核CPU
使用top命令查看容器内进程 CPU 使用情况:
# 进入容器执行top命令
docker exec -it redis-container top
# 输出示例
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 redis 20 0 167364 73428 2364 S 95.0 18.4 390:22.67 redis-server
可以看到 redis-server 进程 CPU 使用率高达 95%,确实存在异常。
3.2 Redis 进程资源占用分析
使用perf工具分析 Redis 进程的 CPU 使用情况:
# 安装perf工具
apt-get install -y linux-perf
# 分析Redis进程
perf top -p $(pgrep redis-server)
# 输出示例
Samples: 42K of event 'cycles', 4000 Hz, Event count (approx.): 16151693910
Overhead Shared Object Symbol
35.27% redis-server [.] dictFind
12.46% redis-server [.] dictGenHashFunction
8.93% redis-server [.] dictScan
7.65% redis-server [.] lookupKey
从输出可以看出,Redis 主要的 CPU 消耗在dictFind、dictGenHashFunction等哈希表操作相关的函数上,这些通常与键查找和哈希计算有关,可能暗示着大量的键值操作或者复杂的数据结构操作。
3.3 Redis 内部运行状态分析
使用 Redis 自带的 INFO 命令获取更多内部运行信息:
# 连接Redis
redis-cli
# 查看CPU使用情况
127.0.0.1:6379> INFO CPU
# CPU
used_cpu_sys:35986.54
used_cpu_user:76382.22
used_cpu_sys_children:0.00
used_cpu_user_children:0.00
# 查看命令统计
127.0.0.1:6379> INFO commandstats
# Commandstats
cmdstat_get:calls=1245,usec=7890,usec_per_call=6.34
cmdstat_set:calls=652,usec=4325,usec_per_call=6.63
cmdstat_keys:calls=325,usec=187650,usec_per_call=577.38
cmdstat_scan:calls=136,usec=65430,usec_per_call=481.10
cmdstat_del:calls=189,usec=1203,usec_per_call=6.37
从命令统计可以看出,keys和scan命令虽然调用次数不是最多的,但每次调用的耗时却非常高,这是一个值得关注的点。
3.4 慢查询日志分析
# 查看慢查询配置
127.0.0.1:6379> CONFIG GET slowlog-*
1) "slowlog-log-slower-than"
2) "10000" # 10毫秒
3) "slowlog-max-len"
4) "128"
# 获取慢查询日志
127.0.0.1:6379> SLOWLOG GET 10
1) 1) (integer) 142
2) (integer) 1633432982
3) (integer) 587345
4) 1) "keys"
2) "*user:profile*"
2) 1) (integer) 141
2) (integer) 1633432975
3) (integer) 498234
4) 1) "keys"
2) "*session*"
慢查询日志显示,有多个keys命令执行时间超过了设定的阈值,而且使用了通配符模式进行查询,这在键值较多的情况下会非常消耗 CPU 资源。
3.5 连接与客户端分析
# 查看客户端连接信息
127.0.0.1:6379> INFO clients
# Clients
connected_clients:245
client_recent_max_input_buffer:8
client_recent_max_output_buffer:1
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:175
当前有 245 个客户端连接,其中 175 个处于超时表中,这表明可能有大量的空闲连接没有被正确关闭。
3.6 实时命令监控
使用MONITOR命令(注意:生产环境谨慎使用,会增加额外负载):
127.0.0.1:6379> MONITOR
OK
1633433145.585954 [0 172.16.0.45:53421] "keys" "*user:profile*"
1633433146.123456 [0 172.16.0.46:54123] "keys" "*session*"
1633433146.585954 [0 172.16.0.47:55123] "scan" "0" "MATCH" "*order:*" "COUNT" "1000"
从监控输出可以看到,确实有多个客户端频繁地执行keys和scan命令,而且使用了通配符模式。
3.7 大 key 分析
使用 Redis 自带的工具分析大 key:
# 在容器外执行
docker exec -it redis-container redis-cli --bigkeys
# 输出摘要
-------- summary -------
Sampled 1000000 keys in the keyspace!
Total key length in bytes is 52686875 (avg len 52.69)
Biggest list found 'delayed:jobs' has 154345 items
Biggest hash found 'user:12345:data' has 3210 fields
Biggest string found 'cache:html:homepage' has 1823045 bytes
Biggest set found 'online:users' has 62345 members
Biggest zset found 'leaderboard:daily' has 10000 members
分析显示有一些明显的大 key,特别是delayed:jobs列表含有大量元素,cache:html:homepage字符串非常大。
4. 问题原因定位与解决
结合以上排查结果,我们可以确定导致 Redis 容器 CPU 飙高的主要原因:
- 高复杂度命令频繁执行:
keys命令在生产环境中被频繁调用,这是一个 O(N)复杂度的命令,当键值对数量较多时会严重消耗 CPU 资源。 - 大 key 处理不当:存在多个大型数据结构,如大列表和大字符串,这些结构的操作也会消耗更多的 CPU 资源。
- 客户端连接管理问题:大量客户端连接且部分处于超时状态,增加了 Redis 的连接管理开销。
4.1 解决措施
4.1.1 替换高复杂度命令
修改应用代码,将keys命令替换为更高效的scan命令,并合理设置 scan 的 count 参数:
# 优化前
keys = redis_client.keys('*user:profile*')
# 优化后
keys = []
cursor = 0
while True:
cursor, partial_keys = redis_client.scan(cursor, match='*user:profile*', count=500)
keys.extend(partial_keys)
if cursor == 0:
break
对于需要查询特定前缀的键,可以使用合适的数据结构组织数据:
# 使用集合存储用户ID
redis_client.sadd('all_user_ids', user_id)
# 获取所有用户ID
user_ids = redis_client.smembers('all_user_ids')
# 然后使用pipeline批量获取用户数据
pipeline = redis_client.pipeline()
for user_id in user_ids:
pipeline.hgetall(f'user:{user_id}:data')
user_data = pipeline.execute()
4.1.2 大 key 处理优化
对于大列表,可以考虑分片存储:
# 优化前
redis_client.rpush('delayed:jobs', job_data) # 一个超大列表
# 优化后 - 使用多个列表分片
shard_id = hash(job_id) % 10 # 简单的分片策略
redis_client.rpush(f'delayed:jobs:{shard_id}', job_data)
# 读取时合并结果
all_jobs = []
for i in range(10):
all_jobs.extend(redis_client.lrange(f'delayed:jobs:{i}', 0, -1))
对于大字符串,可以考虑压缩或拆分:
# 优化前
redis_client.set('cache:html:homepage', large_html_content)
# 优化后 - 使用压缩
import gzip
compressed_content = gzip.compress(large_html_content.encode())
redis_client.set('cache:html:homepage', compressed_content)
# 读取时解压
compressed_data = redis_client.get('cache:html:homepage')
original_content = gzip.decompress(compressed_data).decode()
4.1.3 连接池优化
修改应用端连接池配置,确保正确关闭连接:
# 优化前 - 可能存在连接泄漏
redis_client = redis.Redis(host='redis-host', port=6379)
# 优化后 - 使用连接池
pool = redis.ConnectionPool(host='redis-host', port=6379, max_connections=100)
redis_client = redis.Redis(connection_pool=pool)
# 确保在不需要时释放连接
redis_client.connection_pool.disconnect()
4.1.4 Redis 配置优化
修改 Redis 配置,增加对大 key 操作的限制:
# 设置最大执行时间,防止单个命令长时间占用CPU
127.0.0.1:6379> CONFIG SET lua-time-limit 5000
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lru
127.0.0.1:6379> CONFIG SET timeout 300 # 空闲连接超时时间
# 持久化策略优化,降低AOF重写和RDB生成对CPU的影响
127.0.0.1:6379> CONFIG SET appendfsync everysec
127.0.0.1:6379> CONFIG SET save "900 1 300 10 60 10000"
5. 效果验证与长期优化
5.1 实施效果
应用上述优化措施后,Redis 容器的 CPU 使用率从之前的 90%以上降低到了正常的 20%左右,业务接口响应时间也恢复正常。
# 优化后容器CPU使用情况
docker stats redis-container --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT
a5d6e9f8a2b3 redis-container 23.45% 512MiB / 1GiB
5.2 长期监控与优化策略
5.2.1 建立完善的监控体系
# 使用Prometheus + Grafana监控Redis指标
# Redis exporter配置示例
docker run -d \
--name redis_exporter \
-p 9121:9121 \
--network=host \
oliver006/redis_exporter \
--redis.addr=redis://localhost:6379
5.2.2 定期检查大 key 和慢日志
创建定期任务,每天分析 Redis 大 key 和慢查询日志:
#!/bin/bash
# 保存为daily_redis_check.sh
# 分析大key
redis-cli --bigkeys > /var/log/redis/bigkeys_$(date +%Y%m%d).log
# 获取慢日志
redis-cli SLOWLOG GET 100 > /var/log/redis/slowlog_$(date +%Y%m%d).log
# 可以设置为cron任务
# 0 2 * * * /path/to/daily_redis_check.sh
5.2.3 自动化告警设置
设置合理的 CPU 使用率告警阈值,并进行多级别告警:
# Prometheus告警规则示例
groups:
- name: redis_alerts
rules:
- alert: RedisCpuUsageHigh
expr: container_cpu_usage_percentage{container_name="redis-container"} > 70
for: 5m
labels:
severity: warning
annotations:
summary: "Redis CPU usage high"
description: "Redis container {{ $labels.container_name }} CPU usage is {{ $value }}%"
- alert: RedisCpuUsageCritical
expr: container_cpu_usage_percentage{container_name="redis-container"} > 90
for: 2m
labels:
severity: critical
annotations:
summary: "Redis CPU usage critical"
description: "Redis container {{ $labels.container_name }} CPU usage is {{ $value }}%"
6. 最佳实践与经验总结
6.1 Redis 使用的最佳实践
- 避免使用 O(N)复杂度的命令:在生产环境中,避免使用
keys、flushall、flushdb等高复杂度命令。 - 合理使用数据结构:根据业务需求选择合适的数据结构,避免单个 key 存储过多数据。
- 设置合理的过期时间:为键值设置合理的过期时间,避免数据无限增长。
- 使用批量操作:尽可能使用 pipeline 或 multi/exec 批量执行命令,减少网络往返。
# 使用pipeline批量操作
pipeline = redis_client.pipeline()
for i in range(1000):
pipeline.set(f'key:{i}', f'value:{i}')
pipeline.execute()
- 谨慎使用 Lua 脚本:Lua 脚本执行期间会阻塞 Redis,确保脚本执行时间短。
-- 高效的Lua脚本示例,批量删除特定前缀的键
local keys = redis.call('scan', 0, 'MATCH', ARGV[1], 'COUNT', 1000)
local cursor = keys[1]
local found = keys[2]
if #found > 0 then
redis.call('del', unpack(found))
end
return cursor
6.2 容器环境中 Redis 优化建议
- 合理设置资源限制:为 Redis 容器分配足够且合理的 CPU 和内存资源。
# Kubernetes资源配置示例
resources:
requests:
cpu: "1"
memory: "1Gi"
limits:
cpu: "2"
memory: "2Gi"
- 注意网络配置:确保容器网络延迟低,带宽充足。
- 持久化与容器存储:使用高性能的持久化卷,减少 IO 对 CPU 的影响。
- 考虑使用 Redis 集群:对于高负载场景,考虑使用 Redis 集群分担压力。
# Redis集群创建示例
redis-cli --cluster create \
172.16.0.11:6379 172.16.0.12:6379 172.16.0.13:6379 \
172.16.0.14:6379 172.16.0.15:6379 172.16.0.16:6379 \
--cluster-replicas 1
6.3 应急处理流程
当再次遇到 Redis CPU 飙高问题时,可以按照以下流程快速处理:
-
立即检查慢查询:
redis-cli SLOWLOG GET 10 -
快速定位热点 key:
redis-cli --hotkeys -
临时限制问题客户端:
# 查找可疑客户端 redis-cli CLIENT LIST | grep -v "idle=0" # 必要时断开连接 redis-cli CLIENT KILL ADDR 172.16.0.45:53421 -
紧急调整配置:
# 临时禁用危险命令 redis-cli CONFIG SET rename-command KEYS "KEYS_DISABLED"
7. 结论
通过系统性的排查与分析,我们成功解决了 Redis 容器 CPU 飙高的问题。问题的根源主要是不合理的命令使用(特别是keys命令)和大 key 处理不当导致的。通过优化应用代码、调整 Redis 配置、合理设置连接池和建立长期监控机制,我们不仅解决了当前问题,还为系统的长期稳定运行奠定了基础。
在容器环境中运行 Redis 需要特别注意资源管理和性能监控,遵循 Redis 的最佳实践原则,才能确保高性能和稳定性。通过本次实战,我们积累了宝贵的经验,为后续类似问题的快速排查和解决提供了参考。