我们使用 Postgres 构建多租户 SaaS 服务时踩的坑

news2024/9/23 20:10:59

原文 Our Multi-tenancy Journey with Postgres Schemas and Apartment。这篇和之前发出的「如何使用 Postgres 对一个多租户应用分片」相呼应。

file

多租户 (Multip-tenancy) 是当下的热门话题。我对多租户应用程序的定义是一个能够服务于多个客户的软件系统,每个客户都能在该系统中拥有自己数据的独立视图。每个客户及其数据通常被称为一个租户,因此而有了多租户之名。

在早先关于分片多租户应用程序的博客文章中,Craig 深入探讨了任何需要支持多个客户账户的系统中自然发生的租户类型(或在他的例子中,是一家店)。在他的例子中,属于特定租户的所有记录都会获得一个外键 tenant_id,这个键逻辑上将其他表中的行与该租户关联起来。隔离发生在查询级别,比如所有产品(例如)都会使用 tenant_id 来限定范围,针对你感兴趣的特定商店。

当我加入 Influitive 时,他们已经采用了多租户路线,但是使用了PostgreSQL schema 来隔离。这使用了一种略有不同的多租户机制,其中表不再存储对其租户的引用。相反,当我们添加一个新租户时,我们会创建一个新的 Postgres schema,并在该 schema 内创建(复制)所有存储客户数据的表。为一个租户进行的查询设置了 schema search_path,以便所有为该请求获取的数据现在都隐式地限定在该租户范围内。

file

为此,我们将深入探讨我们使用 Postgres schema进行多租户管理的经验(以及我们编写的 gem,Apartment)以及从这次经验中学到的一些教训。

最初,在确定了 Apartment 接口后,这种策略为我们提供了巨大的好处。我们可以引入新的客户,同时添加新的功能,并且不用担心数据隔离。我听说过无数创业公司的故事,其中客户之间的数据发生了泄露,而这简直不是我们需要担心的问题。

直到我们业务开始扩张了。。。

Schema 迁移 (Schema Migrations)

随着我们的客户基础增长到 100 多个客户,以及我们的应用程序增长到 100 多个表,我们开始注意到事情变得缓慢。我没有确切的数字来支持这一点,但我们发现 Postgres schema 的数量、这些 Postgres 中的表在执行迁移时被修改的大小,以及完成迁移所需的时间之间存在直接相关性。schema/表越多/越大,迁移所需的时间就越长。

众所周知,大表的索引更改可能会耗时,甚至导致表锁定。理想情况下,对于列添加之类的操作,你会得到恒定的 O(1) 性能。采用单独 schema 方法来做租户,你现在得到的是 O(N) 性能,其中 N=租户数。现在,当你遇到像索引添加这样不可预测的更改时,情况会变得更糟。我不确切知道如何用大 O 表示,但我认为它可能看起来像 O(WTFxN)。

迁移开始成为我们生存的祸根,这意味着部署开始变成一种麻烦。没有人希望在他们的部署过程中遇到阻碍,尤其是当我们尝试每天或更频繁地部署时。

数据库资源

我不是 PostgreSQL 内部工作原理的专家,但我们在使用这种租户策略时,需要扩展我们主生产数据库的程度似乎远远超过了使用列范围进行租户的任何其他服务。我猜测有一个上限 —— 如果不是硬性的,至少也是一个软性的、推荐的上限 —— 关于你在一个 Postgres 数据库中存储的表/索引等的数量。我们正在运行一个 RDS r3.4xl,每月成本约为3000 美元,用来容纳一个本可以存在于更小实例上的数据库。我们还没有具体深入这个问题,但我相当确定我们拥有的表的数量是一个问题。

客户端内存膨胀

这一点与 Ruby 特别相关,更具体地说是与 ActiveRecord 相关(但可能与任何具有类似实现的库相关)虽然已经进行了一些修复,但根本原因大部分仍未解决。ActiveRecord 在连接到数据库时,会遍历所有表并存储有关列的元数据,以便正确映射 Postgre s数据类型到 Ruby 数据类型。不幸的是,这种操作是通过遍历所有 schema 中的所有表,然后缓存所找到的内容来完成的。这不必要地增加了运行中客户端的负担,因为所有租户的类型完全相同,但我们无法配置 ActiveRecord 仅通过单一 schema 进行映射。
目前,我们任何一个 ruby 进程连接到数据库的那一刻,它的内存立即增长到大约 500MB。其他拥有类似数据量但不使用基于 schema 的租户管理的服务并没有这个问题。而随着我们向系统中添加的每一个客户(租户/架构),这个问题将会继续恶化。

记录 ID 和识别

将 schema 作为租户的一个主要缺点是,你的序列生成器将在每个租户中独立存在。这意味着,如果你有一个带租户的用户表,你现在有 X 个以 id=1 标识的用户(假设均匀分布,对于每一个生成的序列 id 也是如此)。如果你试图跨这些表进行 join 或对所有这些数据进行全局报告,你将遇到一些冲突。此外,如果你将这些数据复制到其他系统而没有将记录限定在租户 id 内,实际上可能会遇到一些权限问题。

