PostgreSQL vs Percona Server: масштабування читань — репліки, що допомагають проти тих, що шкодять

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

Репліки для читання мали бути простою кнопкою «додати вузол, направити аналітику туди, зменшити навантаження на primary і вчасно йти додому». У реальному продакшні репліки часто роблять навпаки: вони посилюють блокування, вводять застарілі читання, що ламають бізнес‑логіку, і тихо перетворюють «масштабувати читання» на «відлагоджувати затримку реплікації о 2:00 ранку».

Ця стаття для людей, які експлуатують бази даних, а не для тих, хто збирає PDF‑буклети від постачальників. Ми порівняємо PostgreSQL і Percona Server (сумісний з MySQL) під тим кутом, який справді має значення: як репліки поводяться під навантаженням, як вони виходять з ладу і які регулятори дійсно варті вашої уваги.

Робоча ментальна модель: що насправді означає «масштабування читань»

Масштабування читань за допомогою реплік — це не одна проблема. Це три:

  1. Пропускна здатність: Чи можуть репліки виконувати ваші запити на читання достатньо швидко, не виснажуючи primary або одна одну?
  2. Актуальність: Наскільки застарілими можуть бути результати, перш ніж ваш продукт стане «креативно неправильним»?
  3. Коректність при фейловері: Чи можна промоутнути репліку без втрати записів або без порушення припущень додатка?

PostgreSQL і Percona Server по‑різному підходять до цих компромісів.

Фізична реплікація PostgreSQL (стрімінг WAL) консервативна: насамперед коректність і простота, і вона готова скасовувати запити на репліках, щоб відновлення йшло вперед. Percona Server успадкував архітектуру реплікації MySQL: універсальний, добре знайомий і дуже здатний — плюс історія інцидентів «все працювало в стенді», спричинених тонкими деталями консистентності.

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

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

Цікаві факти та історичний контекст (що пояснює сучасні гострі моменти)

  • «Hot standby» у PostgreSQL з’явився в 9.0 (2010): до того фізичні репліки переважно використовувалися для фейловера, а не для обслуговування живих читань.
  • Реплікація MySQL починалася як асинхронна відправка binlog: культурною опцією стало «репліки можуть відставати», і багато шаблонів додатків мовчки на це розраховують.
  • Семісинхронна реплікація MySQL з’явилася, щоб звузити розрив: вона покращує гарантії надійності, але може обмежити пропускну здатність при високій затримці.
  • Реплікація PostgreSQL базується на WAL і орієнтована на сторінки: вона ефективна й детерміністична, але пов’язує прогрес репліки з відтворенням і взаємодією з vacuum.
  • GTID у MySQL/Percona зробив фейловер більш передбачуваним: він зменшив хаос «на якій позиції binlog ми зараз?» який колись псували вихідні дні.
  • Percona Server рано додав інструменти для операцій: функції як додаткові метрики й сумісність з performance_schema зробили його популярним у SRE‑команд, що люблять докази.
  • Логічна реплікація PostgreSQL дозріла пізніше (10+): вона відкрила селективну реплікацію і оновлення без простою, але не є панацеєю для масштабування читань.
  • MySQL давно підтримує мультиджерела й складні топології: можна робити хитрі речі, а хитрі речі породжують хитрі відмови.

Репліки PostgreSQL: чому вони зазвичай поводяться й коли кусаються

Як репліки PostgreSQL працюють на практиці

У класичній стрімінговій реплікації PostgreSQL primary генерує WAL (write‑ahead log). Репліки отримують WAL і відтворюють його. Читання на репліці виконуються над консистентним сніпшотом бази станом на відтворену позицію WAL.

Крок відтворення важливий. Якщо відтворення повільне — через IO, CPU, блокування на standby або через обробку конфліктів — відставання репліки зростає. Репліка може бути «жива» і відповідати на запити, але відповідати з минулого.

Режим відмови «репліки, що шкодять»: конфлікти відновлення

PostgreSQL має свою позицію: відновлення має просуватися. Якщо довготривалий запит на standby конфліктує з відтворенням WAL (зазвичай через vacuum на primary), PostgreSQL може скасувати запит на standby, щоб відновлення йшло далі. Ви побачите помилки на кшталт canceling statement due to conflict with recovery.

