Перейти до вмісту

L7. Реплікація і перемикання

просунутийспирається на модуль 12

Пройти на живих вузлах те, що в модулі 12 описано словами: реплікація відстає, клієнт бачить це як «замовлення зникло», перемикання при асинхронній реплікації коштує підтверджених замовлень, а старий мастер без pg_rewind не повертається. Після роботи ви вмієте написати читання, стійке до лагу, підвищити репліку, не створивши split brain, і порахувати, що саме загублено.

Результат лежить у labs/l07-replication/:

  • solution/place_order.sh і solution/my_orders.sh: клієнт, який читає «Мої замовлення» з репліки й не втрачає власних записів. Контракт описано в заготовках у starter/.
  • topology.env: де після перемикання мастер, а де репліка.
  • solution/lost.txt: номери замовлень, які мастер підтвердив клієнтам, але які зникли після перемикання, по одному в рядку.
  • Стан кластера: одного мастера (колишня репліка), який приймає запис, і старого мастера, що повернувся реплікою нової timeline.

Готово, коли ./labs/l07-replication/check.sh виводить «усе гаразд». Чекер перевіряє інваріанти: клієнт під лагом ніколи не відповідає «замовлення не знайдено» для щойно створеного; у кластері рівно один вузол приймає запис; старий мастер не приймає запис і отримує зміни нового на тій самій timeline; lost.txt збігається зі справжніми втратами.

Чого робити не треба: змінювати схему, compose.yml, lib/, incident.sh і check.sh; писати клієнт мовою, відмінною від bash із psql.

Прочитайте модуль 12: розділи про синхронну й асинхронну реплікацію, лаг, перемикання і pg_rewind. Середовище лабораторної має власний compose.yml, а не профіль replica кореневого файлу. Причина: затримка мережі потрібна на виході мастера (там іде WAL), тому NET_ADMIN і tc потрібні на обох вузлах, а «застосунок» працює в третьому контейнері app, щоб його запити не ділили чергу з WAL.

Terminal window
docker compose -f labs/l07-replication/compose.yml up -d --build --wait
docker compose -f labs/l07-replication/compose.yml exec postgres psql -U shop -d shop

Перший запуск збирає образ (додає iproute2 до образу PostgreSQL) і копіює датасет small (Датасет) на мастера, а pg_basebackup знімає репліку. Якщо ви запускаєте compose під власною назвою проєкту, передайте її: COMPOSE_PROJECT_NAME=моя-назва ./labs/l07-replication/check.sh. Потрібні лише Docker і bash (Git Bash на Windows, WSL, Linux, macOS), інших інструментів на хості не треба. Перевірте docker context ls: docker compose має працювати з локальним Docker.

Службові імена: postgres — мастер, replica — репліка, app — застосунок. Клієнтські скрипти виконуються в app через ./labs/l07-replication/client.sh <скрипт> <аргументи> з адресами із topology.env. Паролі й порти навчальні, усе слухає лише 127.0.0.1.

  1. Реплікація і лаг. Підключіться до мастера й до репліки (docker compose … exec replica psql -U shop -d shop). На мастері прочитайте pg_stat_replication: яким є state, sync_state, чому flush_lsn дорівнює replay_lsn? На репліці виконайте SELECT pg_is_in_recovery() і спробуйте INSERT у orders: яка помилка? Тепер додайте затримку на шлях мастера до репліки:

    Terminal window
    ./labs/l07-replication/netem.sh set postgres replica 300

    Скрипт друкує команди tc, які виконує: prio із netem delay на одній смузі й фільтром за адресою репліки, щоб решта трафіку мастера йшла без затримки. Запишіть у мастера кілька замовлень і одразу подивіться, скільки вони йдуть до репліки. Чим write_lag відрізняється від replay_lag? Правила tc живуть у контейнері й зникнуть із ним, але вимкнути їх можна командою ./labs/l07-replication/netem.sh clear postgres.

  2. Наївний клієнт. У starter/ лежать place_order.sh (записує замовлення на мастера) і my_orders.sh (читає з репліки). Вмикайте лаг 3000 мс (два виклики client.sh розділяє півсекунди витрат на docker exec, тож менший лаг сховається):

    Terminal window
    CLIENT_DIR=starter ./labs/l07-replication/client.sh place_order 42
    CLIENT_DIR=starter ./labs/l07-replication/client.sh my_orders 42 -

    Замовлення, щойно створеного, у списку немає. Запустіть CLIENT_DIR=starter ./labs/l07-replication/check.sh ryw і прочитайте, скільки раундів із 10 клієнт провалив.

  3. Читання власних записів. Скопіюйте starter/ у solution/ і виправте обидва скрипти. place_order.sh виводить один рядок «<order_id> <токен>»; токен — це те, що знадобиться другому запиту, щоб зрозуміти, чи можна вже читати з репліки. my_orders.sh отримує токен і повертає замовлення покупця, читаючи з репліки, коли це безпечно. Без токена (-) сторінка має читатись із репліки: чекер це перевіряє, бо інакше репліка нічого не розвантажує. Підказки: після COMMIT візьміть pg_current_wal_lsn(); pg_last_wal_replay_lsn() >= '…'::pg_lsn на репліці; чекання має закінчуватись, а не висіти вічно. Інший правильний шлях: читати своє з мастера протягом кількох секунд після запису. Перевірка: ./labs/l07-replication/check.sh ryw.

  4. Аварія. Вимкніть затримку, якщо вона лишилась, і запустіть сценарій:

    Terminal window
    ./labs/l07-replication/incident.sh

    Він вмикає лаг 2 с, оформлює замовлення, підтверджує їх клієнтам і вбиває мастера (docker kill). Номери підтверджених замовлень і їхніх покупців лежать у acked_orders.txt (формат order_id|customer_id). Підвищте репліку (SELECT pg_promote()), переконайтеся, що вона приймає запис і що pg_is_in_recovery() повернув false, змініть topology.env так, щоб LEADER_HOST вказував на неї, а REPLICA_HOST на старого мастера. Запишіть у solution/lost.txt номери замовлень із acked_orders.txt, яких на новому мастері немає. Порівнюйте пару order_id і customer_id, а не самий номер. Поясніть собі, чому саме ці замовлення, а не інші. Перевірка: ./labs/l07-replication/check.sh failover уже пройде перші дві групи й скаже, що старий мастер не повернувся.

  5. Старий мастер повертається. Контейнер postgres зупинено, а його том з даними лишився. Не запускайте його командою docker compose start postgres: без змін він підніметься як мастер і буде другим. Потрібно: створити на новому мастері слот для старого; перемотати старого мастера за новим (pg_rewind); перетворити його на репліку нового; запустити. Каталог даних: /var/lib/postgresql/18/docker. Разовий контейнер із тим самим томом без запуску PostgreSQL:

    Terminal window
    docker compose -f labs/l07-replication/compose.yml run --rm --no-deps \
    --entrypoint bash postgres -c '…'

    Усередині працюйте від користувача postgres (gosu postgres …). Джерело для pg_rewind — host=replica user=postgres password=postgres dbname=postgres. Для репліки потрібні файл standby.signal і primary_conninfo (користувач replicator, хост replica) у postgresql.auto.conf. Перевірте: на старому мастері SELECT pg_is_in_recovery(), pg_stat_wal_receiver; на новому pg_stat_replication. Чому pg_rewind тут можливий без додаткових налаштувань: SHOW data_checksums, SHOW wal_log_hints.

  6. Здача. ./labs/l07-replication/check.sh без аргументів. Окрім зеленого результату, вам варто вміти пояснити: скільки замовлень і чому втрачено, що змінилося б із синхронною реплікацією і чого це коштувало б клієнтам (модуль 12 має виміри).

