分散式 PostgreSQL 叢集(Citus)官方教程 - 遷移現有應用程式

為少發表於2022-03-16

將現有應用程式遷移到 Citus 有時需要調整 schema 和查詢以獲得最佳效能。 Citus 擴充套件了 PostgreSQL 的分散式功能,但它不是擴充套件所有工作負載的直接替代品。高效能 Citus 叢集需要考慮資料模型、工具和所使用的 SQL 功能的選擇。

第一步是優化現有的資料庫模式,以便它可以在多臺計算機上高效工作。

  • 確定分佈策略
    • 選擇分佈鍵(distribution key)
    • 識別表的型別
  • 為遷移準備源表
    • 新增分佈鍵
    • 回填新建立的列

接下來,更新應用程式程式碼和查詢以處理 schema 更改。

  • 準備申請 Citus
    • 建立開發 Citus 叢集
    • 向查詢新增分佈鍵
    • 啟用安全連線
    • 檢查跨節點流量

在開發環境中測試更改後,最後一步是將生產資料遷移到 Citus 叢集並切換生產應用程式。我們有技術可以最大限度地減少此步驟的停機時間。

  • 遷移生產資料
    • 小型資料庫遷移
    • 大資料庫遷移

確定分佈策略

選擇分佈鍵

遷移到 Citus 的第一步是確定合適的distribution key 並相應地規劃表分佈。 在多租戶應用程式中,這通常是租戶的內部識別符號。我們通常將其稱為“租戶 ID(tenant ID)”。用例可能會有所不同,因此我們建議您在此步驟中進行徹底檢查。

如需指導,請閱讀以下部分:

  1. 確定應用程式型別
  2. 選擇分佈列

我們很樂意幫助您檢查您的環境,以確保選擇了理想的 distribution key。 為此,我們通常會檢查 schema 佈局、更大的表、長時間執行和/或有問題的查詢、標準用例等。

確定表的型別

一旦確定了 distribution key,請檢視 schema 以確定如何處理每個表以及是否需要對錶佈局進行任何修改。我們通常建議使用電子表格進行跟蹤,並建立了您可以使用的模板

表格通常屬於以下類別之一:

  1. 準備分發。 這些表已經包含 distribution key,並準備好分發。
  2. 需要回填。 這些表可以按所選 key 進行邏輯分佈,但不包含直接引用它的列。稍後將修改這些表以新增該列。
  3. 參考表。 這些表通常很小,不包含 distribution key,通常由分散式表連線,和/或在租戶之間共享。這些表中的每一個的副本將在所有節點上維護。常見示例包括國家程式碼查詢、產品類別等。
  4. 本地表。 這些通常不連線到其他表,並且不包含 distribution key。 它們僅在 coordinator 節點上維護。常見示例包括管理員使用者查詢和其他實用程式表。

考慮一個類似於 EtsyShopify 的示例多租戶應用程式,其中每個租戶都是商店。這是簡化 schema 的一部分:

(帶下劃線的專案是主鍵,斜體專案是外來鍵。)

在此示例中,商店是自然租戶。在這種情況下,租戶 IDstore_id。 在叢集中分佈表之後,我們希望與同一儲存相關的行一起駐留在同一節點上。

為遷移準備源表

一旦確定了所需資料庫更改的範圍,下一個主要步驟就是修改應用程式現有資料庫的資料結構。首先,修改需要回填的表,為 distribution key 新增一列。

新增分佈鍵

在我們的店面示例中,storesproducts 表有一個 store_id 並準備好分佈。規範化後,line_items 表缺少商店 ID。如果我們想通過 store_id 分佈,表需要這個列。

-- denormalize line_items by including store_id

ALTER TABLE line_items ADD COLUMN store_id uuid;

請務必檢查所有表中的分佈列是否具有相同的型別,例如:不要混合 intbigint。列型別必須匹配以確保正確的資料託管。

回填新建立的列

更新 schema 後,在新增該列的表中回填 tenant_id 列的缺失值。 在我們的示例中,line_items 需要 store_id 的值。

我們通過從帶有訂單的 join 查詢中獲取缺失值來回填表:

UPDATE line_items
   SET store_id = orders.store_id
  FROM line_items
 INNER JOIN orders
 WHERE line_items.order_id = orders.order_id;

一次執行整個表可能會導致資料庫負載過大並中斷其他查詢。 相反,回填可以更慢地完成。 一種方法是建立一個一次回填小批量的函式,然後使用 pg_cron 重複呼叫該函式。

-- the function to backfill up to one
-- thousand rows from line_items

CREATE FUNCTION backfill_batch()
RETURNS void LANGUAGE sql AS $$
  WITH batch AS (
    SELECT line_items_id, order_id
      FROM line_items
     WHERE store_id IS NULL
     LIMIT 10000
       FOR UPDATE
      SKIP LOCKED
  )
  UPDATE line_items AS li
     SET store_id = orders.store_id
    FROM batch, orders
   WHERE batch.line_item_id = li.line_item_id
     AND batch.order_id = orders.order_id;
$$;

-- run the function every quarter hour
SELECT cron.schedule('*/15 * * * *', 'SELECT backfill_batch()');

-- ^^ note the return value of cron.schedule

回填完成後,可以禁用 cron job

-- assuming 42 is the job id returned
-- from cron.schedule

SELECT cron.unschedule(42);

準備申請 Citus

建立開發 Citus 叢集

在修改應用程式以使用 Citus 時,您需要一個資料庫來進行測試。 按照說明設定您選擇的單節點 Citus

接下來從應用程式的原始資料庫中轉儲 schema 的副本,並在新的開發資料庫中恢復 schema

# get schema from source db

pg_dump \
   --format=plain \
   --no-owner \
   --schema-only \
   --file=schema.sql \
   --schema=target_schema \
   postgres://user:pass@host:5432/db

# load schema into test db

psql postgres://user:pass@testhost:5432/db -f schema.sql

schema 應在您希望分發的所有表中包含一個分發鍵(tenant id)。在 pg_dumping schema 之前,請確保您已完成上一節中的準備源表以進行遷移的步驟。

在鍵中包含分佈列

Citus 不能強制唯一性約束,除非唯一索引或主鍵包含分佈列。因此,我們必須在示例中修改主鍵和外來鍵以包含 store_id

下一節中列出的一些庫能夠幫助遷移資料庫 schema 以將分佈列包含在鍵中。 然而,下面是一個底層 SQL 命令示例,用於在開發資料庫中組合簡單鍵:

BEGIN;

-- drop simple primary keys (cascades to foreign keys)

ALTER TABLE products   DROP CONSTRAINT products_pkey CASCADE;
ALTER TABLE orders     DROP CONSTRAINT orders_pkey CASCADE;
ALTER TABLE line_items DROP CONSTRAINT line_items_pkey CASCADE;

-- recreate primary keys to include would-be distribution column

ALTER TABLE products   ADD PRIMARY KEY (store_id, product_id);
ALTER TABLE orders     ADD PRIMARY KEY (store_id, order_id);
ALTER TABLE line_items ADD PRIMARY KEY (store_id, line_item_id);

-- recreate foreign keys to include would-be distribution column

ALTER TABLE line_items ADD CONSTRAINT line_items_store_fkey
  FOREIGN KEY (store_id) REFERENCES stores (store_id);
ALTER TABLE line_items ADD CONSTRAINT line_items_product_fkey
  FOREIGN KEY (store_id, product_id) REFERENCES products (store_id, product_id);
ALTER TABLE line_items ADD CONSTRAINT line_items_order_fkey
  FOREIGN KEY (store_id, order_id) REFERENCES orders (store_id, order_id);

COMMIT;

至此完成,上一節中的 schema 將如下所示:

(帶下劃線的專案是主鍵,斜體專案是外來鍵。)

請務必修改資料流以向傳入資料新增鍵。

向查詢新增分佈鍵

一旦 distribution key 出現在所有適當的表上,應用程式就需要將它包含在查詢中。 以下步驟應使用在開發環境中執行的應用程式副本完成,並針對 Citus 後端進行測試。在應用程式與 Citus 一起工作後,我們將瞭解如何將生產資料從源資料庫遷移到真正的 Citus 叢集中。

  • 應更新寫入表的應用程式程式碼和任何其他攝取程式以包含新列。
  • Citus 上針對修改後的 schema 執行應用程式測試套件是確定哪些程式碼區域需要修改的好方法。
  • 啟用資料庫日誌記錄是個好主意。 這些日誌可以幫助發現多租戶應用程式中的雜散跨分片查詢,這些查詢應轉換為每租戶查詢。

支援跨分片查詢,但在多租戶應用程式中,大多數查詢應針對單個節點。 對於簡單的 selectupdatedelete 查詢,這意味著 where 子句應按 tenant id 進行過濾。Citus 然後可以在單個節點上有效地執行這些查詢。

許多流行的應用程式框架都有一些幫助程式庫,可以很容易地在查詢中包含租戶 ID

可以先將庫用於資料庫寫入(包括資料攝取),然後再用於讀取查詢。 例如,activerecord-multi-tenant gem 有一個只修改寫查詢的只寫模式(write-only mode)

其他(SQL原則)

如果您使用與上述不同的 ORM,或者更直接地在 SQL 中執行多租戶查詢,請遵循這些一般原則。 我們將使用我們之前的電子商務應用程式示例。

假設我們想要獲取訂單的詳細資訊。過濾租戶 ID 的分散式查詢在多租戶應用程式中執行效率最高,因此下面的更改使查詢更快(而兩個查詢返回相同的結果):

