NanoTDB – Golang 追加型时序数据库

Hacker News Top 工具

摘要

NanoTDB 是一款使用 Go 语言编写的小型嵌入式追加型时序数据库,适用于资源受限的主机,无运行时依赖。它采用 WAL 和分区数据文件,支持行协议数据摄入,并提供高效的时间范围查询。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/15 12:31

aymanhs/nanotdb 来源: https://github.com/aymanhs/nanotdb

NanoTDB

一个用于资源受限主机(树莓派、边缘节点、IoT网关)的小型嵌入式时间序列数据库。运行时无外部依赖。所有数据都存储在单个根目录下的纯文本文件中。


架构概览

Engine
├── "prod" 数据库 → WAL (prod.wal) + 目录 (catalog.json) + 分区 .dat 文件
├── "sensors" 数据库 → WAL + 目录 + 分区 .dat 文件
└── "internal" → 引擎自身指标(相同结构,从不暴露给用户)

Engine 是唯一的入口点。它拥有一组命名的数据库,并根据行协议前缀将摄入的样本路由到正确的数据库。每个 Database 包含三个存储层:

文件用途
WAL.wal崩溃安全:在样本进入页面之前记录每个样本
目录catalog.json将指标名称映射到紧凑的 MetricID 和值类型
数据文件data-.dat从内存中刷出的不可变压缩页面

数据流

摄入 (AddLine)

AddLine("prod/room.temp 21.5 1715000000000000000")
│
├─ 解析行协议 → dbName="prod" metric="room.temp" ts=... value=21.5
├─ getOrCreateDB → 打开或复用 prod 数据库
├─ WAL 追加 → 将紧凑记录写入 prod.wal(崩溃安全)
├─ addToOpenDay → 追加到当天的内存页面桶中
└─ 如果页面已满 → 压缩页面并将页面帧写入 data-.dat,重置 WAL(不再需要回放)

每个指标在整个写入流中,时间戳必须单调非递减。拒绝乱序或过时的样本。

回放(引擎打开时)

当数据库被打开时,如果数据文件落后,则 WAL 会被回放到内存页面中。目录用于解析那些省略了值类型的指标(紧凑格式优化)。完全回放后,引擎即可接受新的写入。

查询 (QueryRange)

QueryRange("prod", "room.temp", fromTS, toTS, stride, callback)
│
├─ 遍历 [fromTS, toTS] 内的 UTC 天
│  ├─ 打开 data-.dat → 扫描页面帧头部
│  │   跳过时间窗口外的帧(不解压)
│  │   解压并扫描匹配的帧
│  └─ 检查当天的内存页面数据
└─ 对每个样本调用回调(如果 stride > 1,则每第 N 个样本)

行协议

