Below are some recommended settings that improve the performance of Drupal WxT sites.
This is the multi-page printable view of this section. Click here to print.
Performance
- 1: PostgreSQL
- 2: Redis
- 3: Varnish
1 - PostgreSQL
To properly configure PostgreSQL with Drupal you should ensure the following configuration is used.
Note: Some customizations might be necessary depending on your individual requirements.
postgresqlConfiguration:
listenAddresses: "'*'"
maxConnections: "200"
sharedBuffers: 512MB
workMem: 2048MB
effectiveCacheSize: 512MB
effectiveIoConcurrency: "100"
maintenanceWorkMem: 32MB
minWalSize: 512MB
maxWalSize: 512MB
walBuffers: 8048kB
byteaOutput: "'escape'"
hugePages: "off"
walLevel: "replica"
maxWalSenders: "0"
synchronousCommit: "on"
walKeepSegments: "130"
checkpointTimeout: "'15 min'"
checkpointCompletionTarget: "0.9"
walCompression: "on"
walWriterDelay: 200ms
walWriterFlushAfter: 1MB
bgwriterDelay: 200ms
bgwriterLruMaxpages: "100"
bgwriterLruMultiplier: "2.0"
bgwriterFlushAfter: "0"
maxWorkerProcesses: "8"
maxParallelWorkersPerGather: "4"
maxParallelWorkers: "4"
Note: The above is written in yaml syntax which will work for both Docker Compose and Kubernetes Helm Charts. For the
postgresql.conf
file itself without using these tools simply find the_
counterpart.
Queries leveraging ILIKE
There is a known PostgreSQL performance issue that exists in Drupal and is related to leveraging queries with ILIKE
.
This issue is particularly noticeable in relation to the path_alias table.
There are patches being worked on to handle this in Drupal core but a very quick fix can be implemented leveraging pg_trgm.
There is a great blog article listed below which goes over this issue in more detail.
The instructions are a bit outdated so the updated syntax to enter in psql is given below:
CREATE EXTENSION pg_trgm;
CREATE INDEX path_alias__alias_trgm_gist_idx ON path_alias USING gist (alias gist_trgm_ops);
CREATE INDEX path_alias__path_trgm_gist_idx ON path_alias USING gist (path gist_trgm_ops);
ANALYZE path_alias;
2 - Redis
To properly configure Redis with Drupal you should ensure the following configuration is added to your settings.php
file.
Note: Some customizations might be necessary depending on your individual requirements.
if (extension_loaded('redis')) {
// Set Redis as the default backend for any cache bin not otherwise specified.
$settings['cache']['default'] = 'cache.backend.redis';
$settings['redis.connection']['interface'] = 'PhpRedis';
$settings['redis.connection']['scheme'] = 'http';
$settings['redis.connection']['host'] = 'localhost';
$settings['redis.connection']['port'] = '6379';
$settings['redis.connection']['password'] = getenv('REDIS_PASSWORD') ?: '';
$settings['redis.connection']['persistent'] = FALSE;
// Allow the services to work before the Redis module itself is enabled.
$settings['container_yamls'][] = 'modules/contrib/redis/example.services.yml';
$settings['container_yamls'][] = 'modules/contrib/redis/redis.services.yml';
// Manually add the classloader path, this is required for the container cache bin definition below
// and allows to use it without the redis module being enabled.
$class_loader->addPsr4('Drupal\\redis\\', 'modules/contrib/redis/src');
$settings['bootstrap_container_definition'] = [
'parameters' => [],
'services' => [
'redis.factory' => [
'class' => 'Drupal\redis\ClientFactory',
],
'cache.backend.redis' => [
'class' => 'Drupal\redis\Cache\CacheBackendFactory',
'arguments' => ['@redis.factory', '@cache_tags_provider.container', '@serialization.phpserialize'],
],
'cache.container' => [
'class' => '\Drupal\redis\Cache\PhpRedis',
'factory' => ['@cache.backend.redis', 'get'],
'arguments' => ['container'],
],
'cache_tags_provider.container' => [
'class' => 'Drupal\redis\Cache\RedisCacheTagsChecksum',
'arguments' => ['@redis.factory'],
],
'serialization.phpserialize' => [
'class' => 'Drupal\Component\Serialization\PhpSerialize',
],
],
];
/** Optional prefix for cache entries */
$settings['cache_prefix'] = 'drupal_';
// Always set the fast backend for bootstrap, discover and config, otherwise
// this gets lost when redis is enabled.
$settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast';
$settings['cache']['bins']['discovery'] = 'cache.backend.chainedfast';
$settings['cache']['bins']['config'] = 'cache.backend.chainedfast';
// Use for all bins otherwise specified.
$settings['cache']['default'] = 'cache.backend.redis';
// Use for all queues unless otherwise specified for a specific queue.
$settings['queue_default'] = 'queue.redis';
// Or if you want to use reliable queue implementation.
// $settings['queue_default'] = 'queue.redis_reliable';
// Use this to only use Redis for a specific queue.
// $settings['queue_service_aggregator_feeds'] = 'queue.redis';
// Use this to use reliable queue implementation.
// $settings['queue_service_aggregator_feeds'] = 'queue.redis_reliable';
}
3 - Varnish
To properly configure Varnish with Drupal you should ensure the following configuration is your default.vcl
file.
Note: Some customizations might be necessary depending on your individual requirements.
vcl 4.0;
import std;
import directors;
backend nginx {
.host = "hostname-nginx";
.host_header = "hostname-nginx";
.port = "80";
}
sub vcl_init {
new backends = directors.round_robin();
backends.add_backend(nginx);
}
sub vcl_recv {
set req.http.X-Forwarded-Host = req.http.Host;
if (!req.http.X-Forwarded-Proto) {
set req.http.X-Forwarded-Proto = "http";
}
# Answer healthcheck
if (req.url == "/_healthcheck" || req.url == "/healthcheck.txt") {
return (synth(700, "HEALTHCHECK"));
}
set req.backend_hint = backends.backend();
# Answer healthcheck
if (req.url == "/_healthcheck" || req.url == "/healthcheck.txt") {
return (synth(700, "HEALTHCHECK"));
}
set req.backend_hint = backends.backend();
# Always cache certain file types
# Remove cookies that Drupal doesn't care about
if (req.url ~ "(?i)\.(asc|dat|tgz|png|gif|jpeg|jpg|ico|swf|css|js)(\?.*)?$") {
unset req.http.Cookie;
} else if (req.http.Cookie) {
set req.http.Cookie = ";" + req.http.Cookie;
set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
set req.http.Cookie = regsuball(req.http.Cookie, ";(SESS[a-z0-9]+|SSESS[a-z0-9]+|NO_CACHE)=", "; \1=");
set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
if (req.http.Cookie == "") {
unset req.http.Cookie;
} else {
return (pass);
}
}
# If POST, PUT or DELETE, then don't cache
if (req.method == "POST" || req.method == "PUT" || req.method == "DELETE") {
return (pass);
}
# Happens before we check if we have this in cache already.
#
# Typically you clean up the request here, removing cookies you don't need,
# rewriting the request, etc.
return (hash);
#return (pass);
}
sub vcl_backend_fetch {
# NEW
set bereq.http.Host = "hostname-nginx";
# Don't add 127.0.0.1 to X-Forwarded-For
set bereq.http.X-Forwarded-For = regsub(bereq.http.X-Forwarded-For, "(, )?127\.0\.0\.1$", "");
}
sub vcl_backend_response {
if (beresp.http.Location) {
set beresp.http.Location = regsub(
beresp.http.Location,
"^https?://[^/]+/",
bereq.http.X-Forwarded-Proto + "://" + bereq.http.X-Forwarded-Host + "/"
);
}
# Only cache select response codes
if (beresp.status == 200 || beresp.status == 203 || beresp.status == 204 || beresp.status == 206 || beresp.status == 300 || beresp.status == 301 || beresp.status == 404 || beresp.status == 405 || beresp.status == 410 || beresp.status == 414 || beresp.status == 501) {
# Cache for 5 minutes
set beresp.ttl = 5m;
set beresp.grace = 12h;
set beresp.keep = 24h;
} else {
set beresp.ttl = 0s;
}
}
sub vcl_deliver {
# Remove identifying information
unset resp.http.Server;
unset resp.http.X-Powered-By;
unset resp.http.X-Varnish;
unset resp.http.Via;
# Comment these for easier Drupal cache tag debugging in development.
unset resp.http.Cache-Tags;
unset resp.http.X-Drupal-Cache-Contexts;
# Add Content-Security-Policy
# set resp.http.Content-Security-Policy = "default-src 'self' *.example.ca *.example.ca; style-src 'self' 'unsafe-inline' *.example.ca https://fonts.googleapis.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.example.ca *.adobedtm.com use.fontawesome.com blob:; connect-src 'self' *.example.ca *.omtrdc.net *.demdex.net *.everesttech.net; img-src 'self' *.example.ca *.omtrdc.net *.demdex.net *.everesttech.net data:; font-src 'self' *.example.ca https://fonts.gstatic.com";
# Add CORS Headers
# if (req.http.Origin ~ "(?i)\.example\.ca$") {
# if (req.url ~ "\.(ttd|woff|woff2)(\?.*)?$") {
# set resp.http.Access-Control-Allow-Origin = "*";
# set resp.http.Access-Control-Allow-Methods = "GET";
# }
# }
# Add X-Frame-Options
if (req.url ~ "^/livechat" || req.url ~ "^/(en/|fr/)?entity-browser/") {
set resp.http.X-Frame-Options = "SAMEORIGIN";
} else {
set resp.http.X-Frame-Options = "DENY";
}
set resp.http.X-Content-Type-Options = "nosniff";
set resp.http.X-XSS-Protection = "1; mode=block";
# Happens when we have all the pieces we need, and are about to send the
# response to the client.
#
# You can do accounting or modifying the final object here.
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
# Handle errors
if ( (resp.status >= 500 && resp.status <= 599)
|| resp.status == 400
|| resp.status == 401
|| resp.status == 403
|| resp.status == 404) {
return (synth(resp.status));
}
}
sub vcl_synth {
# Remove identifying information
unset resp.http.Server;
unset resp.http.X-Powered-By;
unset resp.http.X-Varnish;
unset resp.http.Via;
# Add Content-Security-Policy
# set resp.http.Content-Security-Policy = "default-src 'self' *.example.ca; style-src 'self' 'unsafe-inline' *.example.ca; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.example.ca *.adobedtm.com use.fontawesome.com blob:; connect-src 'self' *.example.ca *.omtrdc.net *.demdex.net *.everesttech.net; img-src 'self' *.example.ca data:;";
# set resp.http.X-Content-Type-Options = "nosniff";
# set resp.http.X-Frame-Options = "DENY";
# set resp.http.X-XSS-Protection = "1; mode=block";
# if (resp.status >= 500 && resp.status <= 599) {
# set resp.http.Content-Type = "text/html; charset=utf-8";
# synthetic(std.fileread("/data/configuration/varnish/errors/503.html"));
# return (deliver);
# } elseif (resp.status == 400) { # 400 - Bad Request
# set resp.http.Content-Type = "text/html; charset=utf-8";
# synthetic(std.fileread("/data/configuration/varnish/errors/400.html"));
# return (deliver);
# } elseif (resp.status == 401) { # 401 - Unauthorized
# set resp.http.Content-Type = "text/html; charset=utf-8";
# synthetic(std.fileread("/data/configuration/varnish/errors/401.html"));
# return (deliver);
# } elseif (resp.status == 403) { # 403 - Forbidden
# set resp.http.Content-Type = "text/html; charset=utf-8";
# synthetic(std.fileread("/data/configuration/varnish/errors/403.html"));
# return (deliver);
# } elseif (resp.status == 404) { # 404 - Not Found
# set resp.http.Content-Type = "text/html; charset=utf-8";
# synthetic(std.fileread("/data/configuration/varnish/errors/404.html"));
# return (deliver);
# } else
if (resp.status == 700) { # Respond to healthcheck
set resp.status = 200;
set resp.http.Content-Type = "text/plain";
synthetic ( {"OK"} );
return (deliver);
}
}
##
# ERROR HANDLING
##
# sub vcl_backend_error {
# set beresp.http.Content-Type = "text/html; charset=utf-8";
# synthetic(std.fileread("/data/configuration/varnish/errors/503.html"));
# return (deliver);
# }