MySQL vs SQLite: наскільки далеко SQLite може зайти, перш ніж зіпсує ваш сайт

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

Усе добре, доки раптом не стає погано. Ваш сайт запускають на SQLite — він швидкий, деплой простий, і база даних — це буквально файл. Потім трафік зростає, з’являються фонові задачі, додається аналітика, і раптом ви дивитеся на database is locked, ніби це риса характеру.

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

Коротко: вибирайте інструмент свідомо

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

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

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

Що ви насправді обираєте: архітектуру, а не синтаксис

SQLite — це бібліотека, яка пише файл; MySQL — це сервіс, що говорить по мережі

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

MySQL виконується поза процесом. Він володіє файлами даних, buffer pool, redo/undo логами, фоновими потоками та реплікацією. Ваш застосунок підключається по TCP (або локальному сокету), і MySQL арбітрує конкуренцію між багатьма клієнтами.

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

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

Але в момент, коли у вас є кілька писачів з кількох процесів (або хостів), ви перейшли від «вибір бази даних» до «розподілених блокувань і семантики відмов». SQLite все ще може працювати, але ви маєте поважати його модель конкуренції і фізику підлеглого сховища.

Принцип надійності, що варто прикріпити на стіну

Вернер Фогельс (Werner Vogels) у світі надійності AWS має фразу, яку часто перефразовують: Все постійно ламається; проектуйте так, щоб система продовжувала працювати (парафразована ідея, Werner Vogels). SQLite і MySQL обидва ламаються. Просто по-різному. Обирайте відмову, яку ви зможете пережити.

Цікаві факти та історія для нарад

  • SQLite створили в 2000 році — D. Richard Hipp для контракту ВМС США — мета була вбудована надійність, а не веб-масштабна слава.
  • SQLite відоме як «безсерверне» — не «cloud serverless», а «немає процесу-сервера». Це бібліотека.
  • SQLite є у величезній кількості пристроїв: телефони, браузери, десктопні додатки, інформаційно-розважальні системи. Такий розгортальний тиск робить його консервативним і стабільним.
  • WAL режим з’явився у 2010 році і значно змінив історію конкуренції SQLite для читано-зосереджених навантажень.
  • Бази SQLite — це одиночні файли (плюс опціональні WAL та shm файли), що полегшує доставку та знімки — але також робить «поставити на NFS» постійною трагедією.
  • InnoDB став дефолтним в MySQL починаючи з MySQL 5.5, і це змінило правила гри: транзакції, відновлення після падіння, блокування на рівні рядка і розумні дефолти.
  • Реплікація MySQL була ключовим патерном десятиліттями — не ідеальна, проте знайома величезній екосистемі (і більшість інструментів очікують її).
  • SQLite має сувору культуру тестування: величезне покриття тестами, fuzzing і очікування довгострокової стабільності, бо його вбудовують всюди.
  • MySQL має історію «пасток» навколо недетермінованих дефолтів і дрейфу конфігурацій — сьогодні не так часто, але фольклор не з’явився без причин.

Наскільки далеко може зайти SQLite (далі, ніж ваша команда думає)

SQLite надзвичайно швидкий, якщо навантаження підходить

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

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

Де SQLite — відмінний дефолт

  • Розгортання на одній VM / одному контейнері з одним основним процесом застосунка (або невеликою кількістю процесів) і контрольованими записами.
  • Читано-важкі навантаження з періодичними пакетними записами, особливо з увімкненим WAL.
  • Вбудовані, десктопні, edge та offline-first додатки, де запуск MySQL — абсурдна складність.
  • Внутрішні інструменти, де потрібен максимальний принцип «просто працює» і мінімум звернень до DBA.
  • Прототипи, які можуть стати реальними, за умови дисципліни міграцій і бекапів з самого початку.

SQLite може витримувати реальний трафік — якщо ви обмежите записи

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

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

Жарт №1: SQLite — як дуже компетентний бібліотекар: тихий, швидкий, організований. Але він все одно штампу єдиний квиток видачі за раз.

Де SQLite ламається: режими відмов, які боляче б’ють у проді

1) Конкуренція: блокування запису — заголовна проблема

