PostgreSQL vs MongoDB транзакції: де реальність відрізняється від документації

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

Якщо вам колись доводилося вести розмову “але в документації написано, що це ACID”, поки пейджер безперервно вібрує на столі,
ви вже розумієте головну думку: транзакції — це не функція, це поведінка системи. А поведінка системи має гострі краї — особливо
коли до справи підключаються реплікація, перемикання провідного вузла, кеші й повторні спроби з боку застосунка.

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

Де документація закінчується і починається реальність

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

Модель транзакцій PostgreSQL старша, суворіша й з чіткими припущеннями. Вона побудована навколо MVCC і WAL,
і зазвичай ламається передбачуваними способами: contention за блокування, блоат (bloat), затримки реплікації, конфлікти на hot-standby.
Історія транзакцій у MongoDB з’явилася пізніше й накладається поверх replica set та документного рушія.
Вона може бути абсолютно правильною — допоки ви не додасте write concern, stepdown’и, довгі транзакції
та патерни навантаження, що сперечаються з її дизайном.

Операційна правда така: якщо вашій системі потрібні інваріанти між сутностями, коректність унікальностей і
передбачувані відкати при конкурентності, PostgreSQL — за замовчуванням. MongoDB може виконувати транзакції,
але за них доведеться платити, і треба явно визначати довговічність та семантику читань.
Якщо ви трактуєте “підтримує транзакції” як “працює як Postgres”, ви створюєте пастку для свого майбутнього я.

Коротка історія: чому системи поводяться так, як вони поводяться

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

  • PostgreSQL походить від POSTGRES (кінець 1980-х), розробленого в академічну еру, де коректність була пріоритетом, а не опційним товаром.
  • MVCC у PostgreSQL став визначальною стратегією конкурентності: читачі не блокують писачів, але vacuum стає вашим мовчазним сусідом, що з’їдає CPU вночі.
  • WAL (Write-Ahead Logging) — це хребет довговічності Postgres; завдяки йому відновлення після краху буває нудним у хорошому сенсі.
  • MongoDB почалася наприкінці 2000-х з акцентом на швидкість розробки і гнучкість документів; ранні версії опиралися на атомарність на рівні документа.
  • Replica set став операційним центром MongoDB; “primary” і “secondaries” — це не лише топологія, вони визначають семантику читань і записів.
  • Мультидокументні транзакції в MongoDB з’явилися пізніше (4.0+), спочатку для replica set, потім для шардованих кластерів — реалізовані через координування записів з додатковою обліковістю.
  • “Majority write concern” з’явився, бо асинхронна реплікація — це не довговічність; це оптимізм з хорошою маркетинговою командою.
  • За замовчуванням PostgreSQL має ізоляцію Read Committed, що дивує розробників, які очікують серіалізованої поведінки за замовчуванням.
  • Рівні read concern у MongoDB еволюціонували, щоб відповісти на питання “що я фактично прочитав” у реплікованому світі (local, majority, snapshot, linearizable).

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

Модель транзакцій PostgreSQL: MVCC, блокування і WAL

MVCC означає “читання бачить знімок”, а не “замки відсутні”

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

Блокування все ще важливі. Оновлення рядка потребує блокувань на рівні рядка. DDL вимагає важчих блокувань.
Унікальність забезпечується поведінкою блокувань на рівні індексів, що є безпечною, але може дивувати при “гарячих” ключах.
І якщо ви підвищуєте конкурентність, тримаючи блокування занадто довго, Postgres терпляче покаже вам чергу страждань.

Рівні ізоляції на практиці

PostgreSQL підтримує Read Committed, Repeatable Read і Serializable.
У продакшні більшість систем працює на Read Committed і покладається на обмеження та уважні запити.
Serializable справжній, але оптимістичний: він може відміняти транзакції з помилкою серіалізації при конкуренції.
Це не баг; це база даних, що каже вам “ваша модель конкурентності не серіалізується”.

Момент, коли “реальність відрізняється від документації”, настає, коли команда вмикає Serializable без реалізації повторних спроб
для SQLSTATE 40001. Тоді вони дізнаються, що коректність вимагає співпраці.

WAL і довговічність: COMMIT — це історія про диск, а не про почуття

У PostgreSQL довговічність фундаментально пов’язана із скиданням WAL на диск. Ручки — це
synchronous_commit, wal_sync_method і поведінка файлової системи та стореджу.
Реплікація додає ще один рівень: чи підтверджується коміт лише первинним вузлом, чи також репліками?