怎么办

上述问题的最终结果使我们基本上放弃了使用单独 schema 的方法来处理多租户问题。对于我们今后构建的所有服务,我们使用了更传统的列作用域方法,并编写了我们自己的包装器,有效地模仿了 Apartment 为我们提供的按请求租户的方法。我们没有开源任何东西,因为实现和我们的场景太耦合了,但关于如何使用您选择的 ORM 实现这种数据隔离,文档资源并不少。

我想以我们的一些建议来结束,这些建议可能会帮助那些已经走了我们走过的路线的人,基于我们所做的进行改变。

选择合适的工具

最初,我们盲目地将所有客户数据放入他们各自的 schema 表中,而没有考虑他们存储的数据类型。如果你发现自己在讨论诸如「事件」、「日志」、「交易」等(即任何暗示高容量写入的东西),考虑使用更合适的工具,如分布式数据库 Citus、Cassandra 等,或者是带有 projection 的事件日志如 Kafka。这可以解决你可能遇到的许多迁移/索引问题。

从一个可信的源头创建租户

当前,Apartment gem 使用 rails 的 schema.rb 来生成新的租户。这是一个错误。当运行 schema 迁移时,这个文件在本地会发生变化,但它确切地代表了那个开发者在其本地数据库中的内容。例如,如果一个功能分支添加了实验性数据库列,这些列可能会在你无意中将 schema.rb 的更改提交到你的主开发分支时悄悄地发布到生产环境中。(这种情况比我们愿意承认的还要多)。这会将那些实验性列添加到下一个被创建的租户中。这就使得不同租户的 schema 完全不同了!如果那个实验性列在你下一次部署时变成了真实存在的列,这将把 schema migration 搞破,因为对那些租户来说,add_column 调用会失败,因为该列已经存在。

使用 UUID

正如上面提到的,使用序列 ID 进行租户化意味着你的系统中的对象没有全局唯一标识符。如果你开始跨租户聚合数据(例如用于报告),或尝试跨租户连接并且只依靠序列ID来进行标识,这将特别麻烦。为此,我们在所有表中添加了 uuid 列,现在只使用序列 ID 进行基于游标的分页。

结论

我希望上述经验教训能为你设计下一个多租户应用程序提供一些洞察。考虑到我们上面看到的各种问题,我现在无法推荐采用 Postgres schema 方法。我希望这篇文章能帮助大家避免我们遇到的一些陷阱,并且还在努力摆脱这些问题。

作者一开始的多租户方案是物理隔离,通过给不同租户单独的 schema。只是后来随着租户的增多,太多的 schema 导致了各种问题。所以后来又回到了给每张表加一个 tenant_id 的路上。但在一些有强制数据合规的场景,比如存不同医院的医疗数据,存不同公司的 HR 数据,物理隔离是必须的,这时也不得不面对采用物理隔离的方案。从作者的复盘中,我们可以看到,如果是采用物理的隔离方案:

  1. 使用 schema 进行隔离会撞到各种表数量的限制,所以使用 database 进行隔离或许是更好的方案。
  2. 缺乏工具对隔离的 schema 进行批量变更。这个时候就可以考虑借助 Bytebase 的批量变更能力,把相同的数据库和数据库表归在一组里,进行批量操作,保证一致性。

file


💡 更多资讯,请关注 Bytebase 公号:Bytebase

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1542904.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

有名的爬虫框架 colly 的特性及2个详细采集案例

一. Colly概述 前言:colly 是 Go 实现的比较有名的一款爬虫框架,而且 Go 在高并发和分布式场景的优势也正是爬虫技术所需要的。它的主要特点是轻量、快速,设计非常优雅,并且分布式的支持也非常简单,易于扩展。 框架简…

javaSSM游泳馆日常管理系统IDEA开发mysql数据库web结构计算机java编程maven项目

一、源码特点 IDEA开发SSM游泳馆日常管理系统是一套完善的完整企业内部系统,结合SSM框架和bootstrap完成本系统,对理解JSP java编程开发语言有帮助系统采用SSM框架(MVC模式开发)MAVEN方式加载,系统具有完整的源代码和…

疫情居家办公OA系统设计与实现| Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW,文末可获取本项目的所有资料。 推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java,…

day04套餐管理模块所有业务功能代码开发

目录 1. 新增套餐1.1 需求分析和设计1.2 代码实现1.2.1 DishController1.2.2 DishService1.2.3 DishServiceImpl1.2.4 DishMapper1.2.5 DishMapper.xml1.2.6 SetmealController1.2.7 SetmealService1.2.8 SetmealServiceImpl1.2.9 SetmealMapper1.2.10 SetmealMapper.xml1.2.11…

shell脚本入门练习(非常详细)零基础入门到精通,收藏这一篇就够了

【脚本1】打印形状 打印等腰三角形、直角三角形、倒直角三角形、菱形 #!/bin/bash \# 等腰三角形 read \-p "Please input the length: " n for i in \seq 1 $n\ do for ((j\$n;j>i;j--)) do echo \-n " " done for m in \seq 1 $i\ do…

