Sequences

多くのアプリケーションでは、一意のサロゲートIDをデータベースエントリに割り当てる必要があります。多くの場合、データベースSEQUENCE オブジェクトを使用してこれらを生成します。 PostgreSQLでは、これらは次のいずれかです。

  • CREATE SEQUENCE コマンドを使用して手動で作成され、nextval() 関数を呼び出すことによって取得されたシーケンス

  • serial およびbigserial 列、またはGENERATED BY DEFAULT AS IDENTITY 列

ただし、PostgreSQLの標準シーケンスはマルチノードを認識せず、ローカルノードでのみ一意の値を生成します。このようなシーケンスによって生成された一意のIDは、マルチマスターレプリケーションで破棄されたINSERT アクションにより競合とデータ損失を引き起こすため、これは重要です。

PGDグローバルシーケンス

このため、PGDは、 グローバルシーケンス と呼ばれるPGDグループ全体でbigintまたはbigserialデータ型のシーケンスを使用して一意のIDを生成するアプリケーション透過的な方法を提供します。

PGDグローバルシーケンスは、アプリケーションがデータベースを使用して、非同期分散システムで一意の合成キーを生成する簡単な方法を提供します。ほとんどの場合に機能しますが、必ずしもすべての場合とは限りません。

PGDグローバルシーケンスを使用すると、挿入の競合に関する問題を回避できます。グローバルシーケンスを使用している列にPRIMARY KEY またはUNIQUE 制約を定義した場合、ノードは他のノードと同じ値を取得できません。 PGDがノード間の挿入を同期する場合、競合することはありません。

PGDグローバルシーケンスはPostgreSQLシーケンスを拡張するため、クラッシュセーフです。それらを使用するには、 bdr_application ロールが付与されている必要があります。

グローバルシーケンスにはさまざまなアルゴリズムが考えられます。

  • SnowflakeIdシーケンス

  • グローバルに割り当てられた範囲シーケンス

SnowflakeIdシーケンスは、任意の時点でノード間通信を必要としないアルゴリズムを使用して値を生成します。より高速で堅牢で、値が作成されたときのタイムスタンプを記録する便利なプロパティがあります。

SnowflakeIdシーケンスには、64ビットBIGINTデータ型でのみ機能し、最大19桁の長さの値を生成するという制限があります。これは、Javascript Integer型などの一部のホスト言語データ型で使用するには長すぎる場合があります。グローバルに割り当てられたシーケンスは、ノード間のコンセンサスによって必要に応じて補充できる値のローカル範囲を割り当て、BIGINTまたはINTEGERシーケンスに適したものにします。

bdr.alter_sequence_set_kind() を使用してグローバルシーケンスを作成できます

機能。この関数は、標準のPostgreSQLシーケンスを取り、それをPGDグローバルシーケンスとしてマークします。また、シーケンスを標準のPostgreSQLシーケンスに変換して戻すこともできます。

PGDは、構成変数 bdr.default_sequence_kind も提供します。これは、 CREATE SEQUENCE コマンドの実行時、または serial 、bigserial 、または GENERATED BY DEFAULT AS IDENTITY 列の作成時に作成するシーケンスの種類を決定します。有効な設定は次のとおりです。

  • local 、新しく作成されたシーケンスが標準のPostgreSQL(ローカル)シーケンスであることを意味します。

  • galloc 、常にグローバルに割り当てられた範囲シーケンスを作成します。

  • snowflakeid 、時間、ノードID、およびカウンターコンポーネントで構成されるBIGINTシーケンスのグローバルシーケンスを作成します。 INTEGERシーケンスでは使用できません(したがって、 bigserial には使用できますが、 serial には使用できません)。

  • timeshard 、これはSnowflakeIdシーケンスの古いバージョンであり、下位互換性のためにのみ提供されています。 SnowflakeIdが優先されます。

  • distributed (デフォルト)、 :ref:` <reference/pgdpostgres-settings/#global-sequence-parameters>` にのみ使用できる特別な値。 int8 シーケンス(つまりbigserial )にはsnowflakeid を選択し、 int4 (つまりserial )およびint2 シーケンスにはgalloc シーケンスを選択します。