SQLite дозволяє кільком читачам працювати одночасно, але конкуренція записів обмежена. У rollback journal режимі писач блокує читачів під час commit. У WAL режимі читачі не блокують писачів і навпаки — але фактично залишається лише один писач одночасно.

Класичний симптом — запити падають або зависають при спалахах записів. Ви побачите SQLITE_BUSY, «database is locked» або збільшену латентність у вигляді випадкових стрибків. Це не випадковість. Це контенція.

Деякі команди намагаються «вирішити» це довшими таймаутами busy. Це може зменшити кількість помилок, але також перетворити невелику проблему контенції в інцидент з загальносайтовою латентністю. Вітаємо, ви поміняли помилку 500 на 30-секундне завантаження сторінки.

2) Семантика сховища: локальний диск проти мережевого файлового сховища

SQLite покладається на файлові блокування і передбачувану поведінку файлової системи. На локальній файловій системі (ext4, xfs, APFS, NTFS) це здебільшого добре. На мережевій файловій системі це може перетворитися на перфомансний суп або ризик коректності залежно від семантики блокувань і кешування.

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

3) Операційні відмови: бекапи «копією файлу» (поки не стануть не такими)

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

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

4) Зміни схеми: маленькі додатки стають великими, і ALTER TABLE стає політичним

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

5) Наблюваність: менше регуляторів, менше лічильників, більше здогадок

У MySQL ви отримуєте процес з метриками: buffer pool hit rate, redo log pressure, lock waits, replication delay, slow query logs, performance schema. У SQLite часто інструментуйте з боку застосунка. Можна робити це добре, але треба робити це навмисно.

6) «Просто додамо ще один сервер додатка» працює інакше

SQLite добре масштабується вгору на одному вузлі: швидший CPU, швидший SSD, більше RAM, налаштовані PRAGMA, кращі запити. За замовчуванням він не масштабується вшир. Якщо ваш наступний крок — додати вузли за балансувальником навантаження, MySQL (або інша серверна база) стає прямим вибором.

Що дає MySQL (і якою ціною)

MySQL дає вам: передбачувану конкуренцію між клієнтами

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

MySQL дає: реплікацію і відновлення як базову операційну вміння

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

MySQL коштує: операційний оверхед і гострі кути

Тепер ви керуєте станним сервісом із конфігурацією, оновленнями, безпекою, моніторингом, розміткою дисків і плануванням потужності. Потрібні пул з’єднань. Потрібна перевірка бекапів. Потрібно думати про зміни схеми і довготривалі транзакції. Ви приймаєте систему, яка може добре вам служити, але вона вимагатиме компетентності.

Жарт №2: MySQL — як взяти собаку: вірна, здатна і захисна. Але ви тепер відповідаєте за її дієту, настрій і іноді гавкіт опівночі.

Швидкий плейбук діагностики: знайдіть вузьке місце швидко

Це чекліст «я на чергуванні й сайт повільний». Найшвидший спосіб втратити час — філософські дебати про бази даних, поки ваш p95 кричить.

Перш за все: вирішіть, чи у вас контенція, I/O чи патологія запитів

  1. Перевірте патерни помилок: чи бачите ви SQLITE_BUSY / «database locked» або lock waits у MySQL?
  2. Перевірте форму латентності: постійно повільно (I/O bound) vs різкі затримки (контенція) vs «деякі кінцеві точки жахливі» (проблема запиту/індексу).
  3. Перевірте швидкість записів: чи додав деплой більше записів, фонових задач, аналітичних подій, записів сесій або міграцій?

Друге: ідентифікуйте, куди йде час

  1. Часування на рівні застосунку: час у DB викликах проти часу в інших місцях (рендеринг, зовнішні API).
  2. Сигнали на рівні БД:
    • SQLite: поведінка WAL/блокувань, довгі транзакції, гарячі таблиці, vacuum/auto_vacuum проблеми.
    • MySQL: slow query log, InnoDB status, lock waits, buffer pool pressure, replication lag.
  3. Рівень хоста I/O: чи диск насичений або файлова система під стресом?

