MariaDB проти PostgreSQL для записів в електронній комерції: хто першим задихається (і як цього уникнути)

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

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

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

Неприємна правда: обидві можуть задихнутися

MariaDB і PostgreSQL — серйозні СУБД. Обидві можуть забезпечити приголомшливу пропускну здатність і дивовижно цивілізовані затримки. Вони також обидві можуть розвалитися у дуже передбачуваних сценаріях, коли ви робите записні операції для електронної комерції як натовп: резервування запасів, стани платіжних машин, оцінки доставки, використання купонів і «записувати все вічно».

Різниця в тому, як вони ламаються, скільки попереджень ви отримуєте і наскільки дорого усунути режим відмови.

  • PostgreSQL зазвичай обмежується динамікою WAL і контрольних точок, боргом vacuum і ризиком обертання ідентифікаторів транзакцій, якщо ігнорувати обслуговування. Коли він «не радіє», ви побачите тиск на IO, блоут і дивні черги блокувань, які виглядають як «база жива, але нічого не рухається».
  • MariaDB (InnoDB) зазвичай обмежується тиском на redo/binlog, динамікою відставання реплікації, взаємною блокуванням/конкуренцією навколо «гарячих» рядків і іноді налаштуваннями «ми налаштували швидкість», які тихо позбавляють гарантій відновлення. Коли він «не радіє», ви побачите відставання репліки, сплески дедлоків і коміти, що вишиковуються в чергу як погана черга контролю аеропорту.

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

Патерни записів, що ламають електронну комерцію

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

1) Гарячі рядки: інвентар, купони та лічильники

Якщо у вас один SKU стає вірусним, один рядок інвентарю стає предметом суперечки. Якщо у вас «залишилось використань купону» в одному рядку, він теж стає предметом суперечки. Якщо ви відстежуєте «orders_today» як один лічильник, ви створили власний внутрішній DDoS.

Обидві СУБД можуть обробляти конкуренцію, але жодна не може порушити закони фізики: один рядок можна оновлювати лише певну кількість разів за секунду, перш ніж ви серіалізуєте весь процес оформлення. Виправлення рідко полягає в «більше CPU». Зазвичай це зміна моделі даних або ізоляція гарячого шляху.

2) Довгі транзакції під час робочих процесів оформлення

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

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

3) Вторинні індекси скрізь

Кожен індекс — це запис. Кожен індекс також потребує обслуговування. У таблицях з великим обсягом записів (orders, order_items, payments, inventory_events) ентузіазм щодо індексів — це спосіб купити затримки та посилення IO за повну вартість.

4) Інтенсивний append-only аудит без партиціонування

Таблиці аудиту — це нові логи. Хоча логи ротаціюються, таблиці аудиту часто — ні. Таблиці з великою кількістю додавань без партиціонування призводять до блоату, гігантських індексів, повільних видалень і стресу vacuum (Postgres) або тиску на purge (InnoDB).

5) Реплікація й надійність перетворені на трюк продуктивності

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

Жарт №1: Вимкнути надійність для швидшого оформлення — це як зняти паски безпеки, бо ви запізнюєтесь: ETA покращується, аж поки не перестане.

Як PostgreSQL зазвичай першим задихається

Механіка записів Postgres в одному абзаці (версія, яка потрібна о 2:00 ночі)

Postgres використовує MVCC з версіями кортежів: оновлення створюють нові версії рядків, старі версії лишаються, поки vacuum їх не звільнить. Кожна транзакція пише WAL для надійності та реплікації. Контрольні точки зливають брудні сторінки. Якщо autovacuum відстає, ви отримуєте блоут і зростаючий IO. Якщо контрольні точки занадто агресивні або WAL обмежений, з’являються IO-сплески і затримка commit. Якщо ігнорувати обслуговування ідентифікаторів транзакцій, можна наблизитися до wraparound і змушені будете робити аварійний vacuum в найгірший момент.

Режим відмов A: Тиск WAL і контрольних точок → сплески затримок

Під сильним навантаженням на запис WAL-потік стає серцебиттям. Якщо WAL не встигає записуватися (повільний диск, насичені IOPS, погана поведінка fsync), коміти вишиковуються в чергу. Потім приходять контрольні точки і починають зливати багато брудних буферів, конкуруючи з foreground-записами. Симптом зазвичай — сплески p99 затримки і повна завантаженість IO, не обов’язково CPU.

Операційно Postgres має властивість «видати себе» через метрики: частота контрольних точок, buffers written by backends, часи запису WAL. Якщо ви дивитесь.

