Multiple magento websites share one varnish instance

When we use one instance of varnish on multiple magento sites, it interferes with each other when we refresh the full page cache. Below is the complete repair method.

1. Modify PHP file

vendor/magento/module-cache-invalidate/Model/PurgeCache.php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\CacheInvalidate\Model;

use Exception;
use Generator;
use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\Cache\InvalidateLogger;
use Magento\PageCache\Model\Cache\Server;
use Laminas\Http\Client\Adapter\Socket;
use Laminas\Uri\Uri;

/**
* Invalidate external HTTP cache(s) based on tag pattern
*/
class PurgeCache
{
public const HEADER_X_MAGENTO_TAGS_PATTERN = 'X-Magento-Tags-Pattern';
public const HEADER_X_MAGENTO_HOST = 'X-Magento-Host';
public const CONFIG_PATH_X_MAGENTO_HOST = 'x_magento_host';

/**
* @var Server
*/
protected $cacheServer;

/**
* @var SocketFactory
*/
protected $socketAdapterFactory;

/**
* @var InvalidateLogger
*/
private $logger;

/**
* @var DeploymentConfig
*/
private $config;

/**
* Batch size of the purge request.
*
* Based on default Varnish 4 http_req_hdr_len size minus a 512 bytes margin for method,
* header name, line feeds etc.
*
* @see https://varnish-cache.org/docs/4.1/reference/varnishd.html
*
* @var int
*/
private $maxHeaderSize;

/**
* Constructor
*
* @param Server $cacheServer
* @param SocketFactory $socketAdapterFactory
* @param InvalidateLogger $logger
* @param DeploymentConfig $config
* @param int $maxHeaderSize
*/
public function __construct(
Server $cacheServer,
SocketFactory $socketAdapterFactory,
InvalidateLogger $logger,
DeploymentConfig $config,
int $maxHeaderSize = 7680
) {
$this->cacheServer = $cacheServer;
$this->socketAdapterFactory = $socketAdapterFactory;
$this->logger = $logger;
$this->config = $config;
$this->maxHeaderSize = $maxHeaderSize;
}

/**
* Send curl purge request to invalidate cache by tags pattern
*
* @param array|string $tags
* @return bool Return true if successful; otherwise return false
*/
public function sendPurgeRequest($tags)
{
if (is_string($tags)) {
$tags = [$tags];
}

$successful = true;
$socketAdapter = $this->socketAdapterFactory->create();
$servers = $this->cacheServer->getUris();
$socketAdapter->setOptions(['timeout' => 10]);

$formattedTagsChunks = $this->chunkTags($tags);
foreach ($formattedTagsChunks as $formattedTagsChunk) {
if (!$this->sendPurgeRequestToServers($socketAdapter, $servers, $formattedTagsChunk)) {
$successful = false;
}
}

return $successful;
}

/**
* Split tags into batches to suit Varnish max. header size
*
* @param array $tags
* @return Generator
*/
private function chunkTags(array $tags): Generator
{
$currentBatchSize = 0;
$formattedTagsChunk = [];
foreach ($tags as $formattedTag) {
// Check if (currentBatchSize + length of next tag + number of pipe delimiters) would exceed header size.
if ($currentBatchSize + strlen($formattedTag ?: '') + count($formattedTagsChunk) > $this->maxHeaderSize) {
yield implode('|', $formattedTagsChunk);
$formattedTagsChunk = [];
$currentBatchSize = 0;
}

$currentBatchSize += strlen($formattedTag ?: '');
$formattedTagsChunk[] = $formattedTag;
}
if (!empty($formattedTagsChunk)) {
yield implode('|', $formattedTagsChunk);
}
}

/**
* Send curl purge request to servers to invalidate cache by tags pattern
*
* @param Socket $socketAdapter
* @param Uri[] $servers
* @param string $formattedTagsChunk
* @return bool Return true if successful; otherwise return false
*/
private function sendPurgeRequestToServers(Socket $socketAdapter, array $servers, string $formattedTagsChunk): bool
{
$headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk,
self::HEADER_X_MAGENTO_HOST => $this->config->get(self::CONFIG_PATH_X_MAGENTO_HOST)];
$unresponsiveServerError = [];
foreach ($servers as $server) {
$headers['Host'] = $server->getHost();
try {
$socketAdapter->connect($server->getHost(), $server->getPort());
$socketAdapter->write(
'PURGE',
$server,
'1.1',
$headers
);
$socketAdapter->read();
$socketAdapter->close();
} catch (Exception $e) {
$unresponsiveServerError[] = "Cache host: " . $server->getHost() . ":" . $server->getPort() .
"resulted in error message: " . $e->getMessage();
}
}

$errorCount = count($unresponsiveServerError);

if ($errorCount > 0) {
$loggerMessage = implode(" ", $unresponsiveServerError);

if ($errorCount == count($servers)) {
$this->logger->critical(
'No cache server(s) could be purged ' . $loggerMessage,
compact('servers', 'formattedTagsChunk')
);
return false;
}

$this->logger->warning(
'Unresponsive cache server(s) hit' . $loggerMessage,
compact('servers', 'formattedTagsChunk')
);
}

$this->logger->execute(compact('servers', 'formattedTagsChunk'));
return true;
}
}

