perf(daily-volume): 每日量改用 volume_range(20s → 54ms),图表改圆柱
=== 核心技巧:体积用 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
This commit is contained in:
1 parent
d0adc095dd
commit
5c7a3ec81d
2 files changed
+58
-25
No files matched your search
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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 ? (
|
||||
<div className="h-64 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data} margin={{ top: 12, right: 16, bottom: 4, left: 0 }}>
|
||||
<BarChart data={data} margin={{ top: 12, right: 16, bottom: 4, left: 0 }} barCategoryGap="18%">
|
||||
<defs>
|
||||
<linearGradient id="dailyVolumeFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="hsl(var(--info))" stopOpacity={0.35} />
|
||||
<stop offset="100%" stopColor="hsl(var(--info))" stopOpacity={0.02} />
|
||||
<linearGradient id="dailyVolumeBar" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="hsl(var(--info))" stopOpacity={0.95} />
|
||||
<stop offset="100%" stopColor="hsl(var(--info))" stopOpacity={0.45} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke="hsl(var(--border))" strokeDasharray="3 3" />
|
||||
@@ -72,15 +72,18 @@ export const DailyVolumeChart = memo(function DailyVolumeChart({ points, days, o
|
||||
return [`${formatBytes(value)}${partial ? ` · ${t('daily.inProgress')}` : ''}`, t('daily.seriesVolume')]
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="bytes"
|
||||
stroke="hsl(var(--info))"
|
||||
strokeWidth={2}
|
||||
fill="url(#dailyVolumeFill)"
|
||||
name={t('daily.seriesVolume')}
|
||||
/>
|
||||
</AreaChart>
|
||||
{/* 圆柱形柱子:顶部圆角 + 渐变,当天未结束的用空心区分 */}
|
||||
<Bar dataKey="bytes" name={t('daily.seriesVolume')} radius={[6, 6, 0, 0]} maxBarSize={44}>
|
||||
{data.map(item => (
|
||||
<Cell
|
||||
key={item.date}
|
||||
fill={item.partial ? 'hsl(var(--info) / 0.35)' : 'url(#dailyVolumeBar)'}
|
||||
stroke={item.partial ? 'hsl(var(--info))' : undefined}
|
||||
strokeDasharray={item.partial ? '3 3' : undefined}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in new issue
Block a user