higress Grafana生产事故复盘:Loki 2.9 WAL 竞态 Bug 导致 5 天审计日志消失

生产事故复盘:Loki 2.9 WAL 竞态 Bug 导致 5 天审计日志消失

一次 Grafana 看板上的"Token 曲线空洞",引出了 Loki 2.9.0 的 WAL checkpoint 竞态 Bug。数据进了 Ingester 却从未落盘,105,633 行日志"人间蒸发"。本文完整记录了从发现异常、10 步证据链排查、根因定位、升级到 3.7.1、数据回灌到自愈脚本落地的全过程。

一、事故概述

1.1 一句话结论

Grafana 看板上 Downstream QPS(Prometheus)与 Token Per Second(Loki)对不上 的根因是:

Loki 2.9.0 的 WAL checkpoint 竞态 Bug :Promtail 重启触发 181MB 历史日志回灌,数据成功进入 Loki Ingester(distributor 计数 105,633 行),但因 WAL checkpoint 与 flush 并发时的竞态,这些数据从未 flush 落盘,导致约 5 天的审计日志在查询时完全消失。

1.2 影响范围

  • 丢失时段:2026-08-29 ~ 2026-09-02(约 5 天)
  • 丢失数据量:105,633 行 / 231MB
  • 发现方式:人工比对 Grafana 看板,发现 QPS 有数据但 Token 曲线为零
  • 发现时间:事故发生后约 5 天

1.3 修复结果

  • 升级 Loki 2.9.0 → 3.7.1
  • 关闭 WAL(物理切断 Bug 路径)
  • 重置 Promtail positions 回灌历史日志
  • 看板 Token 曲线完全恢复

二、发现异常

某天,我在检查 Grafana 看板 higress-ai-gateway-dashboard 时,发现了一个诡异的现象:

复制代码
┌─────────────────────────────────────────────────────┐
│  Downstream QPS (Prometheus)                        │
│  ▂▃▅▇▅▃▂  ▂▃▅▇▅▃▂  ▂▃▅▇▅▃▂  ▂▃▅▇▅▃▂            │
│  08-29      08-30      08-31      09-01            │
│  ↑ 有明显的白天流量峰值                              │
├─────────────────────────────────────────────────────┤
│  Token Per Second (Loki)                            │
│  ───────────────────────────────▂▃▅▇▅▃▂            │
│  08-29      08-30      08-31      09-01  09-03     │
│  ↑ 08-29 ~ 09-02 完全为零,09-03 23:00 才恢复       │
└─────────────────────────────────────────────────────┘

Prometheus 的 QPS 数据显示网关一直在正常处理请求,但 Loki 的 Token 统计在同一时段却是零。两个数据源来自同一个网关,怎么可能对不上?

初步猜测

  1. Grafana 看板配置有问题?
  2. Loki 查询语句写错了?
  3. 日志采集链路断了?

三、10 步证据链排查

第 1 步:排除配置漂移

将本地和远程服务器上的看板 JSON 进行比对,所有 expr 字段完全一致。

结论:排除配置差异。

第 2 步:确认 Prometheus 数据可信

检查 Prometheus 的 up 指标,08-26 ~ 09-03 连续无断点。

结论:网关全程在线,QPS 数据可信,问题在 Loki 侧。

第 3 步:Loki 分小时探测锁定丢失范围

通过 query_range API 按小时粒度查询,精确定位空洞:

bash 复制代码
curl -sG 'http://localhost:3100/loki/api/v1/query' \
  --data-urlencode 'query=count_over_time({job="higress-audit-logs"} |= "ai_log" [1h])' \
  --data-urlencode "time=$(date -d '2026-08-29 20:00' +%s)000000000"

结果

  • 08-29 20:00 起 → 数据消失
  • 09-03 23:00 → 数据恢复

结论:丢失范围精确锁定为 08-29 20:00 ~ 09-03 23:00。

第 4 步:检查 Promtail 采集状态

查看 Promtail 日志和 positions.yaml

复制代码
# Promtail 日志
level=info ts=2026-08-31T09:08:xx msg="Starting Promtail"
# positions.yaml
/var/log/higress/proxy/access.log.1: "182000000"

关键发现:

  • 08-31 09:08 Promtail 发生过重启
  • access.log.1 的 offset 从 1.4MB 推进到了 182MB
  • Promtail 无任何报错

分析 :Promtail 只有在成功推送日志后才会推进 offset。offset 推进到 182MB 说明 Promtail 确实把数据推送出去了

结论:回灌推送成功,问题在 Loki 接收端。

第 5 步:检查 Loki 接收指标

