MySQL vs PostgreSQL: репліки для читання — коли вони допомагають і коли брешуть

Було корисно?

Ви додали репліки для читання, щоб припинити виклики «CPU 95%». Графіки покращилися. Потім служба підтримки надсилає скріншоти: «Я щойно оплатив, але замовлення позначене як неоплачене.»
Репліки не просто не допомогли. Вони допомогли найгіршим можливим способом: були впевнено неправі.

Репліки для читання — легітимний інструмент масштабування. Водночас це відмінний спосіб делегувати коректність фізиці й надії. Це практичний посібник про
те, як репліки MySQL і PostgreSQL поводяться в продакшені, коли вони дають додатковий запас потужності і коли вони брешуть вашому застосунку.

Коли репліки допомагають проти коли брешуть

Репліки допомагають, коли читання — це справжні читання

Репліки для читання блискуче показують себе, коли в навантаженні багато повторюваних читань, які не потребують найсвіжішого стану:
звітність, аналітика, індексація пошуку, перегляд каталогу товарів, стрічки з прийнятною застарілістю, і довгі запити, які краще не виконувати на первинному вузлі. Ви можете відокремити ці запити і звільнити місце для записів.

Вони також корисні, коли потрібна операційна гнучкість: онлайн-зміни схеми, зменшення тиску від vacuum (Postgres),
вторинні середовища для перевірки і безпечніші вікна технічного обслуговування. На репліці можна запускати «дивні» запити без ризику зіпсувати кеш і затримки на primary.

Репліки брешуть, коли ви використовуєте їх як балансувальник навантаження для критичних операцій

Реплікація зазвичай асинхронна. Це означає, що репліка за визначенням відстає. Питання лише в тому «на скільки» і «протягом якого часу». Брехня тонка: репліка відповідає швидко і впевнено, але з історією.

Якщо ви надсилаєте трафік «прочитай мій запис» на репліку без захисту, ви створюєте видиму користувачеві неконсистентність.
Користувач робить дію; ви її приймаєте; вони читають назад; ви показуєте старий стан. Це виглядає як втрата даних, навіть якщо це «просто лаг».

Перше правило: якщо читання змінює те, що користувач зробить далі (гроші, автентифікація, інвентар, дозволи),
не дозволяйте йому приходити з асинхронної репліки, якщо у вас немає явної логіки сесійної консистентності.

Друге правило: репліки не вирішують повільні запити. Вони просто дозволяють виконувати їх більше. Якщо ваш шлях читання — безіндексний смітник, ви побудували більшу систему утилізації сміття — не чистішу кухню.

Жарт №1: Лаг реплікації — як нарада, на яку вас не запросили — ви дізнаєтесь про неї лише після того, як рішення вже прийняті.

Як «допомога» виглядає в метриках

  • CPU первинного вузла падає і залишається нижчим під час пікових читальних навантажень.
  • Покращується hit rate буфера/спільних буферів на primary (менше кочування через читальні запити).
  • p95 і p99 латентності записів покращуються через зниження тиску на замки і менше кочування кешу.
  • Лаг реплік залишається у межах очікуваного пікового бюджету (ви знаєте бюджет і його примусово дотримуєтесь).

Як «брехня» виглядає в інцидентах

  • «Я щойно змінив пароль, але все ще приймається старий.»
  • «Інвентар показує 3 одиниці, але оформлення замовлення провалюється.»
  • «Підтримка бачить це; клієнт — ні.» (підтримка звертається до primary; клієнти — до реплік)
  • Відбувається failover і ваша «репліка», що стала primary, не має записів, які ви думали, що були зафіксовані.

MySQL vs PostgreSQL replication: what’s actually different

Обидві системи можуть мати репліки для читання. Обидві можуть працювати в асинхронному режимі та у варіантах близьких до синхронних. Обидві можуть підвести, якщо ви вважаєте, що реплікація — це магічне дзеркало. Але режими відмов відрізняються через механіку.

MySQL replication: binlogs, GTIDs, and choices you can’t ignore

Класична реплікація MySQL базується на тому, що primary записує зміни у бінлог, а репліки їх завантажують і застосовують. Основні регулятори, що важливі в продакшені:

  • Row-based vs statement-based binlogging: row-based зазвичай безпечніший і більш детермінований; statement-based економніший за розміром, але може бути «креативним».
  • GTID: глобальні ідентифікатори транзакцій сильно полегшують failover та відстеження позицій.
  • Single-thread vs parallel apply: вузькі місця в SQL-потоці реплікації створюють лаг навіть коли мережа в порядку.
  • Semi-sync: зменшує ризик втрати даних під час коміту, вимагаючи, щоб щонайменше одна репліка підтвердила отримання (але не завжди застосування) до повернення успіху.

У MySQL «репліка наздогнала» часто виражається через виконаний набір GTID або через seconds behind source, але треба розуміти, що означає той «seconds»: він виводиться з часових міток подій, а не є точною монотонною мірою істини.

PostgreSQL replication: WAL, streaming, and the reality of “hot standby”

