存储池

本文介绍存储资源池的设计思路与使用方法。

版本限制

  • 实例内核版本需在5.4.18-17066805以上。

  • 逻辑库的分区类型需为Auto模式。

说明

如何查看实例版本,请参见查看实例版本

背景介绍

在平台类应用和系统(如电商CRM平台、仓库订单平台等)中通常需要服务多个用户,服务模型往往是围绕用户维度(用户维度可以是一个卖家或品牌,可以是一个仓库)展开的,为了支持业务系统的水平扩展性,业务数据库通常是按用户维度进行水平切分。

随着业务不断演进,平台中的部分用户往往会有随着服务请求量增多成为热点用户,或者要求单独服务质量作为VIP用户。除平台业务应用层需要对此类需求进行适配外,业务数据库也需要提供相应的能力,确保来自不同租户的数据存储和计算做到服务分级和资源隔离。

就业务系统对数据库的切分方式而言,常见的租户方案有两种:

  • Schema级多租户。多个租户对应的数据分布在不同的数据库上,不同租户需要独立Schema运行。

  • Partition级多租户。多个租户对应的数据分布在同一张逻辑表的不同分区上,不同的租户需要共享Schema。

image.png

image.png

从隔离程度来看, Schema 级多租户比 Partition 级多租户要隔离得更彻底,但前者因为要维护众多的Schema,会比后者会带来更高的运维成本及查询分析成本。Partition 级多租户通常要依赖中间件分库分表或分布式数据库分区功能(否则单机数据库无法做到资源隔离)才能运作,而Schema 级 SaaS 多租户则不需要,用户可基于几个单机RDS实例搭建应用,准入门槛更低。

传统的多租户解决方案存在以下问题:

  • 跨机分布式事务问题:绝大部分分布式中间件无法提供强一致的分布式事务能力,业务需要进行额外的应用改造。

  • 数据运维问题:基于中间件的方案,无论是Schema级还是Partition级租户,用户均须自行处理数据运维问题,如加减列或者索引、增删表等。

  • 租户资源变更管理问题:当多个租户的资源级别定义发生变动时,如平台需要给某一租户分配单独资源时,数据迁移必须依赖外部同步工具构建同步链路,带来昂贵的运维成本。

PolarDB具备完善的分区定义、分区变更等等分区管理能力以及分布式事务、分布式DDL能力。基于以上基础能力,PolarDB在内核集成了原生的存储池功能,提供了面向SaaS用户的资源划分、资源重组、数据库对象(数据库级与分区级)资源绑定、数据库对象资源重定义能力,可有效解决多租户平台应用在跨机分布式事务、数据运维、租户资源管理等等方面的问题。

基本设计

存储资源池将存储节点划分为若干资源池,数据库中的对象(数据库、表、分区)通过关联到不同的资源池隔离存储资源:

  • 对存储资源提供全局完整的划分,并基于已有的划分方式提供完整的数据库对象绑定语义。

  • 在实例变更过程中遵守自定义的划分方式,提供合理的变更和负载均衡功能。

用户可以根据PolarDB分布式存储节点(以下简称DN节点)的规格、可用区、机房位置等性质对存储节点进行人工分组,并将数据库对象绑定到不同的资源池中,从而实现为多租户提供服务隔离的目标。

存储资源池具备以下功能:

  • 支持将实例中的存储节点划分为若干互不相交的资源池。

  • 支持在定义数据库对象时指定资源池属性:允许定义数据库对象时指定资源池,在选择目标拓扑时必须满足用户指定的资源池。

  • 支持变更数据库对象的资源池属性:允许变更表组和分区组的资源池属性,并自动触发分区迁移操作。

  • 支持变更资源池具备的存储节点集合:允许变更存储资源池关联的节点集合,并自动触发分区迁移操作。

image.png

语义与接口

定义存储资源池

通过information_schema.storage_pool_info视图可以查看存储池的定义。

系统中默认具有两个基本存储池:_default_recycle存储池,前者包含当前的所有节点,后者包含有存储池中删除但仍属于本实例的节点。

mysql > select * from information_schema.storage_pool_info;
+------+----------+-----------------------------------------------------+-----------------+---------------------------+--------+---------------------+---------------------+
| ID   | NAME     | DN_ID_LIST                                          | IDLE_DN_ID_LIST | UNDELETABLE_DN_ID         | EXTRAS | GMT_CREATED         | GMT_MODIFIED        |
+------+----------+-----------------------------------------------------+-----------------+---------------------------+--------+---------------------+---------------------+
|    1 | _default | dn0,dn1,dn2,dn3,dn4,dn5,dn6,dn7,dn8,dn9             |                 | dn0                       | NULL   | 2024-04-15 19:46:05 | 2024-04-15 19:46:05 |
|    2 | _recycle |                                                     |                 |                           | NULL   | 2024-04-15 19:46:05 | 2024-04-15 19:46:05 |
+------+----------+-----------------------------------------------------+-----------------+---------------------------+--------+---------------------+---------------------+

对_default存储池缩容后,可将dn节点放入_recycle存储池中(数据迁移是异步的):

mysql > alter storage pool _default drain node 'dn1,dn2,dn3,dn4,dn5,dn6,dn7,dn8,dn9';
Query OK, 0 rows affected (1.52 sec)

mysql > select * from information_schema.storage_pool_info;
+------+----------+-----------------------------------------------------+-----------------+---------------------------+--------+---------------------+---------------------+
| ID   | NAME     | DN_ID_LIST                                          | IDLE_DN_ID_LIST | UNDELETABLE_DN_ID         | EXTRAS | GMT_CREATED         | GMT_MODIFIED        |
+------+----------+-----------------------------------------------------+-----------------+---------------------------+--------+---------------------+---------------------+
|    1 | _default | dn0,                                                |                 | dn0                       | NULL   | 2024-04-15 19:46:05 | 2024-04-15 19:46:05 |
|    2 | _recycle | dn1,dn2,dn3,dn4,dn5,dn6,dn7,dn8,dn9                 |                 |                           | NULL   | 2024-04-15 19:46:05 | 2024-04-15 19:46:05 |
+------+----------+-----------------------------------------------------+-----------------+---------------------------+--------+---------------------+---------------------+

用户可自定义存储资源池,要求:

  • 每一个存储资源池至少包含一个DN,且有一个DN作为undeletable DN不允许被缩容。

  • 在定义存储资源池时要求指定的DN上必须不包含任何数据库对象

  • 不同资源池的DN之间必须互不交叉。

  • 同一个存储资源池内允许包含不同规格的DN节点。

create storage pool pool1 dn_list = "dn1,dn2,dn3" undeletable_dn="dn1";
create storage pool pool2 dn_list = "dn4,dn5,dn6" undeletable_dn="dn5";
create storage pool pool3 dn_list = "dn7,dn8,dn9" undeletable_dn="dn7";
  1. 对于数据库和表,允许指定多个存储资源池(storage_pools),并单独指定一个默认存储资源池列表(primary_storage_pool),一个完整的存储池绑定声明如下:

LOCALITY = 'storage_pools=pool1,pool2,...;primary_storage_pool=pool1'
  1. 数据库(表)中的表(分区)默认只使用默认资源池,但是数据库中的广播表将会建立在所有DN上。仅当用户手工指定下一级对象的存储资源池为非默认资源池时,数据库对象才会分布在非默认资源池上。

  2. 表(分区)的存储资源池,必须从属于上一级对象,即数据库(表)所具备的存储资源池(storage_pools)。

create database d1 locality = "storage_pools=pool1";
use d1;
-- valid operation!
create table t1(a int) single locality = "storage_pools=pool1";

-- invalid operation, because t2 locality storage_pools not in d1 storage_pools
create table t2(a int) single locality = "storage_pools=pool3"; 


create database d2 locality = "storage_pools=pool1,pool2,pool3;primary_storage_pool=pool1,pool2";
use d2;
-- valid operation!
create table t1(a int) locality = "storage_pools=pool1"; 
create table t2(a int) locality = "storage_pools=pool3";
create table t3(a int) locality = "storage_pools=pool2";
create table t4(a int) locality = "storage_pools=pool2";
-- all the logical table locate on storage pool pool1,pool2 defaultly;
-- while broadcast table locate on all the storage pool

变更存储资源池

  1. 对于已有的存储资源池,支持删除其节点,删除节点的存储池变更为_recycle存储池,但undeletable DN无法被删除。

  2. 对于已有的存储资源池,支持增加其节点,要求增加的节点必须不包含已有的。