Режим відмов B: борг vacuum → блоут → все сповільнюється

MVCC чудовий, поки ви не забудете прибирати. Якщо ваші таблиці з великою кількістю записів постійно оновлюються і autovacuum не встигає, ви отримуєте блоут. Блоут означає більші індекси, більший heap, більше промахів кеша, більше IO. База все ще робить ту саму логічну роботу, але тепер вона тягне великий багажник скрізь.

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

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

Postgres добре справляється з блокуваннями на рівні рядка, але довгі транзакції, DDL у продакшені та хибні патерни «SELECT … FOR UPDATE» можуть створювати ланцюги блокувань. Ви почуєте: «Postgres сьогодні повільний.» Він не повільний; він заблокований.

Режим відмов D: занадто багато з’єднань (і ілюзія масштабування)

Postgres використовує процесну модель; надмірна кількість з’єднань створює витрати пам’яті і контекстні переключення. Система з великою кількістю активних з’єднань може виглядати як проблема CPU, але часто це проблема управління з’єднаннями. Виправлення нудне: пулінг, адекватні таймаути і backpressure.

Як MariaDB зазвичай першим задихається

Механіка записів InnoDB в одному абзаці (версія «чому коміти застрягли?»)

InnoDB використовує redo логи і buffer pool. Записи йдуть у пам’ять і redo; сторінки зливаються пізніше. Binary logs (binlog) записують зміни для реплікації. Commit може включати злиття redo і/або binlog залежно від налаштувань надійності та поведінки групового коміту. Під навантаженням система часто обмежена пропускною здатністю запису redo, поведінкою flush або застосуванням змін на репліках.

Режим відмов A: відставання реплікації стає реальним простоєм

Багато налаштувань електронної комерції опираються на репліки для читань. Під write-heavy навантаженням репліки можуть відставати. Тоді ваш застосунок читає застарілий стан: інвентар доступний, коли вже ні, стан платежу виглядає «pending», коли він «captured», і клієнти отримують дубльовані підтвердження. База може бути здорова; архітектура — ні.

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

Режим відмов B: конкуренція за гарячі рядки і дедлоки

InnoDB загалом відмінний, але гарячі рядки (інвентар, лічильники, рядки купонів) створюють блокування. При достатній конкуренції з’являються дедлоки. Дедлоки не є самі по собі поганими — InnoDB їх виявляє і відкочує одну транзакцію — але повторні дедлоки можуть стати стелею пропускної здатності і викликати натовп повторів.

Режим відмов C: fsync і налаштування flush, що швидкі до краху

MariaDB дозволяє легко торгувати надійністю за продуктивність (і люди так роблять). Налаштування на кшталт пом’якшеного flush редо або sync_binlog зменшують fsync і підвищують пропускну здатність. Потім нода падає, і ви виявляєте, що ваш «малий ризик» — це кошик відсутніх замовлень.

Режим відмов D: посилення записів у вторинних індексах і розриви сторінок

Таблиці з великим навантаженням на запис з кількома вторинними індексами можуть створювати багато випадкового IO. Якщо первинний ключ погано обраний (наприклад, випадковий UUID без стратегії локальності), ви можете посилити розриви сторінок і перемикання буферного пулу. Це не унікально для MariaDB, але кластеризований індекс InnoDB робить вибір первинного ключа особливо значущим.

Посібник з вибору: що обрати для write-heavy checkout

Якщо ваш патерн записів — «багато оновлень одного й того ж рядка»

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

  • Використовуйте append-only події для рухів інвентарю і обчислюйте доступність за допомогою похідного представлення (або кешованої проєкції) замість постійних оновлень одного рядка.
  • Шардуйте лічильники (по хвилинах/кошиках) замість одного глобального лічильника.
  • Використовуйте ключі ідемпотентності, щоб повтори не множили записи.

Вибір СУБД тут допомагає менше, ніж дизайн схеми та транзакцій.

Якщо ваш патерн записів — «append-only, висока інтенсивність»

Postgres часто легше утримувати стабільним, якщо ви інвестуєте в партиціонування і налаштування autovacuum; він надає сильну інструментацію. MariaDB також може працювати дуже добре, але потрібно пильно стежити за тиском redo/binlog і швидкістю застосування на репліках.

Якщо ви сильно покладаєтесь на читальні репліки для читань, що впливають на коректність

Будьте обережні з асинхронною реплікацією MariaDB, якщо не спроектували систему для цього. Асинхронна реплікація Postgres також відстає, але в Postgres-шопах часто використовують більш чіткі патерни: read-your-writes на первинному для критичних потоків, пулер плюс логіка маршрутизації. У будь-якому випадку: якщо читання впливає на рішення про запис (залишок інвентарю, чинність купону), читайте з джерела з правильною консистентністю.

