分散式 PostgreSQL 叢集(Citus),分散式表中的分佈列選擇最佳實踐

為少發表於2022-03-21

確定應用程式型別

Citus 叢集上執行高效查詢要求資料在機器之間正確分佈。這因應用程式型別及其查詢模式而異。

大致上有兩種應用程式在 Citus 上執行良好。資料建模的第一步是確定哪些應用程式型別更接近您的應用程式。

概覽

多租戶應用 實時應用
有時 schema 中有幾十個或數百個表 表數量少
一次與一個租戶(公司/商店)相關的查詢 具有聚合的相對簡單的分析查詢
用於服務 Web 客戶端的 OLTP 工作負載 攝取大量幾乎不可變的資料
為每個租戶分析查詢提供服務的 OLAP 工作負載 通常圍繞著一個大的事件表

示例和特徵

多租戶應用

這些通常是為其他公司、帳戶或組織服務的 SaaS 應用程式。大多數 SaaS 應用程式本質上是關係型的。它們具有跨節點分佈資料的自然維度:只需按 tenant_id 分片。

Citus 使您能夠將資料庫擴充套件到數百萬租戶,而無需重新構建應用程式。 您可以保留所需的關係語義,例如 聯接外來鍵約束事務ACID一致性

  • 示例:為其他企業託管店面的網站,例如數字營銷解決方案或銷售自動化工具。
  • 特徵:與單個租戶相關的查詢,而不是跨租戶加入資訊。這包括為 Web 客戶端提供服務的 OLTP 工作負載,以及為每個租戶提供分析查詢的 OLAP 工作負載。 在您的資料庫模式中擁有數十或數百個表也是多租戶資料模型的一個指標。

使用 Citus 擴充套件多租戶應用程式還需要對應用程式程式碼進行最少的更改。我們支援流行的框架,如 Ruby on RailsDjango

實時分析應用

需要大規模並行性、協調數百個核心以快速獲得數值、統計或計數查詢結果的應用程式。 通過跨多個節點對 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)。它包含兩個表,AccountsCampaigns,每個表都由 account_id 分配。陰影框代表分片,每個分片的顏色代表哪個 worker 節點包含它。綠色分片一起儲存在一個 worker 節點上,藍色分片儲存在另一個節點上。 請注意,當將兩個表限制為相同的 account_id 時,AccountsCampaigns 之間的 join 查詢如何將所有必要的資料放在一個節點上。

要在您自己的 schema 中應用此設計,第一步是確定在您的應用程式中構成租戶的內容。 常見例項包括公司(company)帳戶(account)組織(organization)客戶(customer)。列名稱類似於 company_idcustomer_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。 在另一個 keyjoin 的表不會與事實表位於同一位置。 根據 join 的頻率和 join 行的大小,選擇一個維度來共同定位。
  • 將一些維度表更改為引用表。 如果維度表不能與事實表共存,您可以通過將維度表的副本以引用表的形式分發到所有節點來提高查詢效能。

閱讀實時儀表板指南,瞭解構建此類應用程式的詳細示例。

時間序列資料

在時間序列工作負載中,應用程式在歸檔舊資訊的同時查詢最近的資訊。

Citus 中建模時間序列資訊的最常見錯誤是將時間戳本身用作分佈列。 基於時間的雜湊分佈將看似隨機的時間分佈到不同的分片中,而不是將時間範圍保持在分片中。 但是,涉及時間的查詢通常會參考時間範圍(例如最近的資料),因此這樣的雜湊分佈會導致網路開銷。

最佳實踐

  • 不要選擇時間戳作為分佈列。 選擇不同的分佈列。在多租戶應用程式中,使用租戶 ID,或在實時應用程式中使用實體 ID
  • 改為使用 PostgreSQL 表分割槽。 使用表分割槽將一個按時間排序的資料大表分解為多個繼承表,每個表包含不同的時間範圍。在 Citus 中分發 Postgres 分割槽的表會為繼承的表建立分片。

閱讀 Timeseries Data 指南,瞭解構建此類應用程式的詳細示例。

表共置

關聯式資料庫因其巨大的靈活性和可靠性而成為許多應用程式的首選資料儲存。 從歷史上看,對關聯式資料庫的一個批評是它們只能在一臺機器上執行,當資料儲存需要超過伺服器改進時,這會產生固有的限制。 快速擴充套件資料庫的解決方案是分發它們,但這會產生其自身的效能問題:join 等關係操作需要跨越網路邊界。共置(Co-location) 是一種策略性地劃分資料的做法,將相關資訊儲存在同一臺機器上以實現高效的關係操作,但利用整個資料集的水平可擴充套件性。

資料共存的原理是資料庫中的所有表都有一個共同的分佈列,並以相同的方式跨機器分片,使得具有相同分佈列值的行總是在同一臺機器上,即使跨不同的表也是如此。 只要分佈列提供了有意義的資料分組,就可以在組內執行關係操作。

Citus 中用於 hash 分佈表的資料共存

PostgreSQLCitus 擴充套件在能夠形成資料庫的分散式資料庫方面是獨一無二的。Citus 叢集中的每個節點都是一個功能齊全的 PostgreSQL 資料庫,Citus 在頂部新增了單個同構資料庫的體驗。雖然它沒有以分散式方式提供 PostgreSQL 的全部功能,但在許多情況下,它可以通過託管在單臺機器上充分利用 PostgreSQL 提供的功能,包括完整的 SQL 支援、事務和外來鍵。

Citus 中,如果分佈列中值的雜湊值落在分片的雜湊範圍內,則將一行儲存在分片中。 為了確保共置,即使在重新平衡操作之後,具有相同雜湊範圍的分片也始終放置在同一個節點上,這樣相等的分佈列值始終位於跨表的同一個節點上。

我們發現在實踐中執行良好的分佈列是多租戶應用程式中的租戶 ID。 例如,SaaS 應用程式通常有許多租戶,但它們所做的每個查詢都是特定於特定租戶的。 雖然一種選擇是為每個租戶提供 databaseschema,但它通常成本高昂且不切實際,因為可能有許多跨使用者的操作(資料載入、遷移、聚合、分析、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_idpage_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 上的 joinCitus 知道可以使用包含特定租戶資料的一組位於同一位置的分片來回答整個查詢,而 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 的優化也有助於 CitusPostgreSQL 預設帶有保守的資源設定;因此優化這些配置設定可以顯著縮短查詢時間。

我們在文件的查詢效能調優部分討論了相關的效能調優步驟。

更多

相關文章