基于模式的分片通过 Citus 进入 PostgreSQL

     Citus 是 PostgreSQL 的数据库扩展扩展,以其对数据表进行分片和在多个节点之间有效分配工作负载的能力而闻名。在 Citus 12.0 中,   Citus 引入了一个非常令人兴奋的功能,称为基于模式的分片。新的基于架构的分片功能使您可以选择如何在集群中分发数据,对于某些数据模型(例如:多租户应用、微服务等),这种基于架构的分片方法可能要容易得多!

    在这篇博文中,我们将深入探讨新的基于架构的分片功能,您将了解:

详细了解 Postgres 中新的基于架构的分片

    Citus 12.0 引入了一个设置,允许您根据架构对数据库进行分片。启用后,每个新创建的架构都将成为数据库的逻辑分片,从而确保给定租户的所有表都存储在同一节点上。当与基于架构的多租户模型结合使用时,应用程序的每个租户都会映射到可以独立运行的数据库逻辑分片中。值得强调的是,在使用架构对数据库进行分片时,无需调用 create_distributed_table() 函数即可为这些分布式架构中的表创建分布式表。让我们开始为它们创建两个分布式模式和一些数据表:citus.enable_schema_based_sharding

 

-- Enable schema-based sharding in the session, or add it to postgresql.conf to enable it globally
SET citus.enable_schema_based_sharding TO ON;

-- Use regular "CREATE SCHEMA" commands to create two distributed schemas.
CREATE SCHEMA tenant_1;
CREATE SCHEMA tenant_2;

-- Create data tables for those two tenants.
--
-- Note that it's not necessary to keep citus.enable_schema_based_sharding setting enabled while
-- creating data tables because tenant_1 & tenant_2 are already saved as distributed schemas into
-- the Citus metadata.
--
-- Let's use regular "CREATE TABLE" commands to create two tables for our distributed schemas.
CREATE TABLE tenant_1.users (
  id int PRIMARY KEY,
  name text,
  email text
);
CREATE TABLE tenant_1.events (
  id INT PRIMARY KEY,
  name text,
  date date,
  user_id int REFERENCES tenant_1.users(id)
);
CREATE TABLE tenant_2.users (
  id int PRIMARY KEY,
  name text,
  email text
);
CREATE TABLE tenant_2.events (
  id int  PRIMARY KEY,
  name text,
  date date,
  user_id int REFERENCES tenant_2.users(id)
);

 

我们可以使用 Citus 12.0 中引入的视图来查看分布式模式的总大小以及分布式模式表在集群中的存储位置:citus_schemascitus_shards

SELECT * FROM citus_schemas;
┌─────────────┬───────────────┬─────────────┬──────────────┐
│ schema_name │ colocation_id │ schema_size │ schema_owner │
├─────────────┼───────────────┼─────────────┼──────────────┤
│ tenant_1    │             1 │ 32 kB       │ onurctirtir  │
│ tenant_2    │             2 │ 32 kB       │ onurctirtir  │
└─────────────┴───────────────┴─────────────┴──────────────┘
(2 rows)

SELECT * FROM citus_shards WHERE citus_table_type = 'schema';
┌─────────────────┬─────────┬────────────────────────┬──────────────────┬───────────────┬─────────────┬──────────┬────────────┐
│   table_name    │ shardid │       shard_name       │ citus_table_type │ colocation_id │ nodename    │ nodeport │ shard_size │
├─────────────────┼─────────┼────────────────────────┼──────────────────┼───────────────┼─────────────┼──────────┼────────────┤
│ tenant_1.events │  103000 │ tenant_1.events_103000 │ schema           │             1 │ worker2host │     5432 │      16384 │
│ tenant_1.users  │  103001 │ tenant_1.users_103001  │ schema           │             1 │ worker2host │     5432 │      16384 │
│ tenant_2.events │  103002 │ tenant_2.events_103002 │ schema           │             2 │ worker1host │     5432 │      16384 │
│ tenant_2.users  │  103003 │ tenant_2.users_103003  │ schema           │             2 │ worker1host │     5432 │      16384 │
└─────────────────┴─────────┴────────────────────────┴──────────────────┴───────────────┴─────────────┴──────────┴────────────┘
(4 rows)

 

    该视图允许我们了解如何将新的“单分片”表概念用于分布式模式表。在下面的查询中,我们可以看到分布式架构表只有一个分片,并且它们没有分片键。此外,属于同一架构的表共享相同的共置 ID。因此,属于同一架构的所有表将自动作为单分片表彼此位于同一位置。这样,架构中的跨表查询和操作(即联接和外键)将在存储表的节点上处理,并且网络往返次数最少。citus_tables

