PHP: Výsledky srovnání funkcí file_(get)put_contents & f(read/write/flush)

Ok, tak už to asi mám, ale není to úplně ideální občas tam mám chybu "atomicity failed" (to by mohlo nastat pokud čeká moc dlouho). Občas je tam chyba zápisu do souboru.

Celý kód

clearstatcache();
$_DEBUG_ = false;

echo "Atomicity tester.".time()."
";
die;

while ( time()<1570950820 )
 {
 usleep(500);
 }

function atomicFuse($n, $disableDelay = false){
  $start = false;
  if ( !file_exists("$n.t") )
   $start = mkdir("$n.t");
  if ( !$disableDelay ){
    if ( $start == false )
     {
     usleep($n*3);
     echo ($n*3)."
";
     }
    }
  return $start;
}
function test($n, $p, $_DEBUG_){
  $fp = null;
  $sname = "$n";    // source
  $tname = "$n.txt";// target
  echo "<h4>$n at ".time()."</h4>";
  for ($i = 0; $i<2; $i++ ){
    $start_time = microtime(true);
      {
      $start = atomicFuse($n);
      // If n*16 used then 6 time repeat:
      if (!$start) $start = atomicFuse($n);
      if (!$start) $start = atomicFuse($n);
      if (!$start) $start = atomicFuse($n);
      if (!$start) $start = atomicFuse($n);
      if (!$start) $start = atomicFuse($n, false);
      if (!$start) echo "Atomicity failed. ";
      if ( $start )
         {
         /////////////////////////////
         // CHECK FILESIZE VALIDITY //
         /////////////////////////////
         clearstatcache(); // needed for filesize and touch   
         $st = stat("$sname");
         $original_size = $st['size'];
         if ( $_DEBUG_ )
           echo "; 1) prevAccess by ".$st['mtime']." fsize ".$st['size']."; ";
         $fsize = filesize($sname);
         if ( $original_size <> $fsize )
           die("; fsize total FAILTURE; ");
         if ($fsize === 0)
          echo "! The fsize is 0: stat(): ".$st['size']." ;";   
         ///////////////////
         // OPEN THE FILE //
         ///////////////////
         $fp = fopen($sname, "r");
         $s = fread($fp, $fsize );
         $success = fclose($fp);
         if ( $success === false  )
           die("; fclose failed; ");
         // 10 - data načtená , $p - prohlížeč
         if ( $success )
           {
           $result = touch("$sname",strlen($s),$p);
           if ( $_DEBUG_ )
              echo "; TOUCH: $result;";
           }
         else
           die("fclose FAIL.");
         if ( strlen($s)<60 )
            echo "*$s LENGTH:".strlen($s)."
";
         } 
      }
    if ( $start )
      {
      clearstatcache();
      $st = stat("$tname");                               
      if ( $_DEBUG_ )
        echo "; 2) prevAccess by ".$st['mtime']." fsize is ".$fsize."; ";
 
      // WRITE OPERATION WITH LOC_EX
      $fp = fopen($tname, "w");
      if ( true ) {  // acquire an exclusive lock
          $success = fwrite($fp, $s);
          if ( $success === false)
            echo "; w FAILED;";
          else
            if ( $_DEBUG_ )
                  echo " $success B written; ";
          $success = fflush($fp);// flush output before releasing the lock
          if ( $success === false )
            echo "; flush FAILED; ";
          if ( $success === false )
            echo "; release FAILED; ";
          $success = fclose($fp);
          if ( $success === false )
            echo "; fclose FAILED; ";
          clearstatcache(); // needed for filesize and touch
          $fsize = filesize($tname);
          if ($original_size>$fsize)
              {
              echo "; WRITE FAILED, restoring;";
              $original_fname = "$n";
              $result = copy($original_fname, $tname);
              if ($result == false )
                die(" TOTAL FAILTURE: copy failed.");
              else
                echo " RESTORED;";
              }
          else
            {
              if ($fsize === 0)
               echo "! THE FILE WAS NOT WRITTEN: data length: ".strlen($s)." fsize: $fsize RESOURCE: $fp
";   
              if ( $success )
                  touch("$tname",$fsize,$p);
            }
          } else {
              echo "Couldn't get the lock!";
             }
      } // start
     else
       echo "skipped";
     $success = rmdir("$n.t"); // remove atomic fuse
       if ( $success )
         echo "<h4>DIR REMOVED</h4>";
       else
         echo "<h4>DIR NOT REMOVED</h4>";
     $time_elapsed_secs = microtime(true) - $start_time;
     if ( $time_elapsed_secs === 0 )
       echo " FAILED ";
     echo "time: $time_elapsed_secs s
";
  } // for
}

