確定應用程序類型
在 Citus
集群上運行高效查詢要求數據在機器之間正確分布。這因應用程序類型及其查詢模式而異。
大致上有兩種應用程序在 Citus
上運行良好。數據建模的第一步是確定哪些應用程序類型更接近您的應用程序。
概覽
多租戶應用 | 實時應用 |
---|---|
有時 schema 中有幾十個或數百個表 |
表數量少 |
一次與一個租戶(公司/商店)相關的查詢 | 具有聚合的相對簡單的分析查詢 |
用於服務 Web 客戶端的 OLTP 工作負載 |
攝取大量幾乎不可變的數據 |
為每個租戶分析查詢提供服務的 OLAP 工作負載 |
通常圍繞着一個大的事件表 |
示例和特征
多租戶應用
這些通常是為其他公司、帳戶或組織服務的 SaaS
應用程序。大多數 SaaS
應用程序本質上是關系型的。它們具有跨節點分布數據的自然維度:只需按 tenant_id
分片。
Citus
使您能夠將數據庫擴展到數百萬租戶,而無需重新構建應用程序。 您可以保留所需的關系語義,例如 聯接
、外鍵約束
、事務
、ACID
和一致性
。
- 示例:為其他企業托管店面的網站,例如數字營銷解決方案或銷售自動化工具。
- 特征:與單個租戶相關的查詢,而不是跨租戶加入信息。這包括為
Web
客戶端提供服務的OLTP
工作負載,以及為每個租戶提供分析查詢的OLAP
工作負載。 在您的數據庫模式中擁有數十或數百個表也是多租戶數據模型的一個指標。
使用 Citus
擴展多租戶應用程序還需要對應用程序代碼進行最少的更改。我們支持流行的框架,如 Ruby on Rails
和 Django
。
實時分析應用
需要大規模並行性、協調數百個內核以快速獲得數值、統計或計數查詢結果的應用程序。 通過跨多個節點對 SQL
查詢進行分片和並行化,Citus
可以在一秒鍾內對數十億條記錄執行實時查詢。
- 示例: 需要亞秒級響應時間的面向客戶的分析儀表板。
- 特征: 幾張表,通常以設備、站點或用戶事件的大表為中心,並且需要大量攝取大部分不可變的數據。涉及多個聚合和
GROUP BY
的相對簡單(但計算量大)的分析查詢。
如果您的情況類似於上述任何一種情況,那么下一步就是決定如何在 Citus
集群中對數據進行分片。 如概念部分所述,Citus
根據表分布列的哈希值將表行分配給分片。 數據庫管理員對分布列的選擇需要與典型查詢的訪問模式相匹配,以確保性能。
選擇分布列
Citus
使用分布式表中的分布列將表行分配給分片。為每個表選擇分布列
是最重要的建模決策之一,因為它決定了數據如何跨節點分布。
如果正確選擇了分布列,那么相關數據將在相同的物理節點上組合在一起,從而使查詢快速並添加對所有 SQL
功能的支持。如果列選擇不正確,系統將不必要地緩慢運行,並且無法支持跨節點的所有 SQL
功能。
本節提供兩種最常見的 Citus
方案的分布列提示。 最后,它深入探討了 共置(co-location)
,即節點上理想的數據分組。
多租戶應用
多租戶架構
使用一種分層數據庫建模形式在分布式集群中的節點之間分布查詢。 數據層次結構的頂部稱為 tenant id
,需要存儲在每個表的列中。Citus
檢查查詢以查看它們涉及的 tenant id
,並將查詢路由到單個 worker
節點進行處理,特別是保存與 tenant id
關聯的數據分片的節點。 運行將所有相關數據放置在同一節點上的查詢稱為 Table Co-Location
。
下圖說明了多租戶數據模型中的共置(co-location)
。它包含兩個表,Accounts
和 Campaigns
,每個表都由 account_id
分配。陰影框代表分片,每個分片的顏色代表哪個 worker
節點包含它。綠色分片一起存儲在一個 worker
節點上,藍色分片存儲在另一個節點上。 請注意,當將兩個表限制為相同的 account_id
時,Accounts
和 Campaigns
之間的 join
查詢如何將所有必要的數據放在一個節點上。
要在您自己的 schema
中應用此設計,第一步是確定在您的應用程序中構成租戶的內容。 常見實例包括公司(company)
、帳戶(account)
、組織(organization)
或客戶(customer)
。列名稱類似於 company_id
或 customer_id
。檢查您的每個查詢並問自己:如果它有額外的 WHERE
子句將所有涉及的表限制為具有相同 tenant id
的行,它會起作用嗎? 多租戶模型中的查詢通常以租戶為范圍,例如銷售或庫存查詢將在某個商店內進行。
最佳實踐
- 按公共
tenant_id
列對分布式表進行分區。 例如,在租戶是公司的SaaS
應用程序中,tenant_id
可能是company_id
。 - 將小型跨租戶表轉換為引用表。 當多個租戶共享一個小信息表時,將其作為參考表分布。
- 限制按
tenant_id
過濾所有應用程序查詢。 每個查詢應一次請求一個租戶的信息。
閱讀多租戶應用程序指南,了解構建此類應用程序的詳細示例。
實時應用
雖然多租戶架構引入了分層結構並使用數據共置(data co-location)
來路由每個租戶的查詢,但實時架構依賴於其數據的特定分布屬性來實現高度並行處理。
我們在實時模型中使 “entity id”
作為分布列的術語,而不是多租戶模型中的租戶 ID
。 典型的實體是用戶(users)
、主機(hosts)
或設備(devices)
。
實時查詢通常要求按日期(date)
或類別(category)
分組的數字聚合。Citus
將這些查詢發送到每個分片以獲得部分結果,並在 coordinator
節點上組裝最終答案。 當盡可能多的節點做出貢獻並且沒有單個節點必須做不成比例的工作時,查詢運行速度最快。
最佳實踐
- 選擇具有高基數的列作為分布列。 為了比較,訂單表上的
status
字段具有新(new)
、已付款(paid)
和已發貨(shipped)
值,是分布列的一個糟糕選擇,因為它只假設這幾個值。 不同值的數量限制了可以保存數據的分片數量以及可以處理數據的節點數量。 在具有高基數的列中,最好另外選擇那些經常用於group-by
子句或作為join
鍵的列。 - 選擇分布均勻的列。 如果您將表分布在偏向某些常見值的列上,則表中的數據將傾向於在某些分片中累積。持有這些分片的節點最終會比其他節點做更多的工作。
- 將事實表和維度表分布在它們的公共列上。 您的事實表只能有一個分布
key
。 在另一個key
上join
的表不會與事實表位於同一位置。 根據join
的頻率和join
行的大小,選擇一個維度來共同定位。 - 將一些維度表更改為引用表。 如果維度表不能與事實表共存,您可以通過將維度表的副本以引用表的形式分發到所有節點來提高查詢性能。
閱讀實時儀表板指南,了解構建此類應用程序的詳細示例。
時間序列數據
在時間序列工作負載中,應用程序在歸檔舊信息的同時查詢最近的信息。
在 Citus
中建模時間序列信息的最常見錯誤是將時間戳本身用作分布列。 基於時間的散列分布將看似隨機的時間分布到不同的分片中,而不是將時間范圍保持在分片中。 但是,涉及時間的查詢通常會參考時間范圍(例如最近的數據),因此這樣的哈希分布會導致網絡開銷。
最佳實踐
- 不要選擇時間戳作為分布列。 選擇不同的分布列。在多租戶應用程序中,使用
租戶 ID
,或在實時應用程序中使用實體 ID
。 - 改為使用 PostgreSQL 表分區。 使用表分區將一個按時間排序的數據大表分解為多個繼承表,每個表包含不同的時間范圍。在
Citus
中分發Postgres
分區的表會為繼承的表創建分片。
閱讀 Timeseries Data 指南,了解構建此類應用程序的詳細示例。
表共置
關系數據庫因其巨大的靈活性和可靠性而成為許多應用程序的首選數據存儲。 從歷史上看,對關系數據庫的一個批評是它們只能在一台機器上運行,當數據存儲需要超過服務器改進時,這會產生固有的限制。 快速擴展數據庫的解決方案是分發它們,但這會產生其自身的性能問題:join
等關系操作需要跨越網絡邊界。共置(Co-location)
是一種策略性地划分數據的做法,將相關信息保存在同一台機器上以實現高效的關系操作,但利用整個數據集的水平可擴展性。
數據共存的原理是數據庫中的所有表都有一個共同的分布列,並以相同的方式跨機器分片,使得具有相同分布列值的行總是在同一台機器上,即使跨不同的表也是如此。 只要分布列提供了有意義的數據分組,就可以在組內執行關系操作。
Citus 中用於 hash 分布表的數據共存
PostgreSQL
的 Citus
擴展在能夠形成數據庫的分布式數據庫方面是獨一無二的。Citus
集群中的每個節點都是一個功能齊全的 PostgreSQL
數據庫,Citus
在頂部添加了單個同構數據庫的體驗。雖然它沒有以分布式方式提供 PostgreSQL
的全部功能,但在許多情況下,它可以通過托管在單台機器上充分利用 PostgreSQL
提供的功能,包括完整的 SQL
支持、事務和外鍵。
在 Citus
中,如果分布列中值的哈希值落在分片的哈希范圍內,則將一行存儲在分片中。 為了確保共置,即使在重新平衡操作之后,具有相同哈希范圍的分片也始終放置在同一個節點上,這樣相等的分布列值始終位於跨表的同一個節點上。
我們發現在實踐中運行良好的分布列是多租戶應用程序中的租戶 ID
。 例如,SaaS
應用程序通常有許多租戶,但它們所做的每個查詢都是特定於特定租戶的。 雖然一種選擇是為每個租戶提供 database
或 schema
,但它通常成本高昂且不切實際,因為可能有許多跨用戶的操作(數據加載、遷移、聚合、分析、schema 更改、備份等)。隨着租戶數量的增加,這變得更難管理。
共置的實際示例
考慮以下表格,這些表格可能是多租戶 Web
分析SaaS
的一部分:
CREATE TABLE event (
tenant_id int,
event_id bigint,
page_id int,
payload jsonb,
primary key (tenant_id, event_id)
);
CREATE TABLE page (
tenant_id int,
page_id int,
path text,
primary key (tenant_id, page_id)
);
現在我們要回答可能由面向客戶的儀表板發出的查詢,例如:“返回租戶六中所有以‘/blog’開頭的頁面在過去一周的訪問次數。”
使用常規 PostgreSQL 表
如果我們的數據位於單個 PostgreSQL
節點中,我們可以使用 SQL
提供的豐富的關系操作集輕松地表達我們的查詢:
SELECT page_id, count(event_id)
FROM
page
LEFT JOIN (
SELECT * FROM event
WHERE (payload->>'time')::timestamptz >= now() - interval '1 week'
) recent
USING (tenant_id, page_id)
WHERE tenant_id = 6 AND path LIKE '/blog%'
GROUP BY page_id;
只要此查詢的工作集適合內存,這是許多應用程序的合適解決方案,因為它提供了最大的靈活性。但是,即使您還不需要擴展,考慮擴展數據模型的影響也會很有用。
按 ID 分布表
隨着租戶數量和為每個租戶存儲的數據的增長,查詢時間通常會增加,因為工作集不再適合內存或 CPU 成為瓶頸。 在這種情況下,我們可以使用 Citus 跨多個節點分片數據。 分片時我們需要做出的第一個也是最重要的選擇是分布列。 讓我們從一個天真的選擇開始,將 event_id
用於事件表,將 page_id
用於頁表:
-- naively use event_id and page_id as distribution columns
SELECT create_distributed_table('event', 'event_id');
SELECT create_distributed_table('page', 'page_id');
鑒於數據分散在不同的 worker
中,我們不能像在單個 PostgreSQL
節點上那樣簡單地執行 join
。相反,我們需要發出兩個查詢:
跨頁表的所有分片(Q1):
SELECT page_id FROM page WHERE path LIKE '/blog%' AND tenant_id = 6;
跨事件表的所有分片(Q2):
SELECT page_id, count(*) AS count
FROM event
WHERE page_id IN (/*…page IDs from first query…*/)
AND tenant_id = 6
AND (payload->>'time')::date >= now() - interval '1 week'
GROUP BY page_id ORDER BY count DESC LIMIT 10;
之后,應用程序需要組合這兩個步驟的結果。
回答查詢所需的數據分散在不同節點上的分片中,每個分片都需要被查詢:
在這種情況下,數據分布會產生很大的缺陷:
- 查詢每個分片的開銷,運行多個查詢
- Q1 的開銷返回許多行給客戶端
- Q2 變得非常大
- 需要在多個步驟中編寫查詢,組合結果,需要在應用程序中進行更改
相關數據分散的一個潛在好處是查詢可以並行化,Citus
會這樣做。 但是,這只有在查詢的工作量遠遠大於查詢許多分片的開銷時才有用。 通常最好避免直接從應用程序中進行如此繁重的工作,例如通過預先聚合數據。
按租戶分布表
再次查看我們的查詢,我們可以看到查詢需要的所有行都有一個共同的維度:tenant_id
。 儀表板只會查詢租戶自己的數據。這意味着,如果同一租戶的數據始終位於單個 PostgreSQL
節點上,那么我們的原始查詢可以由該節點通過對 tenant_id
和 page_id
執行 join
來一次性回答。
在 Citus
中,具有相同分布列值的行保證在同一個節點上。 分布式表中的每個分片實際上都有一組來自其他分布式表的位於同一位置的分片,這些分片包含相同的分布列值(同一租戶的數據)。從頭開始,我們可以創建以 tenant_id
作為分布列的表。
-- co-locate tables by using a common distribution column
SELECT create_distributed_table('event', 'tenant_id');
SELECT create_distributed_table('page', 'tenant_id', colocate_with => 'event');
在這種情況下,Citus
可以回答您將在單個 PostgreSQL
節點上運行而無需修改 (Q1
) 的相同查詢:
SELECT page_id, count(event_id)
FROM
page
LEFT JOIN (
SELECT * FROM event
WHERE (payload->>'time')::timestamptz >= now() - interval '1 week'
) recent
USING (tenant_id, page_id)
WHERE tenant_id = 6 AND path LIKE '/blog%'
GROUP BY page_id;
由於使用了 tenantid
過濾器和 tenantid
上的 join
,Citus
知道可以使用包含特定租戶數據的一組位於同一位置的分片來回答整個查詢,而 PostgreSQL
節點可以在一個步驟中回答該查詢,從而支持完整的 SQL
支持。
在某些情況下,查詢和表 schema
需要進行少量修改,以確保 tenant_id
始終包含在唯一約束和 join
條件中。 但是,這通常是一個簡單的更改,並且避免了在沒有共置的情況下所需的大量重寫。
雖然上面的示例只查詢一個節點,因為有一個特定的 tenant_id = 6
過濾器,但共置還允許我們在所有節點上有效地執行對 tenant_id
的分布式 join
,盡管存在 SQL
限制。
共置意味着更好的功能支持
Citus
通過共置解鎖的功能的完整列表如下:
- 對一組位於同一位置的分片上的查詢的完整
SQL
支持 - 多語句事務支持對一組位於同一位置的分片進行修改
- 通過
INSERT..SELECT
聚合 - 外鍵
- 分布式外部聯接(
outer join
) - Pushdown CTEs(要求
PostgreSQL >=12
)
數據共置是一種強大的技術,可以為關系數據模型提供水平擴展和支持。 使用分布式數據庫遷移或構建應用程序的成本(通過共置實現關系操作)通常大大低於遷移到限制性數據模型(例如 NoSQL
)的成本,並且與單節點數據庫不同,它可以隨着規模的大小而橫向擴展您的業務。有關遷移現有數據庫的更多信息,請參閱過渡到多租戶數據模型。
查詢性能
Citus
通過將傳入查詢分解為多個在工作分片上並行運行的片段查詢(“任務”)來並行化傳入查詢。這使 Citus
可以利用集群中所有節點的處理能力以及每個節點上的單個核心的處理能力來進行每個查詢。由於這種並行化,您可以獲得集群中所有核心的計算能力的累積性能,與單個服務器上的 PostgreSQL
相比,查詢時間顯着減少。
Citus
在規划 SQL
查詢時采用了兩階段優化器。第一階段涉及將 SQL
查詢轉換為它們的交換和關聯形式,以便它們可以下推並在工作線程上並行運行。 如前幾節所述,選擇正確的分布列
和分布方法
允許分布式查詢規划器對查詢應用多種優化。由於網絡 I/O
減少,這會對查詢性能產生重大影響。
Citus
的分布式執行器然后將這些單獨的查詢片段發送到 PostgreSQL worker 實例。 分布式規划器和執行器都有幾個方面可以調整以提高性能。 當這些單獨的查詢片段被發送給 worker
時,查詢優化的第二階段就開始了。worker
只是運行擴展的 PostgreSQL 服務器,他們應用 PostgreSQL
的標准計划和執行邏輯來運行這些片段 SQL
查詢。 因此,任何有助於 PostgreSQL
的優化也有助於 Citus
。PostgreSQL
默認帶有保守的資源設置;因此優化這些配置設置可以顯着縮短查詢時間。
我們在文檔的查詢性能調優部分討論了相關的性能調優步驟。