MySQL vs SQLite: випадок «безкоштовної швидкості» — коли файлова БД перемагає сервер

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

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

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

Передумова «безкоштовної швидкості»: де SQLite перемагає без зусиль

SQLite — це бібліотека. MySQL — це сервіс. Одна ця фраза пояснює 70% тих скріншотів бенчмарків «SQLite був швидшим»,
які люди махають, мов прапором перемоги.

Якщо ви запускаєте SQLite в процесі, ваш додаток викликає функції бібліотеки, яка читає локальний файл (або кеш сторінок) і повертає рядки. Ніякого TCP.
Ніяких драм з пулом з’єднань. Ніякого планування потоків сервера. Ніякого рукопотискання автентифікації. Ніякого проксі-перехоплення.
Ніякого чекання за іншими клієнтами. Це еквівалент піти до холодильника пішки замість замовлення доставки і сперечатися з домофоном.

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

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

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

Дві ментальні моделі: сервер бази даних проти файлової бази

MySQL: окрема зона відмов, окремий кордон продуктивності

MySQL — це мережевий сервіс зі своїм процесом, пам’яттю, потоками й планувальником вводу/виводу. Ця ізоляція — суперсила:
вона відокремлює роботу бази даних від збоїв додатка, дозволяє багатьом клієнтам підключатися і підтримує складні схеми реплікації
та кластеризації.

Але ця ізоляція коштує. Кожен запит перетинає межу: бібліотека клієнта → сокет → мережевий стек ядра → сервер → движок збереження.
Кожна межа додає накладні витрати і додаткові точки очікування. Під навантаженням черга панує.
Саме тому ваш «простий SELECT по первинному ключу» може перейти від субмілісекунд до десятків мілісекунд без зміни самого запиту.

SQLite: база даних — це файл, сервер — ваш процес

SQLite працює всередині простору вашого процесу. Воно читає й пише один файл бази даних (плюс опціональні журнал/WAL файли),
використовуючи блокування ОС для координації паралелізму. Воно сильно опирається на кеш сторінок ОС і отримує вигоду від локальності.

Ключова деталь: у багатьох навантаженнях дані вже в пам’яті (кеш сторінок). SQLite може влучити в ці сторінки без кругових поїздок.
Коли ви бенчмаркуєте «SELECT 1 рядок», ви вимірюєте виклики функцій і попадання в кеш — не накладні витрати протоколу клієнт‑сервер.

То що ви насправді обираєте?

Ви обираєте, скільки готові платити за спільний доступ, віддалений доступ і багато-писачеву паралельність.
MySQL оптимізований під «багато клієнтів, багато писачів, центральний авторитет».
SQLite оптимізований під «один додаток володіє даними, читання часті, записи координовані».

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

Факти й історія, що змінюють підхід до обох

  • SQLite (2000) був спроектований D. Richard Hipp як вбудована, безсерверна й автономна система, орієнтована на надійність і портативність, а не на розростання фіч.
  • Підхід SQLite «public domain» (фактично відсутність ліцензійних перешкод) допоміг йому з’явитися всюди: у браузерах, телефонах, маршрутизаторах, принтерах і настільних додатках.
  • MySQL (середина 1990‑х) виник у епоху, коли «база даних» означала окремий серверний процес і коли shared hosting вимагав багатокористувацького доступу.
  • InnoDB став двигуном за замовчуванням у MySQL, бо приніс відновлення після краху, транзакції й блокування на рівні рядка у світ, що боявся блокувань таблиць.
  • SQLite додав режим WAL, щоб істотно поліпшити читальну паралельність, відокремивши читачів від писачів — переломний момент для реальних робочих навантажень.
  • SQLite використовує компактний B‑tree дизайн, оптимізований для локального зберігання і передбачуваної продуктивності, що пояснює його поведінку на невеликих пристроях.
  • Реплікація MySQL сформувала сучасні операційні практики (репліки для читання, failover, binlog), але ці переваги приходять з операційною відповідальністю.
  • Тестова культура SQLite відома своєю інтенсивністю: величезне автотестування й агресивне фаззингування зробили його одним із найбільш перевірених інфраструктурних компонентів, від яких ви залежите.

Робочі навантаження: точні випадки, де SQLite перемагає MySQL (і де ні)

SQLite перемагає, коли «база даних» — переважно локальний індекс