-- valid operation
alter storage pool pool1 remove node "dn3";
-- invalid operation, because dn1 is the undeletable storage pool for pool1.
alter storage pool pool1 remove node "dn1";


-- valid operation, because dn4 is in _recycle storage pool
-- and no physical db locate on dn4.
alter storage pool pool2 append node "dn3";
-- invalid operation, because dn3 is hold by pool1.
alter storage pool pool2 append node "dn2";

对存储池的变更将会触发数据自动迁移到合法定义的节点上,可在select * from information_schema.ddl_plan中查看对应的迁移计划。

变更数据库对象关联的存储资源池

对于数据库、表或者分区,允许直接变更存储资源池。

  • 如果对对象关联的存储资源池进行追加,除扩增广播表的节点范围外,不作任何操作。

  • 如果对对象关联的存储资源池进行删减,则消除该对象及子对象在被删除资源池上全部对象的存储资源池定义,并将其迁移。

  • 如果对对象关联的存储资源池完全清除,等同于该对象关联的资源池变更为默认资源池。

alter database d1 set locality = "storage_pools=sp1;primary_storage_pool=sp1";


alter database d2 set locality = "storage_pools=sp1,sp2;primary_storage_pool=sp1"


use d1;

alter table t1 single locality = "storage_pools=sp1";

场景示例

场景一:库级租户

以一个电商平台(以下简称X公司)为例,其系统需要维护多个卖家的订单,其中包含有若干流水显著较高的大卖家和大部分小卖家,X公司对于大小卖家的资源划分要求如下:

  • 大卖家需要使用单独的存储资源存储数据,确保在线流量稳定,同时需要提供单独的跑批数据分析能力。

  • 小卖家需要共享一组存储资源,并且小卖家占用的资源需要尽可能在该组资源中保持均衡。

  • 部分小卖家随着时间演进可能演变为大卖家,部分大卖家可能迁移到其他平台或者从其他平台迁入。

租户定义

X公司的需求可以通过库级资源隔离实现,然后按大卖家的需求,将已有的存储节点划分为若干存储池,对于小卖家的共享库,可直接建在默认存储池之上:

CREATE STORAGE POOL sp1 dn_list="dn4,dn5,dn6" undeletable_dn="dn4";

CREATE STORAGE POOL sp2 dn_list="dn7,dn8,dn9" undeletable_dn="dn7";

CREATE DATABASE orders_comm MODE = "auto"
      LOCALITY= "storage_pools='_default'"; /* 小卖家共享_default存储池(dn1, dn2, dn3 */

CREATE DATABASE orders_seller1 MODE = "auto"
      LOCALITY= "storage_pools='sp1'";   /* 大卖家1使用sp1存储池(dn4, dn5, dn6) */
CREATE DATABASE orders_seller2 MODE = "auto"
      LOCALITY= "storage_pools='sp2'";   /* 大卖家2使用sp2存储池(dn7, dn8, dn9) */
...

通过上述建库语句,应用可获得以下效果:

  • 大卖家之间、大卖家与小卖家之间各自使用单独存储资源存储,存储资源完全隔离。

  • 小卖家之间共享默认存储池的资源dn1~dn3。

  • 大小卖家之间的建表语句可以完全独立,可以自行决定每个卖家的表结构定义以及存储资源。

资源均衡

小卖家的库内部可以使用Locality的单表打散模式全部建成单表,也可以建成分区表。当小卖家之间也存在数据分布的差异,需要在小卖家内部均衡资源分配时,只需直接调用存储池级别的资源均衡语句。

REBALANCE TENANT "_default" POLICY="data_balance";

REBALANCE将会自动按照存储池内的对象属性定义均衡相应的单表和分区分布,与实例级别的扩缩容一样,该操作将自动满足资源约束,并且尽可能均衡所有单表和所有分区在dn1~dn3上的分布。

租户迁入及迁出

当平台迁入新的大卖家时,可在实例中加入新的存储节点(默认加入到_recycle存储池),然后使用该节点单独建立存储池,并新建大卖家相关的库使之独占新的存储池。

CREATE STORAGE POOL spn dn_list = "dn10,dn11,dn12" undeletable_dn="dn10";
/* 新定义存储池spn (dn10, dn11, dn12) */

CREATE DATABASE orders_sellern MODE = "auto"
      LOCALITY="storage_pools='spn'";
