praktycznie od zawsze na listach dyskusyjnych bazo danowych, forach, grupach i kanałach ircowych pojawia się jeden temat: “czy i jak przechowywać w bazie obrazki?". oczywiście nie chodzi tylko o obrazki. chodzi o wszelkiego typu pliki binarne – obrazki, filmy, muzykę, pliki z danymi (excel, word itd.).
osobiście jestem przeciwnikiem trzymania tego typu rzeczy w bazie. ale zrobiłem ostatnio mały research aby wypisać często pojawiające się argumeny za i przeciwko takiemu rozwiązaniu.
te argumenty postaram się skomentować, choćby po to by samemu mieć pewność, że mój wybór jest lepszy.
podstawowymi argumentami “za" trzymaniem plików w bazie danych są:
- trzymanie ich w bazie upraszcza zarządzanie. praca z np. 100 obrazkami nie jest problemem tak czy inaczej, ale jak poradzić sobie jak jest ich kilka milionów?
- wystarcza jeden prosty backup. w tym backupie jest wszystko co konieczne do wznowienia pracy.
- bezpieczeństwo – posiadanie danych w bazie umożliwia proste filtrowanie dostępu.
- pliki poza bazą danych są narażone na rozsynchronizowanie z bazą – skasujemy rekord z bazy, a pliku nie, albo odwrotnie i katastrofa gotowa.
- zapisanie pliku do bazy jest prostsze niż wydzielanie oddzielnego api do zapisywania na filesystemie
podstawowymi (dla mnie) argumentami przeciwko trzymaniu ich w bazie są:
- obrazki zajmują zazwyczaj sporo więcej miejsca niż pozostałe dane. to powoduje pewne utrudnienia przy korzystaniu z baz danych które wymagają prealokacji przestrzeni dyskowej.
- przesyłanie obrazków wykrzystuje bardzo nieetektywnie połączenia do bazy – połączenie trwa długo i praktycznie nic nie robi
- przechowywanie obrazków w bazie praktycznie nic nam nie daje od strony bazodanowej. inne typy danych są użyteczne przy budowaniu warunków wyszukania, łączenia czy sortowania – i w ten sposób “zarabiają" na to by umieścić je w bazie. obrazków (ich plików) nie da się do tego wykorzystać.
- systemy plików w nowych systemach operacyjnych są mocno zoptymalizowane w celu szybkiego dostarczania i cache'owania plików. dodatkowo – nowoczesne serwery http potrafią korzystać z mechanizmów systemu operacyjnego do dalszego przyspieszenia wysłania obrazka.
dodatkowo czasem pojawia się jeszcze jeden argument, który jest używany przez zwolenników trzymania plików w bazie, mimo, że jest argumentem “negatywnym" – wykazującym jedynie brak sensu jednego z argumentów przeciwników.
chodzi o to, że zwolennicy twierdzą, że co prawda trzymanie plików w bazie zwalnia bazę, ale obecnie stosowane serwery są i tak bardzo szybkie, pamięć jest tania, dyski też, więc to nie problem.
argument ten traktuję bardziej jako “wyznanie wiary" niż stwierdzenie faktu. pamięć może i jest tania gdy mówimy o pamięcy do pecetów czy do serwerów tak do 8 giga ramu. każda osoba która korzysta z większych maszyn wie, że ceny pamięci rosną lawinowo powyżej tej granicy – w szczególności 16 giga ramu kosztuje dużo więcej niż 2x koszt 8 giga ramu. a co do dysków – fakt robią się coraz tańsze, ale też wymagamy od nich więcej. macierz którą ostatnio się bawiłem kosztowała około $55000. to nie są “grosze".wróćmy więc do bardziej sensownych argumentów.
argumenty “za":
1. to jest istotny problem. bazy danych świetnie sobie radzą z tabelami mającymi kilka milionów rekordów. natomiast katalog z kilkoma milionami plików to porażka.ale wydaje mi się, że problem jest rozdmuchany. wystarczy wymyśleć sensowną konwencję nazewnictwa katalogów/plików i problem znika. przykładowo – w systemie nad którym ostatnio sporo siedzę, nazwy plików są stworzone przez taką transformatę:
- metadane obrazka są wpisywane do bazy
- obrazek dostaje swoje id – integera
- integera konwertujemy na wartość hexadecymalną
- dopełniamy opcjonalnym zerem do parzystej ilości cyfr
- wstawiamy “slashe" między pary cyfr
- i doklejamy rozszerzenie.
brzmi skomplikowanie. ale jest proste. przykład:
- obrazek o id: 1533672
- po konwersji na hex uzyskujemy: 1766e8
- ilośc cyfr jest parzysta więc nie muszę dopełniać (dostawiać z przodu 0)
- wstawiam slashe i uzyskuję: 17/66/e8
- dodaję rozszerzenie i mam: 17/66/e8.jpg
- i koniec.
dzięki temu na każdym poziomie katalogów z obrazkami mam maksymalnie 512 pozycji (256 plików i 256 katalogów). wyszukiwanie i serwowanie tego jest szybkie i proste.
2. jeden prosty backup może i tak. ale ten argument się w/g mnie mocno kłóci z milionami plików.
dużo prościej jest zbackupować bazę o wielkości x giga i do tego katalog z milionem plików niż bazę danych o wielkości x giga + ten milion plików.
czyli – prosty backup tak, ale dla relatywnie małych ilości plików. potem się okazuje, że zwykły dump staje się nierealny i trzeba używać rzeczy typu hot-backup.
3. argument celny o ile pliki są wystawione “po prostu" w documumentroot'cie apache'a.
ograniczanie dostępu na poziomie bazy jest oczywiście miłe i proste, ale zrobienie tego samego po stronie apache'a też nie jest trudne – są .htaccess'y, mod_perl, mod_rewrite, zarządzanie uprawnieniami “w locie". wymaga to trochę innego zestawu umiejętności niż zaprogramowania aplikacji, ale w końcu nie jest to jakiaś wiedza tajemna. są manuale, są przykłady.
dodatkowo – dla większości aplikacji kontrola dostępu do plików binarnych nie jest niezbędna. zazwyczaj (nie zawsze!) są one tylko dodatkiem do danych tekstowych.
sam znam kilka miejsc gdzie obrazki są w ogóle nie chronione – chroni się teksty. jak ktoś się domyśli jaki jest url do obrazka – fajnie. da się przeżyć. a jak się nie da – są metody naprawdę prostej ochrony takich danych.
4. to faktycznie jest problem. w sensie – nie da się zapewnić braku możliwości skasowania pliku z filesystemu. a i robienie triggera kasującego plik z filesystemu przy skasowaniu rekordu byłoby problematyczne (choćby kwestia konieczności użycia poleceń systemowych, co większość baz danych utrudnia, czy rollbacków).
natomiast realnie (mówiąc realnie mam na myśli serwisy którymi się zajmuję) – problem tak naprawdę nie występuje. pliki są wgrywane na filesystem. nikt ich ręcznie stamtąd nie kasuje. dostęp na roota jest limitowany do adminów, innych kont praktycznie na webserwerach nie ma.
jak plik zostanie skasowany z bazy (w sensie, jego metadane) – to przecież istnienie go na dysku nam nie przeszkadza – tzn. zajmuje trochę miejsca, ale to wszystko.
aby to zużycie miejsca przez skasowane pliki zminimalizować wystarcza naprawdę prosty automat który robi listę plików w filesystemie, listę plików z bazy, porównuje i kasuje te których nie powinno być. mały cron który np. raz dziennie robi porządki. nam ten cron zajął jakieś 20 minut pracy. to chyba nie jest za dużo?
5. ostatni argument nt. prostoty api muszę przyznać, że mnie zaskoczył.
spotkałem się z nim relatywnie najrzadziej, ale i tak pojawił się kilka razy. musiałem się nad nim chwilę zastanowić. efekt?
pomyślmy. api wpisu obrazka do bazy dla fajnych baz wygląda:
INSERT INTO obrazki (file) VALUES (‘?');
i wstawienie tak danych binarnych. to jest faktycznie proste. do czasu.
po przejści na inną (większą) bazę pojawiają się bloby których obsługa jest już zdecydowania inna. trzeba otworzyć, pisać, zamknąć – zupełnie jak przy pliku.
co do obsługi pliku, wystarcza trójca: open(); write(); close();. niezależnie od języka programowania wygląda to zasadniczo podobnie. dodatkowo część języków ma wbudowane mechanizmy do robienia tego jeszcze prościej – np. w perlu, można użyć modułu io::all, i potem takiej konstrukcji:
$dane > io(‘/tmp/some.file');
co zajmie się otwarciem, zapisaniem i zamknięciem. bezpiecznie. z obsługą sytuacji krytycznych.
i zadziała tak samo dla site'u mającego 400 wizyt tygodniowo i takiego co ma 400 wizyt na sekundę.
tak więc prostota api wydaje mi się być mocno dyskusyjna. ale ponieważ się pojawiła, trzeba było skomentować 🙂
zostały argumenty przeciwników zapisywania obrazków w bazie – z nimi pójdzie mi łatwiej, bo są to też moje argumenty:
1. na temat tego argumentu mam relatywnie najmniej do powiedzenia. do tej pory używałem dwóch baz wymuszającym prealokację przestrzeni (adabas d i oracle). w obu przypadkach uznałem pomysł za chybiony i maksymalnie upierdliwy. nie jestem specem od oracle'a czy adabasa. może się da to jakoś rozwiązać ładniej. ale jedna rzecz mnie “boli":
mając plik na dysku który ma 1 megabajt. zajmuje on na dysku fizycznie ten 1 megabajt plus wielkosć inode'a. łącznie powiedzmy 1 megabajt i 1 kilobajt.
plik w bazie podlega innym regułom. np. w postgresie – pole dłuższe niż 2 kilobajty jest dzielone i przechowywane w tabelach typu “toast". realnie oznacza to, że na każde 2 kilobajty jest zapisywane dodatkowo około 26 bajtów (plus 4 jeśli używamy oid'ów).
oznacza to, że nasz 1 megabajtowy obrazek w bazie zajmuje około 1megabajt i 13 kilobajtów.
12 dodatkowych kilobajtów to niedużo. a co jak obrazków jest milion?
2. z mojej dotychczasowej praktyki z bazami wynika, że jednego zasobu nigdy nie ma za dużo – są to połączenia do bazy. wystarczy jedno źle napisane zapytanie i potrafi zapchać serwer tak, że nawet najprostsze i najszybsze selecty się nie wykonają. z tego punktu widzenia obrazki to koszmar.
mały, prosty select, indeksowany – powinien być błyskawiczny. i faktycznie jest. dopóki czytamy dane lokalnie.
jeśli jednak request jest zdalny, to połączenie będzie wykorzystywane przez cały czas transmisji do klienta. czyli w skrajnym przypadku kilka minut. oops? czy widzicie ten problem?
nie ma znaczenia co zrobicie – wystarczy, że więcej ludzi zechce obejrzeć wasze obrazki korzystając z kiepskich łącz i system siada. a chyba nie o to chodziło?
3. to wydaje się być oczywiste. wrzucenie do bazy danych np. wielkości obrazka pozwala na proste filtrowanie. daty daje możliwość wyszukania najnowszych obrazków. a co nam daje wrzucenie samego obrazka? nic.
baza nie będzie filtrowała używając danych obrazka. nie założymy na tym indeksu. nie zrobimy order by czy nic takiego. po prostu dane do jednokrotnego wstawienia i selectowania. zero zysku przy dostępie.
4. ten argument wydaje mi się najistotniejszy.rozważmy co musi zrobić aplikacja aby zaserwować obrazek.
załóżmy, że nasza aplikacja jest w php, pracuje pod apache'em. baza danych – postgres.
- php otwiera połączenie do postgresa (albo korzysta z gotowego)
- wysyła zapytanie
- postgres szybko znajduje plik w tabeli
- odczytuje rekord
- zwraca do php
- znajduje nastepny blok
- odczytuje
- przesyła do php
- itd. aż do końca pliku.
- wtedy postgres informuje: to już koniec
- a php przesyła to do klienta.
skomplikowane. w dodatku – postgres nie ma i nie może mieć za bardzo zoptymalizowanego dostępu do tego pliku – w końcu – to dane jak inne.
co się dzieje gdy request o plik trafia do apache'a który serwuje pliki statyczne z filesystemu?
- mając otwarte połączenie do klienta
- apache otwiera plik który ma serwować
- a następnie wykonuje jednego syscalla: sendfile
ten syscall realizuje przesłanie zawartości jednego uchwytu pliku do drugiego. w tym przypadku całej zawartości pliku do klienta. całośc odbywa się w kernelu i jest niesamowicie szybka.
zgadza się, że nowe maszyny są szybsze niż kiedyś. ale wymagamy od nich więcej. i jeśli mam obsłużyć 2 miliony page-views dziennie, to nie będę poświęcał czasu maszyn na “durne" przerzucanie danych między procesami (baza, php, klient) skoro można po prostu przepompować dane na poziomie kernela. bez żadnych pętli, warunków itd.
do tego wszystkiego dochodzi jeszcze jeden argument – dla mnie istotny. w życiu każdego systemu pojawia się sytuacja, że kończy się wydajność sprzętu. trzymając wszystko w bazie musimy modyfikować serwer bazodanowy. a mając dane “rozrzucone" mamy większe marginesy bezpieczeństwa i więcej możlwości modyfikacji.
cały czas sensowne klastrowanie baz danych jest marzeniem. są produkty które to robią, ale kosztują horrendalne pieniądze.
natomiast klastrowanie apache'y czy innych fileserwerów jest proste, łatwe i praktycznie bezkosztowe (poza sprzętem oczywiście).
kończąc ten wpis – na pewno są sytuacje gdy względy różne (prawne czy biznesowe) wymuszają stosowanie przechowywania obrazków w bazie. wierzę, że są też takie sytuacje i takie powody o których ja nie wiem.
wiem jednak, że dla praktycznie wszystkich trzymanie obrazków w bazie jest dobrowolną rezygnacją z wydajności i skalowalności w imię mitycznych i łatwo obalalnych idei.