Це не PostgreSQL злостивий; це PostgreSQL чесний. Ви просите репліку бути одночасно машиною часу і дата‑складом. Виберіть одне або інженеруйте навколо цього:

  • Уважно використовуйте hot_standby_feedback, щоб зменшити скасування (але прийміть ризик bloat на primary).
  • Встановіть max_standby_streaming_delay, щоб обміняти відставання відтворення на можливість завершення запитів.
  • Маршрутизуйте довгі аналітичні запити на логічну репліку або виділену систему, а не на HA‑standby.

Контролі свіжості, що мають значення

PostgreSQL дає важелі для обмеження застарілості. Найкращий з оперативної точки зору — synchronous_commit разом із налаштуваннями синхронної реплікації (synchronous_standby_names). Це може гарантувати, що принаймні один standby підтвердить отримання (або застосування) перед поверненням COMMIT.

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

Продуктивність реплік у Postgres: не ігноруйте базові речі

Standby‑вузли потребують тієї ж уваги, що і primary: shared_buffers треба налаштувати адекватно, effective_cache_size — реалістично, і зберігання має витримувати випадкові зчитування плюс записи відтворення WAL. Standby з повільними дисками може відставати навіть якщо ледь‑ледь відповідає на запити.

Також репліки Postgres не дають «безкоштовних читань». Кожен запит потребує CPU, пам’яті та IO. Якщо ви спрямували флот дашбордів на один standby без пула з’єднань, ви можете ввічливо DDoS‑нути власну базу даних.

Жарт #1: Репліка для читання — як стажер, що веде нотатки: корисний, поки ви не попросите його передбачити майбутнє, тоді він панікує і видаляє вашу таблицю Excel.

Репліки Percona Server: швидкі, знайомі та повні пасток

Що насправді працює, коли ви запускаєте Percona Server

Percona Server сумісний із MySQL, зазвичай використовується як drop‑in заміна з додатковою інструментацією й функціями продуктивності. Для масштабування читань ключова поведінка походить від реплікації MySQL: primary пише binlog, репліки отримують і застосовують їх. Історично застосування було однопоточним; новіші версії пропонують паралельну реплікацію, але лише якщо ви її налаштували і ваша робоче навантаження цьому сприяє.

Класична проблема: відставання репліки — це дефолт

Багато налаштувань MySQL/Percona трактують репліки як «врешті‑наступне консистентне кешування». Це працює, поки хтось не спрямовує бізнес‑критичні читання на репліки без договору про свіжість. Якщо ваш додаток робить «запис, потім читання, потім списання грошей», відставання перетворюється на звернення в підтримку і злих фінансистів.

Semisync, GTID і ілюзія безпеки

Семісинхронна реплікація допомагає тим, що primary чекає на підтвердження від щонайменше однієї репліки про отримання транзакції перед commit (залежно від налаштування). Це зменшує вікно втрати при відмові primary, але не гарантує, що репліки встигли застосувати транзакції для читань. Отримання ≠ застосування.

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

Продуктивність репліки часто проблема застосування, а не запитів

Коли репліка Percona відстає, команди часто додають CPU або збільшують buffer pool в надії. Іноді це допомагає. Частіше репліка забуксувала на SQL‑потоку застосування: велика транзакція, зміна схеми, одна гаряча таблиця або недостатня паралельність.

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

Підводні міни коректності реплік: statement vs row та поведінка DDL

Сучасна краща практика — реплікація на рівні рядків (row‑based) для безпеки. Реплікація на рівні запитів (statement) може дрейфувати через недетерміновані функції або відмінності часових зон. Змішаний режим — компроміс, і компроміси — осередок інцидентів.

DDL також може блокувати застосування реплікації або створювати довгі паузи. Онлайн‑зміни схеми допомагають, але вони додають власну операційну складність і можуть повернутися бумерангом, якщо ваші інструменти не налаштовані під навантаження.

Жарт #2: Паралельна реплікація — як найняти більше вантажників: чудово, поки ви не зрозумієте, що диван пройде через двері тільки в одному положенні.

Розділення читань/записів: шаблони маршрутизації, що не підпалять вам систему

Шаблон 1: «Read your writes» з прив’язкою сесії

Якщо користувач щойно записав дані і очікує побачити їх одразу, у вас є два розумні варіанти:

  • Маршрутизувати цю сесію на primary на деякий час (sticky primary після запису), а потім дозволяти читання з реплік.
  • Використовувати межу свіжості: читати лише з реплік, що підтвердили застосування щонайменше LSN/GTID цієї операції (складніше, але точно).

Все інше — інженерія надії.

Шаблон 2: Розділяйте за навантаженням, а не за кінцевою точкою