-- before
SELECT *
  FROM orders
 WHERE order_id = 123;

-- after
SELECT *
  FROM orders
 WHERE order_id = 123
   AND store_id = 42; -- <== added

租戶 id 列不僅對插入語句有益,而且至關重要。插入必須包含租戶 id 列的值,否則 Citus 將無法將資料路由到正確的分片並引發錯誤。

最後,在 join 表時,請確保也按租戶 ID 進行過濾。 例如,這裡是如何檢查給定商店已售出多少“很棒的羊毛褲”:

-- One way is to include store_id in the join and also
-- filter by it in one of the queries

SELECT sum(l.quantity)
  FROM line_items l
 INNER JOIN products p
    ON l.product_id = p.product_id
   AND l.store_id = p.store_id
 WHERE p.name='Awesome Wool Pants'
   AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'

-- Equivalently you omit store_id from the join condition
-- but filter both tables by it. This may be useful if
-- building the query in an ORM

SELECT sum(l.quantity)
  FROM line_items l
 INNER JOIN products p ON l.product_id = p.product_id
 WHERE p.name='Awesome Wool Pants'
   AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'
   AND p.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75'

啟用安全連線

客戶端應使用 SSL 連線到 Citus 以保護資訊並防止中間人攻擊。 事實上,Citus Cloud 拒絕未加密的連線。要了解如何建立 SSL 連線,請參閱使用 SSL 連線

檢查跨節點流量

對於龐大而複雜的應用程式程式碼庫,應用程式生成的某些查詢通常會被忽略,因此不會對它們使用 tenant_id 過濾器。Citus 的並行執行器仍然會成功執行這些查詢,因此,在測試期間,這些查詢仍然隱藏,因為應用程式仍然可以正常工作。但是,如果查詢不包含 tenant_id 過濾器,Citus 的執行程式將並行訪問每個分片,但只有一個會返回資料。 這會不必要地消耗資源,並且只有在遷移到更高吞吐量的生產環境時才會出現問題。

為了防止在生產中啟動後才遇到此類問題,可以設定一個配置值來記錄命中多個分片的查詢。在正確配置和遷移的多租戶應用程式中,每個查詢一次只能命中一個分片。

在測試期間,可以配置以下內容:

-- adjust for your own database's name of course

ALTER DATABASE citus SET citus.multi_task_query_log_level = 'error';

如果 Citus 遇到將命中多個分片的查詢,它將出錯。 測試期間出錯允許應用程式開發人員查詢和遷移此類查詢。

在生產啟動期間,可以配置相同的設定來記錄,而不是錯誤輸出:

ALTER DATABASE citus SET citus.multi_task_query_log_level = 'log';

配置引數部分包含有關此設定支援的值的更多資訊。

遷移生產資料

此時,已更新資料庫 schema 和應用程式查詢以與 Citus 一起使用,您已準備好進行最後一步。是時候將資料遷移到 Citus 叢集並將應用程式切換到其新資料庫了。

資料遷移路徑取決於停機時間要求資料大小,但通常屬於以下兩類之一。

  • 小型資料庫遷移
  • 大資料庫遷移

小型資料庫遷移

對於可以容忍一點停機時間的較小環境,請使用簡單的 pg_dump/pg_restore 程式。以下是步驟。

  1. 從您的開發資料庫中儲存資料庫結構:

    pg_dump \
       --format=plain \
       --no-owner \
       --schema-only \
       --file=schema.sql \
       --schema=target_schema \
       postgres://user:pass@host:5432/db
    
  2. 使用 psql 連線到 Citus 叢集並建立 schema

    \i schema.sql
    
  3. 執行您的 create_distributed_tablecreate_reference_table 語句。如果您收到有關外來鍵的錯誤,通常是由於操作順序所致。 在分發表之前刪除外來鍵,然後重新新增它們。

  4. 將應用程式置於維護模式,並禁用對舊資料庫的任何其他寫入。

  5. 使用 pg_dump 將原始生產資料庫中的資料儲存到磁碟:

    pg_dump \
       --format=custom \
       --no-owner \
       --data-only \
       --file=data.dump \
       --schema=target_schema \
       postgres://user:pass@host:5432/db
    
  6. 使用 pg_restore 匯入 Citus

    # remember to use connection details for Citus,
    # not the source database
    pg_restore  \
       --host=host \
       --dbname=dbname \
       --username=username \
       data.dump
    
    # it'll prompt you for the connection password
    
  7. 測試應用。

  8. 執行。

大資料庫遷移(Citus Cloud)

