Autor Zpráva
Clint
Profil
Zdravíčko, měl bych dotaz.
Mám v tabulce uloženo přes 100 000 emailových adres (co mail to řádek). Dále mám uložený příslušný data, abych mohl vytvořit emaily, jako je obsah emailu, odesílatel, přílohy apod.

Mám PHP script, která má za úkol vygenerovat email (pozor, každý email obsahuje unikátní klíč, aby se dotyčný mohl odhlásit, abych mohl sledovat prokliky, zobrazení emailu apod. Čili mám v proměnné $bodyMail načtený celý obsah mailu a ten následně upravuji pomocí http://simplehtmldom.sourceforge.net/. Klíč generuji z mailu příjemce a id kampaně, který proháním fci base64. Když je email hotový ukládám jej do tabulky, ze které si emaily bere phpMailer, který už načte příslušný počet mailu a odešle (používám zde více vláknový provoz, kdo bude chtít mohu mu s tím poradit).

A kde je chyba? Problém je s pamětí pokud jsem nastavil pomocí ini_set('memory_limit','1024M'); vygenerovalo se mi pouze 19 tis. mailu a skript skončil, při hodnotě ini_set('memory_limit','2048M'); se vygenerovalo už 36 tis. mailů a když jsem nastavil ini_set('memory_limit','5120M'); (doporučuji nikdy nezkoušet pokud nemáte stabilní server) se vygenerovaly všechny maily. Problém je, že jsem tímhle totálně zabil paměť a pokud bych chtěl vygenerovat více než 150 000 tak se to nevygeneruje.

Způsob jak generuji maily:
SQL dotaz na všechny maily
foreach přes maily {
generování mailu
ukládání do tabulky
}

při googlování jsem narazil na jeden ze způsobů http://www.zdrojak.cz/clanky/lazy-evaluation-v-php/, otázkou je zda je to správný postup nebo zda má někdo lepší řešení. Ideální by bylo abych při nastavení ini_set('memory_limit','1024M'); mohl generovat až 500 000, pokud je to možné.

Ještě další možný způsob je http://cs.wikipedia.org/wiki/HipHop_for_PHP ale nezkoušel jsem to. V tomhle jsem tak trochu nováček.
Alphard
Profil
Největším žroutem paměti bude SimpleHtmlDom, ujistěte se, že po dokončení zpracování jsou uvolněny veškeré prostředky, které zabral. Ideálně ho hromadně vůbec nepoužívat, nemůžete mít v šabloně na místech pro unikátní kódy nějaké zástupné znaky, které nahradíte třeba řádově šetrnějším str_replace()?
Clint
Profil
Alphard:
díky za pošťouchnutí :-) teď mě napadlo, že ten obsah mailu pokaždé parsuji přes ten SimpleHtmlDom, možná by stálo to provést jednou a někam do pole si uchovat linky u kterých budu přidávat unikátní klíče a další maily a obsahy už zpracovávat přes to pole (kde budou uložený linky) pokud bude existovat
Clint
Profil
Ještě jeden dotaz, pomocí ini_set('memory_limit','512M'); si nastavím paměť. Pokud mám 10tis. mailů hned na začátku to zabere 29MB, pokud je mailu např 50tis. zabere to paměť 55MB. Se vzrůstajícím počtem mailu roste nárok na pamět. Jak, ale zpracovat maily když dopředu nevím kolik jich bude, ale chci zachovat nastavenou paměť 512M. Třeba budu chtít zpracovat 500 000 mailů, jak to provést?