it is recommended to update the code in vendor by patching it

fix_vendor_varnish_purge.patch
diff --git a/PurgeCache.php b/PurgeCache.php
--- a/vendor/magento/module-cache-invalidate/Model/PurgeCache.php (revision bdc334687441fc6e1fbffbbd793535760ba9ab61)
+++ b/vendor/magento/module-cache-invalidate/Model/PurgeCache.php (revision bfbbb8731f5cb5ccff96631705e07313cd1899ef)
@@ -7,6 +7,7 @@

use Exception;
use Generator;
+use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\Cache\InvalidateLogger;
use Magento\PageCache\Model\Cache\Server;
use Laminas\Http\Client\Adapter\Socket;
@@ -18,6 +19,8 @@
class PurgeCache
{
public const HEADER_X_MAGENTO_TAGS_PATTERN = 'X-Magento-Tags-Pattern';
+ public const HEADER_X_MAGENTO_HOST = 'X-Magento-Host';
+ public const CONFIG_PATH_X_MAGENTO_HOST = 'x_magento_host';

/**
* @var Server
@@ -34,6 +37,11 @@
*/
private $logger;

+ /**
+ * @var DeploymentConfig
+ */
+ private $config;
+
/**
* Batch size of the purge request.
*
@@ -52,17 +60,20 @@
* @param Server $cacheServer
* @param SocketFactory $socketAdapterFactory
* @param InvalidateLogger $logger
+ * @param DeploymentConfig $config
* @param int $maxHeaderSize
*/
public function __construct(
Server $cacheServer,
SocketFactory $socketAdapterFactory,
InvalidateLogger $logger,
+ DeploymentConfig $config,
int $maxHeaderSize = 7680
) {
$this->cacheServer = $cacheServer;
$this->socketAdapterFactory = $socketAdapterFactory;
$this->logger = $logger;
+ $this->config = $config;
$this->maxHeaderSize = $maxHeaderSize;
}

@@ -129,7 +140,8 @@
*/
private function sendPurgeRequestToServers(Socket $socketAdapter, array $servers, string $formattedTagsChunk): bool
{
- $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk];
+ $headers = [self::HEADER_X_MAGENTO_TAGS_PATTERN => $formattedTagsChunk,
+ self::HEADER_X_MAGENTO_HOST => $this->config->get(self::CONFIG_PATH_X_MAGENTO_HOST)];
$unresponsiveServerError = [];
foreach ($servers as $server) {
$headers['Host'] = $server->getHost();

 2. varnish config

/ect/varnish/default.vcl

 

# VCL version 5.0 is not supported so it should be 4.0 even though actually used Varnish version is 6
vcl 4.0;

import std;
# The minimal Varnish version is 6.0
# For SSL offloading, pass the following header in your proxy server or load balancer: 'X-Forwarded-Proto: https'

backend default {
.host = "localhost";
.port = "8080";
.first_byte_timeout = 600s;
.probe = {
.url = "/health_check.php";
.timeout = 2s;
.interval = 5s;
.window = 10;
.threshold = 5;
}
}

acl purge {
"localhost";
}

sub vcl_recv {
if (req.restarts > 0) {
set req.hash_always_miss = true;
}

if (req.method == "PURGE") {
if (client.ip !~ purge) {
return (synth(405, "Method not allowed"));
}
# To use the X-Pool header for purging varnish during automated deployments, make sure the X-Pool header
# has been added to the response in your backend server config. This is used, for example, by the
# capistrano-magento2 gem for purging old content from varnish during it's deploy routine.
if (!req.http.X-Magento-Tags-Pattern && !req.http.X-Pool) {
return (synth(400, "X-Magento-Tags-Pattern or X-Pool header required"));
}
if (req.http.X-Magento-Tags-Pattern) {
# ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);
ban("obj.http.X-Host ~ " + req.http.X-Magento-Host + " && obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern);

}
if (req.http.X-Pool) {
ban("obj.http.X-Pool ~ " + req.http.X-Pool);
}
return (synth(200, "Purged"));
}

if (req.method != "GET" &&
req.method != "HEAD" &&
req.method != "PUT" &&
req.method != "POST" &&
req.method != "TRACE" &&
req.method != "OPTIONS" &&
req.method != "DELETE") {
/* Non-RFC2616 or CONNECT which is weird. */
return (pipe);
}

# We only deal with GET and HEAD by default
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}

# Bypass customer, shopping cart, checkout
if (req.url ~ "/customer" || req.url ~ "/checkout") {
return (pass);
}

# Bypass health check requests
if (req.url ~ "^/(pub/)?(health_check.php)$") {
return (pass);
}

# Set initial grace period usage status
set req.http.grace = "none";

# normalize url in case of leading HTTP scheme and domain
set req.url = regsub(req.url, "^http[s]?://", "");

# collect all cookies
std.collect(req.http.Cookie);

# Compression filter. See https://www.varnish-cache.org/trac/wiki/FAQ/Compression
if (req.http.Accept-Encoding) {
if (req.url ~ "\.(jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|ogg|swf|flv)$") {
# No point in compressing these
unset req.http.Accept-Encoding;
} elsif (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} elsif (req.http.Accept-Encoding ~ "deflate" && req.http.user-agent !~ "MSIE") {
set req.http.Accept-Encoding = "deflate";
} else {
# unknown algorithm
unset req.http.Accept-Encoding;
}
}

# Remove all marketing get parameters to minimize the cache objects
if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") {
set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", "");
set req.url = regsub(req.url, "[?|&]+$", "");
}

# Static files caching
if (req.url ~ "^/(pub/)?(media|static)/") {
# Static files should not be cached by default
return (pass);

# But if you use a few locales and don't use CDN you can enable caching static files by commenting previous line (#return (pass);) and uncommenting next 3 lines
#unset req.http.Https;
#unset req.http.X-Forwarded-Proto;
#unset req.http.Cookie;
}

# Bypass authenticated GraphQL requests without a X-Magento-Cache-Id
if (req.url ~ "/graphql" && !req.http.X-Magento-Cache-Id && req.http.Authorization ~ "^Bearer") {
return (pass);
}

return (hash);
}

sub vcl_hash {
if ((req.url !~ "/graphql" || !req.http.X-Magento-Cache-Id) && req.http.cookie ~ "X-Magento-Vary=") {
hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1"));
}

# To make sure http users don't see ssl warning
if (req.http.X-Forwarded-Proto) {
hash_data(req.http.X-Forwarded-Proto);
}


if (req.url ~ "/graphql") {
call process_graphql_headers;
}
}

sub process_graphql_headers {
if (req.http.X-Magento-Cache-Id) {
hash_data(req.http.X-Magento-Cache-Id);

# When the frontend stops sending the auth token, make sure users stop getting results cached for logged-in users
if (req.http.Authorization ~ "^Bearer") {
hash_data("Authorized");
}
}

if (req.http.Store) {
hash_data(req.http.Store);
}

if (req.http.Content-Currency) {
hash_data(req.http.Content-Currency);
}
}

sub vcl_backend_response {

set beresp.http.X-Host = bereq.http.Host;

set beresp.grace = 3d;

if (beresp.http.content-type ~ "text") {
set beresp.do_esi = true;
}

if (bereq.url ~ "\.js$" || beresp.http.content-type ~ "text") {
set beresp.do_gzip = true;
}

if (beresp.http.X-Magento-Debug) {
set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control;
}

# cache only successfully responses and 404s that are not marked as private
if (beresp.status != 200 &&
beresp.status != 404 &&
beresp.http.Cache-Control ~ "private") {
set beresp.uncacheable = true;
set beresp.ttl = 86400s;
return (deliver);
}

# validate if we need to cache it and prevent from setting cookie
if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
unset beresp.http.set-cookie;
}

# If page is not cacheable then bypass varnish for 2 minutes as Hit-For-Pass
if (beresp.ttl <= 0s ||
beresp.http.Surrogate-control ~ "no-store" ||
(!beresp.http.Surrogate-Control &&
beresp.http.Cache-Control ~ "no-cache|no-store") ||
beresp.http.Vary == "*") {
# Mark as Hit-For-Pass for the next 2 minutes
set beresp.ttl = 120s;
set beresp.uncacheable = true;
}

# If the cache key in the Magento response doesn't match the one that was sent in the request, don't cache under the request's key
if (bereq.url ~ "/graphql" && bereq.http.X-Magento-Cache-Id && bereq.http.X-Magento-Cache-Id != beresp.http.X-Magento-Cache-Id) {
set beresp.ttl = 0s;
set beresp.uncacheable = true;
}

return (deliver);
}

