sql >> データベース >  >> RDS >> PostgreSQL

PostgreSQLのカスタムトリガーベースのアップグレード

    最初のルール: トリガーベースのレプリケーションでPostgreSQLをアップグレードしない
    2番目のルール: トリガーベースのレプリケーションでPostgreSQLをアップグレードしないでください
    3番目のルール: トリガーベースのレプリケーションでPostgreSQLをアップグレードする場合は、苦しむ準備をしてください。そしてよく準備してください。

    PostgreSQLのアップグレードにpg_upgradeを使用しないという非常に深刻な理由があるはずです。

    OK、たとえば、数秒以上のダウンタイムを許容できないとしましょう。次にpglogicalを使用します。

    OK、9.3を実行しているため、pglogicalを使用できないとします。 Londisteを使用してください。

    読み取り可能なREADMEが見つかりませんか? SLONYを使用してください。

    複雑すぎる?ストリーミングレプリケーションを使用し、スレーブをプロモートしてpg_upgradeを実行します。次に、プロモートされた新しいサーバーで動作するようにアプリを切り替えます。

    あなたのアプリはいつも比較的書き込みが多いですか?考えられるすべてのソリューションを検討しましたが、カスタムトリガーベースのレプリケーションをセットアップしたいですか?そのとき注意すべきことがあります:

    • すべてのテーブルにPKが必要です。 ctidに依存しないでください(自動真空が無効になっている場合でも)
    • すべての制約結合テーブルに対してトリガーを有効にする必要があります(また、遅延FKが必要になる場合があります)
    • シーケンスには手動同期が必要です
    • 権限は複製されません(イベントトリガーも設定しない限り)
    • イベントトリガーは、新しいテーブルのサポートの自動化に役立ちますが、すでに複雑なプロセスを過度に複雑にしない方がよいでしょう。 (テーブル作成時にトリガーと外部テーブルを作成する、外部サーバーに同じテーブルを作成する、同じ変更でリモートサーバーテーブルを変更するなど)
    • ステートメントごとのトリガーの信頼性は低くなりますが、おそらく単純です
    • 既存のデータ移行プロセスを鮮明に想像する必要があります
    • トリガーベースのレプリケーションを設定して有効にする際は、テーブルへのアクセスを制限するように計画する必要があります
    • この方法を実行する前に、関係の依存関係と制約を完全に理解する必要があります。

    十分な警告?もう遊びたい?それでは、いくつかのコードから始めましょう。

    トリガーを作成する前に、モックアップデータセットを作成する必要があります。なんで?データを取得する前にトリガーを設定する方がはるかに簡単ではないでしょうか。では、データは一度に「アップグレード」クラスターに複製されますか?確かにそうでしょう。では、何をアップグレードしたいのでしょうか。新しいバージョンでデータセットを作成するだけです。したがって、より高いバージョンへのアップグレードを計画していて、テーブルを追加し、データを配置する前にレプリケーショントリガーを作成する必要がある場合は、後でレプリケートされないデータを同期する必要がなくなります。しかし、そのような新しいテーブルは、簡単な部分だと言えます。それでは、アップグレードを決定する前に、データがある場合はまずケースをモックアップしましょう。

    古いサーバーがp93(サポートされている最も古い)と呼ばれ、複製先のサーバーがp10と呼ばれると仮定します(11は今四半期に進行中ですが、まだ発生していません):

    \c PostgreSQL
    select pg_terminate_backend(pid) from pg_stat_activity where datname in ('p93','p10');
    drop database if exists p93;
    drop database if exists p10;

    ここではpsqlを使用しているため、\cメタコマンドを使用して他のデータベースに接続できます。このコードを別のクライアントで追跡する場合は、代わりに再接続する必要があります。もちろん、これを初めて実行する場合は、この手順は必要ありません。サンドボックスを数回再作成する必要があったため、ステートメントを保存しました…

    create database p93; --old db (I use 9.3 as oldest supported ATM version)
    create database p10; --new db 

    したがって、2つの新しいデータベースを作成します。次に、アップグレードするものに接続し、いくつかのファンキーデータ型を作成し、それらを使用して、後で既存と見なされるテーブルに入力します。

    \c p93
    create type myenum as enum('a', 'b');--adding some complex types
    create type mycomposit as (a int, b text); --and again...
    create table t(i serial not null primary key, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit);
    insert into t values(0, now(), '{"a":{"aa":[1,3,2]}}', 'foo', 'b', (3,'aloha'));
    insert into t (j,e) values ('{"b":null}', 'a');
    insert into t (t) select chr(g) from generate_series(100,240) g;--add some more data
    delete from t where i > 3 and i < 142; --mockup activity and mix tuples to be not sequential
    insert into t (t) select null;

    今、私たちは何を持っていますか?

      ctid   |  i  |           ts           |          j           |  t  | e |     c     
    ---------+-----+------------------------+----------------------+-----+---+-----------
     (0,1)   |   0 | 2018-07-08 08:03:00+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha)
     (0,2)   |   1 | 2018-07-08 08:03:00+03 | {"b":null}           |     | a | 
     (0,3)   |   2 | 2018-07-08 08:03:00+03 |                      | d   |   | 
     (0,4)   |   3 | 2018-07-08 08:03:00+03 |                      | e   |   | 
     (0,143) | 142 | 2018-07-08 08:03:00+03 |                      | ð   |   | 
     (0,144) | 143 | 2018-07-08 08:03:00+03 |                      |     |   | 
    (6 rows)

    OK、いくつかのデータ-なぜ私はそんなに多くを挿入してから削除したのですか?さて、私たちはしばらくの間存在していたデータセットをモックアップしようとします。少し散らばらせようとしています。もう1行(0,3)をページの最後(0,145)に移動しましょう:

    update t set j = '{}' where i =3; --(0,4)

    ここで、PostgreSQL_fdwを使用すると仮定します(ここでdblinkを使用すると、基本的に同じで、おそらく9.3の方が高速になるため、必要に応じて使用してください)。

    create extension PostgreSQL_fdw;
    create server p10 foreign data wrapper PostgreSQL_fdw options (host 'localhost', dbname 'p10'); --I know it's the same 9.3 server - change host to other version and use other cluster if you wish. It's not important for the sandbox...
    create user MAPPING FOR vao SERVER p10 options(user 'vao', password 'tsun');

    これで、pg_dump -sを使用してDDLを取得できますが、上記にあります。データを複製するには、上位バージョンのクラスターに同じテーブルを作成する必要があります。

    \c p10
    create type myenum as enum('a', 'b');--adding some complex types
    create type mycomposit as (a int, b text); --and again...
    create table t(i serial not null primary key, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit);

    ここで、9.3に戻り、データ移行に外部テーブルを使用します( f_ を使用します) ここでのテーブル名の規則、fは外部を表します):

    \c p93
    create foreign table f_t(i serial, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit) server p10 options (TABLE_name 't');

    ついに!挿入関数とトリガーを作成します。

    create or replace function tgf_i() returns trigger as $$
    begin
      execute format('insert into %I select ($1).*','f_'||TG_RELNAME) using NEW;
      return NEW;
    end;
    $$ language plpgsql;

    ここと後で、より長いコードのリンクを使用します。まず、話されたテキストが機械語に沈まないようにするためです。 2つ目は、同じ関数のいくつかのバージョンを使用して、コードがオンデマンドでどのように進化するかを反映しているためです。

    --OK - first table ready - lets try logical trigger based replication on inserts:
    insert into t (t) select 'one';
    --and now transactional:
    begin;
      insert into t (t) select 'two';
      select ctid, * from f_t;
      select ctid, * from t;
    rollback;
    select ctid, * from f_t where i > 143;
    select ctid, * from t where i > 143;

    結果:

    INSERT 0 1
    BEGIN
    INSERT 0 1
     ctid  |  i  |           ts           | j |  t  | e | c 
    -------+-----+------------------------+---+-----+---+---
     (0,1) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
     (0,2) | 145 | 2018-07-08 08:27:15+03 |   | two |   | 
    (2 rows)
    
      ctid   |  i  |           ts           |          j           |  t  | e |     c     
    ---------+-----+------------------------+----------------------+-----+---+-----------
     (0,1)   |   0 | 2018-07-08 08:27:15+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha)
     (0,2)   |   1 | 2018-07-08 08:27:15+03 | {"b":null}           |     | a | 
     (0,3)   |   2 | 2018-07-08 08:27:15+03 |                      | d   |   | 
     (0,143) | 142 | 2018-07-08 08:27:15+03 |                      | ð   |   | 
     (0,144) | 143 | 2018-07-08 08:27:15+03 |                      |     |   | 
     (0,145) |   3 | 2018-07-08 08:27:15+03 | {}                   | e   |   | 
     (0,146) | 144 | 2018-07-08 08:27:15+03 |                      | one |   | 
     (0,147) | 145 | 2018-07-08 08:27:15+03 |                      | two |   | 
    (8 rows)
    
    ROLLBACK
     ctid  |  i  |           ts           | j |  t  | e | c 
    -------+-----+------------------------+---+-----+---+---
     (0,1) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
    (1 row)
    
      ctid   |  i  |           ts           | j |  t  | e | c 
    ---------+-----+------------------------+---+-----+---+---
     (0,146) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
    (1 row)

    ここに何が見えますか?新しく挿入されたデータがデータベースp10に正常に複製されていることがわかります。したがって、トランザクションが失敗した場合はロールバックされます。ここまでは順調ですね。しかし、p93のテーブルがはるかに大きいことに気付くことができませんでした(はい、はい-そうではありません)-古いデータは複製されませんでした。どうやってそこにたどり着くのですか?とても簡単です:

    insert into … select local.* from ...outer join foreign where foreign.PK is null 

    するだろう。そして、これはここでの主な関心事ではありません-更新と削除に関する既存のデータをどのように管理するかを心配する必要があります-下位バージョンのデータベースで正常に実行されているステートメントは失敗するか、上位のゼロ行に影響を与えるだけです-既存のデータがないという理由だけで!!そして、ここでダウンタイムのフレーズの秒に来ます。 (映画の場合はもちろん、ここでフラッシュバックがありますが、残念ながら、「秒のダウンタイム」というフレーズが以前に注目されなかった場合は、上に移動してフレーズを探す必要があります...)

    すべてのステートメントトリガーを有効にするには、テーブルをフリーズし、すべてのデータをコピーしてからトリガーを有効にする必要があります。これにより、下位バージョンと上位バージョンのデータベースのテーブルが同期され、すべてのステートメントが同じになります(または、物理的であるため、非常に近くなります)。分布は異なります。ctid列の最初の例をもう一度見てください)。ただし、1つのbiiiiiigトランザクションでテーブルに対してこのような「レプリケーションをオンにする」を実行しても、数秒のダウンタイムにはなりません。潜在的に、サイトは何時間も読み取り専用になります。特に、テーブルがFKによって他の大きなテーブルと大まかに結合されている場合。

    読み取り専用は完全なダウンタイムではありません。ただし、後で、すべてのSELECTSと一部のINSERT、DELETE、UPDATEを機能させたままにします(新しいデータでは失敗し、古いデータでは失敗します)。テーブルまたはトランザクションを読み取り専用に移動するには、さまざまな方法があります。これは、PostgreSQLのアプローチ、アプリケーションレベル、またはアクセス許可に応じて一時的に取り消す場合もあります。これらのアプローチ自体が独自のブログのトピックになる可能性があるため、ここでのみ言及します。

    ともかく。トリガーに戻ります。同じアクションを実行するには、ローカルで行うのと同じようにリモートテーブルで個別の行(UPDATE、DELETE)を操作する必要があります。物理的な場所が異なるため、主キーを使用する必要があります。また、主キーは異なる列を持つ異なるテーブルに作成されるため、テーブルごとに一意の関数を作成するか、ジェネリックを作成する必要があります。 (簡単にするために)PKが1列しかない場合は、この関数が役立ちます。だからついに!ここに更新機能を持たせましょう。そして明らかにトリガー:

    create trigger tgu before update on t for each row execute procedure tgf_u();
    今日のホワイトペーパーをダウンロードするClusterControlを使用したPostgreSQLの管理と自動化PostgreSQLの導入、監視、管理、スケーリングを行うために知っておくべきことについて学ぶホワイトペーパーをダウンロードする

    そして、それが機能するかどうかを見てみましょう:

    begin;
            update t set j = '{"updated":true}' where i = 144;
            select * from t where i = 144;
            select * from f_t where i = 144;
    Rollback;

    結果:

    BEGIN
    psql:blog.sql:71: INFO:  (144,"2018-07-08 09:09:20+03","{""updated"":true}",one,,)
    UPDATE 1
      i  |           ts           |        j         |  t  | e | c 
    -----+------------------------+------------------+-----+---+---
     144 | 2018-07-08 09:09:20+03 | {"updated":true} | one |   | 
    (1 row)
    
      i  |           ts           |        j         |  t  | e | c 
    -----+------------------------+------------------+-----+---+---
     144 | 2018-07-08 09:09:20+03 | {"updated":true} | one |   | 
    (1 row)
    
    ROLLBACK

    わかった。そして、まだ暑いうちに、削除トリガー機能とレプリケーションも追加しましょう:

    create trigger tgd before delete on t for each row execute procedure tgf_d();

    そして確認してください:

    begin;
            delete from t where i = 144;
            select * from t where i = 144;
            select * from f_t where i = 144;
    Rollback;

    与える:

    DELETE 1
     i | ts | j | t | e | c 
    ---+----+---+---+---+---
    (0 rows)
    
     i | ts | j | t | e | c 
    ---+----+---+---+---+---
    (0 rows)

    私たちが覚えているように(誰がこれを忘れることができますか!)、トランザクションで「レプリケーション」サポートを有効にしません。そして、一貫性のあるデータが必要な場合はそうすべきです。上記のように、すべてのFK関連テーブルでのALLステートメントトリガーは、データを同期することによって事前に準備された1つのトランザクションで有効にする必要があります。そうしないと、次のように陥る可能性があります:

    begin;
            select * from t where i = 3;
            delete from t where i = 3;
            select * from t where i = 3;
            select * from f_t where i = 3;
    Rollback;

    与える:

    p93=# begin;
    BEGIN
    p93=#         select * from t where i = 3;
     i |           ts           | j  | t | e | c 
    ---+------------------------+----+---+---+---
     3 | 2018-07-08 09:16:27+03 | {} | e |   | 
    (1 row)
    
    p93=#         delete from t where i = 3;
    DELETE 1
    p93=#         select * from t where i = 3;
     i | ts | j | t | e | c 
    ---+----+---+---+---+---
    (0 rows)
    
    p93=#         select * from f_t where i = 3;
     i | ts | j | t | e | c 
    ---+----+---+---+---+---
    (0 rows)
    
    p93=# rollback;

    やいき!新しいバージョンではなく、古いバージョンのデータベースの行を削除しました。そこになかったからです。これは、正しい方法(begin; sync; enable trigger; end;)で実行した場合には発生しません。しかし、正しい方法では、テーブルが長い間読み取り専用になります。最もハードコアな読者は、「なぜそれならトリガーベースの複製を行うのか」とさえ言うでしょう。

    「普通の」人がするように、pg_upgradeでそれを行うことができます。また、ストリーミングレプリケーションの場合は、すべてのセットを読み取り専用にすることができます。アプリケーションがまだスレーブのROである間に、xlogの再生を一時停止してマスターをアップグレードします。

    その通り!始めませんでしたか?

    トリガーベースのレプリケーションは、非常に特別なものが必要なときにステージに登場します。たとえば、ROだけでなく、新しく作成されたデータに対してSELECTといくつかの変更を許可してみることができます。オンラインアンケートがあるとしましょう。ユーザーは登録し、回答し、ボーナスフリーポイントを獲得します。他の人は誰も必要としません。このような構造を使用すると、まだ上位バージョンにないデータの変更を禁止するだけで、新しいユーザーのデータフロー全体を許可できます。

    そのため、オンラインのATMで働く人をほとんど捨てて、アップグレードの最中であることに気付かずに、新参者を働かせることができます。ひどいように聞こえますが、私は仮説的に言いませんでしたか?私はしませんでした?ええと、私はそれを意味しました。

    実際のケースがどのようなものであっても、それをどのように実装できるかを見てみましょう。削除および更新機能が変更されます。そして、最後のシナリオを今すぐ確認しましょう:

    BEGIN
    psql:blog.sql:86: ERROR:  This data is not replicated yet, thus can't be deleted
    psql:blog.sql:87: ERROR:  current transaction is aborted, commands ignored until end of transaction block
    psql:blog.sql:88: ERROR:  current transaction is aborted, commands ignored until end of transaction block
    ROLLBACK

    上位のバージョンでは行が見つからなかったため、下位のバージョンでは行が削除されませんでした。更新でも同じことが起こります。自分で試してみてください。これで、トリガーベースのレプリケーションに含めるテーブルの多くの変更を停止することなく、データ同期を開始できます。

    いいですか?悪い?それは異なります。グローバルROシステムに比べて多くの欠陥といくつかの利点があります。私の目標は、安定したよく知られたプロセスで特定の能力を獲得するために、なぜ誰かが通常よりもこのような複雑な方法を使用したいと思うのかを示すことでした。もちろんいくらかコストがかかります…

    したがって、データの一貫性が少し安全になり、テーブルtの既存のデータがp10と同期しているときに、他のテーブルについて話すことができます。それはすべてFKでどのように機能しますか(結局、私はFKについて言及したので、私はそれをサンプルに含める必要があります)。さて、なぜ待つのですか?

    create table c (i serial, t int references t(i), x text);
    --and accordingly a foreign table - the one on newer version...
    \c p10
    create table c (i serial, t int references t(i), x text);
    \c p93
    create foreign table f_c(i serial, t int, x text) server p10 options (TABLE_name 'c');
    --let’s pretend it had some data before we decided to migrate with triggers to a higher version
    insert into c (t,x) values (1,'FK');
    --- so now we add triggers to replicate DML:
    create trigger tgi before insert on c for each row execute procedure tgf_i();
    create trigger tgu before update on c for each row execute procedure tgf_u();
    create trigger tgd before delete on c for each row execute procedure tgf_d();

    多くのテーブルを「トリガー」することを目的として、これら3つを関数にまとめることは確かに価値があります。しかし、私はしません。これ以上テーブルを追加するつもりはないので、2つの参照されるリレーショナルデータベースはすでにそのような混乱したネットです!

    --now, what would happen if we tr inserting referenced FK, that does not exist on remote db?..
    insert into c (t,x) values (2,'FK');
    /* it fails with:
    psql:blog.sql:139: ERROR:  insert or update on table "c" violates foreign key constraint "c_t_fkey"
    a new row isn't inserted neither on remote, nor local db, so we have safe data consistencyy, but inserts are blocked?..
    Yes untill data that existed untill trigerising gets to remote db - ou cant insert FK with before triggerising keys, yet - a new (both t and c tables) data will be accepted:
    */
    insert into t(i) values(4); --I use gap we got by deleting data above, so I dont need to "returning" and know the exact id -less coding in sample script
    insert into c(t) values(4);
    select * from c;
    select * from f_c;

    結果:

    psql:blog.sql:109: ERROR:  insert or update on table "c" violates foreign key constraint "c_t_fkey"
    DETAIL:  Key (t)=(2) is not present in table "t".
    CONTEXT:  Remote SQL command: INSERT INTO public.c(i, t, x) VALUES ($1, $2, $3)
    SQL statement "insert into f_c select ($1).*"
    PL/pgSQL function tgf_i() line 3 at EXECUTE statement
    INSERT 0 1
    INSERT 0 1
     i | t | x  
    ---+---+----
     1 | 1 | FK
     3 | 4 | 
    (2 rows)
    
     i | t | x 
    ---+---+---
     3 | 4 | 
    (1 row)

    また。データの一貫性が保たれているようです。新しいテーブルcのデータの同期を開始することもできます…

    疲れた?私は間違いなくそうです。

    結論

    結論として、このアプローチを検討しているときに私が犯したいくつかの間違いを強調したいと思います。 pg_attributeからすべての列を動的にリストして、updateステートメントを作成しているときに、かなりの時間を失いました。後で、UPDATE(list)=(list)構文を完全に忘れてしまったことに失望したことを想像してみてください。そして、関数ははるかに短く、より読みやすい状態になりました。

    ですから、一番の間違いは、到達可能に見えるという理由だけで、すべてを自分で構築しようとしたことです。まだですが、いつものように、誰かがすでにそれを改善した可能性があります。実際にそうかどうかを確認するためだけに2分を費やすと、後で考える時間を節約できます。

    そして第二に、私には物事がはるかに単純に見え、それらがはるかに深いことが判明し、PostgreSQLトランザクションモデルによって完全に保持されている多くのケースを複雑にしすぎました。

    そのため、サンドボックスを構築しようとした後で初めて、このアプローチの見積もりについてある程度明確に理解できました。

    したがって、計画は明らかに必要ですが、実際にできる以上の計画は立てないでください。

    経験には練習が伴います。

    私のサンドボックスは、コンピューター戦略を思い出させました。昼食後に座って考えてみてください。隣人。 2時間の栄光。」そして突然、あなたは翌朝、仕事の2時間前に、「どうやってここに着いたの?私の最後の長弓の男を救うために、なぜ私は洗っていない野蛮人とこの屈辱的な同盟に署名しなければならないのですか?

    読み:

    • https://www.PostgreSQL.org/docs/current/static/different-replication-solutions.html
    • https://stackoverflow.com/questions/15343075/update-multiple-columns-in-a-trigger-function-in-plpgsql

    1. PostgreSQLで現在の時刻を取得する方法

    2. プロシージャ内で別のPL/SQLプロシージャを呼び出す

    3. java.sql.SQLException:jdbc:mysql:// localhost:3306/dbnameに適したドライバーが見つかりません

    4. SQLでの二重コロン(::)表記