postgres logica replication ddl - ghdrako/doc_snipets GitHub Wiki

inicjalnie

pg_dump --schema-only

replicate_ddl_command()

SELECT pglogical.replicate_ddl_command(
  'ALTER TABLE users ADD COLUMN last_seen_at timestamptz'
);

Co się działo:

  • DDL wykonywał się lokalnie
  • pglogical wysyłał tekst polecenia
  • subscriber wykonywał ten sam DDL

Gdzie dziala:

  • dodanie kolumny
  • proste CREATE TABLE
  • proste indeksy

Znane problemy:

  • złożone ALTER-y
  • zmiany typów
  • indeksy CONCURRENTLY
  • DDL zależne od danych
  • race conditions

event trigger + tabela zmian

DDL → event trigger → ddl_queue → logical replication → apply worker → EXECUTE
CREATE TABLE ddl_queue (
  id bigserial primary key,
  ddl text,
  executed boolean default false,
  created_at timestamptz default now()
);
CREATE OR REPLACE FUNCTION capture_ddl()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
  obj record;
BEGIN
  FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands()
  LOOP
    INSERT INTO ddl_queue(ddl)
    VALUES (obj.command_tag || ' ' || obj.object_identity);
  END LOOP;
END;
$$;

CREATE EVENT TRIGGER ddl_capture
ON ddl_command_end
EXECUTE FUNCTION capture_ddl();

Na subscriberze

CREATE OR REPLACE FUNCTION apply_ddl_queue()
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN
        SELECT id, ddl
        FROM ddl_queue
        WHERE executed = false
        ORDER BY id
        FOR UPDATE
    LOOP
        BEGIN
            -- wykonanie DDL
            EXECUTE r.ddl;

            -- oznaczenie jako wykonane
            UPDATE ddl_queue
            SET executed = true,
                executed_at = now()
            WHERE id = r.id;

        EXCEPTION WHEN others THEN
            -- logowanie i retry w przyszłości
            RAISE WARNING 'Failed to execute DDL id %: %, will retry', r.id, SQLERRM;
            -- nie przerywamy pętli, zostanie powtórzone przy następnym run
        END;
    END LOOP;
END;
$$;


CREATE OR REPLACE FUNCTION ddl_queue_trigger()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    -- wywołanie worker’a dla nowego wiersza
    PERFORM apply_ddl_queue();
    RETURN NEW;
END;
$$;

CREATE TRIGGER ddl_queue_after_insert
AFTER INSERT ON ddl_queue
FOR EACH ROW
EXECUTE FUNCTION ddl_queue_trigger();

Problemy:

  • Replikacja nie jest synchroniczna
    • Logical replication ma lag.
    • Trigger odpali się dopiero gdy wiersz dotrze do subscriber.
    • Jeśli DML na tabeli pojawi się przed DDL, może dojść do błędu. Insert juz z nowa kolumna dla ktorej jeszcze altr nie dotarl
  • Race conditions
    • Trigger odpala się w kontekście insertu
    • Jeśli kilka DDL wpada w krótkim czasie:
    • worker może próbować wykonać je równolegle
    • może dojść do deadlock lub error (column exists)
  • Blokady i long-running DDL
    • ALTER TABLE na dużej tabeli może trwać długo
    • Trigger blokuje insert do ddl_queue, aż worker skończy
    • Może spowalniać replikację lub nawet powodować timeout
  • Idempotency
    • Każdy insert wywołuje worker
    • Jeśli DDL już było wykonane (np. retry), trzeba obsłużyć błąd w workerze
    • Dlatego zawsze stosuj IF NOT EXISTS / try-catch w workerze
  • Wielkość kolejki
    • Trigger odpala się przy każdym wierszu
    • Przy dużym napływie DDL może to spowodować dużo concurrency i logów
    • Często lepiej batchować w pętli w workerze niż wywoływać przy każdej insercji