Репліки Postgres транслюють WAL (write-ahead log). Репліки можуть бути hot standby: читабельні під час застосування WAL. Це зручно в експлуатації, доки ваші читальні запити не конфліктують з vacuum і очищенням версій рядків.

  • Replication slots: захищають WAL, потрібний реплікам, але можуть заповнити диск, якщо репліка сильно відстала.
  • WAL receiver/replay: лаг може бути на етапі прийому, flush або replay — кожен вказує на різне вузьке місце.
  • Hot standby conflicts: довгі читання можуть блокувати очищення; очищення може скасовувати запити, залежно від налаштувань.
  • Synchronous replication: може примусити чекати коміту підтверджень зі стендбай — ви вибираєте, скільки мають підтвердити.

Postgres дає багаті метрики, прив’язані до етапів, що дуже корисно — нарешті можна припинити сперечатися з командою додатка про те, чи «seconds behind» — це просто відчуття чи число.

The big operational difference: what “durable” means under replication

Якщо ви підтверджуєте запис клієнту, клієнт вважає його істинним. Якщо primary падає відразу після цього, і під час failover ви піднімаєте репліку, яка ніколи не отримала цей запис, ваша система зробила випадкову подорож у часі.

MySQL semi-sync і Postgres synchronous replication обидва пом’якшують це, але мають різну семантику. MySQL semi-sync зазвичай означає «щонайменше одна репліка отримала подію». Synchronous replication у Postgres може означати «один або кілька стендбаїв підтвердили flush» (або в новіших конфігураціях — більш нюансні вибори), що ближче до «воно записано на диск ще десь».

Практична порада: якщо ви використовуєте репліки для безпеки при failover, явно визначте необхідну витривалість коміту.
Якщо ви використовуєте репліки для масштабування читань, явно визначте прийнятну застарілість. Це різні цілі.
Змішування їх без усвідомлення компромісів дає вам найгірше з обох світів.

Моделі консистентності, які ви можете реалізувати (і скільки вони коштують)

1) «Читання з реплік — за принципом найкращих зусиль» (дешево, небезпечно)

Ви розподіляєте GET-запити по репліках і приймаєте, що іноді користувачі бачитимуть старі дані. Це підходить для некритичного контенту. Це не підходить для переходів стану. Якщо ви не можете легко класифікувати читання, ви випадково застосуєте цю модель для всього.

2) Read-your-writes на рівні сесії (дорослий дефолт)

Після того як користувач щось записав, наступні читання в цій сесії повинні це бачити. Ви можете реалізувати це кількома способами:

  • Primary pinning: після запису направляйте цього користувача на primary протягом N секунд.
  • GTID/WAL position fencing: зберігайте коміт-позицію (GTID або LSN), повернену primary; читайте тільки з реплік, які її відтворили.
  • Кеш на рівні додатка з версіонуванням: зберігайте результати останніх записів у кеші та об’єднуйте з читаннями з репліки (складніше, ніж здається).

Вартість: складність маршрутизації й інструментування. Перевага: ваш продукт перестає газлайтити клієнтів.

3) Синхронна реплікація для критичних записів (дорого, інколи необхідно)

Якщо ви не можете втратити зафіксовані транзакції при відмові одного вузла, потрібно чекати підтверджень від реплік перед поверненням успіху. Це збільшує латентність коміту і зменшує пропускну здатність — особливо між зонами доступності.

Використовуйте для переказів грошей, незворотних робочих процесів і облікових даних. Не вмикайте це масово тільки тому, що хтось почув фразу «сильна консистентність» і зрадів.

4) Логічна реплікація / потоки змін для read-моделей (часто найкраща історія масштабування)

Інколи правильна відповідь: припинити змушувати primary базу обслуговувати кожну форму читання. Використовуйте реплікацію (logical decoding у Postgres; CDC на базі binlog у MySQL) для побудови оптимізованих під читання сховищ: індекси для пошуку, кеші, денормалізовані таблиці, OLAP-движки. Це — масштабування. Репліки — лише напівзаходи.

Цікаві факти та історичний контекст

  • Факт 1: Реплікація MySQL передувала тому, як InnoDB став дефолтним; ранні припущення про реплікацію формувалися простішими рушіями зберігання і менш суворими очікуваннями витривалості.
  • Факт 2: Statement-based реплікація була популярною частково тому, що зменшувала розмір бінлогу, але відкривала користувачів до недетермінованості (функції часу, випадковий порядок без ORDER BY тощо).
  • Факт 3: GTID у MySQL стали великою операційною зміною: вони перетворили «знайти правильний binlog-файл і позицію» на «перевірити ідентичність транзакції», що значно менш схильне до помилок при failover.
  • Факт 4: WAL-реплікація у PostgreSQL еволюціонувала від відправлення файлів до стрімінгу; стрімінг зробив «репліку як сервіс» реалістичною замість «завдань restore і молитви».
  • Факт 5: Hot standby у Postgres запровадив новий клас продакшн-драми: читальні запити на репліках можуть конфліктувати з vacuum, викликаючи скасування запитів або здуття таблиць.
  • Факт 6: Replication slots у Postgres одночасно є ременем безпеки і «стрілянням собі в ногу»: вони запобігають втраті WAL для відсталих реплік, але можуть швидко заповнити диск, якщо репліка зникла.
  • Факт 7: Метрики типу «Seconds behind master/source» вводили інженерів в оману десятиліттями, бо це не універсальна міра свіжості; це евристика, прив’язана до часових міток подій.
  • Факт 8: Semi-synchronous реплікація в MySQL з’явилася тому, що асинхронна реплікація плюс автоматичний failover створювали неприйнятні вікна втрати даних для багатьох бізнесів.
  • Факт 9: Синхронна реплікація у Postgres дозволяє вибирати, скільки стендбаїв має підтвердити, що дає можливість кворумної витривалості за рахунок хвостової латентності.

