Якщо ви коли-небудь сиділи на зустрічі, де хтось каже: «Нам треба поміняти базу — буде швидше», ви знаєте відчуття: тривога зростає. Більшість тверджень про «швидше» — це бенчмарки з невірною схемою, неправильним набором даних, невірною конкуренцією та неправильними режимами відмов.
PostgreSQL і Percona Server (варіант MySQL, орієнтований на продуктивність) обидва можуть бути неймовірно швидкими. Вони також обидва можуть бути болісно повільними. Різниця — не в інтуїції. Вона в формі навантаження, дисципліні експлуатації та в тому, чи випадково ви не бенчмаркуєте кеш.
Міфи про продуктивність, які не вмирають
Міф 1: «Percona Server швидший за PostgreSQL»
Percona Server може бути швидшим за PostgreSQL для класичного MySQL-подібного OLTP: багато коротких індексованих точкових запитів, передбачувані шаблони доступу й команди, які вміють правильно експлуатувати InnoDB. PostgreSQL також може бути швидшим за Percona Server для того ж навантаження, якщо ваші шаблони запитів краще лягають на планувальник Postgres або інстинкти налаштування команди більше відповідають регулюванням і засобам спостереження в Postgres.
«Швидше» тут — це твердження, що потребує одиниці виміру: p95 латентність? Пропускна здатність при фіксованій латентності? Час відновлення після відмови? Години, витрачені на гонитву за реплікаційним дрейфом? База даних, що «перемагає», залежить від того, який біль ви вимірюєте.
Міф 2: «PostgreSQL повільний через MVCC-блоат»
MVCC — це не рахунок податку, від якого ніяк не втекти. Це проєктне рішення, яке дає консистентні зчитування і конкуренцію. Податок з’являється, коли ви ігноруєте vacuum, довгі транзакції та блоат таблиць/індексів. Керуйте ним правильно — і Postgres чисто справляється з великою конкуренцією. Керуйте лінькувато — і він поступово перетворить сховище на скрапбук з мертвими кортежами.
Міф 3: «MySQL завжди швидший для читань»
Читання — це не однорідна річ. Є точкові читання, скани діапазонів, fan-out join’и, аналітичні агрегати і «читання», які насправді є очікуванням блокувань. InnoDB може летіти на точкових читаннях з гарячим буферним пулом та простою схемою. Postgres теж може летіти — особливо з хорошими індексами, ефективним кешуванням і планами запитів, що уникають неочікуваних вкладених циклів.
Міф 4: «Бенчмарки все вирішують»
Бенчмарки вирішують саме одне: чи конкретна база даних на конкретній конфігурації, з конкретним набором даних, з конкретним генератором навантаження, на конкретній машині, у конкретній казці без відмов, досягла якогось числа. Корисно. Не універсально.
Короткий жарт для освіження смаку: бенчмарки як резюме — усе виглядає чудово, поки не перевірите рекомендації під навантаженням.
Міф 5: «Зміна движка вирішує проблеми продуктивності»
Зміна движка змінює ваш вузький момент, а не відповідальність. Ви не втечете від фізики. Ви поміняєте один набір операційних підводних каменів на інший. Якщо ваша проблема — відсутні індекси, погані запити або недостатнє сховище, міграція — це просто дорогий спосіб відкласти ту саму роботу.
Цікаві факти та історичний контекст (те, що пояснює поведінку сьогодні)
- Родовід PostgreSQL бере початок від POSTGRES в UC Berkeley у 1980-х; назва «PostgreSQL» відображає раннє додавання SQL, яке закріпилося.
- InnoDB став стандартним движком для MySQL у версії 5.5; до того MyISAM був поширений і давав зовсім іншу історію блокувань і стійкості.
- Percona Server з’явився, бо багато команд хотіли сумісності з MySQL плюс інструменти інструментації й особливості продуктивності без очікування випусків upstream.
- Postgres додав паралельне виконання запитів поступово (не відразу), і це змінило розрахунки для певних звітних навантажень, які раніше стандартно віддавали в Spark.
- Історія реплікації MySQL починалася зі statement-based реплікації, що була швидшою в деяких випадках, але могла бути недетермінованою; row-based реплікація покращила коректність ціною додаткових витрат.
- WAL у Postgres (журнал попереднього запису) завжди був центральним, але функції на кшталт logical decoding спростили побудову pipeline’ів змін даних без тригерів.
- Буферний пул InnoDB — один із найважливіших важелів продуктивності MySQL/Percona; помилка в його налаштуванні може зробити швидкий сервер схожим на повільний диск.
- Vacuum у Postgres не є опційним; це частина того, як MVCC підтримує стабільність продуктивності. Autovacuum існує, бо люди ненадійні.
- Обидві екосистеми дозріли до рівня «справжніх» корпоративних систем; сьогоднішній аргумент рідко про можливості і в основному про відповідність і операції.
Що означає «швидше»: архетипи навантажень
1) Hot-key OLTP: багато точкових читань/записів на невеликому робочому наборі
Саме тут Percona Server (InnoDB) часто виглядає відмінно: гарячі сторінки сидять у буферному пулі, працюють механізми буферизації змін і адаптивна поведінка, а движок оптимізовано під стиль «багато дрібних запитів». Postgres теж може так працювати, але вам доведеться більше піклуватися про управління зʼєднаннями, вибір індексів і уникнення транзакційних шаблонів, що блокують vacuum.
Типова пастка: ви бенчмаркуєте на крихітному наборі даних, який поміщається в ОЗП, і оголошуєте перемогу. У продакшені робочий набір більший, і раптом ви вимірюєте латентність сховища і заміну буферів замість SQL.
2) Інтенсивний запис OLTP: постійні вставки/оновлення з вимогами на стійкість
Тут «швидше» обмежується обсягом журналу, поведінкою fsync і чекпоінтів. У Postgres важливі обсяг WAL і налаштування чекпоінтів, як і те, щоб autovacuum встигав. В InnoDB домінують розмір redo логу, поведінка флешу та подвійний запис (doublewrite). Обидва можуть впертися в I/O-стіни; вони просто вдаряться по різних стінах залежно від налаштувань і схеми.
3) Змішане навантаження з складними join’ами
Планувальник Postgres потужний, а його статистика та варіанти індексування (включно з expression та partial індексами) можуть дати дуже сильні плани для складних реляційних запитів. MySQL/Percona значно покращився, але є навантаження, для яких Postgres часто є «менш дивним» движком для join’ів і просунутої семантики SQL.
Точка рішення: якщо ваш застосунок орієнтується на реляційний дизайн і складні звітні запити поряд з OLTP, Postgres часто зменшує кількість випадків «ми переписали SQL, щоб задобрити оптимізатор».
4) Реплікація читань і фан-аут читань
Реплікація — це продуктивність. Якщо ви не можете масштабувати читання без того, щоб затримка реплікації не стала багом продукту, ваш «швидкий» первинний вузол не має значення. У Percona Server часто покладаються на флот реплік; у Postgres теж, але операційні шаблони відрізняються (фізична стрімінг-реплікація, логічна реплікація та розширення).
5) «Нудні» адміністративні роботи: бекапи, зміни схеми та відновлення
Швидкість — це також те, як швидко ви можете доставити зміни. Онлайн-зміни схеми, час створення індексу, vacuum та час відновлення вирішують, чи ви спите. Обидва движки можна експлуатувати безпечно; обидва можна загнати в кювет, якщо ставитися до технічного обслуговування як до опціонального.
Де движки відрізняються (в частинах, які вам болять)
Контроль конкуренції: MVCC не однаковий скрізь
Обидві системи використовують концепти MVCC, але операційні наслідки різняться. Postgres зберігає кілька версій рядків у самій таблиці, а видимість визначається ID транзакцій. Це означає, що мертві кортежі накопичуються, поки vacuum їх не звільнить. Це не помилка; це домовленість.
InnoDB також використовує MVCC з undo-логами. Старі версії живуть в undo-сегментах, а не як мертві кортежі в сторінках таблиці. Це змінює форму «блоату» і симптоми, які ви бачите: довгі транзакції можуть перешкоджати очищенню, росте довжина історії і виникає деградація продуктивності в інший спосіб.
Стійкість і патерни I/O: WAL проти redo/undo + doublewrite
Postgres записує WAL-рядки та пізніше скидає брудні сторінки; чекпоінти можуть створювати піки записів при поганому налаштуванні. InnoDB має redo-логи та data-файли, плюс механізми як doublewrite для захисту від часткових записів сторінок. Регулювальні ручки відрізняються, але фізика одна: ваш підсистема зберігання має бюджет, і база даних його витрачає.
«Податок оптимізатора»: планувальники — упереджені машини
Планувальник Postgres може робити блискучі вибори і іноді жахливі, коли статистика застаріла або розподіли дивні. Оптимізатор MySQL/Percona має свої особливі моменти, особливо навколо складних join’ів і певних патернів підзапитів. Ви не обираєте «кращий оптимізатор». Ви обираєте оптимізатор, який ви можете передбачити, інструментувати і виправляти.
З’єднання: Postgres карає спалахи з’єднань
Postgres за замовчуванням використовує модель процес-на-з’єднання. Громада короткоживих з’єднань може стати самостійною DoS. Фікс — не «зробити базу більшу». Фікс — pooling (часто PgBouncer), правельне обмеження max connections і навчання застосунку повторно використовувати з’єднання.
MySQL зазвичай справляється з багатьма з’єднаннями з іншими накладними витратами. Це не означає, що варто дозволяти клієнтам робити що завгодно. Це означає, що режим відмов може настане пізніше й матиме іншу сигнатуру.
Спостережуваність: обидва мають інструменти, але дефолти різні
Postgres з pg_stat_statements дає вам цінну видимість запитів. Percona Server відомий додатковою інструментацією і широкою екосистемою Percona toolkit. У будь-якому разі, якщо ви не збираєте правильні метрики й трасування запитів, ви в кінці кінців будете «налаштовувати» на основі забобонів.
Одна цитата, бо операційникам заслугою хоч одну хорошу фразу: Надія — не стратегія.
— Vince Lombardi
План швидкої діагностики
Ось що робити, коли хтось каже: «База даних повільна», і у вас є 15 хвилин, перш ніж канал інцидентів перетвориться на перформанс-арт.
Перше: підтвердіть симптом і одиницю
- Це латентність чи пропускна здатність? p95 латентність запитів проти загального QPS. Вони ламаються по-різному.
- Це база даних? Потоки додатка, мережа та зовнішні виклики можуть видавати себе за проблеми БД.
- Це один клас запитів? Один поганий запит може отруїти весь пул.
Друге: класифікуйте вузьке місце в одну з чотирьох корзин
- CPU-bound: високий CPU, стабільний I/O, повільні запити з важкими обчисленнями (сорти, hash join’и, обробка JSON).
- I/O-bound: висока латентність читань/записів, низький відсоток попадань у кеш, тиск чекпоінтів/флешів.
- Lock/transaction-bound: очікування, конфлікти блокувань, довгі транзакції, затримки застосування на репліках.
- Пам’ять/невідповідність робочого набору: треш кешу, заміна буферного пулу, часте використання тимчасових файлів.
Третє: оберіть найкоротший шлях до доказу
- На Postgres: почніть з
pg_stat_activity,pg_stat_statements, співвідношення попадань у кеш, статистики чекпоінтів і прогресу vacuum. - На Percona: почніть з
SHOW PROCESSLIST, метрик буферного пулу InnoDB, статистики redo/flush і зведення повільних запитів. - На хості: перевірте iostat, stalls під тиском, латентність файлової системи і чи тихо не кричить підсистема зберігання.
Ще один короткий жарт (і все): Найшвидша база даних — та, на яку ви випадково не спрямували звітну задачу.
Практичні завдання: команди, що означає вивід і які рішення приймати
Це реальні завдання, які можна виконати під час інциденту або огляду продуктивності. Кожне містить: команду, приклад виводу, інтерпретацію і наступне рішення. Команди розбиті між PostgreSQL і Percona/MySQL, плюс перевірки на рівні ОС, що вирішують, куди копати далі.
Завдання 1 (PostgreSQL): Побачити, що виконується і що чекає
cr0x@server:~$ psql -X -d appdb -c "select pid, usename, state, wait_event_type, wait_event, now()-query_start as age, left(query,80) as query from pg_stat_activity where state <> 'idle' order by age desc limit 10;"
pid | usename | state | wait_event_type | wait_event | age | query
------+--------+--------+-----------------+---------------+----------+--------------------------------------------------------------------------------
8123 | app | active | Lock | transactionid | 00:02:11 | update orders set status='paid' where id=$1
7991 | app | active | IO | DataFileRead | 00:01:05 | select * from order_items where order_id=$1
7902 | app | active | | | 00:00:40 | select count(*) from orders where created_at > now()-interval '1 day'
Що це означає: Маємо очікування блокування на transactionid (часто конфлікти на рівні рядка, що ескалюють до транзакційних очікувань) і I/O-очікування на читання файлів даних.
Рішення: Якщо домінують блокування — знайдіть блокуючий PID і виправте поведінку транзакцій. Якщо домінує I/O — перевірте відсоток попадань у кеш і латентність сховища перш ніж міняти SQL.
Завдання 2 (PostgreSQL): Визначити блокувальника
cr0x@server:~$ psql -X -d appdb -c "select blocked.pid as blocked_pid, blocker.pid as blocker_pid, blocked.query as blocked_query, blocker.query as blocker_query from pg_locks blocked_locks join pg_stat_activity blocked on blocked_locks.pid=blocked.pid join pg_locks blocker_locks on blocked_locks.locktype=blocker_locks.locktype and blocked_locks.database is not distinct from blocker_locks.database and blocked_locks.relation is not distinct from blocker_locks.relation and blocked_locks.page is not distinct from blocker_locks.page and blocked_locks.tuple is not distinct from blocker_locks.tuple and blocked_locks.virtualxid is not distinct from blocker_locks.virtualxid and blocked_locks.transactionid is not distinct from blocker_locks.transactionid and blocked_locks.classid is not distinct from blocker_locks.classid and blocked_locks.objid is not distinct from blocker_locks.objid and blocked_locks.objsubid is not distinct from blocker_locks.objsubid and blocker_locks.pid != blocked_locks.pid join pg_stat_activity blocker on blocker_locks.pid=blocker.pid where not blocked_locks.granted;"
blocked_pid | blocker_pid | blocked_query | blocker_query
------------+------------+------------------------------------------+---------------------------------------------
8123 | 7001 | update orders set status='paid' where id=$1 | begin; update orders set ...; -- long txn
Що це означає: PID 7001 утримує блокування. Зверніть увагу на шаблон begin;: довга транзакція.
Рішення: Виправте область транзакцій у застосунку. Під час інциденту: розгляньте завершення блокувальника, якщо це безпечно. У довгостроковій перспективі: скоротіть час транзакцій, уникайте сценаріїв «відкриваємо транзакцію під час думання користувача».
Завдання 3 (PostgreSQL): Топ запитів за загальним часом (вимагає pg_stat_statements)
cr0x@server:~$ psql -X -d appdb -c "select calls, round(total_exec_time::numeric,1) as total_ms, round(mean_exec_time::numeric,2) as mean_ms, rows, left(query,100) as query from pg_stat_statements order by total_exec_time desc limit 5;"
calls | total_ms | mean_ms | rows | query
-------+----------+---------+-------+-----------------------------------------------------------------------------------------------------
12000 | 980000.0 | 81.67 | 12000 | select * from order_items where order_id = $1
800 | 410000.0 | 512.50 | 800 | select count(*) from orders where created_at > now()-interval '1 day'
300 | 220000.0 | 733.33 | 9000 | select * from orders o join customers c on c.id=o.customer_id where c.email like $1
Що це означає: Один запит домінує за загальним часом через велику кількість викликів; інший повільний на виклик.
Рішення: Для запиту з великою кількістю викликів — сфокусуйтеся на індексах і кешуванні. Для повільного запиту — виконайте EXPLAIN (ANALYZE, BUFFERS) і виправте форму плану або додайте потрібний індекс.
Завдання 4 (PostgreSQL): Перевірити відсоток попадань у кеш (сигнал, не догма)
cr0x@server:~$ psql -X -d appdb -c "select datname, round(100*blks_hit/nullif(blks_hit+blks_read,0),2) as cache_hit_pct, blks_read from pg_stat_database where datname='appdb';"
datname | cache_hit_pct | blks_read
--------+---------------+-----------
appdb | 93.41 | 1829934
Що це означає: 93% може бути добре або погано залежно від навантаження. Але якщо вчора було 99% — робочий набір виріс або пам’ять зменшилась.
Рішення: Якщо відсоток попадань падає й читається більше — перевірте тиск на ОЗП, коректність shared_buffers і чи новий шаблон запитів не сканує великі таблиці.
Завдання 5 (PostgreSQL): Знайти таблиці з ознаками блоату (мертві кортежі)
cr0x@server:~$ psql -X -d appdb -c "select relname, n_live_tup, n_dead_tup, round(100*n_dead_tup/nullif(n_live_tup+n_dead_tup,0),2) as dead_pct from pg_stat_user_tables order by n_dead_tup desc limit 5;"
relname | n_live_tup | n_dead_tup | dead_pct
------------+------------+------------+---------
orders | 12500000 | 4100000 | 24.69
sessions | 800000 | 2900000 | 78.38
events | 90000000 | 1200000 | 1.32
Що це означає: sessions — фабрика блоату. Імовірно часті оновлення/видалення плюс поганий прогрес vacuum, можливо довгі транзакції.
Рішення: Виправте шаблон churn (TTL-партіціонування, менше оновлень), і налаштуйте autovacuum для цієї таблиці. Якщо вона вже величезна — плануйте контрольований VACUUM (FULL) або перезапис таблиці у вікні обслуговування.
Завдання 6 (PostgreSQL): Перевірити, чи чекпоінти вам дошкуляють
cr0x@server:~$ psql -X -d appdb -c "select checkpoints_timed, checkpoints_req, round(checkpoint_write_time/1000.0,1) as write_s, round(checkpoint_sync_time/1000.0,1) as sync_s, buffers_checkpoint from pg_stat_bgwriter;"
checkpoints_timed | checkpoints_req | write_s | sync_s | buffers_checkpoint
------------------+-----------------+---------+--------+-------------------
124 | 198 | 905.2 | 210.3 | 88412211
Що це означає: Багато запитаних чекпоінтів свідчить про тиск WAL або налаштування, що примушують часті чекпоінти. Це може створювати I/O-сплески і латентність.
Рішення: Розгляньте збільшення max_wal_size, налаштування checkpoint_completion_target і перевірку латентності записів на сховищі. Не «виправляйте» це відключенням стійкості.
Завдання 7 (Percona/MySQL): Хто виконується й хто завис
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G"
*************************** 1. row ***************************
Id: 22341
User: app
Host: 10.0.2.19:51244
db: appdb
Command: Query
Time: 132
State: Waiting for row lock
Info: update orders set status='paid' where id=?
*************************** 2. row ***************************
Id: 22110
User: app
Host: 10.0.2.20:49821
db: appdb
Command: Query
Time: 141
State: Sending data
Info: select * from order_items where order_id=?
Що це означає: Маємо очікування блокувань рядків і запит, що застряг у стані «Sending data» (часто означає, що він сканує/повертає багато рядків або чекає на I/O).
Рішення: Для очікувань рядків — ідентифікуйте блокуючу транзакцію. Для «Sending data» — перевірте план запиту й індекси і підтвердіть попадання в буферний пул і читання з диска.
Завдання 8 (Percona/MySQL): Знайти очікування InnoDB і блокувальника
cr0x@server:~$ mysql -e "SELECT r.trx_id waiting_trx_id, r.trx_mysql_thread_id waiting_thread, r.trx_query waiting_query, b.trx_id blocking_trx_id, b.trx_mysql_thread_id blocking_thread, b.trx_query blocking_query FROM information_schema.innodb_lock_waits w JOIN information_schema.innodb_trx b ON b.trx_id=w.blocking_trx_id JOIN information_schema.innodb_trx r ON r.trx_id=w.requesting_trx_id\G"
*************************** 1. row ***************************
waiting_trx_id: 9012231
waiting_thread: 22341
waiting_query: update orders set status='paid' where id=?
blocking_trx_id: 9012198
blocking_thread: 21990
blocking_query: update orders set status='processing' where id=?; -- txn open
Що це означає: Потік 21990 блокує. Імовірно довга транзакція або потік застосунку, що утримує блокування занадто довго.
Рішення: Якщо безпечно — вбийте блокувальника. У довгостроковій перспективі — скоротіть область транзакцій і переконайтеся, що індекси підтримують цільові оновлення (щоб уникати блокування зайвих рядків).
Завдання 9 (Percona/MySQL): Перевірити ефективність буферного пулу
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name | Value |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests | 9834421190 |
| Innodb_buffer_pool_reads | 8832211 |
+---------------------------------------+------------+
Що це означає: Співвідношення пристойне (читання з диска — невелика частка від запитів). Якщо Innodb_buffer_pool_reads швидко росте — ви втрачаєте кеш.
Рішення: Якщо читань з диска багато — збільшіть розмір буферного пулу (у межах ОЗП) або зменшіть робочий набір через індекси і зміни запитів.
Завдання 10 (Percona/MySQL): Шукати тиск redo логу / чекпоінтів
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
=====================================
2025-12-30 10:12:01 0x7f2c3c0a0700 INNODB MONITOR OUTPUT
=====================================
Log sequence number 146990331122
Log flushed up to 146990220981
Last checkpoint at 146988102000
0 pending log flushes, 0 pending chkp writes
Що це означає: LSN, позиції flushed і чекпоінтів показують, наскільки відстає запис/флеш. Великі розриви під навантаженням можуть означати тиск redo або обмеження I/O.
Рішення: Якщо чекпоінти відстають під піком — перегляньте розмір redo логів і налаштування флешу, і перевірте латентність записів на сховищі. Не змінюйте налаштування стійкості без погодження ризиків.
Завдання 11 (OS): Підтвердити латентність диска і чи ви I/O-bound
cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (db01) 12/30/2025 _x86_64_ (32 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
18.21 0.00 4.12 22.33 0.00 55.34
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s w_await wareq-sz aqu-sz %util
nvme0n1 220.0 18944.0 0.0 0.00 9.40 86.10 480.0 61200.0 18.70 127.50 12.30 98.00
Що це означає: Високе %iowait, велике w_await і %util близько 100% — сигнал про насичення сховища. Ваша база чекає на диск, який не тримає обіцянок.
Рішення: Не починайте з налаштування SQL. Зменшіть write amplification (індекси, churn), розподіліть I/O (окремі пристрої для WAL/redo і даних, якщо можливо) або оновіть сховище. Також перевірте «шумних сусідів» на загальному диску.
Завдання 12 (OS): Перевірити тиск пам’яті і свопінг
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 81264 14000 612000 0 0 120 980 9200 8800 18 4 56 22 0
3 1 0 64220 13890 590100 0 0 110 1100 9400 9100 17 4 54 25 0
Що це означає: Свопінгу немає (si/so = 0), але є I/O wait. Якби був свопінг — ви б побачили драматичну латентність і випадкові «все повільно» скарги.
Рішення: Якщо є свопінг — зупиніться і виправте розмір пам’яті (буферні пули, shared_buffers, work_mem, тимчасові таблиці). Свопінг для баз даних — це хобі, а не стратегія.
Завдання 13 (PostgreSQL): Підтвердити використання тимчасових файлів (сорти/hashes на диск)
cr0x@server:~$ psql -X -d appdb -c "select datname, temp_files, pg_size_pretty(temp_bytes) as temp_size from pg_stat_database where datname='appdb';"
datname | temp_files | temp_size
--------+------------+-----------
appdb | 41290 | 98 GB
Що це означає: 98 GB тимчасових даних — запити спилюють у файл. Часто причина — погані індекси, неправильні плани або недостатній work_mem для конкретного навантаження (не глобально).
Рішення: Ідентифікуйте запити, що спилюють, через EXPLAIN (ANALYZE, BUFFERS). Спочатку виправляйте запит/індекс. Якщо підвищуєте work_mem — робіть це обережно, щоб не викликати OOM при високій конкуренції.
Завдання 14 (Percona/MySQL): Перевірити, чи slow query log дає правду
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time';"
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| slow_query_log | ON |
| slow_query_log_file | /var/log/mysql/slow.log |
+---------------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| long_query_time | 0.2 |
+-----------------+-------+
Що це означає: Slow log увімкнено і поріг агресивний для фіксації p95-поведінки (0.2s тут). Якщо встановлено 10s — це фактично інструмент лише для постмортему.
Рішення: Переконайтеся, що slow logging увімкнено в продакшені з розумним порогом і ротацією. Інакше ви будете гадати, який запит вас поклав.
Завдання 15 (PostgreSQL): Швидко перевірити затримку реплікації
cr0x@server:~$ psql -X -d appdb -c "select client_addr, state, write_lag, flush_lag, replay_lag from pg_stat_replication;"
client_addr | state | write_lag | flush_lag | replay_lag
-------------+-----------+-----------+-----------+-----------
10.0.3.12 | streaming | 00:00:01 | 00:00:02 | 00:00:08
Що це означає: Replay lag — 8 секунд. Для когось це може бути нормою, для когось — катастрофою в залежності від вимог read-your-writes.
Рішення: Якщо затримка велика і росте — перевірте I/O/CPU репліки, довгі запити на репліках і обсяг WAL на первинному. Розгляньте переміщення звітності з реплік, що обслуговують продукт.
Завдання 16 (MySQL/Percona): Перевірити здоров’я реплікації і затримку
cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep 'Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Relay_Log_Space'
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 37
Relay_Log_Space: 1849423872
Що це означає: SQL-потік працює, але затримка — 37 секунд, і relay-логи великі. Репліка не встигає застосовувати зміни.
Рішення: Дослідіть обмеження ресурсів репліки, важкі читання на репліці і чи одно-потокове застосування не є вузьким місцем (і чи налаштовано паралельну реплікацію для вашого навантаження).
Три корпоративні міні-історії з полі бою продуктивності
1) Інцидент через неправильне припущення: «Бенчмарк сказав, що Percona швидший»
Компанія була у фазі росту з платіжним навантаженням: багато коротких транзакцій, але також небезпечна бічна звітність, що робила ad-hoc фільтри по кількох великих таблицях. Техлід запустив бенчмарк: прості пошуки по первинному ключу, кілька вставок і акуратний набір даних, що помістився в пам’ять. Percona Server показав кращі цифри за обраним тестом. Рішення прийняте.
Міграція пройшла. Перший тиждень — усе нормально. На третьому тижні прийшла реальність: звітні запити не відповідали бенчмарку. Це були широкі join’и з селективними фільтрами, і бізнес-користувачі постійно змінювали фільтри. Оптимізатор обирав «розумні» плани, поки не виник скос даних. Потім почалися скани, спилювання і перетворення шару реплік на обігрівач.
Сам інцидент був класичним: квартальний звіт запустився під пік. Затримка реплікації зросла. Читання продукту почали потрапляти на застарілі репліки і показувати неконсистентний стан. Підтримка назвала це «корупцією даних». Це не було корупцією — це була затримка плюс припущення. Інженери ганялися за фантомними багами годинами, поки хтось не перевірив Seconds_Behind_Master і slow query log.
Виправлення вимагало визнати неприємну правду: проблемою не був движок, а розуміння навантаження. Вони перенесли звітність в окрему систему і перепроєктували стратегію індексування. База стала «швидшою» за ніч без зміни жодного бінарного файлу — бо вони припинили просити невірну машину робити невірну роботу у невірний час.
2) Оптимізація, що повернулася бумерангом: «Ми збільшили пам’ять і прибрали fsync-очікування»
Інша організація використовувала Postgres для пайплайну інґесту подій з інтенсивним записом. Вони бачили періодичні сплески латентності. Хтось вказав на чекпоінти. І дійсно: були видимі сплески запису чекпоінтів, і p99 мав свій ритм.
Початкове налаштування було розумним: коригування темпу чекпоінтів, збільшення WAL-розміру, моніторинг I/O. Але потім «оптимізація» еволюціонувала: підняли work_mem глобально, тому що кілька запитів спилювали, і підняли ліміти з’єднань, бо команда додатка хотіла швидшої паралельності.
Через два дні база почала вбиватися OOM під навантаженням. Не тому, що work_mem — зло, а тому, що це пам’ять на сорти/хеші, помножена на одночасні сесії. Додайте занадто багато з’єднань — і отримаєте пам’яткову бомбу з повільним запаленням. Чекпоінт-сплески були видимою проблемою; налаштування пам’яті — тихим вбивцею.
Виправлення було нудним: обмежити з’єднання й використовувати пулінг, встановити work_mem консервативно і застосувати пер-ролі обмеження для аналітичних запитів, що потребують більше пам’яті. Команда навчилась, що «більше пам’яті» — не стратегія налаштування; це рахунок, який доведеться платити пізніше за час доступності.
3) Нудна, але правильна практика, що врятувала день: «Ми репетирували відновлення»
Ця історія без геройств. Тут були дорослі люди в кімнаті.
SaaS-компанія працювала на Percona Server з репліками і нічними бекапами. Вони також робили непопулярну річ: квартальні drills відновлення. Не просто «бекап успішний», а реальне відновлення в staging, валідація запитів на рівні застосунку і вимірювання часу відновлення. Рунбук містив реальні команди й оцінки.
Одного дня, баг прошивки сховища на первинному вузлі спричинив періодичні зупинки записів. База не впала; вона просто стала ненадійною і повільною. Команда вирішила переключитися. Під час фейловера виявили, що останній ланцюжок бекапів мав розрив через неправильно налаштовану політику ретеншену. Зазвичай це перетворилося б на довгу, пітливу ніч.
Натомість вони спокійно відновилися з останнього відомо-робочого бекапу, застосували binlog до безпечної точки і підняли заміну первинного вузла. Це зайняло час, але час, який вони вже виміряли. Бізнес помітив деградацію, а не катастрофу. Практика, на яку «ніколи нема часу», окупилася за один інцидент.
Поширені помилки: симптом → корінна причина → виправлення
1) Симптом: p99 сплески латентності кожні кілька хвилин (Postgres)
Корінна причина: сплески записів чекпоінтів через агресивне чекпоінтінг та тиск WAL; сховище не може поглинути сплеск.
Виправлення: Збільшити max_wal_size, налаштувати темп чекпоінтів, перевірити латентність сховища і розглянути розміщення WAL на швидшому носії.
2) Симптом: поступове падіння продуктивності протягом днів (Postgres)
Корінна причина: autovacuum не встигає; довгі транзакції перешкоджають очищенню; блоат росте.
Виправлення: Знайти довгі транзакції, виправити патерни застосунку, налаштувати autovacuum по таблицях і запланувати ремедіацію блоату (reindex, rewrite) там, де потрібно.
3) Симптом: «CPU в порядку, але запити повільні» (обидва)
Корінна причина: I/O wait і промахи кешу; набір даних виріс більше пам’яті; новий шаблон запитів робить скани.
Виправлення: Перевірити відсотки попадань у кеш/читання буферного пулу, валідувати індекси, зменшити скануючі запити і оновити сховище або пам’ять на основі виміряних промахів.
4) Симптом: раптові шторми блокувань після релізу (обидва)
Корінна причина: нова область транзакцій, відсутній індекс на предикат оновлення або патерни «select тоді update», що підвищують конфлікти.
Виправлення: Ідентифікуйте блокувальника, додайте потрібний індекс, скоротіть час утримання блокувань і перепроєктуйте гарячі рядки (шардінг лічильників, уникнення глобальних оновлень рядків).
5) Симптом: репліки відстають у піку, але відновлюються у непік (MySQL/Percona)
Корінна причина: потік застосування на репліці не встигає, важкі читання на репліці або сплески обсягу binlog через масові оновлення.
Виправлення: Оптимізувати патерни записів (уникати масових multi-row оновлень), налаштувати паралельну реплікацію відповідно до навантаження і ізолювати звітність від критичних реплік.
6) Симптом: Postgres «занадто багато з’єднань» і велика кількість контекстних переключень
Корінна причина: застосунок відкриває забагато з’єднань; накладні витрати процес-на-з’єднання домінують.
Виправлення: Додати PgBouncer, обмежити розмір пулів у застосунку, знизити max_connections і трактувати кількість з’єднань як показник пропускної спроможності.
7) Симптом: MySQL «Sending data» скрізь
Корінна причина: запити повертають надто багато рядків, відсутні індекси або сканування при прив’язці до диска. Іноді — мережевий зворотний тиск.
Виправлення: EXPLAIN запиту, додати covering індекси, впровадити пагінацію і перевірити, чи проблема в мережі, а не в читаннях з диска.
8) Симптом: продуктивність гірша після «додавання індексу» (обидва)
Корінна причина: ви підвищили write amplification; кожна вставка/оновлення тепер платить за додаткове обслуговування індексу. Або оптимізатор вибрав гірший план.
Виправлення: Додайте лише ті індекси, які ви можете собі дозволити. Перевірте плани запитів до/після. Використовуйте partial/expression індекси (Postgres) або продумані covering індекси (MySQL).
Чеклісти / покроковий план
Покроково: вибір між PostgreSQL і Percona Server за продуктивністю (без самообману)
- Опишіть своє навантаження у 10 запитах, що важливі, з указаною конкуренцією і співвідношенням читань/записів. Якщо не можете — ви не готові робити вибір.
- Визначте метрики успіху: p95/p99 для ключових запитів, пропускна здатність при фіксованій латентності, бюджет затримки реплікації, цільовий час відновлення.
- Тестуйте на даних, схожих на продакшн: реалістичний скіп. Мінімум — перевищити ОЗП, щоб виміряти поведінку сховища.
- Тестуйте режими відмов: затримка репліки під піком, поведінка фейловера і як швидко ви можете відновити.
- Спочатку інструментуйте: увімкніть
pg_stat_statementsабо slow logs MySQL і збирайте системні метрики. - Зробіть нудне налаштування: розмір пам’яті, розміри журналів/чекпоінтів, пулінг з’єднань і розумні налаштування autovacuum/flush.
- Тільки потім порівнюйте. Якщо один движок перемагає — ви знатимете чому й зможете утримувати перемогу в продакшні.
Операційний чекліст: тримати Postgres швидким
- Моніторити активність autovacuum і мертві кортежі; сповіщати про довгі транзакції, що блокують vacuum.
- Обмежити з’єднання і використовувати пулінг для пікової навантаженості.
- Слідкувати за ростом тимчасових файлів; трактувати спилювання як проблему запиту/індексу перед підвищенням пам’яті.
- Слідкувати за статистикою чекпоінтів і обсягом WAL; налаштовувати під плавні записи, а не героїчні сплески.
- Репетирувати відновлення і перевіряти бекапи реальними тестами відновлення.
Операційний чекліст: тримати Percona Server швидким
- Розмір буферного пулу InnoDB під робочий набір і перевірка через метрики читань.
- Увімкнути slow query лог з порогом, що відповідає вашим SLO.
- Моніторити очікування блокувань і довгі транзакції; налаштовувати гарячі рядки і область транзакцій.
- Стежити за затримками реплікації і ростом relay-логів; не використовувати критичні репліки для звітності.
- Переконатися, що налаштування стійкості відповідають бізнес-риску, а не фантазіям бенчмарку.
FAQ
1) Чи Percona Server — це «просто MySQL»?
Він сумісний з MySQL, але постачається з фічами продуктивності та інструментацією і має іншу операційну екосистему. Сумісність — мета; додатки — причина вибору.
2) Чи PostgreSQL завжди повільніший для OLTP?
Ні. Postgres може відмінно працювати для OLTP. Він менш толерантний до неохайного управління з’єднаннями і запущеного vacuum. Якщо експлуатувати його правильно — він дуже конкурентний.
3) Який найшвидший спосіб дізнатися, чи я I/O-bound?
Перевірте iostat -xz на латентність і використання, та корелюйте з метриками рівня БД (Postgres blks_read, читання буферного пулу InnoDB). Якщо латентність диска росте разом з латентністю запитів — відповідь є.
4) Чи варто налаштовувати Postgres, різко збільшуючи shared_buffers?
Не сліпо. shared_buffers важливий, але також важливий кеш ОС. Орієнтуйтеся на робочий набір, тренди попадань у кеш і уникнення сканів. Великі числа без вимірювань зазвичай породжують нові режими відмов.
5) Чи просто поставити величезний InnoDB buffer pool?
Розміруйте під робочий набір, залишаючи резерв для ОС, інших сервісів і піків. Надмірне збільшення може викликати свопінг або голодання файлового кешу ОС — це дорогий шлях до уповільнення.
6) Чому бенчмарки показують великі виграші, яких продакшн ніколи не бачить?
Бо ймовірно ви бенчмаркували теплі кеші, малі набори даних і ідеальну локальність. Продакшн має скіни, фон-джоби, бекапи, зміни схеми і користувачів, що роблять дивні речі під пік.
7) Що краще для складного SQL і звітності?
Часто Postgres, особливо якщо ви використовуєте просунуті індекси і реляційні можливості. Але «краще» залежить від ваших конкретних запитів і чи можете ви ізолювати звітність від OLTP.
8) Як не помилитися з вибором бази, орієнтуючись на враження?
Визначте ключові запити, тестуйте в реалістичному масштабі, під конкуренцією та в режимах відмов. Приймайте вибір на основі повторюваної поведінки, а не враження.
9) Реплікаційна затримка — це проблема продуктивності чи коректності?
І те, і інше. Затримка — це «продуктивність», коли вона ламає SLO, і «коректність», коли продукт передбачає read-your-writes. Трактуйте затримку як метрику першого класу.
Висновок: наступні кроки, що не зганьблять вас
Якщо ви хочете реальної відповіді на «PostgreSQL vs Percona Server: що швидше», припиніть ставити питання як про одне число. Питайте, що швидше для вашого навантаження, у ваших операційних обмеженнях, з вашими звичками команди і з режимами відмов, яких уникнути не вдасться.
- Проінвентаризуйте навантаження: топ-10 шаблонів запитів, обсяг записів, конкуренція і цільові латентності.
- Інструментуйте обидві сторони: Postgres з
pg_stat_statementsі views активності; Percona з slow logs і InnoDB status, плюс метрики хосту. - Запустіть реалістичний тест: набір даних більший за ОЗП, ті ж індекси, той самий мікс запитів, ті ж очікування по стійкості.
- Зробіть один швидкий drill діагностики: навмисно навантажте, знайдіть вузьке місце за планом і подивіться, у якій системі ваша команда швидше розуміє проблему.
- Оберіть движок, який ви зможете тримати швидким: не того, що виграє демо, а того, який ви зможете чисто експлуатувати о 3-й годині ночі.