bash 复制代码
curl -s localhost:3100/metrics | grep loki_distributor_lines_received
# 结果:loki_distributor_lines_received_total{tenant="fake"} 105633

curl -s localhost:3100/metrics | grep loki_discarded_samples
# 结果:指标不存在(无丢弃)

结论:Loki distributor 确实收到了 105,633 行(231MB),且零丢弃。数据"进了门"但去哪了?

第 6 步:检查 Loki 容器状态

bash 复制代码
docker inspect higress-loki --format '{{.RestartCount}} {{.State.StartedAt}}'
# 结果:RestartCount=0, StartedAt=2026-08-28T18:03:xx

结论:Loki 容器没有重启过,没有 OOM,排除内存数据丢失。

第 7 步:检查 chunks 存储目录(关键证据)

bash 复制代码
ls -la /loki/chunks/
# 结果:102.4M / 1058 个文件
# 但是:没有 fake 租户目录!

ls -la /loki/wal/
# 结果:仅 272K

这是最关键的发现

  • Loki 收到了 105,633 行数据
  • 但 chunks 目录下没有 fake 租户的子目录
  • 意味着数据从未被 flush 到磁盘

结论:数据"进了 Ingester 的内存,但从未落盘"。

第 8 步:检查索引

bash 复制代码
ls /loki/boltdb-shipper-cache/
# 结果:索引表 20685~20699 齐全
# 但是:空洞时段(08-29~09-02)的索引表无条目

结论:索引层也没有数据,查询路径在源头就断了。

第 9 步:排除误删

bash 复制代码
ls /loki/compactor/
# 结果:空目录,无 delete request

结论:排除 compactor 误删。

第 10 步:排除时间戳解析错乱

bash 复制代码
head -1 /var/log/higress/proxy/access.log.1
# 结果:{"start_time":"2026-08-30T19:38:00.607Z", ...}

结论:日志时间戳格式正常,排除时间解析问题。


四、根因定位

4.1 证据汇总

证据 说明
Promtail offset 已推进 数据确实被推送出去了
Loki distributor 计数 105,633 行 数据确实被接收了
Loki 零丢弃 数据没有被主动丢弃
chunks 目录无 fake 租户 数据从未 flush 到磁盘
索引表空洞区间无条目 索引也没有写入
容器无重启、无 OOM 不是内存数据丢失

4.2 结论:WAL Checkpoint 竞态

所有证据指向同一个结论:

数据进了 Ingester 内存,但在 WAL checkpoint 与 flush 并发时,checkpoint 把正在 flush 的 chunk 误判为"已完成"而跳过,导致数据既没有落盘,也没有被重试。

这是一个典型的竞态条件 Bug

复制代码
时间线:
  t1: Ingester 接收数据,写入 WAL
  t2: flush 协程开始将 chunk 写入磁盘(耗时较长)
  t3: checkpoint 协程运行,看到 t2 的 chunk "正在处理"
  t4: checkpoint 误判该 chunk 已完成,截断 WAL
  t5: flush 协程失败/超时,但 WAL 已截断,无法重试
  → 数据"蒸发"

4.3 历史线索

这不是第一次出现:

  • 08-26 :首次事故,回灌 gap-20260824 时丢失 22 个流 120 条数据
  • 08-28:重新启用 WAL 并调大 checkpoint 间隔(试图缓解)
  • 08-31:大规模回灌 181MB 时竞态复发,5 天数据全部丢失

规律:WAL 在 bulk 回灌场景下必然丢数据。


五、修复方案

5.1 升级 Loki(根本修复)

Loki 2.9.0 已于 2025-11 EOL,WAL 竞态 Bug 官方已在 3.x 修复。

yaml 复制代码
# docker-compose.yaml
services:
  loki:
    image: grafana/loki:3.7.1   # 从 2.9.0 升级

5.2 关闭 WAL(双保险)

即使升级了版本,仍然关闭 WAL 作为双保险:

yaml 复制代码
# loki-config.yaml
ingester:
  wal:
    enabled: false    # 物理切断 Bug 代码路径

关闭 WAL 的代价:Loki 崩溃时会丢失最近约 10 秒的内存数据。相比竞态吞掉 5 天数据,这个代价完全可以接受。

5.3 Loki 3.x 配置适配(5 个必改项)

升级到 3.x 后,有 5 个配置必须修改,否则 Loki 拒绝启动或静默丢数据:

必改 1:关闭 structured metadata
yaml 复制代码
limits_config:
  allow_structured_metadata: false

原因 :structured metadata 要求 TSDB 索引 + v13 schema,而当前配置仍使用 boltdb-shipper + v11。不关闭则启动报 CONFIG ERROR 拒绝启动。

