你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:使用弹性群集设计多租户数据库

在本教程中,将 Azure Database for PostgreSQL 与弹性群集配合使用,了解如何设计受益于横向扩展的多租户应用程序。

  • Prerequisites
  • 使用 psql 实用工具创建架构
  • 在节点之间将表分片
  • 引入示例数据
  • 查询租户数据
  • 租户之间共享数据
  • 自定义每租户架构

Prerequisites

通过以下方式之一创建弹性群集:

使用 psql 实用工具创建架构

使用 psql 连接到弹性群集后,可以配置弹性群集。 本教程指导你创建一个应用程序平台,使公司能够跟踪其广告市场活动。

注意

在群集中分发数据时,任何唯一的数据约束都限定在其分布“分片”内。 在我们的多租户示例中,每个租户强制实施应用程序数据唯一性(例如公司 ID)。 因此,主键和外键约束的分布式表定义始终包含公司 ID 列。

创建一个表来保存您的多租户公司信息,并为他们的活动创建另一个表。 在 psql 控制台中,运行这些命令:

CREATE TABLE companies (
  id bigserial PRIMARY KEY,
  name text NOT NULL,
  image_url text,
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL
);

CREATE TABLE campaigns (
  id bigserial,
  company_id bigint REFERENCES companies (id),
  name text NOT NULL,
  cost_model text NOT NULL,
  state text NOT NULL,
  monthly_budget bigint,
  blocked_site_urls text[],
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL,

  PRIMARY KEY (company_id, id)
);

每个广告系列会支付运行广告的费用。 使用以下代码在 psql 中添加广告表:

CREATE TABLE ads (
  id bigserial,
  company_id bigint,
  campaign_id bigint,
  name text NOT NULL,
  image_url text,
  target_url text,
  impressions_count bigint DEFAULT 0,
  clicks_count bigint DEFAULT 0,
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL,

  PRIMARY KEY (company_id, id),
  FOREIGN KEY (company_id, campaign_id)
    REFERENCES campaigns (company_id, id)
);

最后,你希望根据广告选择和广告展示来追踪统计信息:

CREATE TABLE clicks (
  id bigserial,
  company_id bigint,
  ad_id bigint,
  clicked_at timestamp without time zone NOT NULL,
  site_url text NOT NULL,
  cost_per_click_usd numeric(20,10),
  user_ip inet NOT NULL,
  user_data jsonb NOT NULL,

  PRIMARY KEY (company_id, id),
  FOREIGN KEY (company_id, ad_id)
    REFERENCES ads (company_id, id)
);

CREATE TABLE impressions (
  id bigserial,
  company_id bigint,
  ad_id bigint,
  seen_at timestamp without time zone NOT NULL,
  site_url text NOT NULL,
  cost_per_impression_usd numeric(20,10),
  user_ip inet NOT NULL,
  user_data jsonb NOT NULL,

  PRIMARY KEY (company_id, id),
  FOREIGN KEY (company_id, ad_id)
    REFERENCES ads (company_id, id)
);

现在,可以通过运行 psql 查看新创建的表:

\dt

在节点之间将表分片

直到现在,您已经创建了标准的 Postgres 表,但最终需要在弹性群集中创建分布式表。 弹性群集中的分布式表根据分布列定义的值将数据存储在不同节点上。 此列用于确定基础工作器节点中的行位置。

将分布列设置为company_id,该列充当多租户标识符。 在 psql 中运行以下函数:

SELECT create_distributed_table('companies',   'id');
SELECT create_distributed_table('campaigns',   'company_id');
SELECT create_distributed_table('ads',         'company_id');
SELECT create_distributed_table('clicks',      'company_id');
SELECT create_distributed_table('impressions', 'company_id');

注意

若要利用具有 Azure Database for PostgreSQL 性能特性的弹性群集,需要分布表。 除非你分发表和模式,否则群集节点不会参与任何分布式查询或操作。

引入示例数据

在 psql 外部的普通命令行中,下载示例数据集:

for dataset in companies campaigns ads clicks impressions geo_ips; do
  curl -O https://raw.githubusercontent.com/Azure-Samples/azure-postgresql-elastic-clusters/main/multi-tenant/${dataset}.csv