Коли хтось каже “Postgres втратив зафіксовані дані”, це зазвичай одне з:
асинхронна реплікація під час фейловеру, свідомо послаблена довговічність (synchronous_commit=off),
стек зберігання, що брешуть (кеш запису без належних бар’єрів), або людська помилка.

Суперсила Postgres: обмеження гарантовано виконуються в транзакціях

Зовнішні ключі, унікальні обмеження, exclusion constraints, check constraints. Ви можете моделювати інваріанти близько
до даних і покладатися на них під навантаженням. Це не лише зручність; це зменшення площі поверхні для гонок на рівні застосунку.

Модель транзакцій MongoDB: сесії, write concern і replica set

Транзакції в MongoDB існують — але вони не безкоштовні

Мультидокументні транзакції MongoDB (для replica set і шардованих кластерів) надають ACID-семантику в межах транзакції.
Слово “в межах” тут багато означає. Рушій підтримує додатковий стан, а координатор повинен управляти
комітом між учасниками (особливо в шардованих розгортаннях). Затримки зростають. Пропускна здатність часто падає.
При конкуренції ви можете бачити більше відхилень і тимчасових помилок, які вимагають повторних спроб.

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

Write concern — це різниця між “підтверджено” і “достатньо довговічно”

Write concern MongoDB визначає, що означає “успіх” для запису. Класична пастка: використання
w:1 (підтверджено первинним) і припущення, що це означає, що запис переживе фейловер.
Може бути. А може й ні, залежно від таймінгу й реплікації.

Для довговічності через відмову провідного вузла зазвичай хочуть w:majority плюс адекватні параметри журналювання
(journaling за замовчуванням увімкнено в сучасних версіях, але перевірте). Потім ви дізнаєтеся наступну реальність:
majority може бути повільнішим, особливо з повільними вторинними вузлами або розгортанням між зонами.

Read concern: що ви фактично прочитали?

Read concern у MongoDB контролює гарантії видимості: local, majority,
snapshot і linearizable (з обмеженнями). Read preference додає ще одну вісь:
читання з primary або secondary.

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

Сесії, повтори і податок “невідомого результату коміту”

Драйвери MongoDB реалізують retryable writes і поведінку повтору транзакцій. Добре — поки не ні.
Ви можете отримати випадки, коли клієнт таймаутиться і не знає, чи пройшов коміт.
Тоді застосунок повторює дію і випадково створює дублікати, якщо ви не спроектували ідемпотентність.

Це не злість MongoDB; це розподілені системи такі, як вони є. Але ви маєте планувати це.

Семантика, що має значення: практична матриця порівняння

1) Обсяг атомарності

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

2) Рівні ізоляції і здивування розробників

PostgreSQL за замовчуванням Read Committed: кожен оператор бачить знімок на початку операції. Багато аномалій запобігаються
обмеженнями та уважною структурою запитів, але ви все ще можете отримати гонки, якщо припустите “повторюване” поводження.

MongoDB: поза транзакціями ви перебуваєте в світі окремих операцій з read concern і write concern.
Всередині транзакцій ви можете отримати поведінку snapshot isolation. Сюрприз часто в тому, що означає “знімок”
відносно реплікації та read preference.

3) Семантика довговічності при фейловері

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

MongoDB: запис, підтверджений з w:1, може бути відкотений при фейловері. З w:majority
ризик відкату значно зменшується, бо запис репліковано на більшість.

4) “Читати свої записані” та монотонні читання

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

MongoDB: якщо читати з вторинних без сесій з причинно-консистентною поведінкою (та правильних налаштувань), можна читати застарілі дані.
Навіть із сесіями, зміни топології ускладнюють гарантії. Система може бути коректною; ваші припущення можуть не співпадати.

5) Наблюваність

PostgreSQL відкриває стан транзакцій і блокувань дуже прямо через системні каталоги і в’юхи.
MongoDB відкриває стан через serverStatus/currentOp, профайлер і метрики реплікації.
Обидві системи є спостережуваними. Postgres зазвичай дає чіткішу картину “хто блокує кого”; MongoDB змушує думати про здоров’я replica set і виконання write concern.

6) Операційний радіус ураження

Postgres: один поганий запит може заблокувати таблицю, надути сторедж або наситити I/O; відмови часто локалізуються на вузлі,
з лагом реплікації як вторинним симптомом.