SELECT table_name, colocation_id, distribution_column, shard_count FROM citus_tables WHERE citus_table_type = 'schema';

   table_name    | colocation_id | distribution_column | shard_count
-----------------+---------------+---------------------+-------------
 tenant_1.events |             1 | <none>              |           1
 tenant_1.users  |             1 | <none>              |           1
 tenant_2.events |             2 | <none>              |           1
 tenant_2.users  |             2 | <none>              |           1
(4 rows)

 

    也就是说,如果您按架构对 PostgreSQL 数据库进行分片,则跨表操作(例如联接和外键)应仅针对单个架构,或者您可能在常规架构中创建的 Citus 引用表(例如“public”):

-- Create "countries" table as a Citus reference table and add a "country_id" column to each tenant's
-- "users" table with a foreign key to "countries" table.
CREATE TABLE public.countries (
  id int PRIMARY KEY,
  name text UNIQUE
);

SELECT create_reference_table('public.countries');

ALTER TABLE tenant_1.users ADD COLUMN country_id INT REFERENCES public.countries(id);
ALTER TABLE tenant_2.users ADD COLUMN country_id INT REFERENCES public.countries(id);

 

     然后,您可以像使用普通 PostgreSQL 一样查询租户表。但若要确保查询正确的租户,需要将 设置为相关租户,或者需要对查询中引用的表进行架构限定:search_path

-- List distinct event dates for tenant_1.
SET search_path TO tenant_1;
SELECT DISTINCT(e.date) FROM users u JOIN events e ON (u.id = e.user_id);

-- Compute the number of users per country and return the country name along with the
-- corresponding user count.
SELECT c.name country_name, COUNT(*) user_count
FROM tenant_2.users u JOIN public.countries c ON (u.country_id = c.id) GROUP BY c.name;

 

    只需更改查询中的架构名称,查询就会透明地路由到其他节点。search_path

幕后花絮:我们如何实现基于架构的分片

    Citus 开发是一个进化过程,我们经常根据我们从用户那里观察到的情况,在其他功能引入的基础设施之上构建新功能。

    Citus 一直支持分布式表。分布式表的行根据分布列中的值细分为分片。分布式表的分片可以移动(“重新平衡”),分布式表可以添加到“共置组”中,这样可以保证具有相同哈希范围的分片(“分片组”)始终放置在同一个节点上。为了获得良好的性能,查询必须按分布列进行筛选和/或联接,以便可以将查询委派给各个分片组。

    Citus 还支持引用表,并且可以在协调器上管理本地表。这些表没有分布列,不能四处移动或添加到共置组。由于单个节点包含所有数据,因此这些表大多具有与 PostgreSQL 相同的性能和 SQL 特征,而无需额外的过滤器/联接。此外,分布式表和本地表可以与引用表联接,而不会产生额外的网络开销,因为引用表会复制到所有节点。

    通过这些表类型,Citus 用户可以分发具有非常大表的数据库。但是,我们发现一些用户拥有非常大的数据库,但没有非常大的表,因为他们已经将数据细分为许多表或模式。例如,多租户应用程序通常使用单独的架构,因此每个租户使用一组单独的表。通常,许多查询的范围限定为单个架构/租户。如果我们可以将每个架构设置为一个单独的分片组,那么范围限定为该架构的查询可以委托给存储分片组的任何位置,并且我们可以将架构分布到整个集群中。我们仍然可以支持使用引用表进行联接。

    基于架构的分片分两个阶段构建:首先,我们引入了一种新型的 Citus 表,它只有一个分片,没有分布列(如引用表和本地表),但与分布式表一样,它可以添加到共置组并由重平衡器移动。其次,我们使用 PostgreSQL 的 DDL 钩子为每个架构自动创建一个新的共置组,添加到该架构中的每个表都成为具有共置组的单个分片表。这样一来,我们就干净地扩展了现有的基础设施,同时获得了基于架构的分片的所有可用性优势。

