生产事故复盘: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 统计在同一时段却是零。两个数据源来自同一个网关,怎么可能对不上?
初步猜测:
- Grafana 看板配置有问题?
- Loki 查询语句写错了?
- 日志采集链路断了?
三、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 网关)的审计日志:
- 检查 Loki 版本:如果还在 2.9.x,尽快升级到 3.x
- 评估 WAL 策略:在 bulk 回灌场景下,WAL 的风险可能大于收益
- 部署 watchdog:用脚本自动检测 positions 异常
- 配置断流告警:网关有流量但 Loki 无数据时立即报警
- 保留源日志:确保 access log 源文件在回灌前不会被清理
生产环境没有银弹,每一个"稳定运行"的背后,都可能藏着未被发现的竞态。保持警惕,保持监控,保持自动化。