Третє: оберіть найменш ризикове пом’якшення

  • Якщо SQLite заблоковано: зменште конкуренцію записів, скоротіть транзакції, увімкніть WAL (якщо безпечно), пакетну обробку записів, перенесіть записи на одного воркера.
  • Якщо MySQL має блокування: додайте/підкоригуйте індекси, зменшіть область транзакцій, вбивайте runaway-запити, змінюйте ізоляцію там, де доречно.
  • Якщо I/O завантажений: додайте RAM (для кешу), перейдіть на швидший SSD/NVMe, зменшіть fsync тиск, налаштуйте redo логи MySQL, обережно налаштуйте synchronous у SQLite.

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

Це реальні завдання, які можна виконати на Linux-хостах і поширених MySQL/SQLite деплойментах. Кожне включає, що означає вивід і яке рішення робити далі.

Task 1: Підтвердіть, де знаходиться файл SQLite і яку файлову систему він використовує

cr0x@server:~$ df -T /var/www/app/db/app.sqlite3
Filesystem     Type  1K-blocks      Used Available Use% Mounted on
/dev/nvme0n1p2 ext4  192152472  81324512 101245120  45% /

Значення: Файл на локальному ext4 — добра новина. Якщо ви бачите nfs або FUSE-оверлей з дивною семантикою, ставте підозру на блокування і латентність за замовчуванням.

Рішення: Якщо на NFS і у вас кілька писачів, зупиніться і переробіть: перейдіть на MySQL/Postgres або забезпечте патерн single-writer.

Task 2: Перевірте супутні файли SQLite (WAL/shm) і зростання розміру

cr0x@server:~$ ls -lh /var/www/app/db/
total 2.3G
-rw-r----- 1 www-data www-data 1.7G Dec 30 10:12 app.sqlite3
-rw-r----- 1 www-data www-data 512M Dec 30 10:12 app.sqlite3-wal
-rw-r----- 1 www-data www-data  32K Dec 30 10:12 app.sqlite3-shm

Значення: WAL режим активний (є файл -wal). Великий файл WAL може означати, що контрольні точки не виконуються, у додатку є довго живі читачі або неправильно налаштоване чекпоінтинг.

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

Task 3: Перевірте режим журналу SQLite і налаштування synchronous

cr0x@server:~$ sqlite3 /var/www/app/db/app.sqlite3 'PRAGMA journal_mode; PRAGMA synchronous;'
wal
2

Значення: wal увімкнено. synchronous=2 означає FULL. Це безпечніше, але може збільшувати fsync-латентність.

Рішення: Якщо ви I/O bound і можете терпіти ризик міцності, можна розглянути synchronous=NORMAL — але робіть це лише з чіткою моделлю відмов і протестованим відновленням.

Task 4: Визначте події «database is locked» в логах застосунка

cr0x@server:~$ grep -R "database is locked" -n /var/log/app/ | tail -n 5
/var/log/app/app.log:44182 sqlite error: database is locked (SQLITE_BUSY)
/var/log/app/app.log:44190 sqlite error: database is locked (SQLITE_BUSY)
/var/log/app/app.log:44201 sqlite error: database is locked (SQLITE_BUSY)
/var/log/app/app.log:44222 sqlite error: database is locked (SQLITE_BUSY)
/var/log/app/app.log:44228 sqlite error: database is locked (SQLITE_BUSY)

Значення: Це не «випадкові глюки». Це тиск конкуренції або довгі транзакції.

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

Task 5: Знайдіть довго працюючі транзакції, що стримують SQLite (через інспекцію процесів застосунка)

cr0x@server:~$ ps -eo pid,etimes,cmd | grep -E "gunicorn|uwsgi|node|python" | head
 2143   8123 /usr/bin/python3 /var/www/app/worker.py
 2190   4201 /usr/bin/python3 /var/www/app/web.py
 2211    233 /usr/bin/python3 /var/www/app/web.py

Значення: Довго живі воркери часто тримають відкриті DB-з’єднання і можуть ненавмисно утримувати транзакції читання.

Рішення: Аудитуйте область дії з’єднань/транзакцій. Переконайтеся, що кожен запит/завдання використовує короткі транзакції і закриває курсори негайно. Якщо ваш ORM відкриває неявні транзакції, вмикайте autocommit там, де безпечно.

