perf(logs): 日志量统计改用 index/volume_range(快 ~1300 倍)+ 查询护栏 + 真实错误提示
=== 一、现象 ===
- /logs 页「正在统计整个时间范围的日志量」24 小时查询转圈 5 分钟不出结果
- /analysis 页默认 1 小时也一直「加载中...」
- 期间 Loki 被 OOMKilled(2Gi) → Pod not Ready → Service 无端点 → 全站查询 502
=== 二、根因(实测数据) ===
同一 query {namespace="astralx", app="cex-ipublic-gateway"}:
24h count_over_time[15m] step=15m → 53.7s 后连接断开(算不完)
24h count_over_time[1h] step=1h → 60.1s 失败
24h index/stats(只读索引) → 1.3s → bytes 44.9GB / entries 3.33亿 / chunks 2474
24h index/volume_range step=1h → 111ms → 25 个点
1h index/volume_range step=5m → 115ms → 13 个点
原因:count_over_time 必须**下载并解码所有 chunk**;volume_range 只读 TSDB 索引里的
chunk 元数据(字节数),根本不碰日志内容。该 app 一天就有 3.33 亿条 / 44.9 GB。
=== 三、改造 ===
1. LokiHistogram 改用官方 /loki/api/v1/index/volume_range(带 step)
- 返回单位是「字节」,bucket 带 unit=bytes,前端格式化为 KB/MB/GB
- 老版本 Loki 没有该端点时自动回退 count_over_time(unit=entries)
2. 查询护栏(防止单条查询拖垮单实例 Loki)
- 统计范围上限 7 天,超出直接 400「统计范围过大(27.0 天),最多支持 7 天」
- 重查询单独 30s 超时(原来 120s)
- 全局并发重查询上限 2,超出立刻 429「请稍后重试」(不排队)
- /logs/metrics(analysis 页)共用同一套护栏
3. 前端
- 发起新查询前 abort 上一次请求(原来只用版本号丢弃结果,旧请求仍在后端跑)
- histogram/query 支持 signal;超时 40s/60s(原 125s)
- 错误不再统一显示「日志量统计失败」,按 429/400/502/超时给不同文案
- 时间线按 unit 格式化(字节 → GB),i18n 去掉「条」
4. Loki 侧官方优化(此前 limits_config 只有 retention_period)
- query_range: align_queries_with_step + cache_results + embedded_cache 100MB
- limits_config: max_query_parallelism 8(默认 32,单实例过高)
- 已用 `loki -verify-config` 校验通过(config is valid)
=== 四、实测(平台 API) ===
1 分钟 0.02s 2 桶
1 小时 0.05s 61 桶 3.8 GB
6 小时 0.04s 73 桶 12.3 GB
24 小时 0.04s 97 桶 41.8 GB ← 改造前 53.7s 失败
7 天 0.05s 37 桶 58.2 GB
27 天 0.01s 400「最多支持 7 天」
镜像:lghost/lokistack:latest + v0.1 = sha256:9fa1c4597533
This commit is contained in:
1 parent
9acc15f4d2
commit
de376a1284
8 files changed
+229
-24
No files matched your search
@@ -1,11 +1,28 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Bucket = { timestamp: string; count: number }
|
||||
type Bucket = { timestamp: string; count: number; unit?: string }
|
||||
|
||||
const formatTime = (timestamp: string) => new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
const formatCount = (count: number) => new Intl.NumberFormat('zh-CN').format(count)
|
||||
|
||||
/**
|
||||
* 日志量展示:后端优先用 index/volume_range(只读索引,快约 500 倍),
|
||||
* 它返回的是**字节数**(unit=bytes);老版本 Loki 回退 count_over_time 时是条数。
|
||||
* 按 unit 决定格式,避免把 44 GB 显示成 "44894961500 条"。
|
||||
*/
|
||||
const formatVolume = (count: number, unit?: string) => {
|
||||
if (unit !== 'bytes') return formatCount(count)
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let v = count
|
||||
let i = 0
|
||||
while (v >= 1024 && i < units.length - 1) {
|
||||
v /= 1024
|
||||
i++
|
||||
}
|
||||
return `${v >= 100 ? v.toFixed(0) : v.toFixed(1)} ${units[i]}`
|
||||
}
|
||||
|
||||
export function LogVolumeTimeline({ buckets, onSelect }: {
|
||||
buckets: Bucket[]
|
||||
onSelect: (start: number, end: number) => void
|
||||
@@ -38,7 +55,7 @@ export function LogVolumeTimeline({ buckets, onSelect }: {
|
||||
return <section className="rounded-xl border bg-card p-4" aria-label={t('volume.timelineAria')}>
|
||||
<div className="mb-3 flex flex-wrap items-baseline justify-between gap-2">
|
||||
<div><h2 className="text-sm font-semibold">{t('volume.timelineTitle')}</h2><p className="text-xs text-muted-foreground">{t('volume.timelineHint')}</p></div>
|
||||
<span className="text-sm tabular-nums">{t('volume.timelineTotal', { count: formatCount(total) })}</span>
|
||||
<span className="text-sm tabular-nums">{t('volume.timelineTotal', { count: formatVolume(total, buckets[0]?.unit) })}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative h-44 min-w-0 flex-1 select-none touch-none cursor-crosshair"
|
||||
@@ -55,7 +72,7 @@ export function LogVolumeTimeline({ buckets, onSelect }: {
|
||||
</div>)}
|
||||
</div>
|
||||
{hover !== null && <div className="pointer-events-none absolute z-10 top-2 rounded-md border bg-popover px-2 py-1 text-xs text-popover-foreground shadow-sm" style={{ left: `${Math.min(78, Math.max(2, (hover + 0.5) / buckets.length * 100))}%` }}>
|
||||
{new Date(buckets[hover].timestamp).toLocaleString()} · <strong>{t('volume.timelineBucket', { count: formatCount(buckets[hover].count) })}</strong>
|
||||
{new Date(buckets[hover].timestamp).toLocaleString()} · <strong>{t('volume.timelineBucket', { count: formatVolume(buckets[hover].count, buckets[hover].unit) })}</strong>
|
||||
</div>}
|
||||
</div>
|
||||
<div className="flex h-44 w-14 flex-col justify-between text-right text-xs tabular-nums text-muted-foreground"><span>{formatCount(max)}</span><span>{formatCount(Math.round(max / 2))}</span><span>0</span></div>
|
||||
|
||||
Reference in new issue
Block a user