sub vcl_deliver {
if (resp.http.x-varnish ~ " ") {
set resp.http.X-Magento-Cache-Debug = "HIT";
set resp.http.Grace = req.http.grace;
} else {
set resp.http.X-Magento-Cache-Debug = "MISS";
}

# Not letting browser to cache non-static files.
if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(pub/)?(media|static)/") {
set resp.http.Pragma = "no-cache";
set resp.http.Expires = "-1";
set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0";
}

if (!resp.http.X-Magento-Debug) {
unset resp.http.Age;
}
unset resp.http.X-Magento-Debug;
unset resp.http.X-Magento-Tags;
unset resp.http.X-Powered-By;
unset resp.http.Server;
unset resp.http.X-Varnish;
unset resp.http.Via;
unset resp.http.Link;
}

sub vcl_hit {
if (obj.ttl >= 0s) {
# Hit within TTL period
return (deliver);
}
if (std.healthy(req.backend_hint)) {
if (obj.ttl + 300s > 0s) {
# Hit after TTL expiration, but within grace period
set req.http.grace = "normal (healthy server)";
return (deliver);
} else {
# Hit after TTL and grace expiration
return (restart);
}
} else {
# server is not healthy, retrieve from cache
set req.http.grace = "unlimited (unhealthy server)";
return (deliver);
}
}

3. Modify app/etc/env.php

'x_magento_host' => 'local.dmtq.com',
'http_cache_hosts' => [
[
'host' => '127.0.0.1',
'port' => '6081'
]
]

 This step is important to distinguish which domain site is requesting varnish purge.

 



Copyright © 2013-present Magento, Inc. All rights reserved.