Task 6: Перевірте латентність диска і насичення під час сплесків

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

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           6.12    0.00    2.01    8.42    0.00   83.45

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         85.0   2048.0     0.0    0.0    3.20    24.10   210.0   8192.0     0.0    0.0   18.70    39.01    4.12   92.5

Значення: %util близько 90% і w_await ~19ms свідчать, що диск — вузьке місце для записів. SQLite з FULL synchronous це відчує.

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

Task 7: Перевірте поведінку відкритих файлових дескрипторів (корисно при NFS)

cr0x@server:~$ lsof /var/www/app/db/app.sqlite3 | head
COMMAND  PID     USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
python3  2190 www-data    7u   REG  259,2 1825368064 393226 /var/www/app/db/app.sqlite3
python3  2211 www-data    7u   REG  259,2 1825368064 393226 /var/www/app/db/app.sqlite3

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

Рішення: Якщо це мульти-хостовий доступ — зупиніться. Централізуйте записи або мігруйте на серверну БД.

Task 8: Перевірте цілісність SQLite після крашу або підозрілої I/O події

cr0x@server:~$ sqlite3 /var/www/app/db/app.sqlite3 'PRAGMA integrity_check;'
ok

Значення: «ok» означає, що внутрішні структури консистентні.

Рішення: Якщо не ok — відновлюйте з бекапу негайно і розслідуйте підлегле сховище та краш процесів.

Task 9: Подивіться план запиту SQLite для відомого повільного запиту

cr0x@server:~$ sqlite3 /var/www/app/db/app.sqlite3 "EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id=42 ORDER BY created_at DESC LIMIT 20;"
QUERY PLAN
`--SCAN orders
    `--USE TEMP B-TREE FOR ORDER BY

Значення: Він сканує таблицю і сортує тимчасовим b-tree. Це дорого.

Рішення: Додайте індекс типу (user_id, created_at). Більшість скарг «SQLite повільний» — насправді «ви забули індекс». Це стосується й MySQL.

Task 10: Перевірте MySQL на повільні запити (якщо ви вже мігрували або оцінюєте)

cr0x@server:~$ sudo mysql -e "SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';"
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| slow_query_log | ON    |
+----------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| long_query_time | 1.000 |
+-----------------+-------+

Значення: Slow query log увімкнено; запити довше 1с логуються.

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

Task 11: Перевірте контенцію блокувань і активні транзакції MySQL

cr0x@server:~$ sudo mysql -e "SHOW FULL PROCESSLIST;"
+----+------+-----------+------+---------+------+------------------------+-------------------------------+
| Id | User | Host      | db   | Command | Time | State                  | Info                          |
+----+------+-----------+------+---------+------+------------------------+-------------------------------+
| 17 | app  | 10.0.1.12 | app  | Query   |   12 | Waiting for table lock | UPDATE sessions SET ...       |
| 23 | app  | 10.0.1.13 | app  | Query   |    0 | Sending data           | SELECT * FROM orders WHERE... |
+----+------+-----------+------+---------+------+------------------------+-------------------------------+

Значення: Запит чекає на блокування таблиці вже 12 секунд. Це не ок.

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

Task 12: Сніпшот здоров’я InnoDB для MySQL (команда «скажи, що болить»)

cr0x@server:~$ sudo mysql -e "SHOW ENGINE INNODB STATUS\G" | head -n 40
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
=====================================
2025-12-30 10:20:11 0x7f3b6c1fe700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 10 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 1120 srv_active, 0 srv_shutdown, 332 srv_idle
srv_master_thread log flush and writes: 1452
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 1021
OS WAIT ARRAY INFO: signal count 1004
Mutex spin waits 0, rounds 0, OS waits 0
RW-shared spins 112, OS waits 19
RW-excl spins 88, OS waits 25

Значення: Цей вивід може показати lock waits, buffer pool pressure і проблеми з лог-флашем. Навіть швидкий перегляд вкаже, чи двигун очікує I/O або блокувань.

Рішення: Якщо бачите важкі log waits або buffer pool misses — налаштовуйте MySQL і сховище. Якщо бачите lock waits — оптимізуйте запити/транзакції/схему.

