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已注册序列数。 - Rollup:
RollupAsync(..., 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/Last;Count聚合不读样本值。 - 值编码契约 = 8B double LE(
Count除外)——非该编码的源 =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 |
时钟供给源(故障注入面) |
反模式
PageSize照抄 Ring 缺省 16GB ——mem 卷把页跨度物化成真实内存,OOM 实锤;时间序列请用产品缺省 1MB。Indexed = false当性能旋钮——它是显式降档契约:查询 API 不可用、恢复全扫,不是"更快"。- 假设 Append 即落盘——可见性与持久化分离;崩溃窗口内的数据靠显式
FlushAsync/ 策略化收口。 - 单序列模式(缺省)下用三参 API 写非零序列——单序列实例仅 seriesId 0 合法,非零序列
fail-fast;多序列请启用
DenseSeries。 - dense 实例上跨序列假设共享回收边界——逐序列水位独立;序列 A 的 trim 不放宽序列 B 的 写入守卫(Ring 物理截断由慢序列钉住,自动收口)。
- 假设水位可回退 / 同刻写覆盖——均不成立(水位单调;同刻多样本写入序共存)。
想深入
- 同族产品:TierKv、TierQueue——组合积木语义可参照。
- raft 组合档:Products.Net raft-node。