希尔伯特-黄变换(Hilbert-Huang Transform, HHT)详解

目录 经验模态分解(EMD) 希尔伯特谱分析(HSA) 定义 连续时信号的Hilbert变换定义 离散时信号的Hilbert变换定义 解析信号定义: 解析信号的傅里叶变换 解析信号的重要意义 解析信号的属性 希尔伯特--黄变换(…

LabVIEW电动汽车直流充电桩监控系统

LabVIEW电动汽车直流充电桩监控系统 随着电动汽车的普及,充电桩的安全运行成为重要议题。通过集成传感器监测、单片机技术与LabVIEW开发平台,设计了一套电动汽车直流充电桩监控系统,能实时监测充电桩的温度、电压和电流,并进行数…

Geohash编码

1. 简介 地理位置(经纬度坐标对)编码为字母数字串,将空间分为网格形状每个网格使用一个编码,是Z阶曲线的众多应用之一。 2. 编码原理 (1) 首先根据区域划分的精度大小选择Geohash的字符串的长度&#xf…

[DDD] ValueObject的一种设计落地及应用

目录 前言一、ValueObject二、设计2.1 接口2.2 单一值ValueObject2.3 单一字符串ValueObject 三、实现3.1 示例3.1.1 PhoneNumber3.1.2 SocialCreditCode 四、使用4.1 异常处理4.2 Json 反/序列化4.2.1 请求体4.2.2 HTTP接口4.2.3 用例 4.3 JPA/MyBatis4.3.1 Converter或TypeHa…

HarmonyOS实战开发-如何使用首选项能力实现一个简单示例。

介绍 本篇Codelab是基于HarmonyOS的首选项能力实现的一个简单示例。实现如下功能: 创建首选项数据文件。将用户输入的水果名称和数量,写入到首选项数据库。读取首选项数据库中的数据。删除首选项数据文件。 最终效果图如下: 相关概念 首选…

第二证券|基本面向好预期强化 全球资本加紧布局A股

开年以来,在我国经济上升向好的态势持续稳固增强的大布景下,结合各方努力,A股商场企稳上升痕迹明显。受一系列稳定商场预期政策出台的加持,全球本钱正在加速布局A股商场。 业界人士指出,当时我国本钱商场依然具有明显…

QT(6.5) cmake构建C++编程,调用python (已更新:2024.3.23晚)

一、注意事项 explicit c中,一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数),承担了两个角色,构造器、类型转换操作符, c提供关键字explicit,阻止转换构造函数进行的隐式转换的发生&#…

jvm底层

逐步细化 静态链接:静态方法(符号引用)替换为内存指针或者句柄直接引用) 动态链接:程序期间将符号引用替换为直接引用 对象头: 指针压缩: -XX:UseCompressedOops 开启指针压缩 减少内存消耗;大指针在主内存 缓存间移…

人脸聚类原理和算法解释

人脸聚类是指将大量人脸图像根据它们的相似性分组到不同的群集中的过程。人脸聚类通常利用人脸的特征向量表示来度量人脸之间的相似性,并将相似的人脸图像聚集在一起。 以下是人脸聚类的一般原理: 人脸特征提取:对每张人脸图像提取特征向量。…

上海市开展专项行动,提升车联网行业网络和数据安全防护水平

近日,上海市通信管理局发布了《关于开展“铸盾车联”2024年车联网网络和数据安全专项行动的通知》。通知中提到,此次专项行动是为了提升本市车联网行业网络和数据安全防护水平,筑牢车联网网络和数据安全防线,护航智能网联汽车产业…

Spring之事务原理篇

(/≧▽≦)/~┴┴ 嗨~我叫小奥 ✨✨✨ 👀👀👀 个人博客:小奥的博客 👍👍👍:个人CSDN ⭐️⭐️⭐️:Github传送门 🍹 本人24应届生一枚,技术和水平有…

opencv各个模块介绍(1)

Core 模块:核心模块,提供了基本的数据结构和功能。 常用的核心函数: cv::Mat:表示多维数组的数据结构,是OpenCV中最常用的类之一,用于存储图像数据和进行矩阵运算。 cv::Scalar:用于表示多通道…

Redis - 高并发场景下的Redis最佳实践_翻过6座大山

文章目录 概述6座大山之_缓存雪崩 (缓存全部失效)缓存雪崩的两种常见场景如何应对缓存雪崩? 6座大山之_缓存穿透(查询不存在的 key)缓存穿透的原因解决方案1. 数据校验2. 缓存空值3. 频控4. 使用布隆过滤器 6座大山之_…

水果检测15种YOLOV8

水果检测15种YOLOV8,只需要OPENCV,采用YOLOV8训练得到PT模型,然后转换成ONNX,OPENCV调用,支持C/PYTHON/ANDROID开发

41 arr.at is not a function

前言 一台机器 获取前端服务1, 一个列表能够展示出来 然后 一台机器 同样获取前端服务1, 这个列表展示不出来 然后 console里面没有任何报错[实际上是有报错, 但是没看到, 需要在vue的js代码里面去调试] 然后 这里面最终出现问题的地方是 Array.at 的使用, 我这边 js引擎版…