Якщо коротко: хто першим задихається?

  • PostgreSQL схильний першим «задихнутися» через IO та борг обслуговування: пропускна здатність WAL, сплески flush контрольних точок, відставання vacuum, перевантаження з’єднань.
  • MariaDB схильна першим «задихнутися» через поведінку комітів/реплікації та конкуренцію: flush/redo/binlog, відставання реплік, конкуренція на гарячих рядках, хвилі дедлоків.

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

Цікаві факти та історичний контекст (корисне, не тривіальне)

  1. Лінія MVCC PostgreSQL походить з академічних робіт 1980-х; саме завдяки багатоверсійності читання не блокують запис традиційним способом.
  2. InnoDB не спочатку був «рушієм MySQL»; він став дефолтним пізніше, бо виправив епоху «о, ні, блокування на рівні таблиці», яку старі користувачі MySQL добре пам’ятають.
  3. MariaDB була створена як форк через побоювання щодо управління MySQL; багато операційних поведінок залишилися у MySQL-стилі, включно з конвенціями реплікації.
  4. Реплікація Postgres дозрівала пізніше за MySQL-подібний binlog; сучасна стрімінг-реплікація сильна, але екосистема все ще агресивніше просуває найкращі практики, як-от пулінг з’єднань.
  5. Оновлення в Postgres не відбуваються на місці; кожне оновлення створює новий кортеж. Це перевага, і також причина, чому блоут — це те, що треба планувати в системах з великою кількістю записів.
  6. Кластеризований первинний ключ InnoDB означає, що дані таблиці фізично організовані за первинним ключем. Вибір первинного ключа може сильно впливати на посилення записів і розриви сторінок.
  7. Postgres має «HOT-оновлення» (heap-only tuple updates), які іноді дозволяють уникнути оновлень індексів і можуть суттєво зменшити записний IO для певних патернів оновлень.
  8. Обидві системи можуть мати «асинхронний коміт» та інші компроміси надійності. Пастка — трактувати ці налаштування як фічі продуктивності, а не як множники ризику.
  9. Великі транзакції непропорційно шкодять реплікації в обох екосистемах: вони затримують видимість на репліках і ускладнюють відновлення після краху.

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

Коли затримка записів у checkout зростає, у вас є хвилини, щоб вирішити: це CPU, IO, блокування, реплікація чи нагромадження з’єднань зі сторони застосунку? Ось порядок, який найшвидше знаходить вузьке місце на практиці.

Перше: підтвердьте, що симптом реальний і масштабований

  • Чи піднялась p95/p99 затримка коміту, чи лише підмножина endpoint’ів?
  • Чи це всі записи, чи лише одна таблиця (inventory/orders/payments)?
  • Чи вражений primary або лише репліки?

Друге: перевірте блокування і очікування, а не «повільні запити»

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

Третє: перевірте насичення IO і поведінку fsync

Якщо сховище завантажене, база не зможе швидко комітити. Перевірте використання пристрою, час очікування і швидкості запису WAL/binlog. Якщо у вас мережеве сховище, підозрюйте його як першого підозрюваного.

Четверте: здоров’я реплікації і відставання

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

П’яте: борг обслуговування

В Postgres: autovacuum, блоут, контрольні точки. В MariaDB: purge lag, довжина history list, тиск buffer pool. Обслуговування — це місце, де «вчора було добре» помирає.

Парафразована ідея Вернера Вогельса (надійність/ops): «Усе ламається; проектуйте так, щоб ви могли швидко відновитися.» Це позиція, яка потрібна під час інцидентів із записами — швидке обмеження шкоди краще за ідеальну діагностику.

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

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

Задача 1: Чи прив’язана система до IO зараз?