MongoDB: хворий член replica set, повільні secondaries або вибори можуть перетворити “швидкі записи” в “чому все таймаутиться”.
В шардованих кластерах мульти-шардні транзакції можуть швидко поширити біль.

Режими відмов, що зустрічаєш о 2-й ночі

PostgreSQL: contention за блокування і “чому COMMIT повільний?”

Затримка COMMIT у Postgres зазвичай пов’язана з латентністю стореджу (fsync), contention на WAL або очікуванням синхронної реплікації.
Рідко це просто “Postgres повільний” в абстрактному сенсі. Майже завжди це “ваші налаштування довговічності роблять бажану семантику дорогою”.

Ще класика: довга транзакція заважає vacuum очищати мертві кортежі, призводить до блоату індексів,
і тоді все починає гальмувати у вигляді “випадкового I/O краху”.

MongoDB: вибори, очікування write concern і повторні спроби транзакцій

В MongoDB операційний біль часто виникає через те, що replica set виконує свою роботу:
відбуваються вибори, провідні вузли здають роль, і клієнти бачать тимчасові помилки. Якщо застосунок не обробляє їх добре,
інцидент виглядає як “база даних лежить”, коли насправді це “ваші повторні спроби — DDoS проти вашого власного primary”.

Транзакції можуть посилювати це. Транзакція, що тримається відкрита під час великої роботи, зв’язує ресурси
і підвищує шанс, що щось зміниться під вами (наприклад stepdown), примушуючи відкат і повторну спробу.

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

Обидві системи можуть поставити вас у ситуацію “чи це зафіксовано?” коли клієнт втрачає відповідь.
Postgres зазвичай залишає вам реалізацію ідемпотентності на шарі застосунку.
MongoDB робить проблему більш явною через retryable writes і поведінку підтвердження транзакцій — але ви все одно маєте її спроєктувати.

Одна цитата, яку варто повісити на стіну, бо вона вірна незалежно від вибору БД:
Hope is not a strategy. —Gene Kranz

Жарт №1: Транзакція — як обіцянка: вона заспокоює, поки її не доведеться виконувати в суді, тобто в продакшні.

Три міні-історії з корпоративного світу

Інцидент: неправильне припущення про “підтверджені записи”

Середня SaaS-компанія використовувала MongoDB для профілів користувачів і стану білінгу. Схема була чиста, код сучасний,
і команда мала звичку читати “acknowledged” як “довговічне”. Записи використовували дефолтний write concern, а застосунок
читав з primary. У стейджингу все було добре, і в продакшні теж місяцями.

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

Інцидент не був драматичною аварією. Було гірше: кілька дій клієнтів “зникли”. Надходили тікети в саппорт із скриншотами.
Інженери спочатку полювали на привидів у фронтенді, бо логи бекенду показували успішні записи.
Дані просто зникли.

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

Оптимізація, що зіграла зле: “давайте пришвидшимо коміти в Postgres”

Фінтех-команда використовувала PostgreSQL для навантаженого реєстру. Вони ганялися за регресією p99 і помітили, що
латентність комітів корелює з піками I/O. Хтось запропонував класичний трюк:
встановити synchronous_commit=off для “не критичних” записів і покластися на реплікацію.

Це спрацювало. Латентність одразу покращилася. Пропускна здатність зросла. Графіки виглядали як звіт для просування.
Потім на primary сталася некоректна подія живлення. Сервер перезавантажився. WAL не встиг бути скинутим для частини комітів.
База відновилася коректно — тобто відкотила ті транзакції, бо згідно з новими налаштуваннями вони ніколи не були гарантовано довговічними.

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

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

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

B2B-платформа зберігала замовлення в PostgreSQL і мала бекграунд-воркер, що “фіналізував” замовлення створенням рахунків,
зменшенням запасів і відправленням листів клієнтам. Робочий процес був розподілений між сервісами, бо як інакше.

Команда зробила непопулярну річ: змоделювала інваріанти в базі даних. Зменшення запасів було обмежено, щоб ніколи не опуститися нижче нуля.
Номери рахунків були унікальні з жорстким індексом. Машина станів замовлення була захищена check constraints,
а переходи виконувалися в транзакційному SQL.

Під час розгортання баг у роботі спричинив подвійне виконання воркера для підмножини замовлень. У багатьох системах це переростає в багатоденне прибирання.
Тут друге виконання в основному швидко завершувалося з помилками обмежень.
Сервіс логував помилки, SRE побачили сплеск, і клієнти майже нічого не помітили.

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