PgBouncer 和 search_path 问题

     在不同架构之间切换的一种便捷方法是使用非限定名称并使用设置,但需要注意的是,这是一个会话级别的参数。这意味着,如果拓扑在客户端和 Citus 协调器之间包含连接池程序,则必须确保会话参数按预期工作。池程序可以以非透明的方式在多个客户端之间共享相同的数据库会话。在这种情况下,客户端可能会无意中覆盖彼此的会话参数。search_pathsearch_path

     如果您在事务池模式下使用 PgBouncer则 pgbouncer 1.20.0 或更高版本支持在事务池模式下使用会话参数。通过使用该设置,您可以配置 PgBouncer,使客户端可以在事务池模式下保留任意会话参数,例如:track_extra_parameterssearch_path

track_extra_parameters = search_path

 

     如上所述配置时,pgbouncer 会在切换到新的服务器连接时传播客户端指定的值 (via )。但是,Pgbouncer 从不解析查询,因此它实际上并不知道 SET 命令何时发生......pgbouncer怎么知道的值是多少?search_pathSET search_path TO ..search_path

     事实证明,PostgreSQL协议作为“参数状态”消息,当某些设置发生更改时,postgres会在查询(或SET!)后发出这些消息,pgbouncer可以处理这些消息以获取新值。不幸的是,还不是这些设置之一,但 PostgreSQL 扩展可以做很多事情。Citus 12.0+ 将设置更改为也发出,以便 PgBouncer 可以正确传播它,因此它会自动使用 Citus 进行基于模式的分片。search_pathsearch_path

将基于架构的分片与citus_stat_tenants相结合

使用基于架构的分片时,可以继续受益于 Citus 的强大功能,例如租户统计信息收集。在查询视图之前,需要确保在查询分布式架构之前已启用租户级统计信息收集,方法是设置为“all”:citus_stat_tenantscitus.stat_tenants_track

SELECT tenant_attribute, query_count_in_this_period FROM citus_stat_tenants;
┌──────────────────┬────────────────────────────┐
│ tenant_attribute │ query_count_in_this_period │
├──────────────────┼────────────────────────────┤
│ tenant1          │                          3 │
│ tenant2          │                          5 │
└──────────────────┴────────────────────────────┘
(2 rows)

 

     如果使用基于行的分片(哈希分布表),则 反映分布列中的值。使用基于架构的分片时,架构名称将显示为租户属性,并且将架构的查询计数和 CPU 使用率相加。tenant_attribute

使用 Citus shard-rebalancer 重新平衡架构分片群集

     默认情况下,Citus 会尝试通过遵循简单的基于轮询的方法尽可能公平地分配租户架构 - 将一个租户放入第一个工作节点,然后将下一个租户放入第二个工作节点,依此类推。但是,这并不总是导致公平分配,因为其中一个租户可能会变得更大,如上例所示。在这种情况下,shard-rebalancer 将帮助您在 Citus 集群中重新平衡租户架构,就像传统上对分布式表所做的那样。

-- Assume that later we've introduced a bigger tenant (tenant3) into the cluster in a way that it would
-- make more sense to group tenant1 & tenant2 into one worker node and tenant3 into other.
SELECT table_name, shardid, nodename, nodeport, shard_size FROM citus_shards WHERE citus_table_type = 'schema';
┌────────────────┬─────────┬─────────────┬──────────┬────────────┐
│   table_name   │ shardid │ nodename    │ nodeport │ shard_size │
├────────────────┼─────────┼─────────────┼──────────┼────────────┤
│ tenant1.events │  103000 │ worker2host │     5432 │      16384 │
│ tenant1.users  │  103001 │ worker2host │     5432 │      16384 │
│ tenant2.events │  103002 │ worker1host │     5432 │      16384 │
│ tenant2.users  │  103003 │ worker1host │     5432 │      16384 │
│ tenant3.events │  103004 │ worker1host │     5432 │      16384 │
│ tenant3.users  │  103005 │ worker1host │     5432 │    7217152 │
└────────────────┴─────────┴─────────────┴──────────┴────────────┘

