L7. Реплікація і перемикання
Пройти на живих вузлах те, що в модулі 12 описано словами: реплікація відстає, клієнт бачить це як «замовлення зникло», перемикання при асинхронній реплікації коштує підтверджених замовлень, а старий мастер без pg_rewind не повертається. Після роботи ви вмієте написати читання, стійке до лагу, підвищити репліку, не створивши split brain, і порахувати, що саме загублено.
Завдання
Section titled “Завдання”Результат лежить у 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.
Перед початком
Section titled “Перед початком”Прочитайте модуль 12: розділи про синхронну й асинхронну реплікацію, лаг, перемикання і pg_rewind. Середовище лабораторної має власний compose.yml, а не профіль replica кореневого файлу. Причина: затримка мережі потрібна на виході мастера (там іде WAL), тому NET_ADMIN і tc потрібні на обох вузлах, а «застосунок» працює в третьому контейнері app, щоб його запити не ділили чергу з WAL.
docker compose -f labs/l07-replication/compose.yml up -d --build --waitdocker 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.
-
Реплікація і лаг. Підключіться до мастера й до репліки (
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. -
Наївний клієнт. У
starter/лежатьplace_order.sh(записує замовлення на мастера) іmy_orders.sh(читає з репліки). Вмикайте лаг 3000 мс (два викликиclient.shрозділяє півсекунди витрат наdocker exec, тож менший лаг сховається):Terminal window CLIENT_DIR=starter ./labs/l07-replication/client.sh place_order 42CLIENT_DIR=starter ./labs/l07-replication/client.sh my_orders 42 -Замовлення, щойно створеного, у списку немає. Запустіть
CLIENT_DIR=starter ./labs/l07-replication/check.sh rywі прочитайте, скільки раундів із 10 клієнт провалив. -
Читання власних записів. Скопіюйте
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. -
Аварія. Вимкніть затримку, якщо вона лишилась, і запустіть сценарій:
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уже пройде перші дві групи й скаже, що старий мастер не повернувся. -
Старий мастер повертається. Контейнер
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. -
Здача.
./labs/l07-replication/check.shбез аргументів. Окрім зеленого результату, вам варто вміти пояснити: скільки замовлень і чому втрачено, що змінилося б із синхронною реплікацією і чого це коштувало б клієнтам (модуль 12 має виміри).
Перевірка
Section titled “Перевірка”./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 упав) чекер вважає провалом і завершується з ненульовим кодом.
Часті помилки
Section titled “Часті помилки”Перемикання без зупинки старого мастера. 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.
Далі, якщо цікаво
Section titled “Далі, якщо цікаво”Ввімкніть на мастері synchronous_standby_names = 'walreceiver' і повторіть етап 4: що в acked_orders.txt і що на репліці, скільки часу займає оформлення замовлення під лагом 50 мс? Додайте другу репліку й перевірте монотонне читання: два запити на репліки з різним лагом. Підніміть профіль logical (контейнер subscriber), опублікуйте orders з фільтром status = 'delivered' і додайте колонку лише на мастері. Перегляньте pg_replication_slots під час аварії: що тримає покинутий слот?