Практичні завдання: команди, виводи й рішення (12+)

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

Завдання 1 (PostgreSQL): Підтвердити налаштування ізоляції та довговічності

cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SHOW default_transaction_isolation; SHOW synchronous_commit; SHOW wal_level;"
 default_transaction_isolation
-----------------------------
 read committed
(1 row)

 synchronous_commit
--------------------
 on
(1 row)

 wal_level
-----------
 replica
(1 row)

Значення: За замовчуванням Read Committed; коміти чекають на скидання WAL; WAL налаштований для реплікації.
Рішення: Якщо ви дебагуєте аномалії, підтвердьте, чи застосунок очікує Repeatable Read/Serializable.
Якщо латентність комітів висока, тримайте synchronous_commit=on, поки ви явно не приймете втрату даних.

Завдання 2 (PostgreSQL): Визначити, хто блокує кого

cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocking.pid AS blocking_pid, blocking.query AS blocking_query FROM pg_stat_activity blocked JOIN pg_locks blocked_locks ON blocked_locks.pid = blocked.pid JOIN pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocking_locks.pid != blocked_locks.pid JOIN pg_stat_activity blocking ON blocking.pid = blocking_locks.pid WHERE NOT blocked_locks.granted;"
 blocked_pid |        blocked_query         | blocking_pid |         blocking_query
------------+------------------------------+--------------+-------------------------------
      24190 | UPDATE orders SET ...        |        23811 | ALTER TABLE orders ADD COLUMN
(1 row)

Значення: DDL блокує запис. Це не “транзакції повільні”, це “хтось взяв важке блокування”.
Рішення: Зупиніть DDL, якщо це небезпечно, або заплануйте його правильно. Використовуйте concurrent index builds і онлайн-патерни міграцій.

Завдання 3 (PostgreSQL): Перевірити довгі транзакції, що блокують vacuum

cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT pid, now() - xact_start AS xact_age, state, query FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_age DESC LIMIT 5;"
  pid  |  xact_age  | state  |                 query
-------+------------+--------+----------------------------------------
 31245 | 02:13:08   | idle   | BEGIN;
 29902 | 00:18:41   | active | SELECT ... FROM events ORDER BY ...
(2 rows)

Значення: Ідл-транзакція відкрита кілька годин. Вона фіксує старі версії рядків і може спричиняти блоат.
Рішення: Виправте застосунок: забезпечте короткі транзакції і завжди commit/rollback.
Розгляньте idle_in_transaction_session_timeout.

Завдання 4 (PostgreSQL): Перевірка тиску на WAL та чекпоінти

cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time FROM pg_stat_bgwriter;"
 checkpoints_timed | checkpoints_req | checkpoint_write_time | checkpoint_sync_time
------------------+-----------------+-----------------------+----------------------
             1021 |             487 |              98765432 |             1234567
(1 row)

Значення: Багато запитаних чекпоінтів; час запису високий. Тиск на чекпоінти може впливати на латентність комітів і I/O.
Рішення: Налаштуйте max_wal_size, checkpoint_timeout і переконайтеся, що сторедж може обробляти WAL та запис даних.

Завдання 5 (PostgreSQL): Виміряти лаг реплікації та вік ризику

cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT application_name, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;"
 application_name |   state   | sync_state | write_lag | flush_lag | replay_lag
------------------+-----------+------------+-----------+-----------+------------
 pg-replica-1      | streaming | async      | 00:00:02  | 00:00:03  | 00:00:05
(1 row)

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

Завдання 6 (PostgreSQL): Помітити помилки серіалізації і потребу в повторі

cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT datname, xact_commit, xact_rollback, conflicts FROM pg_stat_database WHERE datname='app';"
 datname | xact_commit | xact_rollback | conflicts
---------+-------------+---------------+-----------
 app     |    98765432 |        123456 |       842
(1 row)

Значення: Є відкати/конфлікти. Не всі відкати погані, але сплески можуть вказувати на помилки серіалізації чи deadlock.
Рішення: Корелюйте з помилками застосунку (SQLSTATE 40001, 40P01). Додайте логіку повторів з джитером і зменшіть гарячі точки конкуренції.

Завдання 7 (MongoDB): Перевірити здоров’я replica set і частоту виборів