(6 rows)

-- Start the shard-rebalancer and wait for a while.
SELECT citus_rebalance_start();

-- Then we can observe that data tables of tenant2 are moved to "worker2host" too.
SELECT table_name, shardid, nodename, nodeport, shard_size FROM citus_shards WHERE citus_table_type = 'schema';
┌────────────────┬─────────┬─────────────┬──────────┬────────────┐
│   table_name   │ shardid │ nodename    │ nodeport │ shard_size │
├────────────────┼─────────┼─────────────┼──────────┼────────────┤
│ tenant1.events │  103000 │ worker2host │     5432 │      16384 │
│ tenant1.users  │  103001 │ worker2host │     5432 │      16384 │
│ tenant2.events │  103002 │ worker2host │     5432 │      16384 │
│ tenant2.users  │  103003 │ worker2host │     5432 │      16384 │
│ tenant3.events │  103004 │ worker1host │     5432 │      16384 │
│ tenant3.users  │  103005 │ worker1host │     5432 │    7217152 │
└────────────────┴─────────┴─────────────┴──────────┴────────────┘
(6 rows)

     除了使用 shard-rebalancer 之外,还可以选择使用 function 将租户放入任意节点,但请记住,如果稍后运行重新平衡器,它可能会决定将架构放入其他节点:citus_move_shard_placement()

-- Calling citus_move_shard_placement() for one of the shard placements within tenant_3 would move
-- the whole tenant into desired worker node.
SELECT citus_move_shard_placement(103004, 'worker1host', 5432, 'worker2host', 5432);

 

为现有架构启用基于架构的分片

在上面的示例中,我们为租户创建了架构和数据表,Citus 会自动分发它们,因为我们在创建架构之前启用了设置。但是,如果您在创建其中一个架构之前忘记启用基于架构的分片,则可以使用 function:citus.enable_schema_based_shardingcitus_schema_distribute()

SELECT citus_schema_distribute('my_local_tenant_schema');

复制

就像在该架构中创建新的租户架构和数据表时发生的情况一样,function 会将架构中的现有表转换为保证彼此位于同一位置并且可以在 Citus 集群中移动的表。另请注意,您无需启用设置即可使用该函数,因为设置仅用于分发刚刚创建的架构。这里要注意的另一件重要事情是,如果要使用函数分发表,则不应在架构中包含分布式表。citus_schema_distribute()citus.enable_schema_based_shardingcitus_schema_distribute()citus.enable_schema_based_shardingcitus_schema_distribute()

如果你想取消分发分布式模式,那么你可以使用 function。值得强调的是;若要使用 function,则在创建 schema 之前,是否已使用 function 或通过设置 setting 来分发架构并不重要:citus_schema_undistribute()citus_schema_undistribute()citus_schema_distribute()citus.enable_schema_based_sharding

SELECT citus_schema_undistribute('tenant_schema');

 

当您取消分发架构时,Citus 会将架构中的分布式表转换为存储在协调器节点上的常规 PostgreSQL 表。这意味着,在取消分发架构后,您的数据表将只能从协调器节点访问,而您可以从任何节点查询分布式架构表

您还可以将分布式架构表的架构更改为其他内容,例如,如果您在其他租户的架构或常规架构中错误地创建了它:

-- Moves the data stored in "users" table into the node that stores data for tenant4.
ALTER TABLE tenant1.users SET SCHEMA tenant4;

-- Undistributes "events" table and moves it into "public" schema.
ALTER TABLE tenant1.events SET SCHEMA public;

-- Moves the data stored in "local_tenant_data" into the node that stores data for tenant1.
ALTER TABLE public.local_tenant_data SET SCHEMA tenant1;

 

