典型场景 | PolarDB-X 如何支撑SaaS多租户?
SaaS多租户背景
很多平台类应用或系统(如电商CRM平台、仓库订单平台等等),它们的服务模型是围绕用户维度(这里的用户维度可以是一个卖家或品牌,可以是一个仓库,等等)展开的。因此,这类型的平台业务,为了支持业务系统的水平扩展性,业务的数据库通常是按用户维度进行水平切分。
可是,当平台类应用的一些用户慢慢成长为大用户(比如大品牌、大卖家、大仓库等)后,这些大用户由于其数据量或流量明显要比其它用户多得多,容易导致以下的现象
- 大用户所在分片会成为业务系统的热点,占用大量的数据库资源,其服务质量容易因资源受限导致不稳定;
- 其它小用户容易受到大用户资源消耗的影响,服务质量也受到影响。
最后就整个平台的业务系统的热点频现,数据库访问不稳定,业务服务受影响。
SaaS多租户模型作为一种应用的架构,常用来解决业务的上述问题。在SaaS多租户模型中,业务系统会需要服务多个用户,每个用户(或每批用户)可以被视为一个租户。
这些不同的租户在业务系统内会使用共同的基础设施及其平台进行运行,但来自不同租户的数据仍将被独立隔离,因此,通常租户拥有自己物理资源来单独存储与管理数据。所以,SaaS多租户解决业务系统稳定性问题以及租户资源弹性定制的核心思路,就是租户间的资源隔离及数据隔离。
在实际的不同应用场景下,常见的 SaaS 多租户方案有两种:
- Schema 级 SaaS 多租户
- Schema 级 SaaS 多租户,是指一个租户对应一个包含多个Table定义的Schema(或一个Database,在MySQL, Schema概念等同Database), 不同租户的Schema会分布在不同的机器上(如下图1所示),实现资源隔离,该方案适用于不同租户需要使用独立Schema运行的场景;
图1 图2
- Partition 级 SaaS 多租户
- Partition 级 SaaS 多租户,是指一个租户会对应一个Table的一个或多个的分区(或是一个Table的一部分 rows),不同租户的Parittion会分布在不同的机器上(如上图2所示),以实现资源隔离,该方案比较适用于不同租户需要使用统一Schema运行的场景。
从隔离程度来看, Schema 级 SaaS 多租户比 Partition 级 SaaS 多租户要隔离得更彻底,但前者因为要维护众多的Schema,会比后者会带来更高的运维成本及查询分析成本。
不过,Partition 级 SaaS 多租户通常要依赖中间件分库分表或分布式数据库分区功能(不然单机数据库无法做到资源隔离)才能运作,而Schema 级 SaaS 多租户则不需要,用户自己搭建几个单机MySQL也可以运作起来,准入门槛更低。
业务的问题
业务多租户场景
只说应用架构可能有些抽象,为了方便读者更容易地理解 SaaS 多租户是如何帮助业务解决问题, 本文将以一个真实的案例来进行阐述。
正马软件的班牛平台是国内领先的提供电商全周期客户服务的卖家订单管理平台(以下简称B公司)。它的业务系统需要维护多个不同品牌的众多卖家。通常一个品牌会有多个卖家(比如,一个品牌可能会开通多个线上店铺),所以,品牌与卖家是一对多的关系。
目前,B公司的订单管理平台管理着超过50T的订单数据,日QPS 近 3W+,不同品牌的订单量差异会比较大(大品牌的订单可能是小品牌的订单量的近百倍或更高)。 一些大品牌除了订单量比其它品牌的大很多之外,还会使用更高级的付费VIP服务:比如,要求订单数据独占资源与数据隔离、允许独立地统计分析自己品牌的订单数据等。
B公司为了解决不同品牌的数据的资源使用及其服务差异,就会对它的卖家按品牌划分(相当于一个品牌是一个租户):
- 大品牌诉求:
- 订单量大(比如,订单数据存储的大小超过 1T 或 2T ),数据存储量大
- 独占一组的存储资源、有独立访问分析数据的需求
- 该品牌的所有商家都必须同一组的存储资源
- 大品牌的大卖家150+,后边还会陆续增加
- 小品牌诉求:
- 订单表小,商家数目大(6W+卖家)
- 共用一组存储资源
- 要求所有卖家数据在存储上均衡分布
。现在的核心问题是,B公司的订单管理平台的数据库应该如何设计,才能满足上述众多不同品牌及其大卖家对于不同资源使用与数据隔离的诉求?
普通中间件方案及其问题
对于上述的业务场景,B公司若不使用分布式数据库,而是简单通过单机MySQL及一些开源的分库分表中间件,自己搭建一套SaaS多租户方案(比如,将品牌及其卖家切分为租户),进行租户的资源隔离。表面上,这貌似可行;但实际上,业务会因此要面临更多更为棘手的问题。
首先是跨机分布式事务问题。 绝大多数的分库分表中间件无法提供强一致分布式事务能力,或者只能提供基于最终一致性的事务补偿方案,这意味着业务需要做很多额外的应用改造成本,才能尽量来避免跨机事务导致业务出现报错。
然后是 Schema 一致性问题。 基于中间件分库分表,无论是采用 Schema 级多租户及是 Partition 级多租户,B公司的订单平台都要面临自己维护各个租户的 Schema 或 Table 的元数据一致性。比如,MySQL的建删表、加减列、加减索引等常见的DDL操作,中间件的方案无显然法保证平台所有租户的表能同时生效,一旦执行中断,必须靠人工介入来订正,人力成本高。
接着是租户的数据迁移问题。基于 SaaS多租户方案,B公司若要给一个大品牌分配新的独立资源,这自然免不了将租户数据从原来机器到新机器的数据迁移。这个数据迁移操作只能依赖额外的同步工具构建同步链路才能完成,这中间切割过程甚至还需要业务停机。这意味,业务执行添加一个新租户这一基本操作,也会带来非常高昂的运维成本。
综合上述的分析,B公司直接基于单机MySQL及一些中间件的 SaaS 多租户方案,并不是一个成本低廉的方案。
SaaS多租户PolarDB-X方案
事实上,在分布式数据库 PolarDB-X 2.0 中,B公司已经可以通过结合非模板化二级分区 与 Locality 两项能力,来很好的解决其上述业务所面临的问题。为了方便读者更易理解,以下先简单介绍下 PolarDB-X 2.0 的非模板化二级分区 与 Locality 两项的功能。
非模板化二级分区
PolarDB-X 从 5.4.17 开始支持使用二级分区创建分区表。与其它分布式数据库所有不同(比如 TiDB/ CockroachDB 等等),PolarDB-X 的二级分区除了语法能完全兼容原生 MySQL 二级分区语法外,还额外扩展很多的二级分区的能力,比如,支持用户定义非模板化二级分区(原生 MySQL 只支持模板化二级分区)。
所谓的非模板化二级分区,就是各个一级分区之下的二级分的分区数目及其边界值定义允许不一致,如下所示:

