LibreChat × FerretDB 多租户落地方案:Database-Per-Org 隔离、水平分片 PoC 与死锁重试策略
【免费下载链接】LibreChatEnhanced ChatGPT Clone: Features Agents, MCP, Skills, DeepSeek, Anthropic, AWS, OpenAI, Responses API, Azure, Groq, o1, GPT-5, Mistral, OpenRouter, Vertex AI, Gemini, Artifacts, AI model switching, message search, Code Interpreter, langchain, DALL-E-3, OpenAPI Actions, Functions, Secure Multi-User Auth, Presets, open-source for self-hosting. Active项目地址: https://gitcode.com/GitHub_Trending/li/LibreChat
本文基于 LibreChat 仓库中的 FerretDB 多租户调研文档,完整还原其以 FerretDB(PostgreSQL 后端、DocumentDB 模式)实现「每组织一个数据库」隔离架构的调研过程:从 PostgreSQL 底层表结构映射、98 个自定义索引的兼容性验证,到 100 租户的扩展曲线、分片路由 PoC、死锁重试工具与生产级备份/迁移方案。读完后,你将掌握一套可复制到任意 Mongoose 多租户项目的 FerretDB 选型依据、基准测试方法、容量规划阈值和运维操作手册。
1. 调研目标与约束
该调研(文档状态:Active Investigation)的核心目标是:使用 FerretDB(PostgreSQL 后端)实现 database-per-org 的数据隔离,并通过多个 FerretDB + Postgres 实例对进行水平分片扩展。文档明确排除了两个备选:原生 MongoDB 和 AWS DocumentDB 均不在选项之列。
这一定位意味着:
- 隔离粒度是「逻辑数据库」(Mongoose 的
useDb()),而非单个集合加orgId字段; - 水平扩展方式是「多套 FerretDB + Postgres 实例对 + 租户路由」,而非 MongoDB 原生的 chunk 分片;
- 所有性能数据都在真实 FerretDB v2.7.0 实例上实测得出,不是纸面推断。
2. FerretDB 底层架构:DocumentDB 后端在 PostgreSQL 中到底长什么样
文档的第一项发现揭示了 FerretDBpostgres-documentdb后端的真实存储结构,这直接决定了隔离模型和备份方式:
- 不会为每个 MongoDB 数据库创建独立的 PostgreSQL schema。所有数据都集中在单一的
documentdb_dataPG schema 中; - 每个 MongoDB collection 映射为
documents_<id>+retry_<id>的表对(retry 表用于 DocumentDB 语义下的事务重试日志); - 目录(catalog)由
documentdb_api_catalog.collections与documentdb_api_catalog.collection_indexes两张表维护; - Mongoose 的
mongoose.connection.useDb('org_X')会在 DocumentDB 的 catalog 中创建一条逻辑数据库记录,即所谓「逻辑隔离」。
关键含义:不存在 PG 级别的 schema 隔离,隔离由 FerretDB 的 wire protocol 层强制执行;因此备份/恢复必须走 FerretDB(即 MongoDB 协议/驱动),不能直接对底层 Postgres 做pg_dump——这一点在仓库基准测试 multiTenancy.ferretdb.spec.ts 中通过catalogMetrics()函数直接对 catalog 表做count(*)快照来印证,该函数正是用 psql 查询documentdb_api_catalog与information_schema来度量目录增长的。
3. 兼容性验证:29 个模型、98 个自定义索引
LibreChat 的 Mongoose 模型规模对任何替代存储都是压力测试。调研在 FerretDB v2.7.0 上验证了全部 29 个 org-local 模型与 98 个自定义索引,结果全部通过:
| 索引类型 | 数量 | 状态 |
|---|---|---|
| Sparse + unique | 9(User 的 OAuth ID) | Working |
| TTL(expireAfterSeconds) | 8 个模型 | Working |
| partialFilterExpression | 2(File、Group) | Working |
| Compound unique | 5+ | Working |
| 并发创建(全部 29 模型) | 单 org 场景无死锁 | Working |
对应验证逻辑见 multiTenancy.ferretdb.spec.ts 的 Phase 2:它从User集合的indexes()回读结果中断言 sparse 与 TTL 索引各至少 1 个,并专门检查File与Group模型回传了partialFilterExpression——即 FerretDB 不只是「建了索引」,而是按类型如实回传索引元数据,这保证了 Mongoose 的syncIndexes类操作不会误判。
模型清单并非手写副本,而是从活体模型注册表动态派生(见 schemas.ts 的getModelSchemas),避免基准测试与真实 schema 漂移。
4. 扩展曲线:10 → 100 个组织,初始化与查询延迟保持平坦
基准测试(spec 的 Phase 3)逐档创建组织(默认档位10,50,100,可用环境变量SCALE_TIERS覆盖),每档记录 catalog 增长、单组织初始化耗时和点查询延迟(50 次迭代取 avg/p95)。实测结果:
| 组织数 | 集合数 | Catalog 索引 | 数据表 | pg_class | 初始化/组织 | 查询均值 | 查询 p95 |
|---|---|---|---|---|---|---|---|
| 10 | 450 | 1,920 | 900 | 5,975 | 501ms | 1.03ms | 1.44ms |
| 50 | 1,650 | 7,040 | 3,300 | 20,695 | 485ms | 1.00ms | 1.46ms |
| 100 | 3,150 | 13,440 | 6,300 | 39,095 | 483ms | 0.83ms | 1.13ms |
核心结论:初始化时间与查询延迟在 100 个组织规模内保持平坦,无退化。目录表行数(pg_class约 4 万行)与 catalog 索引 1.3 万条并未成为瓶颈。
该测试还顺带对比了「共享集合 + orgId 判别字段」的替代方案(Phase 5):向单集合插入 100 组织 × 50 用户共 5,000 条文档,测量复合唯一索引{orgId:1, email:1}下的点查、列表与计数性能,为 database-per-org 方案提供了横向参照基线。
5. 写放大:11+ 索引 vs 零索引,仅 1.11x
Phase 4 的写放大实验对User模型(11+ 个索引)与一个零索引的裸集合各执行 200 次updateOne(WRITE_AMP_DOCS可调),并同时用pg_stat_wal.wal_bytes差值度量 WAL 字节数。实测时间比仅1.11x——即高索引模型的写开销只比零索引多 11%,说明 DocumentDB 后端的 JSONB 索引维护效率足够高,「索引多的核心模型」不构成写路径隐患。
6. 分片 PoC:TenantRouter 的 fill-then-spill 路由
调研实现了完整的租户路由器 PoC(sharding.ferretdb.spec.ts),验证了多「池」(每池 = 一对 FerretDB + Postgres)下的租户分配与数据流。PoC 中两个池指向同一 FerretDB 实例,生产环境下每个池 URI 对应独立实例对。
6.1 核心设计
- 分配表:独立 control 连接中的
OrgAssignment集合(orgId唯一索引 +poolId索引),持久化组织到池的映射; - 容量限制 + fill-then-spill:
selectPoolWithCapacity()顺序扫描池,选择countDocuments({ poolId }) < maxOrgs的池,全满则抛出All pools at capacity. Add a new pool.(见 sharding.ferretdb.spec.ts); - 幂等分配:重复调用返回既有分配;并发下依赖唯一索引的
code === 11000(duplicate key)错误回读已有记录,避免覆盖(L164-L176); - 懒加载模型注册:
getOrgModels()首次访问时才在目标 org 连接上注册全部 29 个模型,并缓存 connection 与 models Map; - Express 中间件模式:为
req挂载getModel(name)(L442-L461 的模拟中间件测试),使业务代码对「多池 + 多库」完全无感——这是该 PoC 对上层框架最重要的接口承诺。
6.2 实测数据
| 指标 | 结果 |
|---|---|
| 跨池数据隔离 | org_1(池 A)与 org_6(池 B)的 User/Message 互不可见,并发读写正常 |
| 热缓存路由开销 | 0.001ms(亚微秒级,纯 Map 命中) |
| 冷路由(DB 查分配表 + 建连接 + 注册模型) | 6ms |
| 容量溢出 | 全部池满时正确抛错;重复分配幂等 |
| 批量供给 10 个 org | 逐池统计均值,全流程通过 |
冷路由 6ms 意味着首次请求一次组织可接受;热路径 0.001ms 意味着路由层本身几乎不占用 QPS 预算。
7. 规模阈值:何时需要第二套 Postgres
| 组织数 | Postgres 实例数 | 说明 |
|---|---|---|
| 1–300 | 1 | 默认配置即可 |
| 300–700 | 1 | 调优 autovacuum、PgBouncer、shared_buffers |
| 700–1,000 | 1–2 | 监控信号出现压力时再拆分 |
| 1,000+ | N / 每实例约 500 | 每 ~500 个组织配一对 FerretDB + Postgres |
即 300 组织以内单实例无压力,超过 1,000 后按「一对实例约 500 组织」线性扩展,这正是第 6 节 TenantRouter 存在的理由。
8. 死锁行为与生产重试策略
8.1 实测到的死锁模式
- 单组织并发建索引:无死锁(DocumentDB 后端自行处理);
- 批量供给(10 个 org 顺序供给):在 Pool B 上发生真实死锁,通过重试恢复。
即死锁不是发生在单组织内部,而是批量供给/迁移场景下多个组织同时打 catalog 表时出现的 PostgreSQL 级死锁。
8.2 重试工具实现
生产工具是 retry.ts 中的retryWithBackoff(调研文档以retryWithBackoff.ts之名引用,实际实现导出于src/utils/retry.ts)。从源码看其默认参数(L12-L18)为:
maxAttempts: 5,baseDelayMs: 100,maxDelayMs: 10_000,jitter: true;- 可重试错误按消息子串匹配:
deadlock、lock timeout、write conflict、ECONNRESET; - 退避公式:
min(baseDelay × 2^(attempt-1) + random jitter, maxDelay)(L62-L64),并暴露onRetry钩子用于监控埋点。
同文件还封装了两个上层函数:
createIndexesWithRetry(model)(L84-L93):替代裸model.createIndexes()的安全入口;initializeOrgCollections(models)(L100-L122):逐模型顺序执行createCollection()+ 带重试的createIndexes(),故意串行以最小化 DocumentDB catalog 上的竞争,并返回{ totalMs, perModel }供部署脚本记录耗时。
8.3 真实死锁恢复数据
| 场景 | 尝试次数 | 总耗时 | 备注 |
|---|---|---|---|
| 无竞争的组织初始化 | 1 | 165–199ms | 大多数组织一次成功 |
| User 索引死锁 | 2 | 994ms | 单次重试即恢复 |
| 重试叠加的最坏情况 | 2–3 | 1,839ms | 5 组织顺序批中最差值 |
5 组织批量供给的完整日志:
retry_1: 193ms (29 models) — clean retry_2: 199ms (29 models) — clean retry_3: 165ms (29 models) — clean retry_4: 1839ms (29 models) — deadlock on User indexes, recovered retry_5: 994ms (29 models) — deadlock on User indexes, recovered Total: 3,390ms for 5 orgs (678ms avg, but 165ms median)User模型(11+ 索引,含 9 个 sparse unique)是最容易触发死锁的集合。retry_4与retry_5证明了工具确实在真实 FerretDB 负载下捕获并恢复了死锁,而非仅覆盖单元测试路径。
9. 按组织备份/恢复:驱动级方案取代 mongodump
由于mongodump/mongorestoreCLI 在 FerretDB 下不可用,调研验证了纯驱动层方案(orgOperations.ferretdb.spec.ts):
- 备份:
listCollections()枚举 → 每集合find({}).toArray()→ 汇聚为内存OrgBackup结构; - 恢复:向新组织数据库逐集合
collection.insertMany(docs); - BSON 类型保真验证通过:ObjectId、Date、String 全部正确往返;
- 数据一致性验证通过:
_id、字段值、文档计数与源完全一致; - 性能:24ms 备份 / 15ms 恢复(29 个集合中 25 个为空、共 8 条文档的真实组织);
- 耗时随文档数线性增长,瓶颈是到 FerretDB 的网络 I/O 而非序列化。
| 操作 | 耗时 | 明细 |
|---|---|---|
| 备份(整组织) | 24ms | 8 条文档 / 29 集合(25 空) |
| 恢复(到新组织) | 15ms | 每集合含insertMany() |
| 索引重建 | ~500ms | 独立的initializeOrgCollections调用 |
10. 跨组织 Schema 迁移
| 操作 | 总耗时 | 折算每组织 |
|---|---|---|
| 幂等重初始化(无变更) | 86ms | 86ms |
| 新增集合(AuditLog)+ 4 索引 → 5 组织 | 109ms | 22ms/组织 |
users新增复合索引{username:1, createdAt:-1}→ 5 组织 | 22ms | 4.4ms/组织 |
| 全量迁移(29 模型 × 5 组织) | 439ms | 88ms/组织 |
关键特性:
createIndexes()幂等,可安全重跑;- 已有数据在迁移后完整保留;
createIndexes与createCollection不锁定既有数据,迁移可在线上服务流量期间执行;- 外推:1,000 个组织 × 88ms ≈ 88–90 秒完成一次全量迁移扫描。
11. 环境准备与复现步骤
仓库提供了最小可用的 FerretDB 开发/测试栈 docker-compose.ferretdb.yml:
- PostgreSQL 后端镜像:
ghcr.io/ferretdb/postgres-documentdb:17-0.0.07.0-ferretdb-2.7.0对应版本标签(Postgres 17 + FerretDB 2.7.0 的 DocumentDB 兼容镜像); - FerretDB 镜像:
ghcr.io/ferretdb/ferretdb:2.7.0,宿主端口27020映射容器27017; - 连接串
FERRETDB_POSTGRESQL_URL=postgres://ferretdb:ferretdb@ferretdb-postgres:5432/postgres。
测试通过独立 Jest 配置运行(jest.ferretdb.config.mjs 明确注明这些测试依赖运行中的 FerretDB 实例,不在 CI 中执行):
# 启动 FerretDB + Postgres docker compose -f packages/data-schemas/misc/ferretdb/docker-compose.ferretdb.yml up -d # 多租户基准(Phase 1-5,耗时较长) FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/mt_bench" \ npx jest multiTenancy.ferretdb --testTimeout=600000 # 分片 PoC FERRETDB_URI="mongodb://ferretdb:ferretdb@127.0.0.1:27020/shard_poc" \ npx jest sharding.ferretdb --testTimeout=120000可用的环境变量:FERRETDB_URI(必填,未设置时整个测试文件自动describe.skip)、PG_CONTAINER(psql 直查 catalog 用的容器名,默认librechat-ferretdb-postgres-1)、SCALE_TIERS、WRITE_AMP_DOCS。
测试文件全景:
| 文件 | 用途 |
|---|---|
| multiTenancy.ferretdb.spec.ts | 5 阶段基准(useDb 映射、索引、扩展曲线、写放大、共享集合对照) |
| sharding.ferretdb.spec.ts | 分片 PoC(路由、分配、隔离、中间件模式) |
| orgOperations.ferretdb.spec.ts | 生产运维(备份/恢复、迁移、死锁重试) |
| retry.ts | 生产重试工具(retryWithBackoff / createIndexesWithRetry / initializeOrgCollections) |
12. 生产运维建议
调研文档给出的四条落地建议,均与仓库源码相互印证:
- 组织供给:所有新组织一律走
initializeOrgCollections()(retry.ts);批量供给按 10 个一批用Promise.all()跨池并行、池内串行,兼顾吞吐与 catalog 竞争控制。 - 备份策略(驱动级,替代 mongodump):
listCollections()枚举集合;- 大集合用
find({}).batchSize(1000)流式拉取; - 按集合写 NDJSON 到对象存储(S3/GCS);
- 恢复用 1,000 条一批的
insertMany()。
- Schema 迁移:把
migrateAllOrgs()作为部署步骤——从分配表枚举全部组织 → 逐组织注册模型、createCollection()、createIndexesWithRetry();幂等可重跑,千级组织约 90 秒完成。 - 监控:跟踪每组织供给/迁移耗时,中位数供给时间超过 500ms/组织时排查 PostgreSQL catalog 压力,具体看三个指标:
pg_stat_user_tables.n_dead_tup(autovacuum 健康度);pg_stat_bgwriter.buffers_backend(缓冲压力);documentdb_api_catalog.collections行数(总表数规模)。
13. 结论与适用边界
这份调研给出的可执行结论可以概括为三点:
- FerretDB(postgres-documentdb)完整兼容 LibreChat 的 29 模型 / 98 索引体系,含 sparse、TTL、partial、复合唯一等全部索引类型,且 100 组织规模内无性能退化;
- 隔离与扩展模型成立:
useDb()逻辑数据库 + catalog 层隔离 + 每 ~500 组织一对 FerretDB+Postgres 的水平分片,路由热路径开销可忽略(0.001ms); - 运维闭环已验证:死锁重试(指数退避 + 抖动,100ms 起、10s 封顶)、驱动级备份/恢复(BSON 保真、线性扩展)、幂等跨组织迁移(~88ms/组织)三者均有真实负载下的恢复记录。
适用边界需要说明:所有数字均产生于单机 Docker 内的 FerretDB 2.7.0 + Postgres 17 环境、以 29 个 org-local 模型为基准,属于「调研期实测」而非 SLA 承诺;生产环境在 300+ 组织后需要按第 7 节阈值逐步调优 autovacuum、PgBouncer 与 shared_buffers,并在中位供给耗时越过 500ms 告警线时介入排查。
【免费下载链接】LibreChatEnhanced ChatGPT Clone: Features Agents, MCP, Skills, DeepSeek, Anthropic, AWS, OpenAI, Responses API, Azure, Groq, o1, GPT-5, Mistral, OpenRouter, Vertex AI, Gemini, Artifacts, AI model switching, message search, Code Interpreter, langchain, DALL-E-3, OpenAPI Actions, Functions, Secure Multi-User Auth, Presets, open-source for self-hosting. Active项目地址: https://gitcode.com/GitHub_Trending/li/LibreChat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考