Zrádné počty zobrazení příspěvku

K napsání tohoto příspěvku mě inspiroval blogpost “Admin-ajax.php zpomaluje stránky”, který správně uvádí proč byl daný web pomalý a jak byl problém, aspoň částečně, odstraněn. Proč je problém odstraněn jen částečně rozvádím ke konci tohoto příspěvku.

Vývojář se čas od času setká s poždavkem na zobrazování počtu zobrazení příspěvku na frontentu. Ovšem taková feature je celkem zrádná. V čem je problém?

Na otázku kam s daty si většinou vývojář celkem rychle odpoví, že přeci logicky patří do post_meta.

Tím velmi jednoduše získá místo, kam své údaje může ukládat. Pokud navíc nechce skladovat žádné další údaje o tom, kdo kdy odkud a jak se na web podíval, bude se jednat jen o číslo a takové číslo moc místa v databázi nezabírá. Jednoduché.

První nástřel fungování může vypadat takto:

//Uložíme shlédnutí do databáze
add_action( 'wp_head', function() {
    if ( is_single() ) {
        //získáme uložená data
        $pageviews = get_post_meta( get_the_ID(), 'myawesome_pageview', true );
        //zvětšíme o jedno
        $pageviews = intval( $pageviews ) + 1;
        //znovu uložíme
        update_post_meta( get_the_ID(), 'myawesome_pageview', $pageviews );
    }
}, 10, 0 );

//funkce pro zobrazování počtu shlédnutí
add_filter( 'the_content', function( $content ) {
    if ( ! is_single() ) {
        return $content;
    }
    $content .=  "\nZobrazeno: " . get_post_meta( get_the_ID(), 'myawesome_pageview', true ) 'x';
}, 10, 0 );

Těhle pár řádků kódu dělá přesně to, co vývojář řekl. Ukládá a zobrazuje počty načtení příspěvku.

Jak na page level cache

Pokud ovšem web, na kterém je takový kód přítomný, používá nějaký cachovací plugin, nebude nepřihlášenému uživateli (tedy většině) kód fungovat, jelikož se akce wp_head nespustí, nic se neuloží a zobrazovat se bude stále stejný počet zobrazení uložený spolu s celou stránkou ve statickém HTML.

Ovšem i tento problém lze celkem jednoduše vyřešit pomocí AJAXu:

<?php
add_action( 'wp_ajax_add_pageview', 'myawesome_pageview_add_pageview' );
add_action( 'wp_ajax_nopriv_add_pageview', 'myawesome_pageview_add_pageview' );

//Uložíme shlédnutí do databáze
function myawesome_pageview_add_pageview() {
    $post_id = $_REQUEST['post_id'];
    //získáme uložená data
    $pageviews = get_post_meta( $post_id, 'myawesome_pageview', true );
    //zvětšíme o jedno
    $pageviews = intval( $pageviews ) + 1;
    //znovu uložíme
    update_post_meta( $post_id, 'myawesome_pageview', $pageviews );
    die( $pageviews );
}

//funkce pro zobrazování počtu shlédnutí
function myawesome_pageview( $post_id = null ) {
    if ( false === empty( $post_id ) && true === is_singular() ) {
        $post_id = get_the_ID();
    } else {
        return;
    }
    echo '<div id="myawesome_pageviews">'.get_post_meta( $post_id, 'myawesome_pageview', true ).'</div>';
}

add_action( 'wp_head', function(){
    if ( ! is_singular() )
        return;
    $ajax_nonce = wp_create_nonce( "my-special-string" );
?>
<script type="text/javascript">
jQuery(document).ready(function($){
	var data = {
		action: 'add_pageview',
		security: '<?php echo $ajax_nonce; ?>',
		post_id: '<?php echo get_post_ID(); ?>'
	};
	$.post( '<?php echo admin_url( "admin-ajax.php" ); ?>' , data, function(response) {
		$( "#myawesome_pageviews" ).html( response );
	});
});
</script>
<?php
} );

Tohle řešení bude fungovat i s libovolným cachovacím pluginem. Ovšem nastane jiný problém. A to s výkonem. Ten sice existoval již předtím, nicméně zůstal skryt cachovacím pluginem.

Co se nyní děje při požadavku na wp-admin/admin-ajax.php

WordPress, k tomu aby mohl fungovat, musí načíst jádro, pluginy, šablony, options z databáze a další data. To neplatí jen a pouze o necachovaném pageview, ale také o AJAX requestech, které se v podobně POST requestu určitě necachují.