django 租户和分布式 Postgres

     基于模式的分片非常适合多租户应用程序,特别是如果你的应用程序将你的租户表示为单独的模式,这已经是 django-tenants 所做的。当与 Citus 中基于架构的分片功能结合使用时,您的租户将自动成为 Citus 集群的逻辑分片,只需进行少量的应用程序更改。

对 django 应用程序执行迁移后,如下所示:

python manage.py makemigrations my_shared_app
..
python manage.py makemigrations my_tenant_app
...
python manage.py migrate

 

您可以连接到 Citus 协调器并运行以下命令,为新创建的租户启用基于架构的分片:

ALTER SYSTEM SET citus.enable_schema_based_sharding TO ON;
SELECT pg_reload_conf();

-- And convert the fact tables that would be referenced by tenant schemas to reference tables, if any.
SELECT create_reference_table('public.my_shared_app_tbl');

 

     就是这样!从现在开始,当新租户进入您的应用程序时,该租户将自动成为 Citus 集群的逻辑分片。如果您不打算构建新应用程序,并且想要开始对后备 PostgreSQL 数据库进行分片,则可以使用 function 将支持现有租户的架构转换为分布式架构。citus_schema_distribute()

     还值得一提的是;如果你有一个更适合基于行的分片的多租户应用程序,并且你的应用程序在 Django 中运行,我们鼓励你将 django-multitenant 与 Citus 一起使用。但是,如果你已经有一个通过 django-tenants 使用基于模式的多租户模型的应用程序,并且你想避免应用程序更改,那么按照上述步骤使用 Citus 和 django-tenants 无缝扩展你的应用程序会更合适。如果你不熟悉 django-tenants,你可能想按照 django-tenants 教程进行操作,同时牢记这些 SQL 命令。

这完全取决于您的应用程序需求

     Citus 中基于模式的分片功能具有许多好处,包括但不限于:

  • 易用性:通过采用基于架构的分片,如果您正在构建多租户应用程序,Citus 可以显着简化数据库的管理。使用基于架构的分片,您不再需要配置分布列,这为您提供了迁移到 Citus 的更简单方法。
  • 高效的资源分配:基于架构的分片允许您在 Citus 集群中分布数据库,同时确保给定租户的数据存储在同一节点上,从而最大限度地减少网络开销,并优化跨表操作的数据局部性。与将所有租户存储在单个 PostgreSQL 数据库中相比,此方法有助于加快查询执行速度并提高整体性能。
  • 可扩展性和灵活性:基于架构的分片在简单性和可扩展性之间取得了平衡。它为优先考虑易用性而不牺牲水平扩展能力的应用程序提供了理想的解决方案。

     除了基于架构的分片带来的灵活性和易用性之外,基于架构的分片还支持基于行的分片无法实现的某些用例,例如:

  • 具有非同构架构的多租户:如果租户需要存储不同的属性集或单个索引,则基于架构的分片将比跨租户共享表更适合。
  • 微服务:在这样一个世界中,每个微服务都拥有自己作为数据库架构的小型而简单的状态,基于架构的分片允许微服务有效地管理和访问数据,实现跨微服务的数据共享和事务执行,同时通过共享资源来降低成本。

     而且,由于分布式系统中的情况大多如此,基于架构的分片的易用性需要权衡取舍:当有大量租户时,基于行的分片可以更好地扩展。对于数以百万计的架构,添加新表或列可能成为一项重大挑战。此外,在数据库中拥有大量表会带来开销,特别是会增加目录缓存的大小。

     如果您的应用程序依赖于基于行的分片,并且您希望继续受益于其强大的功能,则无需更改应用程序中的任何内容。基于行的分片仍然是使用分片分发 Postgres 表的有用且有效的方式。但现在,您有了一个新的选项,即基于架构的分片,它非常适合多租户应用程序、微服务,甚至是执行垂直分区的应用程序。您甚至可以选择在同一个 Citus 数据库集群中同时使用基于行的分片和基于架构的分片,方法是将现有的基于行的分片表保留在常规架构中,并在分布式架构中创建新的数据表。或者,您可能只想使用任何一种方法,这完全取决于您的应用程序的需求。