Швидкий план діагностики

Ви на виклику. Трафік читань піковий. Хтось каже «просто додайте ще одну репліку». Перш ніж це робити, знайдіть вузьке місце.
Цей план розрахований на отримання відповіді за < 15 хвилин, а не філософські дебати.

Перше: визначте, чи справді primary залежить від читань

  • Перевірте CPU та I/O wait на primary: якщо CPU завантажений, але I/O wait низький, можливо ви обчислювально-залежні (погані запити, відсутні індекси, невдалий план).
  • Перевірте hit rate буфера/кешу: якщо робочий набір не вміщується в пам’ять, репліки можуть просто помножити проблему I/O.
  • Перевірте топ-запитів: один запит може пожерти цілу ферму реплік на сніданок.

Друге: виміряйте лаг реплікації правильно

  • MySQL: порівнюйте набори GTID, а не відчуття. Seconds behind source — підказка, не обіцянка.
  • Postgres: розділяйте receive lag та replay lag. Мережа, диск і CPU проявляються по-різному.

Третє: перевірте обмеження, характерні для реплік

  • MySQL: SQL-потік реплікації не встигає, паралельні воркери неправильно налаштовані, довгі транзакції на primary створюють величезні події.
  • Postgres: WAL replay завис через I/O, backlog replication slot, hot standby конфлікти скасовують запити або затримують очищення.

Четверте: валідуйте маршрутизацію і поведінку read-after-write

  • Підтвердіть, який відсоток трафіку йде на репліки vs primary.
  • Підтвердіть, чи correctness-sensitive читання закріплені або fenced.
  • Підтвердіть, чи ваш балансувальник не відкриває тихо шлях до реплік під час перевантаження primary.

Практичні завдання: команди, виводи, рішення

Нижче — продакшн-завдання. Кожне містить команду, приклад виводу, що це означає і яке рішення прийняти.
Виконуйте з безпечного адміністративного хоста. Налаштуйте імена користувачів, сокети і хости під своє середовище.

Task 1 (MySQL): Check replication state and lag

cr0x@server:~$ mysql -e "SHOW REPLICA STATUS\G"
*************************** 1. row ***************************
             Replica_IO_State: Waiting for source to send event
                Source_Host: mysql-primary-1
              Source_Log_File: binlog.003921
          Read_Source_Log_Pos: 812993441
             Relay_Log_File: relay.000778
              Relay_Log_Pos: 813002118
      Replica_SQL_Running: Yes
       Replica_IO_Running: Yes
          Seconds_Behind_Source: 7
           Retrieved_Gtid_Set: 2f1c9b2a-...:1-91833122
            Executed_Gtid_Set: 2f1c9b2a-...:1-91833098

Що це означає: I/O і SQL-потоки запущені; репліка повідомляє про 7 секунд відставання, але GTID показує, що їй бракує 24 транзакцій.

Рішення: Якщо ваш SLA на свіжість < 1s (часто для стану користувача), ця репліка не може обслуговувати читання після запису. Закріплюйте на primary після записів або використайте fencing по GTID.

Task 2 (MySQL): Compare GTID execution between primary and replica

cr0x@server:~$ mysql -h mysql-primary-1 -e "SELECT @@global.gtid_executed\G"
*************************** 1. row ***************************
@@global.gtid_executed: 2f1c9b2a-...:1-91833122
cr0x@server:~$ mysql -h mysql-replica-1 -e "SELECT @@global.gtid_executed\G"
*************************** 1. row ***************************
@@global.gtid_executed: 2f1c9b2a-...:1-91833098

Що це означає: Репліка відстає на конкретний діапазон GTID; ви можете кількісно визначити свіжість без гадань.

Рішення: Використайте це в автоматизації: направляйте сесію на репліки тільки після того, як вони досягнуть цільового GTID (або направляйте на primary до того часу).

Task 3 (MySQL): Find whether replication is bottlenecked by single-threaded apply

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'replica_parallel_workers'; SHOW VARIABLES LIKE 'replica_parallel_type';"
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| replica_parallel_workers | 0     |
+--------------------------+-------+
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| replica_parallel_type  | DATABASE |
+------------------------+----------+

Що це означає: Паралельні воркери відключені. Один SQL-потік застосовує все.

Рішення: Якщо швидкість записів на primary висока і лаг зростає під навантаженням, увімкніть паралельну реплікацію (обережно), потім повторно виміряйте лаг і обмеження порядку комітів.

Task 4 (MySQL): Check for large transactions causing bursty lag

cr0x@server:~$ mysql -e "SHOW PROCESSLIST;"
+----+-----------+-----------------+------+---------+------+------------------------+------------------+
| Id | User      | Host            | db   | Command | Time | State                  | Info             |
+----+-----------+-----------------+------+---------+------+------------------------+------------------+
| 12 | repl      | 10.0.2.15:45512  | NULL | Binlog Dump GTID |  0 | Source has sent all binlog to replica | NULL |
| 33 | app       | 10.0.3.21:60433  | prod | Query   | 218  | updating               | UPDATE orders ... |
+----+-----------+-----------------+------+---------+------+------------------------+------------------+

Що це означає: Довгий UPDATE на primary може створити величезні бінлог-події; репліки застосовують їх пізніше, потім «наздоганяють» різкими стрибками.

Рішення: Розбивайте масивні оновлення на частини або виконуйте поза піком. Якщо не можна, припиніть обслуговувати читання, чутливі до консистентності, під час операції.

Task 5 (Postgres): Measure receive, flush, and replay lag

cr0x@server:~$ psql -h pg-replica-1 -d postgres -c "SELECT now() AS ts, pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn(), pg_last_xact_replay_timestamp();"
              ts              | pg_last_wal_receive_lsn | pg_last_wal_replay_lsn | pg_last_xact_replay_timestamp
-----------------------------+--------------------------+-------------------------+-------------------------------
 2025-12-29 14:03:11.120+00  | 3A/9F12A7B0              | 3A/9F0C1028             | 2025-12-29 14:03:05.004+00
(1 row)

Що це означає: Репліка приймає WAL попереду від того, що вона відтворила. Replay відстає; остання відтворена транзакція — приблизно 6 секунд тому.

Рішення: Якщо receive попереду, але replay відстає — фокусуйтеся на CPU/диску репліки та налаштуваннях replay. Якщо receive теж відстає — перевіряйте мережу або WAL sender на primary.

Task 6 (Postgres primary): Identify lagging standbys and their states

cr0x@server:~$ psql -h pg-primary-1 -d postgres -c "SELECT application_name, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
 application_name |   state   | sync_state | write_lag | flush_lag | replay_lag
------------------+-----------+------------+-----------+-----------+------------
 pg-replica-1      | streaming | async      | 00:00:01  | 00:00:01  | 00:00:06
 pg-replica-2      | streaming | async      | 00:00:00  | 00:00:00  | 00:00:01
(2 rows)

Що це означає: Replica-1 відтворює з відставанням 6s; replica-2 майже в реальному часі.

Рішення: Якщо ваш роутер може вибирати, надавайте перевагу replica-2 для читань, чутливих до свіжості. Також досліджуйте, чому replica-1 відтворює повільніше (I/O, CPU, конфлікти).

Task 7 (Postgres): Detect hot standby query cancellations

cr0x@server:~$ psql -h pg-replica-1 -d postgres -c "SELECT datname, confl_lock, confl_snapshot, confl_bufferpin, confl_deadlock FROM pg_stat_database_conflicts;"
 datname | confl_lock | confl_snapshot | confl_bufferpin | confl_deadlock
---------+------------+----------------+-----------------+----------------
 prod    |          0 |             17 |               0 |              0
(1 row)

Що це означає: Snapshot-конфлікти відбулись 17 разів — запити були скасовані, бо replay потрібно було видалити версії рядків, від яких залежав довгий запит.

Рішення: Перенесіть ці довгі запити в інше місце, налаштуйте max_standby_streaming_delay, або прийміть скасування з логікою повторної спроби. Не прикидайтеся, що це «випадково».

Task 8 (Postgres): Check replication slots for runaway WAL retention

cr0x@server:~$ psql -h pg-primary-1 -d postgres -c "SELECT slot_name, active, restart_lsn, wal_status FROM pg_replication_slots;"
    slot_name     | active | restart_lsn | wal_status
------------------+--------+-------------+------------
 replica1_slot     | f      | 39/FF0012A0 | extended
 replica2_slot     | t      | 3A/9E1000A0 | reserved
(2 rows)

Що це означає: replica1_slot неактивний; Postgres може утримувати WAL з restart_lsn далі, що потенційно заповнить диск.

Рішення: Якщо replica-1 пішла назавжди — видаліть слот. Якщо вона тимчасово відключена — бюджетуйте диск і швидко відновіть репліку. Це не «пізніше» робота.

Task 9 (System): Verify disk latency on the replica (common replay bottleneck)

cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (pg-replica-1) 	12/29/2025 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.30    0.00    3.90   24.10    0.00   59.70

Device            r/s     w/s   rkB/s   wkB/s  avgrq-sz avgqu-sz   await  svctm  %util
nvme0n1         120.0   380.0  8120.0 48000.0     279.0     9.80   21.40   1.20  60.00

Що це означає: iowait високий; середній час очікування ~21ms. Ймовірно, WAL replay обмежено I/O.

Рішення: Припиніть звинувачувати «Postgres replication». Виправте зберігання: швидший диск, менше конкуренції, відокремлений WAL, налаштуйте checkpointing або зменшіть конкуренцію за ресурси на репліці.

Task 10 (MySQL): Check if semi-sync is enabled (and what it implies)

cr0x@server:~$ mysql -h mysql-primary-1 -e "SHOW VARIABLES LIKE 'rpl_semi_sync_master_enabled'; SHOW STATUS LIKE 'Rpl_semi_sync_master_status';"
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| rpl_semi_sync_master_enabled  | ON    |
+-------------------------------+-------+
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Rpl_semi_sync_master_status| ON    |
+----------------------------+-------+

Що це означає: Semi-sync увімкнено і активне. Коміти повинні чекати, доки принаймні одна репліка не підтвердить отримання подій.

Рішення: Добре для зменшення втрати даних при failover, але це не гарантує, що репліка одразу готова для читань. Не вважайте це за «безпечно читати з реплік негайно».

Task 11 (Postgres): Confirm synchronous replication configuration

cr0x@server:~$ psql -h pg-primary-1 -d postgres -c "SHOW synchronous_commit; SHOW synchronous_standby_names;"
 synchronous_commit
-------------------
 on
(1 row)

 synchronous_standby_names
--------------------------
 FIRST 1 (pg-replica-2)
(1 row)

Що це означає: Primary чекає одного іменованого стендбаю (replica-2) для синхронних комітів.

Рішення: Якщо ви розраховуєте на це заради витривалості, моніторте здоров’я іменованого стендбая. Якщо він випаде, ваша система може застопоритися або повернутися до async залежно від налаштувань і оркестрації.

Task 12 (Postgres): Identify slow queries on a replica (because replicas can be overloaded too)

cr0x@server:~$ psql -h pg-replica-1 -d prod -c "SELECT pid, now()-query_start AS age, state, wait_event_type, wait_event, left(query,120) AS q FROM pg_stat_activity WHERE state <> 'idle' ORDER BY age DESC LIMIT 5;"
 pid  |   age    | state  | wait_event_type |  wait_event   |                          q
------+----------+--------+-----------------+--------------+----------------------------------------------------------
 8412 | 00:03:41 | active | IO              | DataFileRead | SELECT ... FROM events WHERE ... ORDER BY created_at DESC
 9120 | 00:01:12 | active | CPU             |              | SELECT ... FROM orders JOIN order_items ...
(2 rows)

Що це означає: Репліка зайнята. Один запит чекає I/O; інший — тяжкий на CPU.

Рішення: Якщо репліки перевантажені, додавання реплік може допомогти — але тільки якщо ви виправите шаблон запитів або індексацію. Інакше ви масштабуватимете біль по горизонталі.

Task 13 (MySQL): Observe read distribution (proxy/load balancer sanity)

cr0x@server:~$ mysql -h mysql-primary-1 -e "SHOW GLOBAL STATUS LIKE 'Questions';"
+---------------+-----------+
| Variable_name | Value     |
+---------------+-----------+
| Questions     | 819332199 |
+---------------+-----------+
cr0x@server:~$ mysql -h mysql-replica-1 -e "SHOW GLOBAL STATUS LIKE 'Questions';"
+---------------+-----------+
| Variable_name | Value     |
+---------------+-----------+
| Questions     | 83322188  |
+---------------+-----------+

Що це означає: Primary все ще обробляє більшість запитів. Або маршрутизація не працює, або додаток надто агресивно закріплює з’єднання на primary.

Рішення: Перед додаванням реплік виправте маршрутизацію. Якщо ви не можете довести, що трафік читань змістився, ви просто купуєте обладнання для прикраси дашбордів.

Task 14 (System): Check network retransmits (streaming replication hates packet loss)

cr0x@server:~$ ss -ti dst 10.0.1.10:5432 | head -n 20
ESTAB 0 0 10.0.2.20:48932 10.0.1.10:5432
	 cubic wscale:7,7 rto:204 rtt:2.1/0.9 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:9231123 bytes_acked:9231000 bytes_received:1840012 segs_out:6123 segs_in:5981 data_segs_out:5123 data_segs_in:4981 send 55.2Mbps lastsnd:8 lastrcv:7 lastack:7 pacing_rate 110Mbps retrans:14/231

Що це означає: Є повторні передачі. Декілька — нормально; зростаюче число разом з лагом вказує на проблеми мережі, що спричиняють receive lag.

Рішення: Якщо receive lag зростає разом з retransmits, не «налаштовуйте Postgres». Виправте мережевий шлях або перемістіть трафік реплікації на менш завантажений лінк.

Три короткі історії з корпоративного життя

Коротка історія 1: Інцидент через неправильне припущення

Середній підписний бізнес мав чисту архітектуру на папері: primary база для записів, дві репліки для читання для «масштабування» і проксі, що маршрутизував SELECT.
Фіч-тім додав потік «скасувати підписку». Він записував на primary, а потім відразу читав стан підписки, щоб вирішити, що показати в інтерфейсі далі.
При невеликих навантаженнях все здавалося добре. При пікових — це стало джерелом звернень до служби підтримки.

Неправильне припущення було просте: «репліка майже в реальному часі». Проксі не мав поняття сесійної консистентності. Він не знав, що користувач щойно щось записав.
Тож іноді після скасування запит потрапляв на репліку, яка все ще показувала «active». Інтерфейс пропонував неправильні дії. Клієнти повторювали дію. Повторні спроби створювали більше записів.
Лаг погіршувався. Далі ви здогадуєтесь.

Кумедна частина: дашборди виглядали «здоровими». CPU реплік був низьким. CPU primary — помірний. Латентність — нормальна. Єдина кричуща метрика — заяви до підтримки.
Інженери спочатку ганялися за фронтенд-кешем, потім за наративом «eventual consistency», потім додали третю репліку. Це нічого не покращило, бо проблема була не у пропускній здатності. Вона була в семантиці.

Виправлення було нудним і миттєвим: після запису в цьому робочому процесі вони фіксували читання на primary на 30 секунд для цього користувача/токена сесії.
Вони також додали метрику «свіжість реплік» до проксі, щоб уникати відсталих реплік узагалі.
Інцидент скінчився, і всі вдавали, ніби завжди знали, що це проблема маршрутизації.

Коротка історія 2: Оптимізація, яка відбилася бумерангом

Команда ентерпрайзу вирішила зменшити навантаження на primary, перемістивши «всі звітні запити» на Postgres hot standby. Розумно. Але звітні запити не були звітами;
це були довільні дашборди з багатохвилинними сканами і звичкою експортувати CSV під кінець кварталу.

На папері це було безпечно, бо стендбай не обслуговував записів. Насправді ці довгі знімки утримували очікування очищення і викликали конфлікти на стендбаї.
Ops-команда підкоригувала налаштування, щоб не скасовувати запити — бо керівники ненавидять часткові дашборди. Це зменшило скасування, але збільшило відставання WAL replay.

Потім пройшов тест failover. Промотований стендбай довше, ніж очікували, ставав консистентним, і частина часовочутливих читань обслуговувалася вузлом, що відставав на хвилини.
Нічого не «зіпсовано». Просто дані були неактуальні. Тим часом на primary зріс використання диска, бо WAL утримувався довше, ніж передбачалося.

Оптимізація, що відбилася бумерангом, була у змішуванні навантажень «звіт на людський час» з обов’язками «операційного стендбаю». Правильне рішення — розділити ролі:
окрема аналітична репліка з агресивним дозволенням скасувань і окремий стендбай, налаштований для швидкого replay і передбачуваної поведінки під час failover.

Коротка історія 3: Нудна, але правильна практика, що врятувала день

Платформа, що працювала з платежами, мала і MySQL, і Postgres у різних доменах. У них була правило, яке звучало як бюрократія:
будь-яка репліка, що використовується для читань, повинна публікувати сигнал свіжості, і роутер має відмовляти їй, якщо вона перевищує ліміт лагу.

Люди скаржилися на це на плануваннях. «Ми просто додамо більше реплік.» «Нам не потрібна ця складність.»
Відповідь SRE була послідовною: «Якщо ми не можемо виміряти свіжість, ми не можемо гарантувати коректність.»
Тож вони реалізували fencing: робочі процеси MySQL зберігали останній GTID від транзакції запису; Postgres workflows зберігали останній LSN.

Через рік контролер зберігання на одній репліці почав періодично затримувати записи. Репліка виглядала «вгору», приймала з’єднання і обслуговувала читання.
Але WAL/relay apply сповільнився і лаг дрейфував поза бюджетом.

Роутер автоматично вивів цю репліку з пулу. Клієнтів не зачепило. On-call отримав чистий алерт: «свіжість репліки порушена», а не «агресивні клієнти лаються».
Вони замінили несправне обладнання під час робочого дня, бо нудні запобіжники купують вам нудні інциденти.

Типові помилки: симптом → корінь проблеми → виправлення

1) Користувачі не бачать своїх оновлень

Симптом: Після оновлення UI показує старе значення на кілька секунд; оновлення іноді виправляє.

Корінь: Read-after-write спрямовано на асинхронні репліки; відсутня сесійна консистентність.

Виправлення: Закріплюйте сесію на primary після записів або використайте fencing по GTID/LSN. Додайте примусове дотримання бюджету лагу в роутері.

2) «Seconds behind source» низький, але дані відсутні

Симптом: MySQL показує 0–1s відставання, але щойно зафіксований рядок відсутній на репліці.

Корінь: Метрика ґрунтується на часових мітках і може вводити в оману; також SQL apply міг тимчасово зупинитися або часові мітки подій різняться.

Виправлення: Порівнюйте набори GTID; застосовуйте fencing на основі executed GTID. Розглядайте Seconds_Behind_Source як пожежну сигналізацію, а не як лінійку.

3) Лаг репліки зростає лінійно під час піку, а потім «наздоганяє» у сплесках

Симптом: Лаг поступово зростає, а потім раптово падає; читання іноді застарілі.

Корінь: Великі транзакції або одноманове застосування; репліка не може паралелізувати роботу.

Виправлення: Розбивайте записи; увімкніть/перевірте паралельний apply; уникайте гігантських multi-table оновлень у пікові вікна.

4) Postgres standby скасовує запити

Симптом: Звіти падають з помилкою «canceling statement due to conflict with recovery.»

Корінь: Конфлікти hot standby між replay (очищенням) і довгими читаннями.

Виправлення: Прискоріть запити, перенесіть звітність з стендбаю або прийміть скасування з повторними спробами. Налаштуйте пороги затримки стендбаю навмисно.

5) Primary диск заповнюється несподівано (Postgres)

Симптом: Каталог WAL швидко росте; алерти диска; реплікація інше «в порядку».

Корінь: Неактивний replication slot утримує WAL для відсутньої або заблокованої репліки.

Виправлення: Видаляйте невикористані слоти; моніторте відставання слотів; розглядайте неактивні слоти як production-ризик, а не цікавинку.

6) Додавання реплік не знижує навантаження на primary

Симптом: Ви додаєте дві репліки; CPU primary ледь змінюється.

Корінь: Маршрутизація не відправляє суттєвий трафік, або важливий трафік — write-bound, або додаток надто закріплює з’єднання на primary.

Виправлення: Виміряйте кількість запитів на вузол; перевірте правила роутера; класифікуйте читання за потребами консистентності; спочатку виправте повільні запити.

7) Репліки швидкі, але застосунок повільнішає

Симптом: Вузли БД в порядку; app p95 зростає; таймаути збільшуються.

Корінь: Шторм з’єднань, невірні розміри пулів або ретраї балансувальника, що створюють thundering herd на репліках.

Виправлення: Обмежте розміри пулів; використовуйте transaction pooling де можливо; впровадьте circuit breakers, коли репліки перевищують лаг або рівень помилок.

8) Failover призводить до «відсутніх» даних (насправді: втрачених підтверджених записів)

Симптом: Після аварії primary і промоції інша нода втрачає останні кілька секунд транзакцій.

Корінь: Асинхронна реплікація; failover піднімав репліку, яка не отримала/не застосувала ці записи.

Виправлення: Використовуйте синхронну реплікацію для таких транзакцій або явним чином прийміть RPO. Узгодьте автоматику failover з вашою моделлю витривалості.

Жарт №2: Репліка для читання — чесна база даних з поганою пам’яттю. На жаль, вашим користувачам байдуже до її біографії.

Чеклісти / покроковий план

Покроково: вирішуємо, чи допоможуть репліки вашому навантаженню

  1. Класифікуйте читання: позначте endpoints/запити як «потрібно свіжість» vs «застарілість допустима». Якщо не можете — вважайте, що потрібна свіжість.
  2. Виміряйте вузьке місце на primary: CPU, iowait, час блокувань, hit rate буфера, топ-запити.
  3. Встановіть бюджет лагу: оберіть число (наприклад, 250ms, 1s, 5s) для кожного класу читань. Запишіть його.
  4. Виберіть політику маршрутизації: закріплення на primary після записів або GTID/LSN fencing. Не вводьте репліки без цього.
  5. Інструментуйте свіжість реплік: експортуйте метрики лагу, які відображають ваш механізм fencing.
  6. Навантажувальне тестування з реплікацією: включайте реалістичні швидкості записів; вимірюйте пропускну здатність apply реплік, а не тільки пропуск запитів.
  7. Плануйте відмови реплік: що відбувається, якщо репліка повільна, впала або застаріла? Роутер має плавно деградувати на primary.

Покроково: побудова шляху читання «репліки не брешуть»

  1. Під час запису: фіксуйте GTID (MySQL) або LSN (Postgres) з контексту транзакції.
  2. Збережіть його в сесії/запиті: у cookie, токені або server-side session store.
  3. Перед читанням з репліки: оберіть репліку, яка виконала/відтворила принаймні цю позицію.
  4. Якщо жодна не підходить: читайте з primary або дочекайтеся невеликого таймауту (небезпечне; може посилити навантаження).
  5. Примусово дотримуйтеся глобального ліміту лагу: якщо репліка перевищує бюджет — автоматично виводьте її з пулу.
  6. Аудит ендпоінтів щоквартально: нові фічі люблять підсмикувати читання, чутливі до консистентності, у «масштабовані» пула.

Операційний чекліст: репліки як SRE-функція, а не слайд в архітектурі

  • Алерти лагу реплік, що пейджать у точці видимої для користувача неконсистентності, а не при «нескінченному терпінні».
  • План ємності для реплік: CPU і диск I/O розраховані для replay і запитів одночасно.
  • Runbook-и для: пересіву/пересинхронізації реплік, відновлення після backlog у слотах/WAL, виправлення ломки relay log, та контрольованого failover.
  • Chaos-тест: вбивайте репліку, вводьте 1% втрати пакетів, уповільнюйте диск — перевіряйте, що роутинг дренує і fencing працює.
  • Бекапи існують і тестуються. Реплікація — не бекап; це дублювання ваших помилок зі швидкістю мережі.

FAQ

