6 December 2016
Magento 2 has brought with it many advantageous built-in features, so it’s easy to presume that built-in cache is automatically the better option. However, Varnish is not to be overlooked; it’s Magento’s recommended solution and here’s why…
Magento 2 provides two options for a full page cache out of the box: the 'built-in' cache, which uses the filesystem similarly to M1 Enterprise Edition, and support for Varnish as a reverse-proxy in front of the web server. Varnish is a superior option since it caches static files as well, and avoids slow database or filesystem read/writes.
Following recent support work for a client, we have collated information to clarify how in practice Magento decides what to cache and how that information reaches Varnish.
As you'll already know, Magento uses a front controller pattern. Since cache hits need to be returned as early in the process as possible, in order to keep response times low, the built-in cache is implemented as a Magento plugin around the FrontController.
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" config="" xsi:nonamespaceschemalocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Framework\App\FrontControllerInterface"> <plugin name="front-controller-builtin-cache" type="Magento\PageCache\Model\App\FrontController\BuiltinPlugin"></plugin> <plugin name="front-controller-varnish-cache" type="Magento\PageCache\Model\App\FrontController\VarnishPlugin"></plugin> </type> ... </config>
Since the Magento application bootstrapping still has to occur for the front controller to be run, the built-in cache is at a disadvantage to Varnish. We'll see why Varnish avoids this later in our post.
In magento/module-page-cache/Model/App/FrontController/BuiltinPlugin.php we can see that the first check is whether the PageCache module is enabled, and if so, if it's set to use the built-in cache mechanism:
if (!$this->config->isEnabled() || $this->config->getType() != \Magento\PageCache\Model\Config::BUILT_IN) { return $proceed($request); } ...process built-in caching
The Varnish option is very simple:
$response = $proceed($request); if ($this->config->getType() == \Magento\PageCache\Model\Config::VARNISH && $this->config->isEnabled() && $response instanceof \Magento\Framework\App\Response\Http) { $this->version->process(); if ($this->state->getMode() == \Magento\Framework\App\State::MODE_DEVELOPER) { $response->setHeader('X-Magento-Debug', 1); } } return $response;
In essence, regardless of whether Varnish is enabled or not in the admin, the Varnish plugin simply continues with the request, sets up the private content cookie if needed ($this->version->process()) then adds some debug headers according to settings.
As the admin setting to switch between built-in and Varnish is a dropdown, both plugins will run but are essentially mutually exclusive – only one at a time will affect the response.
So if the Varnish option just returns the Magento result as usual (cookie and debug header aside), how does Varnish itself know what to cache?
If at least one block of the layout xml for the response is declared as not cacheable, the whole request is deemed to not be cacheable:
magento/framework/View/Layout.php public function isCacheable() { $this->build(); $cacheableXml = !(bool)count($this->getXml()->xpath('//' . Element::TYPE_BLOCK . '[@cacheable="false"]')); return $this->cacheable && $cacheableXml; }
This isCacheable() method is checked in a plugin around the response:
... public function afterGenerateXml(\Magento\Framework\View\Layout $subject, $result) { if ($subject->isCacheable() && $this->config->isEnabled()) { $this->response->setPublicHeaders($this->config->getTtl()); } return $result; }...
The call to set PublicHeaders results in http headers which therefore tell Varnish itself that the page can be cached:
magento/framework/App/Response/Http.php public function setPublicHeaders($ttl) { if ($ttl < 0 || !preg_match('/^[0-9]+$/', $ttl)) { throw new \InvalidArgumentException('Time to live is a mandatory parameter for set public headers'); } $this->setHeader('pragma', 'cache', true); $this->setHeader('cache-control', 'public, max-age=' . $ttl . ', s-maxage=' . $ttl, true); $this->setHeader('expires', $this->getExpirationHeader('+' . $ttl . ' seconds'), true); }
Interestingly, the built-in cache uses the same approach – it checks whether the response has a cache-control header with a ttl >0, and if it does, stores the response in the appropriate backend cache.
What about non-cacheable requests? The FrontController sets no-cache headers as default, which are then replaced with a TTL if the request is later deemed to be cacheable.
So the process in short:
If Varnish is not present at all or has a cache miss...
Request to Magento for page → both built-in and Varnish plugins run but built-in quits out if Varnish is enabled → Magento builds response as normal, setting no-cache headers → Magento checks if the page should be cacheable (it assumes yes unless at least one block is not cacheable) → If cacheable, Magento sets cache-control and a TTL → if using built-in cache, the pagecache plugin looks for the caching header and saves the response accordingly, setting the cache header back to no-cache → the output is returned to Varnish or to the browser if Varnish is not present → Varnish uses the cache-control header to decide whether to store the response in cache.
Takeways: