4237 字
21 分钟
FerretDB & MongoDB —— 浅谈SLDB以及对PG的一点思考

FerretDB 2.0 VS MongoDB —— SLDB 之争,以及关于PG前景的一点讨论#

vs


    写在前面,我觉得这篇文章能出现都算Mongo咎由自取,就很难想象,能有哪个轻量级NoSchema数据库能对——拥有嵌入式模型、管道操作、窗口函数、文档级并发的MongoDB起威胁的,就按照常理来说,Mongo理应在这条赛道上一骑绝尘,但在2018 年 MongoDB 转向 Server Side Public License (SSPL) ,其许可限制引发了一些争议,尤其是对开源社区和早期商业项目而言,那我们用的好好的,帮你也宣传了这么久,现在你名满天下了,大明星了,要和我们这些糟粕之妻切割?!

    说的优点极端了哈,一方面,技术人,挣点钱不寒碜,一方面,用SSPL也是情有可原,有些云厂商太恶心了,直接拿着Mongo的源码卖服务,其实开源精神本身就有点邪乎,谁沾上点都容易被回旋镖(笑) , 你说是吧,安特雷兹

    扯远了,好,今天我们也不是非你Mongo不可,今天就介绍一款开源的SLDB——基于PostgreSQL二开的FerretDB

    FerretDB 作为一个新兴的开源替代品,旨在通过与 MongoDB 兼容的接口和 PostgreSQL 后端,提供一个无许可限制的文档数据库解决方案。

    本文将从表面到底层,全面比较 FerretDB 和 MongoDB 的异同,涵盖许可、兼容性、数据存储、查询语言、复制机制、存储架构、应用场景及未来发展前景。好好聊聊这两款SLDB


许可#

虽然这个聊起来没啥意义,但还是说两嘴,毕竟也是Mongo挨骂的开端,在开源界,选择一款适合自己项目的许可协议还是很重要的


MongoDB 的 SSPL 许可证#

MongoDB 自 2018 年 10 月 16 日起采用 SSPL 许可证。SSPL 要求对 MongoDB 的增强功能必须向社区发布,并限制其他公司将 MongoDB 作为托管服务提供时不开放相关服务软件的源代码。尽管 SSPL 符合开源软件的定义,但其条款对云服务提供商和某些商业场景构成了限制,导致一些开发者认为它偏离了真正的开源精神。这种许可策略部分是为了保护 MongoDB 的商业模式,防止云厂商直接利用其代码提供竞争性服务。


FerretDB 的 Apache 2.0 许可证#

FerretDB 采用 Apache 2.0 许可证,这是一个宽松的开源许可证,允许用户自由使用、修改和分发软件,无需强制公开修改后的源代码。这种许可模式使 FerretDB 更适合开源项目和希望避免许可复杂性的开发者。Apache 2.0 的灵活性降低了使用门槛,尤其对初创公司和社区驱动的项目具有吸引力。


MongoDBFerretDB
许可证SSPLApache 2.0
开源性质有限制(需公开服务代码)宽松(无需强制公开修改)
适用场景商业化、云服务受限开源项目、灵活部署

下面用AI列举了一些常见的协议,以及他们的特点

关键异同对比#

协议允许闭源专利授权衍生作品要求”传染性”
MIT❓(隐含)保留版权声明即可
Apache 2.0需标注变更
GPL-3.0必须开源且同许可证
LGPL✅(动态链接)修改部分必须开源⚠️
AGPL网络服务也必须开源
BSD-2/3-Clause禁止署名推广(3-Clause)
MPL-2.0✅(组合代码)修改文件必须开源⚠️

如何选择?#

  1. 最大化传播:MIT/BSD(适合工具库)。

  2. 企业/专利敏感:Apache 2.0。

  3. 强制开源:GPL/AGPL(慎用,可能吓退商业用户)。

  4. 混合项目:MPL/LGPL。

注意:GPL与MIT/Apache代码不兼容(GPL项目不能直接包含MIT代码,除非整个项目改用GPL)。



使用兼容性#

我觉得这里是Ferret的一个特点,那首先,大家之所以知道Ferret无非是从Mongo引流来的,我们做的中间件既没有达到Mongo的性能,也没有Mongo的高可用,那我们的优势只能是吸引那些开发者或者没有太多预算的组织或者公司。那我们需要的肯定是无缝衔接那些伤心Mongoer,所以,Ferret可以直接接入Mongo的Wire Protocol

FerretDB 设计目标是与 MongoDB 5.0+ 的驱动和工具兼容,通过实现 MongoDB wire protocol,将客户端的 MongoDB 查询转换为 PostgreSQL 的 SQL 查询。它支持许多 MongoDB 的核心命令,如 findinsertupdatedeleteaggregate,并可与 MongoDB Shell 和 MongoDB Compass 等工具无缝协作。然而,FerretDB 尚未实现所有 MongoDB 命令,例如 bulkWrite 和某些角色管理命令(详见 FerretDB 兼容性文档)。这意味着在某些高级功能依赖场景下,FerretDB 可能需要额外的适配工作。



命令类别MongoDB 支持FerretDB 支持FerretDB 未支持(示例)
管理命令create, drop, listCollectionscreate, drop, listCollectionscloneCollectionAsCapped, setParameter
查询命令find, aggregate, countfind, aggregate, countbulkWrite
用户管理createUser, dropUsercreateUser, dropUsergrantRolesToUser, revokeRolesFromUser

> Ferret在早期根本就没想实现形同Mongo一般的RBAC的鉴权模型,这也侧面说明,可能早期的Ferret并没有很明确的大型服务供应的计划,或者说他们不觉得这个功能有什么作用。。(虽然我觉得挺有用的,尤其是在多个业务数据交互在同一个Mongo的时候) >

存储#

存储才是重中之重,从存储中才能看到原生SLDB和Ferret这种基于PostgreSQL这种关系型数据库二开的SLDB的区别,虽然都是面向Document存储,但由于底层存储的方式不同,性能也会有所不同

MongoDB#

众所周知,Mongo的底层是采用了优化后的 Bson来存储的Document数据,Bson来Bson去,说白了也是B+Tree,但Mongo的存储是这样的,以 _idRootNode去构建一棵B+Tree,这棵B+Tree叶子节点就负责存储这一整个Document的数据,也就是Bson,二进制Json,但也不仅仅是把Json Binary,而是做了一些其他的优化,下面介绍一下MongoDB底层的存储特点:

  • 线性存储:BSON 将文档的键值对按顺序编码为连续的二进制数据,每个字段包含:

    • 类型标识符(1字节,标明值的类型,如字符串、整数等)。

    • 字段名(以 null 结尾的字符串)。

    • 字段值(根据类型存储,如 int32、double、嵌套 BSON 等)。

  • 长度前缀:文档开头会存储整个 BSON 对象的长度(4字节),便于快速跳过解析不需要的字段。

  • 嵌套支持:嵌套文档(子文档)和数组会被编码为独立的 BSON 对象,递归存储在父文档中。

可以发现的是,这又是 元数据 的应用,这种设计真的广泛地应用在各个领域,小到操作系统的分页,大到Golang 2.14 的SwissTable,它主张把数据缓存化、索引化,目的只有一个——减少回盘,减少IO,从而提升整体效率。感兴趣的朋友可以去看看我另一篇文章 [SwissTable]

