perf(analysis): 分析页按索引/内容分层,告警默认阈值抬高

=== 1. 分析页"一直转圈/请求超时(40000ms)"的真实原因 ===
三个问题叠加,逐个量化:

a) 首屏用空 namespace 发查询
   页面初始 namespace='',等策略返回前就发出 `{namespace=~".+"}` 全集群查询,
   策略到位后又查一次 → 一次访问跑两遍。
   → 加 namespaceReady 门控:解析完再发(effectiveSourceId)。

b) histogram 索引查询和内容查询抢同一个信号量
   /logs/histogram 首选 index/volume_range(毫秒级),但仍要先抢重型令牌,
   而令牌被 4 条 count_over_time(各 14~29s)占满 → 排队/429/超时。
   → 索引路径**不再占用重型令牌**(15s 独立超时),只有回退到
     count_over_time 时才抢令牌。实测各跨度(15m/1h/6h/24h)均 0s。

c) 重查询条数过多
   原来 4 条全局 count_over_time(total/error/warn)+ 2 条 by-app,
   串行 1~2 分钟。现在只留 2 条 by-app:
     - 全局总量已由 histogram(索引)提供
     - 全局 error 由 errByApp 序列求和得出
     - 全局 warn 放弃(优先级最低却要 20s)
   实测 by-app 单条 26s,两条串行 ~52s;主图 0s 先出。

d) 日报 7 天 count_over_time(2.6TB 扫描)必然失败
   → 改走 /logs/histogram(索引)按自然日聚合,只出"每日总量",
     应用维度交给下方 Top 应用表格(有自己的时间范围)。
   实测 7 天跨度毫秒级。

=== 2. 告警默认阈值抬高(告警疲劳治理)===
验证时用 `{namespace="astralx"}` + 阈值 0 触发了一次:
15 分钟命中 3051 万条 —— 阈值 0 等于把告警变成日志流复读机。

新基线(15 分钟窗口,冷却统一 120 分钟):
    致命日志 FATAL           0 → 10
    Go panic                 0 → 10
    JVM OutOfMemoryError     0 → 5
    StackOverflowError       0 → 5
    应用错误激增             1000 → 50000(改名,原"error > 1000"名不副实)
    ingress-nginx 5xx         100 → 500
    ingress-nginx 4xx        1000 → 5000
    数据库连接失败             50 → 200

迁移只改"仍等于旧默认值"的规则,用户手动调过的阈值不动。
设计原则写进 seed 注释:绝不用 threshold 0;宁可少报也不要让人对告警脱敏;
绝对阈值不跨环境通用,大集群必须按 app/namespace 收窄。

=== 实测(v6-1028-2218 / 0bf66199de66)===
  规则 8 条,阈值=0 的规则数 0,冷却全部 120m
  histogram astralx(15m/1h/6h/24h): 0s
  by-app metrics astralx 15m: 26s,正常返回数据
This commit is contained in:
opsx committed 2026-10-28 22:20:19 +00:00
1 parent 95dcd9408c
commit fa7ddd67c1
6 files changed
+161 -77

No files matched your search

+19 -13
View File
@@ -468,35 +468,41 @@ func LokiHistogram(ctx context.Context, src *models.LogSource, query string, sta
return nil, fmt.Errorf("统计范围过大(%.1f 天),最多支持 %.0f 天;请缩小时间范围后重试",
float64(span)/float64(24*time.Hour), maxHistogramRange.Hours()/24)
}
// 并发护栏:拿不到令牌立刻返回,不排队(排队只会让用户等更久)
if !acquireHeavyQuery() {
return nil, ErrTooManyQueries
}
defer releaseHeavyQuery()
// 独立超时:Loki 侧再慢也不会超过 30s,防止单查询长期占用实例
ctx, cancel := context.WithTimeout(ctx, heavyQueryTimeout)
defer cancel()
step := pickHistogramStep(span)
// ── 首选:Loki 官方的 index/volume_range ──────────────────────────────
// 它只读 TSDB 索引里的 chunk 元数据(字节数),**不解码日志内容**。
// 实测同一条件 24 小时:count_over_time 53s 超时失败,volume_range 111ms。
// 返回单位是「字节」,所以 bucket 里带 unit=bytes,前端据此格式化为 KB/MB/GB。
//
// ⚠️ 这条路径**不占用重型查询令牌**:
// 索引查询本身是毫秒级的,如果和 count_over_time 之类要读 chunk 的查询抢
// 同一个信号量,就会出现"图表明明 0 秒能出,却因为别人在跑分析查询而排队
// 十秒、最后 429/超时"的怪现象(分析页实际踩过)。
q := url.Values{}
q.Set("query", query)
q.Set("start", strconv.FormatInt(start, 10))
q.Set("end", strconv.FormatInt(end, 10))
q.Set("step", step)
q.Set("limit", "50")
if body, status, err := lokiProxyGET(ctx, src, "/loki/api/v1/index/volume_range", q); err == nil && status < 400 {
idxCtx, idxCancel := context.WithTimeout(ctx, 15*time.Second)
if body, status, err := lokiProxyGET(idxCtx, src, "/loki/api/v1/index/volume_range", q); err == nil && status < 400 {
if buckets, ok := parseVolumeBuckets(body); ok {
idxCancel()
return buckets, nil
}
}
// 老版本 Loki 没有 volume_range(404/400)时回退到 count_over_time
return lokiHistogramByCount(ctx, src, query, step, start, end)
idxCancel()
// ── 回退:老版本 Loki 没有 volume_range 时才走 count_over_time ──
// 这条要真读 chunk,才需要抢重型令牌 + 长超时。
if !acquireHeavyQuery() {
return nil, ErrTooManyQueries
}
defer releaseHeavyQuery()
heavyCtx, cancel := context.WithTimeout(ctx, heavyQueryTimeout)
defer cancel()
return lokiHistogramByCount(heavyCtx, src, query, step, start, end)
}
// parseVolumeBuckets 解析 index/volume_range 的 matrix 响应。