Подумайте: runner задач, що відстежує стан завдання; сервіс‑кеш для API‑відповідей; CLI‑інструмент для метаданих; настільний додаток;
edge‑колектор, що буферизує події; або однопроцесний веб‑сервіс з чітким паттерном читань.

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

SQLite перемагає, коли домінують читання, а записи координуються

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

MySQL перемагає, коли потрібна витривала багатописачева паралельність

MySQL з InnoDB створено для паралельних записів. Блокування на рівні рядка, MVCC, фонове змивання і окремі буферні пули
спроектовані під світ «багато писачів». Якщо у системі часті записи з багатьох інстансів і ви не можете їх координувати,
SQLite перетвориться на генератор контенції.

MySQL перемагає, коли потрібен віддалений доступ і спільне володіння

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

SQLite перемагає, коли потрібна простота «доставити з додатком»

Запакувати SQLite — це як запакувати бібліотеку. Запустити MySQL — це запустити екосистему: конфігурації, оновлення, бекапи, моніторинг,
управління користувачами і іноді постморти через «чому воно свопить».

Другий короткий жарт: найпростіша міграція бази даних — це та, яку ви ніколи не робите, тому люди тримають SQLite як стару, але надійну худі.

Бюджет затримки: ваш запит невинний, винна поїздка

У продакшні більшість «поганих» часів відповіді бази даних — це не план запиту. Це все інше:
цикли з’єднань, планування потоків, очікування блокувань, очікування fsync, шумні сусіди і хвостова мережна затримка.

SQLite прибирає мережу та шари планування сервера. Ось і ця безкоштовна швидкість. Якщо ваше навантаження вміщується в ОЗП
(або переважно в кеші ОС), і якщо записи помірні, медіана і хвостова затримка можуть стати значно кращими.

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

Що ви насправді вимірюєте в бенчмарках

Коли хтось публікує бенчмарк «SQLite в 3× швидше за MySQL», запитайте:

  • Вони використовують локальні сокети чи реальну мережу?
  • Чи повторно використовують з’єднання або реконектяться на кожен запит?
  • Чи MySQL fsync‑ить на кожну транзакцію, а SQLite ні (або навпаки)?
  • Чи набір даних у кеші для одного і не в кеші для іншого?
  • Чи вони вимірюють однопотоковість чи реальну конкуренцію?
  • Чи використовують вони режим WAL і розумні налаштування synchronous?

Якщо бенчмарк не відповідає на ці питання — це історія, а не доказ.

Стійкість і семантика падіння: що означає «безпечне» насправді

Перше серйозне питання, яке ставлять SRE: не «наскільки швидко», а «що станеться, якщо хост впаде під час запису».
І MySQL, і SQLite можуть бути надійними. Обидва також можна налаштувати так, щоб отримати проблеми.

Ручки стійкості SQLite: режим журналювання і synchronous

Стійкість SQLite в основному визначається режимом журналювання (DELETE, TRUNCATE, PERSIST, MEMORY, WAL)
і PRAGMA synchronous (OFF, NORMAL, FULL, EXTRA). Режим WAL зазвичай покращує читальну паралельність і пропускну здатність записів
шляхом додавання у WAL файл та пізнішої контрольної точки.

Неприємна істина: багато додатків «бенчмаркують» SQLite з synchronous=OFF, а потім дивуються, коли відключення живлення призводить до корупції.
Це не проблема бази даних — це проблема рішення.

Ручки стійкості MySQL: innodb_flush_log_at_trx_commit та інші

Стійкість MySQL живе в InnoDB: redo‑логи, doublewrite‑буфер, змивання буферного пулу. Відомий перемикач —
innodb_flush_log_at_trx_commit. Встановіть 1 — і ви fsync‑ите при кожному commit (надійно, повільніше).
Встановіть 2 або 0 — і ви міняєте стійкість на пропускну здатність.

Обидві системи дозволяють робити вибір. Головне — робити його свідомо, документувати та тестувати поведінку при відмовах.

Ідея про надійність, яку варто пам’ятати

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

Паралелізм: блокування, MVCC і чому «багато писачів» — це стиль життя

SQLite: один писач одночасно, за задумом

SQLite дозволяє багато читачів і одного писача. Режим WAL поліпшує історію читачів, бо читачі можуть продовжувати читати старий снимок, поки писач додає у WAL.
Але якщо у вас багато одночасних писачів, вони ставлять у чергу.

