Citus分布式方案(二)- 多租户场景的应用

(二)多租户场景的应用

本节内容:以多租户应用为例,介绍使用Citus对其进行建模以实现可伸缩性。我们将研究多租户应用程序的典型挑战,比如,不同租户间的隔离、扩展硬件以容纳更多数据、存储不同租户之间的不同数据。PostgreSQL和Citus提供了处理这些问题所需的工具。

扩展关系数据模型

我们通过一个跟踪在线广告表现的应用程序的数据模型,来模拟多租户场景下的关系模型。为了便于演示,我们简化模式,该模型只需满足:跟踪多个公司,每个公司都在进行广告活动;活动有很多广告,每个广告都有它的点击和效益的相关记录。

1. 有效分发与隔离的数据模型

PostgreSQL作为关系数据模型非常适合应用程序,它保护数据完整性,允许灵活的查询,并适应数据的变化。作为传统数据库,关系型数据库常常被认为不能满足大型SaaS应用程序所需的工作负载。

结合Citus,我们可以保持原有的数据模型,并使其具备可伸缩性。对于上层应用,Citus看起来就像一个单独的PostgreSQL数据库,但实际上,它在内部将查询路由到数量可调的物理服务器(节点),并通过这些服务器并行处理请求。

业务查询,通常总是一次请求一个承租者的信息,而不是混合请求多个承租者。例如,当销售人员在搜索潜在客户信息时,返回结果集是针对他的雇主公司的,其他企业的信息并不包括在内。

由于应用程序查询仅限于单个租户(如商店或公司),因此提高多租户应用程序查询的效率的一种方法是:将给定租户的所有数据存储在同一个节点上。这将最大限度地减少节点之间的网络开销,并允许Citus高效地支持应用程序的所有连接、关键约束和事务。

在Citus中,我们通过确保模式中的每个表都有一个列来明确标记哪个租户拥有哪些行来实现这一点。在AD分析应用程序中,租户是公司,因此我们必须确保所有表都有company_id列。可以理解为PostgreSQL中分区键,相同范围的数据处于同一张分区表(即节点)中。
当某行记录被标记为同一公司时,Citus使用这个列来读取和写入同一节点,即,company_id将是分布列。

2. 创建关系表结构并导入数据

Citus要求主键和外键约束必须包括分布列,即,主键和外键需要包含company_id。这恰好满足多租户的情况,因为我们真正需要的是确保每个租户的唯一性。

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,       -- was: PRIMARY KEY
  company_id bigint REFERENCES companies (id),
  name text NOT NULL,
  cost_model text NOT NULL,
  state text NOT NULL,
  monthly_budget bigint,
  blacklisted_site_urls text[],
  created_at timestamp without time zone NOT NULL,
  updated_at timestamp without time zone NOT NULL,
  PRIMARY KEY (company_id, id) -- added
);

CREATE TABLE ads (
  id bigserial,       -- was: PRIMARY KEY
  company_id bigint,  -- added
  campaign_id bigint, -- was: REFERENCES campaigns (id)
  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),         -- added
  FOREIGN KEY (company_id, campaign_id) -- added
    REFERENCES campaigns (company_id, id)
);

CREATE TABLE clicks (
  id bigserial,        -- was: PRIMARY KEY
  company_id bigint,   -- added
  ad_id bigint,        -- was: REFERENCES ads (id),
  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),      -- added
  FOREIGN KEY (company_id, ad_id)    -- added
    REFERENCES ads (company_id, id)
);

CREATE TABLE impressions (
  id bigserial,         -- was: PRIMARY KEY
  company_id bigint,    -- added
  ad_id bigint,         -- was: REFERENCES ads (id),
  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),       -- added
  FOREIGN KEY (company_id, ad_id)     -- added
    REFERENCES ads (company_id, id)
);

关系表结构创建完毕,我们需要告诉Citus根据company_id在数据节点上创建分片。函数create_distributed_table将通知Citus,关系表应该如何在节点之间分布,并且将来对这些表的查询需要通过分布式执行。该函数还为数据节点上的表创建分片,这些分布是Citus用于向节点分配数据时的存储单元。

