Vývoj náhrady za současný FileJournal
- Acci
- Člen | 83
Ahoj. Díval jsem se na současnou implementaci FileJournalu v Nette a zdá se mi dost pomalá. Mám rozpracovanou implementaci založenou na B stromu, která se zdá být o dost rychlejší (například 100 insertů trvá na mém notebooku u současného FileJournalu kolem 20 sekund, u mé rozpracované implementaci kolem 1 sekundy, u získávání výsledků by to mohlo být ještě více). Je na tom ještě ovšem hodně práce (například chybí podpora pro priority či vyvažování stromu) a proto bych se vás chtěl zeptat, zda má smysl tento nový žurnál dopracovat do konečné podoby a zda by se mohl teoreticky stát náhradou současného žurnálu. Díky.
Editoval Acci (4. 12. 2010 18:29)
- David Grudl
- Nette Core | 8227
Určitě to smysl má! Klíčové je ale také to, jak se bude chovat v konkurečním prostředí. Viz https://forum.nette.org/…ti-na-sqlite?…
- Acci
- Člen | 83
Ahoj. Dost jsem na vývoji pokročil a mám první měření rychlosti současného souborového a Sqlite žurnálu a mého založeného na B+ stromech.
100 inserts
SqliteJournal: 1.64009404182 s
FileJournal: 26.8340108395 s
BtreeFileJournal: 0.143335103989 s
Clean priority
SqliteJournal: 0.0232048034668 s
FileJournal: 0.0149970054626 s
BtreeFileJournal: 0.0244851112366 s
Clean tag
SqliteJournal: 0.00389790534973 s
FileJournal: 0.0135850906372 s
BtreeFileJournal: 0.00154900550842 s
Jak jde z výsledků vidět, vývoj je na dobré cestě. Ještě mi zbývá optimalizovat mazání podle priority a otestovat jeho chování v konkurenčním prostředí. Poté bych jej uvolnil pro širší použití a v případě, že by se vám zalíbil, mohl by snad nahradit současný souborový žurnál.
- Acci
- Člen | 83
Zrovna jsem dokončil i testy v konkurenčním prostředí: Proces zapisuje 5× do žurnálu po 20ms intervalech záznam obsahující dva tagy a prioritu. Těchto procesů běželo zároveň 10 po dobu 30 sekund.
Výsledky jsou následující:
- SQLite: 7,45 požadavku za sekundu
- FileJournal: 2,65 požadavku za sekundu
- BtreeJorunal: 80,04 požadavku za sekundu
Ne, opravdu jsem se nespletl a můj žurnál podává o řád lepší výsledky než SQLite žurnál. Abych řekl pravdu, samotného mne to dost překvapilo, předpokládal jsem, že SQLite na tom bude mnohem lépe.
Mám se tedy podle vás snažit žurnál odladit do použitelné podoby nebo by to byla zbytečná práce? Díky.
- Ped
- Člen | 64
Pokud tam neni nejaky race condition nebo skryty bug, tak 10× lepsi vykon smysl urcite ma.
Dale uvazil si pri jake strategii (situaci) je B-tree nejpomalejsi a testoval jsi to na takovou situaci? (priznavam ze o B-tree z hlavy toho moc nevim, takze otazka neni rypava, ale laicka) Je sice dulezite jak se to chova pri „bezne“ zatezi, ale je dobre vedet jak moc to muze byt spatne v extremni situaci.
- srigi
- Nette Blogger | 558
Bolo by faj to otestovat na realnom zdielanom hostingu. Ked to testujes na svojej masine, I/O je exluzivne dostupne tvojmu testu. Ked aplikacku presunies na zivy server, I/O (hrabanie disku) obsluhuje desiatky inych aplikacii a tvoj benchmark ma len malu cast v I/O queue.
No a v takejto situaci je treba spravit test aspon niekolko desiatok konkurencnych requestov na aplikaciu. Ziskas tak vysledky z ako tak realnych podmienok.
- Acci
- Člen | 83
Bugy tam zatím určitě budou, ono napsat v podstatě kompletní databázi není jen tak. Proto se nyní zaměřuji na psaní několika desítek testů. Race condition by snad nikdy nastat nemělo.
Co se týká otázky: Nenapadá mne situace, kdy by mohl být B strom nějak extrémně pomalý. Je to v podstatě nejlepší možná datová struktura používaná u prakticky všech souborových systémů a databází.
- westrem
- Člen | 398
B+ stromy maju problem najme ked sa maze a mali by sa vyvazovat a upravovat podla „standardnych“ pravidiel. Co cca viem, tak toto sa casto vobec neimplementuje, alebo implementuje len ciastocne, pretoze to spomaluje, inac s B+ stromami nevidim problem, a ak to bude nakodene dobre – co podla vysledkov ocividne je, bude to super a minimalne ja som za postupne nahradenie sucasnej implementacie v Nette.
- Acci
- Člen | 83
Teoreticky taková situace nastat v nejhorším případě nastat může, ovšem v reálu je to dost nepravděpodobné. Nějakou zjednodušenou formu vyvažování ovšem v budoucnu plánuji implementovat.
Jinak jsem dokončil testy na hostingu IC s následujícími výsledky:
1000 vložení
SqliteJournal: 38.4401259422
FileJournal: 75.590485096
BtreeJorunal: 2.39867806435
Smazání 400 záznamů podle priority
SqliteJournal: 0.0956561565399
FileJournal: 0.496645927429
BtreeJorunal: 0.0224220752716
Smazání 600 záznamů podle tagu
SqliteJournal: 0.265986919403
FileJournal: 0.241114854813
BtreeJorunal: 0.0310559272766
- Acci
- Člen | 83
Tak konečně jsem se dopracoval k vydání testovací verze svého žurnálu. Je umístěn na Githubu i s celkem podrobným popisem. V průběhu zítřejšího dne se ještě pokusím dodat testy a benchmark, abyste si mohli ověřit výsledky.
Předem vám děkuji za otestování a v případě jakýchkoliv nesrovnalostí mne prosím kontaktujte.
- Acci
- Člen | 83
Přikládám ještě benchmark s kompletně hotovým žurnálem:
Init
SqliteJournal: 0.047590970993 s
FileJournal: 0.000597953796387 s
BtreeFileJournal: 0.000799894332886 s
1000 inserts
SqliteJournal: 8.93768596649 s
FileJournal: 69.2337641716 s
BtreeFileJournal: 1.148884058 s
Clean all
SqliteJournal: 0.0803961753845 s
FileJournal: 0.524681806564 s
BtreeFileJournal: 0.0179469585419 s
Same 1000 inserts again
SqliteJournal: 18.481400013 s
FileJournal: 78.605672121 s
BtreeFileJournal: 0.363624095917 s
Clean priority lower then 400
SqliteJournal: 0.0780811309814 s
FileJournal: 0.597638130188 s
BtreeFileJournal: 0.0151479244232 s
Clean one tag
SqliteJournal: 0.00354504585266 s
FileJournal: 0.00116205215454 s
BtreeFileJournal: 0.000761032104492 s
Clean 3 tags
SqliteJournal: 0.00691986083984 s
FileJournal: 0.00250887870789 s
BtreeFileJournal: 0.00107407569885 s
Clean all other tags
SqliteJournal: 0.0618770122528 s
FileJournal: 0.36325097084 s
BtreeFileJournal: 0.053032875061 s
Clean All
SqliteJournal: 0.0154650211334 s
FileJournal: 0.000211000442505 s
BtreeFileJournal: 0.0049159526825 s
- David Grudl
- Nette Core | 8227
Takže tvůj žurnál jsem zkusil nasadit do poslední vývojové větve. A děkuji ti za vývoj! (samozřejmě jsem vděčný i Jakubu Kulhanovi za starší FileJournal)
Poprosil bych ještě o drobnost: na pár místech v kódu se vyskytuje
==
resp. !=
, můžeš ho, pokud je to možné,
nahradit za ===
nebo v opačném případě doplnit komentářem
// intentionally ==
nejlépe i s vysvětlením?
A taky jsem narazil na jednu docela závažnou chybu: pokud spustím (na Windows) test a násilně ho přeruším (ve fázi bigTests, dřív to nestihnu), další spuštění končí výjimkou, protože soubor nejspíš není v konzistentním stavu.
Bylo by super, kdyby jsi mohl komity posílat už přímo do svého forku Nette.
- David Grudl
- Nette Core | 8227
Právě i s vypnutým debug módem. Ale už vím, kde je problém: chybí
tam flock($this->handle, LOCK_SH)
ve chvílích, kdy chceš
soubor číst. Soubor je tak možno číst kdykoliv, nezávisle na tom, zda je
exkluzivně uzamčen nebo ne. Na windows se to projeví tak, že se přečte
prázdný řetězec a tím pádem selže kontrola hlavičky v konstruktoru.
Můžeš tam prosím ty sdílené zámky doplnit?
Také kontrola file_exists
je zbytečná, protože v zápětí
mohl někdo soubor vytvořit, správný postup je rovnou volat
fopen($this->file, 'xb')
a pak teprve kontrolovat výsledek.
Sice nelze rozlišit, jestli se soubor nevytvořil proto, že už existuje, nebo
proto, že adresář je neplatný, ale s tím holt v PHP nelze nic dělat,
správa chyb je tu zprasená. Případně lze prvně volat ‚r+b‘ a teprve
když to selže, tak ‚wb‘.
- Acci
- Člen | 83
Udělal jsem tedy fork Nette na GitHubu, do kterého jsem přidal potřebné opravy. Že Windows načte z exkluzivně zamčeného souboru prázdný řetězec je fakt divné chování, na Mac OS i Linuxu to normálně funguje.
A k tvému druhému požadavku. Funkce file_exists je volána proto, že je rychlejší než fopen a nevidím v tom žádný problém s atomicitou. Pokud soubor neexistuje, funkce @fopen($this->handle, ‚xb‘) se jej pokusí vytvořit a pokud byl již v té době vytvořen, skončí chybou, která ovšem nevyvolá výjimku kvůli druhé kontrole pomocí funkce file_exists. Nebo se v něčem pletu?
- David Grudl
- Nette Core | 8227
Je to divné a hloupé chování, taky už jsem s tím měl nepříjemnosti.
Shared lock je každopádně potřeba používat pokaždé, když je soubor čten (ať už jedním příkazem nebo celou řadou příkazů). Jinak totiž exkluzivní zamykání nemá smysl. Pokud soubor má rozečteno 20 vláken a jedno je chce zamknout pro zápis, tak je třeba počkat, až čtenáři dočtou. Což právě zajistí použití shared locku. A naopak, pokud chci číst, tak musím počkat, než jiné vlákno dopíše, což opět provedu vytvořením sdíleného zámku. Tedy těch zámků bude potřeba v žurnálu asi přidat víc. (pokud už handle má shared lock, lze z něj udělat exclusive lock bez odemknutí).
Ad file_exists: tohle řešení není principiálně správné. Mezi jakýmkoliv file_exists a fopen se prostě může na disku něco změnit. Jistě, byla by to velká náhoda, proto se bavím o principiální stránce. Protože existuje vhodnější a rychlejší řešení (rovnou otevřít s r+b), šel bych touto cestou.
- Acci
- Člen | 83
V žurnálu se před každou operací (write nebo clean) vytváří exkluzivní zámek a tedy by v ten okamžik neměl nikdy jiný ze souboru číst nebo do něj zapisovat a soubor je odemčen až po provedení všech změn. Jediné místo, kde se četlo bez použití zámků bylo v hlavičce, protože tam na Unixu nebyl potřeba.
A jasně, mezi file_exists
a fopen
může dojít
k vytvoření souboru, ale s tím počítám a proto volání
fopen
volám se zavináčem. Ale máš pravdu, přímé volání
r+b bude lepší.
- David Grudl
- Nette Core | 8227
Acci napsal(a):
V žurnálu se před každou operací (write nebo clean) vytváří exkluzivní zámek a tedy by v ten okamžik neměl nikdy jiný ze souboru číst nebo do něj zapisovat.
Tak to právě nefunguje. Exkluzivní zámek neříká, že ze souboru nikdo nečte a nepíše, ale že nikdo nedrží sdílený zámek.
(samozřejmě může to na nějakém OS fungovat trošku jinak nebo obskurně, viz třeba ty windows, ale obecně zámky se vztahují jen k zámkům, nikoliv k reálným operacím se soubory).
- David Grudl
- Nette Core | 8227
Ještě: pokud do souboru zapisuji dvakrát související data, musím to udělat v rámci jednoho zámku, jinak může dojít k nekonzistenci. A taky soubor otevřený přes r+b může mít po získání čtecího zámku prázdný obsah, protože mohl být otevřen a uzamčen právě mezi operacemi fopen + flock(LOCK_EX) běžícími v jiném vlákně.
- David Grudl
- Nette Core | 8227
Acci napsal(a):
Ale protože do souboru zapisuje nebo z něj čte pouze FileJournal, který vždy vyžaduje exkluzivní zámek, neměly by být žádné jiné zámky potřeba.
A FileJournalu nevadí, když čte data, která mu někdo jiný přepisuje nebo je ještě nedopsal?
- David Grudl
- Nette Core | 8227
Proces A začne číst data, než je přečte všechny, proces B část z nich přepíše.
anebo proces B zapisuje data, než je zapíše všechna, proces A je nekompletní přečte.
- Acci
- Člen | 83
Proces A vytvoří exkluzivní zámek, začne číst data, proces B čeká na uvolnění zámku, proces A zapíše data a uvolní exkluzivní zámek, proces B uzamkne soubor, načte a zapíše data a nakonec uvolní zámek.
Zkoušel jsem najednou spustit několik stovek procesorů a žádný problém nenastal.
- David Grudl
- Nette Core | 8227
Už tomu rozumím, jediný okamžik, kdy proces čte a přitom nezapisuje, je v tom konstruktoru, takže čtecí zámek patří jen na to jedno místo. Ok.
- piler
- Člen | 111
gmvasek napsal(a):
Jen z tohoto ti nikdo nic nepoví. Přilož chybu z Laděnky, soubor na kterém to padá…
File: FileJournal.php
<?php
/**
* Lock file for writing and reading and delete node cache when file has changed.
* @return void
*/
private function lock()
{
if ($this->handle) {
if (!flock($this->handle, LOCK_EX)) {
throw new InvalidStateException('Cannot acquire exclusive lock on journal.');
}
if ($this->lastModTime !== NULL) {
clearstatcache();
if ($this->lastModTime < @filemtime($this->file)) { // intentionally @
$this->nodeCache = $this->dataNodeFreeSpace = array();
}
}
}
}
?>
V ladenka vyhodi vynimku: „Cannot acquire exclusive lock on journal.“
- piler
- Člen | 111
jasir napsal(a):
Přilož kompletní výpis laděnky, nejen ten error, kapišto?
Skusim to inak :).
Vsetko funguje OK na localhost. Ked som to uploadol live, tak sa prejavila chyba. Chyba vznika presne tu
<?php
if (!flock($this->handle, LOCK_EX)) {
?>
Takze je mozno potrebne nastavit na hostingu nieco, aby flock fungoval spravne. Je problem tam, alebo je problem v $this->handle
- Ondřej Mirtes
- Člen | 1536
Asi nerozumíš česky :) Nech si zobrazit Laděnku s touto chybou, dej v prohlížeči Uložit jako… a pošli nám html.
- PaBi3
- Bronze Partner | 62
Ja že je to vtip, tak čakám v napätí, čo sem pošle :-).
piler: pozri si Nette\Debug
- piler
- Člen | 111
Mam niekedy pocit, ze vy uz poriadne ani neporozmyslate nad chybou, pokial to
nie je pekne obalene v Ladenke.
Nechcem zobrazovat Ladenku, lebo obsahuje informacie o URL a ine informacie,
ktore zatial zverenovat nechcem.
Ale skusim to inak.
Vo FileJournal.php je kod:
<?php
$this->handle = fopen($this->file, 'r+b');
echo $this->handle; // Resource id #151
?>
Nasleduje funkcia flock(), ktora podla manualu vracia TRUE, alebo FALSE.
<?php
if (!flock($this->handle, LOCK_EX)) {
throw new Exception('...');
}
?>
A ta vracia FALSE. S ladenky sa neda nic viac vycitat, okrem tychto info co tu pisem. Problem nevidim so samotnym frameworkom, ale s flock() (lebo na localhoste mi funguje spravne). Su tam mozno nejake zavislosti spojene s touto funkciou, ktore sa snazim zistit.
Dakujem za porozumenie :).
- Patrik Votoček
- Člen | 2221
Tohle je nějákej blbej vtip? Co ten člověk nechápe na tom pošli laděnku?
Jde o to že když pošleš laděnku tak vidíme kompletní stacktrace (a další údaje) toho jak k chybě došlo. A ty nám při odhalování chyby pomohou. Pokud nechceš zveřejňovat něco co obsahuje laděnka tak to před jejím posláním odstraň. Tak a teď už konečně POŠLI TU LADĚNKU!
- Mikulas Dite
- Člen | 756
Zkus pustit syrový
<?php
$handle = /* test handle, nějaký foo soubor */;
var_dump(flock($handle, LOCK_EX));
?>
úplně bez nette, samotné php.
Jestli tohle selže taky, není chyba v Nette/tvojí aplikaci, ale v prostředí.