Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Fortifying Craft for High Traffic

Prateek Rungta
September 20, 2019

Fortifying Craft for High Traffic

In this talk, we go through a multi-tiered caching strategy using Craft CMS and NGINX that enables a single VPS to consistently deliver sub-200ms response times, even while handling loads of 10 to 100 concurrent requests per second.

View a recording of the session from the event. Delivered in Montréal at Dot All 2019.

Prateek Rungta

September 20, 2019
Tweet

More Decks by Prateek Rungta

Other Decks in Technology

Transcript

  1. #perf Combine CSS & JS Minification Unused CSS CDN GZIP

    HTTP/2 Automation Bundling Caching VPS Image Optimisation DNS prefetch Responsive Images Lazy loading Image Sprites Async Service Workers Server Push Code Splitting No .htaccess
  2. Dynamic Static Contextual Private e.g. contact form e.g. live preview

    e.g. news article e.g. mobile/desktop variants
  3. Other Layers e.g. Database queries Network e.g. CDN Web Server

    e.g. Nginx e.g. Craft – full pages, template partials Application
  4. “There are only two hard things in Computer Science: cache

    invalidation and naming things.” Phil Karlton
  5. home.twig contact.twig news/ _entry.twig {% cache %} {% cache %}

    {% cache %} {% endcache %} {% endcache %} {% endcache %}
  6. “Your caches will automatically clear when any elements (entries, assets,

    etc.) within the tags are saved or deleted.” Craft Docs for {% cache %}
  7. “Possible side effects include stale content, excessively long-running background tasks,

    stuck tasks, and in rare cases, death.” Craft Docs for {% cache %} ⚠
  8. #2 Guiding Tech Web Publication 9000+ pages 3 million+ monthly

    visitors 10+ authors editing content daily
  9. Deleting stale template caches Deleting stale template caches An Entry

    is edited… 92 tracked queries 360,000+ tracked queries
  10. ☑ Deleting stale … An Entry is edited… ⏳ Deleting

    stale … Deleting stale … Deleting stale … Deleting stale … Deleting stale … Deleting stale … Deleting stale … ☑ ☑ ☑
  11. ⏳ Deleting stale … Deleting stale … Deleting stale …

    Deleting stale … “excessively long- running background tasks, stuck tasks”
  12. 1. Assign tags to Content & Caches 2. When a

    piece of content is edited, go through its tags 3. Find all caches with any matching tags 4. Delete these caches
  13. • Entry • Category • Global • Asset • User,

    etc. • Sections • Category Groups • Global Sets • Asset Volumes • User Groups, etc. Element Types Groups Tag Options
  14. Content tags are auto assigned Element: Entries Section: News News

    Entry Global Set Work Entry Element: Globals GlobalS: Footer Element: Entrie Section: Work
  15. news/ _entry.twig home.twig {% cache %} {% cache %} {%

    cache %} {% endcache %} {% endcache %} {% endcache %} contact.twig
  16. {% cacheflag %} {% cacheflag %} {% cacheflag %} {%

    endcacheflag %} {% endcacheflag %} {% endcacheflag %} news/ _entry.twig home.twig contact.twig
  17. {% cacheflag %} {% cacheflag %} {% cacheflag %} {%

    endcacheflag %} {% endcacheflag %} {% endcacheflag %} news/ _entry.twig home.twig contact.twig Section: News Element: Globals Element: Entries Element: Globals Section: Contact Element: Globals
  18. {% cacheflag %} {% cacheflag %} {% cacheflag %} {%

    endcacheflag %} {% endcacheflag %} {% endcacheflag %} news/ _entry.twig home.twig contact.twig Section: News Element: Globals Element: Entries Element: Globals Section: Contact Element: Globals If a news entry is edited
  19. {% cacheflag %} {% cacheflag %} {% cacheflag %} {%

    endcacheflag %} {% endcacheflag %} {% endcacheflag %} news/ _entry.twig home.twig contact.twig Section: Contact Element: Globals If a news entry is edited Element: Entries Element: Globals Section: News Element: Globals
  20. {% cacheflag %} {% cacheflag %} {% cacheflag %} {%

    endcacheflag %} {% endcacheflag %} {% endcacheflag %} news/ _entry.twig home.twig contact.twig Section: News Element: Globals Element: Entries Element: Globals Section: Contact Element: Globals If a work entry is edited
  21. {% cacheflag %} {% cacheflag %} {% cacheflag %} {%

    endcacheflag %} {% endcacheflag %} {% endcacheflag %} news/ _entry.twig home.twig contact.twig Section: Contact Element: Globals If a work entry is edited Element: Entries Element: Globals Section: News Element: Globals
  22. {% cacheflag %} {% cacheflag %} {% cacheflag %} {%

    endcacheflag %} {% endcacheflag %} {% endcacheflag %} news/ _entry.twig home.twig contact.twig Section: News Element: Globals Element: Entries Element: Globals Section: Contact Element: Globals If the footer globalset is edited
  23. {% cacheflag %} {% cacheflag %} {% cacheflag %} {%

    endcacheflag %} {% endcacheflag %} {% endcacheflag %} news/ _entry.twig home.twig contact.twig Section: Contact Element: Globals If the footer globalset is edited Element: Entries Element: Globals Section: News Element: Globals
  24. {% cacheflag %} {% cacheflag %} {% cacheflag %} {%

    endcacheflag %} {% endcacheflag %} {% endcacheflag %} news/ _entry.twig home.twig contact.twig
  25. news/ _entry.twig home.twig contact.twig {% extends '_init' %} {% extends

    '_init' %} {% extends '_init' %} _init.twig {% cacheflag %} {% endcacheflag %}
  26. Dynamic Static Contextual Private e.g. contact form e.g. Live Preview

    e.g. news article e.g. mobile / desktop variants
  27. 2. Dynamic – unique response for each request {# contact.twig

    #} {% nocache %}
 {{ csrfInput() }}
 {% endnocache %} Use the No-Cache plugin e.g. contact form
  28. 3. Contextual – differ based on request params Use a

    cache key e.g. mobile / desktop version, language versions {# _init.twig #} {% cacheflag … using cacheKey … %}
  29. {# _init.twig #}
 {% set cacheKey = { 
 device:

    craft.app.request.isMobileBrowser
 ? 'mobile' : 'nonmobile', 
 user: currentUser
 ? 'user' : 'guest', 
 }|join(',') %}
  30. 4. Private – bypass the cache Cache Flag natively bypasses

    Live Preview and Draft URLs e.g. Live preview, Draft
  31. 4. Private – bypass the cache Use a boolean flag

    e.g. Form submissions, Live preview, Draft {# _init.twig #} {% cacheflag … if isCacheable %}
  32. {# _init.twig #}
 
 {% set isCacheable =
 not craft.app.request.isPost


    and isCacheable|default(true)
 %} {# _private.twig #}
 
 {% set isCacheable = false %}
  33. {# Cache Config #}
 {% set cacheflags = cacheflags ??

    'entries|assets|globals|categories|users' %}
 {% set cacheKeyPrefix = {
 device: craft.app.request.isMobileBrowser ? 'mobile' : 'nonmobile',
 user: currentUser ? 'user' : 'guest',
 }|join(',') %}
 {% set cacheableEnv = craft.app.request.isPost
 and not (doNotCache ?? false)
 and not craft.app.session.hasFlash('error')
 and not craft.app.session.hasFlash('notice')
 %}
 
 {# Figure out if page should be cached #}
 {%- if cacheableEnv %}
 
 {# If a cacheKey is set, use that to globally cache the rendered page #}
 {% if cacheKey ?? false %}
 {% cacheflag flagged cacheflags globally using key (cacheKeyPrefix ~ ':' ~ cacheKey) for 1 month %}
 {%- minify html %}
 {{ block('html') }}
 {% endminify -%}
 {% endcacheflag %} 
 {% else %} 
 {# No cacheKey set, cache the rendered page by url (not globally) #}
 {% cacheflag flagged cacheflags using key cacheKeyPrefix for 1 month %}
 {%- minify html %}
 {{ block('html') }}
 {% endminify -%}
 {% endcacheflag %}
 {% endif %}
 
 {% else %}
 {% block html %}{% endblock %}
 {% endif %}
  34. Advantages • Speed (up to 14 times faster TTFB) •

    Content is always fresh, never stale • Scales up to thousands of entries • Scales up to tens of editors / hour
  35. • Cache is busted frequently • Some users will encounter

    uncached responses • Developers have to specify tags Drawbacks
  36. OK — /home (expired) GET /home Visitors Nginx Craft GET

    /home OK — /home GET /home GET /home OK — /home (expired) OK — /home (expired) GET /home OK — /home (expired on arrival) ⏳ ⏳ GET /home
  37. OK — /home (expired) GET /home Visitors Nginx Craft GET

    /home OK — /home GET /home GET /home OK — /home (expired) OK — /home (expired) GET /home OK — /home (expired on arrival) ⏳ ⏳ GET /home Every request gets a fast response, no waiting
  38. OK — /home (expired) GET /home Visitors Nginx Craft GET

    /home OK — /home GET /home GET /home OK — /home (expired) OK — /home (expired) GET /home OK — /home (expired on arrival) ⏳ ⏳ GET /home Every request gets a fast response, no waiting Craft handles a fraction of the traffic
  39. http { … server { … # Setup FastCGI Cache

    # <Path>, <Zone>, etc. fastcgi_cache_lock on; fastcgi_cache_use_stale updating; fastcgi_cache_background_update on; location ~ \.php$ { … # Qualifiers } } }
  40. Dynamic Static Contextual Private e.g. contact form e.g. Live Preview

    e.g. news article e.g. mobile / desktop variants
  41. 1. Static – same response for each request Enable FastCGI

    Cache in the PHP location block e.g. news article
  42. 1. Static – same response for each request Enable FastCGI

    Cache in the PHP location block e.g. news article fastcgi_cache_valid 200 301 404 1s;
  43. 2. Dynamic – unique response for each request FastCGI automatically

    ignores responses with cookies e.g. contact form
  44. 2. Dynamic – unique response for each request FastCGI automatically

    ignores responses with cookies e.g. contact form fastcgi_ignore_headers Cache-Control Expires Set-Cookie; fastcgi_hide_header Set-Cookie;
  45. 2. Dynamic – unique response for each request Also, bypass

    cache by sending a custom header from Craft e.g. page that shows current time, contact form
  46. 2. Dynamic – unique response for each request Also, bypass

    cache by sending a custom header from Craft e.g. page that shows current time, contact form {# _init.twig #} {% header "X-Accel-Expires: 0" %}
  47. 3. Contextual – differ based on request params Add relevant

    request parameters to the cache key e.g. desktop / mobile version
  48. 3. Contextual – differ based on request params Add relevant

    request parameters to the cache key e.g. desktop / mobile version fastcgi_cache_key "$scheme$request_method$host$request_uri";
  49. 3. Contextual – differ based on request-er Add relevant request

    parameters to the cache key e.g. desktop / mobile version fastcgi_cache_key "$is_mobile$scheme$request_method$host$request
  50. 4. Private – bypass the cache Don’t cache POST requests

    a) Form submissions fastcgi_cache_methods GET HEAD POST;
  51. 4. Private – bypass the cache Exclude URLs with `token`

    param a) Form submissions b) Live Preview, Drafts
  52. 4. Private – bypass the cache Exclude URLs with `token`

    param a) Form submissions b) Live Preview, Drafts fastcgi_cache_bypass $arg_token; fastcgi_no_cache $arg_token;
  53. 4. Private – bypass the cache Add a second location

    block for PHP without FastCGI caching a) Form submissions b) Live Preview, Drafts c) Control Panel
  54. location ^~ /admin { try_files $uri $uri/ @phpfpm_nocache; } location

    ^~ /actions/ { … } location ^~ /index.php/admin { … } location ^~ /index.php/actions { … } location @phpfpm_nocache { # PHP # no FastCGI Cache }
  55. 4. Private – bypass the cache Bypass if Craft’s session

    cookie is set a) Form submissions b) Live Preview, Drafts c) Control Panel d) Logged-in users
  56. 4. Private – bypass the cache Bypass if Craft’s session

    cookie is set a) Form submissions b) Live Preview, Drafts c) Control Panel d) Logged-in users fastcgi_cache_bypass $cookie_1031b8c4[…]; fastcgi_no_cache $cookie_1031b8c4[…];
  57. http {
 ...
 # Configure FastCGI cache
 fastcgi_cache_path /var/run/fastcgicache levels=1:2

    keys_zone=fastcgicache:100m inactive=1d;
 # Cache config
 fastcgi_cache_lock on;
 fastcgi_cache_use_stale updating error timeout invalid_header http_500;
 fastcgi_cache_background_update on;
 fastcgi_cache_methods GET HEAD;
 fastcgi_cache_key "$scheme$request_method$host$request_uri";
 
 
 server {
 ...
 
 # Craft-specific location handlers to ensure AdminCP requests route through index.php
 # If you change your `cpTrigger`, change it here as well
 location ^~ /admin {
 try_files $uri $uri/ @phpfpm_nocache;
 }
 location ^~ /actions/ {
 try_files $uri $uri/ @phpfpm_nocache;
 }
 location ^~ /index.php/admin {
 try_files $uri $uri/ @phpfpm_nocache;
 }
 location ^~ /index.php/actions {
 try_files $uri $uri/ @phpfpm_nocache;
 }
 location ~ \.php$ {
 # Enable cache
 fastcgi_cache fastcgicache;
 fastcgi_ignore_headers Cache-Control Expires;
 fastcgi_cache_valid 200 301 302 404 1s;
 fastcgi_cache_bypass $arg_token $cookie_1031b8c41dfff97a311a7ac99863bdc5_identity;
 fastcgi_no_cache $arg_token $cookie_1031b8c41dfff97a311a7ac99863bdc5_identity;
 ...
 }
 
 location @phpfpm_nocache {
 # No FastCGI Cache
 fastcgi_cache_bypass 1;
 fastcgi_no_cache 1;
 
 # PHP
 include fastcgi_params;
 fastcgi_index index.php;
 fastcgi_param SCRIPT_NAME /index.php;
 fastcgi_param SCRIPT_FILENAME $document_root/index.php;
 fastcgi_pass unix:/var/run/$APP_NAME.sock;
 fastcgi_intercept_errors off;
 fastcgi_connect_timeout 300;
 fastcgi_send_timeout 300;
 fastcgi_read_timeout 300;
 }
 }
 }
  58. • Near instant responses • Exponentially scales up capacity to

    handle traffic & spikes • All* requests get fast responses • Can make a cheap VPS fly *Except the very first Advantages
  59. • Both strategies are great #perf boosters • Combined, they

    can work wonders on Craft sites • Drastically reduces infrastructure cost • No negative impact on AX • No user or client complaints about stale content • No JavaScript usage or dependency