done

返回到 psql 内部,大容量加载数据。 请务必在下载数据文件的目录中运行 psql。

SET client_encoding TO 'UTF8';

\copy companies from 'companies.csv' with csv
\copy campaigns from 'campaigns.csv' with csv
\copy ads from 'ads.csv' with csv
\copy clicks from 'clicks.csv' with csv
\copy impressions from 'impressions.csv' with csv

分布式表中的数据现在分布在弹性群集的工作节点上。

查询租户数据

当应用程序请求特定公司的数据时,数据库现在可以有效地在适当的工作器节点上执行查询。 例如,以下查询 (company_id = 5) 筛选广告和展示次数。 尝试在 psql 中运行它并查看结果。

SELECT a.campaign_id,
       RANK() OVER (
         PARTITION BY a.campaign_id
         ORDER BY a.campaign_id, count(*) DESC
       ), count(*) AS n_impressions, a.id
  FROM ads AS a
  JOIN impressions AS i
    ON i.company_id = a.company_id
   AND i.ad_id      = a.id
 WHERE a.company_id = 5
GROUP BY a.campaign_id, a.id
ORDER BY a.campaign_id, n_impressions DESC;

租户之间共享数据

到目前为止,你通过company_id将所有表分发到集群中。 但是,某些类型的数据自然“属于”所有租户,并可以与所有租户分发一起放置。 例如,广告平台中的所有公司可能希望根据 IP 地址详细信息获取其受众的地理信息。

创建用于保存此地理 IP 信息的引用表。 在 psql 中运行以下命令:

CREATE TABLE geo_ips (
  addrs cidr NOT NULL PRIMARY KEY,
  latlon point NOT NULL
    CHECK (-90  <= latlon[0] AND latlon[0] <= 90 AND
           -180 <= latlon[1] AND latlon[1] <= 180)
);
CREATE INDEX ON geo_ips USING gist (addrs inet_ops);

接下来,标识 geo_ips 为“引用表”。 群集通过在每个群集工作器节点上存储同步表来管理此表。

SELECT create_reference_table('geo_ips');

现在,将您的示例数据加载到参考表中。 请记住从下载数据集文件的目录运行此命令。

\copy geo_ips from 'geo_ips.csv' with csv

联接所选表的 geo_ips SQL 语句现在在所有节点上都有效。 请注意,此联接可查找 ad 290 上选择的每个 IP 的位置。 尝试在 psql 中运行查询:

SELECT c.id, clicked_at, latlon
  FROM geo_ips, clicks c
 WHERE addrs >> c.user_ip
   AND c.company_id = 5
   AND c.ad_id = 290;

自定义每个租户的架构

在某些情况下,单个租户可能需要存储其他租户不需要的特殊信息。 但是,所有租户共享具有相同数据库架构的通用定义。 可将额外数据放在何处?

一种解决方案是使用灵活的列类型,例如 PostgreSQL 的 JSONB。 我们的架构在 clicks 中有一个 JSONB 字段,称为 user_data。 公司(例如公司 5)可以使用此列跟踪用户是否在移动设备上的信息。

这里有一个查询,公司 5 可将其用于查找是移动设备访客还是传统访客的选择次数更高。

SELECT
  user_data->>'is_mobile' AS is_mobile,
  count(*) AS count
FROM clicks
WHERE company_id = 5
GROUP BY user_data->>'is_mobile'
ORDER BY count DESC;

PostgreSQL 包含一项功能强大的功能,可用于为数据的特定部分或子集编制索引。 可以通过创建 部分索引进一步优化公司 5 的查询。

CREATE INDEX click_user_data_is_mobile
ON clicks ((user_data->>'is_mobile'))
WHERE company_id = 5;

此外,提高包含 JSONB 列的查询的性能的另一种方法是对 JSONB 列中的每个基础键和值创建 GIN 索引

CREATE INDEX click_user_data
ON clicks USING gin (user_data);

-- this speeds up queries like, "which clicks have the is_mobile key present in user_data?"

SELECT id
  FROM clicks
 WHERE user_data ? 'is_mobile'
   AND company_id = 5;

后续步骤