graph TD
    subgraph "MongoDB 主 B+ 树"
        Root["Root Node
        [_id:1]"]
        Leaf1["Leaf Node
        [文档: {_id:1, name:'Alice', age:25}]"]
        Root --> Leaf1
    end

    subgraph "MongoDB 索引 B+ 树"
        IndexRoot["Root Node
        [name:'Alice']"]
        IndexLeaf1["Leaf Node
        [name:'Alice', _id:1]"]
        IndexRoot --> IndexLeaf1
    end

和MySQL一样,Mongo的每一个 Collection(Table) 在底层都对应着一棵 B+ Tree,并通过主键索引来映射 叶子节点上的 Document(Row),所以在索引上,也有着同样的二级索引的概念和应用机制。

graph TD
    subgraph "MySQL 主键索引"
    
        PKRoot["Root Node 
        [id:1]"]
        PKLeaf1["Leaf Node 
        [id:1, name:'Alice', age:25]"]
        PKRoot --> PKLeaf1
    end

    subgraph "MySQL 辅助索引"
    
        SecRoot["Root Node
        [name:'Alice']"]
        SecLeaf1["Leaf Node
        [name:'Alice', id:1]"]
        SecRoot --> SecLeaf1
    end

FerretDB#

Ferret基于PostgreSQL,使用了Microsoft的DocumentDB,来实现的SLDB,所以底层是采用了PostgreSQL的 JsonB来进行数据的存储

JsonB 本质上也是对 Json 进行了二进制转储,但区别在于Bson拥有很多索引类型,例如

  • 日期(datetime)
  • 二进制数据(binary)
  • 128 位 IEEE 754-2008 浮点数(decimal128),用于高精度数值计算
  • 正则表达式(regex)
  • JavaScript 代码(JavaScript)
  • 哈希值(MD5)
  • 等等

但JsonB存储的是标准 JSON 数据类型(字符串、数字、布尔值、数组、对象、null),但以二进制形式存储。JSONB 本身不引入新的数据类型,但 PostgreSQL 提供了对 JSON 数据的高级操作和索引功能。

Postgre底层采用的是 堆存储,一个 heap 下存储着 Page,每张 Page 又存储着行数据 ( Tuples ),所以,即使JsonB和Bson很相似,但由于基于的存储引擎不同,最终存储的形式也不相同,想进一步了解的同学,可以去看看Postgre的官方文档和Mongo Wired Tiger的技术报告。

下面贴一张图来展示一下 Ferret大致的工作流程

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ffefd5', 'edgeLabelBackground':'#fff', 'fontFamily': 'Arial', 'fontSize': '14px'}}}%%
flowchart TB
  %% Title for context
  accTitle: FerretDB: MongoDB 数据存储到 PostgreSQL JSONB 的流程
  accDescr: 本图展示了 FerretDB 如何将 MongoDB 的 BSON 文档转换为 JSONB 并存储在 PostgreSQL 中,包括协议转换、表结构、堆存储和 JSONB 存储细节。

  %% ========== 第一层:协议转换 ==========
  subgraph Protocol_Conversion["协议转换"]
    direction LR
    MongoDB_Client[("MongoDB 客户端")] -->|发送 BSON 数据| FerretDB_Server[["FerretDB 服务"]]
    FerretDB_Server -->|转换 BSON 为 JSONB| PG_Driver[["PostgreSQL 驱动"]]
    PG_Driver -->|执行 SQL 插入/更新| PostgreSQL[("PostgreSQL 数据库")]
  end

  %% ========== 第二层:表结构 ==========
  subgraph Table_Structure["表结构"]
    direction TB
    Table[["ferretdb_collections"]] -->|列| ID["_id UUID
    主键"]
    Table -->|列| Doc["doc JSONB
    (支持 TOAST 压缩)"]
  end

  %% ========== 第三层:堆存储细节 ==========
  subgraph Heap_Storage["堆存储细节"]
    direction LR
    Heap_File["堆文件
    (OID=12345)"] --> Page0["页面 0
    (8KB)"]
    Heap_File --> Page1["页面 1"]
    Heap_File --> Page2["..."]

    subgraph Page0[" "]
      direction TB
      Page_Header["页头
      (LSN, 校验和)"]
      Row_Pointers["行指针数组
      (偏移量, 长度)"]
      Free_Space["空闲空间"]
      Row_Data["行数据
      (元组)"]

      Page_Header --> Row_Pointers
      Row_Pointers --> Free_Space
      Free_Space --> Row_Data
      Row_Pointers -.->|指向| Row_Data
    end
  end

  %% ========== 第四层:元组与 JSONB 存储细节 ==========
  subgraph Tuple_JSONB["元组与 JSONB 存储"]
    direction LR
    Tuple["元组"] --> Tuple_Header["元组头
    (xmin, xmax, ctid, flags)"]
    Tuple --> JSONB_Data["JSONB 数据"]

    subgraph JSONB_Data[" "]
      direction TB
      JSONB_Header["JSONB 头
      (版本, 压缩标志)"]
      Inline["内联键值对
      (若 < TOAST 阈值)"]
      TOAST_Pointer["TOAST 指针
      (若 ≥ TOAST 阈值)"] --> TOAST_Table["TOAST 表
      (分块存储)"]
      
      JSONB_Header --> Inline
      JSONB_Header --> TOAST_Pointer
    end
  end

  %% ========== 层级连接 ==========
  PostgreSQL --> Table
  Table --> Heap_File
  Row_Data --> Tuple

  %% ========== 样式定义 ==========
  classDef client fill:#ffefd5,stroke:#ffa500,stroke-width:2px
  classDef server fill:#e0ffff,stroke:#20b2aa,stroke-width:2px
  classDef driver fill:#f0e68c,stroke:#ffd700,stroke-width:2px
  classDef db fill:#dcd0ff,stroke:#9370db,stroke-width:2px
  classDef table fill:#f5f5f5,stroke:#d3d3d3,stroke-width:2px
  classDef storage fill:#ffe4e1,stroke:#ff6b6b,stroke-width:2px
  classDef header fill:#f0fff0,stroke:#3cb371,stroke-width:2px
  classDef data fill:#e6e6fa,stroke:#9370db,stroke-width:2px

  class MongoDB_Client client
  class FerretDB_Server server
  class PG_Driver driver
  class PostgreSQL db
  class Table table
  class Heap_File,Page0,Page1,Page2,TOAST_Table storage
  class Page_Header,Tuple_Header header
  class Row_Data,Tuple,JSONB_Data data

  %% ========== 连接线样式 ==========
  linkStyle 6,7,10 stroke:#666,stroke-width:1.5px,stroke-dasharray:5

Bson VS JsonB#

这里算是整篇文章的一个核心讨论点了,首先我认为必须下一个定论,那就是目前而言,基于原生引擎的MongoDB对于Document存储的各方面能力是要远高于Ferret的,这是无可争议的,但实际上,我们很多时候对各种新兴产物的出现是喜闻乐见的(即使他们确实meaningless),所以我觉得在进行讨论的时候不需要太严苛。。。大多数时候抱着 为什么它可以 而不是 为什么它不可以去探讨,往往能得到更大收获

Bson和JsonB最大的区别其实在于结构上

Bson的特点在于引入了元数据,例如 二进制编码与长度前缀,这种设计允许 MongoDB 快速跳过无关字段或定位特定字段,而无需解析整个文档,就好似 ProtoBuf一样,事先会告诉你需要的哪些字段在哪里,提前做了一个索引。

JsonB采用分解式存储方式,将 JSON 对象分解为独立的键值对 比如{"name": "Alice", "age": 30} 被拆分为 "name" -> "Alice""age" -> 30"并以二进制形式存储。这种结构允许 PostgreSQL 直接访问特定键值,而无需解析整个文档。

由于结构的不同,索引设计就也不相同了,下面贴一下

MongoDB 针对 BSON 文档提供了多种索引类型,以支持灵活的查询需求:

  • B-tree 索引
    默认索引类型,支持等值查询、范围查询和排序。适用于单字段或复合索引(多字段组合)。
  • 复合索引
    可以在多个字段上构建索引,优化多条件查询。例如,{ “name”: 1, “age”: 1 } 可加速联合查询。
  • 地理空间索引
    支持 2dsphere 和 2d 索引,用于地理位置查询,如查找附近点或地理围栏。
  • 全文索引
    支持文本字段的全文搜索,包含分词和语言特定优化。
  • 索引实现
    索引直接构建在 BSON 文档的字段上。BSON 的长度前缀设计使得字段提取高效,索引构建和查询性能优异。

PostgreSQL 为 JSONB 提供了专门的索引机制,优化 JSON 数据查询:

  • GIN 索引
    GIN(广义倒排索引)是 JSONB 的核心索引类型,支持路径和值查询。例如:
    • @>(包含)操作符:检查 JSONB 是否包含某子结构。
    • ?(存在)操作符:检查某键是否存在。 GIN 索引特别适合复杂条件查询。
  • BTREE 索引
    可在 JSONB 的特定路径上创建 BTREE 索引。例如,jsonb_column->‘age’ 可建立 BTREE 索引,支持等值和范围查询。
  • 路径查询支持
    JSONB 使用 ->(取对象)和 ->>(取文本)操作符访问嵌套字段,配合 GIN 或 BTREE 索引加速查询。
  • 索引实现
    JSONB 的分解式存储允许索引直接作用于键值对,而无需解析整个文档。这在查询特定路径或值时效率极高。

其实看到这里,你也应该可以Get到,比起Mongo这种天生为了SchemaLess存储设计的架构,JsonB其实显得很羸弱,当然,我们今天的目的不是说要探讨两种存储方式的孰强孰弱,而是接机去感受两种设计的区别和哲学

参考 Json VS Bson VS JsonB


Replica#

其实还是基于 Mongo 的opLog 和 Postgre的 WAL,这里其实能对比一下的,市面上的主从复制方案太多了,可以借此来看看区别

首先,opLog的结构是要比WAL简单很多的,opLog更像MySQL的binlog,记录的是具体的操作,比如“插入这个文档”“更新那个字段”,而WAL是基于存储架构的,它记录的是数据页的字节级修改(“第42页,第17字节变了”)或表级操作(“往 users 表插一行”),具体取决于 wal_level 设置。每条记录有个 LSN(日志序列号),就像时间戳,方便追踪进度。

然后在具体应用上,其实都是从机订阅日志来实现时间重放,这点上没有太大区别。

WAL更适合金融、电商等需要严格一致性的场景,还有PITR做容灾恢复,其实在可用性上做了很大的保证。不过分布式新主场景下,需要引入像是Hot Standby来保证高可用。

opLog 就更适合内容管理、IoT 等灵活数据场景,有自动选主,日志本身也更轻量化

## LSN: 0/3000028 Record Type: XLOG/INSERT Timestamp: 2025-04-22 10:00:01.123 UTC Transaction ID: 54321 Relation: public.users (OID: 16384) Page: 42 Offset: 8192 Data: [Tuple Data: (id=1, name='Alice', age=30)]
LSN: 0/3000050 Record Type: XLOG/CHECKPOINT Timestamp: 2025-04-22 10:00:02.456 UTC Checkpoint LSN: 0/3000028 Redo LSN: 0/3000020 WAL File: 000000010000000000000003
## { "ts": { "$timestamp": { "t": 1713866401, "i": 1 } }, "op": "i", "ns": "mydb.users", "o": { "_id": "user123", "name": "Alice", "age": 30 } }
{ "ts": { "$timestamp": { "t": 1713866402, "i": 1 } }, "op": "u", "ns": "mydb.users", "o": { "$set": { "age": 31 } }, "o2": { "_id": "user123" } }

Ferret和Mongo的对比就到这里,贴一下整理好的Benchmark结果,下面唠点别的

vs

benchmark放这里,有兴趣的朋友可以自己跑一跑

BenchMark


Why Postgre?#

写到这里其实已经偏题很多了,但这才是我真正的思考所在,Ferret、Postgis这些数据库都是基于Postgre开发,为什么?

一方面我觉得是因为开源,没错,因为PostgreSQL的开源协议非常宽松。我觉得整个业界其实都很愿意推动、拥抱发展,发展会带来利益,利欲会熏心。。。哈哈哈扯歪了

还有就是PG支持的类型是非常丰富的,支持 JSON、XML、数组等复杂数据类型,十分适合处理现代应用程序中的非结构化数据。

最重要的是PostgreSQL 的设计注重可扩展性,允许开发者通过插件和扩展添加新功能。无需修改核心代码,就可以引入新的数据类型、函数、操作符或聚合函数

现在又有 pgvector 之类的向量数据库出现,积极拥抱着AI和社区,我认为未来是属于开源数据库的,众人拾薪火势高。

先写到这里吧,何时想起何时续写


FerretDB & MongoDB —— 浅谈SLDB以及对PG的一点思考
https://fuwari.vercel.app/posts/ferret_vs_mongo/
作者
Simon
发布于
2025-04-22
许可协议
CC BY-NC-SA 4.0