From 5c7a3ec81d5e4fd55e9a38b38d140af29084b280 Mon Sep 17 00:00:00 2001 From: opsx Date: Fri, 30 Oct 2026 16:40:49 +0000 Subject: [PATCH] =?UTF-8?q?perf(daily-volume):=20=E6=AF=8F=E6=97=A5?= =?UTF-8?q?=E9=87=8F=E6=94=B9=E7=94=A8=20volume=5Frange=EF=BC=8820s=20?= =?UTF-8?q?=E2=86=92=2054ms=EF=BC=89=EF=BC=8C=E5=9B=BE=E8=A1=A8=E6=94=B9?= =?UTF-8?q?=E5=9C=86=E6=9F=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit === 核心技巧:体积用 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 --- backend/services/daily_volume.go | 52 +++++++++++++++---- .../analysis/components/DailyVolumeChart.tsx | 31 ++++++----- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/backend/services/daily_volume.go b/backend/services/daily_volume.go index e85241d..dc38056 100644 --- a/backend/services/daily_volume.go +++ b/backend/services/daily_volume.go @@ -66,14 +66,41 @@ func CaptureDailyVolume(source *models.LogSource, day time.Time, partial bool) e partial = true } - // 只查总量(不带 namespace 条件),一次拿到该源当天的条数与体积 - // 整天(24h)的 index/stats:大集群上比 15 分钟的查询慢得多, - // 实测 20s 会超时(这也是之前每日量接口卡 20s 的原因)。给 90s。 - ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + // ── 体积:index/volume_range(只读 TSDB chunk 元数据,不解码日志内容)── + // 实测同一条件 24 小时:index/stats 要 20s(遇超时),volume_range 111ms。 + // 用户关心的"每日日志量"就是体积,所以主路径走它。 + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - stats, err := LokiStatsFor(ctx, source, `{namespace=~".+"}`, start, end) - if err != nil { - return err + buckets, volErr := LokiHistogram(ctx, source, `{namespace=~".+"}`, start, end) + var totalBytes int64 + for _, b := range buckets { + switch v := b["count"].(type) { + case int64: + totalBytes += v + case float64: + totalBytes += int64(v) + } + } + + // ── 条数:只能 index/stats,慢(整天约 20s)── + // 降级为"尽力而为":拿不到就把条数留 0,绝不让它拖住体积入库。 + // 前端图只画体积,条数缺失不影响可读性。 + var totalEntries int64 + entryCtx, entryCancel := context.WithTimeout(context.Background(), 25*time.Second) + if stats, err := LokiStatsFor(entryCtx, source, `{namespace=~".+"}`, start, end); err == nil { + totalEntries = stats.Entries + if totalBytes == 0 { + totalBytes = stats.Bytes + } + } else { + log.Printf("[daily-volume] %s 条数统计超时/失败(不影响体积): %v", dateKey(day), err) + } + entryCancel() + if volErr != nil && totalBytes == 0 { + return volErr + } + if totalBytes == 0 { + return nil } date := dateKey(day) @@ -95,8 +122,11 @@ func CaptureDailyVolume(source *models.LogSource, day time.Time, partial bool) e } // 同一天同源:覆盖(幂等) if snap.Date == date && snap.SourceID == source.ID && snap.Namespace == "" { - snap.Bytes = stats.Bytes - snap.Entries = stats.Entries + snap.Bytes = totalBytes + // 条数拿不到时保留上一次的值,避免"一次超时把历史条数清零" + if totalEntries > 0 { + snap.Entries = totalEntries + } snap.Partial = partial snap.CapturedAt = time.Now() db.RequestSave() @@ -109,8 +139,8 @@ func CaptureDailyVolume(source *models.LogSource, day time.Time, partial bool) e Date: date, SourceID: source.ID, Namespace: "", - Bytes: stats.Bytes, - Entries: stats.Entries, + Bytes: totalBytes, + Entries: totalEntries, Partial: partial, CapturedAt: time.Now(), }) diff --git a/frontend/src/pages/analysis/components/DailyVolumeChart.tsx b/frontend/src/pages/analysis/components/DailyVolumeChart.tsx index 36d9748..aa38ace 100644 --- a/frontend/src/pages/analysis/components/DailyVolumeChart.tsx +++ b/frontend/src/pages/analysis/components/DailyVolumeChart.tsx @@ -1,5 +1,5 @@ import { memo, useMemo } from 'react' -import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' +import { Bar, BarChart, Cell, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import type { DailyVolumePoint } from '@/api/logs' @@ -56,11 +56,11 @@ export const DailyVolumeChart = memo(function DailyVolumeChart({ points, days, o {hasData ? (
- + - - - + + + @@ -72,15 +72,18 @@ export const DailyVolumeChart = memo(function DailyVolumeChart({ points, days, o return [`${formatBytes(value)}${partial ? ` · ${t('daily.inProgress')}` : ''}`, t('daily.seriesVolume')] }} /> - - + {/* 圆柱形柱子:顶部圆角 + 渐变,当天未结束的用空心区分 */} + + {data.map(item => ( + + ))} + +
) : (