核心使命与设计理念
6.1 What - MetricCache 是什么?
MetricCache 是 Koordlet 中的时间序列数据库(TSDB),用于存储和查询历史指标数据。
核心职责:
- 接收来自 MetricAdvisor 的实时指标
- 高效存储指标数据(内存 + 磁盘)
- 支持快速范围查询(时间范围、聚合函数)
- 支持复杂的时间序列分析操作
- 自动清理过期数据,维护存储空间
MetricCache 的数据视图:
css
┌─────────────────────────────────────────────────┐
│ MetricCache 中的数据组织 │
├─────────────────────────────────────────────────┤
│ │
│ 存储维度: │
│ ├─ Pod 级指标 │
│ │ ├─ Pod UUID -> [CPU 时间序列] │
│ │ ├─ Pod UUID -> [Memory 时间序列] │
│ │ └─ Pod UUID -> [Network 时间序列] │
│ │ │
│ ├─ Node 级指标 │
│ │ ├─ Node Name -> [CPU 时间序列] │
│ │ ├─ Node Name -> [Memory 时间序列] │
│ │ └─ Node Name -> [Network 时间序列] │
│ │ │
│ ├─ Container 级指标 │
│ │ ├─ Container ID -> [CPU 时间序列] │
│ │ └─ Container ID -> [Memory 时间序列] │
│ │ │
│ └─ 聚合指标 │
│ ├─ Node 上所有 LSR Pod 的平均 CPU │
│ ├─ Node 上所有 BE Pod 的总内存 │
│ └─ 集群级别的资源使用聚合 │
│ │
│ 时间精度: │
│ ├─ 秒级: 最近 1 小时 │
│ ├─ 分钟级: 最近 1 周 │
│ └─ 小时级: 历史存档 │
│ │
└─────────────────────────────────────────────────┘
6.2 Why - 为什么需要 TSDB?
问题 1:需要查询历史数据进行分析和预测
bash
没有 MetricCache 的方案:
QOSManager 需要做决策:
├─ 决策: 我应该现在驱逐 BE Pod 吗?
├─ 需要的信息:
│ ├─ 当前 LS Pod 的 CPU 使用: 2.5 CPU
│ ├─ 过去 5 分钟 LS CPU 的趋势: 从 1 到 2.5(快速增长)
│ ├─ 过去 1 小时 LS CPU 的模式: 每整点时突发
│ └─ 历史上相同时段的情况: 通常在 3.5 CPU
│
├─ 问题:
│ ├─ 仅看当前值 (2.5) 无法判断趋势
│ ├─ 无法区分"突发"与"正常波动"
│ ├─ 无法根据历史规律预测接下来的行为
│ └─ 决策极易出错
有 MetricCache 的方案:
├─ 查询历史: GetMetrics(pod_uid, 1h) 获取过去 1 小时的数据
├─ 分析: 计算 p50/p95/p99,看出趋势斜率
├─ 预测: 基于增长趋势,预测 10 分钟后的值
├─ 决策: 如果预测超过 limit,则提前驱逐
└─ 效果: 精准、可靠、防患于未然
问题 2:需要高效查询而不是重新采集
yaml
没有缓存的方案:
每次做决策都重新采集:
├─ QOSManager: "我需要 Pod A 过去 1 小时的 CPU 数据"
├─ 方案: 从头采集 Pod A 的历史 cgroup 数据
│ └─ 读取 cgroup 文件 × 3600 次(每秒一次采样)
├─ 耗时: 3600 秒的数据读取 = 无法容忍的延迟
└─ 不可行!
有 MetricCache 的方案:
├─ 数据已经在内存中,无需重新读取
├─ 查询 1 小时 3600 个数据点: < 10ms
└─ 高效!
性能对比:
┌─────────────────────────────────┐
│ 查询时间对比 │
├─────────────────────────────────┤
│ 无缓存(重新采集) | 3600s+ │
│ 有缓存(内存查询) | <10ms │
│ 性能提升 | 360000x │
└─────────────────────────────────┘
问题 3:支持复杂的时间序列分析
yaml
常见的分析需求:
需求 1: 时间窗口聚合
└─ 计算过去 1 小时每 5 分钟的平均 CPU
需求 2: 趋势计算
└─ 计算过去 6 小时的 CPU 增长速度(/分钟)
需求 3: 异常检测
└─ 当前值是否超过历史 p95?
需求 4: 衰减权重聚合
└─ 最近的数据权重更高,旧数据权重更低
需求 5: 时间对齐
└─ 对齐多个 Pod 的 CPU 数据,计算相关性
需求 6: 跨指标组合
└─ CPU 和内存的相关性分析
直接在采集器中实现:
└─ 每个决策都重新计算 → CPU 占用高,逻辑重复
使用 MetricCache:
└─ 所有分析都在 TSDB 中完成 → 逻辑集中、高效
6.3 How - TSDB 的核心机制
时间序列存储架构:
ini
┌──────────────────────────────────────────┐
│ MetricCache 的三层存储架构 │
├──────────────────────────────────────────┤
│ │
│ L1: 热数据层(内存,最近 1 小时) │
│ ├─ 数据结构: 数组(Ring Buffer) │
│ ├─ 时间精度: 秒级 │
│ ├─ 容量: ~3600 个数据点 │
│ ├─ 访问延迟: < 1ms │
│ └─ 更新: 实时推送 │
│ │
│ L2: 温数据层(内存,1-24 小时) │
│ ├─ 数据结构: 压缩时间序列(gorilla) │
│ ├─ 时间精度: 分钟级 │
│ ├─ 容量: ~1440 个数据点 │
│ ├─ 访问延迟: 1-5ms │
│ └─ 压缩率: 10x(原始大小的 10%) │
│ │
│ L3: 冷数据层(磁盘,1 周以上) │
│ ├─ 数据结构: RocksDB(K-V 存储) │
│ ├─ 时间精度: 小时级 │
│ ├─ 容量: ~168 个数据点 │
│ ├─ 访问延迟: 10-50ms │
│ └─ 压缩率: 100x │
│ │
└──────────────────────────────────────────┘
访问模式:
最近 1 小时: L1 (热)
└─ 查询延迟 < 1ms
└─ 用途: 实时决策 (QOSManager)
1 小时前 - 1 天前: L2 (温)
└─ 查询延迟 < 5ms
└─ 用途: 趋势分析、预测
1 天前 - 1 周前: L3 (冷)
└─ 查询延迟 10-50ms
└─ 用途: 历史存档、审计
查询流程:
用户请求: GetMetrics(pod_uid, from=1h ago, to=now)
│
├─→ 检查 L1: [now-1h, now] 范围有数据
├─→ 直接返回 L1 的秒级数据
└─→ 延迟: < 1ms
用户请求: GetMetrics(pod_uid, from=24h ago, to=now)
│
├─→ [now-1h, now]: 从 L1 获取(秒级)
├─→ [now-24h, now-1h]: 从 L2 获取(分钟级,需要解压)
└─→ 延迟: < 10ms
MetricCache 的完整实现
6.4 Ring Buffer - 秒级数据结构
高性能的环形缓冲区:
go
// Ring Buffer 的实现思想
type RingBuffer struct {
data []float64 // 固定大小数组 (3600 元素)
pos int // 当前写入位置
full bool // 是否已填满一圈
}
插入操作:
pos 指向下一个要写的位置
新数据覆盖最老的数据
当 pos 达到末尾时,回到开头
特点:
├─ O(1) 的插入性能
├─ O(1) 的查询性能
├─ 固定内存占用(不会增长)
└─ 自动丢弃最老的数据(无需显式 GC)
Ring Buffer 的实际使用:
ini
时间线演示:
时刻 T=10:00:00, 数据推送开始
Ring Buffer 初始状态:
pos=0, data=[nil, nil, nil, ..., nil] (3600 个槽)
T=10:00:01: 推送数据 2.5 (CPU)
写入: data[0] = 2.5
更新: pos = 1
T=10:00:02: 推送数据 2.6
写入: data[1] = 2.6
更新: pos = 2
... (持续推送数据)
T=10:59:59: 第 3599 个数据
写入: data[3598] = 2.4
更新: pos = 3599
T=11:00:00: 第 3600 个数据(缓冲区满)
写入: data[3599] = 2.3
更新: pos = 0, full = true
T=11:00:01: 第 3601 个数据(开始覆盖)
写入: data[0] = 2.4 ← 覆盖 T=10:00:01 的数据
更新: pos = 1
结果:
├─ 保持最近 3600 秒的数据
├─ 最老的数据被最新的覆盖
└─ 内存占用永远不增加
查询操作:
Query: 获取最近 1 小时(过去 3600 秒)的数据
如果 full=false(缓冲区未满):
└─ 返回 data[0:pos](已有的数据)
如果 full=true(缓冲区已满):
├─ 将 [pos, 3600) 的数据作为旧部分
├─ 将 [0, pos) 的数据作为新部分
└─ 按时间顺序拼接返回
性能指标:
├─ 插入: O(1), < 0.01ms
├─ 查询: O(n), n=3600, < 1ms
└─ 内存: 固定 ~14 KB (3600 × 8 bytes float64)
6.5 Gorilla 压缩 - 分钟级数据压缩
时间序列压缩算法:
ini
为什么需要压缩?
L1 (秒级,1 小时): 3600 数据点 × 8 bytes = 28.8 KB
L2 (分钟级,24 小时): 1440 数据点 × 8 bytes = 11.52 KB × 24 = 276 KB
└─ 如果有 1000 个 Pod,总计 276 MB
问题:
└─ 随着 Pod 数量和时间增长,内存占用会线性增加
Gorilla 压缩的思想:
└─ 时间序列通常有很高的冗余
└─ 相邻数据点通常非常接近(差值小)
└─ 可以只存储"差值"而不是绝对值
压缩率对比:
原始数据(24 小时,分钟采样):
CPU 使用率: 2.5, 2.6, 2.4, 2.5, 2.3, 2.1, 2.2, 2.4, ...
字节大小: 1440 × 8 = 11.52 KB
差值存储:
首个数据: 2.5 (完整存储,32 bits)
差值序列: 0.1, -0.2, 0.1, -0.2, -0.2, 0.1, 0.2, ...
└─ 大多数差值很小,可以用 4 bits 或 8 bits 表示
└─ 压缩后大小: ~1.5 KB
压缩率: 11.52 KB / 1.5 KB = 7.68x
实际效果:
└─ 对于平稳变化的指标: 8-10x 压缩率
└─ 对于剧烈波动的指标: 2-4x 压缩率
Gorilla 算法的工作流程:
ini
编码(压缩):
输入: 时间戳序列 [T0, T1, T2, ...]
数值序列 [V0, V1, V2, ...]
步骤 1: 时间戳增量编码
DeltaT0 = T0 (完整存储)
DeltaT1 = T1 - T0 = 60 秒
DeltaT2 = T2 - T1 = 60 秒
DeltaT3 = T3 - T2 = 61 秒
└─ 大多数 Delta 都是 60 秒
└─ 可以只编码"与标准 60 秒的偏差"
步骤 2: 数值变化编码
V0 = 2.5 (完整存储,32 bits)
V1 = 2.6 (差值: +0.1)
V2 = 2.4 (差值: -0.2)
V3 = 2.5 (差值: +0.1)
└─ 差值绝对值 < 0.5,用 4-8 bits 表示
步骤 3: 位编码
└─ 使用变长整数编码(VInt)
└─ 小的差值用少量 bit,大差值用多 bit
输出: 压缩后的字节流
解码(解压):
解压器按照编码顺序反向操作:
1. 恢复时间戳增量
2. 恢复数值差值
3. 累加得到完整序列
性能:
├─ 编码: ~100 ns/point
├─ 解码: ~50 ns/point
└─ 压缩到磁盘: ~0.5 KB/小时/Pod
6.6 RocksDB - 长期存储
使用 RocksDB 存储冷数据:
yaml
RocksDB 的特点:
为什么选择 RocksDB?
├─ LSM Tree 结构,顺序 I/O 高效
├─ 支持列式存储压缩
├─ 可配置的压缩算法(LZ4, ZSTD)
├─ 性能与空间的平衡
└─ 生产级别成熟度(Facebook 开源)
数据组织:
Key: {pod_uid}_{metric_name}_{timestamp_hour}
Value: 该小时的压缩时间序列
示例:
Key: "pod-123_cpu_2025-12-30-10"
Value: [Gorilla 压缩的该小时 CPU 数据]
Size: ~50 bytes (相比原始 3600 × 8 = 28.8 KB,压缩 500+x)
存储大小估算(1000 Pod 集群,2 周存档):
原始大小(不压缩):
├─ 1000 Pod × 3 指标(CPU/Mem/Net)
├─ × 14 天 × 24 小时
└─ = 1000 × 3 × 14 × 24 × 8 bytes ≈ 80 GB
RocksDB 压缩后:
├─ 时间戳增量压缩: ~7x
├─ Gorilla 数值压缩: ~8x
├─ RocksDB 列压缩: ~3x
└─ = 80 GB / (7 × 8 × 3) ≈ 475 MB
存储成本:
└─ 每个 Pod 每天: 475 MB / 1000 / 14 ≈ 34 KB
└─ 整个集群: ~34 MB/天(可接受)
6.7 查询接口和使用示例
MetricCache 的查询 API:
go
interface MetricCache {
// 单指标查询
GetMetric(resourceID string, metric string,
from time.Time, to time.Time) ([]Point, error)
// 聚合查询
GetMetricAgg(resourceID string, metric string,
from time.Time, to time.Time,
aggregator AggregatorFunc) (float64, error)
// 批量查询
GetMetricsInBatch(queries []QueryRequest) ([]TimeSeries, error)
// 实时最新值
GetLatest(resourceID string, metric string) (float64, error)
}
支持的聚合函数:
├─ Average: 平均值
├─ Max: 最大值
├─ Min: 最小值
├─ Sum: 求和
├─ Percentile(p): 分位数
├─ Derivative: 导数(变化速率)
└─ Integral: 积分(累计)
生产案例:趋势分析和预测
ini
场景:QOSManager 需要判断是否应该驱逐 BE Pod
决策逻辑:
检查点 1: 当前状态
current_cpu = GetLatest(pod_uid, "cpu") // 查询最新值
if current_cpu < 1.5 CPU:
└─ 不驱逐,资源充足
检查点 2: 短期趋势(最近 5 分钟)
recent_metrics = GetMetric(pod_uid, "cpu",
time.Now()-5m, time.Now())
trend = CalculateTrend(recent_metrics)
if trend > 0.2 CPU/min: // CPU 快速增长
└─ 说明有突发,需要更激进地抑制
检查点 3: 历史对比(过去 1 小时同类时段)
hour_ago = GetMetric(pod_uid, "cpu",
time.Now()-1h-5m, time.Now()-1h)
hour_ago_avg = GetMetricAgg(..., Average)
if current_cpu > hour_ago_avg * 1.5: // 比历史高 50%
└─ 说明异常,可能需要驱逐
检查点 4: 多维度风险评分
mem_trend = CalculateTrend(GetMetric(..., "memory", ...))
network_trend = CalculateTrend(GetMetric(..., "network", ...))
risk_score = 0
if cpu_trend > 0.1: risk_score += 30
if mem_trend > 5%: risk_score += 40
if network_trend > 20%: risk_score += 10
if risk_score > 60:
└─ 驱逐该 BE Pod,保护 LS Pod
完整决策:
T=10:10:00 采集新指标
T=10:10:05 MetricCache 可用,开始查询
T=10:10:06 获取结果:risk_score = 75
T=10:10:07 决策:驱逐 Pod
T=10:10:08 执行:发送驱逐信号
T=10:10:12 验证:Pod 开始关闭,CPU 释放
总耗时: 12 秒,从检测到执行完成
MetricCache 的运维和优化
6.8 缓存淘汰策略
三级数据淘汰:
ini
L1 (热数据,秒级) 淘汰:
├─ 保留期限: 1 小时
├─ 淘汰方式: Ring Buffer 自动覆盖
├─ 淘汰时机: 新数据进来,自动覆盖最老的
└─ 特点: 零额外 GC 成本
L2 (温数据,分钟级) 淘汰:
├─ 保留期限: 24 小时
├─ 淘汰方式: 当 Ring Buffer 满后,不再更新
│ 旧数据保留不变
├─ 手动清理: 24h 后的分钟级数据转移到 L3
└─ 触发: 定时任务(每小时检查一次)
L3 (冷数据,小时级) 淘汰:
├─ 保留期限: 7 天(可配置)
├─ 淘汰方式: TTL 淘汰
│ └─ 读取时检查是否超过 TTL
│ └─ 后台定时扫描删除过期 key
├─ RocksDB 的 Compaction:
│ └─ 删除的 key 在 Compaction 时真正移除
│ └─ 优化存储空间
└─ 触发: 定时 Compaction(每天凌晨 02:00)
淘汰流程:
T=Day 0:
L1: 最近 1 小时数据
L2: 空
L3: 空
T=Day 1 (1 小时后):
└─ L1 满,最旧的秒级数据被覆盖
└─ 将 L1 的数据聚合为分钟级,保存到 L2
T=Day 2 (24 小时后):
└─ L2 满,最旧的分钟级数据确定
└─ 将 L2 的数据聚合为小时级,保存到 L3
└─ 压缩并写入 RocksDB
T=Day 8 (7 天后):
└─ TTL 过期,标记为删除
└─ 等待 Compaction 清理
└─ 内存和磁盘空间释放
6.9 性能优化技巧
问题 1:查询延迟过高
markdown
症状:GetMetrics() 耗时 > 20ms
可能原因:
├─ 数据存储在 RocksDB(冷数据)而不是内存
├─ 查询范围过大(超过 24 小时)
├─ RocksDB 的 Compaction 正在进行
诊断:
metrics_latency = time.Since(query_start)
if metrics_latency > 20ms:
check location: // L1/L2/L3?
check range: // 查询范围多长?
check compaction: // Compaction 状态?
优化方案:
1. 提前预热 L1 数据
└─ 系统启动时,预先加载最近 1 小时的数据
2. 缩小查询范围
└─ 改为: 查询最近 1 小时(L1)+ 聚合值
└─ 不要查询整个历史
3. 调整 RocksDB 的 Compaction 时机
└─ 改为凌晨 00:00-02:00 进行
└─ 避免业务高峰
4. 使用缓存加速
└─ 对常见的聚合查询(如 1h avg)进行缓存
└─ 避免重复计算
5. 并行查询
└─ L1 和 RocksDB 的查询可以并行进行
└─ 取最后一个完成的结果
问题 2:内存占用过高
yaml
症状:MetricCache 占用 > 5 GB 内存(1000 Pod 节点)
可能原因:
├─ L1 + L2 的缓冲区设置过大
├─ 没有正确转移数据到 L3
├─ 内存泄漏
诊断:
理论值 = Pod 数 × 指标数 × 环缓冲区大小
1000 × 3 × (3600 + 1440) × 8 bytes = ~165 MB (太小)
如果实际 5 GB,说明有问题
优化方案:
1. 减少 Ring Buffer 大小
└─ 改为 900 秒而不是 3600 秒
└─ 减少内存 70%,仍能满足大多数查询
2. 加速 L2 到 L3 的迁移
└─ 改为 8 小时后迁移(而不是 24 小时)
└─ 使用压缩减少内存占用
3. 按优先级保留数据
└─ LS Pod: 保留 7 天
└─ BE Pod: 保留 3 天(变化快,旧数据价值小)
问题 3:数据丢失或不一致
markdown
症状:查询同一时间段的数据,值不一样
可能原因:
├─ L1→L2 迁移时的数据丢失
├─ RocksDB 的 Compaction 过程中数据不一致
├─ 查询到的是聚合值而不是原始值
避免方案:
1. 使用 Write-Ahead Log (WAL)
└─ 在迁移前,记录操作日志
└─ 宕机时可以恢复
2. 双写验证
└─ 数据同时写入 L1 和 L2
└─ 定期对比验证一致性
3. 只保存原始精度的数据
└─ 不要聚合,避免精度损失
└─ 聚合操作在查询时执行
4. 版本控制
└─ 每次修改数据时记录版本号
└─ 支持时间点恢复
6.10 生产配置建议
yaml
# MetricCache 配置示例
apiVersion: v1
kind: ConfigMap
metadata:
name: koordlet-config
namespace: koordinator-system
data:
koordlet-config.yaml: |
metricCache:
# L1 - 热数据(秒级)
hotDataBufferSize: 3600 # 1 小时的数据点
hotDataRetention: 1h
# L2 - 温数据(分钟级)
warmDataBufferSize: 1440 # 1 天的数据点
warmDataRetention: 24h
warmDataCompression: gorilla
# L3 - 冷数据(小时级)
coldDataRetention: 168h # 7 天
coldDataCompression: zstd
rockdbPath: /var/lib/koordlet/metric-cache
rockdbCompactionHour: 2 # 凌晨 2:00 Compaction
# 查询优化
queryTimeout: 10s
maxConcurrentQueries: 100
queryCache:
enabled: true
ttl: 30s
# 性能调优
asyncPush: true # 异步推送指标
pushBatchSize: 100 # 批量推送大小
pushInterval: 100ms # 推送间隔
6.11 监控指标
promql
# MetricCache 关键监控指标
# 缓存大小
koordlet_metric_cache_size_bytes{level="L1"}
koordlet_metric_cache_size_bytes{level="L2"}
koordlet_metric_cache_size_bytes{level="L3"}
# 查询延迟
koordlet_metric_cache_query_latency_milliseconds{level="L1"}
koordlet_metric_cache_query_latency_milliseconds{level="L3"}
# 数据转移
koordlet_metric_cache_migration_points{from="L1", to="L2"}
# RocksDB 状态
koordlet_rockdb_compaction_duration_seconds
koordlet_rockdb_size_bytes
# 命中率
koordlet_metric_cache_hit_ratio # 应该 > 95%
MetricCache 与其他模块的协作
6.12 QOSManager 的使用模式
典型查询模式:
less
QOSManager 的工作循环(每秒执行一次):
步骤 1:获取最新值(L1,延迟 < 1ms)
current_metrics = cache.GetLatest(pod_uid)
步骤 2:分析短期趋势(L1,过去 5 分钟)
trend = cache.GetMetricAgg(
pod_uid, "cpu",
time.Now()-5m, time.Now(),
metric.Derivative // 导数 = 变化速率
)
步骤 3:与历史对比(L2,过去 1 小时相同时段)
historical = cache.GetMetricAgg(
pod_uid, "cpu",
time.Now()-1h-5m, time.Now()-1h,
metric.Percentile(95) // 95 分位
)
步骤 4:做出决策
if current_metrics.CPU > request * 0.9:
and trend > 0.5 CPU/min:
and current > historical * 1.3:
then: evict_pod(pod_uid)
性能目标:
├─ 步骤 1: < 1ms
├─ 步骤 2: < 5ms
├─ 步骤 3: < 10ms (L2 可能需要解压)
└─ 总计: < 20ms (每秒 1000 个 Pod 时可接受)
6.13 PredictServer 的使用模式
负载预测:
markdown
PredictServer 通过历史数据预测未来:
输入:
├─ 过去 24 小时的 CPU 时间序列
│ └─ 从 MetricCache.GetMetric() 获取
│
├─ 时间特征
│ ├─ 当前时刻(小时)
│ ├─ 星期几
│ └─ 是否工作日
│
└─ 集群事件(扩容/发布等)
算法:
1. 周期识别
└─ 识别 24h、7d、28d 的周期性
2. 趋势提取
└─ 使用 Goertz 变换提取趋势分量
3. 异常检测
└─ 去除离群值
4. 回归模型
└─ ARIMA 或 Prophet 进行预测
输出:
├─ 未来 1 小时的 CPU 预测(分钟级)
├─ 预测的置信区间
└─ 可信度评分
使用场景:
├─ 提前 10 分钟预测,主动驱逐 BE Pod
├─ 资源预留时,参考 p99 预测值
└─ 自动扩容时,提前准备容量
查询调用:
future_cpu = predictor.Predict(
pod_uid, "cpu",
horizon=10min, // 预测未来 10 分钟
confidence=95
)
// future_cpu = 3.2 ± 0.3 CPU (95% 置信度)
总 - 生产调优指南
6.14 常见问题与解决方案
问题 1:MetricCache 导致 Koordlet 启动变慢
markdown
症状:启动 Koordlet 需要 2-3 分钟
原因:
└─ RocksDB 打开、读取索引、加载热数据
解决:
1. 异步加载
└─ L1/L2 在后台异步加载
└─ 启动时先用空缓存,不阻塞
2. 跳过冷数据
└─ 只在第一次查询冷数据时加载 L3
└─ 不要启动时全加载
3. 预热优化
└─ 只预热最近 6 小时的数据
└─ 不要预热全部 7 天数据
预期效果:
└─ 启动时间从 2-3 分钟减少到 10-20 秒
问题 2:查询性能波动很大(有时 5ms,有时 100ms)
markdown
原因:
└─ RocksDB 的 Compaction 正在进行
└─ 磁盘 I/O 阻塞查询
解决:
1. 使用后台 Compaction
└─ 不要同步等待,异步进行
2. 限制 Compaction 的 I/O
└─ rate_limiter = 100MB/s
└─ 不要让 Compaction 阻塞用户查询
3. 时间隔离
└─ 在业务低谷进行 Compaction
└─ 例:凌晨 00:00-02:00
4. 使用 SSD
└─ HDD 导致查询慢
└─ SSD 可以将 RocksDB 查询从 50ms 降到 5ms
问题 3:MetricCache 占用的磁盘空间太大(> 100 GB)
markdown
原因:
├─ RocksDB 存储的冷数据过多
├─ Compaction 没有真正删除数据
└─ 压缩算法设置不当
解决:
1. 减少保留期限
└─ 改为 3 天而不是 7 天
└─ 大多数分析用不到 7 天前的数据
2. 强制 Compaction
└─ 手动触发 RocksDB 的全量 Compaction
└─ 可以回收 20-30% 的磁盘空间
3. 选择更好的压缩算法
└─ 改为 ZSTD(代替 LZ4)
└─ 压缩率从 10x 改为 20x
└─ 代价:CPU 多消耗 10%
4. 分片存储
└─ 不同类型的 Pod 数据分别存储
└─ 可以选择性删除 BE Pod 的冷数据
6.15 最佳实践
✅ 为不同的查询范围选择合适的数据层(L1/L2/L3) ✅ 定期检查 RocksDB 的磁盘占用,及时 Compaction ✅ 在业务低谷进行 Compaction,避免影响实时决策 ✅ 使用分位数而不仅看平均值,掌握数据的真实分布 ✅ 监控查询延迟,保持在 < 10ms
总结 - 章节要点汇总
6.16 关键概念速查
| 概念 | 含义 | 保留期 | 查询延迟 |
|---|---|---|---|
| L1 | Ring Buffer,秒级 | 1h | < 1ms |
| L2 | Gorilla 压缩,分钟级 | 24h | < 5ms |
| L3 | RocksDB,小时级 | 7d | 10-50ms |
| Gorilla | 时间序列压缩算法 | - | 编解 < 100ns |
| TTL 淘汰 | 自动删除过期数据 | 可配置 | - |
6.17 三层存储的选择指南
scss
查询需求 推荐数据层
─────────────────────────────────────────────
实时决策(最新值) L1 (热)
短期趋势(最近 1 小时) L1 (热)
日常分析(最近 24 小时) L2 (温)
历史对标(过去 1 周) L3 (冷)
长期审计(过去 1 个月) 归档存储
本章核心收获:
- 理解 TSDB 的三层架构设计和每层的应用场景
- 掌握 Ring Buffer、Gorilla 压缩和 RocksDB 的工作原理
- 学会如何高效查询历史数据支持决策
- 理解缓存淘汰和存储优化的实践方法
- 学会监控和优化 MetricCache 的性能