cr0x@server:~$ mongosh --host rs0/mb-primary,mb-secondary-1,mb-secondary-2 --eval "rs.status().members.map(m=>({name:m.name,stateStr:m.stateStr,health:m.health,optime:m.optime.ts}))"
[
  { name: 'mb-primary:27017', stateStr: 'PRIMARY', health: 1, optime: Timestamp({ t: 1735550101, i: 1 }) },
  { name: 'mb-secondary-1:27017', stateStr: 'SECONDARY', health: 1, optime: Timestamp({ t: 1735550101, i: 1 }) },
  { name: 'mb-secondary-2:27017', stateStr: 'SECONDARY', health: 1, optime: Timestamp({ t: 1735550096, i: 1 }) }
]

Значення: Один secondary відстає на кілька секунд. Це має значення для латентності w:majority і застарілих читань з secondaries.
Рішення: Якщо majority-записи повільні, дослідіть відстаючих членів; якщо використовуються читання з secondaries, розгляньте жорсткіший read concern або маршрутизацію.

Завдання 8 (MongoDB): Підтвердити дефолтні write concern і journaling

cr0x@server:~$ mongosh --host mb-primary --eval "db.getMongo().getWriteConcern()"
{ w: 1 }

Значення: Дефолт — w:1. Це “підтверджено первинним”, а не “виживе після втрати primary”.
Рішення: Для критичних даних встановіть w:majority на клієнті або на рівні колекції, і використовуйте таймаути, щоб уникнути зависань записів.

Завдання 9 (MongoDB): Перевірити відхилені транзакції й “шторм” повторів

cr0x@server:~$ mongosh --host mb-primary --eval "db.serverStatus().transactions"
{
  currentActive: Long('14'),
  currentInactive: Long('3'),
  currentOpen: Long('17'),
  totalAborted: Long('982'),
  totalCommitted: Long('184433'),
  totalStarted: Long('185415')
}

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

Завдання 10 (MongoDB): Знайти повільні операції, що тримають транзакції відкритими

cr0x@server:~$ mongosh --host mb-primary --eval "db.currentOp({active:true, secs_running:{$gte:5}}).inprog.map(op=>({secs:op.secs_running,ns:op.ns,desc:op.desc,command:op.command && Object.keys(op.command)}))"
[
  { secs: 31, ns: 'app.orders', desc: 'conn12345', command: [ 'find' ] },
  { secs: 12, ns: 'app.inventory', desc: 'conn23456', command: [ 'update' ] }
]

Значення: Активні операції працюють довго. У транзакціях довгі опи збільшують ймовірність конфліктів і використання ресурсів.
Рішення: Додайте індекси, зменшіть обсяг сканування документів і обмежте обсяг роботи в транзакції. Якщо це runaway-запит — вбийте його свідомо.

Завдання 11 (MongoDB): Підтвердити read preference і read concern у клієнтському шляху

cr0x@server:~$ mongosh --host mb-primary --eval "db.getMongo().getReadPref()"
{ mode: 'secondaryPreferred' }

Значення: Читання можуть йти на secondaries. Це свідомий вибір коректності, а не безкоштовний трюк масштабування.
Рішення: Для видимості “я щойно записав” використовуйте читання з primary або причинно-консистентні сесії з відповідним readConcern.

Завдання 12 (PostgreSQL): Підтвердити поведінку fsync і виявити “брехливий сторедж” рано

cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SHOW fsync; SHOW full_page_writes; SHOW wal_log_hints;"
 fsync
-------
 on
(1 row)

 full_page_writes
------------------
 on
(1 row)

 wal_log_hints
---------------
 off
(1 row)

Значення: WAL скидається через fsync; full page writes увімкнено (важливо для безпеки після краху).
Рішення: Тримайте ці налаштування увімкненими в продакшні, якщо у вас немає конкретної причини і компенсуючих контролів.
Якщо підозрюєте сторедж, перевіряйте кеші на апаратному рівні в рамках інцидент-розбору.

Завдання 13 (PostgreSQL): Знайти deadlock-и і запити, що їх спричиняють

cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SELECT deadlocks, conflicts FROM pg_stat_database WHERE datname='app';"
 deadlocks | conflicts
-----------+-----------
        19 |       842
(1 row)

Значення: Deadlock-и мали місце. Postgres вб’є одного з учасників; ваш застосунок отримає помилку і має повторити.
Рішення: Визначте таблиці/запити (через логи), уніфікуйте порядок блокувань у коді і тримайте транзакції короткими.

Завдання 14 (MongoDB): Перевірити лаг реплікації в секундах, а не на відчуття

cr0x@server:~$ mongosh --host mb-primary --eval "rs.printSecondaryReplicationInfo()"
source: mb-secondary-1:27017
syncedTo: Mon Dec 30 2025 02:41:55 GMT+0000 (UTC)
0 secs (0 hrs) behind the primary

source: mb-secondary-2:27017
syncedTo: Mon Dec 30 2025 02:41:49 GMT+0000 (UTC)
6 secs (0 hrs) behind the primary

Значення: Один secondary відстає на 6 секунд. Це впливає на швидкість підтвердження більшістю і застарілі читання.
Рішення: Дослідіть цей вузол (диск, CPU, мережу). Якщо він регулярно відстає, він переслідуватиме ваші латентності транзакцій.

Завдання 15 (PostgreSQL): Підтвердити синхронні налаштування реплікації, якщо потрібна “відсутність втрат”

cr0x@server:~$ psql -h pg-primary -U postgres -d app -c "SHOW synchronous_standby_names; SHOW synchronous_commit;"
 synchronous_standby_names
--------------------------
 'ANY 1 (pg-replica-1)'
(1 row)

 synchronous_commit
--------------------
 on
(1 row)

Значення: Primary чекає щонайменше один синхронний standby. Це спосіб зменшити “помилки: підтверджено, але втрачено”.
Рішення: Використовуйте це для критичних систем, але моніторьте: воно пов’язує латентність записів зі здоров’ям реплік і якістю мережі.

План швидкої діагностики

Коли поведінка транзакцій “дивна”, не починайте дебатів про бази даних. Почніть з визначення вузького місця.
Ось порядок, що допомагає знайти корінь проблеми до появи запрошення на зустріч.

Перше: визначте, чи біль — це латентність, пропускна здатність чи коректність

  • Сплеск латентності: коміти повільні, запити повільні, таймаути.
  • Падіння пропускної здатності: зростання черг, бэклог воркерів, збільшення з’єднань.
  • Проблема коректності: відсутні записи, дублікати, читання у неправильному порядку.

Друге: перевірте “контракт довговічності та реплікації”

  • PostgreSQL: чи реплікація асинхронна? чи стався фейловер? чи synchronous_commit послаблений? чи повільно fsync?
  • MongoDB: який write concern? чи відбуваються вибори? чи відстають secondaries? чи клієнти читають з secondaries?

Третє: перевірте contention

  • PostgreSQL: блокування, deadlock-и, довгі транзакції, гарячі рядки, contention індексів.
  • MongoDB: конфлікти транзакцій, довгі опи, очікування write concern, оверхед координатора шардінгу.

Четверте: перевірте насичення стореджу та хоста

  • Латентність диска — невидима рука за “COMMIT повільний”.
  • Насичення CPU може виглядати як “блокування”, бо все сповільнюється і формуються черги.
  • Мережеві джитери можуть виглядати як “нестабільність бази”, бо реплікація і вибори чутливі до таймаутів.

П’яте: підтвердьте поведінку клієнта

  • Ви робите повтори на кожну помилку без backoff? Вітаю, ви створили множник відмови.
  • Таймаути коротші за час шляху коміту під час фейловеру? Тоді ви свідомо породжуєте “невідомий результат коміту”.
  • Ви змішуєте read preferences і очікуєте “читати свої записи”? Тоді ви граєте у рулетку в продакшні.

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

1) “Підтверджені дані зникли після фейловеру”

Симптоми: логи застосунку показують успішні записи; після фейловеру дані відсутні.

Корінь: асинхронна реплікація при фейловері (Postgres), або MongoDB-записи, підтверджені з w:1, відкотилися.

Виправлення: Для Postgres — використовувати синхронну реплікацію для критичних даних або уникати підвищення ролі відстаючих реплік.
Для MongoDB — використовувати w:majority і проєктувати ідемпотентність.

2) “Транзакції повільні лише під час піку”

Симптоми: p95/p99 латентність скачуть; пропускна здатність падає; CPU виглядає нормально; користувачі таймаутять.

Корінь: contention (блокування в Postgres; очікування write concern або конфлікти транзакцій у MongoDB), або насичення fsync стореджу.