較大的環境可以使用 Citus Warp 進行線上複製。Citus Warp 允許您在更改發生時將更改從 PostgreSQL 源資料庫流式傳輸到 Citus Cloud 叢集。 就好像應用程式自動寫入兩個資料庫而不是一個,除非具有完美的事務邏輯。Citus Warp 可與啟用了 logical_decoding 外掛的 Postgres 9.4 及更高版本一起使用(只要您使用的是 9.4 或更高版本,Amazon RDS 就支援此功能)。

對於此過程,我們強烈建議您通過開 ticket、聯絡我們在 Slack 上的解決方案工程師之一或任何適合您的方法來聯絡我們。為了進行 warp,我們通過 VPC 對等或 IP 白名單Citus 叢集的 coordinator 節點連線到現有資料庫,並開始複製。

以下是開始 Citus Warp 流程之前需要執行的步驟:

  1. 在目標 Citus 叢集上覆制 schema 結構
  2. 在源資料庫中啟用邏輯複製
  3. 允許從 Citus coordinator 節點到源的網路連線
  4. 聯絡我們開始複製

重複 schema

將資料遷移到 Citus 的第一步是確保 schema 完全匹配,至少對於您選擇遷移的表而言。 一種方法是針對您的開發資料庫(用於本地測試應用程式的 Citus 資料庫)執行 pg_dump --schema-only。在 coordinator Citus 節點上重放輸出。 另一種方法是針對目標資料庫執行應用程式遷移指令碼。

您希望遷移的所有表都必須具有主鍵。 相應的目標表也必須具有主鍵,唯一的區別是這些鍵也允許組合以包含分佈列,如識別分佈策略中所述。

還要確保在開始複製之前在叢集中分佈表,這樣資料就不必單獨放在 coordinator 節點上。

啟用邏輯複製

某些託管資料庫(例如 Amazon RDS)需要通過更改伺服器配置引數來啟用複製。在 RDS 上,您需要建立一個新引數組,在其中設定 rds.logical_replication = 1,然後將引數組設為活動引數組。 應用更改需要重新啟動資料庫伺服器,這可以安排在下一個維護時段。

如果您正在管理自己的 PostgreSQL 安裝,請將這些設定新增到 postgresql.conf

wal_level = logical
max_replication_slots = 5 # has to be > 0
max_wal_senders = 5       # has to be > 0

需要重新啟動資料庫才能使更改生效。

開放訪問網路連線

在 Cloud 控制檯中,確定主機名(以 db.citusdata.com 結尾)。Dig 主機名以找到其 IP 地址:

dig +short <hostname> A

如果您使用的是 RDS,請編輯入站資料庫安全組以新增自定義 TCP 規則:

Protocol

  • TCP

Port Range

  • 5432

Source

  • /32

這會將 Citus coordinator 節點的 IP 地址列入白名單以進行入站連線。 連線兩者的另一種方法是在它們的 VPC 之間建立對等互連。如果需要,我們可以幫助進行設定。

開始複製

通過在 Citus Cloud 控制檯中開啟 support ticket 與我們聯絡。雲工程師將使用 Citus Warp 連線到您的資料庫,以執行初始資料庫轉儲、開啟復制槽並開始複製。 我們可以在遷移中包含/排除您選擇的表。

在複製的第一階段,如果資料庫處於寫入負載下,Postgres 預寫日誌 (WAL) 可能會大幅增長。 在開始此過程之前,請確保源資料庫上有足夠的磁碟空間。我們建議 100GB 可用空間或總磁碟空間的 20%,以較大者為準。一旦初始 dump/restore 完成並開始複製,那麼資料庫將能夠再次歸檔未使用的 WAL 檔案。

隨著 Warp 的進行,請注意源資料庫上的磁碟使用情況。 如果源和目標之間存在資料型別不匹配,或其他意外的 schema 更改,則複製可能會停止。 在長時間停頓期間,複製槽可以在源上無限增長,從而導致潛在的崩潰。

由於複製停滯的可能性,我們強烈建議在進行 Citus warp 時儘量減少 schema 更改。 如果需要進行侵入式 schema 更改,您將需要停止 warp 並重試。

進行侵入式 schema 更改的步驟:

  1. 請求 Citus Cloud 工程師停止 warp
  2. 更改源資料庫上的 schema
  3. 更改目標資料庫上的 schema
  4. 再次開始 warp

切換到 Citus 並停止與舊資料庫的所有連線

當複製趕上源資料庫的當前狀態時,還有一件事要做。 由於複製過程的性質,序列值不會在目標資料庫上正確更新。為了獲得正確的序列值,例如 id 列,您需要在開啟對目標資料庫的寫入之前手動調整序列值。

一旦這一切完成,應用程式就可以連線到新資料庫了。 我們不建議同時寫入源資料庫和目標資料庫。

當應用程式切換到新資料庫並且源資料庫上沒有發生進一步的更改時,請再次聯絡我們以刪除複製槽。 遷移完成。

更多

相關文章