Разработка

SQLite в продукция: кога е правилният избор и как да го използвате безопасно

От Bozhidar Ivanov · Софтуерен инженер 5 мин. четене
Икона на цилиндър за база данни с три стрелки, сочещи към него, символизиращи три отделни процеса, записващи в един и същ файл

Един файл, три записващи процеса, без сървър за наглеждане

Три отделни процеса записват в един и същ файл с база данни на сайта на Arc AI: уеб сървърът, "работник" на опашка за фонови задачи и планировчик по разписание (scheduler). Четенето се случва постоянно, записът, няколко пъти на час. Няма сървър за бази данни, само файл на диска. Това покрива кое този файл безопасен с повече от един записващ процес и къде SQLite престава да е правилният инструмент.

Кога SQLite е подходящ и заключването, до което рано или късно ще стигнете

SQLite подхожда на един сървър, с натоварване предимно от четене, запис от време на време, а не постоянен, и предпочитание към прости резервни копия пред инфраструктура със собствени идентификационни данни и ред на стартиране. Спира да е подходящ при втори приложен сървър, защото файлът на SQLite живее на една машина, както и щом записът стане чест и едновременен, или щом ви трябват реплики за четене или управлявано превключване при отказ.

Един бонус: около 670-те PHP теста на сайта работят срещу база данни SQLite в паметта, без контейнери, без схема, зареждана през мрежова връзка.

Дайте обаче на еднофайлова база данни три процеса, които искат да пишат върху нея едновременно, и рано или късно ще получите грешка "database is locked" ("базата е заключена"). SQLite допуска само един записващ процес наведнъж; втори, пристигнал по средата на транзакция, се проваля веднага, вместо да изчака. При уеб заявка, работник на опашка и планировчик, работещи върху едни и същи таблици - това е въпрос на кога, не на дали.

Трите настройки, които правят едновременния запис безопасен

WAL режимът (write-ahead logging) изпраща новите записи първо в отделен лог файл, който по-късно се включва в основната база. Така четящите процеси виждат последователна картина, докато записващ процес работи. Това премахва по-голямата част от конфликтите.

Таймаут при заетост, пет секунди при сайта на Arc AI, позволява на записващ процес, заварил файла заключен, да изчака заключването да се освободи, вместо да се провали веднага.

Настройката, която повечето пропускат: транзакция, която започва с четене и по-късно решава да пише, пак може да се провали моментално, дори с включени WAL и таймаут, защото SQLite не изчаква заключване за запис по средата на транзакция. Това крие риск от взаимно блокиране. Решението е да зададете предварително на SQLite, че транзакцията ще пише, за да поземе заключването веднага и да се нареди коректно на опашката. Проверката на плана на Arc AI прихвана този пропуск, преди сайтът да види реален трафик.

Две таблици, един адрес, един път за запис

Уникалните индекси пак могат да позволят на две таблици да се сблъскат. Представете си таблици posts и categories, всяка раздаваща адреси от едно и също пространство със собствен уникален индекс. Нито един индекс не знае за другата таблица, затова от два отделни процеса всеки може да намери адрес, който е свободен в тяхната си таблица, и да завършат транзакцията почти едновременно - два реда в две таблици, споделящи един адрес, като и двата индекса са удовлетворени.

Никой индекс в рамките на една таблица не може да прихване това. Решението тук е всеки запис, задаващ такъв адрес, да минава през една транзакция, проверяваща двете таблици, преди да потвърди която и да е от тях. Ако пътят е зает, потребителят вижда "опитайте отново", но никога адрес, споделен от две неща едновременно.

Какво може тихо да отмени преизграждане на таблица

SQLite не прилага директно всяка промяна в схемата; много такива изискват създаване на нова таблица, копиране на всеки ред, изтриване на старата и преименуване на новата на нейно място. Това запазва само онова, което миграцията изрично пресъздава. За сайта на ARC AI една миграция преизгради таблица по несвързана причина, ала новото ѝ определение пропусна едно CHECK ограничение ("статусът трябва да е чернова или публикувано"), и правилото тихо престана да съществува. То беше възстановено и вписано в самото определение на таблицата, за да го запазва автоматично при следващо преизграждане.

Резервните копия крият подобен капан. В WAL режим последните потвърдени транзакции могат да седят в отделния лог файл, а не в основния, така че копирането само на основния файл, докато сайтът работи, може да пропусне последните няколко минути записи. Собственият механизъм на SQLite за правене на резервни копия онлайн копира двата файла последователно; обикновено копиране на файл не го прави. Отварянето на активния файл през Docker споделяне на файлове, докато контейнерът работи, също може да покаже остарели данни, защото WAL разчита на споделена памет, която не преминава чисто през тази граница.

Преди да се доверите на SQLite в продукция

  • Имате един сървър с натоварване предимно от четене? SQLite е подходяща опция.
  • WAL режимът е включен, зададен е таймаут при заетост, а записващите транзакции са отбелязани като незабавни от самото начало.
  • Всичко, което трябва да е уникално в повече от една таблица, минава през единен път за запис.
  • След всяка миграция, преизграждаща таблица, проверете дали старите ограничения все още отхвърлят невалидни стойности.
  • Правете резервни копия чрез вградения онлайн механизъм на SQLite и действително тествайте възстановяването от тях.
  • Запишете някъде, видимо за целия екип, кое би означавало, че е време да преминете към друга база данни.

Претегляне на SQLite срещу сървър за бази данни

Използването на MySQL вместо това би означавало още един контейнер, стотици мегабайти памет, които не са нужни в момента, отделни данни за идентификация, ред на стартиране за уточняване и втора рутина за създаване на резервни копия успоредно с първата, без да се добави нито една функция, от която сайтът реално се нуждае. Честните признаци, че е време за преразглеждане, не са философски: втори приложен сървър, изчаквания за заключване, появяващи се в логовете, или реална нужда от реплики или управлявано превключване при отказ. Никое от тях засега не важи.

Още по темата.

Продължете с четенето

Разработка 4 мин. четене

Преди да добавите колона за статус: планиране на публикации без cron задача

Планираните публикации обикновено означават нов статус и cron задача. Поле за бъдеща дата може да свърши цялата работа, ако запишете какво е позволено да закъснее.

Bozhidar Ivanov