Takže každé načtení stránky na daném webu produkuje nyní dva requesty. Jeden cachovaný a druhý přímo mířící na origin – tedy váš server, kde se WordPress načítá s veškerou parádou. Takový AJAX request nebude blokovat načtení stránky, uživatel se dostane k obsahu teoreticky celkem rychle a na ten počet zobrazení v hlavičce článku si holt počká. Ale server bude přetížen.

Tohle je ta část problému, jejíž řešení je uvedeno v článku odkazovaném v úvodu. Nicméně to není vše.

Nekončící zápisy do databáze

Už zmiňované necachované requesty na server jsou špatné, ale je to ještě horší. Daný AJAX request nejen, že načítá data z databáze, on také data zapisuje. To znamená, že co načtení stránky, to zápis do databáze. V malých objemech to sever nejspíš ustojí, ale v případě nějakého spiku v návštěvnosti se ukáže, kdo používá MyISAM a kdo InnoDB jako úložiště dat pro MySQL.

Zatímco stále ještě nejpoužívanější MyISAM zamyká při zápisu celé tabulky, InnoDB zamyká jen řádky. InnoDB pomůže hlavně v tom, že administrátor či editoři nebudou mít problémy s vytvářením novým příspěvků, kvůli tomu, že by tabulka wp_post_meta byla zamknutá kvůli všem těch zápisům z pageviews pluginu. Ale problém to stejně neřeší.

Když jsem nakousl MySQL, tak cítím, že je vhodné také zmínit pro nefunguje cache, kterou má MySQL zabudovanou. MySQL cachuje dotazy a v případě identického dotazu dokáže vrátit data bez toho, aniž by dotaz vykonala. Ovšem MySQL tuto cache maže vždy, když dojde ke změně v relevantních tabulkách. Takže i jednoduché čtení z databáze se nám začne prodražovat.

Nepomůže změnit cahovací plugin

Věřím, že řada provozovatelů webu na WordPressu se začne poohlížet po lepším cachovacím pluginu. Neznám podrobně všechny page level cachovací pluginy, ale dokážu si představit že mažou cache pro stránku v případě updatu relevantní post_meta. Aspoň tak tomu je u různých object cache pluginů, které znám lépe.

Uživatel se dost možná dočte o něčem, čemu se říká object cache a zkusí nějaký ten redis či memcache nasadit. Ovšem kýženého výsledku se opět nedosáhne. Jednoduše proto, že object cache pro daný post, který obsahuje nejen údaje z tabulky wp_posts, ale také z tabulky wp_post_meta, se bude s každým zápisem do databáze mazat, tedy vždy, když někdo spustí daný AJAX request – tedy navštíví danou stránku. Takže vlastně pořád. To je opět stejný problém, jako s MySQL cachí.

Opět se vracím k problému a řešení popsaném ve zmiňovaném blogpostu. Ten správně odstranil AJAX request, ovšem plugin/šablona použitá na webu není tak “elegantní”, pokud se o eleganci ještě vůbec dá hovořit, a neprovádí čtení i zápis v rámci jednoho requestu, nýbrž ve dvou. “Zapisovací” request je v době psaní tohoto článku stále ještě aktivní a vesele zapisuje do databáze, znovu a znovu.

Správné řešení?

Jediné, co můžeme provést je celý takovýto návrh smést ze stolu.

Statistiky jako pageviews patří na systémy, které jsou na nápor dat připraveny. A můžeme si vybírat. Jmenujme aspoň Google Analytics, Piwik či Jetpack a jeho modul Stats.

Pluginy na zobrazování počtu shlédnutí daného příspěvku poté budou muset využít API zmíněných nástrojů a získat tak kýžená data. Jaké pluginy to jsou a jak obecně na to je nad rámec tohoto článku, jehož cílem je především poučit o tom, proč se s page/post views pluginy většinou spálíte a proč.

PS: Původně jsem nechtěl žádný plugin jmenovat a veškerý kód jsem napsal jen ilustračně (aniž bych jej testoval), nicméně když jsem zjistil, že plugin WP-PostViews má více jak 200000 stažení a jeho kód je velmi podobný tomu, co jsem zde nastínil, nedalo mi to. Schválně se na kód pluginu podívejte.