Результат не в тому, що «все ламається». Результат — стрибки затримки, таймаути busy і інколи ефект «thundering herd»,
коли всі повторюють запити одночасно і лише погіршують становище.

MySQL/InnoDB: створено для паралельних писачів, але це не магія

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

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

Стратегії координації, що роблять SQLite життєздатним

  • Архітектура з одним писачем: направляйте записи через один процес або потік. Читачів може бути багато.
  • Пакетні записи: менше транзакцій, більші коміти (в розумних межах).
  • Використовуйте WAL + busy_timeout: зменшіть хибні відмови при легкій контенції.
  • Тримайте транзакції короткими: «Зроби роботу, потім запиши» краще, ніж «записуй, поки думаєш».

Операційні накладні витрати: прихований податок MySQL (якого у SQLite немає)

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

Операційна модель SQLite така: зробіть бекап файлу; моніторьте диск; перевіряйте, що ви не корумпуєте його; і не дозволяйте десятьом писачам битися у коридорі.
Ця простота коштує грошей.

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

Три корпоративні міні-історії з поля бою

Міні‑історія 1: інцидент через хибне припущення

Продуктова команда зробила невеликий внутрішній сервіс‑дашборд. Він був читальним і в основному повертав кешовані аналітичні результати.
Вони обрали SQLite, щоб не ставити MySQL. Розумно.

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

У перший понеділок після релізу сервіс почав повертати періодичні 500. Логи показували «database is locked».
On‑call зробив те, що роблять on‑call: збільшив кількість повторів. Кількість помилок зросла, бо тепер кожен клієнт повторював одночасно,
фактично перетворивши «один писач» на «чергу з мегафоном».

Виправлення не полягало в відмові від SQLite. Виправлення — взяти його таким, яким воно є: базою з одним писачем. Вони ввели чергу записів
(один фоновий воркер робив транзакції), скоротили обсяг транзакцій, увімкнули WAL і встановили адекватний busy timeout.
Рівень помилок впав до нуля, а затримки нормалізувалися.

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

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

Інша команда використовувала MySQL для зберігання сесій з помірними читаннями/записами. Вони ганялися за p99 і помітили очікування fsync.
Хтось запропонував зменшити стійкість, бо «сесії тимчасові». Вони змінили поведінку InnoDB щодо змивання, щоб зменшити fsync‑тиск.
Затримки покращилися одразу. Зміна була відсвяткована.

Через два тижні хост перезавантажили під час техобслуговування — і частина недавніх записів сесій зникла. Користувачі були викинуті,
кошики зникли, і служба підтримки отримала незаплановане навантаження.

Постмортем був не драматичним. Він був нудним, а це гірше. Команда ненав’язливо переозначила значення «commit».
Вони оптимізували для бенчмарків і забули оптимізувати для досвіду користувача.

Вони відкотили зміну стійкості і полагодили затримки правильно: більші redo‑логи, кращий розмір буферного пулу
і пакетування транзакцій на рівні додатка. MySQL знову став стабільним.

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

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

Невеликий флот edge‑колекторів буферизував телеметрію локально і відправляв батчами. На кожному пристрої використовували SQLite.
Записи були частими, але координованими: один процес ingester писав; аплоадери читали.
Команда зробила те, що болісно неспокусливо: протестувала відключення живлення і ситуації з повним файловим простором.

Під час одного деплою баг дозволив ретраям аплоаду вирости. Пристрої почали заповнювати диски.
Інжест процес почав відмовляти записами з «disk I/O error». Це могло перерости в тиху втрату даних,
бо edge‑флот відмінно вміє тихо падати.

Але в них були два запобіжники: (1) моніторинг вільного дискового простору з жорстким відсіканням, що ставив інжест на паузу до повного виснаження,
і (2) періодична перевірка цілісності SQLite, що виконувалася у вікнах низької активності.

Коли інцидент трапився, пристрої припинили інжест до корупції бази, послали чіткий сигнал здоров’я
і автоматично відновилися після виправлення бага з ретраями. Ніяких героїчних ручних правок файлів бази.
Ніякої загадкової корупції.

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

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

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

Завдання 1: Підтвердити режим журналювання і рівень synchronous SQLite

cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA journal_mode; PRAGMA synchronous;"
wal
2

