Mirin webspace

Nejbohatší život má ten, kdo žije s minimem nároků

12. 7. 2011 - Komentáře (11) PHP Nette

Session a Nette zapouzdření

Původně jsem chtěl zase vypustit něco na téma léto začalo a celý svět se začíná pomalu vařit. Ceny rostou - energií zejména - svět se topí v dluzích a jde do háje atd. Ale dobrá, když už je to léto, tak pro dnešek trochu odlehčím a napíši něco málo o session v PHP a trochu se otřu i o framework Nette, se kterým mám tu čest obcovat v práci dost často. O Zendu už toho moc asi ani do budoucna nenapíšu, protože už téměř rok nemám čas se mu věnovat.

Co je session a k čemu je dobrá je jasné - v podstatě jde o zachování nějakých dat a tím i stavu aplikace mezi jednotlivými HTTP požadavky. V PHP je už dnes session prakticky výhradně obsluhována přes superglobal pole $_SESSION. Používat něco takového přímo v objektovém kódu aplikace je ale superprasárna, takže frameworky nabízejí různá objektová zapouzdření - o tom za chvíli. Většinou tedy stačí vědět o této proměnné a pár funkcích pro startování session, její destrukci atd. To pro tvorbu aplikace většinou stačí. Časem ale narazíte na pár zádrhelů.

Implicitní session handler

Ukládání a načítání session dat v PHP zajišťuje implicitní session handler, který pracuje se soubory filesystému. Ten má implementovánu jednu věc, která je poměrně zásadní ale moc se nezmiňuje - zamykání právě probíhající session. Požadavek, který přistoupí k session si tuto zamkne pro zápis, takže všechny ostatní požadavky, které by do této session také chtěli něco zapsat, čekají. Jinými slovy zařadí se hezky za sebe - serializují se. To je velmi důležité právě když se vyskytnou dva alespoň z části paralelní HTTP požadavky, kteří chtějí pracovat s jednou a tou samou session. Pokud by tam nebyl výše uvedený mechanizmus zamykání přítomen, může dojít k přepsání a ztrátě dat v session, což může být za určitých okolností dost závažné. Tohle si vývojář většinou hned neuvědomí, protože při vlastním vývoji se s tím většinou nesetká. Ono i v reálném provozu k takovému paralelnímu průběhu dvou požadavků dochází zřídka, většinou při silné zátěži. Dnešní obliba ajaxu a různých jiných asynchronních vymyšleností také přispívá k větší pravděpodobnosti výskytu těchto případů. O to horší je pak následné chyby odhalit.

Vlastní session handler

Z různých důvodů se můžete rozhodnout, že budete implementovat vlastní session handler. PHP vám to umožňuje přes registraci callbacků pomocí funkce session_set_save_handler. Většinou vytvoříte nějaké objektové řešení s nějakou třídou a metodami, které budou fungovat jako callbacky při otevření, zápisu a uzavření session. Pozor si musíte dát zejména

  • na volaní callbacků pro write a close při destrukci objektů
  • na pořadí volání callbacků v různých situacích, např. záludné může být regenerování session
  • zajistit serializaci požadavků
  • činnost session handleru je dost na "okraji" userspace PHP, takže vypisování něčeho na výstup většinou moc nepomáhá, je nutné velmi pečlivě logovat a možnost podrobnějšího logování mít k dispozici i při ostrém provozu.

S prvními dvěma body pomůže PHP manuál pro funkci session_set_save_handler ale serializace požadavků je na vás. U databázového handleru se zajišťuje zamykání většinou pomocí tzv. "read locks" - SELECT FOR UPDATE. Moc informací o této problematice jsem na Internetu nenašel, jedna drobná zmínka je na mysql performance blogu.

S tím souvisí nutnost mít v session handleru transakce a můžete si být téměř jisti (možná prvním) nepříjemným setkáním s deadlocky na transakcích v databázi. Hledání příčiny deadlocku je obecně docela problém, v případě PHP session handleru to platí dvojnásob.

Nette a session

