Redis 容器 CPU 飙高排查实战

331 阅读4分钟

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

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

从命令统计可以看出,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 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"

从监控输出可以看到,确实有多个客户端频繁地执行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 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 飙高的主要原因:

  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 = 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 使用的最佳实践

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

  1. 合理设置资源限制:为 Redis 容器分配足够且合理的 CPU 和内存资源。
# Kubernetes资源配置示例
resources:
  requests:
    cpu: "1"
    memory: "1Gi"
  limits:
    cpu: "2"
    memory: "2Gi"
  1. 注意网络配置:确保容器网络延迟低,带宽充足。
  2. 持久化与容器存储:使用高性能的持久化卷,减少 IO 对 CPU 的影响。
  3. 考虑使用 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 的最佳实践原则,才能确保高性能和稳定性。通过本次实战,我们积累了宝贵的经验,为后续类似问题的快速排查和解决提供了参考。