/* 大卖家N使用spn存储池 */

如果大卖家迁出,可删除相应库并且释放相应资源。

DROP DATABASE orders_sellern; 

DELETE STORAGE POOL spn;/* 删除存储池,相应的节点将会自动进入_recycle存储池 */

ALTER STORAGE POOL _recycle DRAIN NODE "dn7, dn8, dn9"; /* 在集群中释放相应的节点 */

在新增以及删除上述存储池和库时,已有的业务不会受到影响,对应用几乎是透明无感的。

场景二:分区级租户

对于场景一中X公司的需求,也可以通过分区级资源隔离实现,相比于库级租户,分区级租户可以提供完全一致的资源隔离能力,并且具备更多优势:

  • 所有分区共享逻辑表的定义,加减列、加减索引等数据运维操作以及广播表的下推关系均由数据库自动维护。

  • 通过分区分裂、分区合并等已有的分区管理能力,可以获得更加灵活的资源变更能力,如将部分租户升级到VIP服务、合并部分租户的资源等等。

租户定义

首先将存储节点划分为相应的存储池,然后使用全部存储池建立一个公共库。

CREATE DATABASE orders_db MODE = "auto" 
       LOCALITY = "storage_pools='_default,sp1,sp2,...',primary_storage_pool='_default'"

USE orders_db;

CREATE TABLE commodity(
  commodity_id int,
  commodity_name varchar(64)
) BROADCAST;
/*commodity作为一张广播表,将会在所有的存储池上建立分表,因此可与orders_sellers的任何一个分区进行join下推。*/

CREATE TABLE orders_sellers(
 order_id int AUTO_INCREMENT primary key,
 customer_id int,
 commodity_id int,
 country varchar(64),
 seller_id int,
 order_time datetime not null)
partition BY list(seller_id)
(
  partition p1 VALUES IN (1, 2, 3, 4),
  partition p2 VALUES IN (5, 6, 7, 8),
  /*orders_seller的分区p1, p2默认会使用_default存储池中的节点进行存储,共享_default存储池中的资源。*/
  ...
  partition pn VALUES IN (k) LOCALITY = "storage_pools='sp1'",
  partition pn+1 VALUES IN (k+1, k+2) LOCALITY = "storage_pools='sp2'",
  /*orders_seller的分区pn, pn+1会分别使用sp1, sp2存储池中的节点进行存储,分别独自占用相应存储节点的资源。*/
  ...
) LOCALITY = "storage_pools='_default,sp1,sp2,...'";
  • 库的定义中包含了全部存储池,但是同时定义了primary_storage_pool为_default存储池,因而orders_sellers中的分区默认会使用_default存储池作为存储节点列表,而广播表会自动分布在所有存储池节点上。

  • 不同分区的定义包含不同的租户集合,并且可以自行指定分区的存储资源。

租户迁入和迁出

对于新增的小租户,可以直接添加分区,默认不迁移数据,只产生新分区。

ALTER TABLE orders_sellers ADD PARTITION 
(PARTITION p3 values in (32, 33));

对于新增的大租户,可向已有的存储节点中添加节点,并且加入到分区路由直接添加分区,同时指定其占用的存储池。

CREATE STORAGE POOL spn dn_list = "dn10,dn11,dn12" undeletable_dn="dn10";
/*向实例中新增了一个存储池spn.*/

ALTER DATABASE orders_db SET LOCALITY = 
  "storage_pools='_default,sp1,sp2,...,spn',primary_storage_pool='_default'"
/*向orders_db的存储池列表中添加spn, 这一修改将会自动触发广播表迁移到新的存储池节点之上。*/

