在构建多租户(如SaaS)应用时,部分热点租户因其巨大的数据量和高查询负载,可能会过度消耗其所在节点的计算和存储资源。这会导致同一节点上的其他租户遭遇性能瓶颈,即嘈杂邻居问题。为解决此问题,PolarDB PostgreSQL分布式版提供了租户隔离功能。该功能允许您将特定租户的数据在线迁移至独立的物理节点,为其提供专属资源,从而保障集群整体的性能和稳定性。
功能简介
在PolarDB PostgreSQL分布式版集群中构建多租户应用时,通常是使用租户ID(如tenant_id)作为分布列。该模式通过分片剪枝可以高效地处理大部分针对单个租户的查询,确保其数据访问聚合在单一分片上。
然而,在实际应用中,租户的规模往往是不均衡的。系统通过哈希算法将所有租户(无论大小)的分片均匀地分散到所有数据节点上,这意味着:一个物理节点上必然会同时承载多个租户的数据。
当集群中出现热点租户(通常具有海量数据和高访问负载)时,这种共享资源的模式就会暴露其弊端,主要体现在:
资源争用与邻居效应:热点租户的高频查询会过度消耗其所在节点的CPU、内存和I/O资源,导致同一节点上的其他邻居租户查询性能显著下降。
存储与负载失衡:集群的存储空间和计算负载会向少数几个承载着热点租户的节点倾斜,导致资源利用不均。
索引效率降低(特定场景):在向量搜索等场景下,一个分片内如果绝大多数数据都属于某个热点租户,会导致分片过大,从而降低HNSW等索引的扫描效率,影响所有使用该分片的租户。
为解决热点租户带来的上述问题,PolarDB PostgreSQL分布式版提供了租户隔离功能。其核心原理是将指定租户的数据从共享的物理分片中剥离,并迁移至一个专属的物理节点。整个过程分为逻辑隔离和物理迁移两个阶段,均为在线操作,对业务无中断影响:
逻辑隔离:首先,系统会为指定租户创建一个新的、只包含该租户数据的逻辑分片。此时,该租户的数据在逻辑上已被分离,但物理上仍位于原数据节点。
物理迁移:然后,您可以将这个新创建的专属分片迁移至集群中资源更充裕的另一个数据节点,完成物理层面的资源隔离。
优势
资源保障与隔离:为高负载的热点租户提供专用的节点资源(CPU、内存、I/O),消除其对其他租户的性能影响。
提升性能稳定性:避免因资源争抢导致的中小租户查询性能抖动,保障绝大多数用户的服务体验。
均衡集群负载:通过合理调配租户的数据分布,使集群中各数据节点的存储和计算负载更加均衡。
优化特定查询:对于向量检索等场景,隔离后的分片更小、数据更纯粹,有助于提升索引扫描效率。
注意事项
整个隔离和迁移过程均为在线操作,不会阻塞对目标表的读写请求(DML/DQL)。
创建隔离分片(
isolate_tenant_to_new_shard)时,如果存在与目标表同一亲和组的其他表,则需使用CASCADE选项,否则租户隔离操作将无法进行。分片迁移是一项消耗资源的后台任务,建议在业务低峰期执行,并关注集群负载。
操作步骤
以下操作为您演示如何隔离热点租户。
数据准备
创建一个名为multi_tenant_orders的分布表为例,其分布列为tenant_id。
创建一张普通表
multi_tenant_orders。CREATE TABLE multi_tenant_orders ( order_id bigint not null, tenant_id text not null, order_details jsonb, PRIMARY KEY (tenant_id, order_id) -- 定义复合主键 );转换为分布表,并以
tenant_id列作为分布列。SELECT create_distributed_table('multi_tenant_orders', 'tenant_id');插入测试数据。其中
large company为热点租户。-- 为普通租户插入少量数据 INSERT INTO multi_tenant_orders (tenant_id, order_id, order_details) VALUES ('company1', 101, '{"item": "pen", "quantity": 10}'), ('company1', 102, '{"item": "book", "quantity": 5}'), ('company2', 201, '{"item": "stapler", "quantity": 2}'); -- 为热点租户插入大量数据(例如10万条) INSERT INTO multi_tenant_orders (tenant_id, order_id, order_details) SELECT 'large company', i, ('{"item": "widget", "order_num": ' || i || '}')::jsonb FROM generate_series(1, 100000) as i;
为热点租户创建隔离分片
使用isolate_tenant_to_new_shard函数为指定租户创建一个仅包含其数据的专属分片。
语法说明
SELECT isolate_tenant_to_new_shard('<表名>', <租户ID值>, 'CASCADE');参数说明
表名:需要操作的分布表名称。
租户ID值:需要隔离的热点租户的ID。该参数可接受任意类型的值,请根据租户ID列的实际类型填写。
CASCADE:可选项。如果同一亲和组中存在其他表,则需指定CASCADE参数才能完成创建隔离分片操作。
返回值说明
执行隔离分片操作后,函数会返回新创建的隔离分片的ID。需记录此ID,将使用它进行迁移操作。
示例
SELECT isolate_tenant_to_new_shard('multi_tenant_orders', 'large company', 'CASCADE');
isolate_tenant_to_new_shard
-----------------------------
102199
(1 row)将隔离分片迁移至目标节点
创建隔离分片后,需将其从原节点迁移至资源更充裕的目标节点,以完成物理隔离。您可根据实际情况选择以下任一策略。
(推荐)迁移热点租户
迁移热点租户是最直接和常用的方法。
查询隔离分片当前所在的源节点。此处以上一步中新创建的分片ID为例。
SELECT nodename, nodeport FROM pg_dist_shard_placement WHERE shardid = 102199;返回结果如下:
nodename | nodeport -----------+---------- 10.0.0.1 | 5432 (1 row)使用
polar_cluster_move_shard_placement函数执行迁移。SELECT polar_cluster_move_shard_placement(<shard_id>, '<source_node_name>', <source_node_port>, '<target_node_name>', <target_node_port>);示例:将分片
102199从10.0.0.1:5432迁移到10.0.0.2:5432。SELECT polar_cluster_move_shard_placement(102199, '10.0.0.1', 5432, '10.0.0.2', 5432);
迁移其他租户
如果热点租户所在节点上的其他租户数据量都很小,您也可以反向操作,将这些小租户的分片全部迁出,从而让热点租户独占该节点。
确认热点租户所在节点。此处以上一步中新创建的分片ID为例。
SELECT nodename, nodeport FROM pg_dist_shard_placement WHERE shardid = 102199;返回结果如下:
nodename | nodeport -----------+---------- 10.0.0.1 | 5432 (1 row)查询该节点上除热点租户隔离分片外的其他所有分片。
说明shardid为新创建的分片ID。nodename为热点租户所在节点。nodeport为热点租户所在节点的端口。
-- 将't'替换为表名, 102108替换为隔离分片ID, '10.0.0.1'和5432替换为热点节点地址 SELECT shardid, pg_size_pretty(shard_size) AS size FROM polar_cluster_shards WHERE table_name::text = 'multi_tenant_orders' AND shardid <> 102199 AND nodename = '10.0.0.1' AND nodeport = 5432 ORDER BY shard_size;使用
polar_cluster_move_shard_placement函数逐个地将查询出的非热点分片迁移到其他节点,直至该节点资源被释放。SELECT polar_cluster_move_shard_placement(<shard_id>, '<source_node_name>', <source_node_port>, '<target_node_name>', <target_node_port>);