/* 一级分区 LIST COLUMNS + 二级分区HASH分区 的非模板化组合分区 */
CREATE TABLE t_order /* 订单表 */ (
id bigint not null auto_increment,
sellerId bigint not null,
buyerId bigint not null,
primary key(id)
)
PARTITION BY LIST(sellerId/*卖家ID*/) /* */
SUBPARTITION BY HASH(sellerId)
(
PARTITION pa VALUES IN (108,109)
SUBPARTITIONS 1 /* 一级分区 pa 之下有1个哈希分区, 保存大品牌 a 所有卖家数据 */,
PARTITION pb VALUES IN (208,209)
SUBPARTITIONS 1 /* 一级分区 pb 之下有1个哈希分区, 保存大品牌 b 所有卖家数据 */,
PARTITION pc VALUES IN (308,309,310)
SUBPARTITIONS 2 /* 一级分区 pc 之下有2个哈希分区, 保存大品牌 c 所有卖家数据 */,
PARTITION pDefault VALUES IN (DEFAULT)
SUBPARTITIONS 64 /* 一级分区 pDefault 之下有64个哈希分区, 众多小品牌的卖家数据 */
);基于上述的 LIST+HASH 非模板化二级分区,它能给应用直接带来的的效果是:
- 对于大品牌的卖家(相当一个租户),可以将数据路由到单独的一组分区;
- 对于中小品牌,可以将数据按哈希算法自动均衡到多个不同分区,从而避免访问热点。
当大品牌与中小品牌的商家数据按LIST分区实现了分区级的隔离后,那实现大品牌与中小品牌的存储资源的物理隔离也就自然而言的事了。在 PolarDB-X 2.0 中,用户可以借助 Locality 的能力,很容易地实现不同分区之间的资源隔离。
LOCALITY 资源绑定
PolarDB-X 支持通过 LOCALITY 关键字来指定数据库分区的实际存储资源位置(PolarDB-X中存储资源由多个数据节点(DN节点)组成,可以通过DN的ID进行位置分配),以实现数据隔离或数据的均匀分布。它的具体语法如下所示:
ALTER TABLE #tableName
MODIFY (SUB)PARTITION #(sub)partName
SET LOCALITY='dn=dn1[, dn2,...]'例如,B公司可以使用以下的SQL命令将 t_order 中的大品牌 pa 的数据全部单独挪到一个存储节点 dn4 :
ALTER TABLE t_order MODIFY PARTITION pa SET LOCALITY='dn=dn4'在实际使用中,用户可以通过 SHOW STORAGE 查询 PolarDB-X 的所有DN节点实例ID列表,例如:
mysql> show storage;
+----------------------------+----------------+------------+-----------+----------+-------------+--------+-----------+------------+--------+
| STORAGE_INST_ID | LEADER_NODE | IS_HEALTHY | INST_KIND | DB_COUNT | GROUP_COUNT | STATUS | DELETABLE | DELAY | ACTIVE |
+----------------------------+----------------+------------+-----------+----------+-------------+--------+-----------+------------+--------+
| polardbx-storage-0-master | 10.0.x.1:3306 | true | MASTER | 41 | 66 | 0 | false | null | null |
| polardbx-storage-1-master | 10.0.x.1:3307 | true | MASTER | 41 | 53 | 0 | true | null | null |
| ...... | ...... | true | META_DB | 2 | 2 | 0 | false | null | null |
+----------------------------+----------------+------------+-----------+----------+-------------+--------+-----------+------------+--------+设计 SaaS 多租户方案
回到之前B公司的例子,B公司的核心需求是要实现大品牌与中小品牌的卖家数据及其存储资源的隔离。那么,B公司可以在上述的二级分区的分区表的基础上,通过再给每个一级分区增加对应的 LOCALITY 定义,以指定一级分区及其所有二级分区所允许使用的存储资源,那么业务就可以在建表阶段直接实现SaaS层多租户(即品牌方)存储资源的隔离,如下所示:

/* 一级分区:list columns,二级分区:key 的非模板化组合分区 */
CREATE TABLE t_orders /* 订单表 */ (
id bigint not null auto_increment,
sellerId bigint not null,
buyerId bigint not null,
primary key(id)
)
PARTITION BY LIST(sellerId /* 卖家ID */ )
SUBPARTITION BY HASH(sellerId)
(
PARTITION pa VALUES IN (108,109,....)
LOCALITY='dn=dn16' /* 大品牌 pa 独占一个DN dn4 */
SUBPARTITIONS 1,
PARTITION pb VALUES IN (208,209,....)
LOCALITY='dn=dn17' /* 大品牌 pb 独占一个DN dn5 */
SUBPARTITIONS 1 ,
PARTITION pc VALUES IN (308,309,310,...)
LOCALITY='dn=dn18,dn19' /* 大品牌 pc 独占两个DN: dn6 与 dn7 */
SUBPARTITIONS 2,
PARTITION pDefault VALUES IN (DEFAULT)
/* 一级分区 pDefault 占用 dn0 ~ dn15 共16个DN, 中小品牌共享 */
LOCALITY='dn=dn0,dn1,...,dn2,dn15'
SUBPARTITIONS 64
);。如上图所示,通过 Locality 对各个一级分区的DN节点资源的绑定,pa、pb、pc这3个大品牌的租户被分别配了DN16、DN17 与 DN18~DN19 3组的节点资源,而中小卖家池的 pDefault 分区则被绑定了16个DN节点。