协调节点,运行:

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');

导入数据(使用Citus官网的示例数据):

# download and ingest datasets from the shell
for dataset in companies campaigns ads clicks impressions geo_ips; do
  curl -O https://examples.citusdata.com/mt_ref_arch/${dataset}.csv
done

作为PostgreSQL的一个扩展,Citus支持使用COPY命令批量加载。

\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

3. 集成上层应用

完成了前面的模式修改,应用程序只需很少的工作就可以扩展。应用程序连接到Citus,让数据库负责保持查询速度和数据安全,与连接到单个数据库实例无异。

在数据库中执行的SQL在每个表上包含一个WHERE company_id = :value子句时,Citus将认识到查询应该路由到单个节点,并在该节点上执行查询。

下面是对单个租户进行的简单查询和更新操作:

-- campaigns with highest budget
SELECT name, cost_model, state, monthly_budget
  FROM campaigns
WHERE company_id = 5
ORDER BY monthly_budget DESC
LIMIT 10;

-- double the budgets!
UPDATE campaigns
  SET monthly_budget = monthly_budget*2
WHERE company_id = 5;

Citus支持事务:

-- transactionally reallocate campaign budget money
BEGIN;

UPDATE campaigns
   SET monthly_budget = monthly_budget + 1000
WHERE company_id = 5
   AND id = 40;

UPDATE campaigns
   SET monthly_budget = monthly_budget - 1000
WHERE company_id = 5
   AND id = 41;

COMMIT;

同时还支持表连接,它的工作方式和在PostgreSQL中的一样:

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;

简而言之,当查询的范围确定为租户时,插入、更新、删除、复杂SQL和事务都将按PostgreSQL预期工作。

4. 共享数据

到目前为止,所有的表都是根据company_id分布的,但是有时候有些数据可以由所有租户共享,而且这些数据不单独属于任何租户。例如,所有使用这个示例广告平台的公司可能都希望根据IP地址来获取客户的地理信息。在单个机器数据库中,这可以通过一个IP查找表来完成,结构如下:

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);

为了在分布式环境中更高效地使用这个表,Citus提供了一种方法,为每个节点保存一份geo_ips表。这样,只需通过本地查询即可获取用户的地理信息,避免产生网络流量。即,我们通过指定geo_ips表作为参考表来实现这一点:

-- Make synchronized copies of geo_ips on all workers
SELECT create_reference_table('geo_ips');

参考表会赋值到所有数据节点上,并且有修改时,Citus自动保持它们同步。

导入geo_ips参考表数据:

\copy geo_ips from 'geo_ips.csv' with csv

查询点击ID为290的广告的用户信息:

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;

5. 模式同步

模式同步,即任何模式的更改都需要在所有租户之间一致地反映出来。在Citus中,简单地使用标准PostgreSQL DDL命令即可更改表的模式,并且Citus将使用两阶段提交协议将它们从协调节点传播到数据节点。

以增加字段为例:

ALTER TABLE ads ADD COLUMN caption text;

在协调节点上执行,将会同步到各个数据节点上。

6. 差异数据

大多数情况下,多个承租者可能共享一个共同的模式和硬件基础设施,因此可能需要容纳自己不需要的信息。例如,使用本例的广告数据库的一个租户应用程序可能希望存储cookie信息,而另一个租户可能关心浏览器代理。传统的做法,会创建固定数量的预分配自定义列或者使用外部扩展表,来存放不同内容的信息,有些租户可能用到这些字段,而有些可能不会用到。然而,PostgreSQL的非结构化列类型提供了一种更简单的方法,我们熟知的就是JSONB类型。

查询某公司谁的移动访客点击量更多:

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;

Citus支持创建部分索引来提高单个租户查询模式的速度:

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

Citus同样支持PostgreSQL的GIN索引,在JSONB列上创建GIN索引将在该JSON文档中的每个键和值上创建索引:

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;

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值