Snad každý framework má nějaký objektový wrapper pro práci se session. Nette není vyjímka. Jak už jsem předeslal, použití superglobální proměnné je něco, co by se ve slušné objektové web aplikace nemělo vyskytnout. V Nette proto existují objekty Sesssion a SessionNamespace, což je takový bych řekl standard. SessionNamespace jakási oblastí působnosti, které pojmenuje a vyhradí část session pro práci s určitou specifickou částí aplikace (logování, košík v eschopu atd.).

Bohužel je v souvislosti se session v Nette pár poměrně závažných a skrytých věcí, které dost (i nepříjemně) překvapí. Pojedu od méně důležitého.

Sdílení session namespace

Obsah session namespace se sdílí a okamžitě propisuje. To znamená, že pokud použijete na dvou místech session namespace stejného jména, na každém místě pod vlastní objektovou instancí, změna jakékoli proměnné v jednom objektu session namespace se projeví i ve všech ostatních instancích namespace téhož jména. Takto to je jistě správně, jiná možnost ani být nemůže. Ale vývojářům toto chování nedochází a z nepochopení zavádějí velmi často statické instance session namespece objektů. Přitom to je zásadní vlastnost, která by měla být v dokumentaci zdůrazněna.

Startování session

V Nette je metoda Session::start(), která při opakovaném volání nedělá nic. To trochu překvapí, pokud jste zvyklí že PHP fce. session_start() vyhazuje notice. Já bych byl dokonce pro ještě jasnější OO API a vyhazoval výjimku. Když je k dispozici metoda Session::isStarted() může si to každý pohlídat krásně sám. Tohle byla pořád spíš kosmetika, přijdou horší věci.

Nemožnost použít PHP pro start

Tohle je už závažnější. Framework se nadřazuje nad ostatní a pokouší se uživateli vnutit svou představu o fungování a jakýkoli předešlý pokus o nastartování session tradičními prostředky PHP (session_start(), php.ini autostart) trestá. Tohle tak v podstatě zasahuje do samotného PHP a co více, je to velmi nepříjemná komplikace při integraci různých aplikačních částí, které o nějakém Nette nemusejí mít vůbec tušení. Když už takto, tak přes konfigurační volbu a jako volitelná věc.

Tupé startování vždy

Tohle mě už tedy dostalo. Framework automaticky pokaždé startuje session. Chudáci Ti, co se snaží o nějaký lazy přístup a šetřit databázi vlastního session handleru. Řekl bych, že v aplikaci se najde vždy spousta stránek, které žádnou session nepotřebují, např. rss feedy, různé XML výstupy atd. Všude Nette session nastartuje a vesele vám zatěžuje CPU serveru session managementem. Tohle měla být jasná volba pro konfigurační direktivu implicitně vypnutou.

Ne všechny uvedené věci jsem testoval, většinou jsem tak usuzoval ze zdrojových kódů a to na staré verzi 0.9 (k nové se v práci asi nikdy nepropracujeme), takže to může být i jinak. Nechci tím říci, že Nette je špatný framework, ani dříve jsem to netvrdil, ale např. u výše zmíněné session se vyskytuje až moc snahy "napravovat" a měnit chování PHP, někdy i skrytě, což nemám rád, protože to komplikuje integraci a stěžuje přechod mezi frameworky a nástroji.


