一例 Redis 导致服务器 CPU 飙高的问题排查

文章目录

    昨晚 10 点,一个网站的负载飙高,触发了 uptime 的宕机报警。从 top 命令结果里看,redis 进程 cpu 飙高,但是我起初以为是被恶意攻击了,把 cloudflare 防护规则严格了一些,临时负载降下来了。由于太困,明天还有事情,就没有继续观察。

    早上 6 点起来,又看了,负载还是高。但是并不影响 golang 相关的服务,只有 php 的旧站点受到了影响。
    哎,PHP 这些破玩意真糟心。牛马还是要上班,就只能寄希望于晚上回来解决了。

    晚上捣鼓了两个小时,算是解决了。

    查看 redis 运行状态

    redis 放在 docker 里,所以登录 redis 使用:

    docker compose exec redis redis-cli
    

    查看运行状态:

    > INFO stats
    
    total_connections_received:168608
    total_commands_processed:11273966
    instantaneous_ops_per_sec:326
    total_net_input_bytes:203350837911
    total_net_output_bytes:26614034188264
    total_net_repl_input_bytes:0
    total_net_repl_output_bytes:0
    instantaneous_input_kbps:31.01
    instantaneous_output_kbps:453887.44
    instantaneous_input_repl_kbps:0.00
    instantaneous_output_repl_kbps:0.00
    rejected_connections:0
    sync_full:0
    sync_partial_ok:0
    sync_partial_err:0
    expired_keys:189148
    expired_stale_perc:0.36
    expired_time_cap_reached_count:0
    expire_cycle_cpu_milliseconds:22144
    evicted_keys:0
    evicted_clients:0
    total_eviction_exceeded_time:0
    current_eviction_exceeded_time:0
    keyspace_hits:7151109
    keyspace_misses:2783287
    pubsub_channels:0
    pubsub_patterns:0
    pubsubshard_channels:0
    latest_fork_usec:11041
    total_forks:245
    migrate_cached_sockets:0
    slave_expires_tracked_keys:0
    active_defrag_hits:0
    active_defrag_misses:0
    active_defrag_key_hits:0
    active_defrag_key_misses:0
    total_active_defrag_time:0
    current_active_defrag_time:0
    tracking_total_keys:0
    tracking_total_items:0
    tracking_total_prefixes:0
    unexpected_error_replies:0
    total_error_replies:3546
    dump_payload_sanitizations:0
    total_reads_processed:11920639
    total_writes_processed:18558028
    io_threaded_reads_processed:0
    io_threaded_writes_processed:0
    reply_buffer_shrinks:123859
    reply_buffer_expands:375511
    
    • 输入带宽 (instantaneous_input_kbps):31.01 KB/s(请求很小)
    • 输出带宽 (instantaneous_output_kbps):453887.44 KB/s ≈ 443 MB/s(响应巨大)

    结论:输入和输出的差距高达 1.4万倍。这说明客户端只发了一个很小的请求(比如 HGETALL 或 SMEMBERS),但Redis返回了一个极其庞大的数据给客户端。

    查看当前哪个客户端的输出缓冲区占用最大

    CLIENT LIST
    

    输出:

    id=112411 addr=[2400:8901::7]:41764 laddr=[2400:8901::4]:6379 fd=21 name= age=28381 idle=0 flags=N db=2 sub=0 psub=0 ssub=0 multi=-1 qbuf=0 qbuf-free=20474 argv-mem=0 multi-mem=0 rbs=4096 rbp=4096 obl=0 oll=1 omem=20971544 tot-mem=20996888 events=rw cmd=get user=default redir=-1 resp=2
    id=112430 addr=[2400:8901::7]:52090 laddr=[2400:8901::4]:6379 fd=26 name= age=28366 idle=0 flags=N db=2 sub=0 psub=0 ssub=0 multi=-1 qbuf=0 qbuf-free=20474 argv-mem=0 multi-mem=0 rbs=16384 rbp=16384 obl=0 oll=1 omem=20971544 tot-mem=21009176 events=rw cmd=get user=default redir=-1 resp=2
    

    有 3个客户端连接 正在拉取 超大Key,每个连接的输出缓冲区(omem)都达到了 20,971,544 字节(约 20 MB).这三个连接都在执行 cmd=get,且都在 db=2(数据库2),说明它们都在 反复读取同一个超大字符串类型的Key(或者内容完全相同的大Key)。每个响应包约 20MB,三个连接同时输出,瞬时输出带宽自然飙到 450MB/s。

    找到这个大 KEY

    连 redis db 2:

    执行完后,它会告诉你哪个数据类型的Key最大(比如最大的Hash有多大)。

    > docker compose exec redis redis-cli -n 2 --bigkeys -i 0.1
    
    Sampled 4658 keys in the keyspace!
    Total key length in bytes is 297586 (avg len 63.89)
    
    Biggest string found '"9c6a2e0bcbb03f32fb0382d38fa00bbe-cache-com_modules-5cdbee63315b6b81941ea5c4844ffd8c"' has 26627066 bytes
    

    -i 0.1 表示每扫描100个Key就休息0.1秒,防止在高负载时加剧CPU抖动。

    删除这个大 KEY

    docker compose exec redis redis-cli -n 2 DEL "9c6a2e0bcbb03f32fb0382d38fa00bbe-cache-com_modules-5cdbee63315b6b81941ea5c4844ffd8c"
    

    cpu 就正常了。但是只正常了半个小时,然后这个 key 又变大到了 15M。。。

    删了几次,还是反复。我无语了。。。

    gemini 救命

    改问 gemini 之后,找到了根源:

    joomla 的 redis 缓存中,这个 key : cache-com_modules 一直暴涨,有 20 多M,这是为什么

    在 Joomla 中,cache-com_modules 这个 key 专门用于存储网站所有模块(Modules)的缓存数据(即模块渲染后的 HTML 代码及相关参数)。

    这个 key 的体积暴涨到 20 多兆(在 Redis 这种内存数据库中,对于单一文本 key 来说已经非常庞大),通常是由以下几个核心原因导致的:

    开启了“渐进式缓存”(Progressive Caching)—— 最常见原因
    Joomla 的全局缓存分为两种模式:保守式(Conservative)和渐进式(Progressive)。

    • 保守式缓存: 按模块生成缓存。1000 个未登录访客访问同一个页面,共用同一份模块缓存。
    • 渐进式缓存: 为每一个独立访客(基于 Session)单独生成一份模块缓存。

    如果你的网站开启了“渐进式缓存”,当有大量的搜索引擎爬虫(蜘蛛)、恶意扫描机器人或大量真实散客访问时,Joomla 会为每一个全新的 Session 生成一份完全独立的模块缓存。这些缓存全部堆积在 com_modules 下,导致该 key 的体积呈指数级暴涨。

    切换回“保守式缓存”. 这是最立竿见影的解决方法:

    • 进入 Joomla 后台 -> 系统 (System) -> 全局设置 (Global Configuration)。
    • 点击 系统 (System) 选项卡。
    • 在 缓存设置 (Cache Settings) 中,将“系统缓存”修改为 保守式缓存 (Conservative caching)。
    • 保存设置。

    这个 key 的过期时间

    # docker compose exec redis redis-cli -n 2 TTL "9c6a2e0bcbb03f32fb0382d38fa00bbe-cache-com_jmap-9667e9aa5d8b93f1cb1835bd4315b7c9"
    (integer) 3456
    

    返回结果 (integer) 3456 表示该 Key 的剩余存活时间为 3456 秒,换算一下大约是 57.6 分钟(接近 1 小时)。

    看来不需要我加定时任务自动清理了。

    关于作者 🌱

    我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式