【BUG】1PWAF event_id 查询未命中部分索引,导致 OpenResty 单个 worker 长期占满 CPU、日志队列溢出

反馈问题:【BUG】

环境信息

  • 1Panel 版本:v2.2.5 stable
  • 操作系统:Debian GNU/Linux 12(bookworm)
  • 内核:6.1.0-49-cloud-amd64
  • 架构:x86_64
  • Docker:29.5.2
  • OpenResty 镜像:1panel/openresty:1.31.1.1-2-3-noble
  • CPU:16 核
  • 内存:15 GiB
  • WAF 状态:开启,protection 模式
  • WAF 日志:开启,配置为 maxDay: 180maxSize: 1

问题现象

OpenResty 容器长期保持约 97%~100% CPU,但并不是全部 worker 都繁忙。

进程检查发现:

  • 只有第一个 OpenResty worker 长期占用约 91%~98% CPU。
  • 该 worker 已运行约 4 天 10 小时,累计 CPU 时间约 4 天 1 小时。
  • 其余 15 个 worker 基本为 0%。
  • 服务器整体负载约 1.x,内存充足,没有发生 OOM。
  • 当前访问日志抽样流量约 1.7 请求/秒,不足以解释持续满一核。

容器状态示例:

NAME                    CPU %    MEM USAGE
1Panel-openresty-xxxx    97.37%   982.8 MiB

worker 状态示例:

PID       STAT   %CPU   ELAPSED       TIME
379818    R      91.3   4-10:50:30   4-01:36:14

影响

OpenResty 错误日志持续出现 WAF 攻击日志队列已满:

[lua] attack_log.lua:0: attack_log queue full,
dropped: 111651,
error: pending limit reached while logging request

首次出现队列满的时间为:

2026/08/10 04:28:40

截至排查时:

  • attack_log queue full 告警记录约 3420 条。
  • WAF 内部丢弃计数至少达到 111651。
  • 历史日志还出现过 database is locked 和监控日志绑定类型错误。

定位结果

1. 高 CPU 来自 worker 0 的 WAF 日志后台任务

OpenResty 配置包含:

init_worker_by_lua_file /usr/local/openresty/1pwaf/worker.lua;

对当前镜像中的 worker.lua 反汇编后确认,worker ID 0 会周期运行:

ngx.timer.every(2, waf_task.insert_log)

因此 WAF 日志消费者集中在第一个 worker 中执行。

2. worker 正在反复读取 SQLite 攻击日志数据库

对高 CPU worker 进行 5 秒 /proc/<pid>/io 采样:

逻辑读取增量:约 5.59 GB
读取系统调用增量:1,365,508 次
平均读取调用:约 273,000 次/秒
物理磁盘读取增量:0

说明进程正在页缓存中高频扫描数据库,并不是磁盘 I/O 卡住。

使用 strace 抓取后,确认它连续读取:

/usr/local/openresty/1pwaf/data/db/waf/attack_logs.db

示例:

pread64(116</usr/local/openresty/1pwaf/data/db/waf/attack_logs.db>,
        ..., 4096, 370524160) = 4096
pread64(116</usr/local/openresty/1pwaf/data/db/waf/attack_logs.db>,
        ..., 4096, 370528256) = 4096
pread64(116</usr/local/openresty/1pwaf/data/db/waf/attack_logs.db>,
        ..., 4096, 370540544) = 4096

读取偏移持续递增,符合 SQLite 全表扫描特征。

3. event_id 查询没有使用现有索引

对当前镜像的 waf_task.lua 反汇编后,确认存在动态查询:

SELECT id FROM <table> WHERE event_id = ? LIMIT 1

attack_logs 为例,实际查询为:

SELECT id
FROM attack_logs
WHERE event_id = ?
LIMIT 1;

数据库现有索引是部分唯一索引:

CREATE UNIQUE INDEX idx_attack_logs_event_id
ON attack_logs(event_id)
WHERE event_id IS NOT NULL
  AND event_id <> '';

但对实际查询执行 EXPLAIN QUERY PLAN,结果是:

SCAN attack_logs

即现有部分索引没有被使用。

如果在查询中补齐部分索引条件:

SELECT id
FROM attack_logs
WHERE event_id = ?
  AND event_id IS NOT NULL
  AND event_id <> ''
LIMIT 1;

查询计划立即变为:

SEARCH attack_logs
USING COVERING INDEX idx_attack_logs_event_id (event_id=?)

同样的问题也存在于:

  • nginx_logs
  • block_ips

验证结果:

attack_logs:
原查询:SCAN attack_logs
补齐条件:SEARCH attack_logs USING COVERING INDEX idx_attack_logs_event_id

nginx_logs:
原查询:SCAN nginx_logs
补齐条件:SEARCH nginx_logs USING COVERING INDEX idx_nginx_logs_event_id

block_ips:
原查询:SCAN block_ips
补齐条件:SEARCH block_ips USING COVERING INDEX idx_block_ips_event_id

排查时数据库规模

attack_logs.db
大小:约 718 MB
自增序号:约 4,950,276

nginx_logs.db
大小:约 1.94 GB
自增序号:约 4,950,257

block_ips.db
大小:约 4.86 MB
自增序号:约 106,721

waf_task 每处理一条日志都执行一次没有命中索引的 event_id 查询时,会反复扫描数百 MB 甚至接近 2 GB 的数据库,导致 worker 0 长期占满一个 CPU,并进一步造成日志队列积压和丢弃。

复现方式

  1. 使用 1panel/openresty:1.31.1.1-2-3-noble
  2. 开启 1PWAF 和 WAF 日志。
  3. attack_logsnginx_logs 累积到较大数据量。
  4. 检查 OpenResty worker CPU。
  5. 对以下查询执行查询计划:
EXPLAIN QUERY PLAN
SELECT id FROM attack_logs WHERE event_id = ? LIMIT 1;

实际结果:

SCAN attack_logs
  1. 补充部分索引条件后再次检查:
EXPLAIN QUERY PLAN
SELECT id
FROM attack_logs
WHERE event_id = ?
  AND event_id IS NOT NULL
  AND event_id <> ''
LIMIT 1;

结果变为索引查询。

预期结果

WAF 日志写入过程中,使用 event_id 判断记录是否存在时,应命中 event_id 索引,不能随日志数据库增长而反复全表扫描。

实际结果

实际 SQL 未包含部分索引的完整条件,SQLite 未使用现有索引,导致:

  • worker 0 长期满一核。
  • SQLite 数据库被反复全表扫描。
  • WAF 日志消费速度下降。
  • 日志队列达到上限。
  • 大量攻击日志被丢弃。

建议修复

建议修改 WAF 日志事件查询,将:

SELECT id FROM %s WHERE event_id = ? LIMIT 1

修改为:

SELECT id
FROM %s
WHERE event_id = ?
  AND event_id IS NOT NULL
  AND event_id <> ''
LIMIT 1

该修改可以直接使用现有部分索引,避免额外增加重复索引。

需要同时确认动态表名涉及的以下表:

  • attack_logs
  • nginx_logs
  • block_ips

另一种兼容方案是为这些表增加不带 WHERE 条件的普通 event_id 索引,但会和现有部分索引重复,占用额外磁盘空间并增加写入开销,因此更建议修正查询语句。

建议增加回归测试,确保以下查询计划不能出现:

SCAN attack_logs
SCAN nginx_logs
SCAN block_ips