Commit Graph
5 Commits
Author SHA1 Message Date
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
opsx 7b1ffd0274 fix(daily-volume): 30/90/365 天切换"没反应";/logs/stats 加短缓存
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
=== 1. 切换范围没反应(真 bug)===
现象:7/30/90/365 天切换后图完全一样。
根因:GetDailyVolumeHistory 只返回**已抓到的快照**。平台只有 4 天数据,
      所以任何 days 都只返回 4 个点 → 图表一模一样。
修复:始终返回 days 个点,缺失的天用 Missing=true 标记。
      实测:days=7→7 点(缺3)、30→30 点(缺26)、90→90(缺86)、365→365(缺361)。

为什么必须区分 Missing 和"0 字节":
  前者是"平台没留存",后者是"那天确实没日志"。
  混在一起会让 30/90 天的图上出现大片 0,被误读成"日志量暴跌/系统停摆"。
前端:缺失的天画灰色浅柱,tooltip 显示"未留存",图下方提示
      "其中 N 天平台没有留存记录(灰色柱),不是当天没有日志"。

=== 2. /logs/stats 加 90 秒缓存 ===
分析页每次进入/切范围都打 index/stats,实测 5 分钟窗口冷查询 1.2 秒
(60 分钟更慢)。这类聚合值可容忍分钟级陈旧,加与指标查询一致的缓存:
  90s TTL + 时间窗按 2 分钟取整 + 上限 200 条 + 过期清理
实测:重复请求 1227ms → 8ms。

=== 3. 顺带确认已生效的优化 ===
  histogram(24h)   : 54ms(volume_range)
  daily-volume     : 10ms(只读库)
  stats 二次请求    : 8ms(新缓存)
  长范围(>1h)       : 不跑 count_over_time(预聚合快照 + 跳过内容扫描)

Pod = Hub latest = 12453ea723c1
2026-10-30 16:46:40 +00:00
opsx 5c7a3ec81d perf(daily-volume): 每日量改用 volume_range(20s → 54ms),图表改圆柱
CI / Backend (go vet + test) (push) Canceled after 0s
CI / Frontend (lint + build + test) (push) Canceled after 0s
=== 核心技巧:体积用 index/volume_range,不要用 index/stats ===
同为 24 小时、同一 selector,两条索引查询差 370 倍:
    index/stats      : 整天聚合,实测 20s 直接超时(之前每日量接口卡 20s 的根因)
    index/volume_range: 按 step 分桶只累加 chunk 字节,实测 54ms,97 个桶

原因:index/stats 要跨全部 chunk 去重统计 streams/entries;
volume_range 只把索引里记录的 chunk 字节数按时间桶求和,不解码内容。

=== 后端改动 ===
services/daily_volume.go:
  体积:LokiHistogram(走 volume_range,step 由跨度自动选)→ 累加 54ms
  条数:只能 index/stats,降级为「尽力而为」25s 超时;
        拿不到就保留上一次的值(不能因为一次超时把历史条数清零),
        绝不让它拖住体积入库。
  前端图只画体积,条数缺失不影响可读性。

=== 前端改动 ===
DailyVolumeChart:AreaChart(面积)→ BarChart(圆柱)
  顶部圆角 radius=[6,6,0,0]、渐变填充、maxBarSize=44
  当天未结束的柱子用半透明 + 虚线描边区分,避免把"才过半天"误读成"量掉了"

=== 实测 ===
  24h volume_range: 54ms,97 桶,合计 369.61 GB(与全集群 ~370GB/天 吻合)
  /logs/daily-volume 接口: 10ms(读库)
  Pod = Hub latest = 4cdc8b9c4841
2026-10-30 16:40:49 +00:00
opsx cb1ef1dff4 fix(alerts,analysis): 告警引擎稳定性 5 项修复;每日量接口去掉同步 Loki 查询(20s→10ms)
=== 一、告警引擎稳定性审查(引擎本身是异步的:ticker + goroutine)===
1) 【高】ticker 无重叠保护
   ticker 每 1 分钟触发;一轮规则检查 8 条 × 20~30s = 2~4 分钟,远超一分钟。
   原来每过一分钟就再开一轮 → 查询越堆越多,抢走页面查询的 Loki 资源,
   每轮越来越慢,最终表现为"告警不响了"。
   → 新增 alertCheckRunning(atomic.Bool),上一轮未完成直接跳过本轮并记日志。

2) 【高】单条规则无超时
   RunAlertCheck(context.Background()) 没有 deadline:Loki 一旦挂住,整轮永久运行,
   叠加问题 1 会让引擎再也不启动新一轮(静默死亡)。
   → 每轮总超时 5 分钟(alertRoundTimeout)+ 单条规则 90s(alertRuleTimeout)。

3) 【中】panic 只在整个 tick 层 recover
   一条规则 panic 会让本轮后续规则全部跳过。
   → per-rule recover,panic 记为一条 failed 投递记录,后续规则继续。

4) 【中】冷却记录每分钟写一条,把真实发送记录挤光
   AlertDeliveries 只保留最近 50 条,而处于冷却期时每个 tick 都写一条 cooldown,
   一小时内真实的"已发送"记录就全被挤掉了,发送历史没法看。
   → recordCooldownThrottled():同一规则 10 分钟最多写一条。附单测。

5) 规则串行本身保留(避免同时压垮 Loki),但配合 1) 才安全。