pokud použiji unset($x); tak proměnnou $x sice zničím, ale v paměti stále zabírá místo - lze pomocí php 5.3 fci nějak uvolnit paměť například pomocí Enable Garbage Collector
Alphard
Profil
Garbage Collector místo uvolní až bude mít čas. Celkově jsem překvapený, že to tak roste, myslím, že máte v aplikaci paměťovou díru a něco se neuvolňuje. Také je otázka, kolik místa zabere výsledek vrácený z databáze, netaháte nějaký objemný sloupec, který nepotřebujete?
Pro zpracování velkých sad dat můžete zavést fronty a spouštět to cronem.
Jan Tvrdík
Profil
Clint:
Úloha, tak jak ji popisujete, by měla být řešitelná s konstantní velikostí paměti. Např. není potřeba v jeden okamžik mít v paměti celou tu tabulku se 100k záznamy. Lze např. načíst vždy prvních 100 nezpracovaných záznamů, zpracovat je a označit v databázi za zpracované. Nebo lze použít unbuffered query. Množství aktuálně alokované paměti lze zjistit pomocí funkce memory_get_usage. Profilování paměti umí např. xhprof od Facebooku. Pokud ti tam vznikají cyklické reference, tak může být užitečné ručně zavolat gc_collect_cycles a vynutit si tak úklid paměti.
Amunak
Profil
1) nezvětšuj (tak extrémně) paměť, není to dobrý nápad
2) nepoužívej ten HTML parser - jak psal Alphard, mělo by stačit použít zástupné znaky a nahrazovat přes str_replace
3) poctivě uvolňujte vše, co jde uvolnit
4) pokud ty tři body nebudou stačit, vždycky můžete dávku "rozbít" na několik částí a zpracovávat ji třeba po 5000 mailech. Skript prostě nejprve zamkne tabulku mailů, nastaví ten iterátor na 1, a projde všechny maily od 1 do 5000. Pak se přesměruje na sebe, nastaví si nějaký parametr o jedno větší, a iterátor pak bude od 5000*parametr do 5000*parametr+5000. A jakmile už nebude další mail, skript odemkne tabulku mailů a skončí. Tím se vám všechna paměť uvolní jakmile dojde k přesměrování.
5) můžete si to jen nějak "předchroustat" a konkrétní mail skládat až při odesílání - nebude ho pak ani potřeba nikam ukládat (stačí si uložit ty unikátní klíče a identifikátory).
Clint
Profil
Díky všem za odpovědi. Určitě budu zpracovávat maily po částech, čili LIMIT 0, 1000, dále LIMIT 1000, 1000 .... 2000, 1000 atd. zkusím se podívat zda se dá něco vyvést s tím SimpleHtmlDom a po skončení zpracování prvních 1000 mailu odstraním proměnné a zkusím vinut úklid paměti.


Alphard:
Ten Garbage Collector, lze jen spustit v průběhu scriptu? používám toto:

ty proměnné co nechci jim nastavuji null nebo jsem zkoušel unset

gc_enable(); // Enable Garbage Collector
var_dump(gc_enabled()); // true
var_dump(gc_collect_cycles()); // # of elements cleaned up
gc_disable(); // Disable Garbage Collector

ale paměť se mi neuvolní
Amunak
Profil
Clint:
Říkám - pokud chceš "na jistotu" vyčistit paměť, prostě se po těch 1000 mailech přesměruj sám na sebe s navýšením nějakého parametru. Tím, že se ukončí skript, se vše vyčistí, a nemusíš se o garbage collector starat.
Clint
Profil
Amunak:
Tak jsem zjistil toho největšího žrouta paměti: jsou to dotazy do db, které dělám přes dibi, takže nějaký návrh, jak uvolnit paměť po vykonání dotazu? mysql_free_result mi nějak nechce zabírat
juriad
Profil
Clint:
Pokud používáš dibi, musíš používat vždy jeho metody. http://bnhelp.cz/wwwlibs/dibi-1.2/dibi.api/dibi/DibiResult.html#methodfree.
Clint
Profil
juriad:
Nejsem si jistý jak to přesně použít

$xxx = dibi::query('INSERT ....')->free();

nebo

$xxx->free();

nebo úplně jinak, vždy to skončí chybou
juriad
Profil
$xxx = dibi::query('INSERT ....');

// nějaká práce s vysledkem dotazu

// až zpracuješ celý výsledek
$xxx->free();



zkus ještě při připojování k databázi pomocí dibi přidat parametr 'unbuffered' => TRUE. Pro velké dotazy by to mělo snížit množství paměti za cenu zvýšení času a znemožnění některých operací.
Clint
Profil
juriad:
$xxx->free(); se mi nechce vůbec spustit

mám tam pole $arr = array(); ve které mám data co chci uložit do db před dibi
$firstDb = dibi::query('INSERT INTO [tabulka]', $arr);
$firstDb ->free();

tak nevím jestli to nepoužívám chybně
juriad
Profil
Clint:
Jsem to ale .*, vůbec nečtu. Pokud provádíš INSERT, tak výsledkem je počet vložených řádků. Tady free() vůbec nemá smysl.

INSERTy ti paměť nezvyšují; zvyšuje ji jen SELECT a ten tam přece máš, ne?
Jan Tvrdík
Profil
Clint:
$xxx->free(); se mi nechce vůbec spustit
A na to jsi, prosím tě, přišel jak? Projevuje se to nějak?
Clint
Profil
juriad:
Tak to nechápu mám cca 84 000 záznamů, které nějak zpracovávám. Pokud je ukládám do db zabere mi to pomalu 120MB paměti, pokud ty záznamy pouze zpracuji a neukládám, zabere mí to v paměti pouze 60 MB. Jelikož ukládám do dvou tabulek, tak mi to při 84000 záznamech sežere pomalu 500 MB paměti. Takže otázkou je co tu paměť využívá a jak to z ní odstranit.

