多租户SaaS早期经常采用共享数据库、共享表的架构。所有租户的数据放在同一组表中,通过tenant_id区分租户。这种模式部署简单、数据库数量少、运维成本低,但随着租户数量、数据规模和业务复杂度增加,单个数据库中的资源竞争、数据隔离、备份恢复以及故障影响范围都会逐渐成为架构问题。
当企业准备把部分或者全部租户迁移到独立数据库时,真正需要解决的并不是“为每个租户创建一个数据库”这么简单,而是如何让应用准确找到租户对应的数据库,如何保证迁移过程中数据不丢失、不重复,如何在出现问题时回滚,以及迁移完成后如何管理数量可能从几个增加到数百甚至数千个数据库实例。
第一件事是重新定义租户隔离边界
共享表架构通常类似下面的设计:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP NOT NULL
);
迁移到独立数据库后,tenant_id仍然可以保留。它可以继续作为业务层面的租户标识,而数据库本身通过不同连接目标实现物理或逻辑隔离。
这里需要区分几种架构。共享数据库、共享表属于弱隔离;共享数据库、不同Schema属于中间方案;每个租户独立数据库属于更强的隔离方式;如果要求独立计算资源、独立存储甚至独立基础设施,则属于更高等级的资源隔离。
独立数据库并不天然适合所有高并发业务。它真正带来的价值通常是降低租户之间的资源竞争、缩小故障影响范围、满足部分合规要求,以及允许不同租户采用不同的数据库容量和维护策略。数据库数量增加之后,连接管理、升级、备份、监控和故障处理也会同步变得更加复杂。
租户路由必须先设计好
从共享表迁移到独立数据库以后,应用不能再简单执行:
SELECT * FROM users WHERE tenant_id = ?;
而应该首先根据租户身份确定数据库连接,再在目标数据库执行查询。
因此,需要建立类似下面的租户元数据:
CREATE TABLE tenants (
tenant_id BIGINT PRIMARY KEY,
database_cluster VARCHAR(100) NOT NULL,
database_name VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL,
region VARCHAR(50),
created_at TIMESTAMP NOT NULL
);
实际生产系统还可以记录数据库版本、迁移状态、连接端点、容量等级、加密策略以及故障转移信息。
应用层需要有统一的Tenant Router或者Database Resolver。登录用户经过身份认证以后,根据租户ID查询租户元数据,然后获得对应的数据库连接。
这个路由层非常重要,因为以后如果一个租户从共享数据库迁移到独立数据库,应用代码不应该到处修改SQL。理想状态是业务代码只知道“当前租户的数据源”,而数据库位置由基础设施层统一决定。
全局唯一ID不应该轻易取消
原文提出“避免使用全局唯一ID,采用租户内唯一ID”,这一点并不适合迁移场景。
如果数据库独立以后,每个租户都从1开始生成自增ID,那么跨租户数据合并、日志追踪、事件系统、数据仓库同步以及迁移工具都可能遇到ID冲突。
UUID、ULID或者其他全局唯一标识符反而可以降低这种风险。尤其是在数据需要从共享表逐步复制到独立数据库的情况下,全局唯一ID能够让源数据和目标数据更容易进行对应和校验。
如果业务使用整数自增ID,也可以保留租户内自增ID,同时增加全局业务ID,但必须提前确定哪些字段是真正的业务主键,哪些只是数据库内部主键。
真正困难的是迁移过程中的一致性
对于已经运行多年的SaaS系统,数据库可能每天持续产生大量INSERT、UPDATE和DELETE。直接停止业务、复制数据、修改数据库连接再重新上线,虽然简单,但停机时间可能无法接受。
更现实的方法通常是分阶段迁移。
第一阶段先建立目标数据库,并确保Schema、索引、约束、字符集和数据库参数与生产要求一致。
第二阶段进行历史数据复制。
第三阶段处理增量变化。可以采用应用双写、数据库CDC、binlog/WAL解析等方式,把源数据库在复制期间产生的变化持续同步到目标数据库。
第四阶段进行数据校验,包括行数、主键范围、关键字段哈希、关联关系以及业务统计结果。
第五阶段执行租户切换。应用停止向旧数据库写入或者进入极短的切换窗口,完成最后一批增量同步,然后把租户路由切换到新数据库。
最后观察一段时间,在确认目标数据库运行稳定之后,再决定是否删除旧数据。
这里最重要的是不要把“数据复制完成”误认为“迁移完成”。如果复制完成以后源数据库仍然不断产生新的订单、用户、支付记录或者状态变化,那么目标数据库实际上已经落后。
双写必须防止数据分叉
如果采用应用层双写,例如先写旧库再写新库,就必须考虑其中一次成功、另一次失败的情况。
例如订单写入旧数据库成功,但目标数据库连接超时。如果应用简单返回失败,旧库已经产生订单;如果简单返回成功,新库又没有数据,两个数据库就出现分叉。
因此,生产环境通常需要设计可靠的事件记录、重试机制、幂等键和失败补偿,而不是简单执行两条INSERT。
如果条件允许,也可以考虑CDC,让源数据库作为迁移期间的事实来源,再将变化持续复制到目标数据库。
对于关键业务,还应该建立迁移专用的reconciliation程序,定期比较源库和目标库的数据数量以及关键字段,发现差异后自动生成修复任务。
Schema必须能够长期独立演进
迁移之前不能只复制当前数据库结构,还需要检查未来Schema变更。
共享数据库时代,一次ALTER TABLE可能影响所有租户。独立数据库之后,一个Schema变更可能需要在几百个数据库上执行。
因此必须建立Schema版本管理机制。
Doctrine Migrations等工具可以用于Schema迁移,但它解决的是数据库结构版本控制,不等于完整的数据迁移系统。大量历史数据搬迁仍然需要专门的数据迁移程序、CDC或者批处理工具。
更重要的是,数据库升级应该支持向后兼容。例如新版本应用可能先增加一个可为空字段,完成所有数据库升级以后,再由后续版本开始强制使用该字段。这样能够降低滚动升级过程中出现旧应用访问新Schema的风险。
索引也要重新设计
共享表时代通常会使用:
CREATE INDEX idx_tenant_status
ON orders (tenant_id, status);
迁移到独立数据库以后,所有数据已经属于同一个租户,因此很多以tenant_id开头的索引不再有必要。
例如原来的:
(tenant_id, status, created_at)
在独立数据库中可能变成:
(status, created_at)
具体是否需要调整,必须根据实际查询模式和EXPLAIN结果决定,而不是机械删除tenant_id。
与此同时,独立数据库意味着每个数据库中的数据量可能显著下降,一些原来为了应对超大共享表而设计的分区策略也可能不再需要。
不要把连接池和持久连接混为一谈
PHP应用中经常有人提出使用PDO持久连接解决数据库连接数量问题,但PDO并不存在所谓PDO::Pconnect。
PDO可以通过PDO::ATTR_PERSISTENT启用持久连接,但这与Java、Go等环境中常见的成熟连接池并不是完全相同的概念,而且在PHP-FPM等运行模型下,持久连接的行为、连接数量和数据库端资源消耗必须经过实际测试。
当SaaS从一个数据库变成几百个数据库以后,连接管理反而可能成为新的瓶颈。
如果每个PHP工作进程都为多个租户建立独立数据库连接,数据库服务器可能出现大量空闲连接。因此应该重新设计连接生命周期、连接上限、超时、连接复用以及数据库代理层,而不能简单地认为“数据库越多,连接池自动解决问题”。
加密不能简单通过AES-256解决
敏感数据是否加密,需要根据字段性质、威胁模型以及密钥管理方案设计。
AES-256可以作为一种对称加密算法,但真正困难的是密钥放在哪里、谁可以访问、如何轮换、如何吊销以及如何进行审计。
如果应用把数据库密钥和加密数据放在同一个服务器或者同一个配置文件中,加密带来的安全价值会明显下降。
因此生产系统通常需要结合KMS或者HSM进行密钥管理。对于需要搜索的敏感字段,还要特别考虑加密后的查询能力。随机化加密通常不能直接支持普通的LIKE或者精确匹配查询,盲索引、哈希索引等方案也需要权衡泄露风险与查询需求。
备份必须从数据库一致性出发
“每个租户使用LVM快照备份”不能作为通用方案。
LVM快照只是块设备层面的快照机制。数据库正在写入时直接进行文件系统快照,并不自动意味着获得一个可靠的数据库一致性备份。
生产环境需要根据MySQL、PostgreSQL或者其他数据库的机制设计备份。例如全量备份结合binlog或WAL,可以提供时间点恢复能力。对于独立数据库,还需要能够单独恢复某一个租户,而不是恢复整个SaaS平台。
因此迁移前应该明确三个指标:RPO允许丢失多少数据,RTO要求多长时间恢复,以及单租户恢复是否必须独立完成。
监控模式也必须改变
共享数据库时代,监控一个数据库的CPU、内存、连接数、慢查询和磁盘I/O就可以覆盖大量租户。
独立数据库以后,监控系统必须增加租户维度。
至少需要观察数据库连接数、CPU、内存、磁盘空间、I/O延迟、慢查询、锁等待、复制延迟、事务失败率以及应用响应时间。
还应该建立租户级成本指标。例如一个租户占用了整个数据库80%的CPU,那么继续增加数据库容量之前,应该先判断究竟是正常业务增长、低效查询还是异常任务造成的。
成本可能比想象中增长得更快
独立数据库最大的隐性成本通常不是磁盘,而是数据库数量。
100个数据库意味着可能需要100套备份策略、100组监控对象、100套Schema升级任务以及更多数据库连接和故障处理工作。
如果每个租户都配置独立计算资源,成本增长会更加明显。因此实际生产环境可以采用分层模式。
例如,小租户继续共享数据库,中型租户使用独立数据库,大型企业客户使用独立数据库集群,极少数高合规客户再提供独立区域或者独立基础设施。
这种混合模式通常比“一租户一数据库、一租户一服务器”更加容易控制成本。
Kubernetes不能直接解决数据库扩展问题
如果SaaS应用运行在Kubernetes上,可以利用HPA根据CPU、内存或者自定义指标扩展应用Pod,但这并不意味着数据库可以按照相同方式水平扩展。
数据库扩容涉及存储、事务一致性、复制、锁、连接以及数据分片等问题。
因此,迁移到独立数据库以后,应首先解决数据库容量规划和故障转移,再考虑Kubernetes应用层面的弹性扩展。
如果单个租户的数据规模已经达到单数据库的能力边界,才需要进一步研究读写分离、分片、分布式数据库或者数据库集群,而不是因为使用了Kubernetes就自动获得数据库弹性。
真正应该提前准备的是回滚方案
迁移方案最容易忽略的是失败以后怎么办。
如果一个租户切换到新数据库以后发现订单金额错误、部分数据缺失或者第三方接口出现兼容问题,系统能否重新切回旧数据库?
答案不能只依赖“数据库快照”。
如果切换之后新数据库已经产生了新的订单和用户数据,那么简单把应用连接切回旧数据库可能直接造成数据丢失。因此回滚设计必须与双写、CDC、幂等以及切换窗口一起设计。
对于高价值租户,正式迁移前最好先进行完整演练,包括数据复制、增量同步、业务验证、正式切换、故障注入和回滚测试。
最终,SaaS从共享表迁移到独立数据库,本质上不是一次简单的数据库拆分,而是一次数据架构和运维体系的升级。真正需要提前设计的是租户路由、数据库生命周期、Schema版本、数据复制、双写一致性、数据校验、备份恢复、监控、成本和回滚机制。独立数据库确实可以提高隔离性,并降低部分租户之间的资源干扰,但同时也会把原来一个数据库的问题变成几十个、几百个甚至更多数据库的管理问题。成熟的方案通常不是追求绝对隔离,而是根据租户规模、合规要求、性能特征和业务价值建立分层架构,让数据库隔离程度与实际需求匹配。
多租户 SaaS 从共享表迁移到独立数据库,需要提前设计什么
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP