NanoTDB – Golang 追加型时序数据库
摘要
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.14、1e-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 | :8428 | HTTP 服务器地址 |
wal.max_segment_size | 67108864 (64 MiB) | 页面刷新后 WAL 重置前的大小 |
wal.fsync_policy | segment | segment = 在 WAL 重置时 fsync;always = 每次追加都 fsync |
durability.profile | strict | strict / balanced / throughput(见下方) |
stats.enabled | true | 将引擎自身指标发送到 internal 数据库 |
stats.interval | 30s | 统计信息刷新的频率 |
持久性配置:
| 配置 | 页面文件 fsync | 目录 fsync |
|---|---|---|
strict | 是 | 是 |
balanced | 是 | 否 |
throughput | 否 | 否 |
每个数据库的设置(保留、分区、WAL 跳过窗口、页面刷新阈值、汇总)位于 /manifest.toml 中,默认值可以在 engine.toml 的 [manifest_defaults] 下设置。[retention] 中的分区选项:
partition = "day"(默认):data-YYYY-MM-DD.datpartition = "month":data-YYYY-MM.datpartition = "year":data-YYYY.datpartition = "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 | 数据库 | 否 | `true | false(默认 false`) |
rollups.checkpoint_file | 数据库 | 否 | 字符串(默认 rollup.checkpoints.log) | 检查点日志路径,相对于源数据库目录。 |
rollups.default_grace | 数据库 | 否 | Go 持续时间或空 | 当任务省略 grace 时使用。 |
rollups.jobs[].id | 任务 | 是 | 非空字符串 | 每个源数据库唯一,用于检查点跟踪。 |
rollups.jobs[].source_metric | 任务 | 是 | 非空字符串 | 从源数据库读取的指标。 |
rollups.jobs[].interval | 任务 | 是 | 有效的 Go 持续时间(>0) | 汇总桶大小(例如 1h、24h)。 |
rollups.jobs[].aggregates | 任务 | 否 | `min | max |
rollups.jobs[].destination_db | 任务 | 是 | 非空字符串 | 接收汇总样本的目标数据库。 |
rollups.jobs[].destination_metric_prefix | 任务 | 否 | 字符串(默认 source_metric) | 输出名称格式为 <prefix>.<aggregate>。 |
rollups.jobs[].grace | 任务 | 否 | Go 持续时间或空 | 覆盖此任务的 default_grace。 |
说明:
- 检查点存储在源数据库(默认
rollup.checkpoints.log)中。 - 目标数据库也可以定义自己的汇总任务以创建级联(例如
1h -> 1d)。 - 对于低频率的汇总输出,请在目标数据库上使用较粗的分区(例如
month或year),以避免产生大量微小的日期文件。
二进制文件
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-check、duration-hours=30、metrics=10、cadence-seconds=10、gap-metrics=2
生成的工件放置在 <root-dir>/work 中,便于发现:
scenario_summary.json(持续时间、速率、计数、每个指标统计)known_gaps.csv(temp.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 | 查询解码后的数据点 |
Timestamp | int64 Unix 纳秒 |
MetricID | 每数据库 uint16 指标地址 |
相似文章
Show HN: GETadb.com – 每个GET请求创建一个数据库
GETadb.com 提供一个即时后端,包含关系型数据库、同步引擎和认证,通过简单的GET请求即可访问,无需注册,允许像Claude或Codex这样的AI智能体无缝构建全栈应用。
TabPFN-3:技术报告
TabPFN-3 是一个新的表格数据基础模型,在合成数据上预训练,可扩展到 100 万训练行,同时减少训练和推理时间,在表格预测、时间序列和关系数据上实现了最先进的性能。
用 10 MB 的 FST(有限状态转换器)二进制文件替换 3 GB 的 SQLite 数据库
作者描述了将 3 GB 的 SQLite 数据库替换为 10 MB 的有限状态转换器(FST)二进制文件,以优化芬兰语-英语词典工具,在保持性能的同时将内存使用量减少了 300 倍。
datasette 1.0a28
Datasette 1.0a28 alpha 版本修复了前一个 alpha 版本中发现的兼容性错误和资源管理问题,包括修复 execute_write_fn() 回调、数据库清理方法,以及新增用于测试中自动清理的 pytest 插件。
时衰减 Shapley:一种面向时间序列数据的时间感知数据估值框架
本文提出了时衰减 Shapley(TDS),这是一种面向时间序列数据的数据估值框架,通过引入时衰减机制和多尺度融合策略,解决了样本值随时间变化的特性,在噪声检测和数据选择方面优于传统方法。