Redis 宕机排查与修复报告

Redis 宕机排查与修复报告

  • 日期:2026-07-07
  • 服务器:185.158.137.66(hostname: love,Debian,16G 内存 + 4G swap)
  • 项目:V2Board 面板(/www/wwwroot/api123.52pokemon.cc,宝塔 + php-fpm 8.1 + MySQL + Redis + Horizon 队列)

一、故障现象

  • 今天 12:01 Redis 宕机(此前 7月1日 14:37 也发生过一次同样的宕机)。
  • 宕机后由 redis-watchdog 定时脚本自动拉起,数据(587MB dump.rdb)无丢失。

二、排查结论

不是攻击,不是泄漏

  • 宕机前请求量平稳(每分钟 1200~1400 次,与日常基线一致),无 CC/DDoS 流量尖峰;12:01 的小峰值是宕机恢复后的积压请求回放。
  • 来源 IP 均为 Cloudflare 回源节点、自有代理节点服务器(轮询 UniProxy 接口)、自有客户端 App(UA: SuperAccelerator),无爆破、无异常扫描命中。
  • api_attack.log 为空;内核里的 [UFW BLOCK] 仅为互联网常规端口扫描噪音,均被防火墙拦截。

真实原因:群发提醒邮件 → 内存耗尽 → OOM 连环击杀

完整链条:

  1. 11:30 V2Board 定时任务 send:remindMail(Kernel.phpdailyAt('11:30'))触发,一次性向队列塞入 2545 封邮件(2043 封”服务即将到期” + 498 封”流量达 95%”,数据来自 v2_mail_log 表)。
  2. 该任务代码用 User::all() 一次性加载 38.7 万用户进内存,且 ini_set('memory_limit', -1) 不设上限。
  3. Horizon 队列 balance=automaxProcesses 按内存公式算出 94,队列堆积后疯狂扩容 worker。
  4. OOM 瞬间(12:00:23 内核快照):php-fpm 151 个进程共占约 12.5GB + Horizon php 进程 65 个约 1.2GB + mysqld 1.7GB,16G 物理内存 + 4G swap 全部打满。
  5. 内核 OOM Killer 依次击杀:12:00:23 mysqld → 12:01:08 systemd/sd-pam → **12:01:08 redis-server(两个实例)**。
  6. Redis 是”连坐”受害者(自身 RSS 仅约 70MB),真正的元凶是邮件群发引发的 php 进程风暴。

三、已执行的修改

修改的文件(共 3 个,均在服务器 185.158.137.66 上)

# 文件 改动内容
1 /www/wwwroot/api123.52pokemon.cc/app/Console/Commands/SendRemindMail.php ① 删除 ini_set('memory_limit', -1);② User::all() 全表加载 → WHERE (remind_expire=1 OR remind_traffic=1 OR telegram_id IS NOT NULL) 预筛 + chunkById(500) 分块读取;③ 新增错峰逻辑:每 100 封邮件入队延迟 +60 秒(常量 BATCH_SIZE=100BATCH_DELAY_SECONDS=60 可调)
2 /www/wwwroot/api123.52pokemon.cc/app/Services/MailService.php remindExpire()remindTraffic() 增加 int $delaySeconds = 0 参数,SendEmailJob::dispatch(...) 改为 ->delay(now()->addSeconds($delaySeconds)) 延迟入队;两方法返回入队邮件数(int),供命令统计
3 /www/wwwroot/api123.52pokemon.cc/config/horizon.php supervisor 一拆二:
V2board(order_handle / traffic_fetch / stat / send_telegram):maxProcesses 由动态公式(算出 94)改为固定 15
V2boardMail(send_email / send_email_mass,新增):maxProcesses = 2

备份位置(与原文件同目录,root 所有)

1
2
3
/www/wwwroot/api123.52pokemon.cc/app/Console/Commands/SendRemindMail.php.bak-claude-20260707-124443
/www/wwwroot/api123.52pokemon.cc/app/Services/MailService.php.bak-claude-20260707-124443
/www/wwwroot/api123.52pokemon.cc/config/horizon.php.bak-claude-20260707-124443

回滚方法(如需):

1
2
3
4
5
6
7
cd /www/wwwroot/api123.52pokemon.cc
cp app/Console/Commands/SendRemindMail.php.bak-claude-20260707-124443 app/Console/Commands/SendRemindMail.php
cp app/Services/MailService.php.bak-claude-20260707-124443 app/Services/MailService.php
cp config/horizon.php.bak-claude-20260707-124443 config/horizon.php
chown www:www app/Console/Commands/SendRemindMail.php app/Services/MailService.php config/horizon.php
sudo -u www /www/server/php/81/bin/php artisan config:cache
sudo -u www /www/server/php/81/bin/php artisan horizon:terminate # supervisord 会自动重启 horizon

部署与验证(已完成)

  • ✅ 三个文件 php -l 语法检查通过,属主已改回 www:www
  • php artisan config:cache 重建配置缓存
  • php artisan horizon:terminate 重启 Horizon(由宝塔 supervisord 自动拉起)
  • ✅ 新 supervisor 已生效:V2board max-processes=15V2boardMail max-processes=2
  • ✅ 所有队列积压为 0,各队列均有 worker 正常消费
  • ✅ 内存正常:used 7.9G / available 7.7G

四、对业务的影响评估

  • 用户代理流量、节点延迟:零影响。代理流量转发不经过面板;节点与面板的通信(UniProxy 拉取用户/上报流量)走 php-fpm HTTP 接口,不走 Horizon 队列,本次未改动。
  • 订单、流量记账、TG 通知:零影响。这些队列日常仅需个位数 worker,15 上限充足(验证时:积压 0)。
  • 邮件:日常零星邮件(验证码、工单回复)仍秒级送达。仅每天 11:30 群发时,2500 封从”1 分钟内轰完”变为”约 25 分钟内匀速发完”,用户无感知。
  • 边界情况:群发期间的验证码邮件可能延迟 1~2 分钟(与群发共用 send_email 队列)。如在意,可将验证码拆到独立高优先队列。

五、遗留事项(待处理)

  1. 观察明天 11:30 群发:第一次实战检验,建议中午看一眼 free -hv2_mail_log 发信是否正常、Horizon 是否稳定。
  2. 群发时间仍在业务高峰:Kernel.phpdailyAt('11:30') 建议改到凌晨低峰(如 04:30),一行改动。
  3. 清理大日志,可释放约 22GB 磁盘:storage/logs/laravel-2026-05-16.log 单文件 21GB,以及 5 月其他大日志。
  4. php-fpm 仍未限制(OOM 时曾 151 进程 / 12.5GB):建议 pm.max_children 降到 ~50,加 pm.max_requests = 500
  5. redis/mysql 无 OOM 保护:建议 systemd unit 加 OOMScoreAdjust=-800,防止再次被优先击杀。
  6. MySQL 内存:检查 innodb_buffer_pool_size,16G 多服务机器建议 ≤ 2G。

报告生成:2026-07-07,由 Claude Code 排查并执行修复