必改 2:关闭 pattern ingester
yaml 复制代码
pattern_ingester:
  enabled: false

原因 :Loki 3.x 默认开启 pattern ingester,且强制要求 WAL 开启。与 wal.enabled: false 冲突,必须显式关闭。

必改 3:放宽单行大小限制
yaml 复制代码
limits_config:
  max_line_size: 4194304   # 4MB

原因 :Loki 3.0 起默认单行上限从"不限"改为 256KB,超长行会被静默丢弃 。Envoy access log 包含 ai_log 嵌套 JSON,单行可能超过 256KB。如果不放宽,回灌时长日志会被悄悄丢掉。

必改 4:调大并发查询限制
yaml 复制代码
query_scheduler:
  max_outstanding_requests_per_tenant: 1024

原因 :默认 100,Grafana 切换时间范围时并发查询容易被拒(400 too many outstanding requests)。

必改 5:确认存储路径配置
yaml 复制代码
common:
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules

原因:3.x 对路径配置更严格,确保与 volume 挂载一致。

5.4 配置验证

升级后先验证配置,不要直接启动:

bash 复制代码
docker run --rm \
  -v $PWD/loki/loki-config.yaml:/config/loki.yaml:ro \
  grafana/loki:3.7.1 \
  -config.file=/config/loki.yaml -verify-config=true
# 输出:config is valid

六、数据回灌

配置修复后,需要把丢失的 5 天日志重新推送到 Loki。

6.1 回灌原理

复制代码
源日志文件(仍在磁盘上)
    │
    ▼  重置 positions offset
Promtail 从 offset=0 重新读取
    │
    ▼  推送
Loki 3.7.1 (WAL=off)
    │
    ▼  直接 flush(无 WAL 竞态)
chunks 落盘 + 索引写入
    │
    ▼  查询
Grafana 看板恢复

关键点:Loki 按日志内的 start_time 字段打时间戳(由 Promtail timestamp 插件处理),所以历史日志会被写入正确的时间位置,看板曲线原样恢复。

6.2 回灌脚本

bash 复制代码
#!/bin/bash
# backfill-loki-gap.sh
# 回灌丢失的审计日志

set -euo pipefail

COMPOSE_DIR="/path/to/higress_all_in_one"
POSITIONS="${COMPOSE_DIR}/loki/positions/positions.yaml"
LOKI_URL="http://localhost:3100"

cd "${COMPOSE_DIR}"

# 0) 确认 WAL 已关闭(防止再次竞态丢数)
if grep -A2 '^[[:space:]]*wal:' loki/loki-config.yaml | grep -q 'enabled: false'; then
    echo "[0/5] WAL 已关闭,OK"
else
    echo "[0/5] 错误:WAL 仍为开启状态,请先修改 enabled: false"
    exit 1
fi

# 1) 备份 positions
cp -a "${POSITIONS}" "${POSITIONS}.bak.$(date +%Y%m%d%H%M%S)"
echo "[1/5] positions 已备份"

# 2) 重置丢失文件的 offset
#    access.log.1 -> 1393461(丢失起始位置)
#    access.log-20260831 -> 0(整个文件丢失)
sed -i 's|/var/log/higress/proxy/access\.log\.1: "[0-9]*"|/var/log/higress/proxy/access.log.1: "1393461"|' "${POSITIONS}"
sed -i 's|/var/log/higress/proxy/access\.log-20260831: "[0-9]*"|/var/log/higress/proxy/access.log-20260831: "0"|' "${POSITIONS}"
echo "[2/5] positions 已重置"

# 3) 重启 Loki,等待 ready
docker compose restart loki
for i in $(seq 1 30); do
    if curl -fs "${LOKI_URL}/ready" >/dev/null 2>&1; then break; fi
    sleep 2
done
echo "[3/5] Loki 已重启并 ready"

# 4) 重启 Promtail 开始回灌
docker compose restart promtail
echo "[4/5] Promtail 已重启,开始回灌(约 181MB,预计数分钟)"

# 5) 等待 3 分钟后验证
sleep 180
RESP=$(curl -sG "${LOKI_URL}/loki/api/v1/query" \
    --data-urlencode 'query=sum(count_over_time({job="higress-audit-logs"} |= "ai_log" [4d]))' \
    --data-urlencode "time=$(date -u +%s)000000000")
echo "[5/5] 最近 4 天 ai_log 行数: ${RESP}"
echo "回灌完成,请在 Grafana 验证 Token 曲线是否恢复。"

6.3 回灌注意事项

