Zend Framework: proč ty dotazy trvají tak dlouho?

Posted on: Neděle, Pro 21, 2008

zend_framework_logo Vánoční prázdniny mi začaly jak jinak než prací. Loni touto dobou vrcholily přípravy uživatelských blogů pro HWMag (budiž mu země lehká), letos mám průšvih se SISem. Studijní informační systém, který připravuji pro střední školy je poměrně rozsáhlá aplikace, momentálně bohužel rozdělená do dvou částí – jedné, která stojí na balastu a druhé, která stojí na Zend Framework.

Má to ale jeden háček, balast je podstatně rychlejší. Pochází z doby, kdy jsem byl vyjukaný bastlič v PHPku, který se čerstvě seznámil se sessions a informace o přihlášeném uživateli si nepodával mezi stránkami pomocí GETu (jo, to byly časy, kdy se na serverech začala vypínat direktiva register_globals a pomalu každý skript začínal příkazem extract; faktury za papírové sáčky posílejte do konce roku).

A proč je aplikace na Zend Framework tak pomalá? Prvním důvodem je to, že používám Dojo. Javascriptový framework napojený na Zend Form je super věc, dobře se to používá, nedostatků je málo. Nepodstatným nedostatkem jsou uživatelé, kteří javascriptovou validaci nečtou, dokud na ně nevyskočí alert.  Ale některé jsou zásadní. Například pro načtení formuláře obsahujícího textbox, filtering select a submit button potřebujete načíst kromě CSS stylů Dojo theme také Dojo jako takové a dalších 48 skriptů, které si Dojo natáhne pomocí XHR (programmatic mode, v declarative je to o něco málo lepší, ale zde jsou zase nahraní lidé, kteří potřebují validní web). Nejen že skripty nejsou zpracované nějakou chytrou utilitkou, která by vyházela nadbytečné bílé znaky, ale to by ani tak nevadilo, jako ten počet. Každé načtení skriptu je realizováno pomocí nového get požadavku, jehož navázání trvá. A pokud máte systém třeba na Hostmonsteru, tak to není zrovna mrknutí oka, ale o něco déle. Na 48 souborech se to už pěkně posčítá a najednou se vám stránka načítá kvůli JavaScriptu 8 vteřin a to už uživatel začíná přemýšlet, co se mu stalo s připojením, že to jede jako dial-up.

A to už se dostávám o měsíc a půl zpátky, kdy jsem seděl na Google Developer Day a poslouchal chlapíka jménem Andrew Bowers, který zde měl přednášku Measure in Milleseconds: Performance tips for Google Web Toolkit (and AJAX in general) – odkaz vede na prezentaci. Mluvilo se zde právě o tom, že dnes není problém výpočetní výkon, ale síťová kapacita. Nic dražšího Google nekupuje… A pak se počítá každý GET a každý kB, který po síti teče. GWT jde dokonce tak daleko, že přejmenuje proměnné a pokouší se provádět nějaké optimalizace. Mluvilo se zde i o tom, že pokud na jedné stránce načítáte mnoho malých ikonek, je inteligentní z nich udělat jeden větší soubor a zobrat pomocí pozicování pozadí v CSS. Pomalu se tak dostáváme k aplikační vrstvě, která úzce spolupracuje s cache a dává dohromady obsah, který k sobě patří.

Ve chvíli, kdy probíhá renderování formu v ZF, aplikace přesně ví, jaké elementy stránka obsahuje a nemůže pro ni být problém uložit do cache jeden velký javascript, který obsahuje těch 48 malinkých. Podpora Dojo je do ZF zkrátka přidaná hlavně na krásu, časem člověk objeví, že všechno má své mouchy.

A konečně k pomalým dotazům. Na jednom českém webhostingu mi ustřihli databázi k SISu, protože “dlouhodobě způsobovala brutální zatížení serveru”. Tak pokud to bylo dlouhodobě, jsem rád, že si k těm červeným číslům někdo sedl v poslední pátek odpoledne před Vánocemi, ideální doba na to, aby člověk přepisoval SQL dotazy. Je pravda, že když něco na výpis třiceti řádků nejdřív napočítá něco kolem 10M a pak to usekne, není to úplně ideální. Ale to už je věc toho, že teď už nejsem vyjukaný PHP bastlič, ale bastlič nevyjukaný, který se nenaučil pořádně JOINovat.

Nicméně to vedlo k tomu, že jsem začal používat Zend_Db_Profiler_Firebug, což je skvělá věc, jak si pomocí pluginu z vaší aplikace pošlete do Firebugu s FirePHP profily všech proběhlých SQL dotazů. Samozřejmě jen na svojí IP :-).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
 
	final class Plugin_Database extends Zend_Controller_Plugin_Abstract
	{
 
		public function routeStartup(Zend_Controller_Request_Abstract $request)
		{
			Zend_Db_Table_Abstract::getDefaultAdapter()->Query("SET NAMES utf8");
			if ($_SERVER['REMOTE_ADDR'] == '**.**.***.***')
			{
				$profiler = new Zend_Db_Profiler_Firebug('All DB Queries');
				$profiler->setEnabled(true);
				Zend_Db_Table_Abstract::getDefaultAdapter()->setProfiler($profiler);
				Zend_Db_Table_Abstract::getDefaultAdapter()->Query("SET SQL_BIG_SELECTS=0");
			}else
			{
				Zend_Db_Table_Abstract::getDefaultAdapter()->Query("SET SQL_BIG_SELECTS=1");
			}
		}
 
	}

Do Firebugu se najednou dostanou poměrně zajímavá, přehledně zpracovaná data.

 

image

 

A mnozí z vás již vidí, kde je problém. V tomto konkrétním případě předchází každému dotazu na nějakou tabulku příkaz DESCRIBE, který leckdy trvá řádově déle než dotaz, který opravdu potřebujete spustit. Obecně platí, že každý konstruktor třídy odvozené od Zend_Db_Table_Abstract zavolá právě DESCRIBE, což nezjistíte, dokud málo čtete manuál a buď neotevřete tuto třídu nebo nepoužíváte profiler. Od ZF verze 0.9.3 (SVN r4609) tedy Zend_Db_Table_Abstract obsahuje podporu na cachování takto získaných metadat. Jak je cachovat zjistíte v manuálu v sekci 13.5.11. Caching Table Metadata. Nebo je můžete do tabulky nasázet “růčo”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected $_metadata = array(
    '<column_name>' =&gt; array(
        'SCHEMA_NAME'      =&gt; <string>,
        'TABLE_NAME'       =&gt; <string>,
        'COLUMN_NAME'      =&gt; <string>,
        'COLUMN_POSITION'  =&gt; <int>,
        'DATA_TYPE'        =&gt; <string>;,
        'DEFAULT'          =&gt; NULL|<value>,
        'NULLABLE'         =&gt; <bool>,
        'LENGTH'           =&gt; <string  - length>,
        'SCALE'            =&gt; NULL|<value>,
        'PRECISION'        =&gt; NULL|<value>,
        'UNSIGNED'         =&gt; NULL|<bool>,
        'PRIMARY'          =&gt; <bool>,
        'PRIMARY_POSITION' =&gt; <int>,
        'IDENTITY'         =&gt; <bool>,
    ),
    // additional columns...
);

To hlavní ale není jak, ale že :-)