Значення: Увімкнено режим WAL. synchronous=2 означає FULL (орієнтовано на стійкість).
Залежно від збірки, числові значення відповідають OFF/NORMAL/FULL/EXTRA.

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

Завдання 2: Перевірити контенцію SQLite через busy_timeout і швидкий тест запису

cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA busy_timeout=5000; BEGIN IMMEDIATE; SELECT 'got write lock'; COMMIT;"
got write lock

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

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

Завдання 3: Спостерігати активні шаблони доступу до SQLite (файлові блокування і писачі)

cr0x@server:~$ sudo lsof /var/lib/myapp/app.db | head
myapp     1187 appuser   12u  REG  259,0  52428800  1048577 /var/lib/myapp/app.db
myapp     1187 appuser   13u  REG  259,0   8388608  1048578 /var/lib/myapp/app.db-wal
myapp     1187 appuser   14u  REG  259,0     32768  1048579 /var/lib/myapp/app.db-shm

Значення: Існують і відкриті WAL і SHM файли. Це очікувано в режимі WAL.
Багато процесів, що тримають файл відкритим, може натякати на ризик мульти‑писання.

Рішення: Якщо ви бачите багато різних PID, що відкривають БД для записів — переробіть так, щоб лише один компонент писав.

Завдання 4: Виміряти зростання DB та WAL (тиск чекпоінта)

cr0x@server:~$ ls -lh /var/lib/myapp/app.db /var/lib/myapp/app.db-wal
-rw------- 1 appuser appuser  48M Dec 30 09:41 /var/lib/myapp/app.db
-rw------- 1 appuser appuser 512M Dec 30 09:43 /var/lib/myapp/app.db-wal

Значення: WAL значно більший за головну БД. Можливо, чекпоінт не відбувається (або блокується через довгих читачів).

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

Завдання 5: Примусити й інспектувати результат SQLite checkpoint

cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA wal_checkpoint(TRUNCATE);"
0|0|0

Значення: Три числа — це (busy, log, checkpointed) сторінки. Усі нулі часто означають, що нічого робити або вже чекпоінтовано.
Якщо «busy» ненульове — чекпоінт не пройшов через активних читачів.

Рішення: Якщо busy‑сторінки тримаються — знайдіть і виправте довгоживучих читачів (витоки з’єднань, стримингові запити).

Завдання 6: Запустити швидку перевірку цілісності SQLite (ловити корупцію рано)

cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA quick_check;"
ok

Значення: Структурна цілісність OK. Якщо бачите щось інше — трактуйте це як термінову проблему.

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

Завдання 7: Перевірити опції монтування ФС (семантика fsync важлива)

cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /var/lib/myapp
/dev/nvme0n1p2 ext4 rw,relatime,errors=remount-ro

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

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

Завдання 8: Подивитися, чи диск — ваш реальний вузький горлечко (iostat)

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

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.00    0.00    6.00   18.00    0.00   64.00

Device            r/s     w/s   rMB/s   wMB/s avgrq-sz avgqu-sz await r_await w_await  svctm  %util
nvme0n1         120.0   400.0    3.2    18.5    86.0     5.2  11.8    2.1   14.7   0.6   31.0

Значення: Нетривіальний iowait і вищий write await натякають на fsync/flush‑тиск.

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

Завдання 9: Перевірити стан MySQL сервера й підказки про негайну контенцію

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Questions';"
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Threads_running | 64    |
+-----------------+-------+
+---------------+----------+
| Variable_name | Value    |
+---------------+----------+
| Questions     | 12893412 |
+---------------+----------+

Значення: Багато running потоків може вказувати на насичення CPU, очікування блокувань або thundering herd.

Рішення: Якщо Threads_running високий і затримка велика — перевірте очікування блокувань і повільні запити перед додаванням більшої кількості воркерів.

Завдання 10: Виявити очікування блокувань в InnoDB

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
=====================================
2025-12-30 09:48:12 0x7f2c1c1fe700 INNODB MONITOR OUTPUT
=====================================
...
LATEST DETECTED DEADLOCK
------------------------
...
TRANSACTIONS
------------
Trx id counter 1829341
Purge done for trx's n:o < 1829200 undo n:o < 0 state: running
History list length 1234
...

Значення: Цей вивід показує, чи є deadlock‑и, накопичення history list (undo) або затримки через блокування.

Рішення: Якщо бачите deadlock‑и або великий history list — скоротіть транзакції і додайте індекси, щоб зменшити обсяг блокувань.
Якщо «LATEST DETECTED DEADLOCK» повторюється, виправляйте патерн у застосунку, а не БД.

Завдання 11: Перевірити налаштування стійкості MySQL, що впливає на fsync

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';"
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1   |
+------------------------------+-------+

Значення: Значення 1 означає, що redo‑лог змивається на диск при кожному commit (надійно).

Рішення: Якщо затримка домінує через fsync і ви можете дозволити мінімальні втрати даних — розгляньте 2.
Якщо не можете — тримайте 1 і виправляйте продуктивність інакше (пакетування, схему, сховище).

Завдання 12: Інспектувати churn з’єднань (тихий вбивця латентності MySQL)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Connections'; SHOW GLOBAL STATUS LIKE 'Aborted_connects';"
+---------------+--------+
| Variable_name | Value  |
+---------------+--------+
| Connections   | 904221 |
+---------------+--------+
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Aborted_connects | 1203  |
+------------------+-------+

Значення: Дуже велика кількість Connections відносно стабільного QPS часто означає, що ви підключаєтесь занадто часто.
Aborted_connects натякає на проблеми автентифікації/мережі або ліміти ресурсів.

Рішення: Якщо з’єднання постійно створюються/зникають: виправте пулінг, підвищте таймаути і припиніть «підключатися на кожен запит».
Якщо ви не можете це швидко виправити — модель in‑process SQLite може реально перевершувати вас для того ж навантаження.

Завдання 13: Перевірити, чи MySQL читає з диска чи кешу (тиск на буферний пул)

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

Значення: Buffer pool reads — фізичні читання; read requests — логічні читання. Низьке відношення — добре.
Зростання Innodb_buffer_pool_reads означає, що ви втрачаєте кеш і влучаєте в диск.

Рішення: Якщо ви втрачаєте кеш: збільшіть buffer pool, зменшіть робочий набір, додайте індекси або перемістіть «гарячі» дані в інше місце.
Якщо робочий набір маленький — SQLite з кешем ОС може бути простішим і швидшим.

Завдання 14: Перевірити, де фізично лежить ваш SQLite файл (підводні камені контейнерів і мережевих сховищ)

cr0x@server:~$ df -T /var/lib/myapp/app.db
Filesystem     Type  1K-blocks     Used Available Use% Mounted on
/dev/nvme0n1p2 ext4  205113320 80422344 114123456  42% /

Значення: БД на локальному ext4. Добре. Якщо вона на NFS або якомусь overlay — ваші гарантії блокувань і fsync можуть стати цікавими.

Рішення: Тримайте SQLite на локальному сховищі, якщо ви не дуже добре розумієте семантику вашої мережевої файлової системи і не тестували її під відмовами.

Швидкий план діагностики: що перевірити першим/другим/третім

Коли «база даних повільна», ваша задача — знайти, яка черга наростає. Не починайте з переписування SQL.
Почніть з пошуку кімнати очікування.

Перше: чи це затримка від перетину меж?

  • MySQL: перевірте churn з’єднань, розмір пулу, DNS, накладні TLS, проксі‑хопи.
  • SQLite: перевірте, чи БД на локальному диску і чи ви випадково не поклали її на мережеве сховище або на шалено навантажений том.

Якщо ви робите connect‑per‑request до MySQL — це ваш вузький горлечко, поки не доведете протилежне.
Якщо SQLite лежить на повільному мережевому монтуванні — це ваш вузький горлечко, поки не доведете протилежне.

Друге: чи це контенція блокувань?

  • MySQL: дивіться InnoDB status на предмет очікувань/deadlock; шукайте гарячі рядки і довгі транзакції.
  • SQLite: шукайте «database is locked», довгі читачі, що блокують чекпоінти, і множинних писачів.

Контенція проявляється як стрибкоподібна латентність і таймаути, тоді як CPU може виглядати «нормально». Це класична чергова картина.

Третє: чи це тиск на диск / fsync?

  • Обидві: перевірте iowait, disk await і чи ви змушуєте синхронізацію при кожній транзакції.
  • SQLite: перевірте зростання WAL; поведінку чекпоінта; налаштування synchronous.
  • MySQL: слідкуйте за flush‑поведінкою redo‑лога і пропусками буферного пулу.