switch ( $_SERVER['HTTP_USER_AGENT'] ):
  // FF 1:
  case "":
    $p = 1; break;
  // Chrome:
  case "":
    $p = 2; break;
  // OPERA:
  case "": 
    $p = 3; break;
endswitch;

copy("523","523.txt");
copy("948","948.txt");
copy("1371","1371.txt");
copy("1913","1913.txt");
copy("2701","2701.txt");
copy("4495","4495.txt");
copy("6758","6758.txt");

test("523",$p,$_DEBUG_);
test("948",$p,$_DEBUG_);
test("1371",$p,$_DEBUG_);
test("1913",$p,$_DEBUG_);
test("2701",$p,$_DEBUG_);
test("4495",$p,$_DEBUG_);
test("6758",$p,$_DEBUG_);
die;


tecka

  • ***
  • 138
    • Zobrazit profil
    • E-mail
Tak jsme se zasmáli. A co teď?

Už to jede zatím test jen se dvěma cykly.

Chyba byla rmdir v nesprávné větvi, delay byl trochu malý a je třeba víckrát opakovat test existence souboru. Tady se může opakovat až 15x. Problém je totiž ve velkém rozsahu hodnot (prodlev), které mohou nastat. Původně jsem to totiž nastavoval na průměry a nepodíval jsem se do skutečných hodnot. U půl megabajtového souboru jdou minima a maxima od 0,001 až na 0,2s.

Kód: [Vybrat]
function atomicFuse($n, $c, $disableDelay = false){
  $start = false;
  if ( !file_exists("$n.t") )
   $start = mkdir("$n.t");
  if ( !$disableDelay ){
    if ( $start == false )
     {
     $n = $n*30;
     switch($c):      // Delay example increase:
       case 0: break; // 0,01569 total
       case 1: break; // 0,03138 total
       case 2: $n = $n*2; break; // 0,06276 total
       case 3: $n = $n*4; break; // 0,12552 total
       // case 4: You need at least *6 or *8 to get out of problems with extrem times
       case 4: $n = $n*6; break; // 0,21966 t.(upper limit)
       case 5: $n = $n*6; break; // 0,3138 total extrem
       case 6: $n = $n*4; break; // 0,36087  total extrem
       case 7: $n = $n*4; break; // 0,42363 total extrem
       default: break;
     endswitch;
     usleep($n);
     echo ($n)."<br>";
     }
    }
  return $start;
}

implementace.
Kód: [Vybrat]
      $start = atomicFuse($n,0);
      if (!$start) $start = atomicFuse($n,1);
      if (!$start) $start = atomicFuse($n,2);
      if (!$start) $start = atomicFuse($n,3);
      if (!$start) $start = atomicFuse($n,4);
      if (!$start) $start = atomicFuse($n,5);
      if (!$start) $start = atomicFuse($n,6);
      if (!$start) $start = atomicFuse($n,7);
      if (!$start) $start = atomicFuse($n, false);
      if (!$start) echo "<b>Atomicity failed.</b> ";
      if ( $start )
         {
         echo "<b>Atomicity OK.</b> ";
         // perform atomic action
         $success = rmdir("$n.t"); // remove atomic fuse
         }

Zatím testuju se smyčkou 10 cyklů.

Dám příklad ~500kB s aktuálními chybama:
CHROME OK - první dva požadavky na soubor jsou za 0.003s a 0.047s
FF1 OK - za 0,2196s
FF2 OK - 0,45501s
OPERA hned na jednou začátku selže: protože má čas 0,45501 ...