bdr.シーケンス ビューは、個々のシーケンスの種類に関する情報を表示します。

currval() およびlastval() は、すべてのタイプのグローバルシーケンスで正しく機能します。

SnowflakeIdシーケンス

SnowflakeIdシーケンスによって生成されたIDは大まかな時間順序付けであるため、標準のPostgreSQLシーケンスのように、それらを使用してデータ挿入のおおよその順序を取得できます。同じミリ秒内に生成された値は、1つのノードでも、順不同の場合があります。緩い時間順序のプロパティは、レンジパーティションキーとしての使用に適していることを意味します。

SnowflakeIdシーケンスは1つ以上のノードで機能し、ノード参加プロセスが完了した後にノード間通信を必要としません。そのため、ネットワークパーティションが拡張されるリスクがあっても、引き続き使用できます。レプリケーションラグやノード間のレイテンシーの影響を受けません。

SnowflakeIdシーケンスは、標準シーケンスとは異なる方法で一意のIDを生成します。このアルゴリズムでは、シーケンス番号に3つのコンポーネントを使用します。シーケンスの最初のコンポーネントは、シーケンス番号生成時のタイムスタンプです。シーケンス番号の2番目のコンポーネントは、各PGDノードに割り当てられた一意のIDです。これにより、さまざまなノードのIDが常に異なることが保証されます。 3番目のコンポーネントは、ローカルシーケンスによって生成された番号です。

シーケンス番号に一意のノードIDを追加するだけで競合がないことを保証できますが、シーケンスの別の便利なプロパティも維持したいでしょう。シーケンス番号の順序は、データがテーブルに挿入された順序にほぼ対応しています。タイムスタンプを最初に置くと、これが保証されます。

SnowflakeIdシーケンスには、いくつかの制限と警告が適用されます。

SnowflakeIdシーケンスは64ビット幅で、 bigint またはbigserial が必要です。生成される値は最大19桁です。実用的な32ビットinteger バージョンはないため、 serial シーケンスでは使用できません。代わりに、グローバルに割り当てられた範囲シーケンスを使用します。

SnowflakeIdの場合、特定のノードでミリ秒ごとに生成されるシーケンス値は4096個(1秒あたり約400万シーケンス値)の制限があります。シーケンス値の生成が指定されたミリ秒以内にラップアラウンドする場合、SnowflakeIdシーケンスは次のミリ秒まで待機し、そのミリ秒の新しい値を取得します。

SnowflakeIdシーケンスはタイムスタンプをシーケンス値にエンコードするため、指定されたタイムフレーム内でのみ(システムクロックに応じて)新しいシーケンス値を生成できます。使用できる最も古いタイムスタンプは2016-10-07で、これはSnowflakeIdのエポックタイムです。値は2086年に負の値にラップし、2156までに数が完全に不足します。

タイムスタンプはSnowflakeIdシーケンスの重要な部分であるため、 Postgresプロセスの有効期間中に使用された最新のものよりも古いタイムスタンプを持つシーケンスを生成しないようにする追加の保護があります(ただし、Postgresの再起動間ではありません)。

SnowflakeIdシーケンスの入力として使用されるシーケンスのINCREMENT オプションは、効果的に無視されます。これは、多くのオブジェクトリレーショナルマッパー(ORM)ツール、特にHibernateのように、シーケンスIDキャッシングを行うアプリケーションに関連する場合があります。シーケンスは時間ベースであるため、アプリケーションがキャッシュされた値を使用して何かを実行できるようになるまでに、シーケンスは新しい非衝突値に進むため、これは実際的な効果はほとんどありません。

