Files
opsx b7de337c9e perf(daily-volume): 每日量抓取拆成「体积每 30 分钟 / 条数每天最多 3 次」
=== 现象 ===
用户问:每日日志量不是有定时任务记录吗?为什么还要每次查询?
盘了一遍发现:定时任务确实有(每 30 分钟),接口也确实只读库(实测 0.1ms),
但每轮抓取都顺手跑一次**整天 index/stats** 拿条数(实测 6ms ~ 20s,随负载波动),
2 天 × 多个数据源 × 每 30 分钟 = 一天几十次重查询,纯浪费单实例 Loki 的算力,
还会和用户页面上的查询抢 CPU。

第二条更糟:仓库里所有快照的 partial 全是 true。
根因:partial 里混了两种语义 ——「今天还没过完」和「起点超出 24h」。
后者让昨天/前天的柱子也画成虚线空心,看起来「每天都在进行中」。

=== 修法 ===
1. 抓取按成本拆成两条路径(体积走 volume_range,条数走 index/stats):
   - 体积:今天 + 昨天,每 30 分钟刷,54ms~4s,无感
   - 条数:只对**已结束**且 entries==0 的天补,每天最多 3 次、间隔≥1h;
     尝试次数**先写库再查**,进程中途被杀也不会退化成每 30 分钟重试
2. partial 只表示「今天还没过完」,昨天起是实心柱
3. 0 与「未留存」分开:起点超出 24h 且查不到数据 → **跳过落库**保持 Missing。
   否则「数据被 retention 删掉」会被画成「那天没有日志」,比灰色柱误导得多
4. 每次抓取打日志:`[daily-volume] 2026-10-30 体积 291.37 GB 已落库(volume_range 4.27s,34 个桶)`
   —— 以后不用猜「到底抓没抓、走的哪条路径」

=== 实测取证(本集群)===
Loki 直接查(port-forward :13100):
  10-30(16.8h) volume_range 291.37GB/34桶   index/stats entries=2.21B
  10-29(24h)   volume_range 121.99GB/19桶   index/stats entries=927.9M
  10-28/10-27  volume_range 0 桶、index/stats 0 —— 已被 retention 删掉
  同范围 count_over_time 两次 60s 超时(永远别用在每日量上)
平台库里的快照与上述数字一致(288.32 / 121.75 GB),说明落库值可信;
10-27/10-28 的 34.87/25.65 GB 是当时抓的 —— 这正是「Loki 只留 1~3 天,
历史必须自己落库」的实证。

go vet ./services/ ./models/ 干净;go build 通过;go test ./services/ 通过。
2026-10-30 16:52:55 +00:00

40 lines
2.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package models
import "time"
// DailyVolumeSnapshot 每日日志量快照(平台自己留存的历史)。
//
// 为什么必须落库:
//
// Loki 的 retention_period 是 24 小时(运维可改,但默认就该短),
// 意味着「近 7 天每日日志量」直接查 Loki 时,只有最近 1 天有数据,
// 其余 6 天是空的 —— 界面上看着像坏了,实际是数据已经不在了。
// 想要 30 天 / 1 年趋势,只能在数据被删前每天抓一次存进平台库。
//
// 粒度说明:
// - 只按「数据源 + 命名空间」存。index/volume 系列接口只能按 selector 标签聚合,
// 拿不到按 app 的拆分(Loki 3.4 不支持 aggregateBy),
// 所以按 app 的历史量暂时无法落库,应用维度请看实时排行(有 20~30s 成本)。
// - 每天一条,日期用**本地时区**的 YYYY-MM-DD,避免跨时区对不上。
type DailyVolumeSnapshot struct {
ID uint `json:"id"`
Date string `json:"date"` // YYYY-MM-DD(本地时区)
SourceID uint `json:"sourceId"` // 数据源
Namespace string `json:"namespace"` // "" 表示该数据源全量
Bytes int64 `json:"bytes"` // 日志体积
Entries int64 `json:"entries"` // 日志条数
// Partial 该日期是否为"不完整的一天"(当天抓取时还没过完)。
// 前端用它区分"真实低峰"和"只统计到一半"。
//
// ⚠️ 只表示"今天还没过完"。早期实现把"起点超出 24 小时"也算成 partial,
// 导致昨天、前天的柱子也画成虚线空心,看起来像"每天都在进行中"(已修)。
Partial bool `json:"partial"`
// EntriesAttempts 条数(index/stats)已尝试次数。
// 整天跨度统计很贵(实测 6ms ~ 20s,取决于实例负载),
// 所以只对"已经过完的天"补,且最多重试 entriesMaxAttempts 次,
// 避免每 30 分钟的定时任务无脑重跑一遍。
EntriesAttempts int `json:"entriesAttempts"`
LastEntriesAttempt time.Time `json:"lastEntriesAttempt"`
CapturedAt time.Time `json:"capturedAt"`
}