Task 13: Орієнтовно виміряйте hit rate Innodb buffer pool

cr0x@server:~$ sudo mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name                         | Value      |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests      | 982345678  |
| Innodb_buffer_pool_reads              | 1234567    |
+---------------------------------------+------------+

Значення: Відношення reads до read_requests вказує на ефективність кешу. Тут фізичних читань небагато — це добре.

Рішення: Якщо фізичних читань стає більше, додайте RAM, налаштуйте buffer pool size або виправляйте запити/індекси, які роблять великі скани.

Task 14: Підтвердіть відставання реплікації MySQL (якщо покладаєтеся на репліки)

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

Значення: 27 секунд відставання. Це достатньо, щоб порушити припущення «читати-свої-записи».

Рішення: Якщо застосунок читає з реплік, потрібні правила консистентності (стикі-сесії, маршрутизація після запису) або зменшення лагу через налаштування і розділення навантаження.

Task 15: Переконайтеся, що SQLite не використовує багатопотоки непомітно

cr0x@server:~$ sqlite3 /var/www/app/db/app.sqlite3 'PRAGMA compile_options;' | grep -E 'THREADSAFE|OMIT_WAL' | head
THREADSAFE=1

Значення: SQLite зібрано threadsafe (добре). Але thread safety — не те ж саме, що масштабування конкуренції; обмеження одного писача залишається.

Рішення: Якщо ваш застосунок вважає «потоки = пропускна здатність», переробіть шляхи запису, перш ніж звинувачувати SQLite.

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

1) Інцидент через помилкове припущення: «Це файл, тож спільне сховище вирішує масштаб»

Середня компанія мала веб-застосунок, що народився на одній VM з SQLite. Було нормально: кілька записів на секунду, переважно читання і простий деплой. Потім додали другий веб-сервер за балансувальником. Ідея була чемною: більше потужності, більше відмовостійкості. База залишилася файлом на спільному мережевому файловому сховищі, бо «обом серверам потрібен доступ».

Перший день усе виглядало нормально. Трафік був невеликий, і спільне сховище справлялося у щасливому сценарії. Потім маркетингова кампанія дала трафік. Записи збільшилися: сесії, відстеження подій і невелика таблиця «last seen», яка оновлювалася майже при кожному запиті. Раптом додаток почав повертати переривчасті помилки. Найгірше: помилки були непослідовні. Деякі запити були швидкими; інші зависали й тайм-аутували.

On-call інженер побачив SQLITE_BUSY і збільшив busy timeout. Помилок стало менше. Латентність подвоїлася. Балансувальник почав позначати хости як unhealthy через повільні відповіді, що сконцентрувало трафік на менше вузлів, що збільшило контенцію блокувань. Так невелика проблема перетворилася на каскадний збій з прямим обличчям.

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

Урок не в тому, що «SQLite поганий». Урок в тому, що «спільне сховище — не кластер бази даних», і семантика файлових блокувань — не стратегія масштабування.

2) Оптимізація, що обернулась проти команди: «Вимкнемо durable-режим, буде швидше»

Команда SaaS тримала SQLite на потужному VM для низьколатентної внутрішньої панелі. Записів було більше, ніж хотілося, і стрибки латентності диска були помітні. Хтось запропонував змінити PRAGMA synchronous на менш жорстку опцію, щоб зменшити витрати на fsync. Бенчмарки виглядали чудово. Графіки — теж. Всі насолоджувалися ілюзією перемоги.

Через два тижні хост несподівано перезавантажився під час оновлення ядра. Додаток швидко піднявся. Потім користувачі почали скаржитися на відсутність останніх змін. Не пошкоджена структура бази — гірше. Дані виглядали коректно, але відсутня остання порція записів, які з точки зору UI були «committed».

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

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

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

3) Нудна, але правильна практика, що врятувала день: тестовані відновлення і дисципліна «один писач»

Інша організація роками тримала контент-сайт на SQLite. Так, роками. У них був один вузол, що обробляв файл бази, і сувора дисципліна записів: тільки один фонова служба виконувала записи, а веб-шар був переважно читальним із чергуванням оновлень.