Якщо диск повільний — база даних повільна. Дискусій тут немає.

Нарешті: тільки тепер аналізуйте плани запитів

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

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

1) SQLite повертає «database is locked» під навантаженням

Симптом: періодичні відмови або довгі очікування на записах, часто під час піків трафіку.

Корінна причина: одночасні писачі; довгі транзакції; відсутній busy_timeout; неправильна конфігурація WAL; чекпоінти блокуються довгими читачами.

Виправлення: направляйте записи через єдиного писача; увімкніть WAL; встановіть busy_timeout; скорочуйте транзакції; забезпечте швидке закриття читачами; налаштуйте чекпоінтинг.

2) SQLite бенчмарки виглядають чудово, у продакшні дані втрачаються після падіння

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

Корінна причина: небезпечні налаштування стійкості (наприклад, synchronous=OFF), або шар зберігання брешуть про flush.

Виправлення: використовуйте WAL з розумним synchronous; тримайте SQLite на локальному сховищі; тестуйте відключення живлення; впровадьте бекапи і перевірки цілісності.

3) MySQL «повільний», але CPU низький і диски в нормі

Симптом: висока латентність запитів при низькій завантаженості ресурсів.

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

Виправлення: зменшіть churn з’єднань; виправте пулінг; перегляньте InnoDB lock waits і deadlock; скоротіть транзакції; додайте індекси, де потрібно.

4) MySQL швидкий у бенчмарках, потім болісний у продакшні

Симптом: відмінний p50, жахливий p99; періодичні стази.

Корінна причина: промахи буферного пулу, сплески fsync, фонове змивання або відставання реплікації, що викликає backpressure на рівні додатка.

Виправлення: розмір буферного пулу; уникайте гарячих індексів; пакетні записи; моніторьте redo і flush‑поведінку; переконайтеся, що репліки не перевантажені, якщо ви на них покладаєтесь.

5) SQLite в Kubernetes поводиться непередбачувано

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

Корінна причина: файл бази на епhemeral FS контейнера, overlay‑шари або том з несподіваною семантикою блокувань.

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

6) «Просто покладемо SQLite на NFS, щоб всі поди його шарили»

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

Корінна причина: семантика мережевих ФС, поведінка менеджера блокувань і гарантії fsync, що не відповідають припущенням SQLite.

Виправлення: не робіть цього. Якщо потрібен спільний доступ між вузлами — використовуйте серверну БД (MySQL/Postgres) або архітектуру реплік локально‑орієнтовану з явною синхронізацією.

Контрольні списки / поетапний план

Чеклист для рішення: чи підходить ця робоче навантаження для SQLite?

  1. Чи набір даних належить одному додатку? Якщо кілька незалежних сервісів мають писати — обирайте MySQL.
  2. Чи низька або координована конкуренція записів? Якщо так — SQLite залишається в грі.
  3. Чи можна тримати БД на локальному сховищі? Якщо ні — подумайте дуже серйозно; SQLite не любить «сюрпризів» зі сховищем.
  4. Чи робочий набір малий і гарячий? SQLite плюс кеш ОС може бути надзвичайно швидким.
  5. Чи потрібні реплікація, failover і віддалений доступ? Якщо так — MySQL перемагає, хіба що ви самі побудуєте ці шари.
  6. Чи операційна простота — ключова вимога? SQLite дає менше ручок і менше тривог pager.

План налаштування SQLite для продакшну (нудно, правильно, повторювано)

  1. Розмістіть БД на персистентній локальній файловій системі з відомою семантикою (ext4/xfs на реальних дисках).
  2. Увімкніть WAL і встановіть busy timeout під час запуску додатка.
  3. Явно вирішіть рівень стійкості (synchronous) і занотуйте це в рунабуку.
  4. Тримайте транзакції короткими; не тримайте транзакцію під час мережевих викликів.
  5. Впровадьте періодичний PRAGMA quick_check у вікнах низької активності.
  6. Резервуйте з консистентним методом (наприклад, online backup API SQLite або контрольовані снапшоти) і тестуйте відновлення.
  7. Моніторьте зростання WAL і вільне місце на диску; реалізуйте зворотній тиск перед повним диском.
  8. Проєктуйте під патерн «один писач, багато читачів»; додайте чергу записів, якщо потрібно.