cr0x@server:~$ iostat -x 1 5
Linux 6.1.0 (db-primary)  12/29/2025  _x86_64_  (16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.44    0.00    6.21   22.35    0.00   59.00

Device            r/s     w/s   rkB/s   wkB/s  avgrq-sz avgqu-sz   await  svctm  %util
nvme0n1         120.0  950.0  4800.0 76000.0      150.2      9.80   10.3   0.7   78.0
dm-0            118.0  940.0  4700.0 75500.0      150.6     14.10   15.5   0.8   92.0

Значення: Високий %iowait і завантаження пристрою %util близько до насичення з підвищеним await вказують, що IO є обмежувачем; коміти будуть шикуватися в чергу.

Рішення: Зменште тиск записів (feature flags, rate limits), перевірте, щоб WAL/binlog не конкурували з даними на тому ж пристрої, і після інциденту розгляньте збільшення налаштувань checkpoint/WAL (Postgres) або поведінки групового коміту redo/binlog (MariaDB).

Задача 2: Який процес завдає IO-пошкодження?

cr0x@server:~$ pidstat -d 1 5
Linux 6.1.0 (db-primary)  12/29/2025  _x86_64_  (16 CPU)

#      Time   UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s  Command
12:01:01 PM   999     21450      50.00  68000.00      0.00  postgres
12:01:01 PM   999     21990       0.00   4200.00      0.00  postgres
12:01:01 PM   999     18210       0.00  39000.00      0.00  mariadbd

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

Рішення: Якщо записи домінуються процесом БД, заходьте всередину БД для інформації про блокування/WAL. Якщо ні — знайдіть справжнього писача (резервне копіювання, logrotate, неконтрольована пакетна робота).

Задача 3 (Postgres): Чи ми чекаємо на блокування чи на IO?

cr0x@server:~$ psql -X -c "SELECT wait_event_type, wait_event, count(*) FROM pg_stat_activity WHERE state <> 'idle' GROUP BY 1,2 ORDER BY 3 DESC;"
 wait_event_type |     wait_event      | count
----------------+---------------------+-------
 Lock           | transactionid        |    23
 IO             | WALWrite             |     7
 LWLock         | BufferMapping        |     4
                |                     |     2

Значення: Багато сесій, що чекають на Lock/transactionid, вказують на конкуренцію або довгі транзакції. IO/WALWrite вказує на вузьке місце запису WAL.

Рішення: Якщо домінують очікування блокувань — ідентифікуйте блокувальників і скоротіть транзакції. Якщо домінує WALWrite — зосередьтесь на сховищі/налаштуваннях WAL і поведінці контрольних точок.

Задача 4 (Postgres): Хто кого блокує?

cr0x@server:~$ psql -X -c "SELECT a.pid AS blocked_pid, a.usename, a.query AS blocked_query, pg_blocking_pids(a.pid) AS blocking_pids FROM pg_stat_activity a WHERE cardinality(pg_blocking_pids(a.pid)) > 0;"
 blocked_pid | usename |            blocked_query             | blocking_pids
------------+---------+--------------------------------------+--------------
      18722 | app     | UPDATE inventory SET reserved = ...  | {18410}
      18740 | app     | UPDATE inventory SET reserved = ...  | {18410}

Значення: Один PID-блокувальник утримує замок і зупиняє багато записів оформлення — класичний випадок гарячого рядка або довгої транзакції.

Рішення: Перевірте блокувальника; якщо безпечно — скасуйте його. Потім виправте патерн застосунку (зменште область блокування, уникайте мережевих викликів всередині txn, перерахуйте інвентар).

Задача 5 (Postgres): Чи контрольні точки не трясуть систему?

cr0x@server:~$ psql -X -c "SELECT checkpoints_timed, checkpoints_req, buffers_checkpoint, buffers_backend, checkpoint_write_time, checkpoint_sync_time FROM pg_stat_bgwriter;"
 checkpoints_timed | checkpoints_req | buffers_checkpoint | buffers_backend | checkpoint_write_time | checkpoint_sync_time
------------------+-----------------+-------------------+-----------------+-----------------------+----------------------
              122 |             480 |           9823412 |         2210344 |              8931201  |              1203310

Значення: Високий checkpoints_req відносно timed вказує, що WAL тисне і примушує контрольні точки. Високий buffers_backend означає, що бекенди роблять власні зливи — часто погано для затримок.

Рішення: Після стримування інциденту налаштуйте max_wal_size, checkpoint_timeout і checkpoint_completion_target, та перевірте пропускну здатність сховища для WAL і даних.

Задача 6 (Postgres): Чи autovacuum встигає за гарячими таблицями?

cr0x@server:~$ psql -X -c "SELECT relname, n_dead_tup, n_live_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;"
   relname    | n_dead_tup | n_live_tup |     last_autovacuum
--------------+------------+------------+--------------------------
orders        |   18422012 |   22100410 | 2025-12-29 11:02:14+00
payments      |    9221011 |    8012200 | 2025-12-29 10:41:02+00
inventory     |    5120099 |     210440 | 2025-12-29 07:12:55+00

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

Рішення: Налаштуйте порогові значення autovacuum на таблицю, зменшіть churn оновлень або перейдіть на append-only події. Розгляньте партиціонування для таблиць з високим обігом.

Задача 7 (Postgres): Чи ми тонули в з’єднаннях?

cr0x@server:~$ psql -X -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;"
        state        | count
---------------------+-------
 idle                |   820
 active              |   160
 idle in transaction |    44

Значення: Багато idle може бути нормальним, але idle in transaction — небезпечно: утримує снапшоти/блоки і блокує vacuum.

Рішення: Застосуйте таймаути на рівні застосунку, вбивайте «idle in transaction» сесії і використовуйте пулер. Також виправте шляхи коду, що залишають транзакції відкритими.

Задача 8 (MariaDB): Чи ми заблоковані або є дедлоки?

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
...
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 98234123, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 18220, OS thread handle 140201, query id 921103 app updating
UPDATE inventory SET reserved=reserved+1 WHERE sku_id=771 AND warehouse_id=3;
*** (2) TRANSACTION:
TRANSACTION 98234124, ACTIVE 0 sec fetching rows
...

Значення: Є реальні дедлоки на гарячих рядках (inventory). Дедлоки спричиняють повтори; повтори посилюють навантаження.

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

Задача 9 (MariaDB): Чи відставання реплікації — реальний вогонь?

cr0x@server:~$ mysql -e "SHOW SLAVE STATUS\G" | egrep "Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running|Last_SQL_Error"
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 148
Last_SQL_Error:

Значення: Репліка відстає на 148 секунд. Читання з неї застарілі; якщо застосунок використовує її для перевірок інвентарю/купонів, ви продаватимете привиди.

Рішення: Маршрутуйте критичні читання на primary; деградуйте некритичні читання до кешу; дослідіть пропускну здатність apply і довгі транзакції на primary.

Задача 10 (MariaDB): Чи ми комітимо безпечно, або «швидко і розслаблено»?

cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog');"
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_flush_log_at_trx_commit | 2   |
| sync_binlog                   | 0    |
+------------------------------+-------+

Значення: innodb_flush_log_at_trx_commit=2 і sync_binlog=0 — класичні прискорювачі пропускної здатності, що зменшують гарантії надійності.

Рішення: Для checkout перейдіть до 1/1 (або свідомо обраного компромісу). Якщо ви не можете собі цього дозволити, потрібні архітектурні зміни (батчування, eventing або інше сховище), а не ілюзія, що фізика домовиться.

Задача 11 (Postgres): Чи вибухає генерація WAL?

cr0x@server:~$ psql -X -c "SELECT now() as ts, pg_current_wal_lsn();"
              ts              | pg_current_wal_lsn
-----------------------------+--------------------
 2025-12-29 12:05:13.112+00  | 3A/9F2C1B80

Значення: Один зразок недостатній; порівнюйте LSN з часом. Якщо він швидко стрибає, ви швидко генеруєте WAL — часто від оновлень, індексного churn або великих пакетних робіт.

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

Задача 12 (Linux): Чи файловa система бреше про затримку flush?

cr0x@server:~$ sudo blktrace -d /dev/nvme0n1 -w 3 -o - | blkparse -i -
  8,0    1        1     0.000000000  21450  Q  WS 0 + 8 [postgres]
  8,0    1        2     0.000210000  21450  G  WS 0 + 8 [postgres]
  8,0    1        3     0.004900000  21450  C  WS 0 + 8 [0]

Значення: Ви бачите завершення синхронних записів; якщо часи завершення (C) сильно ростуть, затримка flush сховища шкодить комітам.

Рішення: Перевірте клас сховища, прошивку, політики RAID/кешу. Для хмарних дисків розгляньте provisioned IOPS або локальний NVMe для WAL/redo. Якщо не можете покращити сховище, зменшуйте частоту fsync тільки там, де коректність дозволяє (не checkout).

Задача 13 (Postgres): Знайти «idle in transaction» винних з текстом запиту

cr0x@server:~$ psql -X -c "SELECT pid, usename, now()-xact_start AS xact_age, state, left(query,120) AS q FROM pg_stat_activity WHERE state='idle in transaction' ORDER BY xact_age DESC LIMIT 10;"
  pid  | usename |  xact_age   |        state         | q
-------+---------+-------------+----------------------+------------------------------------------------------------
 19221 | app     | 00:12:14.22 | idle in transaction  | SELECT * FROM carts WHERE cart_id=$1 FOR UPDATE;

Значення: Хтось відкрив транзакцію, взяв блокування, а потім став idle. Це блокує vacuum і може блокувати записувачів.

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

Задача 14 (MariaDB): Перевірте довжину history list InnoDB і стан purge

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | egrep -i "History list length|Log sequence number|Last checkpoint"
History list length 923441
Log sequence number 1123341234412
Last checkpoint at 1123340031129

Значення: Велика довжина history list може вказувати на відставання purge, часто через довгі транзакції. Це збільшує збереження undo і може деградувати продуктивність.

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

Задача 15 (Обидві): Чи випадково ми робимо величезні транзакції?

cr0x@server:~$ sudo journalctl -u checkout-worker -n 50 --no-pager
Dec 29 12:06:01 app-1 checkout-worker[3112]: bulk_reservation job started: cart_id=... items=420
Dec 29 12:06:02 app-1 checkout-worker[3112]: db transaction open for cart_id=... 
Dec 29 12:06:55 app-1 checkout-worker[3112]: db transaction commit cart_id=... duration_ms=52981

Значення: 52-секундна транзакція — це не «трохи повільно». Це фабрика блокувань/undo/WAL. Вона зашкодить як Postgres, так і MariaDB.

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

Три корпоративні міні-історії з передової write-path

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

Вони вели маркетплейс з сезонними піками. Команда перемістила читальний трафік на репліки і сказала бізнесу, що primary «буде лише для записів». Звучало чисто: primary — для записів, репліки — для читань, всі щасливі.

Неправильне припущення було тонким: вони вважали, що «читання безпечні будь-де». Але їхній потік оформлення читав доступність інвентарю й дійсність купонів з репліки, щоб зменшити навантаження. За звичного відставання — в секундах — це працювало. Під час flash sale відставання реплікації зросло. Репліка показувала, що інвентар ще доступний. Клієнти продовжували оформлювати замовлення. Primary правильно відкидав деякі оновлення, але застосунок уже пообіцяв доступність і нарахував знижки, опираючись на застарілі читання.

Підтримка була завалена питаннями «чому мені зняли кошти, якщо ви сказали, що товар був в наявності?» Інженери дивилися на графіки. Primary був «зеленим». CPU був нормальний. Репліка теж була «зеленою», крім однієї метрики, на яку ніхто не піднімав сповіщення: lag.

Виправлення не було героїчним налаштуванням. Вони змінили політику: будь-яке читання, що впливає на рішення про запис (інвентар, викуп купону, стан платежу), має бути read-your-writes консистентним. На практиці: читати з primary або з кворумного консистентного сховища, і кешувати лише після коміту. Вони також додали жорстку відсіч: якщо відставання репліки перевищує поріг, застосунок припиняє використовувати репліки для цих endpoint’ів.

Постмортемний урок був різким: застарілі читання — це баг коректності, а не фіча продуктивності. Репліки для масштабування, а не для того, щоб прикидатися, що час необов’язковий.

Міні-історія 2: Оптимізація, що дала зворотний ефект

Рітейлер на MariaDB мав жахливе p99 у checkout. Хтось знайшов звичні підозрювані: забагато fsync, латентність коміту і сховище, яке було «не в захваті, але ж живе». Вони «оптимізували», пом’якшивши налаштування надійності. Коміти стали швидшими. Усі похвалилися і повернулися до випуску фіч.

Через два місяці паніка ядра вивела primary з ладу. Failover відбувся. Сайт залишився вгору. А потім почалося дивне: декілька замовлень були в логах застосунку, але відсутні в базі; деякі платежі були захоплені, але стан машини замовлення відсутній; підтримка мусила вручну звіряти стан по звітам платіжного провайдера.

Немає нічого подібного до усвідомлення, що ви побудували дрібну облікову систему, де «врешті-решт узятися» означає «хтось плаче». Вони повернули налаштування надійності для схеми checkout і перенесли роботу з продуктивності на безпечніші рівні: менше індексів на гарячих таблицях, менші транзакції і явне чергування бронювань інвентарю. Справжній виграш прийшов від зменшення конкуренції за блокування і write amplification — а не від того, що вони прикидалися, ніби диски ніколи не виходять з ладу.

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

Компанія з підпискою вела Postgres. Вони не були розкішними. Вони були дисциплінованими. Кожного кварталу вони проводили «репетицію пожежі для write-path»: імітували сплеск, перевіряли алерти і репетирували план дій. У них також було нудне правило: ніякого DDL під час піку, і кожна зміна схеми має бути поетапною з відкатним планом.

Одного дня маркетингова кампанія пройшла краще, ніж очікували. Записи підскочили. Затримки зросли, але залишались в межах. Потім одна репліка почала сильно відставати. On-call не дискутував про філософію; він слідував чеклісту: маршрутні критичні читання на primary, скидати непотрібні функції запису (події wishlist, історія переглядів) і стежити за WAL/контрольними точками.

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

Інцидент ніколи не дійшов до клієнтів. Не тому, що Postgres магічний. Бо хтось робив нудну роботу: видимість, репетиції та контрольовані зміни.

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

1) Симптом: «База повільна», але CPU низький

