原文链接,文章来自 Infisical,一家做密钥管理的开源商业公司,主要对标的是 HashiCorp Vault。
Infisical 在过去一年里迅速发展,平台现在每天处理超过 5000 万个密钥,将应用程序配置和私密数据发送给需要的团队、CI/CD 流水线以及服务器/应用程序。
随着使用量的持续增长,我们不得不不断升级我们的技术栈。最近,Infisical 进行了一次全面的数据库迁移,从MongoDB 迁移到 PostgreSQL。这涉及到对此项计划的深思熟虑、采用新技术、创建新的数据库 schema、重构逻辑、重写查询语句,以及将数百万(如果不是数十亿)的数据库记录迁移到 PostgreSQL。这是一个复杂的过程,但无论如何都是必要且有利于平台改善的步骤。
以下是我们决定从 MongoDB 转向 PostgreSQL,并说明了我们如何做到这一点背后所作出决策的故事。希望这篇文章能够引人入胜,并对那些可能某天会考虑进行类似数据库迁移操作的人有所帮助。
我们从何开始?
当我们首次构建 Infisical 时,我们选择了团队最熟悉的技术栈来构建。作为这个技术栈的一部分,我们选择了 MongoDB + Mongoose ORM,因为这种组合带来的额外负担最小,并且允许我们快速地发布高质量的功能。正如 Tony Hoare 爵士所说,
过早优化是万恶之源
当时确实没有进一步优化的需要。我们更专注于构建 Infisical Cloud,我们的托管 SaaS 服务。并且鉴于这个方向,我们并未预料到会有那么多用户自行托管 (self-host) 产品,因此我们并未针对该用例进行设计。
为何抛弃 MongoDB?
虽然 MongoDB 在早期为 Infisical 提供了良好的服务,但当我们的产品场景不单单是托管服务时,它开始显示出不足之处。随着时间的推移,我们发现许多组织,特别是那些在合规和安全交叉点运营的组织,更喜欢自托管 Infisical 而非使用 Infisical Cloud;其他的有一些本地需求需要满足。
随着对自托管 Infisical 的需求增长,我们发现自己正在开发许多功能以减少学习如何自我托管 Infisical 所需的曲线,并且作为其中一部分,我们最终放弃了 MongoDB 转而支持 PostgreSQL。
实际上,在能力和可用性方面,我们和客户经常遇到 MongoDB 带来的限制问题,比如缺乏对事务、清理、云托管产品中版本一致性的支持,更不同提与 schema-less 数据库设计结构相关联的问题了。
以下是我对其中一些挑战的详细解释:
- 配置数据库事务困难:使用MongoDB,设置事务并不简单,因为它需要在集群模式下运行 MongoDB,并有各种配置开销;这使得客户要运行一个简单的 Infisical POC 变得极其困难,因为它需要生产环境下的 MongoDB 设置。对于处理高度敏感数据且数据完整性必须保证的产品来说,这是无法接受的。
- 缺少关系型特性:使用 MongoDB 后,我们失去了许多来自关系型世界的好功能,比如 CASCADE,当目标资源被删除时,可以删除其他表中所有被引用资源;这尤其令人痛苦,因为我们的数据非常依赖关系型。结果是,在我们旧代码库中采用了大量删除函数却从未能完全完成任务,并在 MongoDB 数据库中留下悬空资源。
- 云提供商支持不足:在 MongoDB 将许可更改为 SSPL 后,许多云提供商选择提供较老版本的 MongoDB。结果是,我们发现很难确保 Infisical 的功能可用性, 除非是运行最新稳定版 MongoDB 的客户。
- 缺乏操作 MongoDB 的经验:由于更多人熟悉部署基于 SQL 的数据库, 他们经常在扩展和正确配置 MongoDB 上遇到困难;这导致我们需要为客户提供的支持量不成比例地增加,特别是因为他们对 MongoDB 不熟悉。
出于以上以及其他更多原因,我们意识到使 Infisical 能够被世界各地的团队和组织更广泛接受所需的最重要功能,其实是需要全面迁移至更加通用的数据库。
为何钟情 PostgreSQL?
在寻找新的数据库时,我们首先列出了对我们最重要的几个方面:管理便利性(即包括配置、部署和扩展),内置事务支持,以及关系型功能。在讨论过程中,我们还考虑了是否应该构建自己的集成存储或寻求外部存储解决方案。
以下是每个选项的意义:
- 集成存储:我们可以将像 SQLite 这样的数据库系统直接打包到 Infisical 中,并采用水平复制策略来通过避免额外的网络跳转来减少延迟。在这个模型中,扩展系统意味着部署多个 Infisical 实例,并让它们通过某种共识算法(如Raft)进行相互通信。虽然这看起来是一个极好的解决方案,因为客户不需要连接任何依赖项就能运行Infisical,但执行此愿景所需的工具生态系统感觉还不够成熟,而且所需的工程努力也太强人所难了。
- 外部存储:我们可以简单地用 PostgreSQL 或 MySQL 等其他数据库替换 MongoDB,并使用其内置的扩展功能。尽管这个解决方案并没有完全消除使用 Infisical 时需要外部依赖项带来的摩擦,但我们认为它已经通过不再是MongoDB,而带来了显著的优点。当涉及到支持一个或多个数据库时,我们认为支持多个会错过每种解决方案各自独特的优点;同时也会增加我们的工程开销。
经过慎重考虑,我们选择了 PostgreSQL。除了拥有活跃的社区、详尽的文档以及大量可用的解决方案和扩展外,我们最欣赏的是它开源的本质以及绝大多数云服务提供商都提供 PostgreSQL 的托管服务。
首先,这意味着 Infisical 的用户可以更容易地在任何云服务提供商上自行部署我们的平台,并将其与相应的托管 PostgreSQL 服务配对使用。此外,鉴于该数据库已被广泛采用,我们有信心用户在使用 Infisical 时操作起来会更加顺畅。
ORM 该怎么办?
在确定使用 PostgreSQL 后,我们需要弄清楚我们的应用程序将如何与数据库进行交互。一开始,我们希望找到一个可以与我们使用 Mongoose ORM 的 MongoDB 体验相媲美的东西。因此,我们开始根据成熟度、可视化和迁移支持以及适当的抽象级别来评估候选者;主要考虑了 Drizzle ORM、Prisma ORM、TypeORM 和 Knex.js(一种查询构建器)。
最后,我们决定使用 Knex.js,一个查询构建器,而不是 ORM 来更好地控制数据库。虽然承认,选择原始 SQL 将具有最多样化的功能和最少的抽象层次,但我们觉得这种方法可能会过于容易出错,并且坦率地说,在没有适当的 TypeScript 支持下维护起来非常麻烦。此外,除了接近裸露的 SQ L之外,Knex.js 还带有自己的 seeding 和 schema 迁移工具包,并拥有成熟的生态系统以及几乎可以回答任何可能查询问题的优秀文档。通过一些自定义 Zod 集成工作,我们设法使其达到对 TypeScript 支持满意度较高水平。
在确定了数据库和 ORM 后, 我们启动了一个流程, 最终的工作量是重写数十个数据结构和应用程序中数百个查询。
我们是如何计划迁移的?
在代码重写的最后阶段,我们开始考虑如何进行迁移操作,将我们的 MongoDB 数据映射到 PostgreSQL,同时对Infisical云平台的干扰降至最低。
鉴于 Infisical 在客户基础设施中的关键作用,我们立即排除了有任何绝对停机时间的可能性。我们不得不做出妥协的部分是,在短暂的迁移窗口期间禁止写入操作(即客户无法创建或更新应用配置),以换取保证数据完整性。这种权衡似乎可以接受,因为客户主要从 Infisical 获取秘密信息。很少他们会每秒钟更新应用配置。
接下来,关于实际迁移操作,我们需要从 MongoDB 导出数据、仔细转换它,并将其重新插入到 PostgreSQL 中。当我们审计迁移序列时,面临着诸如确保各种树形结构从 NoSQL 正确地转化为它们关系型数据库等价物;这对于具有递归考虑因素的文件夹等数据结构特别敏感。我们还发现需要一种持久化方式来存储和映射 MongoDB 中与PostgreSQL 中相同标识符;如果只依赖内存处理,考虑到我们处理的数据量之大,这是行不通的。最后,我们选择使用 LevelDB 键值存储来协助标识符存储和查找操作。有了它,我们可以逐表地将数据移入 PostgreSQL 中。
大迁移
最后,我们准备进行迁移。在此之前,那些没有直接参与代码库重写的人员已经花了一个季度来改进 Infisical 的其他方面,包括进行前端更改、打维护补丁、扩展客户端功能,并编写更好的文档。现在我们所有人都重新聚集起来为迁移本身做准备,即用新的替换应用程序代码库,并将数据从 MongoDB 转移到 PostgreSQL。
作为准备工作的一部分,我们制定了一份详细的迁移清单和预期时间表。
整体来看,计划大致如下:
- 在迁移的几周前,我们会通过电子邮件和应用内横幅提前通知用户即将进行的数据库升级。我们将对平台上的每个功能流程进行彻底测试,并为迁移进行试运行。
- 迁移本身将在一个六小时的窗口期内发生,在此期间只允许对平台进行读取操作。在这个窗口期,我们会运行迁移脚本,把数据从 MongoDB 转移到 PostgreSQL,检查是否有数据丢失,并且如果成功就把 DNS 切换到新实例。当然,如果事情出现问题,我们已经备好了应急方案。
- 最后,在迁移之后,我们会解决任何残留问题,并开始推出 Infisical 和 PostgreSQL一起工作的新文档。
计划在手,我们开干了。
结果
幸运的是,迁移执行过程非常顺利,没有数据丢失,只有少数非关键功能出现故障;我们在接下来的 36 小时内解决了这些问题,并将对客户的影响降到最低。
在迁移之后,我们观察到了许多好处:
- 平台的性能大幅提升,主要归因于对连接查询的优化。使用MongoDB时,平台经常需要进行效率低下的聚合查询和多次网络跳转以实现所需功能。例如,由于我们核心数据的关系特性,我们经常需要执行许多 $lookup 操作来模拟 SQL 中的连接;这些操作效率低下,并且通常需要我们相应地扩展数据库和应用实例。迁移到PostgreSQL 后,我们避免了这些效率低下的操作,也使得数据库账单上的成本减少了50%。
- 现在的平台采用更好的数据验证方式,在数据库层面而不是应用层面进行根源处理。由于 MongoDB 设计上没有 schema 约束, 它依赖 Mongoose 框架定义数据类型、必填字段和验证规则。有了 PostgreSQL, 我们再也不会面临如果数据库被绕过 Mongoose 范围访问或修改时可能出现的数据不一致问题。
- 最后并且最重要的是, 我们认为 Infisical 现在更容易自我托管, 客户可以无需额外配置开销 (如处理 MongoDB 中启用事务功能所需的 replica sets) 就能进行 POC 测试. 总体来说, 考虑到手头目标、任务范围及其执行结果, 我们认为这项举措非常成功。一旦我们有更多数据,我们计划在未来发布更具体的结果。
总的来说,考虑到手头的目标、任务的范围以及最终执行情况,我们认为这次的迁移非常成功。一旦我们有了更多的数据,我们打算在未来发布更具体的结果。
结论
从 MongoDB 迁移到 PostgreSQL 的决定一开始就不是件容易的事。总的来说,这个计划我们花了 3-4 个月时间去执行,期间进行了仔细的规划和讨论,包括我们为何需要执行此项任务、我们将如何去做;然后小心翼翼地执行所有步骤。对于任何阅读此文的人,我强烈建议在尝试这样大规模的工作之前深思其用例和实施方案。总体而言,我非常高兴一切都按计划进行,这将对 Infisical 用户产生巨大影响。
译者后记,Bytebase 和 Infisical 在部署形态上也有相似之处。因为要访问用户数据库,企业出于数据安全的原因,往往需要私有化部署。Bytebase 的一大优势就是部署简单。POC 时,运行一个 Go 的二进制文件,就能把前端,后端以及数据库一起跑起来(利用了 Go 1.16 引入的 embed 功能)。如果是生产部署,就也只需要外接一个 PostgreSQL 数据库就行了。
💡 更多资讯,请关注 Bytebase 公号:Bytebase