同様に、基になるシーケンスのSTART 、MINVALUE 、MAXVALUE 、およびCACHE 設定を変更する場合がありますが、変更してもメリットはありません。シーケンスの下位14ビットが使用され、残りは破棄されるため、値の範囲の制限は関数の結果に影響しません。同じ理由で、 setval() はSnowflakeIdシーケンスには役立ちません。

Timeshardシーケンス

Timeshardシーケンスは、既存のインストールとの下位互換性のために提供されていますが、新しいアプリケーションでの使用はお勧めしません。代わりに SnowflakeId シーケンスを使用することをお勧めします。

TimeshardはSnowflakeIdと非常に似ていますが、制限が異なり、保護が少なく、パフォーマンスが低下します。

timeshardとSnowflakeIdの違いは次のとおりです。

  • Timeshardは、SnowflakeIdよりも多い、ミリ秒あたり最大16384個(1秒あたり約1600万個)を生成できます。ただし、指定されたミリ秒内のラップアラウンドに対する保護はありません。タイムハードシーケンスを使用するスキーマは、特定の列にタイムハード値を使用する場合、 UNIQUE 制約の使用を保護する必要があります。 -timeshardシーケンスのタイムスタンプコンポーネントは、2050年に値を使い果たし、bigintと組み合わせて使用すると、値は2033年に負の数にラップします。これは、2033年以降に生成されたシーケンスが負の値を持つことを意味します。これはSnowflakeIdよりもかなり短い時間スパンであり、これがSnowflakeIdが好ましい主な理由です。 - タイムシャードシーケンスでは、時折ディスク書き込みが必要です(標準のローカルシーケンスと同様)。 SnowflakeIdはメモリ内で計算されるため、SnowflakeIdシーケンスは一般的にtimehardシーケンスよりも少し高速です。

グローバルに割り当てられた範囲シーケンス

グローバルに割り当てられた範囲(またはgalloc )シーケンスは、各ノードに値の範囲(チャンク)を割り当てます。ローカル範囲が使い果たされると、新しい範囲が他のノード間のコンセンサスによってグローバルに割り当てられます。これはキースペースを効率的に使用しますが、現在割り当てられているローカル範囲が使い果たされたときにシーケンスジェネレーターが進行するには、ローカルノードをクラスター内の大部分のノードに接続する必要があります。

SnowflakeIdシーケンスとは異なり、 galloc シーケンスは、PostgreSQLが提供するすべてのシーケンスデータ型:smallint 、integer 、およびbigint をサポートします。これは、64ビットシーケンスに問題がある環境でgalloc シーケンスを使用できることを意味します。例には、53ビット値のみをサポートするため、またはシーケンスが限られたスペースで出力に表示されるため、JavaScriptで整数を使用することが含まれます。

各投票によって割り当てられる範囲は、シーケンスが使用しているデータ型に基づいて現在事前に決定されています。

  • smallint - smallint 1 000の数字

  • 整数 - 1 000 000番号

  • bigint — 1 000 000 000番号

各ノードはseq_chunk_sizeの2つのチャンクを割り当てます。1つは現在の使用に加え、もう1つは将来の使用のために予約されたチャンクのため、1つのノードから生成される値は単調に増加します。ただし、グローバルに見ると、生成される値はまったく順序付けられていません。これは、bツリーインデックスへの影響によりパフォーマンスの低下を引き起こす可能性があり、通常は、生成された値が範囲パーティションキーとして役に立たないことを意味します。

galloc シーケンスの主な欠点は、割り当てられた範囲が使い果たされると、シーケンスジェネレーターは、ノード間通信を必要とするローカルノードの次の範囲についてコンセンサスを求める必要があることです。 PGDグループの大部分にアクセスできない場合、これにより、遅延または運用上の問題が発生する可能性があります。 (これは、後のリリースでは回避される可能性があります。)

CACHE 、START 、MINVALUE 、およびMAXVALUE オプションは、 galloc シーケンスで正しく機能します。ただし、シーケンスをgalloc 種類に変換する前に設定する必要があります。 INCREMENT BY オプションも正しく動作します。ただし、シーケンスデータ型ごとに割り当てられた上記の範囲以上のインクリメント値を割り当てることはできません。 setval() はgalloc シーケンスのグローバル状態をリセットしません。使用しないでください。

galloc シーケンスには、いくつかの制限が適用されます。 PGDは、特別なPGDカタログ bdr.sequence_alloc でgalloc シーケンスを追跡します。このカタログは、 galloc シーケンスに現在割り当てられているチャンクを追跡するために必要です。シーケンス名と名前空間は、このカタログに格納されます。シーケンスチャンクの割り当てはRaftによって管理されますが、シーケンス名前/名前空間の変更はレプリケーションストリームによって管理されるため、PGDは現在、 galloc シーケンスの名前変更、別の名前空間への移動、またはgalloc シーケンスを含む名前空間の名前変更をサポートしていません。アプリケーションスキーマを設計する際は、この制限に注意してください。

ローカルシーケンスからgallocシーケンスへの変換

ローカルシーケンスをgallocに変換する前に、いくつかの前提条件を満たす必要があります。

1.シーケンスと列のデータ型が一致することを確認します

シーケンスのデータ型が、使用される列のデータ型と一致することを確認します。たとえば、 bigint シーケンスを作成し、 integer 列のデフォルトをそのシーケンスによって返されるnextval() に割り当てることができます。 bigint に1 000 000 000のブロックで割り当てられるgallocシーケンスを使用すると、3つ以上のノードが使用されている場合、すぐにnextval() によって返される値がint4 の範囲を超えます。

次の例は、何が起こるかを示しています。

CREATE SEQUENCE int8_seq;

SELECT sequencename, data_type FROM pg_sequences;
 sequencename | data_type
- -------------+-----------
 int8_seq     | bigint
(1 row)

CREATE TABLE seqtest (id INT NOT NULL PRIMARY KEY);

ALTER SEQUENCE int8_seq OWNED BY seqtest.id;

SELECT bdr.alter_sequence_set_kind(public.int8_seq::regclass, galloc, 1);
 alter_sequence_set_kind
- ------------------------

(1 row)

ALTER TABLE seqtest ALTER COLUMN id SET DEFAULT nextval(int8_seq::regclass);

2つのノードでINSERT INTO seqtest VALUES(DEFAULT) を実行した後、テーブルには次の値が含まれます。

SELECT * FROM seqtest;
     id
- -----------
          2
 2000000002
(2 rows)

ただし、シーケンスが値4000000002 を生成したため、3番目のノードで同じ操作を試みると、 integer out of range エラーで失敗します。

Tip

PostgreSQLからシーケンスの現在のデータ型を取得できます pg_sequences

ビュー。現在の値が新しいデータ型の最大値を超えない限り、ALTER SEQUENCE ... AS ... 、たとえばALTER SEQUENCE public.sequence AS integer を使用してシーケンスのデータ型を変更できます。

2.シーケンスの新しい開始値を設定します

シーケンスの種類がgalloc に変更されると、書き換えられ、ローカルシーケンスの定義された開始値からリスタートします。これが本番データベース内の既存のシーケンスで発生した場合は、現在の値を照会してから、開始値を適切に設定する必要があります。このユースケースを支援するために、PGDでは、ユーザーが関数

を使用して開始値を渡すことができます。すでにオフセットを使用していて、複数のノードからの書き込みがある場合は、使用された最大の値を確認し、少なくとも次の値までシーケンスをリスタートする必要があります。