Корінь: черги блокувань або очікування IO. База не обчислює; вона чекає.

Виправлення: У Postgres — перевірте події очікування в pg_stat_activity і PIDs блокувальників; при необхідності вбивайте блокувальника. У MariaDB — перегляньте InnoDB status на предмет очікувань/дедлоків. Потім скоротіть транзакції і зменшіть оновлення гарячих рядків.

2) Симптом: p99 сплески кожні кілька хвилин

Корінь: поведінка checkpoint/flush (контрольні точки Postgres, flush-шторм у MariaDB) або періодичні фон-роботи.

Виправлення: Розподіліть IO за часом налаштуванням контрольних точок (Postgres) і забезпечте розділення WAL/даних, де можливо. Для MariaDB перевірте розміри redo логів і налаштування надійності, а також аудит фон-робіт на предмет стрибкоподібних оновлень.

3) Симптом: репліки «вгору», але клієнти бачать неконсистентний стан

Корінь: відставання реплікації і читання з реплік для логіки, що впливає на коректність.

Виправлення: Забезпечте read-your-writes для checkout і інвентарю. Додайте маршрутизацію з урахуванням lag. Впровадьте ідемпотентність, щоб повтори не дублювали записи.

4) Симптом: сплески дедлоків під час акцій

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

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

5) Симптом: дискове використання Postgres росте, запити повільнішають тиждень за тижнем

Корінь: блоут від оновлень; autovacuum відстає; індекси ростуть.

Виправлення: Налаштуйте autovacuum по таблицях, зменшіть churn оновлень, розгляньте партиціонування і налаштування fillfactor. Якщо блоут серйозний — заплануйте контрольований перепис (vacuum full / reindex / rebuild) з простоєм або онлайн-стратегією.

6) Симптом: коміти в MariaDB повільні, але читання в порядку

Корінь: тиск на redo/binlog, затримки fsync або груповий коміт binlog не встигає.

Виправлення: Перевірте продуктивність fsync на сховищі. Переконайтесь, що ви не ділите redo/binlog з шумними сусідами. Тримаєте транзакції маленькими. Якщо увімкнена реплікація — перевірте налаштування binlog відповідно до вимог надійності.

7) Симптом: autovacuum Postgres працює «постійно» і все одно не справляється

Корінь: посилення записів від надмірної кількості індексів і частих оновлень; замалий maintenance_work_mem; довгі транзакції, що перешкоджають очищенню.

Виправлення: Видаліть невикористовувані індекси, розбийте роботу оновлень, виправте довгі транзакції і налаштуйте autovacuum workers/costs. Також перевірте наявність «idle in transaction».

