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:
1 parent
95dcd9408c
commit
fa7ddd67c1
6 files changed
+161
-77
No files matched your search
@@ -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 响应。
|
||||
|
||||
Reference in new issue
Block a user