ALTER TABLE orders_sellers ADD PARTITION 
(PARTITION p4 values in (34) LOCALITY="storage_pools='spn');
/*在orders_sellers中新增一个租户分区,并且该分区分布在自定义的存储池spn上。*/

同样,对于租户迁出,可直接删除相应分区并释放存储池资源。

ALTER TABLE orders_sellers DROP PARTITION p4;

或者变更相应的分区定义,在分区内部删除小卖家。

ALTER TABLE orders_sellers MODIFY PARTITION p1 DROP VALUES (4, 5);

对分区级租户而言,所有分区共享逻辑表的定义,因而数据模式变更可由数据库统一维护。上述分区变更语句均为online操作,仅影响实际操作涉及的分区,对其他分区的已有业务均无影响。

租户服务级别变更

当小卖家的服务级别需要升级为VIP级别时,可修改分区定义,将其单独分配到一个存储池上:

ALTER TABLE orders_sellers SPLIT PARTITION p2 INTO 
(PARTITION p2 VALUES in (6, 7, 8) ,
PARTITION `pn+2` VALUES in (5) LOCALITY = "storage_pools='spn+3'");

同样,可以根据对大卖家的服务级别进行降级。

ALTER TABLE orders_sellers MERGE PARTITION p1, pn TO p1 LOCALITY="";

上述操作的效果将直接合并p1和pn分区的数据,并且新分区仍然存储于默认存储池之上。

在保留库级租户资源隔离的能力的基础上,分区级租户通过完善的分区管理接口提供了更加灵活的租户资源变更和控制能力。

资源均衡

当租户间在分区级别出现负载不均时,同样可以通过存储池级别的REBALANCE操作在公共存储池中进行负载均衡。

REBALANCE TENANT "_default" POLICY="data_balance";

场景三:单机到分布式的迁移演进

以一个从单机关系型数据库到分布式数据库的迁移过程为例,在迁移初期,用户希望尽可能复用原有的使用模式,在享有分布式数据库scale out能力的同时,尽可能做到无痛改造;随着业务逐步发展,以表为单位逐步推动部分业务负载较大的单表进一步演进到分布式表,同时确保尽可能不影响已有业务的库表模型和资源占用。

通过存储池定义不同表的存储资源,即可实现单表与分布式表共存的集中分布式一体化使用模式,帮助用户平滑演进到分布式数据库。

建库建表

除少数具有广播表语义的表外,用户可使用单表打散模式在PolarDB-X中复用原有的单机建表语句,从而实现存量业务的一键迁移改造。

CREATE DATABASE orders_db MODE = "auto" DEFAULT_SINGLE=on 
    LOCALITY = "storage_pools='_default',primary_storage_pool='_default'";

use orders_db;

CREATE TABLE orders_region1(
 order_id int AUTO_INCREMENT primary key,
 customer_id int,
 country varchar(64),
 city int,
 order_time datetime not null);
CREATE TABLE orders_region2(
 order_id int AUTO_INCREMENT primary key,
 customer_id int,
 country varchar(64),
 city int,
 order_time datetime not null);
CREATE TABLE orders_region3(
 order_id int AUTO_INCREMENT primary key,
 customer_id int,
 country varchar(64),
 city int,
 order_time datetime not null);
CREATE TABLE commodity(
  commodity_id int,
  commodity_name varchar(64)
) BROADCAST;

上述建库和建表语句实现了两个效果:

  • 所有应用单机建表语句的单表将会自动按照数据量和表的数量自动在_default存储池中均衡。

  • 广播表将在_default存储池的所有节点上建立分表。

隔离部分单表

部分单表承载的数据量和访问量过大,可以在实例中添加新的存储节点,并将负载过大的单表单独隔离到新的节点上:

ALTER DATABASE orders_db set LOCALITY = "storage_pools='_default,sp1',primary_storage_pool='_default'";
/* 对数据库的变更将会自动处理广播表的分表扩张行为。 */

ALTER TABLE orders_region_single1 single LOCALITY = "storage_pools='sp1'";
/* 将业务负载较大的单表隔离到sp1存储池的节点上。*/

分布式改造

随着业务演进,用户可将超出单个物理表负载限制的表改造为分布式表,并且指定其所使用的存储节点:

ALTER DATABASE orders_db set LOCALITY = "storage_pools='_default,sp1,sp2,sp3',primary_storage_pool='_default'";


ALTER TABLE orders_region_single2 partition by hash(order_id) partitions 16
  LOCALITY = "storage_pools='sp2'";
/* orders_region_single2 使用存储池sp2 */

ALTER TABLE orders_region_single3 partition by hash(order_id) partitions 16
  LOCALITY = "storage_pools='sp3'";
/* orders_region_single3 使用存储池sp3 */

拆分变更允许用户以online执行的方式自定义目标表的分区方式和存储资源,从而做到表级别的业务模型改造,帮助用户无痛实现从单表到分布式表的演进。

操作步骤

如何创建自定义存储池并管理存储池内的数据节点,请参见数据节点管理