- - determine highest sequence value across all nodes
SELECT max((x->response->command_tuples->0->>nextval)::bigint)
    FROM json_array_elements(
        bdr.run_on_all_nodes(
            ESELECT nextval(\public.sequence\);
            )::jsonb AS x;

- - turn into a galloc sequence
SELECT bdr.alter_sequence_set_kind(public.sequence::regclass, galloc, $MAX + $MARGIN);

ユーザーはシーケンスをロックできないため、 max() 値が照会されている間、操作を続行できるように$MARGIN 値を残す必要があります。

bdr.sequence_alloc テーブルは、チャンクサイズとクラスター全体に割り当てられた範囲に関する情報を提供します。

この例では、シーケンスは333 で始まり、クラスターには2つのノードがあります。割り当て数は4つ、1ノードあたり2つ、チャンクサイズは1000000整数列関連です。

SELECT * FROM bdr.sequence_alloc
    WHERE seqid = public.categories_category_seq::regclass;
          seqid          | seq_chunk_size | seq_allocated_up_to | seq_nallocs |       seq_last_alloc
- ------------------------+----------------+---------------------+-------------+-----------------------------
 categories_category_seq |        1000000 |             4000333 |           4 | 2020-05-21 20:02:15.957835+00
(1 row)

各ノードの特定のシーケンスに現在割り当てられている範囲を表示するには、次のクエリを使用します。

  • ノードNode1 は333 から2000333 の範囲を使用しています。

SELECT last_value AS range_start, log_cnt AS range_end
    FROM categories_category_seq WHERE ctid = (0,2); -- first range
 range_start | range_end
- ------------+-----------
         334 |   1000333
(1 row)

SELECT last_value AS range_start, log_cnt AS range_end
    FROM categories_category_seq WHERE ctid = (0,3); -- second range
 range_start | range_end
- ------------+-----------
     1000334 |   2000333
(1 row)
  • ノードNode2 は2000004 から4000003 の範囲を使用しています。

SELECT last_value AS range_start, log_cnt AS range_end
    FROM categories_category_seq WHERE ctid = (0,2); -- first range
 range_start | range_end
- ------------+-----------
     2000334 |   3000333
(1 row)

SELECT last_value AS range_start, log_cnt AS range_end
    FROM categories_category_seq WHERE ctid = (0,3); -- second range
 range_start | range_end
- ------------+-----------
     3000334 |   4000333

注釈

最初の範囲のみが表示されるため、単一のクエリ(WHERE ctid IN ('(0,2)', '(0,3)') など)に結合することはできません。

ノードがチャンクを終了すると、新しいチャンクのコンセンサスを求め、最初に利用可能なチャンクを取得します。この例では、4000334から5000333です。これは新しい予約済みチャンクであり、古い予約済みチャンクの消費を開始します。

UUID、KSUUID、およびその他のアプローチ

PGDで使用できるグローバルシーケンスを使用せずに、他の方法でグローバルに一意のIDを生成できます。例:

  • UUIDとそのPGDバリアント、KSUUID

  • ノードごとに異なるオフセットを持つローカルシーケンス(つまり、手動)

  • 外部調整されたナチュラルキー

PGDアプリケーションは他の方法を安全に使用できません。シーケンス生成にSELECT ... FOR UPDATE 、UPDATE ... RETURNING ... などに依存するカウンターテーブルベースのアプローチは、PGDで正しく機能しません。複数のノードで同じ値が生成されます。同じ理由で、「ギャップレス」シーケンス生成の通常の戦略はPGDでは機能しません。ほとんどの場合、アプリケーションは、2フェーズコミットを使用して、外部ソースからギャップレスでなければならないシーケンスの生成を調整します。または、PGDグループ内の1つのノードでのみ生成します。

UUID

UUID キーは、代わりにシーケンスを完全に回避し、128ビットのユニバーサル一意識別子を使用します。これらはランダムまたは擬似ランダム値であり、非常に大きいため、同じ値を2回生成することはほぼ不可能です。 UUID キーを使用する場合、ノードが継続的な通信を行う必要はありません。

万が一、衝突が発生した場合、競合検出は、挿入された2つのレコードのうち新しい方を選択して保持します。競合ログが有効になっている場合、そのようなイベントが記録されます。ただし、衝突は約2 ^ 64個のキーが生成された後にのみ実際に発生する可能性があるため、発生する可能性は非常に低いです。

UUID キーの主な欠点は、スペースとネットワークの点でやや非効率的であることです。主キーとしてだけでなく、外部キーで参照される場所やネットワークで送信される場合にも、より多くのスペースを消費します。また、すべてのアプリケーションがUUID キーにうまく対応するとは限りません。

KSUUID

PGDは、 UUID データのK-Sortableバリアントを操作するための関数を提供します。 KSUUIDは、PostgreSQL標準のUUID データ型を使用して保存できる値を生成します。 KSUUID 値は、 UUID 標準に従って、タイムスタンプとランダムデータの両方を格納するという点でUUIDv1 に似ています。違いは、 KSUUID はK-Sortable、つまりタイムスタンプで弱くソートできることです。これにより、よりコンパクトなbtree インデックスが生成され、検索の効率が向上し、結果データの自然な時間ソートが可能になるため、データベースキーとしてより便利になります。 UUIDv1 とは異なり、 KSUUID の値には、それらが生成されたコンピューターのMACが含まれないため、使用してもセキュリティ上の懸念はありません。

現在、すべての場合にKSUUID v2をお勧めします。通常の比較演算子で生成された値を直接ソートできます。

PGDにはKSUUID の2つのバージョンがあります。v1とv2です。レガシーKSUUID v1は非推奨ですが、既存のインストールをサポートするために保持されます。新規インストールには使用しないでください。 v1とv2の内部コンテンツには互換性がありません。そのため、それらを操作する機能も互換性がありません。 KSUUID のv2は、 UUID バージョン番号も保存しなくなりました。

PGDリファレンスの KSUUID v2関数 および KSUUID v1関数 を参照してください。

ステップとオフセットのシーケンス

オフセットステップシーケンスでは、各ノードで通常のPostgreSQLシーケンスが使用されます。各シーケンスは同じ量でインクリメントし、異なるオフセットで始まります。たとえば、ステップ1000では、node1のシーケンスは1001、2001、3001などを生成します。 node2のシーケンスは1002、2002、3002などを生成します。このスキームは、ノードが長期間通信できない場合でもうまく機能します。ただし、デザイナーはスキーマを確立するときにノードの最大数を指定する必要があり、ノードごとの構成が必要です。間違いは、シーケンスの重複に簡単につながる可能性があります。

次のように、1つのノードで目的のシーケンスを作成することにより、PGDでこのアプローチを構成するのは比較的簡単です。

CREATE TABLE some_table (
    generated_value bigint primary key
);

CREATE SEQUENCE some_seq INCREMENT 1000 OWNED BY some_table.generated_value;

ALTER TABLE some_table ALTER COLUMN generated_value SET DEFAULT nextval(some_seq);

次に、 setval() を呼び出す各ノードで、各ノードに異なるオフセット開始値を指定します。例:

- - On node 1
SELECT setval(some_seq, 1);

- - On node 2
SELECT setval(some_seq, 2);

 -- ... etc

将来的に変更することは困難で混乱を招くため、追加する可能性のあるすべてのノード用のスペースを確保できるように、十分な大きさのINCREMENT を必ず許可してください。

bigint 値を使用する場合、10000以上のオフセットを使用しても、キーの枯渇に関する実質的な懸念はありません。何百もの機械が毎秒何百万ものインサートを処理し、枯渇に近づくチャンスを得るには、何百年もかかるでしょう。

PGDは現在、このようなステップ/オフセットシーケンスでノードごとのオフセットを構成するための自動化を提供していません。

複合キー

ステップ/オフセットシーケンスのバリアントは、 PRIMARY KEY (node_number, generated_value) で構成される複合キーを使用することです。ノード番号は通常、各ノードで異なる番号を返す関数から取得されます。 DDLレプリケーションを一時的に無効にし、定数SQLファンクションを作成することにより、このようなファンクションを作成できます。または、レプリケーションセットの一部ではない1行のテーブルを使用して、各ノードに異なる値を格納できます。

こちらもご覧ください