Мультиорендний хостинг — це місце, де оптимізм отримує аудит. Ви продаєте «необмежену» кількість сайтів, один клієнт ставить плагін, що перетворює кожен перегляд сторінки на повнотекстове сканування, і раптом ваша черга підтримки нагадує звіт про denial-of-service.
Проблема не в тому, що MariaDB або PostgreSQL «повільні». Проблема в тому, що спільні бази даних — це загальне користування, і один орендар може буквально зʼїсти його до кореня — CPU, I/O, блокування, підключення, тимчасовий простір, autovacuum/undo-історія, і так далі. Ваше завдання — зробити так, щоб радіус ураження від поганої поведінки був малим, діагностованим і нудним.
Що справді вбиває всіх у мультиорендних базах
Мультиорендні «відмови» рідко бувають одиничною помилкою. Зазвичай це один орендар, який перевищує спільний ліміт, і система ввічливо колапсує для всіх інших.
Звичайні підозрювані (у порядку появи в продакшні)
- Вичерпання підключень: застосунок масштабується шляхом «відкриття ще одного підключення», а база даних масштабується шляхом «валиться».
- Накрутка CPU: predicat’и без індексів, патологічні JOIN’и, фільтрація JSON без GIN/функціональних індексів і «злочини», породжені ORM.
- Насичення I/O: великі сорти на диск, тимчасові таблиці, шторми контрольних точок, та «давайте експортуємо весь tenant кожну годину».
- Конкуренція блокувань: один орендар виконує великі міграції опівдні; інші чекають, потім повторюють, потім навалюються.
- Технічний борг у обслуговуванні: autovacuum в PostgreSQL не встигає, або InnoDB history/list росте, доки все не почне платити відсотки.
- Події «диск заповнений»: тимчасові файли, binary logs/WAL, неконтрольований ріст таблиць — нічого не робить бази даних чеснішими, ніж файлову систему на 100% заповнену.
У мультиорендному хостингу шаблон «один сайт вбиває всіх» не містить містики. Він передбачуваний. І це добре, бо ви можете інженерно протидіяти йому.
Жарт №1: Хмара — це просто чиїсь інші компʼютери, а мультиорендний хостинг — це чиїсь інші компʼютери з вашою відповідальністю на пейджері.
MariaDB vs PostgreSQL: рішення, яке має значення
Люди порівнюють MariaDB і PostgreSQL як список фіч. У мультиорендному хостингу важливіше питання: яке рішення дає вам більше дієвих важелів для ізоляції орендарів і швидкої діагностики шумного сусіда?
Стислий думка
- Якщо ви можете стандартизуватися на pooler і впровадити таймаути, PostgreSQL зазвичай чистіша платформа для мультиорендності. У нього кращі примітиви для управління запитами та контролю по ролях, а також відмінна історія інтроспекції.
- Якщо ви працюєте в масштабі «класичного LAMP-хостингу» з великою кількістю малих орендарів і сильною операційною памʼяттю MySQL/MariaDB, MariaDB може бути цілком надійною — якщо ви серйозно ставитеся до обмежень ресурсів і припините ілюзії, що «max_connections=5000» — це план.
- Якщо ви не готові впроваджувати огорожі, жодна база даних вас не врятує. База даних — не заміна батьківському нагляду.
Де PostgreSQL зазвичай перемагає щодо ізоляції орендарів
- Першокласна модель ролей для призначення привілеїв; легко зіставити «роль орендаря» з політиками.
- Row Level Security (RLS) дозволяє реалізувати row-per-tenant так, щоб це було захищено на рівні бази даних, а не «ми клянемось, що додаток завжди фільтрує tenant_id».
- Потужна спостереженість: pg_stat_statements, pg_locks, pg_stat_activity, auto_explain та взагалі менше моментів «що воно робить?».
- statement timeouts і lock timeouts прості в застосуванні і можуть бути налаштовані по ролях або по базі даних.
Де MariaDB зазвичай виграє (або принаймні завдає менше болю)
- Операційна знайомість в хостинговому середовищі: багато команд вже мають навколо неї інструменти.
- Персональні ліміти ресурсів існують і прості у застосуванні (MAX_QUERIES_PER_HOUR, MAX_USER_CONNECTIONS і т.д.). Це грубі інструменти, але саме грубі інструменти часто використовують.
- Екосистема реплікації може бути простою для масштабування читання в поширених хостингових патернах (з застереженнями щодо очікувань консистентності).
Чого не робити
Не обирайте базу даних тому, що хтось сказав, що вона «швидша». У мультиорендному хостингу ви не налаштовуєтеся під бенчмарк. Ви налаштовуєтеся під найгіршого клієнта в найгірший час.
Моделі ізоляції: база-на-орендаря, схема-на-орендаря, рядок-на-орендаря
Перш ніж сперечатися MariaDB vs PostgreSQL, вирішіть, що ви ізолюєте: дані, продуктивність, операційний радіус ураження або вимоги відповідності. Ви не отримаєте всі чотири безкоштовно.
Модель A: database-per-tenant (сильна ізоляція, важке обслуговування)
Класичний підхід «кожен клієнт отримує власну базу даних». Він чудово підходить для стримування: надмірності, блокування та міграції одного орендаря менш ймовірно вплинуть на інших.
- Переваги: Чіткий радіус ураження, простіше резервне копіювання/відновлення по орендарю, простіше «перемістити орендаря на інший хост», простіше видалення відповідно до GDPR/retention.
- Недоліки: Більше обʼєктів, більше підключень, більше фонового навантаження, більше поверхні моніторингу. Потрібна автоматизація, інакше ви потонете.
- PostgreSQL: Працює добре, але слід стежити за загальною кількістю підключень і навантаженням autovacuum по багатьох базах.
- MariaDB: Теж працює; schema/database тут тісно повʼязані. Слід стежити за table cache і лімітами відкритих файлів.
Модель B: schema-per-tenant (золота середина, все ще складно)
Одна база, багато схем (або в MariaDB — багато баз як простори імен). Ви отримуєте часткову організацію, але ізоляція продуктивності буде слабкою, якщо не додати огорожі.
- Переваги: Менше серверних обʼєктів, ніж DB-per-tenant, іноді простіше робити крос-орендарські звіти.
- Недоліки: Shared buffer pool, shared WAL/binlog, спільний тимчасовий простір. DDL все ще може завдавати болю.
- Кращий випадок: Помірна кількість орендарів зі схожими навантаженнями і суворою SRE-відповідальністю.
Модель C: row-per-tenant (максимальна щільність, мінімальне прощення)
Одна схема, спільні таблиці, tenant_id в кожному рядку. Це ефективно. Але також саме там один відсутній індекс стає кошмаром спільного хостингу.
- Переваги: Спрощує розгортання змін схеми; менше обʼєктів; підходить для SaaS зі стандартизованою моделлю.
- Недоліки: Ризик noisy neighbor найвищий. Резервні копії/відновлення по орендарю складніші. Потрібна сувора дисципліна запитів.
- PostgreSQL: RLS може зробити це відносно безпечним з точки зору доступу до даних.
- MariaDB: Ви покладаєтеся на правильність додатку для фільтрації орендарів, якщо не впровадите складніші view/procedure патерни (які більшість команд не підтримує коректно з часом).
Перевірка реальності ізоляції
«Ізоляція орендаря» — це не тільки права доступу. Це контроль спільних ресурсів: CPU, I/O, памʼять, блокувань і підключень. Якщо ваша модель не дозволяє вам контролювати це, у вас не ізоляція — у вас надія.
Жорсткі ліміти та огорожі (шар «ти не можеш цього робити»)
Огоржі — це те, що ви встановлюєте заздалегідь. Після того, як орендар розтопить кластер, ви все одно їх додасте — просто під стресом і з клієнтами, що кричать. Зробіть це зараз.
Guardrails PostgreSQL, які дійсно працюють
- statement_timeout по ролі або по базі даних: вбиває runaway-запити.
- lock_timeout: запобігає «чекати вічно» чергам.
- idle_in_transaction_session_timeout: зупиняє підключення, що тримають блокування і нічого не роблять.
- work_mem по ролі: не дає одному орендарю зʼїдати RAM сортуваннями (або змушувати великі тимчасові зливи).
- role-based settings через ALTER ROLE … SET: робить політику примусовою.
Недооцінені guardrails MariaDB
- Персональні ліміти користувачів (MAX_USER_CONNECTIONS, MAX_QUERIES_PER_HOUR): грубі, але ефективні.
- max_execution_time (для SELECT): може вбити довгі читання при обережному використанні.
- Налаштування InnoDB, що зменшують шкоду від сплесків (наприклад, розумні розміри log file, розмір buffer pool, поведінка flush).
Чого жодна база даних нативно не дає
Ні MariaDB, ні PostgreSQL не зможуть ідеально застосувати «орендар отримує 10% CPU і 50 IOPS» на рівні SQL. Якщо вам потрібен такий рівень жорсткої мультиорендності, ви в кінцевому результаті використовуєте:
- Окремі інстанси на орендаря (або на рівні тарифного класу).
- OS-рівневий контроль (cgroups) для обмеження ресурсів процесу бази даних — корисно, але грубо.
- Черги на рівні додатку або pooler: ви не зможете виконати те, що не прийняли.
Шторми підключень: пулінг, ліміти та черги
Найшвидший спосіб, яким один орендар вбиває всіх — це churn підключень. PHP-FPM масштабується, Node воркери форкаються, cron завдання накопичуються, і раптом база витрачає життя на встановлення підключень і контекстні переключення.
PostgreSQL: майже завжди потрібен pooler
PostgreSQL добре обробляє конкуренцію, але тисячі активних backend-процесів — це не безкоштовно. У мультиорендному хостингу зі стрибкоподібним трафіком зазвичай потрібен PgBouncer (transaction pooling для більшості веб-застосунків), з пер-орендними лімітами, якщо можна чисто зіставити користувачів/ролі.
MariaDB: пулінг існує, але будьте чесні щодо того, де він живе
MariaDB може приймати багато підключень, але не плутайте «приймає TCP-підключення» з «працює добре». Часто практична стратегія така:
- Обмежувати підключення на користувача.
- Використовувати пулінг на рівні застосунку (залежно від стеку).
- Тримати max_connections реалістичним і проєктувати backpressure.
Жарт №2: Якщо ваша стратегія масштабування — «збільшити max_connections», у вас не база даних; у вас дуже дорога черга.
Брутальна правда про справедливість
Без явних механізмів справедливості гучний орендар перемагає. База даних не знає, що таке «один клієнт». Вона знає сесії. Вам потрібна відповідність: tenant → role/user → limits. Якщо ви не можете зіставити — ви не зможете це застосувати.
I/O і зберігання: зупиніть диск від ставання спільною трагедією
У мультиорендних системах диски «ламаються» дуже специфічно: не від того, що вони помирають, а від того, що вони зайняті. CPU у порядку, RAM у порядку, а ваша база «повільна», бо чекає зберігання. Тоді всі орендарі таймаутяться і повторюють запити, і ви отримуєте шторм повторів, що робить початкову проблему ще гіршою.
Патерни стримування, які працюють
- Відокремлюйте WAL/binlog від даних, коли можете. Це не магія, але зменшує конкуренцію і робить події «диск повний» менш катастрофічними.
- Пер-орендні датасети/томи (де можливо): при database-per-tenant розміщення кожної бази на окремому датасеті дозволяє застосувати квоти і виявляти ріст.
- Моніторинг тимчасового простору: сорти і тимчасові таблиці — це мовчазні пожирачі диску.
- Тюнінг контрольних точок і flush: і PostgreSQL, і InnoDB можуть генерувати пікові записи; в мультиорендному середовищі ці сплески корелюють з піками навантаження сторінок, і так ви отримуєте «всі сайти повільні».
Висновок зі сторони інженерії зберігання: не діліть без телеметрії
Якщо ви не можете приписати I/O до орендаря (або хоча б до бази даних/користувача), ви будете боротися з примарами. Хоч би мінімально — прив’язуйте до запитів (pg_stat_statements / Performance Schema) і до баз даних.
Спостереження: доведіть, хто шумить, швидко
У хостингу питання «який орендар це зробив?» — це не філософія. Це навичка для on-call. Вам потрібно:
- Топ-запити за загальним часом, середнім часом, кількістю викликів і рядками.
- Графи блокувань: хто блокує кого.
- Кількість підключень по мапуванню орендаря (user/role/db).
- Тренди росту диску/WAL/binlog.
- Затримка техобслуговування: затримки autovacuum, довжина history list InnoDB тощо.
Цитата, яку варто мати на стікері. Від John Allspaw: «You can’t improve what you don’t measure.»
Цікаві факти та історичний контекст (бо це формує сьогоднішні компроміси)
- PostgreSQL походить від проєкту POSTGRES в UC Berkeley, розробленого з думкою про розширюваність задовго до того, як «розширення» стали модними.
- MySQL став дефолтом раннього веб-хостингу, бо його було легко розгорнути і він був достатньо швидким для тогочасних навантажень — багато читань, прості схеми, невеликі дані.
- MariaDB була форкнута від MySQL після того, як Oracle купив Sun; форк був частково про управління, частково — про технології.
- MVCC-модель PostgreSQL робить читання неблокуючими за дизайном, але це створює вимогу до обслуговування: vacuuming мертвих кортежів — не опція.
- InnoDB (домінантний рушій MySQL/MariaDB) приніс транзакції і блокування на рівні рядка в екосистему MySQL і змінив надійність хостингу в 2000-х.
- PgBouncer став де-факто стандартом у середовищах Postgres з високою конкуренцією, бо процес-на-підключення має реальні накладні в масштабі.
- Row Level Security у PostgreSQL стала масовою фічею мультиорендності тільки після того, як команди втомилися від «ми обіцяємо, що додаток завжди додає tenant_id».
- Per-user ліміти MySQL/MariaDB існують давно, але багато хостингових стеків їх ігнорували, бо «це може зламати клієнта», поки клієнт не зламав усіх.
Практичні завдання: команди, виводи, рішення (12+)
Це тип перевірок, які ви виконуєте, коли намагаєтесь утримати мультиорендний хостинг стабільним. Кожне завдання включає: команду, реалістичний вивід, що це означає і яке рішення ви приймаєте.
Завдання 1: Знайти топ PostgreSQL-запитів за загальним часом (pg_stat_statements)
cr0x@server:~$ sudo -u postgres psql -d postgres -c "SELECT queryid, calls, round(total_exec_time)::int AS total_ms, round(mean_exec_time,2) AS mean_ms, left(query,120) AS sample FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;"
queryid | calls | total_ms | mean_ms | sample
----------+-------+----------+---------+----------------------------------------------------------
99120311 | 2140 | 987654 | 461.52 | SELECT * FROM orders WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT $2
77112002 | 120 | 501223 | 4176.86 | SELECT count(*) FROM events WHERE tenant_id = $1 AND payload::text ILIKE $2
55110019 | 98000 | 210112 | 2.14 | SELECT id FROM sessions WHERE expires_at < now()
44110070 | 42 | 180990 | 4309.29 | SELECT * FROM invoices WHERE status IN (...) AND tenant_id=$1 ORDER BY id
99120355 | 300 | 150333 | 501.11 | UPDATE products SET stock = stock - 1 WHERE tenant_id=$1 AND id=$2
(5 rows)
Значення: Другий запит — класичний вбивця орендарів (ILIKE по payload text). Високий середній час, менше викликів, великий вплив.
Рішення: Виявити орендаря через логи/параметри запиту, додати індекс або переробити (наприклад, GIN/trigram, виділена колонка), і встановити per-role statement_timeout, щоб обмежити збитки.
Завдання 2: Побачити сесії PostgreSQL і хто чекає
cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, usename, datname, state, wait_event_type, wait_event, now()-query_start AS age, left(query,80) AS q FROM pg_stat_activity WHERE state <> 'idle' ORDER BY age DESC LIMIT 10;"
pid | usename | datname | state | wait_event_type | wait_event | age | q
------+------------+-----------+--------+-----------------+------------+----------+--------------------------------------------------------------------------------
8123 | tenant_442 | appdb | active | Lock | relation | 00:03:14 | ALTER TABLE posts ADD COLUMN foo text
8201 | tenant_101 | appdb | active | | | 00:02:59 | SELECT * FROM orders WHERE tenant_id=$1 ORDER BY created_at DESC LIMIT $2
8210 | tenant_333 | appdb | active | Lock | tuple | 00:02:10 | UPDATE posts SET status='x' WHERE tenant_id=$1 AND id=$2
(3 rows)
Значення: Орендар виконує DDL і тримає блокування; інші орендарі чекають на relation/tuple locks.
Рішення: Вбити або відкласти DDL, застосувати lock_timeout для ролей орендарів і впровадити політику «вікно обслуговування» для міграцій.
Завдання 3: Виявити блокувальників у PostgreSQL
cr0x@server:~$ sudo -u postgres psql -c "SELECT blocked.pid AS blocked_pid, blocked.usename AS blocked_user, blocker.pid AS blocker_pid, blocker.usename AS blocker_user, left(blocker.query,80) AS blocker_query FROM pg_locks bl JOIN pg_stat_activity blocked ON blocked.pid=bl.pid JOIN pg_locks kl ON bl.locktype=kl.locktype AND bl.database IS NOT DISTINCT FROM kl.database AND bl.relation IS NOT DISTINCT FROM kl.relation AND bl.page IS NOT DISTINCT FROM kl.page AND bl.tuple IS NOT DISTINCT FROM kl.tuple AND bl.virtualxid IS NOT DISTINCT FROM kl.virtualxid AND bl.transactionid IS NOT DISTINCT FROM kl.transactionid AND bl.classid IS NOT DISTINCT FROM kl.classid AND bl.objid IS NOT DISTINCT FROM kl.objid AND bl.objsubid IS NOT DISTINCT FROM kl.objsubid AND kl.granted JOIN pg_stat_activity blocker ON blocker.pid=kl.pid WHERE NOT bl.granted LIMIT 5;"
blocked_pid | blocked_user | blocker_pid | blocker_user | blocker_query
-------------+--------------+-------------+--------------+---------------------------------------------------------------
8210 | tenant_333 | 8123 | tenant_442 | ALTER TABLE posts ADD COLUMN foo text
(1 row)
Значення: Є явний блокувальник. Ніяких вгадувань, ніяких відчуттів.
Рішення: Завершити сесію-блокувальник, якщо вона порушує політику; додати контроль DDL у pipeline деплою.
Завдання 4: Перевірити таймаути PostgreSQL для ролей орендарів
cr0x@server:~$ sudo -u postgres psql -c "SELECT rolname, rolconfig FROM pg_roles WHERE rolname IN ('tenant_333','tenant_442');"
rolname | rolconfig
------------+----------------------------------------------------------
tenant_333 | {statement_timeout=30000,lock_timeout=2000,work_mem=16MB}
tenant_442 | {statement_timeout=30000,lock_timeout=2000,work_mem=16MB}
(2 rows)
Значення: Політика закодована на ролі бази даних, а не в якійсь внутрішній вікі, яку ніхто не читає.
Рішення: Якщо відсутні — додати role defaults; якщо надто жорсткі — створити рівні (small/medium/enterprise).
Завдання 5: Перевірити здоровʼя autovacuum у PostgreSQL
cr0x@server:~$ sudo -u postgres psql -c "SELECT relname, n_dead_tup, last_autovacuum, last_vacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
relname | n_dead_tup | last_autovacuum | last_vacuum
-----------+------------+-------------------------------+-------------------------------
events | 5021132 | |
sessions | 901223 | 2025-12-29 08:11:02.12345+00 |
orders | 200111 | 2025-12-29 08:09:10.99431+00 |
invoices | 150991 | 2025-12-29 07:59:34.33121+00 |
posts | 90112 | 2025-12-29 08:00:20.11200+00 |
(5 rows)
Значення: Таблиця events має мільйони мертвих кортежів і немає останніх autovacuum. Це міна для продуктивності.
Рішення: Вияснити, чому autovacuum не виконується (пороги, ризик wraparound, довгі транзакції). Розглянути індивідуальний tuning autovacuum або партиціювання по орендарю/часу.
Завдання 6: MariaDB: визначити топ-запити (Performance Schema digest)
cr0x@server:~$ sudo mariadb -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1000000000000,2) AS total_s FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 3\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT COUNT ( * ) FROM events WHERE tenant_id = ? AND payload LIKE ?
COUNT_STAR: 118
total_s: 632.11
*************************** 2. row ***************************
DIGEST_TEXT: SELECT * FROM orders WHERE tenant_id = ? ORDER BY created_at DESC LIMIT ?
COUNT_STAR: 9801
total_s: 201.44
*************************** 3. row ***************************
DIGEST_TEXT: UPDATE products SET stock = stock - ? WHERE tenant_id = ? AND id = ?
COUNT_STAR: 3002
total_s: 95.12
Значення: Ви бачите «форми запитів», навіть якщо літеральні значення відрізняються. COUNT з LIKE — хуліган.
Рішення: Додати відповідні індекси, обмежити час виконання або переписати функціонал. Якщо орендар наполягає — перевести його на власну інстанцію.
Завдання 7: MariaDB: знайти активні потоки і хто пожирає ресурси
cr0x@server:~$ sudo mariadb -e "SHOW FULL PROCESSLIST;"
Id User Host db Command Time State Info
9112 tenant442 app1:44210 appdb Query 210 Waiting for table metadata lock ALTER TABLE posts ADD COLUMN foo TEXT
9201 tenant101 app2:51012 appdb Query 45 Sending data SELECT * FROM orders WHERE tenant_id=101 ORDER BY created_at DESC LIMIT 50
9303 tenant333 app3:39910 appdb Query 39 Locked UPDATE posts SET status='x' WHERE tenant_id=333 AND id=9
Значення: Та сама історія, що й у Postgres: DDL викликає очікування metadata lock і накопичення запитів.
Рішення: Завершити DDL, застосувати інструменти онлайн-змін схеми/політику і планувати міграції.
Завдання 8: MariaDB: застосувати обмеження підключень на користувача
cr0x@server:~$ sudo mariadb -e "ALTER USER 'tenant442'@'%' WITH MAX_USER_CONNECTIONS 20;"
cr0x@server:~$ sudo mariadb -e "SHOW GRANTS FOR 'tenant442'@'%';"
Grants for tenant442@%
GRANT USAGE ON *.* TO `tenant442`@`%` WITH MAX_USER_CONNECTIONS 20
Значення: Tenant 442 більше не може відкрити 500 підключень. Вони стоятимуть у черзі на рівні застосунку, а не у вашій базі.
Рішення: Встановити рівні лімітів; моніторити помилки «too many connections» і давати рекомендації щодо пулінгу.
Завдання 9: PostgreSQL: подивитися споживання підключень по базах
cr0x@server:~$ sudo -u postgres psql -c "SELECT datname, count(*) AS conns FROM pg_stat_activity GROUP BY datname ORDER BY conns DESC;"
datname | conns
---------+-------
appdb | 312
postgres| 3
(2 rows)
Значення: Більшість тиску в appdb. Не шокує, але тепер ви знаєте.
Рішення: Якщо appdb — мультиорендний, розглянути переміщення важких орендарів у окремі бази або кластери.
Завдання 10: Перевірити системний I/O wait і головних винуватців
cr0x@server:~$ iostat -xz 1 3
Linux 6.8.0 (db01) 12/29/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.10 0.00 5.20 32.40 0.00 50.30
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
nvme0n1 220.0 18432.0 2.0 0.90 8.20 83.78 600.0 51200.0 120.0 16.67 25.10 85.33 17.3 99.20
Значення: 32% iowait і NVMe майже на 100% завантаженні з високим w_await. Це вузьке місце зберігання прямо зараз.
Рішення: Визначити, чому спорлічі записи — контрольні точки, vacuum, binlog/WAL, тимчасові зливи. Розглянути тюнінг checkpoint/flush і забезпечити запас IOPS.
Завдання 11: PostgreSQL: перевірити симптоми генерації WAL
cr0x@server:~$ sudo -u postgres psql -c "SELECT now(), pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),'0/0')) AS wal_bytes_since_start;"
now | wal_bytes_since_start
------------------------------+------------------------
2025-12-29 08:22:10.1122+00 | 842 GB
(1 row)
Значення: Саме по собі це не «погано», але якщо WAL швидко росте і репліки відстають, ви наближаєтесь до тиску на диск.
Рішення: Перевірити replication lag і архівацію; якщо один орендар пише масово — обмежити його або ізолювати.
Завдання 12: MariaDB: перевірити стан механізму InnoDB для конкуренції і історії
cr0x@server:~$ sudo mariadb -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,70p'
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2025-12-29 08:23:01 0x7f3c6c1fe700 INNODB MONITOR OUTPUT
=====================================
...
History list length 412398
...
Trx id counter 928331120
Purge done for trx's n:o < 928300000 undo n:o < 0 state: running
...
Значення: Велика довжина history list може вказувати на відставання purge, часто через довгі транзакції. Це може погіршити продуктивність для всіх орендарів.
Рішення: Знайти і вбити довгі транзакції; змінити поведінку додатку; забезпечити просування purge.
Завдання 13: PostgreSQL: знайти довгі транзакції (блокувальники vacuum)
cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, usename, now()-xact_start AS xact_age, state, left(query,80) AS q FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_age DESC LIMIT 5;"
pid | usename | xact_age | state | q
------+------------+------------+---------------------+--------------------------------------------------------------------------------
9001 | tenant_777 | 02:14:09 | idle in transaction | SELECT * FROM reports WHERE tenant_id=$1
(1 row)
Значення: «Idle in transaction» — вбивця vacuum і ризик блокувань. У мультиорендному середовищі це спільний податок.
Рішення: Застосувати idle_in_transaction_session_timeout для ролей орендарів; виправити шлях у додатку, який забуває commit/rollback.
Завдання 14: Перевірити використання диску і знайти швидко зростаючі області
cr0x@server:~$ df -h /var/lib/postgresql /var/lib/mysql
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 900G 812G 43G 95% /var/lib/postgresql
/dev/nvme1n1p1 900G 620G 235G 73% /var/lib/mysql
Значення: Тому Postgres на 95%. Ви в одному сплеску WAL або тимчасових файлів від дуже пізнавального інциденту.
Рішення: Надзвичайне прибирання (перевірка збереження WAL/архівації, пошук тимчасових файлів), потім постійні квоти/оповіщення. І ще: не дозволяйте кластеру працювати на 95% постійно.
Швидкий план діагностики
Це послідовність «хтось щойно пейджив вас, і всі орендарі кричать». Мета не елегантність. Мета — знайти вузьке місце за хвилини.
По-перше: вирішіть, чи це CPU, I/O, блокування чи підключення
- Завантаження CPU? Перевірте системне навантаження і CPU user/system.
- Якщо CPU високий, а iowait низький: підозрюйте погані запити, відсутні індекси, дорогі сорти/зʼєднання.
- Висока iowait? Якщо iowait високий і %util диска близький до 100%: підозрюйте шторми checkpoint/flush, тимчасові зливи, churn WAL/binlog, відставання vacuum/purge.
- Накупчення блокувань? Шукайте сесії, що чекають на блокування (pg_stat_activity wait_event_type=Lock; MariaDB processlist «Locked» / metadata locks).
- Вичерпання підключень? Перевірте активні підключення проти max; шукати помилки «too many connections» і вибухи потоків/процесів.
По-друге: ідентифікуйте головного винуватця і радіус ураження
- Топ-запити (pg_stat_statements / Performance Schema digests): знайдіть форму запиту, що споживає найбільше часу.
- Зіставте з орендарем: через роль/користувача, базу даних, теги застосунку чи кореляцію логів.
- Підтвердіть вплив: чи викликає це блокування, I/O churn або навантаження CPU?
По-третє: застосуйте найменш погане помʼякшення
- Вбити запит/сесію, якщо він активно шкодить іншим і порушує політику.
- Тротлінг на краю: обмежити підключення на орендаря; зменшити конкурентність у pooler; rate-limit важкі ендпоїнти.
- Тимчасово підвищити справедливість: знизити statement timeouts для гучної ролі орендаря, а не глобально.
- Стабілізувати зберігання: якщо диск — вузьке місце, зменшити тиск на запис (відкласти vacuum full/reindex, обережно тюнити checkpoints, зупинити масові завдання).
Якщо ви послідовно виконуєте ці кроки, «мультиорендний мелтдаун» стає повторюваною діагностикою замість колективного спілкування з духами.
Поширені помилки: симптом → корінь → виправлення
1) Симптом: «Усі сайти повільні» і CPU бази в нормі
Корінь: Насичення зберігання (тимчасові зливи, сплески контрольних точок, backlog WAL/binlog).
Виправлення: Перевірити iowait і %util диска; обмежити work_mem/розміри сортувань; тюнити checkpoints; відокремити WAL/binlog; додати запас IOPS.
2) Симптом: випадкові таймаути, багато «waiting for lock»
Корінь: DDL орендаря або довгі транзакції, що викликають черги блокувань.
Виправлення: Застосувати lock_timeout і idle_in_transaction timeout; вимагати online schema change інструменти; планувати міграції; швидко вбивати блокувальників.
3) Симптом: Періодичні «too many connections» і потім відновлення
Корінь: Шторми підключень від масштабування застосунку або cron; немає пулінгу/backpressure.
Виправлення: Розгорнути PgBouncer (Postgres) або пулінг у застосунку; обмежити підключення на орендаря (MariaDB limits, Postgres role-based pool limits); зменшити кількість воркерів.
4) Симптом: Запити Postgres повільнішають за дні, потім «vacuum не встигає»
Корінь: Autovacuum відстає через довгі транзакції, погані пороги або таблиці з високою змінністю (часто спільні мультиорендні таблиці).
Виправлення: Знайти довгі транзакції; налаштувати autovacuum по таблиці; партиціонувати таблиці з високою змінністю; забезпечити ресурсами vacuum; зменшити фактори здуття.
5) Симптом: Продуктивність MariaDB погіршується при великій кількості записів; проблеми undo/purge
Корінь: Довгі транзакції і відставання purge (history list length росте).
Виправлення: Ідентифікувати довгі транзакції; скоротити обсяги транзакцій; вбивати порушників; забезпечити прогрес purge; переглянути isolation levels і розміри батчів.
6) Симптом: «Це тільки звіт одного орендаря», але він ламає всіх
Корінь: Важкий аналітичний запит на primary, немає сегрегації ресурсів, нема репліки для читання, нема таймауту.
Виправлення: Маршрутизувати звіти на репліки/аналітичне сховище; встановити statement_timeout; створити тариф для орендаря з виділеними ресурсами для звітності.
7) Симптом: Диск заповнений, база аварійно завершується або переходить у read-only
Корінь: Ріст WAL/binlog, неконтрольовані тимчасові файли або необмежені дані орендарів.
Виправлення: Застосувати квоти (де можливо), моніторити ріст, дотримуватися політики вільного простору (не опція), і обмежити зберігання. Ставтеся до disk-full як до проєктної помилки, а не як до сюрпризу.
Три міні-історії з практики
Міні-історія 1: Інцидент через неправильне припущення
Вони запускали спільний PostgreSQL-кластер для флоту невеликих клієнтських сайтів. Кожен орендар отримав свого користувача бази даних, але всі орендарі використовували ту саму базу і схему. Продуктова команда наполягала, що додаток завжди фільтрує по tenant_id. SRE-команда в це повірила, бо логи виглядали чистими і всі хотіли закінчити спринт.
Новий клієнт підʼєднав «аналітичний плагін», який додав пошук по історичних подіях. Плагін використовував динамічний билд запитів, і в одному шляхові коду він іноді забував додати tenant_id у WHERE. Не завжди. Лише для певних комбінацій фільтрів. Тому це було складно відтворити в стейджингу; у стейджингу ніколи не було достатньо даних, щоб нашкодити.
У продакшні запит перетворився з «сканувати кілька тисяч рядків для одного орендаря» на «сканувати сотні мільйонів рядків для всіх орендарів». PostgreSQL зробив те, що йому наказали. CPU піднявся, потім I/O підскочив, бо план почав скидати сорти на диск. Тим часом запити інших орендарів накопичувалися і таймаутились, а логіка повторів лише збільшувала навантаження. Плагін не лише створив ризик витоку даних — він спричинив інцидент продуктивності для всього хостингу.
Виправлення не було героїчним. Вони впровадили Row Level Security, змусили всі запити орендарів виконуватися через SECURITY DEFINER-функцію з контекстом орендаря і додали statement_timeout для ролей орендарів. Більша зміна була культурною: «додаток завжди фільтрує» перестало бути архітектурним рішенням, це стало недовіреним припущенням.
Міні-історія 2: Оптимізація, що відбилася боком
Платформа хостингу стандартизувалася на MariaDB. Одного кварталу хтось вирішив «оптимізувати» шляхом значного збільшення max_connections, бо клієнти іноді бачили помилки підключень під час сплесків трафіку. Міркування були простими: менше помилок — щасливіші клієнти.
Приблизно два дні це виглядало як успіх. Потім маркетингова кампанія вдарила по сайту одного орендаря. PHP-воркери наростилися. Кожен воркер відкривав власне підключення, робив повільний запит і сидів, чекаючи I/O. Більше підключень означало більше одночасної повільної роботи. Буфер пул почав інтенсивно перемішуватися. InnoDB почав сильніше флашити. Затримки збільшилися по всій системі. Інші орендарі повторювали, що створило ще більше підключень і ще більше флашів. Класичний самостворений шторм.
Коли вони зменшили max_connections назад, симптоми покращали, але команда винесла неправильний урок: «max_connections треба налаштувати». Насправді урок був «потрібен backpressure і пулінг». Вони додали пер-юзер ліміти підключень, виправили проблемний запит індексом і впровадили невеликий пул підключень на рівні застосунку для свого керованого стеку.
Постмортем був сухим, але точним: дозволяти більше конкурентності не зменшує попит; воно збільшує здатність системи терпіти більше попиту одразу.
Міні-історія 3: Сумно, але правильна практика, що врятувала день
Інша компанія запускала PostgreSQL для мультиорендного SaaS. Вони не були витончені. Просто мали політики: per-role timeouts, PgBouncer і суворі вікна міграцій. Кожна роль орендаря мала statement_timeout і lock_timeout за замовчуванням, а продакшн-міграції виконувалися окремою «deploy» роллю з підвищеними правами і в певне вікно змін.
Одної п’ятниці орендар викотив нову версію інтеграції, яка випадково виконала великий UPDATE в одному транзакції, зачепивши «гарячу» таблицю. Запит був валідний. Він також був катастрофічним в той час доби. statement_timeout вбив його автоматично. Додаток кілька разів повторив і потім відступив, бо логіка ретраїв була адекватною. База залишилася жива. Інші орендарі майже не помітили.
On-call інженер побачив помилки, швидко ідентифікував орендаря по роль-логах і працював з підтримкою над виправленням. Ніхто не був викликаний на all-hands. Ніхто не «героїчно масштабував» кластер. Система поводилася як система з огорожами.
Це не було гламурно. Ось у чому суть. Найкраща SRE-робота виглядає так, ніби нічого не сталося.
Чеклісти / покроковий план
Покроково: захистити спільну базу від шумних орендарів
- Виберіть модель ізоляції
- Якщо орендарі можуть запускати непередбачувані плагіни/SQL: віддайте перевагу database-per-tenant або instance-per-tier.
- Якщо ви контролюєте всі запити і хочете щільність: row-per-tenant з PostgreSQL RLS працює.
- Забезпечте відображення ідентичності
- Створіть окрему DB роль/користувача для кожного орендаря (або для рівнів тарифів), щоб можна було застосовувати ліміти і приписувати поведінку.
- Встановіть таймаути запитів за замовчуванням
- PostgreSQL: statement_timeout, lock_timeout, idle_in_transaction_session_timeout.
- MariaDB: використовуйте пер-юзер ліміти; розгляньте max_execution_time для SELECT там, де безпечно.
- Реалізуйте контроль підключень
- PostgreSQL: PgBouncer, і налаштування pool size; уникайте тисяч бекендів.
- MariaDB: встановлюйте MAX_USER_CONNECTIONS і тримайте max_connections реалістичним.
- Введіть правило «без DDL опівдні»
- Вимагайте міграцій у вікні; віддавайте перевагу online schema change практикам.
- Зробіть зберігання «нудним»
- Оповіщайте про використання диску рано (наприклад, 70/80/90%).
- Відокремлюйте WAL/binlog, якщо можливо; моніторте тимчасове використання.
- Мінімум спостережуваності
- Топ форми запитів, очікування блокувань, підрахунок сесій по орендарях і здоровʼя техобслуговування (vacuum/purge).
- Створіть шляхи ескалації
- Коли орендар неодноразово викликає інциденти — переведіть його на вищий тариф з виділеними ресурсами або застосуйте жорсткіші ліміти. Не сперечайтеся з фізикою.
Чекліст: при додаванні нового орендаря
- Створити роль/користувача орендаря з відповідними дефолтами (таймаути, work_mem тощо).
- Встановити кап підключень і підтвердити поведінку пулінгу.
- Підтвердити процес резервного копіювання/відновлення для обраної моделі орендаря.
- Увімкнути логування/атрибуцію запитів, щоб ідентифікувати орендаря під час інцидентів.
- Перевірити, що «небезпечні» ендпоїнти (експорти, звіти, масові оновлення) обмежені або винесені.
Чекліст: коли орендар просить «один великий звіт»
- Чи можна виконати його на репліці або в окремому аналітичному сховищі?
- Чи є override таймауту лише для ролі звітності?
- Чи проіндексовано запит і протестовано на даних, подібних до продакшн?
- Чи обмежена конкурентність (один звіт одночасно на орендаря)?
FAQ
1) Використовувати одну базу для всіх орендарів чи одну базу на орендаря?
Якщо орендарі можуть запускати непередбачувані навантаження (плагіни, кастомні звіти, міграції), database-per-tenant зменшує радіус ураження. Якщо ви контролюєте запити жорстко і потрібна щільність — спільні таблиці можуть працювати, але лише з суворими огорожами.
2) Чи PostgreSQL «кращий» для мультиорендності?
PostgreSQL загалом дає сильнішу управлінську базу на рівні СУБД (RLS, per-role налаштування, інтроспекція). Це не замінює хорошу поведінку додатку, але зменшує кількість випадків, коли вам доводиться йому довіряти.
3) Чи MariaDB може забезпечити справедливість між орендарями?
MariaDB може обмежувати підключення на користувача і деякі види активності на користувача. Це не повний планувальник справедливості, але вистачає, щоб зупинити найгіршу поведінку «відкрий 500 підключень» і виграти стабільність.
4) Який найефективніший захист від noisy neighbors?
Контроль підключень плюс таймаути. Обмежте конкурентність на орендаря і вбивайте runaway-роботу. Все інше (індексування, тюнінг, партиціювання) важливе, але ці два механізми зупиняють кровотечу.
5) Чи вирішує Row Level Security у PostgreSQL проблему мультиорендності?
Він вирішує питання доступу до даних. Він не вирішує ізоляцію продуктивності сам по собі. Орендар все ще може виконати дорогий запит, який дозволений, але руйнівний.
6) Чи варто відправляти важких орендарів на репліки?
Розміщуйте читові аналітичні й звітні навантаження на репліках, якщо вимоги консистентності це дозволяють. Але слідкуйте за replication lag і переконайтеся, що додаток не тихо переключається на primary при збільшенні lag.
7) Як заборонити орендарям робити зміни схеми у пікові години?
Не розраховуйте на ввічливість. Застосовуйте обмеження: прибирайте привілеї DDL для ролей орендарів, маршрутизовуйте міграції через контрольовану роль deploy, і використовуйте lock timeouts, щоб запобігти накопиченню.
8) Який найшвидший спосіб знайти орендаря, що спричинив проблеми?
Використовуйте пер-орендні користувачі/ролі бази даних і атрибуцію запитів. Без цього мапінгу ви будете вгадувати з IP та логів застосунку, поки кластер горить.
9) Як вирішити, коли розділити орендарів на окремі інстанси?
Розділяйте, коли навантаження орендаря фундаментально відрізняється (важкі записи, великі дані, постійні звіти) або коли він регулярно викликає інциденти. Ізоляція дешевша за повторні відмови.
10) Чи корисні OS-рівневі контролі (cgroups) для баз даних?
Вони корисні як грубий safety net, особливо в контейнеризованих розгортаннях. Але це не заміна управлінню на рівні запитів і підключень, бо вони не розуміють орендарів чи SQL.
Наступні кроки, які ви можете зробити цього тижня
- Привʼяжіть ідентичність орендаря до ідентичності бази: одна роль/користувач на орендаря або на тариф. Якщо цього не можна зробити — ваше реагування на інциденти залишиться дорогим.
- Встановіть таймаути:
- PostgreSQL: statement_timeout, lock_timeout, idle_in_transaction_session_timeout по ролі орендаря.
- MariaDB: пер-юзер ліміти підключень і контролі виконання там, де це безпечно.
- Впровадьте poolинг/backpressure: PgBouncer для Postgres; розумні ліміти воркерів і стратегія пулінгу для стеків MariaDB.
- Увімкніть потрібну спостережуваність: pg_stat_statements або Performance Schema digest підсумки, плюс дашборди блокувань/сесій.
- Напишіть політику «коли виселяти орендаря»: не як покарання, а як інженерне рішення. Якщо один орендар потребує виділених ресурсів — дайте йому тариф, що відповідає реальності.
Якщо вам потрібен один керівний принцип: проєктуйте так, щоб найгірший орендар не міг зробити вашу компанію заручником. Бази даних — потужні. Вони не ведуть переговорів.