PostgreSQL实战指南:多领域数据库应用与实践


第五部分:PostgreSQL与分布式数据库

虽然 PostgreSQL 是一个单机数据库的典范,但其强大的生态系统提供了多种构建分布式解决方案的途径。本部分将探讨如何通过逻辑复制、外部数据封装器(FDW)以及 CitusData、Greenplum 等专业扩展,将 PostgreSQL 从单机扩展到分布式集群,以应对海量数据和高并发的挑战。


第22章:CitusData:PostgreSQL分布式扩展实践

当单个 PostgreSQL 实例的写入或计算能力达到瓶颈时,就需要进行水平扩展(scaling out)。CitusData(或简称 Citus)是一个开源的 PostgreSQL 扩展,它通过分片(Sharding)技术将标准的 PostgreSQL 转变为一个强大的分布式数据库集群。Citus 使得 PostgreSQL 能够透明地处理海量数据和高并发请求,特别适用于多租户 SaaS 应用和实时分析场景。

22.1 Citus 架构原理

Citus 集群由两种类型的节点组成:

  • 协调节点 (Coordinator Node): 这是集群的入口。应用程序连接到协调节点,并向其发送查询。协调节点本身也是一个带有 Citus 扩展的 PostgreSQL 实例。它存储了集群的元数据(例如,哪个分片存储在哪个工作节点上),负责查询规划,并将查询分发到工作节点上并行执行,最后聚合结果返回给应用。
  • 工作节点 (Worker Nodes): 这些是标准的 PostgreSQL 实例,也安装了 Citus 扩展。它们负责存储数据的分片(Shards)并执行协调节点分发过来的查询片段。

分片 (Sharding): Citus 通过对表进行分片来实现水平扩展。当你将一个表声明为分布式表时,Citus 会根据你指定的“分布键”(Distribution Key)将表的行数据水平地切分成多个小的表,这些小表就是分片。然后,Citus 会将这些分片均匀地分布到不同的工作节点上。

22.2 分布式数据建模

在 Citus 中,主要有两种类型的表:

  • 分布式表 (Distributed Tables): 这是需要水平扩展的大表,例如多租户应用中的 events 表或 measurements 表。创建分布式表时,必须选择一个分布键。
    • 如何选择分布键? 选择分布键是 Citus 数据建模中最关键的一步。一个好的分布键应该能将数据均匀地分布到各个节点,并且能使最常见的查询尽可能地在单个工作节点内部完成,避免跨节点网络通信。在多租户应用中,tenant_idcustomer_id 通常是最佳的分布键。
  • 参考表 (Reference Tables): 这是相对较小、不经常变动、且经常需要与分布式表进行 JOIN 的表,例如 countries 表、product_categories 表。当一个表被声明为参考表时,Citus 会将它的完整副本存储在每一个工作节点上。这样,分布式表在与参考表进行 JOIN 时,就可以在工作节点本地完成,效率极高。
22.3 场景实战:构建多租户 SaaS 应用的分析后台

业务场景描述:

我们正在为一个多租户的网站分析平台构建后台数据库。每个租户(客户)都有多个网站,平台会收集这些网站的页面浏览事件。数据量非常大,我们需要一个能够水平扩展的方案来存储和实时查询这些事件数据。

数据建模与实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- 假设已在一个 Citus 集群环境中 (1个协调节点,多个工作节点)
-- 在协调节点上执行以下操作

-- 1. 启用 Citus 扩展
CREATE EXTENSION citus;

-- 2. 创建表
-- 租户信息表 (小表,适合做参考表)
CREATE TABLE tenants (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    plan TEXT
);

-- 页面浏览事件表 (大表,需要分布式存储)
CREATE TABLE page_views (
    id BIGSERIAL,
    tenant_id INTEGER NOT NULL,
    url TEXT,
    view_time TIMESTAMPTZ DEFAULT NOW(),
    PRIMARY KEY (id, tenant_id) -- 分布式表主键必须包含分布键
);

-- 3. 分布表
-- 将 tenants 表定义为参考表
SELECT create_reference_table('tenants');

-- 将 page_views 表定义为分布式表,并以 tenant_id 作为分布键
SELECT create_distributed_table('page_views', 'tenant_id');

数据写入与查询:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- 插入数据 (就像操作普通表一样)
-- Citus 会根据 tenant_id 的哈希值,自动将数据路由到正确的工作节点
INSERT INTO tenants (id, name, plan) VALUES (1, 'Company A', 'premium'), (2, 'Company B', 'free');

INSERT INTO page_views (tenant_id, url) VALUES
(1, '/home'), (2, '/pricing'), (1, '/features'), (1, '/contact');

-- 查询 (对应用层透明)
-- 查询会被下推到工作节点并行执行

-- 查询租户 'Company A' 的最新10条页面浏览记录
-- 这个查询非常高效,因为它只会被路由到存储 tenant_id=1 的数据的那个工作节点上
SELECT url, view_time
FROM page_views
WHERE tenant_id = 1
ORDER BY view_time DESC
LIMIT 10;

-- 聚合查询:统计每个租户的总浏览量
-- Citus 会将聚合操作下推到每个工作节点并行计算,然后在协调节点上合并最终结果
SELECT
    t.name,
    count(*) AS total_views
FROM
    page_views pv
JOIN
    tenants t ON pv.tenant_id = t.id
GROUP BY
    t.name
ORDER BY
    total_views DESC;

代码解释与思考:

  • create_distributed_table(table_name, distribution_key): 这是将一个标准表转换为分布式表的关键函数。
  • create_reference_table(table_name): 这是将一个表转换为参考表的函数。
  • 主键约束: 对于分布式表,其主键或唯一约束必须包含分布键。这是因为 Citus 需要通过分布键来定位数据,无法在全局范围内高效地保证不包含分布键的列的唯一性。
  • 查询透明性: Citus 的最大优点之一就是对应用层的透明性。你的应用程序仍然像连接单个 PostgreSQL 一样连接到协调节点,使用标准的 SQL 进行查询。Citus 在后台处理了所有的分布式复杂性。
  • JOIN 操作:
    • 分布式表 JOIN 参考表: 效率非常高,因为每个工作节点都有参考表的全量数据,JOIN 可以在本地完成。
    • 分布式表 JOIN 分布式表 (Co-located JOIN): 如果两个分布式表使用相同的分布键(且数据类型相同),Citus 会确保具有相同分布键值的分片被放置在同一个工作节点上。这种情况下,JOIN 也可以在工作节点本地高效完成。我们的例子中,如果还有一个 clicks 表也用 tenant_id 分布,那么 page_viewsclicksJOIN 就会是 Co-located JOIN。
    • 其他 JOIN: 如果 JOIN 条件不满足上述情况,Citus 可能需要将一个表的数据重分布(re-partition)到其他节点,这会涉及大量的网络开销,需要尽量避免。
22.4 总结

本章我们深入了解了 CitusData 扩展如何将 PostgreSQL 转变为一个强大的分布式数据库。我们学习了其协调节点-工作节点的架构、分片的核心原理,以及分布式表和参考表的建模思想。通过构建一个多租户 SaaS 应用的实例,我们实践了如何选择分布键,以及 Citus 如何透明地并行化查询,从而实现数据库的水平扩展。

在下一章,我们将探讨另一个基于 PostgreSQL 的分布式数据库解决方案——Greenplum,它更侧重于大规模并行处理(MPP)和数据仓库场景。