Жарт №2: Якщо ваше виправлення — «збільшити max_connections», ви фактично вирішуєте пробку на дорозі, додаючи більше машин.

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

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

  1. Визначте межі коректності. Ідентифікуйте читання, що впливають на рішення про покупку (інвентар, викуп купону, статус платежу). Примусьте їх бути консистентними з записами.
  2. Скоротіть транзакції. Жодних мережевих викликів всередині транзакцій БД. Якщо потрібна координація — використовуйте ключі ідемпотентності і state machine з частими commit.
  3. Відкиньте індексну марність. Для write-heavy таблиць залишайте тільки індекси, що окупаються. Перевіряйте по статистиці запитів, а не по думкам.
  4. Партиціонуйте append-only монстрів. Orders/events/audit таблиці мають бути партиціоновані за часом або орендарем, якщо вони швидко ростуть і запитуються по недавніх діапазонах.
  5. Зробіть гарячі рядки рідкістю. Замініть глобальні лічильники на розбиті по бакетах. Замініть «оновлення рядка інвентарю на одиницю» на виділення резервів або event sourcing з проєкцією.
  6. Backpressure краще за backlog. Лімітуйте записи на краю, коли БД під стресом. Контрольоване «повторіть пізніше» краще за каскадну відмову.
  7. Інструментуйте шлях запису. Відстежуйте затримку коміту, час очікування блокувань, відставання реплікації і глибину черги на рівні застосунку.
  8. Ретрови проектуйте як частину системи. Ретрай повинні бути ідемпотентними і з джитером, інакше ви створите власний інцидент.
  9. Відокреміть OLTP від аналітики. Звітні запити і «експортуй все» роботи не повинні ділити той самий бюджет блокувань і IO з checkout.
  10. Тренуйтеся у відновленні і реагуванні на lag. Ви не хочете винаходити реакцію на lag під час промоції.