План «зупинити кровотечу» для MySQL, коли здається, що воно повільніше, ніж має бути

  1. Перевірте пулінг з’єднань і зменшіть churn; дивіться на приріст Connections.
  2. Перевірте очікування блокувань/deadlock; знайдіть гарячі рядки і довгі транзакції.
  3. Перевірте затримку диска і iowait; fsync‑застої можуть домінувати у p99.
  4. Інспектуйте пропуски в буферному пулі; якщо ви читаєте з диска постійно — ви вже відстаєте.
  5. Тільки потім: налаштовуйте індекси і плани запитів для реальних топ‑запитів.

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

1) Чи «швидший» SQLite за MySQL?

Іноді — так, особливо для навантажень, де накладні витрати клієнт‑серверу переважають усього іншого.
Але MySQL може перевершувати SQLite при інтенсивних паралельних записах і складних багатоклієнтських робочих навантаженнях.

2) Коли SQLite перемагає MySQL у реальному продакшні?

Коли БД локальна, робочий набір гарячий, читань більше, і записи координуються (один писач або низька контенція).
Також коли ви хочете прибрати інфраструктуру і спростити операції.

3) Чи можна використовувати SQLite для веб‑додатка?

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

4) Чи режим WAL завжди правильний вибір для SQLite?

Зазвичай для паралельних читань — так. WAL покращує взаємодію читачів і писачів. Але він вводить питання чекпоінтування і додаткові файли.
Ви мусите моніторити зростання WAL і слідкувати, щоб читачі не тримали знімки вічно.

5) Чи безпечно використовувати SQLite на мережевих ФС?

Ставте «безпечно» як «доведено безпечне для вашої конкретної ФС, опцій монтування і режимів відмов». На практиці мережеві ФС часто створюють проблеми.
Якщо потрібен спільний доступ між вузлами — використовуйте MySQL (або іншу серверну БД) замість того, щоб намагатися зробити SQLite серверною.

6) Як робити бекапи для SQLite?

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

7) Як щодо міграцій: почати зі SQLite, потім перейти на MySQL?

Це допустима стратегія, якщо ви її продумали: тримайте SQL портативним, уникайте SQLite‑специфічних особливостей і побудуйте ранню пайплайн‑міграцію.
Не чекайте, поки все загориться, щоб придумати експорт даних.

8) Чому MySQL інколи має гірший хвостовий час, ніж SQLite?

Бо в нього більше точок чергування: мережа, планування потоків, очікування блокувань, промахи буферного пулу, сплески fsync, ефекти реплікації.
Простіший шлях SQLite може дати кращий p99 — поки не з’явиться контенція записів.

9) Чи можна масштабувати SQLite за допомогою реплік?

Не в сенсі MySQL. Ви можете реплікувати файл або стримити зміни, але тоді ви будуєте розподілену систему.
Якщо вам потрібна проста реплікація і failover — MySQL це зріла опція.

10) Якщо SQLite такий хороший, чому ним не користуються усюди?

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

Наступні кроки, які реально зробити цього тижня

Якщо ви вирішуєте між MySQL і SQLite — або намагаєтеся врятувати систему, що обрала неправильно — зробіть наступне в такому порядку:

  1. Опишіть форму вашого навантаження: співвідношення читань/записів, пікові одночасні писачі, розмір набору даних, вимоги до стійкості, топологія розгортання.
  2. Виміряйте витрати на межі: churn з’єднань і мережна латентність для MySQL; розміщення сховища і контенцію блокувань для SQLite.
  3. Запустіть реалістичний бенчмарк: той самий набір даних, прогрітий і холодний кеш, реальна конкуренція і налаштування стійкості, як у продакшні.
  4. Оберіть найпростіщу архітектуру, що задовольняє вимоги: якщо SQLite підходить — насолоджуйтесь безкоштовною швидкістю і меншою кількістю рухомих частин.
  5. Якщо потрібен MySQL, зобов’яжіться експлуатувати його належним чином: пулінг, моніторинг, бекапи і схема, що шанує конкуренцію.

Мета не в тому, щоб виграти базову дискусію. Мета — випустити систему, яка швидка, бо вона здорова, — і надійна, бо чесно ставиться до відмов.

← Попередня
Сокети як стратегія: чому платформи зараз важливіші за процесори
Наступна →
Ubuntu 24.04: Оновлення системи зламали модулі — правильно відтворіть initramfs (Випадок №88)

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