Table of Contents

TierTimeSeries —— 时间序列产品

Ring × BTree 组合的时间序列产品:一次一序列、乱序样本吸收、逻辑地址一等公民。 组合配方 = 数据 Ring(追加 + 地址直达)× 时间索引 BTree(时间序检索)× VersionedMetadata 水位 (恢复对账锚点)——产品层只做装配与语义收口,引擎与结构零重写。

快速上手

using TC.Tier.Products.TimeSeries;

var fs = TierFs.New("memory:");

await using var series = await new TierTimeSeriesBuilder(fs, new TimeSeriesOptions
{
    SeriesName = "app.metrics",
})
.StartAsync();

var ct = CancellationToken.None;

// 写样本(timestamp = UTC Ticks;value = 自定义字节负载)
var now = DateTimeOffset.UtcNow.UtcTicks;
var append = await series.AppendAsync(now, BitConverter.GetBytes(42.5d), ct);

// append.Address = 样本的逻辑地址(一等公民)——上层自缓冲地址可经组合积木(Ring 读面)
// 直达取值,免时间索引;产品查询统一走时间索引面(见下)。

// 范围查询:[from, to) 索引序交付(时间戳 / 值 / 地址)
var from = now - TimeSpan.FromMinutes(5).Ticks;
await foreach (var (ts, data, address) in series.RangeAsync(from, now + 1, ct))
{
    Console.WriteLine($"{new DateTimeOffset(ts).LocalDateTime:O} = {BitConverter.ToDouble(data.Span)}");
}

// 显式落盘(返回即内存可见、持久化 = 显式 Flush)
await series.FlushAsync(ct);

写读循环的全部入口都是异步 API——没有同步阻塞形态。

装配怎么选

TierTimeSeriesBuilder(fs, options) 缺省装配开箱即用;With* 注入 = 整机替换语义(替换件而非补丁):

注入 替换件 何时需要
WithRingFactory 数据 Ring 介质/几何调优、测试替身
WithWatermarkMetaFactory 水位 VersionedMetadata meta 策略定制
WithIndexFactory / WithIndexComparer 时间索引 BTree / TimeKey 比较器 索引几何调优
WithStorageOptionsFactory StorageEngineOptions 段几何/缓存调优
WithMetaPolicyFactory Ring meta 策略 meta 持久化定制
WithEpoch / WithLogger LightEpoch / 日志 组合进既有宿主

Indexed = false 是显式降档契约:纯追加形态,查询 API(RangeAsync/LatestAsync/FloorAsync) 不可用,恢复 = 全扫重建。只有在"只写不查 + 重放代价可接受"时选择。

写入样本

var append = await series.AppendAsync(timestamp, value, ct);
  • 返回即内存可见,持久化 = 显式 FlushAsync(trim 前置自动 flush)——可见性与持久化分离。
  • 返回 SampleAppendResult.Address(逻辑地址)——地址直达取值路径,免索引查找。
  • 乱序吸收:交付序 ≠ 地址序;同刻多样本按写入序共存(不覆盖)。
  • 守卫 fail-fast 两态:ts ≤ TrimmedUntil(已回收区间)与超出 MaxOutOfOrderPast 乱序下界的样本直接拒绝。
  • AppendBatchAsync 批量追加,返回地址序列连续。

查询

API 语义
RangeAsync(from, to) [from, to) 索引序交付,元组 = (ts, value, address)
LatestAsync() 最新样本点查,O(log n);空序列 = null
FloorAsync(ts) ≤ ts 的最近样本点查;空序列 = null
GetStatsAsync() FirstTimestamp/LastTimestamp/SampleCount/IndexEntryCount/TrimmedUntilTimestamp/HeadAddress/TailAddress/DurableTail

稠密多序列(DenseSeries)

一个实例承载成千上万命名序列(网关每路由指标量级)——DenseSeries = true 开启:

var series = await new TierTimeSeriesBuilder(fs, new TimeSeriesOptions
{
    SeriesName = "app.metrics",
    DenseSeries = true,                 // 稠密多序列模式
    SeriesCapacity = 1 << 20,           // 序列数护栏(缺省 2²⁰,超限 fail-fast)
}).StartAsync(ct: ct);

// 三参 API = 命名序列(首写惰性注册,零预注册成本)
await series.AppendAsync(routeId, timestamp, value, ct);
await foreach (var (ts, v, addr) in series.RangeAsync(routeId, from, to, ct)) { }
var latest = await series.LatestAsync(routeId, ct);
var stats  = await series.GetStatsAsync(routeId, ct);
await series.TruncateAsync(routeId, beforeTimestampExclusive: cutoff, ct);

// 两参 API = 默认序列(seriesId 0)——与命名序列同实例共存,零迁移
await series.AppendAsync(timestamp, value, ct);
  • 共享面单实例:Ring 页池/索引树/水位引擎文件各一份(引擎文件数不随序列数涨); 每序列成本 = O(1) 侧账(~64B,序列注册表)——N 序列 ≠ N 倍
  • 键模型DenseTimeKey(SeriesId 4B 领先 + Timestamp + Tiebreaker,20B)——单棵 BTree 内 逐序列键前缀域,域内 seek/迭代出域即停;envelope(TTS2)头携 SeriesId,恢复重放按其路由重建。
  • 逐序列治理TruncateAsync(sid, before) 键域前缀截断;Ring 物理截断下限 = 全体序列钉住 地址的最小值(慢序列钉住——快序列回收不越雷池);retention TTL 轮逐序列推进。
  • 观测GetStatsAsync(sid) 逐序列口径;两参版 = 全实例聚合;SeriesCount 已注册序列数。
  • RollupRollupAsync(..., seriesId: s) 逐序列算子;sourceSeriesId/targetSeriesId 双参形式支持跨序列落点。
  • 退出:关闭 DenseSeries 回到单序列模式——TimeKey(16B)路径行为不变,两模式互不读对方文件。

Rollup 降采样

var windows = await TimeSeriesRollup.RollupAsync(
    rawSeries, downsampledSeries,
    windowTicks: TimeSpan.FromMinutes(1).Ticks,
    agg: TimeSeriesRollup.Aggregation.Avg,
    fromTs: from, toTs: to);
  • 组合糖定位RollupAsync 是原子算子,连续调度归业务后台任务(对齐 InfluxDB 连续查询 / Timescale 连续聚合的"只取算子面"形态)。
  • 七算子Min / Max / Sum / Count / Avg / First / LastCount 聚合不读样本值。
  • 值编码契约 = 8B double LECount 除外)——非该编码的源 = InvalidOperationException
  • 窗口按 epoch 对齐;空窗不落(只有含样本的窗才写)。
  • 级联降档:目标序列可再作源(组合律成立;Count 除外——计数再聚合语义不同)。

retention 与治理

  • RetentionTime / MaxBytes 后台轮(RetentionScanInterval,缺省 1 分钟): TTL 与字节上限取更晚锚回收;水位单调不回退;空闲轮次零写入。
  • TruncateAsync(beforeTimestampExclusive) 显式回收,返回回收样本数。

持久化与恢复

文件布局(按序列名):{SeriesName}.ts.ring(数据)/ .ts.index(时间索引)/ .ts.water(水位)。

恢复对账三步:悬空索引清理 → 缺条重建 → 水位 min 校正——索引/水位全自恢复(TimeSeriesRecoveryHints 为预留空形态)。Indexed = false 降档恢复 = 全扫重建。

配置参考(TimeSeriesOptions)

字段 缺省 语义
SeriesName "tc.series" 序列名(文件布局前缀)
SegmentGrowthLimit 64MB 数据 Ring 段增长上限
PageSize 1MB Ring 页大小(2 的幂 [4KB, 1GB])——样本粒度小,小页 = 更细的环绕/驱逐粒度
MemorySize 64MB Ring 内存容量——勿照抄 Ring 16GB 缺省(mem 卷会把跨度物化成真实内存)
OverflowPolicy / MinOverflowSize Disabled / 0 超大值分离到溢出引擎
ColdReadRatio 0.25 冷读回源占页池比例
MaxOutOfOrderPast null 乱序下界保护(null = 不限)
Indexed true 时间索引开关(false = 显式降档,查询 API 不可用)
IndexNodeSize 256 BTree 节点大小(字节)
IndexPersistencePolicy null 锚点帧持久化策略(后台 dump 间隔/增量阈值)
RetentionTime 7 天 TTL 回收
MaxBytes null 字节上限回收(与 TTL 取更晚锚)
RetentionScanInterval 1 分钟 后台回收扫描周期
DenseSeries false 稠密多序列模式(DenseTimeKey 20B + TTS2 + 单树键域化)
SeriesCapacity 2²⁰ 序列数护栏(dense 专用;超限 fail-fast)
Clock TimeProvider.System 时钟供给源(故障注入面)

反模式

  1. PageSize 照抄 Ring 缺省 16GB ——mem 卷把页跨度物化成真实内存,OOM 实锤;时间序列请用产品缺省 1MB。
  2. Indexed = false 当性能旋钮——它是显式降档契约:查询 API 不可用、恢复全扫,不是"更快"。
  3. 假设 Append 即落盘——可见性与持久化分离;崩溃窗口内的数据靠显式 FlushAsync / 策略化收口。
  4. 单序列模式(缺省)下用三参 API 写非零序列——单序列实例仅 seriesId 0 合法,非零序列 fail-fast;多序列请启用 DenseSeries
  5. dense 实例上跨序列假设共享回收边界——逐序列水位独立;序列 A 的 trim 不放宽序列 B 的 写入守卫(Ring 物理截断由慢序列钉住,自动收口)。
  6. 假设水位可回退 / 同刻写覆盖——均不成立(水位单调;同刻多样本写入序共存)。

想深入