Не направляйте «всі SELECT» на репліки. Маршрутуйте конкретні класи запитів. Аналітика, звіти й дашборди — хороші кандидати, якщо вони терплять застарілість і ви захистите репліки від неконтрольованої конкурентності.

Навпаки, «SELECT … FOR UPDATE» належить на primary. Те ж саме для читань, що мають бачити миттєво консистентний стан (перевірки запасів, idempotency‑ключі, логіка виявлення шахрайства).

Шаблон 3: Призначайте реплікам роль

У PostgreSQL HA‑hot standby не те саме, що пул реплік для BI. У Percona репліка для бекапів і ETL не те саме, що та, яку ви будете промоутити під час інциденту. Змішування ролей створює пріоритетні інверсії: ваші бекапи гальмують кандидата на фейловер, або ваші дашборди викликають відміни запитів, що збільшує bloat на primary.

Шаблон 4: Поставте регулятор перед репліками

Пули з’єднань, тайм‑аути запитів і обмеження ресурсів за ролями відпрацюють свій тариф. Репліка — це поділений ресурс. Ставтесь до неї як до такого. «Необмежені з’єднання дашбордів» — це як дізнатися, що таке виснаження дескрипторів файлів.

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

Практичні завдання: команди, виводи та рішення (робіть це в продакшні)

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

PostgreSQL: стан реплік та відставання

Завдання 1: Перевірити стан реплікації з primary

cr0x@server:~$ psql -X -c "SELECT application_name, client_addr, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
 application_name | client_addr  |   state   | sync_state | write_lag | flush_lag | replay_lag
-----------------+--------------+-----------+------------+-----------+-----------+-----------
 standby-a        | 10.0.2.11    | streaming | async      | 00:00:00  | 00:00:00  | 00:00:02
 standby-b        | 10.0.2.12    | streaming | async      | 00:00:00  | 00:00:01  | 00:00:15
(2 rows)

Значення: Відставання розбите по стадіях. Replay lag — це те, що відчувають ваші читачі. Standby‑b відстає на 15 с.

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

Завдання 2: Виміряти відставання в байтах (LSN diff)

cr0x@server:~$ psql -X -c "SELECT application_name, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS bytes_behind FROM pg_stat_replication;"
 application_name | bytes_behind
-----------------+--------------
 standby-a        |       524288
 standby-b        |     73400320
(2 rows)

Значення: Standby‑b відстає приблизно на 70 МБ. Часове відставання може виглядати малим, поки не настане спалах; байти показують накопичення.

Рішення: Якщо bytes_behind росте постійно — у вас стійка нестача потужності (мережа, диск, CPU або конфлікти застосування).

Завдання 3: На standby перевірити відставання за часовою міткою

cr0x@server:~$ psql -X -c "SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;"
 replay_delay
--------------
 00:00:14.832
(1 row)

Значення: «Наскільки минула остання застосована транзакція?» Дуже близько до відчутної користувачем застарілості.

Рішення: Якщо replay_delay перевищує прийнятний для вашого продукту поріг, припиніть використовувати цю standby для читань, що критичні по свіжості.

Завдання 4: Перевірити, чи запити на standby скасовуються

cr0x@server:~$ sudo journalctl -u postgresql -n 50 --no-pager
Dec 30 10:12:01 standby-a postgresql[1832]: ERROR:  canceling statement due to conflict with recovery
Dec 30 10:12:01 standby-a postgresql[1832]: DETAIL:  User query might have needed to see row versions that must be removed.
Dec 30 10:12:01 standby-a postgresql[1832]: STATEMENT:  SELECT ... FROM events WHERE ...

Значення: Конфлікти hot standby. Відтворення WAL видаляє версії рядків; ваш запит їх потребував.

Рішення: Перемістіть довгі запити з standby, або прийміть trade‑off hot_standby_feedback=on (з планом боротьби з bloat), або налаштуйте затримку standby.

Завдання 5: Перевірити, чи ввімкнено hot_standby_feedback

cr0x@server:~$ psql -X -c "SHOW hot_standby_feedback;"
 hot_standby_feedback
---------------------
 off
(1 row)

Значення: Standby не повідомляє primary про потребу зберігати старі версії рядків довше.

Рішення: Якщо ви скасовуєте важливі читання, розгляньте увімкнення, але тільки після перевірки bloat на primary і запасів vacuum‑а.

Завдання 6: Перевірити налаштування синхронної реплікації (контракт свіжості)

