Going crazy with caching
Caching pages of logged in users
Bulgaria PHP Conference,
September 26th, 2015
© David Buchmann
HTTP Caching
- Why re-render content that does not change?
- Scaling and response time
- Less servers, more users
What is a reverse proxy again?
VCL Hook Functions
- vcl_recv: entry point, lookup or pass
- vcl_fetch: receive from backend, cache or not
- vcl_deliver: remove headers not for client
- vcl_hash: determine cache key
- vcl_hit: found in cache, deliver or pass
- vcl_miss: pass or fetch
- vcl_pipe: do not alter request
- vcl_error: define error page
Default Varnish behaviour
- Only attempt to cache GET and HEAD request
- Never cache request with cookies / authorization
- Never cache response with set-cookie
- Only cache safe responses (status 200, 203, 300, 301, 302, 307, 404, 410)
Default Varnish behaviour
- Only attempt to cache GET and HEAD request
- Never cache request with cookies / authorization
- Never cache response with set-cookie
- Only cache safe responses (status 200, 203, 300, 301, 302, 307, 404, 410)
Strategies when working with sessions
- Avoid Sessions, remove when no longer needed
- Cache lookup despite cookies
- Prevent caching when specific
- Vary on Cookie header
- User Context: Cache by group
Avoid Sessions
Avoid Session
- Delete cookie as soon as no longer needed
- Watch out for autostarted PHP sessions
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-3600);
}
Cleanup Cookies: Remove all but session cookie
sub vcl_recv {
set req.http.cookie = ";" + req.http.cookie;
set req.http.cookie = regsuball(req.http.cookie, "; +", ";");
set req.http.cookie = regsuball(req.http.cookie, ";(PHPSESSID)=", "; \1=");
set req.http.cookie = regsuball(req.http.cookie, ";[^ ][^;]*", "");
set req.http.cookie = regsuball(req.http.cookie, "^[; ]+|[; ]+$", "");
if (req.http.Cookie == "") {
remove req.http.Cookie;
}
}
varnish-cache.org/trac/wiki/VCLExampleRemovingSomeCookies #RemovingallBUTsomecookies
Cache lookup despite cookies
Cache lookup despite cookies
- Very easy to shoot yourself in the foot!
- The backend must send correct cache headers
builtin.vcl
// builtin.vcl
sub vcl_recv {
// ...
if (req.method != "GET" && req.method != "HEAD") {
/* We only deal with GET and HEAD by default */
return (pass);
}
if (req.http.Authorization || req.http.Cookie) {
/* Not cacheable by default */
return (pass);
}
return (hash);
}
Cache lookup despite cookies
// default.vcl
sub vcl_recv {
// ...
if (req.method != "GET" && req.method != "HEAD") {
/* We only deal with GET and HEAD by default */
return (pass);
}
return (hash);
}
User Context
Introducing the User Context Hash
- Group based caching
- Transparent (reverse proxy does most of the job)
- Computed for every user in application (footprint)
- Hash generation can be customized
- Cached by session ID, using HTTP cache of course!
User Context
What about the real live?
Mix solutions
- Edge side includes
- Ajax
- Move logic to frontend
Edge Side Includes
Use Edge Side Includes
Like server side include, but on Varnish:
- Content embeds URLs to sub-parts of the content
- Varnish fetches and caches elements separatly
- Individual caching rules per fragment.
E.g. some elements vary on cookie, different TTL, ...
- Symfony has built-in support for ESI
ESI HTML
<html>
<body>
Main body.
<esi:include src="fragment.php" />
</body>
</html>
sub vcl_recv {
// Add a Surrogate-Capability header
// to announce ESI support.
set req.http.Surrogate-Capability = "abc=ESI/1.0";
}
sub vcl_backend_response {
// Check for ESI acknowledgement
// and remove Surrogate-Control header.
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
}
ESI
- Pro
- Separate caching for fragments
- Server side, makes search engines happy
- Simple to use in the application
- Con
- Each include must be resolved before response can be sent
Symfony has built-in ESI support
# app/config/config.yml
framework:
esi: { enabled: true }
fragments: { path: /_fragment }
Make sure either your webserver is only reachable from the Varnish server or add access restrictions for /_fragment
{# index.html.twig #}
{% render_esi(controller( 'AppBundle:Comments:comments', {'param': 42 })) %}
Ajax
Ajax
- Separate endpoint in application for each fragment
- Do request when page loaded, e.g. with jquery:
$(document).ready(function () {
$.ajax({
url: "/sidebar.html"
}).done(function( html ) {
$( "#sidebar" ).append( html );
});
});
Ajax
- Pro
- Can be executed after page already shown
- Good for slow, non-critical information
- Con
- Network overhead (mobile...)
- More complicated to maintain
- Not seen by search engines
Logic in Frontend
Logic in Frontend
- User state is communicated to javascript
- Alter page when loaded, e.g. with jquery:
$(document).ready(function () {
$("#labels-edit").show();
$("#milestone-edit").show();
$("#assignee-edit").show();
});
Logic in Frontend
- Pro
- Fine grained control
- No network overhead, easy on backend
- Good for complex user interfaces
- Con
- Not suitable for HTML fragments
- Development effort
- Even more complicated to maintain
- Search engines likely never see this anyways
Questions / Input / Feedback ?
Twitter: @dbu
Tools
FOSHttpCache
- Cache tagging and invalidation
- User context
FOSHttpCacheBundle
- Request matcher for caching headers
- Annotations for caching headers and invalidation
- Invalidation service for active invalidation
- Listener for user context