Vysvětlení chyby: Ten požadavek z Opery se začne vyhodnocovat později např. při 0,45500s a v té době se požadavek od FF2 ještě vyřizuje. Takže řešením je jedině zvýšit počet požadavků na čekání nebo prodloužit prodlevu.

Konec testů.

Shrnutí
Hlavním faktorem úspěšnosti testu je délka smyčky neboli počet cyklů. Pokud je počet cyklů větší než 6, tak začne vykazovat pár chyb. Nad deset je chybovost vyšší a s dalšími desítkami se zvyšuje. Nejde zde o velikost souboru, i velký soubor např 6MB se zpracuje rychle. Problém je v tom, že se v jednom okamžiku očekává mnoho úloh. Například skript jedna zpracovává 6 požadavků a druhý skript musí čekat. Chyby se objevují na začátku. Při posledním testu s 50 cykly jsem dostal následující chybovost:

požadavek z Chrome: 6 neprovedených úkonů (4x 523kB a 2x 948kB)
požadavek z FF1: 5 neprovedených úkonů (prvních 5 souborů ~523kB)
požadavek z Opery: 0 neprovedených úkonů (100% OK)
požadavek z FF2:   0 neprovedených úkonů (100% OK)

Aktuální funkce:

Kód: [Vybrat]
function atomicFuse($n, $c, $disableDelay = false){
  $start = false;
  if ( !file_exists("$n.t") )
   $start = mkdir("$n.t");
  if ( !$disableDelay ){
    if ( $start == false )
     {
     $n = $n*30;
     switch($c):      // Delay example increase:
       case 0: break; // 0,01569 total
       case 1: break; // 0,03138 total
       case 2: $n = $n*2; break; // 0,06276 total
       case 3: $n = $n*4; break; // 0,12552 total
       // case 4: You need at least *6 or *8 to get out of problems with extrem times
       case 4: $n = $n*8; break; // 0,25104 t.(upper limit)
       // In case of heavy traffic:
       case 5: $n = $n*8; break; // 0,36087 total extrem
       case 6: $n = $n*10; break; // 0,51777 total extrem
       case 7: $n = $n*20; break; // 1,03554 total extrem
       default: $n = $n*8; break;
     endswitch;
     usleep($n);
     echo ($n)."<br>";
     }
    }
  return $start;
}


Můžeš mi prosím napovědět k čemu ten výsledek testu je? O čem ty čísla svědčí ve vztahu k tvému úvodnímu příspěvku?

Ahoj ještě nevím. Data budu za chvíli zpracovávat. Mám na to několik tabulek a grafů. Podle toho pak poznám o čem to svědčí. Co si myslím teď se ještě může změnit. Jisté je jen to, že ty prodlevy, které tam jsou, jsou závislé na tom typu testu - php skript se smyčkou. Kdyby si podnikl jiný druh "útoku" na server například smyčkou v JS odeslal požadavky post() AJAXEM a já bych každý jednotlivý požadavek zpracoval jako načtení a zápis, tak by se to zcela určitě chovalo úplně jinak. Myslím, že tento test co jsem prováděl byl dost vytěžující, ale jinak než běžné přístupy nebo než DOS útoky. Každopádně jsem ti vděčný za poznání druhé metody, která se chová trochu jinak, obě metody mají něco do sebe. Tento test jsem pojmenoval jako T8. Dokonalé to asi nebude nikdy.
« Poslední změna: 13. 10. 2019, 15:36:13 od exkalibr »

Kit

  • *****
  • 704
    • Zobrazit profil
    • E-mail
Můžeš mi prosím napovědět k čemu ten výsledek testu je? O čem ty čísla svědčí ve vztahu k tvému úvodnímu příspěvku?

K čemu je procházení slepými uličkami? K ověření, že jsou slepé. Nejspíš to nikde neuplatní, ale během těch testů se toho dost naučí o chování souborových systémů a v budoucnu nebude vyjevený z nějakých kolizí.

Graf s T8, atomicity tester with directoryFuse.


Selhání