cr0x@server:~$ psql -X -c "SHOW synchronous_commit; SHOW synchronous_standby_names;"
 synchronous_commit
-------------------
 on
(1 row)

 synchronous_standby_names
--------------------------
 standby-a
(1 row)

Значення: Коміти чекають standby‑a (залежно від режиму). Це може підняти затримку записів, але звужує RPO.

Рішення: Якщо бюджет затримки записів не дозволяє цього, не прикидайтеся, що маєте синхронну надійність. Перейдіть на async і спроектуйтеся під це, або збережіть sync, але забезпечте низьку затримку і гарні ресурси для standby‑a.

Завдання 7: Знайти топ‑запити на читання на standby

cr0x@server:~$ psql -X -c "SELECT query, calls, mean_exec_time, rows FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 5;"
                     query                      | calls | mean_exec_time | rows
------------------------------------------------+-------+----------------+------
 SELECT * FROM report_rollups WHERE ...          |   120 |        842.123 |  1200
 SELECT ... JOIN ... WHERE ...                   |  5400 |        120.512 |    40
(2 rows)

Значення: Репліка повільна, бо її змушують виконувати дорогі запити багато разів.

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

Percona Server: стан репліки та відставання застосування

Завдання 8: На репліці перевірити потоки реплікації та відставання

cr0x@server:~$ mysql -e "SHOW REPLICA STATUS\G"
*************************** 1. row ***************************
             Replica_IO_Running: Yes
            Replica_SQL_Running: Yes
                   Seconds_Behind_Source: 27
                  Retrieved_Gtid_Set: 1-100-984433
                   Executed_Gtid_Set: 1-100-984120
                Replica_SQL_Running_State: Waiting for dependent transaction to commit

Значення: IO працює, SQL‑застосування відстає. GTID‑набори показують, наскільки. Стан SQL натякає на залежності/сериалізацію.

Рішення: Зосередьтесь на пропускній здатності застосування: налаштування паралельних воркерів, розмір транзакцій, гарячі рядки/таблиці та продуктивність диска для redo/undo.

Завдання 9: Перевірити конфігурацію паралельної реплікації

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'replica_parallel%'; SHOW VARIABLES LIKE 'slave_parallel%';"
+-----------------------------------+-------+
| Variable_name                     | Value |
+-----------------------------------+-------+
| replica_parallel_workers          | 8     |
| replica_parallel_type             | LOGICAL_CLOCK |
+-----------------------------------+-------+
+-----------------------------------+-------+
| Variable_name                     | Value |
+-----------------------------------+-------+
| slave_parallel_workers            | 8     |
| slave_parallel_type               | LOGICAL_CLOCK |
+-----------------------------------+-------+

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

Рішення: Якщо workers=0 — увімкніть їх обережно; якщо вже ввімкнено — дослідіть великі транзакції чи проблеми з диском, а не «додайте ще воркерів».

Завдання 10: Знайти найбільші недавні транзакції (болі при застосуванні binlog)

cr0x@server:~$ mysql -e "SELECT THREAD_ID, EVENT_NAME, TIMER_WAIT/1000000000000 AS seconds, SQL_TEXT FROM performance_schema.events_statements_history_long ORDER BY TIMER_WAIT DESC LIMIT 3;"
+-----------+--------------------------+---------+--------------------------------------+
| THREAD_ID | EVENT_NAME               | seconds | SQL_TEXT                              |
+-----------+--------------------------+---------+--------------------------------------+
|      2213 | statement/sql/insert     |  12.421 | INSERT INTO audit_log SELECT ...      |
|      1987 | statement/sql/update     |   9.812 | UPDATE orders SET status='...' WHERE...|
+-----------+--------------------------+---------+--------------------------------------+

Значення: Великі оператори можуть серіалізувати застосування і створювати стрибки відставання.

Рішення: Розбивайте монстр‑транзакції, уникайте «INSERT INTO … SELECT» у пікові години, і розгляньте батчинг, який поважає реплікацію.

Завдання 11: Перевірити навантаження InnoDB на репліці (buffer pool і IO)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+--------------------------+----------+
| Variable_name            | Value    |
+--------------------------+----------+
| Innodb_buffer_pool_reads | 18399231 |
+--------------------------+----------+
+----------------------------------+------------+
| Variable_name                    | Value      |
+----------------------------------+------------+
| Innodb_buffer_pool_read_requests | 9912837712 |
+----------------------------------+------------+

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

Рішення: Збільшіть buffer pool (якщо дозволяє пам’ять), зменшіть робочий набір, додайте індекси або перемістіть аналітику з репліки.

Завдання 12: Перевірити, чи репліка утискається fsync/IO

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

Device            r/s     w/s   rMB/s   wMB/s  await  %util
nvme0n1         120.0   410.0     8.1    52.3   18.4  98.7

Значення: Диск насичений (%util ~99%). Await високий. Застосування і читання змагаються за IO.

Рішення: Перемістіть redo/binlog на швидше сховище, зменшіть навантаження читання, налаштуйте параметри flush обережно або додайте репліки для розподілу читань.

Завдання 13: Перевірити binlog/GTID режим на source (готовність до фейловеру)

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'gtid_mode'; SHOW VARIABLES LIKE 'enforce_gtid_consistency'; SHOW MASTER STATUS\G"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode     | ON    |
+---------------+-------+
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| enforce_gtid_consistency | ON    |
+--------------------------+-------+
*************************** 1. row ***************************
             File: binlog.000812
         Position: 94822133
Executed_Gtid_Set: 1-100-984433

Значення: GTID увімкнено і послідовність забезпечена. Це базис для адекватних інструментів фейловеру.

Рішення: Якщо GTID виключено у 2025 році — заплануйте час, щоб це виправити. Можна працювати без нього, але ви обираєте ручні, похибкові операції.

Завдання 14: Виявити помилки маршрутизації читань через processlist

cr0x@server:~$ mysql -e "SHOW PROCESSLIST;"
+-----+------+-----------------+------+---------+------+------------------------+------------------------------+
| Id  | User | Host            | db   | Command | Time | State                  | Info                         |
+-----+------+-----------------+------+---------+------+------------------------+------------------------------+
| 101 | app  | 10.9.1.25:42110  | prod | Query   |   12 | Sending data           | SELECT ... FOR UPDATE ...    |
| 142 | app  | 10.9.1.25:42144  | prod | Query   |    8 | Waiting for table lock | UPDATE users SET ...         |
+-----+------+-----------------+------+---------+------+------------------------+------------------------------+

Значення: Запити на запис і блокуючі читання на репліці вказують на зламану маршрутизацію. Деякі проксі помилково класифікують запити; деякі додатки ігнорують наміри читання/запису.

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

Крос‑зрізні завдання: ОС, мережа і реальність пропускної здатності

Завдання 15: Перевірити мережні ретрансляції (якість стріму реплікації)

cr0x@server:~$ ss -ti dst :5432
ESTAB 0 0 10.0.2.11:50824 10.0.1.10:5432
	 cubic wscale:7,7 rto:214 rtt:3.1/1.2 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:248120122 bytes_retrans:81234 bytes_acked:248038888

Значення: Існують ретрансляції; якщо вони високі та ростуть, частина «відставання репліки» — це мережна проблема.

Рішення: Виправте втрату пакетів перед тим, як налаштовувати базу. Бази даних добре роблять багато речей; терапія від втрати пакетів — не одна з них.

Завдання 16: Підтвердити, що репліка дійсно тільки для читання на рівні БД

cr0x@server:~$ psql -X -c "SHOW transaction_read_only;"
 transaction_read_only
-----------------------
 off
(1 row)

Значення: Ця сесія може писати. На standby записи зазвичай провалюються, але на промотованому або неправильно налаштованому вузлі це може бути небезпечно.

Рішення: Застосовуйте ролі лише для читання і обмеження на боці проксі/додатка. Покладатися на «це standby» як на політику — неприпустимо.

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'read_only'; SHOW VARIABLES LIKE 'super_read_only';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| read_only     | ON    |
+---------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| super_read_only | ON    |
+-----------------+-------+

Значення: Репліка захищена навіть від привілейованих користувачів (з нюансами).

Рішення: Увімкніть super_read_only на репліках, які ніколи не повинні приймати записи.

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

Коли хтось кричить вам «репліки повільні» або «читання неконсистентні», часу на філософські дебати про CAP немає. Робіть так.

Перше: вирішіть, чи це питання свіжості, чи продуктивності

  • PostgreSQL: перевірте now() - pg_last_xact_replay_timestamp() на репліці і pg_stat_replication на primary.
  • Percona: перевірте Seconds_Behind_Source і стан SQL‑потоку, плюс виконані vs отримані GTID.

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

