Redis 容器 CPU 飙高排查实战

355 阅读4分钟

===

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 消耗在dictFinddictGenHashFunction等哈希表操作相关的函数上,这些通常与键查找和哈希计算有关,可能暗示着大量的键值操作或者复杂的数据结构操作。

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

从命令统计可以看出,keysscan命令虽然调用次数不是最多的,但每次调用的耗时却非常高,这是一个值得关注的点。

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"

从监控输出可以看到,确实有多个客户端频繁地执行keysscan命令,而且使用了通配符模式。

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 飙高的主要原因:

  1. 高复杂度命令频繁执行keys命令在生产环境中被频繁调用,这是一个 O(N)复杂度的命令,当键值对数量较多时会严重消耗 CPU 资源。

  2. 大 key 处理不当:存在多个大型数据结构,如大列表和大字符串,这些结构的操作也会消耗更多的 CPU 资源。

  3. 客户端连接管理问题:大量客户端连接且部分处于超时状态,增加了 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 使用的最佳实践

  1. 避免使用 O(N)复杂度的命令:在生产环境中,避免使用keysflushallflushdb等高复杂度命令。

  2. 合理使用数据结构:根据业务需求选择合适的数据结构,避免单个 key 存储过多数据。

  3. 设置合理的过期时间:为键值设置合理的过期时间,避免数据无限增长。

  4. 使用批量操作:尽可能使用 pipeline 或 multi/exec 批量执行命令,减少网络往返。

    使用pipeline批量操作pipeline = redis_client.pipeline()for i in range(1000): pipeline.set(f'key:{i}', f'value:{i}')pipeline.execute()

  5. 谨慎使用 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 优化建议

  1. 合理设置资源限制:为 Redis 容器分配足够且合理的 CPU 和内存资源。

    Kubernetes资源配置示例resources: requests: cpu: "1" memory: "1Gi" limits: cpu: "2" memory: "2Gi"

  2. 注意网络配置:确保容器网络延迟低,带宽充足。

  3. 持久化与容器存储:使用高性能的持久化卷,减少 IO 对 CPU 的影响。

  4. 考虑使用 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 飙高问题时,可以按照以下流程快速处理:

  1. 立即检查慢查询

    redis-cli SLOWLOG GET 10
    
  2. 快速定位热点 key

    redis-cli --hotkeys
    
  3. 临时限制问题客户端

    # 查找可疑客户端redis-cli CLIENT LIST | grep -v "idle=0"# 必要时断开连接redis-cli CLIENT KILL ADDR 172.16.0.45:53421
    
  4. 紧急调整配置

    # 临时禁用危险命令redis-cli CONFIG SET rename-command KEYS "KEYS_DISABLED"
    

7. 结论

通过系统性的排查与分析,我们成功解决了 Redis 容器 CPU 飙高的问题。问题的根源主要是不合理的命令使用(特别是keys命令)和大 key 处理不当导致的。通过优化应用代码、调整 Redis 配置、合理设置连接池和建立长期监控机制,我们不仅解决了当前问题,还为系统的长期稳定运行奠定了基础。

在容器环境中运行 Redis 需要特别注意资源管理和性能监控,遵循 Redis 的最佳实践原则,才能确保高性能和稳定性。通过本次实战,我们积累了宝贵的经验,为后续类似问题的快速排查和解决提供了参考。