Türchen 20: EE Full Page Cache Holepunching

Tobi war so nett mir eine Gelegenheit wieder einen Post im Adventskalender zu schreiben, und nachdem ich zuerst dachte ich hätte keine Zeit, freue ich mich jetzt doch mit dabei sein zu dürfen :) Ich finde den Community-Adventskalender klasse!
Diesmal zum Thema Full-Page Cache und Holepunching.

Eines der Enterprise-Edition Features ist der Full-Page Cache (FPC).

Dieser ermöglicht es Magento die meisten Seiten mit viel weniger Overhead auszuliefern, als es von Haus aus in der CE Version möglich ist. Das ganze funktioniert auch ganz gut, solange keine dynamischen Inhalte in einer Seite angezeigt werden. Zum Glück hat Magento jedoch auch das berücksichtigt: dynamisch Blöcke wie zum Beispiel die zuletzt angesehenen Produkte oder der Mini-Warenkorb werden individuell nach Kunde gecached, und dann in eine Seite aus dem Full-Page Cache während einer Anfrage eingefügt.

Ziel dieses Posts ist es, diesen Prozess zu verdeutlichen, und so Magento-Entwickler zu befähigen, eigene dynamische Blöcke effizient mit dem Full-Page Cache zu verwenden.

Der Full-Page Cache Prozess

Der Full-Page Cache unterscheidet bei Anfragen verschiedene Fälle:

  1. Eine Anfrage darf nicht gecached werden
  2. Eine Anfrage darf gecached werden, liegt aber nicht gecached vor
  3. Eine Anfrage kann aus dem Cache bedient werden und enthält keine dynamischen Inhalte
  4. Eine Anfrage kann aus dem Cache bedient werden, die Seite enthält gecachte dynamische Inhalte
  5. Eine Anfrage kann aus dem Cache bedient werden, die Seite enthält ungecachte dynamische Inhalte

Von Interesse für diesen Post  sind Fall 3, 4 und 5, also wenn eine Anfrage aus dem Cache bedient wird. Magento versucht den Prozess so effizient wie möglich zu gestalten. Aus diesem Grund wird der FPC schon sehr früh im Bootstrap Prozess eingeschaltet, bevor die Initialisierung von Magento abgeschlossen ist.

Hier ein Ausschnitt aus Mage_Core_Model_App::run()