Виправлення: Визначте ланцюги блокувань/довгі опи. Зменшіть обсяг транзакцій. Додайте або виправте індекси.
Перенесіть WAL/journal на швидший сторедж за потреби.

3) “Ми вмикнули Serializable і все почало падати”

Симптоми: помилки Postgres з serialization failures; шторм повторів.

Корінь: Serializable вимагає повторів; висока конкуренція викликає відкати.

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

4) “Транзакція MongoDB постійно відхиляється з тимчасовими помилками”

Симптоми: високий рівень відхилень; stepdown-и; застосунок бачить тимчасові помилки транзакцій.

Корінь: вибори, довгі транзакції, конфлікти або оверхед координації між шардами.

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

5) “Читання неконсистентні одразу після запису”

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

Корінь: читання з реплік/secondaries; недостатній read concern; лаг реплікації.

Виправлення: Направляйте read-your-writes на primary, або використовуйте сесії з причинною консистентністю і відповідний read concern; моніторьте лаг реплікації.

6) “Дискове використання Postgres постійно росте; запити повільнішають поступово”

Симптоми: блоат, зростаючі I/O, autovacuum не встигає.

Корінь: довгі транзакції і табличні оновлення, що створюють мертві кортежі; голодування vacuum.

Виправлення: Усуньте idle-in-transaction; налаштуйте autovacuum на рівні таблиці; розгляньте партиціювання; плануйте обслуговування за потреби.

7) “Ми використали secondaryPreferred і тепер маємо ‘неможливі’ баги”

Симптоми: лічильники йдуть назад; переходи станів виглядають поза порядком.

Корінь: застарілі читання з secondaries; припущення про монотонні читання не виконуються.

Виправлення: Використовуйте читання з primary для машин станів; якщо використовуєте читання з secondaries, свідомо прийміть застарілість і спроєктуйте UI/логіку під неї.

8) “Наша логіка повторів погіршила інцидент”

Симптоми: стрибок QPS і з’єднань під час простою; база даних падає сильніше.

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

Виправлення: Реалізуйте обмежений експоненційний backoff з джитером; відокремлюйте повторювані помилки; додавайте ідемпотентні токени; розгляньте circuit breakers.

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

Чекліст прийняття рішення: чи підходить ця робота для транзакцій Postgres або MongoDB?

  1. Чи потрібні інваріанти між сутностями? Якщо так — обирайте PostgreSQL. Якщо MongoDB, готуйтеся використовувати мультидокументні транзакції і платити за це.
  2. Чи потрібні сильні гарантії унікальності між багатьма документами? PostgreSQL простіший. MongoDB потребує ретельних індексів і дизайну транзакцій.
  3. Чи неприпустима втрата даних при фейловері? Postgres: синхронна реплікація і ретельна політика фейловеру. MongoDB: w:majority і читання більшістю там, де потрібно.
  4. Чи ваш патерн доступу переважно single-document? MongoDB підходить; не перетворюйте його ненароком на реляційну систему через постійні мультидокументні транзакції.
  5. Чи розробники надійно реалізують повтори? Якщо ні — уникайте налаштувань, що часто вимагають їх (Serializable скрізь; довгі транзакції MongoDB під конкуренцією).
  6. Чи можете ви забезпечити експертизу з експлуатації? Обидві системи її вимагають. Експертиза Postgres часто виглядає як тюнінг запитів + vacuum + дисципліна реплікації.
    Експертиза MongoDB — як гігієна replica set/shard + дисципліна write/read concern.

План впровадження: правильно налаштований Postgres для транзакційної коректності

  1. Моделюйте інваріанти в базі (FK, унікальні обмеження, check constraints).
  2. Тримайте транзакції короткими; уникайте “чатких” транзакцій як робочого процесу.
  3. Реалізуйте ідемпотентність для зовнішніх сайд-ефектів (пошта, платежі, вебхуки).
  4. Свідомо обирайте рівень ізоляції; додавайте повтори, якщо використовуєте Serializable.
  5. Визначте вашу політику фейловеру: асинхронно (можлива втрата) чи синхронно (вища латентність).
  6. Інструментуйте: wait-логи блокувань, deadlock-и, лаг реплікації, часи чекпоінтів, латентність fsync на хості.
  7. Встановіть запобіжники: statement_timeout, idle_in_transaction_session_timeout, ліміти пула з’єднань.

