Na navigaci | Klávesové zkratky

Translate to English… Ins Deutsche übersetzen…

Atomické operace se soubory

Co se vlastně myslí pod atomickými operacemi nebo rozumí pod pojmem „thread-safe“? Začněme jednoduchým příkladem:

$original = str_repeat('LaTrine', 10000);
$counter = 1000;

while ($counter--) {
  // write
  file_put_contents('soubor', $original);

  // read
  $content = file_get_contents('soubor');

  // compare
  if ($original !== $content)
    die('ERROR');
}

Dokola zapisujeme a následně čteme stále tentýž řetězec. Může se zdát, že volání die('ERROR') nemůže nikdy nastat. Opak je pravdou. Schválně si zkuste tento skript spustit ve dvou oknech zároveň. Error se dostaví prakticky okamžitě.

Proč tomu tak je, nedávno vysvětloval Jakub Vrána. Uvedený kód není bezpečný (safe), pokud se v jednu chvíli provádí vícekrát (tedy ve více vláknech = threads). Což na internetu není nic neobvyklého, často se v tentýž okamžik pokusí více lidí připojit k jednomu webu. Takže psaní thread-safe aplikací je velmi důležité.

Je třeba zajistit, aby se funkce file_get_contents & spol. vykonávaly atomicky. Pro Nette jsem napsal třídu, obsahující atomické alternativy těchto funkcí. Do stavu ERROR se s nimi nikdy nedostanete. Uvedený příklad stačí upravit takto:

while ($counter--) {
  // write
  NSafeFile::write('soubor', $original);

  // read
  $content = NSafeFile::read('soubor');

  // compare
  if ($original !== $content)
    die('ERROR');
}

Musím říci, že najít ten správný postup byl docela oříšek. Své by o tom mohl vykládat třeba Johno, který SafeCache předělával snad pětkrát, přesto poslední verze má stále chyby. …a nebo taky já, který těch předělávek má na krku ještě víc.

Metoda flock()

Funkce flock, která je k řešení těchto situací určená. Potíž je v tom, že má tolik háčků, až je skoro nepoužitelná. Zamknout není možné soubor před jeho otevřením či vytvořením, stejně tak smazat jej lze až po uzavření (platí pro platformu Windows). A tím pádem veškerá atomicita jde do háje.

Metoda rename()

Existuje další způsob, spočívající v přejmenovávání dočasně vytvořeného souboru – zmiňuje jej Jakub Vrána. Zdál se mi ideální, je však nepoužitelný, pokud chceme přepisovat obsah souboru – pak nelze zajistit atomicitu.

Metoda lock-files

Dále je tu princip založený na vytváření lock-files. Tedy dočasných souborů nulové délky či adresářů, jejichž přítomnost značí přítomnost zámku. Tato metoda je v podstatě nejjednodušší. Zvyšuje však počet souborových operací a celý proces se tak značně zpomalí. Dále je potřeba vyřešit problém s (ne)odstraněním lock-files, například po pádu Apache apod.

Řešení NSafeFile

Nakonec se mi (snad) podařilo vytvořit novou a spolehlivou verzi kombinující první dva postupy. Vyhnul jsem se lock-files, výsledek je tedy znatelně rychlejší. Popis všech „triků“ najdete ve zdrojovém kódu.

Aktualizace: NSafeFile už je historií, stáhněte si nástupce Nette\IO\SafeStream.

