Proč ukládat soubory do databáze

Představte si že pracujete na webové aplikaci která vyžaduje ukládání souborů (např. na fotogalerii). A tady přichází otázka "Mám soubory ukládat v databázi nebo přímo ve filesystému?"  Možná jste tuto otázku položili v nějakém diskusním fóru / mailing listu, a předpokládám že odpověď kterou jste dostali byla zhruba "Je daleko efektivnější ukládat soubory mimo databázi." Dobře, pokusím se vysvětlit proč dávám přednost ukládání souborů v databázi. Tento článek je založen na diskusi kterou jsem vedl s Jamesem Lewisem poté co jsem do mailing listu pg-php poslal obdobný dotaz.

zbláznil ses? proč ukládat soubory do databáze?

Hlavní problém při ukládání souborů mimo databázi (a jejich odkazování pomocí cesty uložené v databázi) je že dochází k míchání transakčních a netransakčních prostředí - databáze má všechny pěkné vlastnosti (je transakční) zatímco souborový systém "rychlý ale hloupý" (žádné transakce, žadný rollback, žádné podobné pěkné vlastnosti). Dle mé zkušenosti toto přináší spoustu problémů pokud chcete vyvinout robustní a spolehlivou aplikaci (kdo nechce?).

Ukládání souborů přímo v souborovém systému je celkem jednoduché - řekněme že máte tabulku "Files" se všemi metadaty souboru (jméno souboru, velikost, časy vytvoření / modifikace, ...) a cestu odkazující na místo v souborovém systému:

CREATE TABLE Files (
    id       SERIAL      PRIMARY KEY,
    filename VARCHAR(64) NOT NULL,
    filesize INT         NOT NULL,
    path     TEXT        NOT NULL,
    created  TIMESTAMP   NOT NULL,
    ... další metadata ...
);

Řekněme že chcete smazat soubor - to znamená smazání řádku z tabulky a souboru z filesystému. Problémy se objeví v situaci kdy se něco pokazí - předpokládejme že smazání řádku z tabulky proběhne v pořádku, ale při mazání souboru ze souborového systému (pomocí cesty uložené v tabulce) z nějakého důvodu selže. Pokud jste commitnuli DELETE z tabulky před pokusem o smazání souboru z filesystému, tak nyní ve filesystému zůstal "osiřelý soubor" (soubor neodkazovaný z tabulky, t.j. zbytečně zabírající místo).

Pokud jste DELETE necommitnuli, můžete jednoduše provést rollback příkazu DELETE a všechno je v pořádku, že? No, není - pokud jste se pokusili smazat několik souborů najednou, a mazání posledního souboru z filesystému selže, potom rollback stejně není možný protože budou chybět data (souborové systémy nemají podporu pro rollback).

Podobné "problémové scénáře" existují pro všechny operace zahrnující transakční a netransakční zdroje (ne jenom souborové systémy). Tyto situace je většinou možno ošetřit, ale znamená že buď budeme k oběma zdrojům přistupovat jako k netransakčním (tj. vůbec nebudeme nepoužívat transakce, což se mi příliš nezamlouvá - konec konců transakce jsou jeden z důvodů pro používání databází) nebo implementovat nějaký druh transakční podpory - a to je poněkud zdlouhavá práce a náchylná k chybám (a to se nezmiňuji o vlivu na výkonnost která byla jedním z důvodů pro použití netransakčního filesystému).

problémy s údržbou

Jistě - pokud máte plnou kontrolu nad aplikací, je často možné potřebné operace ošetřit vhodně napsanými procedurami, ale podívejme se na velice důležitý aspekt živnostního cyklu aplikace - údržbu, a zejména na její velice důležitou část - zálohování.

Zálohování PostgreSQL databáze je celkem jednoduché - pokud je provedeno správně, získáte otisk (snapshot) databáze tak jak existovala v okamžiku spuštění zálohování, a bude 100% konzistentní (žádná chybějící / přebývající data, atd.).

Ale co netransakční zdroje? V běžně používaných souborových systémech neexistuje podpora pro transakce nebo snapshoty (OK, existují souborové systémy s podporou pro snapshoty - například "zfs", ale bude dlouho trvat než se prosadí na větší části produkčních systémů).

Neexistuje nic jako "konzistantní záloha" souborového systému - pokud začnete zálohovat a někdo smaže soubor ze zálohovaného adresáře, může ale nemusí být obsažen v záloze (a je těžké to předpovědět). Pokud nejdříve zazálohujete databázi a až poté souborový systém, někdo může smazat soubor mezi těmito dvěma kroky, takže v podstatě vznikne nekonzistantní záloha - soubor nebude zazálohován, ale bude odkazován ze zálohy databáze. Pokud naopak nejdříve zazálohujete souborový systém (a až poté databázi), někdo může mezi těmito kroky vytvořit soubor, čímž opět vznikne nekonzistentní záloha (opět, soubor bude odkazován z databáze ale nebude obsažen v záloze souborového systému).

A toto nelze ošetřit jednoduchým API :-( Aby toto bylo možno ošetřit je třeba dobře definovánat proceduru zálohování a opatrně implementovat operace se soubory - viz. následující odstavec.

nepěkné řešení uložení "mimo databázi"

Jak již bylo řečeno, kolem souborového systému je možné vystavět funkcionalitu poskytující "falešné transakce" (například bez možnosti rollbacku), takže aplikace bude fungovat celkem bezpečně i když používá netransakční zdroje ... ale je poněkud nepěkné.

Například můžete soubory ukládat do vyhrazeného adresáře pod unikátními ID generovanými z databázové sekvence, a nikdy nemazat nebo nemodifikovat již vytvořený soubor. Například můžete použít tabulku "Files" popsanou výše, ale namísto cesty použít unikátní ID.

CREATE TABLE Files (
    id       SERIAL      PRIMARY KEY,
    filename VARCHAR(64) NOT NULL,
    filesize INT         NOT NULL,
    file_id  SERIAL      NOT NULL,
    created  TIMESTAMP   NOT NULL,
    ... další metadata ...
    CONSTRAINT unique_file_id UNIQUE (file_id)
);

Základní operace by potom byly prováděny takto:

  • CREATE - při ukládání souboru do filesystému ho uložte pod novou hodnotou "file_id" (načtěte ji z posloupnosti před nebo po insertu do tabulky "Files").
  • UPDATE - pokud potřebujete aktualizovat data souboru, jednoduše načtěte novou hodnotu "file_id" a novou verzi uložte pod novou hodnotou (nepřepisujte původní soubor)
  • DELETE - namísto smazání souboru ponechte data na místě (pouze smažte řádek z tabulky "Files")

Při zálohování je třeba nejdříve zazálohovat databázi a až poté souborový systém (v opačném pořadí to nefunguje). Toto řešení funguje (a používám ho na několika místech), ale pokládám ho za poněkud nepěkné a navíc v něm vznikají osiřelé soubory (orphans), tj. soubory neodkazované z databáze.

Tyto "osiřelé soubory" nezpůsobí nebezpečné nekonzistence (chybějící data, atpod.) ale mohou způsobit problémy kvůli zbytečně zabranému prostoru na disku - toto lze vyřešit pomocí periodicky (např. každou noc) spouštěného cron jobu čistícího tyto soubory. Pouze buďte opatrní a neodstraňte nové (zatím necommitnuté) soubory.

O něco čistější by bylo udržování seznamu smazaných souborů - samostatné tabulky smazaných "file_id" hodnot - a jeho použití při čištění osiřelých souborů.

Nicméně i když toto řešení funguje, má několik zásadních nevýhod - je poměrně obtížné zaručit uvedené pořadí zálohování (databáze před filesystémem), např. při použití sdíleného hostingu, administrovaného třetí stranou ...

Komentáře

K tomuto článku zatím žádné komentáře neexistují (nebo čekají na schválení).

Nový komentář

Všechny komentáře podléhají schválení - mezi odesláním komentáře a jeho zobrazením na této stránce tedy může být prodleva. Vyplníte-li e-mailovou adresu, budete o schválení či neschválení komentáře informováni.

V titulku ani v textu nejsou povoleny HTML tagy - budou automaticky odstraněny. Odstavec ukončíte prázdným řádkem.

(nepovinné)