Їхня найбільша операційна звичка не була в тонкому налаштуванні. Це було тестування відновлень. Щотижня задача витягувала останній бекап, відновлювала його на staging-хості, запускала PRAGMA integrity_check і невеликий набір запитів застосунка. Якщо відновлення не проходило — це викликало аварію. Це було нормою, а не героїчним вчинком.

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

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

SQLite їх не врятував. Дисципліна врятувала. SQLite просто не завадив.

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

Симптом: спалахи «database is locked» під час пікових навантажень
Корінна причина: забагато одночасних писачів, довгі транзакції або фонова задача, що колізує з записами запитів.
Виправлення: Увімкніть WAL, якщо доречно; скоротіть область транзакцій; перенесіть записи в чергу з одним воркером; зменшіть частоту записів (дебаунс оновлень сесій).
Симптом: SQLite швидкий локально, але повільний в проді
Корінна причина: продакшен-сховище має вищу fsync-латентність; synchronous FULL це посилює; чекпоінтинг WAL може блокувати.
Виправлення: Виміряйте латентність диска (iostat); розмістіть БД на локальному SSD; налаштуйте чекпоінти; розгляньте MySQL, якщо латентність запису має бути стабільною.
Симптом: випадкові таймаути, особливо під час бекапів
Корінна причина: наївна копія файлу під час активних записів; взаємодія чекпоінтів/блокувань; довгоживі читачі, що перешкоджають чекпоінту.
Виправлення: Використовуйте онлайн backup API SQLite через інструменти; переконайтеся, що бекапи включають стан WAL; плануйте бекапи з обмеженням записів.
Симптом: файл WAL росте гігантським і залишається великим
Корінна причина: чекпоінти не завершуються через довго живі читачі або неправильно налаштований autocheckpoint; додаток тримає відкриті транзакції читання.
Виправлення: Забезпечте короткоживі з’єднання/транзакції; встановіть autocheckpoint; явно робіть checkpoint у періоди низького трафіку; перевірте поведінку ORM.
Симптом: після додавання другого сервера додатка продуктивність падає
Корінна причина: SQLite на спільному сховищі з кількома писачами; контенція блокувань між процесами/хостами; семантика файлової системи.
Виправлення: Не робіть так. Централізуйте записи або мігруйте на MySQL. Якщо потрібні кілька серверів додатка, серверна БД — нормальна відповідь.
Симптом: MySQL «повільний» після міграції, гірший ніж був SQLite
Корінна причина: немає індексів, немає пулу з’єднань, неправильні припущення ізоляції або схема, спроектована під файлові патерни.
Виправлення: Увімкніть slow query log; додавайте індекси за шаблонами запитів; використовуйте пул з’єднань; правильно налаштуйте InnoDB buffer pool.
Симптом: CPU MySQL в нормі, але запити зависають
Корінна причина: очікування блокувань або I/O (fsync тиск redo лога, buffer pool misses).
Виправлення: Використайте SHOW ENGINE INNODB STATUS; скоротіть транзакції; налаштуйте redo log і buffer pool; відсунутий гарячі записи з первинного вузла за можливості.

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

Покроково: вирішуємо, чи SQLite ще безпечний для вашого сайту

  1. Заміруйте всіх писачів. Перерахуйте всі шляхи коду, що пишуть: запити, cron, фон. задачі, аналітика, сесії, інвалідизація кешу, адмін-інструменти.
  2. Виміряйте конкуренцію записів. Не середні записи — пікові перекривні записи. Якщо «невідомо», припустіть, що це «занадто багато», поки не доведено інакше.
  3. Увімкніть WAL (якщо ще не увімкнено) і перевірте його. Підтвердіть PRAGMA journal_mode = WAL і зрозумійте поведінку чекпоінтів.
  4. Забезпечте короткі транзакції. Забороніть «відкриті транзакції під час мережевих викликів». Якщо ваш ORM це полегшує — це не комплімент.
  5. Перевіряйте бекапи. Відновлюйте щотижня. Запускайте integrity checks. Репетируйте RTO, а не лише RPO.
  6. Тримайте БД на локальному сховищі. Якщо не можете — розглядайте це як великий ризик і плануйте міграцію.
  7. Сплануйте історію масштабування вшир. Якщо потрібні кілька серверів додатка, заплануйте міграцію на MySQL, поки ви ще спокійні.