Komentáře (11)

  1. David Grudl - 12. 7. 2011 21:08

    Díky za článek o Nette. Pokusím se uvést některé věci na pravou míru.

    1) Sdílení session namespace

    Nenapadlo by mě, že by uživatel pod stejně pojmenovanou namespace/sekcí očekával cokoliv jiného, sám říkáš, že jiná možnost ani být nemůže. Pokud ale máš z praxe zkušenost, že to vývojářům nedochází, budu rád, když to do dokumentace doplníš (je otevřená). (ps. mám vždycky takový zvláštní pocit, když si člověk, který umí napsat čtivý článek, na něco v dlouhém odstavci postěžuje, byť by to mohl jednou větou vyřešit)

    2) Nemožnost použít PHP pro start

    Důvod? Bezpečnost. Cokoliv jiného by představovalo potencionální díru do PHP aplikace. Teprve od PHP 5.3 se direktivy session nastavují na bezpečné hodnoty, ale ani na to se nedá spoléhat, každý hosting to může mít nastavené jinak. Bezpečnost je klíčovou vlastností frameworku a tohle s ní souvisí.

    3) Tupé startování vždy

    Ačkoliv jsem se s tímto názorem už setkal vícekrát, vůbec se nezakládá na pravdě. Nette naopak používá lazy přístup, po posledních commitech od Dundee jde až na hranice možností. Může klamat tento řádek kódu, který jsem už dříve raději doplnil komentářem, tj. že se otevírá *již nastartovaná* session a tedy rozhodně nestartuje nová.


    Ale pochopitelně Nette hodně napravuje chování PHP, to je fakt, se kterým netřeba polemizovat. Oproti Zendu, který je spíš knihovnou tříd, jde v tomto mnohem dál.

  2. koubel - 12. 7. 2011 22:47

    ad přispívání do dokumentace - jak jsem psal, používáme nette 0.9 a navždy asi budeme (to mj. vypovídá o kvalitě vývoje) takže se mi do aktuální dokumentace pro 2.0 moc přispívat nechce, ale zkusím někam vrazit nějaký komentář.

    - Nemožnost použít PHP pro start
    Bezpečnost ano, ale pokud vynucené bezpečnostní opatření zabraňuje kontrolovanému a bezpečnému použití prověřených kódů třetích stran tak je takové opatření špatné a framework tak místo přínosu a ušetření práce naopak přináší komplikace. A přesně toto Nette dělá když násilně brání použít fce. session_start() bez možnosti takové chování změnit, ačkoli vývojář přesně ví, proč to dělá a že je to bezpečné.

    - Tupé startování vždy
    Koukám, že v Nette 2.0 už to vypadá o dost jinak než v 0.9 ale pořád mi asi něco uniká. Ten odkazovaný kus kódu podle mne udělá to, že pokud existuje session cookie tak při každém požadavku na aplikaci zavolá session_start() a to je přesně to, co nechci. Přítomnost session cookie přece neznamená, že chci něco se session dělat např. na stránce generující rss. Jak je tedy zajištěno to, že se session_start() zavolá až když něco do session zapisuji? Viz. také http://forum.nette.org/cs/5690-deaktivace-automatickeho-startovani-nette-session

  3. David Grudl - 13. 7. 2011 09:25

    - Nemožnost použít PHP pro start

    Asi by bylo potřeba zmínit konkrétní případ, jinak nevidím moc důvod, proč by si kód třetích stran (tj. knihovny, které jsou principiálně podřazené frameworku), měly zapínat session.

    - Tupé startování vždy

    Stejně tak to vypadá v 0.9. A funguje to jak píšeš, pokud existuje session cookie (tj. session již byla nastartována), tak se její obsah zpřístupní. Děje se tak při startu Nette\Application, aby se předešlo chybám, kdy komponenta v polovině stránky potřebuje číst ze session, ale v tu chvíli už ji nelze otevřít.

    Samozřejmě v určitých situacích se session soubor otevírá zbytečně, ale jelikož jde o 1 z 50 nebo ze 100 otevřených souborů při požadavku, vliv na výkon je neměřitelný.

  4. koubel - 13. 7. 2011 11:07

    - nemožnost použít php pro start - je to případ API jednoho diskusního fóra. Na každém místě v aplikaci kde se API použije musím pamatovat na to, že nejdříve musím nastartovat nette session a až pak použít API. Nakonec jsme museli udělat nette wrapper, což je zbytečná práce a zpomalení aplikace. A ty hodiny strávené hledáním chyby, která je vlastně chybou frameworku a ne naší stály hodně peněz. Nedejbože aby se vyskytla knihovna třetí strany používající stejné "bezpečnostní" opatření jako Nette.

    - startování vždy
    Předcházení chybám je nesmysl jak vrata, opět nám házíte klacky pod nohy. U toho minima komponent si to pohlídáme ručně a na rozdíl od předchozího případu se chyba jednoduše odhalí. S výkonem je to přesně naopak. Máme vlastní db session handler, tam je KAŽDÉ session_start() znát, protože vytváří nové spojení do db. I taková stránka "page not found" generovaná nette bude inicializovat session, a takových stránek máme v aplikaci spousta a přístupy na ně rozhodně nejsou ojedinělé. Pěkně děkujeme za přetíženou session db.

    Místo toho aby jste trpělivě uživatelům vysvětloval, jak efektivně využívat session a jak mají postupovat při tvorbě komponent, které ji používají a patřičně se to zdůraznilo v dokumentaci a tutoriálech, tak si tam plácnete raději session_start() v podstatě pokaždé, jasně tím popíráte tvrzení o efektivitě frameworku.

  5. David Grudl - 13. 7. 2011 12:41

    Takže ty knihovny třetích stran plní error-logy zprávou "Notice: A session had already been started - ignoring session_start()" - nebo jakým způsobem koexistují, když každý natvrdo volá session_start()?

    ad startování vždy: u nejpoužívanějšího souborového úložiště to vůbec znát není. Ale souhlasím, že u jiného (často nevhodně zvoleného) úložiště to může být problém. V takovém případě se dá otevřít vlákno na fóru nebo Githubu.

  6. David Grudl - 13. 7. 2011 13:22

    ps. třída Session by se dala upravit tak, aby akceptovala existující session_start(), budou-li splněny _bezpečnostní_ (nikoliv "bezpečností", bezpečnost není něco, co by se mohlo bagatelizovat) požadavky. Dal jsem to do gitu.

    Nicméně stále by mě zajímalo, jak řešíš vícenásobné volání session_start() při použití několika podobných API.

  7. Filip Procházka - 13. 7. 2011 14:11

    Bylo by řešení, navěsit do boostrap spouštění session pomocí callbacku onStartup na Application? Bylo by pak možné startování vypnout bez zásahu do frameworku a vepsat ho pouze tam, kde je potřeba.

  8. koubel - 13. 7. 2011 15:21

    [5] - nic neplní, asi kontrolují, zde je session již inicializovaná nebo použijí @session_start? Nevím, ale to je vcelku fuk, jednoduše fungují i s nastartovanou Nette session, kdežto Nette s jejich nastartovanou session odmítne fungovat.

    [7] ten callback se session_start tam přece Nette dává samo od sebe, viz. ten ten zmíněný řádek v [1], jestli to jde nějak konfiguračně potlačit, tak je to v pořádku a není se o čem bavit.

  9. Filip Procházka - 13. 7. 2011 15:27

    Jde překrýt služba Application, aby to nespouštěla implicitně. Ale mě šlo o to, to dát do bootstrap, odkud bude výrazně jednodušší to odstranit. Takto je potřeba registrovat novou továrničku na službu, duplikovat obsah,... atd.

  10. koubel - 13. 7. 2011 17:12

    [9] tomu moc nerozumím, o bootstrap se přece nestará Nette ani není jeho součástí. Ledaže by si každý, kdo nechce využít implicitní Application onStartup, v bootstrapu smazal všechny onStartup eventy Application, ale nevím jestli to jde. Nicméně i to je jen obcházení problému.

  11. koubel - 14. 7. 2011 18:34

    takže ano, jestli jsem pochopil správně, co chtěl Filip Procházka říci, tak v bootstrapech by bylo něco jako

    $container->application->onStartup = array();
    $container->application->run();
    
    To vypadá na funkční a pěkný workaround, ale je to jen workaround, navíc kdyby Nette začlo implicitně do onStartup dávat i nějaké eventy, které by se hodily, mám problém, resp. musím kopírovat kód, pokud si toho vůbec všimnu.

    Jak jsem řekl, vyhodit implicitní session_start() z frameworku je podle mě lepší. Každý, kdo chce staré chování může jednoduše udělat
    $container->application->onStartup[] = function ...
    čemuž nebude vadit ani případné pozdější přidávání onStartup funkcionality frameworkem.

Komentáře jsou uzavřeny.