Друге: знайдіть категорію вузького місця

  1. Вузьке місце застосування/відтворення: диск насичений, SQL‑потік сериалізований, відтворення WAL застрягло, конфлікти/скасування.
  2. Вузьке місце запитів: дорогі запити, відсутні індекси, надмірна конкурентність, погані плани.
  3. Системне вузьке місце: CPU‑steal, тиск пам’яті, втрата мережі, шумний сусід.

Третє: одразу прийміть рішення про маршрутизацію

  • Якщо відставання > допустимості: маршрутуйте чутливі читання на primary або на більш здорову репліку.
  • Якщо репліка перевантажена: обмежте конкурентність (пулінг, тайм‑аути) і відштовхуйте не критичний трафік читань.
  • Якщо застосування відстає: перестаньте відправляти важку аналітику на HA‑кандидата. Дайте йому відігратись.

Четверте: виправляйте найменшим важелем, що змінює результат

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

Типові помилки

Помилка 1: «Ми можемо відправляти всі SELECT на репліки»

Симптоми: Випадкові застарілі читання, «відсутні» рядки, неконсистентний інтерфейс після оновлень.

Корінь: Відсутність договору про свіжість; асинхронна реплікація зазвичай відстає під навантаженням.

Виправлення: Реалізуйте «прочитати свої записи» (прив’язка сесії до primary або LSN/GTID‑огорожі). Маршрутуйте до реплік лише ті класи запитів, що терпіти можуть застарілість.

Помилка 2: Standby Postgres скасовує BI‑запити

Симптоми: Помилки про конфлікт із відновленням, дашборди тайм‑аутять під час періодів активного vacuum.

Корінь: Конфлікт hot standby між відтворенням WAL і довготривалими сніпшотами запитів.

Виправлення: Перемістіть BI на виділену репліку/логічну репліку; або увімкніть hot_standby_feedback з планом протидії bloat; або жорстко обмежте час виконання запитів.

Помилка 3: Репліка Percona «здоровa», бо IO‑потік працює

Симптоми: IO thread Yes, SQL thread Yes, але відставання росте; читання стають дедалі більш застарілими.

Корінь: Пропускна здатність застосування недостатня (серіалізація SQL‑потоку, великі транзакції, насичення диска).

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

Помилка 4: Репліки використовують для бекапів, звітів і фейловеру

Симптоми: Кандидат на фейловер завжди відстає; промоут = вікно втрати даних; бекапи сповільнюють кластер.

Корінь: Репліки багатоцільові, конкуренція пріоритетів і непередбачуване відставання.

Виправлення: Призначте ролі: хоча б одна «чиста» HA‑репліка, окремі репліки для читань і окремий вузол для бекапів/ETL за потреби.

Помилка 5: Перевантаження гарантій надійності в неправильному шарі

Симптоми: Записи сповільнюються після увімкнення sync/semisync; p99‑затримки стрибають; інциденти зростають.

Корінь: Затримка тепер прив’язана до RTT репліки та fsync. Репліка не була підготовлена до цієї ролі.

Виправлення: Якщо потрібна більша надійність, тримайте синхронні цілі близько і на швидкій інфраструктурі. Інакше прийміть async і інженеруйте RPO/RTO явно.

Помилка 6: Пул читань плавиться від шторму з’єднань

Симптоми: Репліки показують високі контекст‑перемикання CPU, багато ідл‑з’єднань, тиск пам’яті, раптові стрибки затримок.

Корінь: Відсутність пулінгу або лімітів конкурентності; дашборди та пакетні роботи відкривають багато з’єднань.

Виправлення: Використовуйте PgBouncer або ProxySQL (або еквівалент), задавайте розміри пулів, встановлюйте тайм‑аути та ізолюйте пакетні завдання.

Три міні‑історії з корпоративного життя (анонімізовано, болісно знайомі)

Інцидент через хибне припущення: «Читання не можуть ламати грошові потоки»

Середня підпискова платформа перемістила «більшість читань» на репліки сумісні з MySQL, щоб захистити primary. Мета була розумною: знизити CPU primary, зберегти затримку записів стабільною і масштабуватися по горизонталі. Проксi робив спліт за тим, чи починається запит з SELECT.

Через два тижні підтримка помітила патерн: запити, що мали бути миттєвими, іноді здавалися «в очікуванні» протягом кількох хвилин. Фінанси помітили гірше: іноді повторні списання грошей при повторних спробах користувачів. Ніхто не змінював логіку білінгу. Графіки бази даних виглядали нормально — поки не подивилися на відставання реплік у пікові години.

Хибне припущення було простим: «запити білінгу — це просто читання». Але потік білінгу робив запис, потім читання для перевірки стану перед викликом зовнішнього платіжного провайдера. Коли ця перевірка потрапляла на відсталу репліку, вона бачила старий стан і робила повторну спробу. Система працювала консистентно, просто некоректно.

Виправлення не було екзотичним. Додали «прив’язку до primary після запису» на рівні сесії користувача і позначили певні шляхи запитів як тільки‑для‑primary. Відставання все ще траплялося іноді, але перестало бути багом коректності. Також почали алертити на «відставання вище бізнес‑порогу», а не на «відставання вище якоїсь випадкової цифри».

Оптимізація, що вдарила по зворотньому боці: «Давайте змусимо standby не скасовувати запити»

Інша компанія запускала PostgreSQL зі standby, що обслуговував дашборди. BI‑команда скаржилась на скасовані запити під час інтенсивних періодів записів. Команда бази даних перемкнула hot_standby_feedback=on, щоб standby припинив втрачати довгі запити.

Дашборди стали стабільними. Усі святкували. Потім використання диска на primary почало повільно зростати незрозумілим чином. Autovacuum працював більше, але bloat таблиць усе одно ріс. Індексові скани сповільнилися. Primary працював важче, щоб робити ту саму роботу.

Що сталося: hot_standby_feedback заважав vacuum‑у очищати мертві тули, які standby потенциально ще може потребувати. Довгі запити на standby фактично говорили primary «зберігай старі версії», і primary виконував цю вимогу, поки не дійшло до проблем з простором і продуктивністю.

Вони відкочували зміну і розділили навантаження: HA‑standby, оптимізований для відтворення і фейловеру, і окрема репліка для звітів з контрольованими тайм‑аути запитів і графіком важких звітів. BI‑команда не була у захваті від обмежень, але primary перестав повільно задихатися.

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

Одне SaaS‑підприємство мало правило, що звучало бюрократично: одна репліка в кластері — це кандидат на промоут, і її не використовують для довільних читань, ETL, бекапів чи експериментів. Люди твердили, що це «витрачена апаратура». Насправді ні.

Під час інциденту зі сховищем на primary їм довелося фейловернути під тиском. Кандидат‑репліка був послідовно в межах маленького відставання, бо її захищали від шумних навантажень і її IO‑бюджет було зарезервовано для застосування. Промоут пройшов швидко і передбачувано.

Тим часом інша репліка для звітів відставала на хвилини через пакетну роботу з великими сканами. Якби саме її промотнули, вікно втрати записів значно б збільшилося і відновлення вимагало б бізнес‑рівневої синхронізації даних. Натомість усе пройшло як звичайний фейловер і стандартний післяінцидентний розбір.

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

Чеклісти / покроковий план: репліки, що працюють

Крок 1: Визначте контракт свіжості в бізнес‑термінах

  • Перелічіть потоки, що мають «прочитати свої записи» (платежі, автентифікація, інвентар, idempotency, дії адміністратора).
  • Визначте максимально допустиму застарілість для всього іншого (у секундах, не в відчуттях).
  • Перетворіть це на правила маршрутизації та алерти.

Крок 2: Розділіть ролі реплік

  • HA‑репліка: мінімальний трафік читань, налаштована для відтворення/застосування, моніториться на готовність до промоуту.
  • Пул реплік для читань: обслуговують додаткові читання додатків, що терплять відставання; захищені пулами і тайм‑аутами.
  • Репліка для звітності/ETL: окремий вузол, якщо навантаження важке або довге.

Крок 3: Зробіть маршрутизацію застосовною

  • Використовуйте різних користувачів/ролі для primary і реплік.
  • У Percona: увімкніть super_read_only на репліках.
  • У Postgres: використовуйте default_transaction_read_only для ролей реплік (і також контролюйте на рівні проксі/додатка).

Крок 4: Обмежте конкурентність і час виконання

  • Встановіть пулінг з’єднань; задайте жорсткі ліміти на сервіс.
  • Задайте statement timeouts для дашбордів і ad‑hoc інструментів.
  • Слідкуйте за довгими транзакціями; вони руйнують обидві системи по‑різному, але руйнують.

Крок 5: Переконайтесь, що репліки мають IO для двох робіт

  • Репліки мають застосовувати зміни і обслуговувати читання. Це запис + читання IO.
  • Вимірюйте насичення диска під час пікового трафіку, а не в полудень у вівторок.
  • Якщо сховище є вузьким місцем — оптимізація SQL лише хобі, а не вирішення.