注意事项 说明
先确认 WAL 关闭 否则回灌会再次触发竞态
精确重置 offset 只重置丢失的文件,已有数据不重复推送
Loki 不去重 少量重叠数据会被重推,对 rate() 面板影响可忽略
限速退避 Loki per_stream_rate_limit=5MB/s,Promtail 自动退避
时间戳正确性 依赖 Promtail 的 timestamp 插件用 start_time 替换时间戳

七、防止再次发生

7.1 Promtail 自愈脚本

本次事故 5 天后才被人眼发现,这是不能接受的。为此编写了一个 watchdog 脚本,通过 cron 定期执行:

bash 复制代码
#!/bin/bash
# promtail-watchdog.sh
# 检测 Promtail positions offset 异常并自动修复

POSITIONS_FILE="/path/to/loki/positions/positions.yaml"
LOG_DIR="/path/to/logs/proxy"
LOG_FILE="/path/to/promtail-watchdog.log"

NEEDS_FIX=false

while IFS= read -r line; do
    # 只处理日志文件路径
    if [[ ! "$line" =~ ^[[:space:]]+/var/log/ ]]; then
        continue
    fi

    CONTAINER_PATH=$(echo "$line" | sed 's/^[[:space:]]*//;s/:.*//' | tr -d ' ')
    OFFSET=$(echo "$line" | grep -oP '"\K[0-9]+(?=")' || echo "0")
    HOST_PATH=$(echo "$CONTAINER_PATH" | sed 's|^/var/log/higress/proxy/|'"${LOG_DIR}"'/|')

    if [ ! -f "$HOST_PATH" ]; then
        continue
    fi

    FILE_SIZE=$(stat -c%s "$HOST_PATH" 2>/dev/null || echo "0")

    # 核心检测逻辑:offset > 文件大小 = 文件被截断/轮转
    if [ "$FILE_SIZE" -lt "$OFFSET" ] && [ "$OFFSET" -gt 0 ]; then
        echo "$(date '+%Y-%m-%d %H:%M:%S') ALERT: Truncation detected! ${CONTAINER_PATH}: file_size=${FILE_SIZE} < offset=${OFFSET}" >> "$LOG_FILE"
        NEEDS_FIX=true
        # 将 offset 重置为 0
        ESCAPED_PATH=$(echo "$CONTAINER_PATH" | sed 's|/|\\/|g')
        sed -i "s|${ESCAPED_PATH}: \"${OFFSET}\"|${CONTAINER_PATH}: \"0\"|g" "$POSITIONS_FILE"
    fi
done < "$POSITIONS_FILE"