Що б я зробив сьогодні, обираючи для write-heavy ecommerce core

  • Якщо у вас сильна операційна зрілість і ви хочете глибоку інструментацію і надійну транзакційну семантику: PostgreSQL, з серйозною увагою до WAL/контрольних точок і vacuum з першого дня.
  • Якщо ваша організація вже MySQL/MariaDB-нативна і ви можете забезпечити дисципліну транзакцій і гігієну реплікації: MariaDB, але ставте відставання реплікації як SLO першого порядку і не «оптимізуйте» надійність для фінансових записів.

FAQ

1) Чи PostgreSQL завжди кращий для write-heavy навантажень?

Ні. Postgres часто простіше аналізувати, коли ви його правильно інструментуєте, але він цілком може впасти від обмежень WAL/IO, боргу vacuum або перевантаження з’єднань. Успіх у write-heavy — це здебільшого форма транзакцій і пропускна здатність IO.

2) Чи MariaDB завжди швидший при вставках?

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

3) Яка головна причина, чому записи checkout у Postgres гальмують?

У продакшені: тиск IO навколо WAL/контрольних точок у поєднанні з блоутом або відставанням vacuum. Ланцюги блокувань — близький друг, зазвичай спричинені довгими транзакціями або хибними блокувальними читаннями.

4) Яка головна причина, чому записи checkout у MariaDB гальмують?

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

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

Можна, але розумійте витрати на запис. Випадкові UUID можуть збільшити розриви сторінок і погіршити локальність (особливо болісно в InnoDB). Якщо потрібні глобально унікальні ідентифікатори, розгляньте часово-упорядковані варіанти UUID або окремі сурогатні ключі там, де це доречно.

6) Чи можна «вирішити» проблему гарячих рядків за допомогою SELECT FOR UPDATE?

Можна серіалізувати коректність, але можна також серіалізувати пропускну здатність. Блокування — це коректність, а не продуктивність. Масштабоване вирішення — зменшити конкуренцію: виділяти інвентар батчами, шардувати по складу/бакету або використовувати сервіс бронювання, який контролює конкурентність.

7) Потрібен пулінг з’єднань?

Для Postgres: майже завжди, так, у масштабі. Для MariaDB: теж корисно, але поріг болю інший. У будь-якому випадку неконтрольований ріст з’єднань — це як загадкова повість про затримки.

8) Чи асинхронна реплікація прийнятна для електронної комерції?

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

9) Яке найбезпечніше покращення продуктивності для write-heavy checkout?

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

10) Яка СУБД дає кращі інструменти для діагностики вузьких місць записів?

Postgres загалом має сильнішу вбудовану інструментацію щодо очікувань, WAL і поведінки vacuum. MariaDB теж має хороші інструменти, але частіше опора йде на знімки стану InnoDB і метрики реплікації. У будь-якому разі інструменти мають значення тільки якщо ви справді ставите на них алерти.

Наступні кроки, які ви можете виконати

Якщо ви зараз керуєте write-heavy електронною комерцією і хочете менше сюрпризів, зробіть це в порядку:

  1. Зміпуйте межі транзакцій checkout і витягніть будь-які зовнішні виклики з них. Зробіть робочий процес ідемпотентним.
  2. Ідентифікуйте гарячі рядки (інвентар, купони, лічильники) і перерахуйте їх, щоб уникнути конкуренції на одному рядку.
  3. Аудит індексів у топ-5 таблиць з найбільшим обсягом записів. Приберіть усе, що не потрібне для критичних читань.
  4. Інструментуйте вузькі місця: Postgres — wait events/WAL/checkpoints/vacuum; MariaDB — дедлоки/redo/binlog/відставання реплікації.
  5. Впровадьте маршрутизацію з урахуванням відставання, щоб репліки ніколи не псували бізнес-логіку мовчки.
  6. Програйте навантаження, що схоже на реальне життя: зі сплесками, зсувом на декілька гарячих SKU, з ретраями і таймаутами. Вашій базі байдуже до середнього QPS.

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

← Попередня
Debian 13: запис UEFI зник — відновіть завантаження за допомогою efibootmgr за кілька хвилин
Наступна →
Docker: секрети без витоків — припиніть зберігати паролі в .env

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