Крок 6: Тестуйте фейловер як серйозну річ

  • Вимірюйте: час промоуту, час перепідключення клієнтів і вікно втрати даних (або підтвердження його відсутності, якщо синхронно).
  • Перевірте налаштування промотованого вузла: flags read‑only вимкнені, server_id вірний, конфіг реплікації правильний, бекапи налаштовані.
  • Практикуйте відновлення нової репліки з бекапу + доганяння. Якщо ви цього не можете зробити — у вас не система реплікації, а улюбленці.

Питання та відповіді

1) Чи репліки PostgreSQL «більш консистентні», ніж репліки Percona?

За замовчуванням обидві зазвичай асинхронні, отже можуть бути застарілими. PostgreSQL схильний більш явно показувати конфлікти (скасовуючи запити), натомість MySQL/Percona частіше мовчки відстає — але застарілість усе одно реальна.

2) Який найпростіший безпечний підхід до масштабування читань?

Маршруйте на репліки лише читання, що терпіти можуть застарілість; тримайте «прочитати свої записи» на primary через прив’язку сесії; обмежуйте конкурентність на репліках пулами.

3) Чому мій Postgres standby скасовує запити?

Конфлікти hot standby виникають, коли відтворення WAL має видалити старі версії рядків (часто через vacuum), але запит на standby ще їх потребує. Вирішуйте переміщенням довгих запитів, налаштуванням затримки standby або обережним увімкненням hot_standby_feedback.

4) Чому Seconds_Behind_Source іноді бреше?

У MySQL/Percona воно може бути NULL або вводити в оману в деяких станах, і воно базується на часі, а не на беклозі. Порівнюйте GTID‑набори і дивіться стан SQL‑потоку для яснішої картини.

5) Чи слід використовувати синхронну реплікацію, щоб зробити репліки безпечними для читань?

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

6) Чи можна запускати аналітику на HA‑репліці?

Можна, доки не настане день, коли треба фейловерити і виявиться, що ваш HA‑вузол відстає або ресурсно виснажений. Якщо вам важливий передбачуваний фейловер, тримайте одну репліку нудною і захищеною.

7) Чи логічна реплікація краща для масштабування читань?

Іноді. Логічна реплікація може зменшити деякі проблеми відтворення й дозволити реплікувати підмножини даних, але додає операційну складність і не скасовує потребу в дисципліні маршрутизації та налаштуванні продуктивності.

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

Достатньо, щоб витримати пікову конкуренцію читань із запасом, плюс щонайменше один вузол зарезервований для HA (залежно від дизайну фейловеру). Якщо одна репліка завжди завантажена — або вам бракує ресурсів, або ви неправильно маршрутизуєте навантаження.

9) Який найкращий алерт для здоров’я реплік?

Алертуйте по відставанню від бізнес‑порогу, плюс насичення диска на репліках, плюс скасування запитів (Postgres) або зависання застосування (Percona). Алерти, що не пов’язані з впливом на користувача, викликають втому від алертів.

10) Чи можуть проксі повністю автоматизувати розділення читань/записів безпечно?

Вони допомагають, але не можуть читати ваші продукт‑вимоги. Проксі може класифікувати запити; воно не може знати, які читання повинні бути свіжими. Ви мусите закодувати цю логіку в маршрутизації додатка або в політиці сесії.

Практичні наступні кроки

  • Запишіть контракт свіжості для ваших топ‑10 користувацьких потоків. Якщо ви не можете його сформулювати — ви не зможете його застосувати.
  • Визначте ролі реплік: одна чиста HA‑репліка, один або кілька пулів для читань, і (за потреби) вузол для звітності/ETL.
  • Впровадьте захисні правила маршрутизації: прив’язка до primary після запису, користувачі лише для реплік і примусове read‑only на репліках.
  • Запускайте наведені вище команди щотижня і під час піку: відставання в часі і байтах, стан застосування, насичення IO, топ‑запити.
  • Вирішіть найбільший вузький елемент спочатку: насичення диска, неконтрольована конкурентність або один патологічний запит. Не замінюйте капітал‑відповідальні дії настройками.
  • Практикуйте фейловер і вимірюйте його. Якщо фейловер — нетестована теорія, він випробує вас у невдалий момент.

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

← Попередня
Proxmox “pve-apt-hook failed”: чому оновлення блокуються й як безпечно розблокувати
Наступна →
ZFS: таймаути SAS проти SATA — чому SAS «відчувається стабільнішим» під навантаженням

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