Od té doby, co jsem tu před pár dny publikoval článek o atomických operacích se soubory, se hodně změnilo. Udělal jsem značný pokrok a představím vám nyní naprostou novinku.
Možná ještě krátce, proč se této problematice tolik věnuji. Obecně totiž platí, že pokud váš PHP skript vytváří nebo píše do souborů, je nutné toto řešit! Už pouhý jeden zápis je kritický! V opačném případě musíte počítat se ztrátou dat a vznikem těžko odhalitelných chyb.
Tedy věnujte tomu prosím pozornost.
Kde nestačí databáze ani NSafeFile
Určitě víte, že atomického čtení, zápisu a mazání lze dosáhnout použitím databází. Tedy místo ukládání dat ve formě souborů je uložíme jako záznamy do SQlite apod. Nepříjemné je, že třeba takový konfigurační INI soubor se jakožto záznam databáze hůř edituje.
Minule jsem tu dal k dispozici třídu, která uměla atomicky načíst, zapsat a smazat soubor (skript prošel určitým vývojem, o tom později). Pomocí ní můžeme onen INI soubor bezpečně zapsat a přečíst z disku – editace tedy bude pohodlnější.
Jenže ouha! Je tu problém. Soubor chci číst funkcí parse_ini_file(), která zámky nepoužívá. Čímž jde celá snaha o atomicitu do kytek. Databáze samozřejmě nepomůže už vůbec. Co teď?
Nette\IO\SafeStream
Trklo mě to včera večer a mám pocit, že to je fakt bomba 🙂 Ne, vážně, tohle řešení se mi prostě líbí. Nette\IO\SafeStream (dříve NSafeStream) registruje „bezpečný stream“, pomocí něhož můžeme atomicky manipulovat se soubory prostřednictvím standardních funkcí. Stačí jen uvést protokol „safe://“. Příklad:
// před jméno souboru přidáme safe://
$handle = fopen('safe://test.txt', 'x');
// ve skutečnosti se vytvořil dočasný soubor
fwrite($handle, 'La Trine');
fclose($handle);
// a teprve teď se přejmenoval na test.txt
// můžeme soubor smazat
unlink('safe://test.txt');
// a vůbec používat všechny známé funkce
file_put_contents('safe://test.txt', $content);
$ini = parse_ini_file('safe://autoload.ini');
Parádička, ne?
Jak to funguje?
V podstatě se jedná o přepis původního NSafeFile do podoby Stream Wrapper. Nette\IO\SafeFile je mimochodem několikrát překopaný skript, původní verze totiž fungovala na špatném předpokladu, na což mě v diskuzi upozornil mka. Jo, taková interakce se mi vážně líbí. Vlastně jsem rád, že tam ta chyba byla, protože se mi celou věc podařilo zvládnout bez lockfiles. Tedy je to velice výkonné.
Podívejme se na magii řešení:
Čtení
Otevřu soubor v módu r
a pokusím se získat zámek pro
čtení (neboli shared lock, LOCK_SH
). Poté je možné soubor
volně číst. Zámek se uvolní automaticky s uzavřením souboru –
fclose()
nebo při ukončení skriptu.
Zápis
Otevřu soubor v módu r+
. Tím zjistím, zda existuje (budeme
přepisovat) nebo je ho třeba vytvořit.
Zápis do existujícího souboru
Soubor je tedy otevřen v módu r+
. Získáme zámek pro zápis
(neboli exclusive lock, LOCK_EX
). Obsah vymažeme funkcí
ftruncate()
a pak můžeme do souboru volně psát, až do
uzavření a uvolnění zámku.
Zápis do neexistujícího souboru
Není možné vytvořit nový soubor, protože než bych získal zámek, mohl
by s ním pracovat jiný thread. Proto vytvoříme dočasný (temporary) soubor
v módu x
a získáme exkluzivní zámek. Poté do něj volně
zapisujeme. V okamžiku uzavření souboru jej zkusíme přejmenovat na
požadovaný název. Pokud se přejmenování nezdaří (jiný thread mezitím
tento soubor vytvořil), dočasný soubor smažeme.
Zápis v módu append
Protože není možné soubory otevírat v režimech a
nebo
w
, neboť vytvoření nového souboru je nežádoucí akce,
otevřeme jej v módu r+
a posuneme ukazatel na konec via
fseek()
.
Mazání souboru
Soubor prostě smažeme funkcí unlink()
. Že má soubor
otevřený jiný thread, ať už pro čtení nebo zápis, nám nemusí vadit. Ve
Windows totiž otevřený soubor vůbec smazat nelze a unlink
selže. Naopak v Unixu se smaže, jakožto položka adresáře, ale s jeho
obsahem je možné nadále bezpečně pracovat.
Tak a to je celé. Ono je to vlastně docela jednoduché, že? 🙂 Tak můžete testovat a připomínky i nápady jsou vítány!
Komentáře
Borek #1
Nápad to je dobrý, mám jen dvě pragmatické otázky:
Jinými slovy, řešení je to určitě účinné, je ale i účelné?
David Grudl #2
#1 Borku, Databáze je dobré úložiště dat, ale INI soubor z ní nenaparsuji, také ukládat do ní fotky serializované v SQL příkazu je cesta do pekel. Tedy jsou situace, kdy musíme operovat se soubory na disku. A potom je threadsafe přístup nezbytností.
ad 2) A víš jistě, že tyto aplikace zapisují soubory na disk a přitom žádné bezpečnostní prvky nepoužívají?
Mimochodem, ta úvaha je vůbec zcestná. Stavíš na tom, že WordPress vše řeší dokonale a je třeba si z něj brát příklad. Ale proč? Protože je tak populární mezi uživateli? Existuje snad souvislost mezi popularitou a kvalitou kódu? Osobně třeba WordPress považuju za zbastlený kus kódu…
Vyzkoušej si ten příklad, co jsem uváděl minule, je i součástí NSafeStream. Při 1000 opakování mi udělá 5–10 chyb. Každá stá operace tedy končí nečekaným narušením či ztrátou dat. Je to málo?
KLoK #3
Mozna by nebylo od veci dat uzivateli sanci obsah toho souboru zrestaurovat/zapsat pod jinym jmenem. resp. co kdyz je zadouci aby obsah souboru byl ten ktrery byl smazan
pkm #4
A spinlock je opravdu spinlock = aktivní čekání v cyklu? Něco takového používat pro čekání na diskové operace je v podstatě zhůvěřilost možná asi jen v PHP.
Spinlock se používá jen v jádrech operačního systému, kdy je očekávané získání zámku kratší než přepnutí kontextu. Všude jinde by se mělo používat pasivní čekání (semafory, mutexy, monitory).
johno #5
#4 pkme, Ano, ale v PHP je to problém lebo štandardne semafóry nemáme.
Keff #6
Nsafe je vážná věc, ne že budete někdo dělat fórky! :) I DGX to říká, tak tož to už musí být pravda, né?
hvge #7
#4 pkme, Spinlock sa pouziva aj v aplikaciach, nielen v OS. Zvlast s prichodom viac jadrovych procesorov to nabera na vyzname. Napriklad kvalitne navrhnute memory alokatory pouzivaju spinlock :)
pkm #8
Ok, u memory allocatoru to jistě má smysl. Tam právě platí to, že zámek je získan rychle (jedna alokace je krátká záležitost).
Ale u čekání na práci s diskem, když to může trvat stovky a stovky milisekund?-)
To, o co se dgx pokouší je „vážnější práce“ a PHP je IMHO pro vážnější práci nevhodné.
Borek #9
#2 Davide Grudle,
Souhlas, ale právě v těchto případech se jedná o soubory typu „jednou uložit a potom tisíckrát číst“. Tvoje řešení je užitečné pro často čtená a zapisovaná data, kde hrozí, že k oběma operacím dojde současně. A já říkám, že taková data patří do databáze.
Jistě to nevím, to je fakt. Vycházel jsem pouze z toho, že před vývojem NSafeStream jsi se určitě podíval, jestli vůbec a případně jak je to vyřešeno v současných systémech. (K nakouknutí by asi byla ideální filesystémově orientovaná DokuWiki, ale sám jsem ji nezkoumal.)
To jsem nikde neřek. Omlouvám se, jestli to tak vyznělo.
Podle mě ano. Popularita → široké nasazení → reportování chyb → oprava chyb.
Já docela taky.
Abych pravdu řekl, úplně jsi mé pochybnosti nerozptýlil. Uznávám, že pokud někdo chce dělat souborově orientovanou aplikaci (třeba ukládat data do XML), tvoje práce se bezpochyby hodí. Onen člověk si však asi špatně rozmyslel, k čemu se používají soubory a k čemu databáze (sám jsem se jednou takhle špatně rozhodl).
Toť můj pocit.
Hever #10
Dá se nějak konkrétně říct, k čemu by měl v praxi budovaný kód sloužit (reaguji v návaznosti na diskuzi, kde myslím většině není příliš jasné použití)?
llook #11
Jo, taky už jsem PHP4 zavrhnul.
Borek #12
#10 Hevere, Vysvětlení viz https://phpfashion.com/…e-se-soubory
David Grudl #13
#9 Borku,
Ano, a já právě dávám k dispozici funkci, která to umí jednou uložit a tisíckrát číst. Použít běžné fce je sebevražda. Samozřejmě pokud se bavíme na top-profi úrovni ;)
Nikoliv. Už jedno uložení je kritické. To je právě riziko, na které se snažím upozornit.
Nedíval. Znáš to, co si neuděláš sám… Mohl jsem buď den Googlit a hledat řešení, u něhož bych velmi těžko ověřoval, jestli je skutečně, ale skutečně ok. Nebo obětovat 2× tolik času, ponořit se do problému, sebevzdělat se v nové oblasti a pokusit se na něco přijít. A zároveň si být jist, že to je bezchybné. A nakonec i velmi univerzální a přínosné pro všechny.
Každopádně děkuju všem co pomohli, zejména Johnovi!
David Grudl #14
Podíval jsem se, jak to dělá Dokuwiki, a dělá to naprosto špatně. Co z toho soudit?
Borek #15
#13 Davide Grudle, Není mi jasné, proč je i jedno uložení kritické.
Mimochodem, nevíš, proč nějaké thread-safe řešení není přímo součástí PHP? Tvoje práce je bezpochyby za hranicí schopností většiny PHP programátorů.
johno #16
DGX: Páči sa mi to. Toto už hej.
Jozef Izso #17
V PHP sú veľmi obľúbené guestbooky realizované pomocou súboru a nie databázy. A tam dochádza k častému read/write.
Cachovanie v Texty je tiež súborové a aj LucidCMS som si upravil na súború cache a tam by som uvítal tento safe spôsob zápisu.
David Grudl #18
#15 Borku, Zápis není atomická operace, takže může dojít k situaci, kdy ještě není soubor celý zapsán a už se jej snaží někdo číst. Pak přečte nesmysl. Pokud se takto načítá třeba tabulka uživatelských práv, může dojít k závažné bezpečností díře.
NSafeStream to řeší tak, že obsah píše do dočasného souboru, a jakmile je úspěšně zapsán, přejmenuje ho.
ad úspěšně zapsán: zmíněné Dokuwiki dokonce ani nekontroluje, zda se podařilo soubor zapsat celý! Už jsem na hostingu několikrát narazil na nedostatek místa na disku. Bojím se, že mít tam Dokuwiki, tak můžu nenávratně přijít o všechna data doslova na pár kliknutí na „edit“. Má skepse vůči Open Source projektům se tím jen potvrzuje…
#17 Jozefe Izso, Do pluginů Texy teď samozřejmě NSafeStream budu implementovat – respektive budu ho používat všude, kde má své opodstatnění.
hvge #19
Dokuwiki to ma riesene uplne nahovno. Chvilu som sa v tom vrtal a okrem toho io_save.. tam ma este dalsie zvrhlosti. Napriklad sa spyta, ‚je subor niekym editovany?‘ a ak nie je, ‚tak ho edituj‘. Hazard ako z prirucky :)
Vilém Málek #20
Rád bych se zeptal, jak je to se zotavováním z chyb skriptu v průběhu zapisování. Jak se systém zachová, pokud během generování nebo i zapisování dat dojde k nějakému problému? Vznikne mi soubor s polovinou dat nebo systém „pozná“, že je něco v nepořádku a vadný soubor zahodí?
Ondrej Ivanic #21
Preco? lebo napisa nieco aby bolo thread safe nie je trivialne. ZE2 je thread safe, kopa extenzii je thread safe, ide len o to vybrat spravny mix a potom aj to php bude thread safe :)
Thread safe mozem okaslat, radsej by som uvital daky aplikacny server pre php… Celkom je to poznat ked pri kazdom requeste je potrebne nacitat stale rovnake 30kb XML a nieco snim porobit. Niektore veci sa daju poriesit napr. pomocou APC, ale vacsina bohuzial nie.
hakim #22
chtěl jsem se optat, jak to řeší problém kdy edituji/natahuji soubor a někdo se ho mezitim snaží smazat.
2ge #23
dik, urcite to vyskusam na cachovanie, dufam, ze to pojde pekne :)
emme #24
čau, NSafeStream nejde stáhnout, mohl bys ho dát zpět? díky
David Grudl #25
#24 emme, opraveno
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.