План впровадження: транзакції MongoDB без самосаботажу

  1. За замовчуванням віддавайте пріоритет одно-документним атомарним паттернам (embed, atomic operators).
  2. Коли потрібні мультидокументні транзакції — тримайте їх короткими і з невеликою кількістю документів.
  3. Явно встановлюйте write concern для критичних записів (w:majority + таймаут).
  4. Свідомо обирайте read concern і read preference; не поєднуйте “читання з secondaries” з очікуваннями жорсткої коректності.
  5. Реалізуйте ідемпотентні ключі; проєктуйте під “невідомий результат коміту”.
  6. Моніторьте вибори, лаг реплікації, відмови транзакцій і метрики черг/блокувань.
  7. В шардованих кластерах уникайте крос-шардних транзакцій на гарячих шляхах, якщо любите спокійний сон.

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

1) Чи є транзакції MongoDB “справжніми ACID”?

Всередині транзакції — так: атомарність і ізоляція забезпечуються, а довговічність залежить від write concern і журналювання.
Готова виробнича пастка — ваша топологія кластера і write concern визначають, що означає “довговічно” під час фейловеру.

2) Чи гарантує PostgreSQL відсутність втрати даних?

На одному вузлі з налаштуваннями довговічності і чесним стореджем зафіксовані транзакції переживуть крах.
У реплікованому середовищі політика фейловеру має значення: підняття асинхронної репліки може втратити зафіксовані транзакції.

3) Яка найпоширеніша помилка транзакцій MongoDB?

Припущення, що дефолтний write concern означає виживання записів. Якщо потрібна захищеність при фейловері — явно використовуйте w:majority.

4) Яка найпоширеніша помилка PostgreSQL?

Тримати транзакції відкритими занадто довго (часто “idle in transaction”). Це викликає блоат, утримання блокувань і колапс продуктивності, що виглядає неочевидно — поки не стане очевидним.

5) Чи означає Serializable у PostgreSQL “те саме, що послідовне виконання”?

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

6) Чи може MongoDB надати лінійноізовані читання?

MongoDB підтримує linearizable reads в обмежених сценаріях, зазвичай з primary і конкретними налаштуваннями.
Вони повільніші і більш обмежені; використовуйте їх лише коли дійсно потрібна така гарантія.

7) Чому мультидокументні транзакції MongoDB повільніші за операції одного документа?

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

8) Чому Postgres інколи “зависає” під час змін схеми?

Багато DDL-операцій беруть важкі блокування, що блокують читання/записи.
Використовуйте безпечні патерни міграцій (concurrent indexes, фазові міграції і уникайте довгих блокувань у пікові години).

9) Якщо я використовую MongoDB з w:majority, чи я в безпеці?

Безпечніше, але не магічно безпечно. Ви все одно потребуєте коректної логіки повторів, ідемпотентності та плану на випадок таймаутів і невідомих результатів.
Majority також підвищує чутливість до повільних secondaries і проблем мережі.

10) Якщо я використовую синхронну реплікацію в Postgres, чи я в безпеці?

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

Висновок: наступні кроки, які ви реально можете зробити

Припиніть ставитися до транзакцій як до чекбоксу. Розглядайте їх як контракт, який треба перевіряти під відмовами.
PostgreSQL і MongoDB обидва можуть запускати надійні транзакційні системи, але вони дають різні обіцянки за замовчуванням
і карають за різні види ліні.

  1. Запишіть ваші вимоги до коректності: що може бути застарілим, що може бути втраченим, що має бути унікальним, що має бути атомарним.
  2. Зробіть довговічність явною: режим реплікації та політика фейловеру в Postgres; write concern і read concern в MongoDB.
  3. Відпрацьовуйте відмови: симулюйте stepdown-и, вбивайте клієнта під час коміту і перевіряйте, чи застосунок обробляє неоднозначність без дублювання.
  4. Інструментуйте правду: wait-логи блокувань, відхилення, лаг реплікації, латентність fsync, вибори. Якщо ви не бачите — ви не володієте.
  5. Оберіть базу, що відповідає вашим інваріантам: якщо потрібні реляційні обмеження і коректність між рядками — обирайте Postgres. Якщо модель документна і переважно одно-документна атомарність — MongoDB підходить — просто не прикидайте його Postgres.
← Попередня
Надійні дескриптори SMB у ZFS: чому копіювання/переміщення здається дивним
Наступна →
Токени дизайну для теми документації: CSS-змінні, темний режим «як у X», зменшена анімація

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