Покроково: як мігрувати з SQLite на MySQL без кар’єрних наслідків

  1. Закріпіть семантику схеми. Визначте типи, обмеження і дефолти явно. SQLite ліберальний; MySQL змусить вас обирати.
  2. Виберіть стратегію міграції. Для малих наборів даних: міграція з даунтаймом. Для більших: dual-write або change capture (складніше, але можливо).
  3. Побудуйте відтворюваний експорт/імпорт. Перший прогін — репетиція; другий — те, що дозволить вам спокійно спати.
  4. Перевірте кількості та інваріанти. Кількість рядків по таблицях, контрольні суми критичних полів, запуск консистентних перевірок на рівні застосунка.
  5. Спочатку переводьте читання (опціонально). Іноді можна спрямувати read-only endpoint-и на MySQL репліки, поки записи йдуть у SQLite, але будьте уважні до припущень консистенції.
  6. Переключайте записи з чітким планом відкату. Відкат — це не паніка. Це процедура.
  7. Увімкніть спостережуваність MySQL з дня нуль. Slow query log, error log, метрики, бекапи і відпрацювання відновлень.

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

1) Чи може SQLite витримувати «великий трафік»?

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

2) Чи завжди кращий WAL режим?

Зазвичай для веб-навантажень — так. Він поліпшує конкуренцію читачів під час записів. Але він вводить динаміку WAL і чекпоінтингу, яку треба розуміти, а деякі крайові випадки (певні файлові системи або інструменти) потребують уваги.

3) Який найчіткіший знак, що варто мігрувати на MySQL?

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

4) Чи можна запускати SQLite на NFS, якщо бути обережним?

Іноді — але «бути обережним» потребує визначення: перевірені семантики блокувань, протестовані режими відмов і зазвичай дисципліна single-writer. Якщо ви робите багатохостові одночасні записи, ви ставите сайт на ставках від деталей файлової системи.

5) Хіба MySQL не «важчий» і повільніший через мережу?

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

6) Якщо SQLite блокується при записах, чи можна просто збільшити busy timeout?

Можна, і це може зменшити помилки. Але це також перетворює контенцію на латентність. Якщо у вас жорсткі SLO на час відгуку, довгі busy timeouts — це просто повільна відмова з кращими логами.

7) Що з використанням SQLite для сесій або аналітичних подій?

Це класичні сценарії з інтенсивними записами, що створюють контенцію. Якщо настоюєте на SQLite — пакетизуйте й чергуйте записи, уникайте оновлення рядків при кожному запиті. Інакше використовуйте окрему систему (MySQL, Redis або лог-пайплайн), спроектовану для такого профілю записів.

8) Який найбільший «підводний камінь» при переході з SQLite на MySQL?

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

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

Так. Це поширене і валідне розгортання. Але вам усе одно потрібно керувати конкуренцією записів: WAL режим, короткі транзакції і уважне планування фон. задач.

10) Якщо я залишаюся на SQLite, яка найважливіша дисципліна?

Контролюйте записи і тримайте транзакції короткими. Потім перевіряйте бекапи відновленнями. Ці дві звички запобігають більшості історій «SQLite зіпсував наш сайт».

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

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

Робіть практичну роботу:

  • Якщо ви на SQLite: підтвердьте WAL режим, виміряйте латентність диска, перевірте писачів, скоротіть транзакції і тестуйте відновлення. Якщо потрібні кілька серверів додатка з записами — заплануйте міграцію, поки ще є час.
  • Якщо ви на MySQL (або переходите туди): увімкніть slow query logging, стежте за lock waits, правильно налаштуйте buffer pool і ставте бекапи/відновлення як продакшен-фічу, а не як галочку відповідності.
  • У будь-якому випадку: робіть вибір бази даних на основі конкуренції і відновлення після відмови, а не за відчуттями.
← Попередня
Чому Intel прийняв AMD64 (і чому це змінило все)
Наступна →
ZFS recordsize і compression: комбінація, що змінює баланс CPU і диска

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