- 数据库
- 版本控制
- 后端
【免费下载链接】noms
The versioned, forkable, syncable database
Noms 是一个以 Git 版本控制思想为哲学渊源的去中心化数据库,将版本化、可同步、可分叉等特性与结构化数据存储、高效索引、原子事务等传统数据库能力融为一体。本文以仓库 README.md 为核心脉络,结合 技术总览、CLI 指南、NBS 存储层说明 与对应源码,系统讲解 Noms 的 Merkle DAG 数据模型、类型系统、Prolly Trees 索引结构、安装运行流程与 CLI 操作,帮助你快速上手并理解其底层原理。
Noms 是什么
Noms是一个"哲学上源自 Git 版本控制系统的去中心化数据库"。与 Git 一样,Noms 具备两大核心特性:
- 版本化(Versioned):默认保留数据库的所有历史版本。你可以轻松追踪数据库如何演化到当前状态,高效比较任意两个版本,甚至从任意历史版本回滚或分叉。
- 可同步(Synchronizable):同一个 Noms 数据库的多个实例可以长时间断连,之后能够高效且正确地调和彼此的变化。
与 Git 不同,Noms 是一个数据库,因此它还具备以下能力:
- 主要存储结构化数据(而非文件和目录),依托 Noms 类型系统;
- 大规模扩展性好,可支撑大量数据与并发客户端;
- 支持原子事务(单实例 Noms 是 CP 的;生产环境通常以 S3 为后端运行,此时表现为"effectively CA");
- 支持高效索引,依赖 Prolly Trees 概率型 B 树;
- 提供灵活的查询模型(实验性的 GraphQL 桥接层 ngql)。
一个 Noms 数据库既可以存在于文件系统中,也可以存在于云端:内置的 NBSChunkStore实现提供了两个后端——文件系统后端与 S3 bucket 后端,为 Noms 数据库提供持久化。
最后,由于 Noms 是内容寻址(content-addressed)的,它带来了一种非常愉悦的编程模型:使用 Noms 是"声明式"(declarative)的。你不需要INSERT新数据、UPDATE既有数据或DELETE旧数据,只需"声明"数据此刻应当是什么样子。如果两次提交相同的数据,内容寻址会让它被自动去重;如果提交的数据几乎相同,则只有不同的部分会被写入。这一特性从 value_store.go 等底层实现中可以观察:所有值均以内容哈希寻址存储,相同哈希意味着相同值。
核心概念:Merkle DAG、数据库与数据集
数据是一棵 Merkle DAG
与 Git、比特币、以太坊、IPFS 等系统一样,Noms 将数据建模为有向无环图(DAG):每个节点都有一个由节点内编码的值(以及传递性地由该节点可达的所有节点中的值)推导出的hash。换言之,一个 Noms 数据库就是一棵巨大的 Merkle DAG。
- 两个节点拥有相同 hash 时,它们代表完全相同的逻辑值,各自可达的子图在拓扑上等价;
- 反过来也成立:一个逻辑值有且只有一个 hash,hash 不同即逻辑值不同。
正是这种性质使得快速 diff、sync、merge 成为可能——比较两个值只需要比较它们的 hash。
当前 Noms 使用 sha2-512 哈希的前 20 个字节 作为内容哈希。为何这样选择?hash 包注释 给出了明确理由:sha-1 已不再被推荐;sha-3 太新、平台支持不足;blake 不常用;而在 sha-2 家族中 sha-512 在 64 位平台上比 sha-256 更快。截断到 20 字节则是碰撞抵抗力与树扇出(fan-out)之间的平衡——数据库场景下更大的 hash 意味着每个 chunk 中数据更少、树层级更深、迭代与搜索更慢;而 20 字节正好对应 32 个 base32 字符(StringLen = 32,即20 * 8 / log2(32))。哈希的文本序列化使用大端 base32、字母表为{0-9,a-v},不含特殊字符,可在 GUI 中双击选中,且排序后的哈希文本序一致,便于人工扫描。值得注意:哈希函数是序列化版本的一部分,在整个数据库的生命周期内恒定不变,因此客户端无需担心同一数据库内出现多种哈希函数。
数据库(Database)与数据集(Dataset)
数据库是 Noms 的顶层抽象,承担两项职责:一是为内容寻址的 chunk 数据提供存储;二是跟踪零个或多个数据集(dataset)。
Noms 数据库可以构建在任何提供键值存储、且至少具备可选乐观并发控制的底层存储之上——乐观并发只用于存储每个数据集的当前值,chunk 本身是不可变的。仓库中的实现包括:
- 自有的文件后端存储 Noms Block Store (NBS)(通常本地使用);
- 自有的 HTTP 协议(用于连接远程数据库);
- Amazon DynamoDB;
- 内存存储(主要用于测试)。
一个数据集不过是 DAG 中的一个具名指针。下面的命令把数据库内名为foo的数据集"复制"为bar:
noms sync http://localhost:8000::foo http://localhost:8000::bar这个操作几乎是零 IO 的:Noms 先在http://localhost:8000中解析数据集foo得到 hash,再检查该 hash 是否已存在于目标数据库(本例中与源数据库相同),发现存在后只需新增一个指向该 chunk 的数据集即可。若目标数据库已经拥有全部或大部分所需 chunk,跨数据库的同步也可以同样高效。
时间与不可变性
Noms 中所有数据都是不可变的,一旦存储就永不改变。为了表达状态变化,Noms 使用一系列Commit结构:与 Git 一样,提交通常有一个parent(时间上的前一提交),而在合并场景下可以有多个 parent。
当值被存储时,会被拆成一个或多个 chunk。chunk 边界通常是隐式创建的(用于高效存储大型集合,见下文 Prolly Trees);程序员也可以使用Ref类型显式创建 chunk 边界。每个 chunk 编码单个逻辑值,并在持久化层中由其所编码值的 hash 寻址。
类型系统
Noms 是类型化系统,每个 Noms 值都属于以下类型之一:
BooleanNumber(任意精度二进制)String(utf-8 编码)Blob(原始二进制数据)Set<T>List<T>Map<K,V>- Unions:
T|U|V|... Ref<T>(显式的行外引用)Struct(用户自定义记录类型,如Struct Person { name: String, age?: Number })Type(存储一个 Noms 类型的值)
Blob、set、list、map 可以非常巨大——Noms 会把它们内部chunk成合理大小的片段,以高效地存储、搜索与更新。String、number、union、struct 则不会被 chunk,应仅用于"大小合理"的值;需要强制某值放入不同 chunk 时使用Ref。
类型在 Noms 中承担多重职责(详见 intro.md):
- 数据自描述:对任意 Noms
Value调用types.TypeOf,无论多大,都能得到整个值及其可达值的精确描述,使不同软件无需事先约定即可互操作; - 社区内标准化:用户可自定义结构体并发布使用它们的数据,在相似数据的社区中形成临时标准;
- 结构性使用:程序可按要求类型检查传入数据——若传入的根 chunk 匹配该类型或其超集,程序即可确定所有可访问数据的形状,从而支持 schema 随时间扩展;
- 未来计划为数据集增加类型约束(类似于传统数据库的 schema 校验)。
Ref 与 Hash 的区别:hash 只是一串标识更大值的字节,Noms 中每个值都有 hash;而Ref是类型系统的一部分,是一个值——你可以把Ref<T>提交到数据集,但无法提交裸 hash。区别在于Ref除 hash 外还携带目标类型,这使得包含Ref的提交可以被高效校验。
Type Accretion(类型增生):作为不可变数据库,schema 如何演化?答案是类型增生:向只含Number的Set插入字符串后,结果值的类型是Set<Number|String>。数据集层面同理:提交Set<Number>时,产生的 commit 类型为:
Struct Commit { Value: Set<Number> Parents: Set<Ref<Cycle<Commit>>> }随后提交Set<String>时,该 commit 的类型会"增生"为同时描述当前与历史类型:
Struct Commit { Value: Set<String> Parents: Set<Ref<Cycle<Commit>> | Ref<Struct Commit { Value: Set<Number> Parents: Cycle<Commit> }>> } }类型增生的好处包括:可以在不重写任何既有数据的情况下拓宽容器类型(Set<Struct { name: String }>增宽后既有数据全部复用);可以做到其他数据库不允许的拓宽(如Set<Number>→Set<Number|String>);无论向哪个方向(变宽或变窄)改变数据集类型,数据集都能自描述其当前与历史类型。
Prolly Trees:概率型 B 树
为什么需要它
Noms 的关键不变量是历史无关性(history-independence):同一个 Noms 值,无论经历过怎样的逻辑变更序列,最终都由同一组物理 chunk、同样的 hash 表示。这是快速 diff、sync、merge 的基础——两个值只需比较 hash 即可判定相等。
但 Noms 同时也是数据库,需要高效地搜索、扫描、变更大型集合。经典的 B-Tree 与 LSM Tree 无法直接使用,因为它们的内部状态依赖于变更历史(不具备历史无关性)。为此 Noms 引入了Prolly Trees。
结构与构建
Prolly Tree 是一种搜索树,其中每个节点存储的值的数量由存储在树中的数据概率性地决定。它和 B-Tree 相似,但每个节点中的值数量是概率均值而非强制的上下界;每个节点中的值集合由对值做滚动哈希(rolling hash)的输出决定,而非通过超出上下界时的 split/join 操作。
构建 Prolly Tree 使用变体的内容切片(content-slicing)技术(类似 bup、rsync、Camlistore 的做法):将大型有序序列的序列化数据按固定大小窗口逐字节滑动,在每个位置计算窗口内字节的哈希;当哈希中出现具有已知出现概率的模式时,该位置即为boundary。窗口滑到所在项的末尾,写出上一个 boundary 到当前 boundary 之间的新chunk,并存入内容寻址存储。
Noms 中,寻找的模式是 12 个高位全为 1(defaultChunkPattern = 1<<12 - 1),其出现概率为 1/2^12,因此Noms 的平均 chunk 大小约为 4KB。窗口大小为 67 字节(注释说明:2 字节在随机数据下配合 4KB 目标已足够,更大窗口是为了在低熵输入上有更好的分布;素数选择则为重复输入提供更好的分布),任何单字节改变移动一个边界的概率约为 67/4KB ≈ 1.6%。切出第一轮 chunk 后,为每个 chunk 的内容构建索引,再对索引的序列化重复切片,如此递归直到得到一个不再切分的节点——即树的根。
变更与性质
变更一棵 Prolly Tree 时,概念上是从头构建一棵新树,但窗口之外的子树可以原样复用。约 1.6% 的概率一次写入会移动 chunk 边界,导致该层多写一个 chunk;这可能在每一层发生,因此变更一棵树的期望操作数约为1.016 * 树深。一棵 4 层的 Prolly Tree 可容纳约4096^4 ≈ 281TB数据,对它做单次变更只需约 4 次 4KB 写入。
Prolly Tree 与传统结构的对比(n:树叶数据总量,k:平均块大小,w:窗口宽度):
| 操作 | B-Trees | Patricia 树† / HAMTs | Prolly Trees |
|---|---|---|---|
| 1 次随机读 | O(logk(n)) | O(logk(n)) | O(logk(n)) |
| 1 次随机写 | O(logk(n)) | O(2·logk(n)) | O((1+k/w)·logk(n)) |
| 顺序扫描大小为 z 的单个项 | O(z/k) | 不支持 | O(z/k) |
| 计算大小为 d 的 diff | O(n) | O(d) | O(d) |
| 验证、证明 | 不支持 | 支持 | 支持 |
| 结构化共享 | 不支持 | 支持 | 支持 |
†假设已哈希键;未哈希会破坏性能。
由于 Prolly Trees 是有序的(Boolean、Number、String 键按自然序排序,其他键类型按 hash 排序),Noms 集合可以被当作高效索引使用,方式与传统数据库的主索引、二级索引相同。例如构建Map<Number, Set<Person>>即可快速(约 logk(n) 次 seek)查找精确年龄的人群,也能高效进行年龄区间扫描;对两个集合求交(如年龄与发色的交集)也可高效实现。
安装与运行
安装与版本验证
安装 Noms CLI 的方式是下载最新 release 并解压后加入$PATH。安装完成后验证:
$ noms version format version: 7.18 built from <developer build>这里的版本号来自 go/constants/version.go(NomsVersion = "7.18",NomsGitSHA = "<developer build>"),由 noms_version.go 子命令输出。
导入数据并浏览
以纽约市公开数据为例,先通过go install安装示例程序,再下载 CSV 并导入:
go install github.com/attic-labs/noms/samples/go/csv/csv-import curl 'https://data.cityofnewyork.us/api/views/kku6-nxdu/rows.csv?accessType=DOWNLOAD' > /tmp/data.csv csv-import /tmp/data.csv /tmp/noms::nycdemo注意第二个参数/tmp/noms::nycdemo是 Noms 的"拼写"(spelling)规范:<database>::<dataset>——数据库为本地路径/tmp/noms,数据集名为nycdemo。随后浏览:
noms show /tmp/noms::nycdemo输出会展示一个struct Commit,包含meta(日期、输入文件等元数据)、parents(set {},首提交无父)以及value(236 行的struct Row列表,每行含各人口统计字段)。
csv-import 是完整的示例工具,其 importer.go 展示了丰富的参数:--delimiter(分隔符,默认逗号)、--header(表头行)、--lowercase(列名转小写)、--name(行结构体名,默认Row)、--column-types(逗号分隔的每列类型,缺省全为 String)、--path(导入 Noms Blob 而非文件)、--dest-type(list或map:<pk>,pk 为用于唯一标识行的列头或 0 基索引列表)、--skip-records、--limit-records、--commit(默认 true,提交到数据集 head,否则只写入数据集)、--append(追加到数据集 head 的 list,仅兼容 list 导入)、--invert(按列主序而非行主序导入)等。
CLI 实战:数据集管理、日志、展示、同步与差异
不带参数运行noms会列出全部子命令(diff、ds、log、serve、show、sync、version等)。这些子命令在 cmd/noms/noms.go 中注册,包括nomsBlob、nomsCommit、nomsConfig、nomsDiff、nomsDs、nomsList、nomsLog、nomsMerge、nomsJSON、nomsMap、nomsRoot、nomsServe、nomsSet、nomsShow、nomsStats、nomsStruct、nomsSync、splore.Cmd、nomsVersion。使用noms help [command]可查看具体命令说明。
noms ds:列出数据库内的数据集。例如noms ds http://demo.noms.io会显示sf-film-locations/raw、sf-film-locations等。noms log:查看数据集的历史。输出每个 commit 的 hash、Parent、Date 等。注意 Noms 是类型化系统,这里显示的并非文本,而是两个数据集间 diff 的序列化。noms show:展示数据库中任意对象的完整序列化,例如noms show 'http://demo.noms.io::#aprsmg0j2eegk8eehbgj7cd3tmmd1be8'会输出struct Commit的类型定义与实际值(1,241 行的List<struct Row>等)。从 noms_show.go 可以看到它还支持--raw(二进制格式 dump)、--stats(值统计信息,二者互斥)、--tz(时间注释时区,默认 local)等选项,内部通过config.NewResolver()解析路径,再用types.WriteEncodedValue输出。noms sync:在数据库之间(或内部)移动数据集。与 Git 不同,Noms不区分 push 和 pull,两个方向是同一操作:
> noms sync http://demo.noms.io::sf-film-locations /tmp/noms::films > noms ds /tmp/noms films同步到本地后即可断连工作——例如导出 CSV、编辑、再重新导入:
> go install github.com/attic-labs/noms/samples/go/csv/... > csv-export /tmp/noms::films > /tmp/film-locations.csv # 编辑 /tmp/film-location.csv 后: > csv-import --column-types=String,String,String,String,String,String,String,String,Number,String,String \ /tmp/film-locations.csv /tmp/noms::filmscsv-export 的 exporter.go 会读取数据集 head 值,识别List或Map类型后写出 CSV(仅支持这两类根值,否则 panic 提示"Expected ListKind or MapKind")。
noms diff:展示任意两个值的差异,例如修改前后的对比:
> noms diff http://demo.noms.io::sf-film-locations /tmp/noms::films ./.meta { - "date": "2016-07-25T18:51:23+0000" + "date": "2016-07-25T22:51:14+0000" + "inputFile": "/tmp/film-locations.csv" - "inputPath": "http://demo.noms.io::sf-film-locations/raw.value" ./.parents { - pckdvpvr9br1fie6c3pjudrlthe7na18 + q4jcc2i7kntkjiipvjgpr5r02ldroj0g } ./.value[0] { - "Locations": "Epic Roasthouse (399 Embarcadero)" + "Locations": "Epic Roadhouse (399 Embarcadero)" }存储后端 NBS:文件系统与 AWS
NBS(Noms Block Store)是为 Noms 优化的水平可扩展存储层(详见 go/nbs/README.md),可运行在两种配置:本地磁盘后端或 Amazon AWS 后端(后者由 NBS-on-AWS.md 说明)。
- 本地磁盘后端在 Noms 的典型工作负载下显著快于 LevelDB,并支持完整的多进程并发。
- AWS 后端将数据主要存放在 S3,外加单个 DynamoDB 条目,使 Noms "effectively CA":Noms 始终一致,Noms+NBS 的可用性与 DynamoDB 和 S3 相当,同时获得 S3 的成本结构。
NBS 的关键设计:
- 为内容寻址的 DAG 提供存储(恰有一个根),每个节点编码为字节序列,由字节序列的 20 字节 hash 寻址;
- 没有
update或delete——只有insert、update root和garbage collect; - 插入任何新字节序列仅在更新根之后才持久化;
- 支持文件级多进程并发,多个写入者使用乐观锁;
- 写入者无需担心重写重复 chunk,NBS 会高效检测并丢弃(绝大多数)重复项。
仓库给出的本地后端与 AWS 后端使用示例:
# 本地 NBS: ./csv-import foo.csv /Users/bob/csv-store::data # AWS 后端(通过 aws: scheme): ./csv-import foo.csv aws:table/bucket/database::data路径拼写(Spelling)规范
Noms 众多命令与 API 都接受数据库、数据集或值的规格参数,规范见 doc/spelling.md:
- 数据库:
<protocol>[:<path>]。http(s)为 HTTP 远程数据库 URL;mem为内存数据库(path 必须为空);nbs为本地 NBS 数据库(path 为磁盘上的目录,如nbs:/tmp/noms-data;在 Go 中nbs:可省略);aws为直接由 AWS(DynamoDB + S3)支撑的远程 NBS,格式为aws:dynamo-table/s3-bucket/database。 - 数据集:
<database>::<dataset>,dataset 匹配正则^[a-zA-Z0-9\-_/]+$。例如/tmp/test-db::my-dataset、http://localhost:8000::registered-businesses。 - 值:
<database>::<root><path>。root以#开头时解释为 hash,否则作为数据集名。path相对 root:用.field选择 struct 字段(如.value取 Commit 的 value 字段、.meta取 meta 字段);用[...]索引 list/map/set(如.value[42]、.value[0]、list 支持负索引.value[-1]表示末尾);复杂键可用 hash 索引(.value[#hash]),用@key获取 map 的键而非值;用@at(index)按稳定序定位元素(list 中与[index]等价,set/map 中@at(0)恒为最小元素)。
注意 shell 转义:应使用单引号包裹整个参数,否则如noms show ...value["0000024-02-999"].Ownership_Name这类带双引号的路径会报错Invalid index。
实验性查询:ngql(GraphQL 桥接)
Noms 的查询语言尚不完整,团队曾尝试引入 GraphQL(即 ngql),但仍处于实验阶段。ngql 是 Noms 与 GraphQL 之间的实验性桥梁,提供 Noms 类型/值与 GraphQL 类型/值的相互转换 API,以及一些可用于实现 GraphQL 端点的函数。
类型转换规则要点(详见 go/ngql/README.md 与 go/ngql/types.go):
- 除原始类型(Bool、Number、String)外的几乎所有 Noms 值都以 GraphQL struct 表示;
TypeConverter提供NomsTypeToGraphQLType与NomsTypeToGraphQLInputType(输入类型不允许 union 与循环 struct); - Bool/Number/String 分别映射为非空
Bool/Float/String; - List 映射为含
values、size字段的 struct,支持at(起始索引,默认 0)、count(返回数量,默认全部)参数; - Set 额外支持
key(起始值)、through(结束值,含)、keys(仅返回匹配键的值)参数; - Map 映射为含
values、keys、entries(key/value字段的 entry struct)、size的 struct,参数同 Set; - Struct 映射为带额外
hash字段的 GraphQL struct,Noms 可选字段映射为可空类型; - Ref 映射为含
targetHash、targetValue字段的 struct。
目前不支持的类型包括 Blob、Type、含非 Struct 成员的 Union,且 GraphQL 输入类型不支持 union,限制了可用的输入类型。
状态与已知局限
Noms 目前没有活跃维护,README 明确建议:除娱乐或研究外不应使用;若需要类似功能的活跃项目,可关注 Noms 的 fork——Dolt。在依赖它构建系统前,应知晓以下主要未决问题:
- 长提交链下的同步性能(issue #2233);
- 迁移机制(issue #3363);
- 垃圾回收(issue #3374);
- 查询语言尚未定型(GraphQL 桥接不完整);
- 其他各类较小的 bug 与改进。
更多学习资源
- 去中心化数据库:面向去中心化 Web 的 Noms 应用;
- 技术总览:基础概念;
- CLI 指南:命令行界面导览;
- Go SDK 导览:Go API 使用;
- 路径拼写规范:对象与数据集参数写法;
- FAQ:常见问题。
Noms 采用 Apache License 2.0(Attic Labs, Inc.)授权。若对 Noms 的源码级细节感兴趣,建议从 cmd/noms/noms.go(CLI 注册)、go/types/rolling_value_hasher.go(Prolly Tree 切片核心)、go/hash/hash.go(哈希策略)以及 go/nbs 目录(存储层)入手阅读。
- 数据库
- 版本控制
- 后端
【免费下载链接】noms
The versioned, forkable, syncable database
相关推荐
Noms数据库:Git启发的版本化分布式数据库革命
Noms数据库:Git启发的版本化分布式数据库革命 Noms是一个革命性的分布式数据库系统,由Attic Labs团队开发,旨在解决传统数据库在数据版本控制、分
数据库版本控制后端5分钟快速上手PingFangSC字体:免费开源的中文Web排版终极方案
5分钟快速上手PingFangSC字体:免费开源的中文Web排版终极方案 你是否在为网站的中文显示效果而烦恼?不同设备上字体渲染不一致,商业字体授权费用昂贵,这
前端Noms 技术纵览:Merkle DAG 与 Prolly Tree 支撑的版本化、可同步去中心化数据库
Noms 技术纵览:Merkle DAG 与 Prolly Tree 支撑的版本化、可同步去中心化数据库 本文基于仓库 doc/intro.md https:/
数据库版本控制后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考