DB/metric.name value [ts]
  • DB — 数据库名称(首次写入时自动创建)
  • metric.name — 任意指标标识符(斜杠分隔的命名空间效果很好)
  • value — 整数(42-7)或浮点数(3.141e-3)。整数字面量始终创建 int32 指标;浮点数字面量创建 float32 指标。类型在首次写入时固定;同一指标混合类型会报错。
  • ts — Unix 纳秒时间戳(可选;默认为 time.Now()

示例:

prod/room.temp 21.5 1715000000000000000
sensors/pressure.hpa 1013
internal/batch.size 256i

i 后缀强制将看起来像浮点数的值解释为整数。


WAL 格式(紧凑 v2)

每个记录由一个 uvarint 长度前缀 后跟一个固定布局的有效载荷组成:

[uvarint: payload_len] [payload]

有效载荷布局:

偏移  大小  字段
0     2    MetricID uint16 小端
2     3    TS 增量 uint24 小端 相对于基线的纳秒数
5     1    CompactTL 标志  位7 = 新基线,位6 = 新指标
6     8    基线 TS int64 小端(仅当位7设置时)
—     variable  名称长度+名称+值类型(仅当位6设置时)
—     4    值 int32 或 float32 小端,始终存在
  • 热点路径(已知指标,相同基线):2+3+1+4 = 10 字节 + 1 个 varint = 11 字节
  • 新基线在每个 WAL 的第一条记录以及时间戳间隔超过约 16.7 毫秒 (224 ns) 时发出。典型传感器流在基线重置之间可以容纳几百秒。
  • 已知指标(会话中先前见过的)省略了名称和值类型;这些字段在回放期间从目录中恢复。

磁盘布局

/
├── engine.toml — 引擎配置(首次启动时自动创建)
├── catalog.json — 指标注册表:名称 → id + 类型
├── manifest.toml — 每数据库设置(保留、WAL、页面限制)
├── .wal — 预写日志(单一可复用文件)
└── data-.dat — 已完成分区的压缩页面帧

数据文件是仅追加的页面帧序列:

帧 = PageHeader(18 字节) + compressed_len(uvarint) + S2 压缩有效载荷 + CRC32(4 字节)

有效载荷是交错排列的 (MetricID, 时间戳, 值) 三元组的扁平数组,按时间戳排序。S2 压缩通常在真实传感器数据上达到 3-4 倍的压缩率。


配置 (engine.toml)

首次启动时自动创建于 /engine.toml。主要设置:

默认值效果
engine.listen:8428HTTP 服务器地址
wal.max_segment_size67108864 (64 MiB)页面刷新后 WAL 重置前的大小
wal.fsync_policysegmentsegment = 在 WAL 重置时 fsync;always = 每次追加都 fsync
durability.profilestrictstrict / balanced / throughput(见下方)
stats.enabledtrue将引擎自身指标发送到 internal 数据库
stats.interval30s统计信息刷新的频率

持久性配置:

配置页面文件 fsync目录 fsync
strict
balanced
throughput

每个数据库的设置(保留、分区、WAL 跳过窗口、页面刷新阈值、汇总)位于 /manifest.toml 中,默认值可以在 engine.toml[manifest_defaults] 下设置。[retention] 中的分区选项:

  • partition = "day"(默认):data-YYYY-MM-DD.dat
  • partition = "month"data-YYYY-MM.dat
  • partition = "year"data-YYYY.dat
  • partition = "forever"data-forever.dat

汇总 (manifest.toml)

汇总任务在数据库清单的 [rollups] 下定义。示例:

[rollups]
enabled = true
checkpoint_file = "rollup.checkpoints.log"
default_grace = "5m"

[[rollups.jobs]]
id = "outside_temp_1h"
source_metric = "temp.out_dry"
interval = "1h"
aggregates = ["min", "max", "sum", "avg", "count"]
destination_db = "sensors_rollup_1h"
destination_metric_prefix = "temp.out_dry"

汇总配置参考:

字段作用域必需有效值 / 默认值说明
rollups.enabled数据库`truefalse(默认 false`)
rollups.checkpoint_file数据库字符串(默认 rollup.checkpoints.log检查点日志路径,相对于源数据库目录。
rollups.default_grace数据库Go 持续时间或空当任务省略 grace 时使用。
rollups.jobs[].id任务非空字符串每个源数据库唯一,用于检查点跟踪。
rollups.jobs[].source_metric任务非空字符串从源数据库读取的指标。
rollups.jobs[].interval任务有效的 Go 持续时间(>0汇总桶大小(例如 1h24h)。
rollups.jobs[].aggregates任务`minmax
rollups.jobs[].destination_db任务非空字符串接收汇总样本的目标数据库。
rollups.jobs[].destination_metric_prefix任务字符串(默认 source_metric输出名称格式为 <prefix>.<aggregate>
rollups.jobs[].grace任务Go 持续时间或空覆盖此任务的 default_grace

说明:

  • 检查点存储在源数据库(默认 rollup.checkpoints.log)中。
  • 目标数据库也可以定义自己的汇总任务以创建级联(例如 1h -> 1d)。
  • 对于低频率的汇总输出,请在目标数据库上使用较粗的分区(例如 monthyear),以避免产生大量微小的日期文件。

二进制文件

nanotdb — 服务器

nanotdb --config <路径> start   # 使用给定的 engine.toml 启动服务器
nanotdb --init --config <路径>  # 写入默认 engine.toml 并退出

暴露一个与 VictoriaMetrics 即时/范围查询有线格式兼容的小型 HTTP API(/api/v1/query/api/v1/query_range/api/v1/import/prometheus)。

nanocli — 离线 CLI 工具

直接对数据目录进行操作,无需运行服务器。

nanocli inspect db --root <路径> [--db <名称>] [--json] — 所有或单个数据库概览
nanocli inspect dat --root <路径> --db <名称> [--json] — .dat 文件中的页面帧头部
nanocli inspect wal --root <路径> --db <名称> [--json] — WAL 记录转储
nanocli import --root <路径> --in <文件> [--json] — 批量导入行协议文件
nanocli export --root <路径> --db <名称> [--out <文件>] — 导出数据库到行协议(省略 --out 时输出到标准输出)
nanocli query --root <路径> --db <名称> --metric <名称> [--start <时间>] [--end <时间>] [--format table|json]

LP 时间戳(导入和导出文件)接受 / 使用:YYYY-MM-DD HH:MM:SS.nnnnnnnnn(UTC),导入时也接受原始 Unix 纳秒。--start / --end 接受 RFC3339 字符串、YYYY-MM-DD [HH[:MM[:SS[.nnnnnnnnn]]]] 或 Unix 时间戳(秒或纳秒)。

汇总全周期检查脚本

对于确定性端到端验证(生成 LP -> 导入 -> 汇总 -> 导出 -> 比较预期),运行:

./scripts/rollup_full_cycle_check.sh

可选参数:

  • ./scripts/rollup_full_cycle_check.sh <root-dir> <duration-hours> <metrics> <cadence-seconds> <gap-metrics>
  • 默认值:root-dir=test-data/full-cycle-checkduration-hours=30metrics=10cadence-seconds=10gap-metrics=2

生成的工件放置在 <root-dir>/work 中,便于发现:

  • scenario_summary.json(持续时间、速率、计数、每个指标统计)
  • known_gaps.csvtemp.gap_probeXX 指标的确定性缺失窗口)
  • SCENARIO.md(快速人类可读摘要)

引擎 API(嵌入)

e, err := engine.OpenEngine("/data", 0) // 0 = 默认 WAL 段大小
defer e.Close()

// 摄入
err = e.AddLine("sensors/temp 22.1 " + strconv.FormatInt(time.Now().UnixNano(), 10))

// 范围查询
err = e.QueryRange("sensors", "temp", fromTS, toTS, 1, func(s engine.Sample) error {
    fmt.Println(s.TS, s.Float32)
    return nil
})

// 最后一个值(来自内存目录缓存)
sample, ok, err := e.QueryLast("sensors", "temp")

// 批量导入 / 导出
err = e.ImportFile("backup.lp")
err = e.ExportFile("sensors", "backup.lp")

主要类型:

类型描述
Engine顶层协调器;并发安全
Database一个包含 WAL + 目录 + 数据文件的命名数据库
Catalog指标名称 ↔ ID 注册表;以 JSON 持久化
Page交错样本的内存缓冲区;满时刷新
WAL使用紧凑 v2 编码的单一文件预写日志
Sample查询解码后的数据点
Timestampint64 Unix 纳秒
MetricID每数据库 uint16 指标地址

相似文章

TabPFN-3:技术报告

arXiv cs.LG

TabPFN-3 是一个新的表格数据基础模型,在合成数据上预训练,可扩展到 100 万训练行,同时减少训练和推理时间,在表格预测、时间序列和关系数据上实现了最先进的性能。

datasette 1.0a28

Simon Willison's Blog

Datasette 1.0a28 alpha 版本修复了前一个 alpha 版本中发现的兼容性错误和资源管理问题,包括修复 execute_write_fn() 回调、数据库清理方法,以及新增用于测试中自动清理的 pytest 插件。