5 thoughts on “Zrádné počty zobrazení příspěvku”

  1. Sakra Davide, dostal jsi mě! 🙂
    Ale… Když jsem hledal tu správnou js funkci, tak jsem našel i tu “zapisovací”, ale podle kódu , by měl být zápis jen na single. Takže pak to je již jen jeden request. Ale nekontroloval jsem to, to je pravda.

    Liked by 1 person

  2. Zajímavé, díky! Už jsem o tom několikrát přemýšlel, ale třeba souvislost s MySQL cache mě nenapadla. Pokud to ale přesto někdo chce (na nějakém málo navštěvovaném webu), tak mi z toho lépe vychází vůbec nepoužívat cachovací plugin? A když někdo prostě nechce pro statistiky žádnou externí službu (a podobných uživatelů není málo), tak jak bys to doporučil nejlépe řešit? Neukládat data do wp_postmeta, ale někam jinam (vlastní tabulka) nebo jinak (textové soubory)?

    P.S. Docela jsem se trápil s přidáním komentáře, škoda, že musím být někam zalogovaný, abych ho mohl vůbec vložit.

    Like

    1. tak mi z toho lépe vychází vůbec nepoužívat cachovací plugin?

      Tak tahle jsem nechtěl, aby to vyznělo. Cachovací pluginy mají své místo a určitě lidem doporučuji je používat. Důležité také je, že cachovací pluginy, aspoň ty nejpoužívanější, mají většinou vyšší kvalitu kódu a úroveň obecně, než ostatní. Přeci jen příprava takového pluginu vyžaduje vyšší míru znalostí o tom, jak WordPress funguje. Ty menší pluginy potom někdy cachovacím pluginům háží klacky pod nohy – jako v tomto případě – a to rozhodně není problém cachovacích pluginů. Přesně naopak.

      A když někdo prostě nechce pro statistiky žádnou externí službu (a podobných uživatelů není málo), tak jak bys to doporučil nejlépe řešit? Neukládat data do wp_postmeta, ale někam jinam (vlastní tabulka)

      WordPress opravdu nemá kapacitu, ani ambice, být vším, co by si uživatelé přáli. Ukládání velkého množství dat v krátké době je něco, na co musí být systém připravený.

      wp_postmeta má tu nevýhodu, že je potřeba využívat tuto tabulku nejen na statistiky, ale také na běžné používání WordPressu a zobrazování příspěvků. Proto velké množství zápisů do téhle tabulky může způsobit problémy. V případě vlastní tabulky jde opět o to, aby si ji člověk sprváně navrhnul – včetně indexů, a zvolil správný engine. MyISAM zamkne při zápisu celou tabulku, kdežto InnoDB jen řádek.

      Ovšem stále je tu problém s tím, že tahle data se budou často měnit a tabulka bude celkem vytěžovaná. Také každá cesta do nějaké tabulky (od čehož nás cachovací pluginy chrání a tím zrychlují web) je celkem nákladná.

      Pokud bychom na webu použili objektovou cache (memcache, redis, …) tak můžeme většinou využít funkci increment, která zvětší hodnotu v memcache, aniž by se musela mazat a znovu získávat (dotazem do databáze). Tím si tyhle nákladné cesty do databáze ukrátíme. Dokážu si představit implementaci, kdy se počet zobrazení zapisuje do databáze a zároveň dochází k inkrementaci cache.

      To ovšem stále neřeší hlavní problém. I takový systém bude stále trpět v případě, že dojde k situaci, kdy jedna stránka na webu zažije něco, čemu se říká spike – velké množství shlédnutí v krátkém čase. Jak jsem popisoval, v případě page level cache a zapisováním skrze AJAX bude stále docházet k requestům na náš server, který nabootuje celý WordPress a navíc dojde při každém shlédnutí stránky v zápisu do databáze. Zapotí se nejen Apache či NginX, ale také MySQL server. A dost možná začnou zahazovat requesty a statistiky stejně nebudou přesné.

      V případě nepoužívání cachovacího pluginu ovšem dojde k sesypání serveru v daném případě o poznání dříve.

      nebo jinak (textové soubory)?

      Zápisy a čtení / do filesystému jsou na tom ještě hůře co se rychlosti týče, pokud tedy nepoužíváme na serveru SSD disk. Stejně jako MyISAM navíc trpí na zamykání celého souboru, nikoli jen řádku.

      A když někdo prostě nechce pro statistiky žádnou externí službu (a podobných uživatelů není málo)

      Pokud někdo mermomocí nechce svěřovat svá data třetí straně, zkuste si třeba nainstalovat Piwik. Ovšem pozor, i to je systém běžící na Apache/NginX PHP a MySQL, čili platí stejná omezení jako jsem uvedl výše. Výhodou je, že kvůli zápisu do tabulky nedojde k nabootování celého WordPressu (requesty tak budou rychlejší) a velký nápor tak Piwik vydrží o něco déle. Ještě lepší bude, když Piwik poběží na samostatném serveru.

      Já osobně ovšem s klidným svědomím svěřím statistiky systémům, které jsou na to připravené. Ať se již jedná o Google Analytics či WordPress.com Stats a budu mít z velké návštěvnosti radost, namísto starostí.

      Like

Leave a comment