Terminal window
./labs/l07-replication/check.sh ryw # етап 3: клієнт під лагом
./labs/l07-replication/check.sh failover # етапи 4–5: перемикання, втрати, старий мастер
./labs/l07-replication/check.sh # усе разом
ROUNDS=20 ./labs/l07-replication/check.sh ryw

Чекер сам вмикає затримку 500 мс на шляху мастера до репліки й прибирає її наприкінці. У контейнері app він оформлює замовлення вашим place_order.sh і в ту ж мить відкриває «Мої замовлення» вашим my_orders.sh: десять раундів, для покупців 4001–4010. Кожне «не знайдено» означає провал. Перед цим він перевіряє власну пастку: щойно вставлений у мастера рядок справді ще не видно на репліці (інакше лаг не подіяв, і перевірка нічого б не довела). У базі чекер залишає додані замовлення, таблицю l07_probe і помітку в ній; решти не змінює.

Перемикання чекер перевіряє за станом вузлів: скільки з них не в режимі відновлення (має бути один), чи збігається з цим topology.env, чи timeline нового мастера вища за 1, чи старий мастер у стані streaming на тій самій timeline й отримує мітку, записану на новому, чи немає на ньому замовлень, яких не має новий. Перевірка займає кілька десятків секунд. Будь-який збій допоміжного процесу (контейнер не відповідає, psql упав) чекер вважає провалом і завершується з ненульовим кодом.

Перемикання без зупинки старого мастера. pg_promote() на живій парі дає два вузли, що приймають запис. Чекер скаже «два мастери одночасно». У реальних системах так виходить, коли оркестратор сам перезапускає контейнер.

docker compose start postgres одразу після pg_promote(). Старий мастер підніметься мастером: його каталог даних нічого не знає про перемикання.

pg_rewind скаржиться на postmaster.pid. Файл лишився від убитого процесу, а в новому контейнері його номер (PID 1) належить іншому процесу. Якщо впевнені, що PostgreSQL там не працює, видаліть файл і повторіть.

Після pg_rewind репліка дивиться сама на себе. Він переносить з нового мастера й postgresql.auto.conf, де primary_conninfo вказує на хост, звідки репліку колись знімали, тобто на старого мастера. Змініть хост і слот.

Токен з пробілом. Вивід place_order.sh читається як «<order_id> <токен>»: два слова. LSN виду 0/5A1 годиться, рядок із пробілами ні.

Порівняння лише за номерами замовлень. Новий мастер видає номери з проміжками, але на чекері порівнюється пара order_id і customer_id: так ви не сплутаєте втрачене замовлення з іншим, що має той самий номер.

Docker дивиться на віддалену машину. Якщо docker context ls показує зірочку біля SSH-адреси, docker compose працює там. docker context use desktop-linux або default.

Прибрати все, разом із томами й правилами tc: docker compose -f labs/l07-replication/compose.yml --profile "*" down -v.

Ввімкніть на мастері synchronous_standby_names = 'walreceiver' і повторіть етап 4: що в acked_orders.txt і що на репліці, скільки часу займає оформлення замовлення під лагом 50 мс? Додайте другу репліку й перевірте монотонне читання: два запити на репліки з різним лагом. Підніміть профіль logical (контейнер subscriber), опублікуйте orders з фільтром status = 'delivered' і додайте колонку лише на мастері. Перегляньте pg_replication_slots під час аварії: що тримає покинутий слот?