if [ "$NEEDS_FIX" = true ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') FIX: Restarting Promtail..." >> "$LOG_FILE"
    docker compose restart promtail
    echo "$(date '+%Y-%m-%d %H:%M:%S') FIX: Promtail restarted" >> "$LOG_FILE"
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') OK: All offsets consistent" >> "$LOG_FILE"
fi

工作原理

复制代码
positions.yaml 中记录的 offset > 实际文件大小?
    │
    ├── 是 → 文件被截断/轮转,offset 失效
    │         → 自动重置 offset 为 0
    │         → 重启 Promtail 从头采集
    │
    └── 否 → 正常,不做任何操作

配合 cron 定期执行:

bash 复制代码
# 每 5 分钟检测一次
*/5 * * * * /path/to/promtail-watchdog.sh

7.2 Grafana 断流告警

在 Grafana 中配置告警规则:

复制代码
当网关 QPS > 0 时,如果 {job="higress-audit-logs"} 行数持续为 0 超过 10 分钟,触发告警。

这样下次再出现采集链路中断,可以在分钟级发现,而不是 5 天后靠人眼。


八、快速自查手册

下次遇到看板"对不上"时,按以下步骤快速排查:

8.1 先分清口径差异

QPS 面板包含所有 outbound 请求(含控制台、健康检查),Token 只统计 ai_log != "-" 的 AI 请求。两者永远不会严格相等,趋势一致即正常。

8.2 判断是否丢数

bash 复制代码
# 查询 Loki 某小时的日志行数
curl -sG 'http://localhost:3100/loki/api/v1/query' \
  --data-urlencode 'query=sum(count_over_time({job="higress-audit-logs"} |= "ai_log" [1h]))' \
  --data-urlencode "time=$(date -d '2026-09-01 12:00' +%s)000000000"

8.3 对比接收量与可查量

bash 复制代码
# Loki 接收了多少行
curl -s localhost:3100/metrics | grep lines_received

# 实际能查到多少行
# 如果 received 高但查询为空 → 优先怀疑 WAL 问题

8.4 检查 flush 状态

bash 复制代码
# 查看 chunks 目录是否有数据
ls -la /loki/chunks/

# 查看 Promtail 日志是否有报错
docker logs --tail 50 higress-promtail | grep -iE 'error|failed'

# 检查 positions offset 是否正常推进
cat /loki/positions/positions.yaml

8.5 排查决策树

复制代码
看板数据为空?
    │
    ├── Promtail 有报错?
    │       ├── 是 → 检查报错内容,修复采集链路
    │       └── 否 → 继续
    │
    ├── positions offset 正常推进?
    │       ├── 否 → 文件被截断,运行 watchdog 或手动重置
    │       └── 是 → 继续
    │
    ├── Loki metrics 中 lines_received > 0?
    │       ├── 是,但查询为空 → WAL 竞态!检查 wal.enabled
    │       └── 否 → 数据没到 Loki,检查网络/配置
    │
    └── chunks 目录有对应租户数据?
            ├── 否 → flush 失败,检查磁盘/配置
            └── 是 → 索引问题,检查 boltdb-shipper

九、时间线回顾

日期 事件
08-26 首次事故:回灌 gap-20260824 时丢失 22 个流 120 条数据
08-28 重新启用 WAL 并调大 checkpoint 间隔(试图缓解)
08-29 20:00 第二次事故开始(当时未察觉)
08-31 09:08 Promtail 重启,触发 181MB 历史日志回灌
08-31 ~ 09-02 回灌数据进入 Ingester 但从未 flush 落盘
09-03 23:00 数据恢复(新日志正常写入)
09-04 发现异常,完成 10 步排查,定位根因
09-04 升级 Loki 2.9.0 → 3.7.1,关闭 WAL,5 项配置适配
09-04 执行回灌脚本,看板 Token 曲线恢复
09-04 部署 promtail-watchdog.sh 自愈脚本

十、经验总结

10.1 技术层面

教训 说明
不要盲信 WAL WAL 不是万能的,特定版本下它本身就是 Bug 的来源
升级要及时 Loki 2.9.0 已于 2025-11 EOL,使用 EOL 版本就是在积累风险
配置变更要验证 verify-config=true 可以在启动前发现配置错误
回灌前要保护 大规模回灌前,先确认存储端的 Bug 已修复

10.2 运维层面

教训 说明
监控要覆盖"监控本身" 本次事故 5 天才发现,因为没人监控 Loki 是否正常
自愈优于人工 watchdog 脚本 + cron 可以在人发现之前自动修复
日志要保留源文件 源日志文件在磁盘上保留,才能支持回灌
事故文档要详细 详细的排查记录是未来复用的宝贵资产

10.3 给读者的建议

如果你也在使用 Loki 采集 Higress(或其他 Envoy 网关)的审计日志:

  1. 检查 Loki 版本:如果还在 2.9.x,尽快升级到 3.x
  2. 评估 WAL 策略:在 bulk 回灌场景下,WAL 的风险可能大于收益
  3. 部署 watchdog:用脚本自动检测 positions 异常
  4. 配置断流告警:网关有流量但 Loki 无数据时立即报警
  5. 保留源日志:确保 access log 源文件在回灌前不会被清理

生产环境没有银弹,每一个"稳定运行"的背后,都可能藏着未被发现的竞态。保持警惕,保持监控,保持自动化。

相关推荐
IT界的老黄牛10 小时前
Jenkins 监控一条龙:接入、看板 9964、11 条告警规则直接抄走
jenkins·grafana·prometheus·监控·ci-cd·告警规则
Joker-Full-stack1 天前
Higress AI 网关实战:如何实现基于用户的 Token 用量统计与展示
higress
heimeiyingwang1 天前
【Prometheus·可视化篇】Grafana 集成:数据源配置与 Dashboard 设计原则
grafana·prometheus
2601_962097362 天前
GraphQL,Grafana和Dash
python·grafana·数据可视化·graphql·dash
我星期八休息3 天前
软件测试—从认识到BUG
考研·安全·bug
happymade4 天前
7000 台网络设备批量纳管 & 自动拓扑生成实战——MSRM3 完整操作流程与关键要点复盘
运维·服务器·网络·zabbix·grafana·msrm3
AAA@峥4 天前
从零搭建 Prometheus 完整监控告警体系|Linux 部署 + node_exporter+Grafana 可视化
云原生·grafana·prometheus
hulihutu444 天前
拒绝延期、bug、货不对板:2026 管理系统定制开发落地保障手册
github·bug·鸿蒙系统·数据库管理员
Rain5094 天前
谁动了我的 URL?——记一次微前端“灵异 Bug“的排查实录
前端·vue.js·人工智能·前端框架·bug·ai编程