Ještě bych dodal, že u T5-T7 šlo o jiná selhání než u T8. Selhání T8 se týkají atomicity - akce není provedena vůbec. T5-T7 tam jde o problém se zápisem dat - po zapsání fwrite není správná velikost dat, ačkoliv byl použít flock.

Kit

  • *****
  • 704
    • Zobrazit profil
    • E-mail
Kód: [Vybrat]
function atomicFuse($n, $c, $disableDelay = false){
  $start = false;
  if ( !file_exists("$n.t") )
   $start = mkdir("$n.t");
   ...

Zkus ten mkdir použít takhle:
Kód: [Vybrat]
if (@mkdir("$n.t")) {
    // akce
    rmdir("$n.t");
}
Vyloučíš tím kolizi.

@exkalibr: Tak teď jsem se v tom tvém testování úplně ztratil. Jak můžeš do toho grafu zahrnout test "mkdir"? Vždyť to je zase nový test, který je napsaný zase trochu jinak a nechápu jeho spojení s těmi předchozími testy. Už před tím když jsi psal "V posledním testu se to zdálo jako překážka, tak jsem to usleep v T7 odstranil" jsem nechápal jak můžeš takový test do grafu výsledků zahrnout, přece když odstraníš prodlevu, tak tím ovlivníš i výsledný počet sekund, které se pak objevily v tom grafu. Asi jsem na grafy už moc starý :)

Zatím jsem si z tvého grafu odnesl jen to, že na nějakém (nevím jakém) filesystému se určité I/O příkazy v PHP buferují. Myslím, že u jiného filesystému se ale buferovat nemusí.

Mně by dávalo smysl udělat test, který by obsahoval několik funkcí a každá by používala jiný způsob zamykání (mkdir, rename, flock, semafory, ...) a vyhodnotil bych jejich rychlost. Ten nejrychlejší bych pak používal. Pokud by bylo zamykání dobře udělané, tak by si 1. funkce zamkla soubor a ostaní funkce (třeba i z jiných procesů) by čekaly a jediná chyba, která by se mohla objevit by bylo překročení "maximum execution time" (u PHP defaultně 30 sekund).

Máš pravdu, musíš si prostě v hlavě odečíst 50 mikrosekund u každé hodnoty :(

Jenže těch 50 mikrosekund nemá na výsledek grafu žádný vliv. My se pohybujeme v řádů setin až desetin. To se zaokrouhlí a průměry se nezmění. Vliv to má na chování programu. Já si už u T7 všiml, že když odstraním delay je to spolehlivější než když ten čas navyšuju. Ta prodleva je zdrojem problémů u T8. Proto bych v praxi raději volil metodiku T7, která je dvakrát rychlejší. V praxi stejně ke kolizi dojde málokdy a když k ní dojde tak ověřením ji mohu obnovit ze zálohy.

Pro mě výsledek testu znamená, že registraci nemusím dělat do db. Nemusím dokonce dělat ani vzkazník do db. Co ale musím udělat do db je anketa, kde budou uživatelé klikat pomocí AJAXu, a tam se obávám, že by ke kolizím mohlo snadno dojít (zápis do toho samého souboru).

Rename jsem ale zavrhl kvůli tomu, že nadělá strašnou paseku, pokud operace selže. Kdo to pak má přejmenovávat všechny ty soubory?
« Poslední změna: 13. 10. 2019, 19:14:07 od exkalibr »

Ta prodleva je zdrojem problémů u T8. ... V praxi stejně ke kolizi dojde málokdy a když k ní dojde tak ověřením ji mohu obnovit ze zálohy.
Jestli se bavíme o situaci kdy máš soubor zamknutý, tak dokážeš ty "problémy" a "kolize" ke kterým dojde málokdy nějak jednoduše pojmenovat? Z toho co jsi psal před tím jsem to nepochopil. Jediný "problém" co mě napadá by měl být ten "maximum execution time" o kterém jsem psal. A pokud ti dochází k jiným kolizím při zamknutém souboru, tak bych řekl, že to zamykání máš špatně, např. kvůli tomu file_exists o kterém psal Kid.