=== 二、每日日志量接口:20 秒 → 10 毫秒 ===
根因:GetDailyVolumeHistory 里**同步** await 一次"整天 index/stats"补抓当天数据,
那次查询超时 20s → 整个接口 20s → 前端超时 → 页面显示"每日日志量没数据";
更糟的是这条重查询还和页面的 histogram/stats 抢 Loki,导致那两个接口同时 502
(访问日志实证:daily-volume latency_ms 20000.997,stats/histogram 502)。
→ 接口改为**只读库**,不触发任何 Loki 查询;当天数据由后台每 30 分钟的
  定时任务补(CaptureRecentDailyVolumes)。
→ 同时把整天 index/stats 的超时从 20s 提到 90s(370GB/天,20s 不够)。
实测:10ms。

=== 三、通知通道改名「默认告警」(schemaVersion v8→v9) ===
"lark 默认告警" / "默认通知通道(待配置 Webhook)" → 统一为「默认告警」。
只改名,不动 Webhook、不动 enabled:
首次部署创建的占位通道必须保持「空 Webhook + 禁用」,由管理员自己填,
否则会出现"已启用但空地址"的通道,告警一命中就全部投递失败。

=== 四、其它文案 ===
「每日日志量」副标题精简为「每天的日志总量,平台保留历史趋势」,
并移除「平台留存历史」徽标(用户反馈多余)。

=== 实测(303134c0c718)===
  /logs/daily-volume:10ms(原 20000ms)
  通道列表:id=1 name="默认告警" enabled=true hasWebhook=true
  go test ./... 全绿(含新增 TestRecordCooldownThrottled)
2026-10-29 03:38:30 +00:00
opsx 37e2f0febc feat(analysis,alerts): 每日量落库看历史趋势、趋势图/饼图可读性、告警三分页
=== 1. 每日日志量必须落库(核心问题:Loki 只留 24 小时)===
实测:请求"近 7 天每日量",Loki 只返回 27 个**小时**桶,全部在最近 24h 内 ——
因为 retention_period=24h,更早的 chunk 已经被删了。
也就是说「固定近 7 天」这张卡片,过去一直是"6 天空白 + 1 天有数据",
用户看着像坏了,实际是数据物理不存在。

方案:平台自己留存历史。
  - models.DailyVolumeSnapshot{Date(本地YYYY-MM-DD), SourceID, Bytes, Entries, Partial}
  - services/daily_volume.go
      CaptureDailyVolume        按天抓 index/stats 落库(幂等覆盖)
      StartDailyVolumeCapture   启动 20s 后回补 + 每 30 分钟检查一次
      GetDailyVolumeHistory     读库返回最近 N 天(今天会实时补抓)
  - 保留 400 天(够看年度趋势),按 source+date 去重
  - 前端可选 7 / 30 / 90 / 365 天,能看多长取决于平台积累多久

为什么用"每 30 分钟检查昨天有没有落库"而不是固定 00:10 定时:
平台可能整晚没运行,固定时点的定时器会直接错过;检查式回补不依赖平台在线时刻。

Partial 标记(关键,否则会误读):
  - 当天未结束 → partial=true
  - 待抓取日期的起点已超出 24h 保留期 → partial=true(Loki 里就没那么多数据)
实测:10-27 32.5GB/2.65亿条 partial=true(保留期外),
      10-28 348GB/28.56亿条 partial=true(当天进行中)

=== 2. 修一个 Go 副本 bug(自己写的,靠实测发现)===
第一版用 `for _, snap := range db.DailyVolumeSnapshots` 后改 snap 字段,
range 拿到的是**副本**,改动不会写回切片 → 同一天永远保持首次抓取的值,
partial 也永不更新(实测发现 10-27 该是 true 却显示 false)。
改为 `for i := range ... { snap := &db.DailyVolumeSnapshots[i] }`。

=== 3. 趋势图可读性 ===
从"裸折线"换成"渐变面积图",并在图上方给出峰值 / 均值:
  - 面积图能一眼看出"这一段整体比上一段高",折线只能靠对齐格子数
  - 峰值/均值避免用户自己对刻度
  - Y 轴与 tooltip 统一 formatBytes,"每桶体积"写进 subtitle
顺带确认:趋势图只画体积(字节),不再混入 error(条数)——单位不同不能同轴。

=== 4. 饼图:命名空间分布 → 应用日志量分布 ===
本环境的命名空间只有 astralx / log 两个,饼图没有信息量;
改成按应用看谁在刷日志(前 8 名 + 其他),才是运维真正要的答案。

=== 5. 告警页拆成三个 Tab ===
「告警规则 / 通知通道 / 发送历史」独立成分页(用 ui/tabs)。
之前通道是折叠块、历史堆在规则下面,规则一多就要滚很久才看得到。
发送历史增加空状态文案,不再是"空页面不知道有没有在发"。

=== 6. 内置规则关联默认通道 + 首次部署 webhook 留空的评估 ===
结论:**webhook 可以留空,但通道必须是禁用状态**。
  - 若种子给的是"已启用 + 空地址"通道,规则一命中就必然投递失败,
    发送历史堆满红色失败,反而让人以为平台坏了
  - 所以首次部署建一个**禁用**的占位通道「默认通知通道(待配置 Webhook)」,
    内置规则全部挂到它;管理员填好 Webhook 再启用即可,不用逐条关联
  - 保存时加校验:启用则必须有非空 Webhook(返回明确原因,不是裸 500)
  - schemaVersion v5→v6:把已有的"无通道"规则补挂到默认通道
  实测:8 条规则全部已关联通道

=== 实测(v11-1028-2252 / 187062644c0f)===
  /logs/daily-volume?days=7  → 2 天数据,partial 标记正确
  规则 8 条全部有 channelIds
  平台 ready、健康 ok
2026-10-28 22:54:12 +00:00