4. Making Symfony shine with Varnish
Do we need a cache accelerator?
• Symfony is FAST considering all the features it provides
• See my talk in last year’s deSymfony conference in Madrid (in
Spanish):
http://www.desymfony.com/ponencia/2013/porque-symfony2-
es-rapido
6. Making Symfony shine with Varnish
Our case: clippingbook.com
• We were able to handle 100 req/sec
• But this was not enough to handle our load, specially when
doing Facebook promotions
• We chose Symfony because of its lower costs of development
and manteinance, not for its performance
• We do not want to renounce to any Symfony features (ORM,
Twig templates, ...)
• We could have scaled vertically or horizontally but chose to
implement a caching strategy first
7. Making Symfony shine with Varnish
The solution: Varnish
• The solution: install Varnish Cache
• Varnish Cache is a web application accelerator also known as a
caching HTTP reverse proxy
• It sits in front of your HTTP server and caches its responses,
serving content from the cache whenever possible.
• Result: we can now serve 10000 req/sec, a 100x improvement
8. Making Symfony shine with Varnish
What we will not cover
• How HTTP caching works. For more information see:
http://tools.ietf.org/pdf/rfc2616.pdf (HTTP 1.1 specification, see
section 13 for caching)
http://symfony.com/doc/current/book/http_cache.html (HTTP
caching chapter in the Symfony Book)
• Basic Varnish installation and configuration. See Fabien’s talk:
http://www.desymfony.com/ponencia/2012/varnish
9. Making Symfony shine with Varnish
What we will cover
• Why Varnish
• Quick overview of Varnish configuration
• Varnish 4. What’s new
• Using Varnish with Symfony:
• Backends
• URL normalization
• Cookies and sessions
• Internacionalization
• Serving content for different devices
• Defining caching headers
• Cache invalidation
• Cache validation
• Edge Side Includes (ESI)
10. Making Symfony shine with Varnish
Why Varnish?
PROs
• Varnish is really fast and highly configurable
• It is well documented in the Symfony documentation
• There are some bundles which help you interact with it
• Fabien’s talk provided very good information on how to use it
CONs
• Varnish documentation not too good / VCL can be cryptic
• It does not handle SSL
• Only runs on 64 bit machines
11. Making Symfony shine with Varnish
Varnish configuration overview
• Varnish uses VCL, a DSL similar to C or Perl
• Configuration saved to a file, usually /etc/varnish/default.vcl
• Translated into C, compiled and linked => fast
• Uses a number of subroutines which are called at specific times
during the handling of the request. For example vcl_recv
• These functions return a value which defines the next action that
the system will take. For example fetch
• There is a default VCL code for each function which is executed if
no value is returned
• We have some objects which represent the request (req), the
response (resp), the backend request (bereq), the backend
response (beresp) and the object in the cache (obj)
sub vcl_miss {
return (fetch);
}
13. Making Symfony shine with Varnish
Request flow
• A request is received (vcl_recv) and we decide if we want to look it
up in the cache (hash) or not (pass)
• If we do not look it up in the cache (vcl_pass) we fetch the
response from the backend (fetch) and don´t store it in the cache
• If we want to look it up, we create a hash for the content
(vcl_hash) and then look it up (lookup)
• If we find it in the cache (vcl_hit) we deliver it (deliver)
• If we don’t find it in the cache (vcl_miss) we fetch the response
from the backend (fetch)
• If we need to fetch the content, we build a request for the backend
and send it (vcl_backend_fetch)
• We receive a response from the back end (vcl_backend_response),
decide if we want to cache it and deliver it (deliver)
• We finally deliver the response to the client (vcl_deliver)
14. Making Symfony shine with Varnish
Varnish 4: what’s new
• Different threads are used for serving client requests and
backend requests
• This split allows Varnish to refresh content in the background
while serving stale content quickly to the client.
• Varnish now correctly handles cache validation, sending If-
None-Match and If-Modified-Since headers and processing
Etag and Last-Modified headers
15. Making Symfony shine with Varnish
Varnish 4: what’s changed
• req.request is now req.method (for example POST)
• vcl_fetch is now vcl_backend_response
• We have a new vcl_backend_fetch function
• To mark responses as uncacheable (hit for pass) we now use
beresp.uncacheable = true
• The purge function is no longer available. You purge content by
returning purge from vcl_recv
• vcl_recv must now return hash instead of lookup
• vcl_hash must now return lookup instead of hash
• vcl_pass must now return fetch instead of pass
• Backend restart is now retry
• Logging tools like varnishlog now have a new filtering language
which means their syntax has changed (-m option => -q)
16. Making Symfony shine with Varnish
Load balancing: backends
backend back1 {
.host = "back1.clippingbook.com";
.port = "80";
}
backend back2 {
.host = "back2.clippingbook.com";
.port = "80";
}
sub vcl_init {
new backs = directors.hash();
backs.add_backend(back1,1);
backs.add_backend(back2,1);
}
sub vcl_recv {
set req.backend_hint = backs.backend(client.identity);
}
17. Making Symfony shine with Varnish
Load balancing: backends
• Varnish includes a health check mechanism and can exclude
backends which are not healthy
• There are other load balancing mechanisms: random, round-robin,
url-based (or build your own)
• BUT if you are using the standard file-based session save
mechanism of Symfony the only method safe to use is hash
based on client ip or client session cookie
• Even this can lead to problems if one server turns unhealthy
and Varnish has to redirect to another backend
• Our recommendation: switch to a shared session server using a
database (PdoSessionHandler), Memcached
(MemcachedSessionHandler) or Redis (ScnRedisBundle)
18. Making Symfony shine with Varnish
URL normalization
• In vcl_hash we calculate a hash to look up the content in the
cache. By default it uses the URL + the host (or IP)
• We want to normalize this URL/host in order to avoid having
repeated content in the cache
• Convert the host to lowercase using std.tolower
• Remove www from the host if present
• Normalize all the query parameters using std.querysort
• Use RouterUnslashBundle to redirect all URLs to the version not
ending in /
• Note that this hash does not include Vary content
sub vcl_hash {
set req.http.host = std.tolower(req.http.host);
set req.http.host = regsub(req.http.host, "^www.", "");
set req.url = std.querysort(req.url);
}
19. Making Symfony shine with Varnish
Cookies and sessions
• Varnish by default will not cache anything which has a cookie
• Symfony sets a PHPSESSID cookie in almost all responses
• By default no content will be cached!
• We want to pass the PHPSESSID cookie to the backend but still
cache some pages even if it is set
• We must not cache any page where this cookie produces a
different response: logged users, forms (CSRF), flashes
• We do not want to cache any page for logged in users
• Most cookies are used by the client side and can be ignored
• There are some cookies which produce a different response but
it is the same for all users => we can Vary on them
• We want to clear all cookies for static content
20. Making Symfony shine with Varnish
Cookies and sessions
sub vcl_recv {
set req.http.X-cookie = req.http.cookie;
if (!req.http.Cookie ~ "Logged-In") {
unset req.http.Cookie;
}
if (req.url ~ ".(png|gif|jpg|css|js|html)$") {
unset req.http.cookie;
}
}
sub vcl_hash {
set req.http.cookie = req.http.X-cookie;
if (req.http.cookie ~ "hide_newsletter=") {
set req.http.X-Newsletter = 1;
}
}
sub vcl_pass {
set req.http.cookie = req.http.X-cookie;
}
21. Making Symfony shine with Varnish
Cookies and sessions
sub vcl_backend_response {
if (!beresp.http.Vary) {
set beresp.http.Vary = "X-Newsletter";
} elseif (beresp.http.Vary !~ "X-Newsletter") {
set beresp.http.Vary = beresp.http.Vary + ", X-Newsletter";
}
if (bereq.url ~ ".(png|gif|jpg|css|js|html)$") {
unset beresp.http.set-cookie;
}
}
sub vcl_deliver {
set resp.http.Vary = regsub(resp.http.Vary, "X-Newsletter",
"Cookie");
}
22. Making Symfony shine with Varnish
Cookies and sessions
• To create the Logged-In cookie we define a kernel.response
listener, injecting the security.context and adding/removing
the cookie as needed
23. Making Symfony shine with Varnish
Cookies and sessions
public function onKernelResponse (FilterResponseEvent $event)
{
$response = $event->getResponse();
$request = $event->getRequest();
if ($this->context->getToken() && $this->context-
>isGranted('IS_AUTHENTICATED_FULLY')) {
if (!$request->cookies->has('Logged-In')) {
$cookie = new Cookie ('Logged-In','true');
$response->headers->setCookie($cookie);
}
} else {
if ($request->cookies->has('Logged-In')) {
$response->headers->clearCookie('Logged-In');
}
}
}
24. Making Symfony shine with Varnish
Internacionalization
• If you return different content depending on a header, use the
Vary header. A common case is returning different content
based on the Accept-Language header
• But you should normalize it or your cache won’t be efficient
if (req.http.Accept-Language) {
if (req.http.Accept-Language ~ "en") {
set req.http.Accept-Language = "en";
} elsif (req.http.Accept-Language ~ "es") {
set req.http.Accept-Language = "es";
} else {
unset req.http.Accept-Language
}
}
• This is a bit simplistic. Use
https://github.com/cosimo/varnish-accept-language
• Varnish will automatically take care of Accept-Encoding
25. Making Symfony shine with Varnish
Device detection
• Another case may be device detection. We want to normalize
the user-agent and Vary on it. We can use
https://github.com/varnish/varnish-devicedetect
include "devicedetect.vcl";
sub vcl_recv { call devicedetect; } #sets X-UA-Device header
sub vcl_backend_response {
if (!beresp.http.Vary) {
set beresp.http.Vary = "X-UA-Device";
} elseif (beresp.http.Vary !~ "X-UA-Device") {
set beresp.http.Vary = beresp.http.Vary + ", X-UA-Device";
}
}
sub vcl_deliver {
set resp.http.Vary = regsub(resp.http.Vary, "X-UA-Device",
"User-Agent");
}
26. Making Symfony shine with Varnish
Device detection
• We can copy this X-UA-Device header to the user-agent
header (but we are losing information)
sub vcl_backend_fetch {
set bereq.http.user-agent = bereq.http.X-UA-Device;
}
• Else we can use the X-UA-Device directly. If, for example, we
use LiipThemeBundle, we can configure it:
liip_theme:
autodetect_theme: acme.device.detector
• acme.device.director is a service which implements the
LiipThemeBundleHelperDeviceDetectionInterface
interface and which uses X-UA-Device to choose a theme
27. Making Symfony shine with Varnish
Defining caching headers
• Set them directly in the Response object
$response->setSharedMaxAge(600);
$response->setPublic();
$response->setVary('Accept-Language');
• Use SensioFrameworkExtraBundle and the @Cache annotation
use SensioBundleFrameworkExtraBundleConfigurationCache;
/**
* @Cache(smaxage="600")
* @Cache(public=true)
* @Cache(vary={"Accept-Language"})
*/
28. Making Symfony shine with Varnish
Defining caching headers
• Use FOSHttpCacheBundle to set them in your config file
fos_http_cache:
cache_control:
rules:
-
match:
attributes: {route: ^book_list$ }
headers:
cache_control: { public: true, s_maxage: 600 }
-
match:
path: ^/info/*$
headers:
cache_control: { public: true, s_maxage: 3600 }
vary: Accept-Language
29. Making Symfony shine with Varnish
Cache invalidation
• First use case: update pages when you deploy new code
• If it is a minor and non-BC breaking change, just wait for the
cache expiration headers to do their job.
• You may need to use some cache busting mechanism like the
assets_version parameter for cache validation
• If it is a major or BC-breaking change, we just bite the bullet and
clear the whole cache by restarting Varnish
service varnish restart
• Downtime is almost inexistent but you will lose all your cached
content
• If this is important, you may want to build a cache warmer
which preloads all your important urls into the cache
30. Making Symfony shine with Varnish
Cache invalidation
• Second use case: a more granular approach: invalidate
individual pages when the underlying data changes
• We can use FOSHttpCacheBundle. First configure Varnish:
acl invalidators {
"back1.clippingbook.com";
"back2.clippingbook.com";
}
sub vcl_recv {
if (req.method == "PURGE") {
if (!client.ip ~ invalidators) {
return (synth(405, "Not allowed"));
}
return (purge);
}
if (req.http.Cache-Control ~ "no-cache" && client.ip ~
invalidators) {
set req.hash_always_miss = true;
}
}
31. Making Symfony shine with Varnish
Cache invalidation
• We then need to configure a Varnish server in Symfony:
fos_http_cache:
proxy_client:
varnish:
servers: xxx.xxx.xxx.xxx #IP of Varnish server
base_url: clippingbook.com
• We can now invalidate or refresh content programatically
$cacheManager = $container ->
get('fos_http_cache.cache_manager');
$cacheManager->invalidatePath('/books');
$cacheManager->refreshRoute('book_show', array('id' =>
$bookId));
$cacheManager->flush(); //optional
32. Making Symfony shine with Varnish
Cache invalidation
• We can also use annotations:
use FOSHttpCacheBundleConfigurationInvalidatePath;
/**
* @InvalidatePath("/books")
* @InvalidateRoute("book_show", params={"id" =
{"expression"="id"}})")
*/
public function editBookAction($id)
{
}
• This needs that SensioFrameworkExtraBundle is available and, if
we use expressions, that the ExpressionLanguage component is
installed
33. Making Symfony shine with Varnish
Cache invalidation
• Finally, we can set up invalidation in our config file:
fos_http_cache:
invalidation:
rules:
-
match:
attributes:
_route: "book_edit|book_delete"
routes:
book_list: ~
book_show: ~
34. Making Symfony shine with Varnish
Cache validation
• Varnish 4 now supports cache validation
• You should be setting the Etag and/or Last-Modified headers,
which now Varnish understands and supports
• Expiration wins over validation so while the cache is not stale
Varnish will not poll your backend to validate it
• But once the content expires it will call the backend with the
If-None-Match and/or If-Modified-Since headers
• You can use these to determine if you want to send back a
304: Not Modified response
• If you do, Varnish will continue serving the content from the
cache
35. Making Symfony shine with Varnish
Cache validation
public function showBookAction($id, $request)
{
$book = ...;
$response = new Response();
$response->setETag($book->computeETag());
$response->setLastModified($book->getModified());
$response->setPublic();
if ($response->isNotModified($request)) {
return $response; //returns 304
}
... generate and return full response
}
36. Making Symfony shine with Varnish
Edge Side Includes (ESI)
• ESI allows you to have different parts of the page which have
different caching strategies. Varnish will put the page together
• To work with Symfony you have to instruct Varnish to send a
special header advertising this capability and to respond to the
header sent back by Symfony when there is ESI content
sub vcl_recv {
set req.http.Surrogate-Capability = "abc=ESI/1.0";
}
sub vcl_backend_response {
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
}
37. Making Symfony shine with Varnish
Edge Side Includes (ESI)
• Now you need to tell Symfony to enable ESI
• If you are going to reference a controller when including ESI
content you need to enable the FragmentListener so that it
generates URLs for the ESI fragments
• Finally you need to list the Varnish servers as trusted proxies
framework:
esi: { enabled: true }
fragments: { path: /_fragment }
trusted_proxies: [xxx.xxx.xxx.xxx, yyy.yyy.yyy.yyy ]
#IPs of Varnish servers
38. Making Symfony shine with Varnish
Edge Side Includes (ESI)
• In the main controller for the page, set the shared max age
public function indexAction()
{
... generate response
$response->setSharedMaxAge(600);
return $response;
}
• In your template use the render_esi helper to print ESI content
{{ render_esi(controller('...:news', { ’num': 5 })) }}
{{ render_esi(url('latest_news', { ’num': 5 })) }}
• You can now specify a different cache policy for your fragment
public function newsAction()
{
... generate response
$response->setSharedMaxAge(60);
return $response;
}
39. Making Symfony shine with Varnish
Thanks!
¡Gracias! - Thanks!
Any questions?
cgranados@clippingbook.com
@carlos_granados
https://joind.in/talk/view/12942