Komentáře

  1. JersyWoo http://www.jersywoo.com #1

    avatar

    A kdy projekt SODOR zveřejníš, jak jsi sliboval?

    před 11 lety
  2. roman http://www.c64.sk #2

    avatar

    Slušné!!!!

    před 11 lety
  3. hvge http://hvge.sk #3

    Jo toto je vecny problem. Ako (a to sa nebojim povedat) expert na thread-safe aplikacie mam heslo „nevyrieseny hazard sa urcite raz prejavi“. Ked sa to skombinuje s murphyho zakonom, tak sa prejavi v tu najhorsiu moznu chvilu :)

    V tvojom rieseni pouzivas externy subor na lockovanie, co je v php asi jedine mozne a dobre riesenie, akurat presne tomu sa myslim johno chcel vyvarovat :)

    Inak podobne na tom su aj pristupy do databazy, pokial su tabulky v db navrhnute zle, skor ci neskor moze nastat nekonzistencia dat.

    před 11 lety
  4. raver #4

    Neodporúčam testovať v Opere (9.0). Aj napriek tomu, že v niekoľkých taboch/oknách naraz otvoríte rovnakú URL, Opera počká kým sa načíta práve jedenkrát a rovnakú stránku zobrazí vo všetkých oknách.

    Čo sa týka samotného testovania, stress1 u mňa zbehne bez chýb.

    před 11 lety | reagoval [5] hvge [6] cita
  5. hvge http://hvge.sk #5

    Este k tomu spinlocku. Podla mna ked das sleep na 1 az 30ms, tak sa v operacnom systeme prepne kontext (na iny thread/proces). Scheduler si thready radi do „nejakej“ fronty a toto poradie meni iba pri roznych prioritach (co v pripade php asi nehra ulohu). Preto si myslim, ze staci obycajny sleep na konstantnu hodnotu, ktory vlastne len sposobi to prepnutie kontextu.

    #4 ravere, podobne to robi aj mozilla. Uz som sa par krat tiez napalil :)

    před 11 lety | reagoval [7] David Grudl
  6. cita http://citadesign.site.cz #6

    avatar

    #4 ravere, stačí přidat do GET nějaký nesmyslný parameter a opera to bere jako několik samostatně načítaných instancí.

    před 11 lety
  7. David Grudl http://davidgrudl.com #7

    avatar

    Řešení jsem mírne upgradoval, byly tam dvě chybičky. Teď už mi to připadá skutečně neprůstřelné.

    Ještě bych zdůraznil pár podstatných předností tohoto řešení:

    • Do cache je možné ukládat i prázdné řetězce
    • Nedochází k výpadkům obsahu jako při metodě přejmenovávání dočasných souborů (tedy soubor během přepisování není ani na okamžik považován za neexistující)
    • Řešení je odolné vůči pádu Apache apod. Pokud zůstanou nějaké odpadky v podobě lock souborů, ničemu do nevadí.
    • Probíhá dělba práce, tedy pokud jeden thread generuje soubor, ostatní na něj čekají a nepokoušejí se jej generovat také
    • Je to velmi rychlé

    #5 hvge, Nad tím jsem přemýšlel, jestli použít konstatnu nebo random v rozsahu a nakonec jsem dospěl k tomu, že random to může teoreticky zrychlit. Ale tohle je celkem prkotina :-)

    před 11 lety
  8. Honza V. #8

    WOW, tak tohle je zásadní přínos pro PHP! Díky

    před 11 lety
  9. Ivo #9

    Aka je licencia?

    před 11 lety | reagoval [12] David Grudl
  10. medden #10

    avatar

    No neviem, asi to nemáš ešte celkom vychytané, skúsil som si ten stress2 a vraj NOT PASSED…

    Array ( [ok] => 922 [notfound] => 0 [error] => 78 )
    NOT PASSED!
    Array ( [ok] => 934 [notfound] => 0 [error] => 66 )
    NOT PASSED!
    před 11 lety | reagoval [11] medden
  11. medden #11

    avatar

    #10 meddene, Ach jo, sypem si popol na hlavu, neviem čítať ;-)

    před 11 lety
  12. David Grudl http://davidgrudl.com #12

    avatar

    Aktualizace: Tak jsem nakonec úplně vyštípal flock(), kód se ještě více zjednodušil a výsledek je přenositelnější. Předpokládám že další změny už nenastanou. Je to prostě dokonalé :-)

    #9 Ivo, Můžeš používat bez omezení, jen je třeba ponechat původní copyright.

    před 11 lety
  13. Aleš Janda http://www.svice.cz #13

    avatar

    Já jsem to kdysi dělal tak, že jsem zkusil vytvořit adresář ZANEPRAZDNENO. Když se podařil vytvořit, tak hurá, zámek je můj, provedl jsem kritickou sekci a pak jsem adresář smazal.

    Když se vytvořit nepodařil, čekal jsem ve smyčce asi 5 sekund a po sekundě zkoušel.

    Chodilo to výborně, ALE: občas se stávalo, že se ten adresář nesmazal. Jestli se ta kritická sekce nějak zacyklila, vypršel timeout nebo se zrovna resetnul Apache… fakt nevím. Nestávalo se to často, ale přece.

    Proto jsem dal omezení, že pokud 5 sekund čeká na zámek a pořád nic, tak tam prostě vstoupí a hotovo. Ale to je samozřejmě řešení na nic.

    Dneska to dělám pouze přes databázi. Jednak je to mnohem čistší, rychlejší, ale hlavně je atomicita zaručena z povahy databáze.

    před 11 lety | reagoval [14] David Grudl
  14. David Grudl http://davidgrudl.com #14

    avatar

    #13 Aleši Jando, ano, s tímto je třeba také počítat. Že vlivem nečekaných okolností dojde k narušení konzistence dat. Mě se to snad podařilo vyřešit takto: Místo adresáře vytvářím soubor, jehož handle si během kritické sekce držím. Zámek pak nepředstavuje existence/neexistence souboru, ale to, jestli soubor existuje i po pokusu o smazání. Otevřený soubor totiž nelze smazat. poznámka: to byl špatný postup, už lockfiles nepoužívám

    před 11 lety | reagoval [15] mka
  15. mka #15

    #14 Davide Grudle,

    … Otevřený soubor totiž nelze smazat.

    Tohle mozna stoji za upresneni. Na UNIXech (z uzivatelskeho pohledu) smazat jde. Nazev souboru je jen polozka v adreari, ktera ukazuje na nejaky inode (proto taky jeden soubor muze existovat pod vice nazvy, pri pouziti hardlinku)

    Unlink jen „odpoji“ nazev souboru od daneho inodu. (a uzivatel si muze myslet, ze soubor smazal)
    Pokud ale uz ma nejaky proces ten soubor otevreny, muze jej dal pouzivat i po unlinku, ale nikdo uz se ten soubor nevidi a teprve po uzavreni souboru jadro (ovladac filesystemu) prohlasi data toho souboru za volne misto.

    Tohle me napise „16 chars written“:

     <?
    $testfile = 'writabledir/unlink'
    $f = fopen($testfile,"w");
    fputs($f, "test\n");
    $result = unlink($testfile);
    if (!$result) {
      echo "unlink failed";
    }
    // porad jeste muzu psat
    $len = fputs($f, "jeste jeden test");
    echo "$len chars written\n";
    fclose($f);
    ?>
    před 11 lety | reagoval [18] David Grudl
  16. mka #16

    tohle na linuxu taky projde, vypise „16 chars written“ a ve writabledir/unlock je text „test2“:

    <?
    $testfile = "writabledir/unlink";
    $f = fopen($testfile,"w");
    fputs($f, "test\n");
    $result = unlink($testfile);
    if (!$result) {
      echo "unlink failed";
    }
    clearstatcache();
    if (is_file($testfile)) {
      echo "$testfile still exists";
    }
    // nazev testovaciho souboru uz je volny,
    // klidne muzeme vytvorit novy soubor se stejnym nazvem
    $f2 = fopen($testfile,"x");
    if (!$f2) {
      echo "open new testfile failed";
    }
    fputs($f2, "test2\n");
    $len = fputs($f, "jeste jeden test");
    echo "$len chars written\n";
    fclose($f);
    fclose($f2);
    ?>
    před 11 lety
  17. spaze http://exploited.cz #17

    Z textu (kua, jsem udělal překlep texy) jsem pochopil, že více vláken = více přístupů na web. Jestli jsem to pochopil správně, tak to není pravda, více přístupů na web může probíhat i bez vláken, s více procesy. Záleží na tom, jak pracuje webserver, jestli na každý požadavek vytvoří nový proces, nebo jen vlákno apod. čili thread-safe znamená, že je v Apache možné použít třeba mpm_worker místo mpm_prefork. http://httpd.apache.org/…2.0/mpm.html.

    PHP, alespoň na unixu, resp. některý moduly PHP nejsou thread-safe vůbec.

    před 11 lety
  18. David Grudl http://davidgrudl.com #18

    avatar

    #15 mko, díky, to je hodně podnětný postřeh. Bohužel se tím bortí jeden z principů tohoto řešení. Nejvíc mě překvapuje, že jsem samozřejmě dělal testy na windows i linuxu a vyšel mi opak, tedy že unlink nesmaže otevřený soubor.

    Musím tedy kód ještě předělat…

    před 11 lety
  19. llook http://llook.wz.cz/weblog/ #19

    avatar

    V PHP jsem to nezkoušel, protože ho mám zrovna trochu rozvrtaný (právě běží make). Ale schválně jsem jeden nicnedělající skript:

    #!/bin/sh
    sleep 60

    pustil s odkazem na soubor: ./skript < ./soubor a na druhym terminálu jsem ten soubor v pohodě smazal.

    S chybovou hláškou „soubor nelze smazat, protože je zrovinka spuštěný nebo otevřený“ jsem se zatím setkal jen na Windows. Jedině, že by si to nějak PHP hlídalo v rámci svého procesu (a tím by bylo ze hry CLI/CGI :-( ).

    Jak často může nastat smetí? Pokud by to bylo málokdy (ignore_user_abort…), dalo by se testovat stáří zámku.

    Třeba pět sekund starý zámek je s největší pravděpodobností neplatný a pokud by to znamenalo krátké zdržení pro méně než promile návštěvníků, dalo by se s tím žít, ne?

    před 11 lety | reagoval [20] David Grudl
  20. David Grudl http://davidgrudl.com #20

    avatar

    #19 llooku, ad mazání souborů: ano, byla to moje hloupost. Našel jsem jiné řešení, dokonce výrazně rychlejší a jednodušší.

    před 11 lety | reagoval [21] hvge
  21. hvge http://hvge.sk #21

    #20 Davide Grudle, Tak si sa predsa len vratil k flock ? :-)

    před 11 lety
  22. Jakub Vrána http://php.vrana.cz/ #22

    Vygeneruje microtime v různých procesech spolehlivě různou hodnotu? Nedalo by se použít tempnam?

    před 11 lety | reagoval [23] David Grudl
  23. David Grudl http://davidgrudl.com #23

    avatar

    #22 Jakube Vráno, to je právě otázka. Tempnam mi nezaručuje, že soubor bude vytvořen na stejné partition. Proto používám microtime. Ale díky za tip, přidal jsem tam pojistku proti dvoum současně zapisujícím procesům.

    před 11 lety | reagoval [25] Jakub Vrána [28] Jakub Vrána
  24. Majkls #24

    Funguje to, jestliže to je vláknový server, takže s preforkem by se to mělo chovat korektně (opravte mě, jestli se pletu). Pokud to dělá i s preforkem, tak už to není problém TS, ale zamykání mezi procesy, což by mělo být už vyřešeno na úrovni glibc a podobných knihoven. Někde byl pěkný odkaz, proč nepoužívat vláknové PHP… tady: http://marc.theaimsgroup.com/?…
    Nicméně abych nedemotivoval – psát TS aplikaci je přínosné, už jen proto, že TS aplikace berou míň paměti, jsou rychlejší, atd atd…

    před 11 lety
  25. Jakub Vrána http://php.vrana.cz/ #25

    #23 Davide Grudle, Jak to, že tempnam nezaručuje vytvoření souboru na stejné partition? Pokud adresář existuje a dá se do něj zapisovat, tak ho tam vytvoří.

    před 11 lety | reagoval [26] David Grudl
  26. David Grudl http://davidgrudl.com #26

    avatar

    #25 Jakube Vráno, ano. A pokud adresář neexistuje, nebo do něj nelze zapisovat, potřebuju vrátit false případně chybovou hlášku, zatímco vytvořit jej někde jinde se mi nehodí.

    před 11 lety | reagoval [28] Jakub Vrána
  27. llook http://llook.wz.cz/weblog/ #27

    avatar

    Taky by možná stálo za to zprovoznit https://texy.info/nette/license, když se na to v docbloku odkazuješ.

    před 11 lety
  28. Jakub Vrána http://php.vrana.cz/ #28

    #26 Davide Grudle, tempnam vrátí jméno vytvořeného souboru, takže se dá zjistit, kde byl vytvořen, a případně ho zase smazat. Ale pokud řešení s microtime funguje, tak je asi lepší. Čekal jsem, že pojistka zmiňovaná v #23 David Grudl bude zohledňovat číslo procesu (což by nefungovalo u více vláken), ale ty jsi zvolil jiné řešení. Nejsem si jist, že je úplně neprůstřelné (vycházím z kódu NSafeStream 01):

    P1 a P2 se rozhodnou vytvořit stejnojmenný dočasný soubor, což se jim povede. P1 ho zamkne, P2 čeká. Když ho P1 ve stream_close zavře, tak do něj může začít zapisovat P2. A teď v závislosti na tom, jestli jde přejmenovat zamčený soubor a jestli do odstraněného souboru jde zapisovat (v závislosti na platformě), se stane každopádně něco nehezkého.

    Možná bych přeci jen použil tempnam a tím se těmhle problémům vyhnul.

    před 11 lety | reagoval [29] David Grudl
  29. David Grudl http://davidgrudl.com #29

    avatar

    #28 Jakube Vráno, Nepovede se jim vytvořit stejný dočasný soubor. Pojistka je v podobě režimu ‚x‘, ve kterém se soubor vytváří.

    před 11 lety | reagoval [30] Jakub Vrána
  30. Jakub Vrána http://php.vrana.cz/ #30

    #29 Davide Grudle, Já tam tedy vidím $this->acquireLock($tmp, $mode.$flag, LOCK_EX); a ne 'x'.$flag.

    před 11 lety | reagoval [31] David Grudl
  31. David Grudl http://davidgrudl.com #31

    avatar

    #30 Jakube Vráno, ten $mode == ‚x‘ byl ošetřen podmínku case – nicméně máš zrovna staženou verzi, kde je překlep, sorry. To získávání exkluzivity u temporary files jsem pak ostranil jako bezpředmětné.

    před 11 lety | reagoval [32] Jakub Vrána
  32. Jakub Vrána http://php.vrana.cz/ #32

    #31 Davide Grudle, Vidím, že jsi to ve verzi NSafeStream 01 potichu opravil. Doufám, že pak bude ještě nějaká finální verze, protože řada lidí teď věří tomu, že původní 01 je OK.

    před 11 lety | reagoval [33] David Grudl
  33. David Grudl http://davidgrudl.com #33

    avatar

    #32 Jakube Vráno, nikoliv potichu, jen z názvu archívu mi vypadlo písmenko (v hlavičce třídy je verze správně).

    Mimochodem, přidal jsem právě do archívu NSafeStream pro PHP4. Byl to oříšek, stále to nefungovalo ideálně. Než jsem zjistil, že usleep() v PHP4 pod Windows nefunguje…

    před 11 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.