===
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+ COMMAND1 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.): 16151693910Overhead 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 命令获取更多内部运行信息:
# 连接Redisredis-cli# 查看CPU使用情况127.0.0.1:6379> INFO CPU# CPUused_cpu_sys:35986.54used_cpu_user:76382.22used_cpu_sys_children:0.00used_cpu_user_children:0.00# 查看命令统计127.0.0.1:6379> INFO commandstats# Commandstatscmdstat_get:calls=1245,usec=7890,usec_per_call=6.34cmdstat_set:calls=652,usec=4325,usec_per_call=6.63cmdstat_keys:calls=325,usec=187650,usec_per_call=577.38cmdstat_scan:calls=136,usec=65430,usec_per_call=481.10cmdstat_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 101) 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# Clientsconnected_clients:245client_recent_max_input_buffer:8client_recent_max_output_buffer:1blocked_clients:0tracking_clients:0clients_in_timeout_table:175
当前有 245 个客户端连接,其中 175 个处于超时表中,这表明可能有大量的空闲连接没有被正确关闭。
3.6 实时命令监控
使用MONITOR命令(注意:生产环境谨慎使用,会增加额外负载):
127.0.0.1:6379> MONITOROK1633433145.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 itemsBiggest hash found 'user:12345:data' has 3210 fieldsBiggest string found 'cache:html:homepage' has 1823045 bytesBiggest set found 'online:users' has 62345 membersBiggest 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 = 0while True: cursor, partial_keys = redis_client.scan(cursor, match='*user:profile*', count=500) keys.extend(partial_keys) if cursor == 0: break
对于需要查询特定前缀的键,可以使用合适的数据结构组织数据:
# 使用集合存储用户IDredis_client.sadd('all_user_ids', user_id)# 获取所有用户IDuser_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 gzipcompressed_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 操作的限制:
# 设置最大执行时间,防止单个命令长时间占用CPU127.0.0.1:6379> CONFIG SET lua-time-limit 5000127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lru127.0.0.1:6379> CONFIG SET timeout 300 # 空闲连接超时时间# 持久化策略优化,降低AOF重写和RDB生成对CPU的影响127.0.0.1:6379> CONFIG SET appendfsync everysec127.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-streamCONTAINER ID NAME CPU % MEM USAGE / LIMITa5d6e9f8a2b3 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# 分析大keyredis-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))endreturn 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 的最佳实践原则,才能确保高性能和稳定性。通过本次实战,我们积累了宝贵的经验,为后续类似问题的快速排查和解决提供了参考。