1) Чи можу я використовувати репліки для масштабування записів?

Ні. Репліки масштабують читання (і деякі операційні завдання). Записи все ще є вузьким місцем на primary, якщо ви не шардуєте або не перекроюєте дизайн.
Якщо primary є write-bound, репліки цього не виправлять; вони можуть навіть погіршити ситуацію, додаючи наклад для реплікації.

2) Чи реплікація PostgreSQL «більш консистентна», ніж MySQL?

Обидві можуть бути налаштовані для сильних гарантій витривалості з синхронною реплікацією, і обидві часто запускаються асинхронно заради продуктивності.
Різниця не моральна; вона механічна і спостережувана. Postgres дає більш детальну видимість лагу по етапах; GTID-інструменти MySQL можуть бути відмінними, якщо їх правильно використовувати.

3) Чи означає MySQL semi-sync, що мої читання на репліках свіжі?

Ні. Semi-sync зазвичай означає, що репліка підтвердила отримання подій до повернення коміту. Це не гарантує, що репліка застосувала їх і зробила видимими для читання.

4) Яка найпростіша безпечна стратегія маршрутизації?

Закріплення на primary після записів з коротким TTL — найпростіша. Це не ідеально, але зазвичай достатньо і легко зрозуміти.
Якщо вам потрібна суворіша коректність з меншою кількістю читань на primary, перейдіть на GTID/LSN fencing.

5) Скільки реплік мені потрібно?

«Достатньо, щоб витримати пікове читальне навантаження при відсутності однієї репліки» — практичний старт. Потім додайте вимоги:
окремий стендбай для failover, окрема репліка для звітності і, можливо, ще одна для тестування/обслуговування.
Змішування всіх ролей на одній репліці — шлях до конфлікту ролей під час інцидентів.

6) Чому лаг репліки зростає під час бекапів або обслуговування?

Бекапи можуть насичувати диск і виганяти кеш. Роботи з обслуговування можуть створювати важкий I/O і навантаження CPU. Apply реплікації конкурує за ті ж ресурси.
Виправлення — ізоляція: виділені репліки для бекапів/звітності, обмеження обслуговування і моніторинг replay/SQL thread utilization.

7) Чи репліки замінюють кешування?

Не зовсім. Репліки все одно виконують запити і все ще звертаються до сховища. Кеші поглинають повторні читання дешево; репліки розподіляють виконання запитів.
Використовуйте кеші для гарячих ключів/обчислених відповідей; використовуйте репліки для зняття навантаження зі складних читань і ізоляції робочих навантажень.

8) Який найшвидший спосіб зрозуміти, додати репліку чи виправити запити?

Якщо кілька топ-запитів домінують і повільні через відсутні індекси або поганий план — спочатку виправляйте запити.
Якщо у вас багато помірно дорогих читань і primary завантажений CPU, репліки можуть допомогти.
Якщо ви I/O-bound через набор даних, що не вміщується в пам’ять, репліки часто просто помножать дискову конкуренцію.

9) Чи можна робити мульти-регіональні репліки для низької латентності?

Так, але ви купуєте латентність заради застарілості. Крос-регіональна асинхронна реплікація завжди відставатиме при значущому рівні записів.
Для читань, чутливих до консистентності, вам все одно потрібне закріплення на primary або fencing, і це, як правило, повертає трафік у регіон запису.

10) Яку цитату запам’ятати, коли хочеться «просто додати репліки»?

«Hope is not a strategy.» — парафразована ідея з культури інженерії та операцій.
Операційна версія: вимірюйте лаг, примушуйте бюджети і проектуйте під ту консистентність, яка вам справді потрібна.

Наступні кроки, які ви можете зробити цього тижня

Якщо ви хочете, щоб репліки допомагали, а не брехали, зробіть це в порядку:

  1. Запишіть ваш бюджет застарілості: для кожного endpoint або робочого потоку. Якщо не можете — вважайте 0 для всього, що стосується грошей, автентифікації або дозволів.
  2. Виміряйте лаг правильним інструментом: GTID-набори в MySQL; receive/replay lag і LSN в Postgres.
  3. Додайте правило маршрутизації: закріплення на primary після записів або реалізуйте GTID/LSN fencing для сесій.
  4. Автоматично виводьте відсталих реплік: репліка поза бюджетом повинна розглядатися як часткова відмова, а не невелика незручність.
  5. Розділяйте ролі: не запускайте квартальні звіти на тому ж стендбаї, який ви плануєте піднімати під час відмови primary.
  6. Виправте найгірші запити: репліки не є приводом залишати 90-секундний запит у продакшені. Зробіть його швидким або ізолюйте.

Репліки — це інструмент. Хороші інструменти все ще можуть поранити, якщо їх використовувати необережно. Якщо ви проектуєте шлях читання з явними правилами свіжості і примусовим їх дотриманням,
репліки стають нудними — а нудне те, що ви хочете о 3-й ранку.

← Попередня
ZFS-знімки: суперсила, яка може заповнити ваш пул
Наступна →
ZFS Docker: Розмітка датасетів, яка зупиняє експлозію шарів

Залишити коментар