...
if ($this->_cache->processRequest()) {
    $this->getResponse()->sendResponse();
} else {
    $this->_initModules();
...

Sofern also processRequest() true zurückgibt wird das meiste Bootstrapping übersprungen, wie zum Beispiel das Routing und das dispatchen eines Action Controllers.

Eine ganze Seite zu cachen ist, denke ich, nicht weiter spannend. Interessant ist das Einfügen der dynamischen Inhalte in eine gecachte Seite.

Lädt der Full-Page Cache eine Seite aus dem Cache, sucht dieser in dem Seiten-Inhalt nach Platzhaltern für dynamische Blöcke. Sind welche vorhanden, instanziiert Magento für jeden dynamischen Block eine Container-Klasse, welcher die Aufgabe zukommt, die dynamischen Inhalte des Blocks in den Content einzufügen.

Zuerst wird auf dem Block-Container die Methode applyWithoutApp() aufgerufen.

Nun versucht der Container, den Content aus dem Block-Cache zu laden und in den Seiten-Inhalte einzufügen. Für diese Blöcke können auch für Cache-Einträge für individuelle Kunden geschrieben werden.

Konnten die Inhalte für alle Platzhalter von den Containern aus dem Cache geladen werden, liegt die Seite jetzt vollständig vor, processRequest() gibt true zurück und die Seite wird an den Kunden ausgeliefert.

Wurden jedoch nicht alle dynamischen Inhalte von den Containern im Block-Cache gefunden, so beginnt die nächste Phase.

Damit der Content generiert werden kann, benötigen die entsprechenden Blöcke jedoch eine voll aufgesetzte Laufzeit-Umgebung: der Full-Page Cache setzt das Request Objekt auf die Route pagecache/request/process und bestimmt durch das Flag isStraight(true) das keine URL Rewrites geladen werden sollen.

...
Mage::register('cached_page_content', $content);
Mage::register('cached_page_containers', $containers);
Mage::app()->getRequest()
    ->setModuleName('pagecache')
    ->setControllerName('request')
    ->setActionName('process')
    ->isStraight(true);

$routingInfo = array(
    'aliases' => $this->getMetadata('routing_aliases'),
    'requested_route' => $this->getMetadata('routing_requested_route'),
    'requested_controller' => $this->getMetadata('routing_requested_controller'),
    'requested_action' => $this->getMetadata('routing_requested_action')
);
Mage::app()->getRequest()->setRoutingInfo($routingInfo);
...

Dann wird von processRequest() false zurückgegeben, und der Magento dispatch Vorgang nimmt seinen Lauf.

Steigen wir wieder ein im dem Action Controller des Enterprise_PageCache Moduls, Enterprise_PageCache_RequestController::processAction()

Hier nun wird auf den noch nicht verarbeiteten Containern aus der Registry die Methode applyInApp() aufgerufen.

Hierbei wird die Block-Klasse instanziiert und toHtml() aufgerufen, und die Ausgabe in den Seiten-Content eingefügt.

Dynamische Blöcke einfügen

Wenn irgend möglich, sollten ein Block immer gecached werden, damit applyWithoutApp() ausreicht um die Inhalte in die Seite einzufügen.

Wenn applyInApp() auf dem Container verwendet werden muss, fällt ein Großteil des Nutzens des FPC weg, da der ganze dispatch-Process von Magento durchlaufen wird.

Welche Schritte sind nun nötig, um eigene Blöcke dynamisch im Full-Page Cache anzeigen zu lassen?

Um einen Block dynamisch in den Full-Page Cache einzugliedern, muss zuerst in dem etc/ Verzeichnis des Moduls eine cache.xml Datei angelegt werden.

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <placeholders>
        <webguys_example>
            <block>webguys_example/view</block>
            <name>product.info.example</name>
            <placeholder>CACHE_TEST</placeholder>
            <container>Webguys_Example_Model_Container_Cachetest</container>
            <cache_lifetime>86400</cache_lifetime>
        </webguys_example>
    </placeholders>
</config>

Der und der muss der type="" und des name="" deklaration des Blocks aus  dem Layout XML entsprechen.

<block type="webguys_example/view" name="product.info.example" as="example" template="webguys/example.phtml" />

Der ist ein Code welcher einmalig innerhalb einer Magento Installation sein sollte.

Der ist der Name der Klasse welche verantwortlich ist die Block Ausgaben zu cachen, aus dem Cache zu laden und in dem Inhalt einzufügen.

Und schließlich der Knoten, der in bisher allen Magento Enterprise Versionen keine Auswirkungen hat. Sofern in der _saveCache() Methode des Containers keine anderen Angaben gemacht werden, wird ein Block für immer gecached bis er invalidiert wird. Es ist sicher eine gute Idee den Knoten trotzdem anzugeben sollte das sich in Zukunft ändern.

Nachdem die etc/cache.xml nun existiert ist der nächste Schritt die Container Klasse anzulegen. Sie muss Enterprise_PageCache_Model_Container_Abstract erweitern. Zumindest eine Methode muss implementiert werden: _renderBlock(). Sie gibt den Inhalt des Blocks zurück in dem Fall das applyInApp() aufgerufen wird. Üblicherweise wird der Block mit new() instanziiert, da das Layout Objekt aus Performance-Gründen besser nicht verwendet wird. Dann wird ganz normal toHtml() aufgerufen um den Block zu rendern. Der Block sollte also so wenig Resourcen wie möglich benutzen, damit der Vorteil des Full-Page Caches nicht verloren geht.

Damit applyWithoutApp() auch funktioniert muss noch _getCacheId() in der Container Klasse implementiert werden. Die Methode liefert, welch Überraschung, den Cache-Identifier für den Block zurück. Wenn jeder Kunde seine eigene Variante des Blocks sehen soll, kann $this->_getCookieValue(Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER, '') in die Cache-Id eingefügt werden.

Wenn der Container die Klasse Enterprise_PageCache_Model_Container_Customer extended wird die Cache-Lifetime des Block Caches automatisch auf die Dauer der Kunden-Session gesetzt.

Hier ein Beispiel für einen Container:

class Webguys_Example_Model_Container_Cachetest
    extends Enterprise_PageCache_Model_Container_Abstract {

    protected function _getIdentifier() {
        return $this->_getCookieValue(Enterprise_PageCache_Model_Cookie::COOKIE_CUSTOMER, '');
    }

    protected function _getCacheId() {
        return 'EXAMPLE_' . md5($this->_placeholder->getAttribute('cache_id') . '_' . $this->_getIdentifier();
    }

    protected function _renderBlock() {
        $blockClass = $this->_placeholder->getAttribute('block');
        $template = $this->_placeholder->getAttribute('template');
        $block = new $blockClass;
        $block->setTemplate($template)
        return $block->toHtml();
    }
}

Werte durchreichen

Wenn der Block beim Rendern Werte aus der Magento Umgebung benötigt, die nicht während einer pagecache/request/process Anfrage zur Verfügung stehen, kann folgender Trick verwendet werden. Von dem von $block->getCacheKeyInfo() zurückgegebenen Array werden alle Werte, die einen String als Key haben, als Placeholder Attribut gespeichert. Hier ein Beispiel für eine Produkt-Id.

In der Block Klasse wird der Wert dem CacheKeyInfo Array hinzugefügt:

public function getCacheKeyInfo()
{
    $info = parent::getCacheKeyInfo();
    if (Mage::registry('current_product')) {
        $info['product_id'] = Mage::registry('current_product')->getId();
    }
    return $info;
}

In der _getCacheId() und der _renderBlock() Methode des Containers wird die Id dann einfach mit $this->_placeholder->getAttribute('product_id') ausgelesen.

$block = new $blockClass;
$block->setTemplate($template)
    ->setProductId($this->_placeholder->getAttribute('product_id'));

Insgesamt ist es also sehr einfach dynamische Inhalte in den Full-Page Cache einzufügen. Mit dem nötigen Know-How und etwas Vorsicht kann dabei die Performance erhalten bleiben. Ein einfaches Beispielmodul für diesen Beitrag kann zum Testen heruntergeladen werden.



Ein Beitrag von Vinai Kopp
Vinai's avatar

Vinai Kopp arbeitet seit Oktober 2011 als Manager of Developer Education für Magento Inc. Vorher war er als freier Magento Entwickler und Berater tätig, mit dem Schwerpunkt Entwicklerschulung. Desweiteren ist er Co-Autor des Magento Entwicklerhandbuchs, erschienen im O'reilly Verlag.

Alle Beiträge von Vinai

Kommentare
Vinai Kopp am

Für die Community Edition gibt es mehrere Alternativen für Full Page Caches. Beliebte kommerzielle Module sind http://www.tinybrick.com/improve-magentos-slow-performance.html/ und http://www.nitrogento.com/ (ich selbst habe jedoch keines von beiden getestet).

Es gibt auch ein kostenloses Modul auf Magento Connect: http://www.magentocommerce.com/magento-connect/zoom-full-page-cache-1742.html

Alle diese Module bieten irgend eine Art von Holepunching an (soweit ich weiß).

Bart am

Hi Vinai,

ich bin Magento Newbie und auf der Suche nach einem brauchbaren Cache-System für die Community Edition.

Gibt es in der Community Edition die Möglichkeit, ein solches Caching auf für dynamische Elemente durchzuführen?

Ich wäre für einen Tipp sehr dankbar!

Viele Grüße Bart

Vinai Kopp am

May I refer to this post on StackOverflow which should give an answer: http://stackoverflow.com/questions/8174021/magento-pass-current-product-id-to-module/8179567#8179567

Martijn am

How Do I retrieve the product_id in my module? The first time I load the module I am able to retrieve it by the following code:

$this->getRequest()->getParam('id');

But after a page reload it is gone.

Could you please help me? Thanks in advance, Martijn

Matthias Zeis am

Dankeschön für den Beitrag. Ich habe mich ja immer schon gefragt, wie der FPC arbeitet, und nach deinen Erzählungen war ich umso gespannter darauf. ;)

Dein Kommentar