Jan Tvrdík: script spouštím ajaxově a projevuje se to tak, že zahlásí 500 chybu

--------------------------------------------------------------------------------

teď mě napadlo, že já ty data ukládám ve foreach, možná by stálo za to, ukládání volat fci a po skončení té fce přeci zanikají data, čili se uvolní paměť, pokud dobře chápápu -> tak po zkoušce toto taky nedopadlo dobře, pořád je to v paměti :(
Amunak
Profil
Znova opakuju - neukládej do DB celý email, je to hovadina. Ulož jen ty části, které jsou unikátní. Vkládej v jednom dotazu nějaký rozumný počet dat - rozhodně nespouštěj tisíc insertů, ale mít jeden půlgigabajtový asi taky není ono. Uvolni pak obsah všech proměnných, které nepoužíváš.
Clint
Profil
Amunak:
Musím ukládat všechno, protože pak je cron, který si načte data z tabulky a začne je rozesílat. Nejvíce zabírá obsah mailu, problém je, že každý uživatel ho má trochu jiný (unikátní klíč), abych hlídal prokliky, přečtení mailu, odhlášení.

Ukládání mailů mám řešený ve smyčkách čili zjistím max. počet mailů, vydělím to 500 maily na smyčku a zjistím kolikrát budu smyčku pouštět.

Původně (první verze) mi to vzalo skoro 5G na 100 000 mailů, díky tomu, že jsem to převedl do OOP a dbám zvýšenou pozornost na proměnné co nepotřebuji jsem se dostal na 768MB (aktuální verze). Ale chci jí ještě níž, aspoň na 256 MB za např 500 000 mailů, ale jak jsem psal, tak script jako takový využívá max. 55 MB paměti, ale ten nárůst způsobuje dibi díky ukládáním dat.

Samozřejmě je možné obsah mailu neukládat do db, tím mi to klesne např. k 75MB, ale nechci generovat obsah mailu (může být personalizovaný) v cronu, který má sloužit, jako posílání mailu na mail server a žádné sestavování obsahu mailu.

proměnné které již nepotřebuji uvolňuji pomocí unset()
Amunak
Profil
Clint:
Proto jsme ti už na začátku radili, aby v tom cronu, který to zpracuje, jsi načetl jen nějakou šablonu, a pomocí nahrazovacích funkcí (i kdyby ti to nezpracovávalo PHP nebo jiný "chytrý jazyk", pořád můžeš použít třeba sed, je-li to na linuxu) tam doplnil jen ty unikátní údaje. Hodně si tím usnadníš práci, nebudeš plýtvat prostředky a zrychlíš tím generování mailů. DIBI nemůže za to, že to není napsané efektivně.

I kdybys měl velmi personalizovaný mail, prostě uložíš jen ten personalizovaný odstavec (silně pochybuji, že pro 100000 zákazníků vymyslíš i byť jen jeden unikátní odstavec pro každého). A ten se tam při odesílání doplní. Ale ukládat víc než jen to, co je nezbytně nutné, je prostě hloupé. Hlavně teď to vyřešíš pro 100000 zákazníků, a až jich bude o něco víc, budeš to řešit znovu.
Clint
Profil
Amunak:
to jsem taky změnil, obsah originálního mailu, mám uložené v proměnné a k ním přistupuji, ale jak jsem psal, ten největším problém je v dibi. Personalizovaný mail lze taky pochopit, že každý mail bude mít unikátní klíč pro hlídání prokliku, přečtení mailu, odhlášení.
Amunak
Profil
Clint:
Jestli ti ale DIBI spotřebuje paměť, pak je to tím, že máš v dotazu moc (velkých) dat - tedy že máš buď gigantický insert, anebo z DB taháš moc dat.

Personalizovaný mail lze taky pochopit, že každý mail bude mít unikátní klíč pro hlídání prokliku, přečtení mailu, odhlášení.
Ano. Takže uložíš do DB těch pár unikátních klíčů, a pak je jen doplníš do šablony mailu při odesílání.
Jan Tvrdík
Profil
Clint:
ten největším problém je v dibi
To by mě zajímalo, jak jsi na to vlastně přišel. Vzhledem k tomu, že si dibi (pokud vím) žádná z databáze načtená data v paměti nedrží, tak dost těžko může zabírat tolik paměti. Tu paměť předpokládám zabírá ta hromada načtených dat, kterou nejsi schopen dealokovat.

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm: