HTTP caching and PHP
David de Boer
Hello DPC!
• I’m David
• Lead developer at Driebit, Amsterdam
• Open source enthusiast
– Phil Karlton
“There are only two hard things in
Computer Science: cache invalidation and
naming things.”
Why we cache
• Reduce response times
• Reduce server load
• Cheap
How we cache
Client App
Client AppCache
How we cache
Efficient caching
• Maximize cache hits
• TTL ∞



GET	
  /apple	
  HTTP/1.1

Host:	
  fresh-­‐fruit.com

Connection:	
  close

...



!
!
HTTP/1.1	
  200	
  OK

Host:	
  fresh-­‐fruit.com

X-­‐Cache:	
  HIT

Age:	
  567648000

...
• Maximize cache hits
• TTL ∞
• Invalidation
Efficient caching
AppCache
Let’s go invalidate
Cache
• Varnish
• Nginx
Varnish
#	
  /etc/varnish/default.vcl	
  
sub	
  vcl_hit	
  {

	
   if	
  (req.request	
  ==	
  "PURGE")	
  {

	
   	
   purge;

	
  	
  	
  	
  	
  error	
  204	
  "Purged";

	
  	
  	
  }

}	
  
!
sub	
  vcl_miss	
  {

	
   if	
  (req.request	
  ==	
  "PURGE")	
  {

	
   	
   purge;

	
   	
   error	
  204	
  "Purged	
  (Not	
  in	
  cache)";

	
   }

}
$	
  composer	
  require	
  friendsofsymfony/http-­‐cache	
  @alpha
FOSHttpCache
App
Connect and purge
use	
  FOSHttpCacheProxyClientVarnish;



$servers	
  =	
  ['10.0.0.1:6081'];

$proxyClient	
  =	
  new	
  Varnish($servers);	
  
$proxyClient

	
   -­‐>purge('/news/articles/42')

	
   -­‐>purge('/news')

	
   -­‐>flush();
Level up
sub	
  vcl_recv	
  {	
  
	
   if	
  (req.request	
  ==	
  "BAN")	
  {	
  
	
   	
   ban("obj.http.x-­‐host	
  ~	
  "	
  +	
  req.http.x-­‐host	
  
	
  	
  	
  	
  	
  	
   +	
  "	
  &&	
  obj.http.x-­‐url	
  ~	
  "	
  +	
  req.http.x-­‐url	
  
	
   	
   	
   +	
  "	
  &&	
  obj.http.content-­‐type	
  ~	
  "	
  +	
  req.http.x-­‐content-­‐type	
  
	
   	
   );	
  
!
	
   	
   error	
  200	
  "Banned";	
  
	
   }	
  
}	
  
!
sub	
  vcl_fetch	
  {	
  
	
  	
  	
  #	
  Set	
  ban	
  lurker-­‐friendly	
  custom	
  headers	
  
	
   set	
  beresp.http.x-­‐url	
  =	
  req.url;	
  
	
  	
  	
  set	
  beresp.http.x-­‐host	
  =	
  req.http.host;	
  
}
Level up
use	
  FOSHttpCacheCacheInvalidator;	
  
!
$invalidator	
  =	
  new	
  CacheInvalidator(

	
  	
  $proxyClient

);	
  
!
$invalidator

	
  	
  -­‐>invalidateRegex('.*',	
  'image/png')

	
  	
  -­‐>invalidateRegex('^/admin')

	
   -­‐>flush();
Get organized
• Complex relationships between URLs
• Knowing when to invalidate what
• Depends on application logic
Tagging
sub	
  vcl_recv	
  {	
  
	
   if	
  (req.request	
  ==	
  "BAN")	
  {	
  
	
   	
   if	
  (req.http.x-­‐cache-­‐tags)	
  {

	
   	
   	
   ban("obj.http.x-­‐host	
  ~	
  "	
  +	
  req.http.x-­‐host	
  
	
   	
   	
   	
   +	
  "	
  &&	
  obj.http.x-­‐url	
  ~	
  "	
  +	
  req.http.x-­‐url	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  +	
  "	
  &&	
  obj.http.content-­‐type	
  ~	
  "	
  	
  +	
  req.http.x-­‐content-­‐type	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  +	
  "	
  &&	
  obj.http.x-­‐cache-­‐tags	
  ~	
  "	
  	
  +	
  req.http.x-­‐cache-­‐tags	
  
	
   	
   	
   );	
  


	
   	
   	
   error	
  200	
  "Banned";	
  
	
   	
   }	
  else	
  {	
  
	
   	
   	
   #	
  ...	
  
	
   	
   }	
  
	
   }	
  
}	
  
!
#	
  ...
Tagging
//	
  overview.php

header('Cache-­‐Control:	
  max-­‐age=3600');

header('X-­‐Cache-­‐Tags:	
  art-­‐1,art-­‐2,art-­‐3');	
  
!
!
//	
  article.php

header('Cache-­‐Control:	
  max-­‐age=3600');

header('X-­‐Cache-­‐Tags:	
  art-­‐2');	
  
$invalidator-­‐>invalidateTags(['art-­‐2']);
use	
  FOSHttpCacheTestsVarnishTestCase;	
  
!
class	
  MyTest	
  extends	
  VarnishTestCase

{

	
   public	
  function	
  testHit()

	
   {	
  
	
   	
   $first	
  =	
  $this-­‐>getResponse('/cached.php');	
  
	
   	
   $this-­‐>assertMiss($first);	
  
	
   	
   $second	
  =	
  $this-­‐>getResponse('/cached.php');	
  
	
   	
   $this-­‐>assertHit($second);

	
  	
  }

}
OK	
  (1	
  test,	
  2	
  assertions)
Test-driven invalidation
Test-driven invalidation
class	
  MyTest	
  extends	
  VarnishTestCase

{

	
   public	
  function	
  testTags()

	
   {

	
   	
   $miss	
  =	
  $this-­‐>getResponse('/article.php');

	
  	
  	
  	
  	
  $hit	
  =	
  $this-­‐>getResponse('/article.php');	
  
	
  	
  	
  	
  	
  $invalidator-­‐>invalidateTags(['art-­‐2']);

	
  	
  	
  	
  	
  $this-­‐>assertMiss($this-­‐>getResponse('/article.php'));

	
  	
  	
  }

}
OK	
  (1	
  test,	
  1	
  assertion)
Symfony bundle
$	
  composer	
  require	
  friendsofsymfony/http-­‐cache-­‐bundle	
  @alpha	
  
!
/**

	
  *	
  @InvalidateRoute("fruits-­‐overview",	
  params={"type"	
  =	
  "latest"})	
  
	
  *	
  @InvalidateRoute("fruits",	
  params={"num"	
  =	
  "id"})	
  
	
  */

public	
  function	
  editAction($id)

{	
  
	
   #	
  ...



/**

	
  *	
  @Tag(expression="'fruit-­‐'~$id")

	
  */

public	
  function	
  showAction($id)

{	
  
	
   #	
  ...
Symfony bundle
fos_http_cache:	
  
	
   cache_control:	
  
	
   	
   rules:	
  
	
   	
   	
   -­‐	
  
	
   	
   	
   	
   match:	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
   	
   host:	
  ^login.example.com$	
  
	
   	
   	
   	
   headers:	
  
	
   	
   	
   	
   	
   cache_control:	
  
	
   	
   	
   	
   	
   	
   public:	
  false	
  
	
   	
   	
   	
   	
   	
   s_maxage:	
  0	
  
	
   	
   	
   	
   	
   	
   last_modified:	
  "-­‐1	
  hour"	
  
	
   	
   	
   	
   	
   vary:	
  [Accept-­‐Encoding,	
  Accept-­‐Language]	
  
	
   	
   	
   -­‐	
  
	
   	
   	
   	
   match:	
  
	
   	
   	
   	
   	
   attributes:	
  {	
  _controller:	
  ^AcmeBundle:Default:.*	
  }	
  
	
   	
   	
   	
   	
   additional_cacheable_status:	
  [400]	
  
	
   	
   	
   	
   headers:	
  
	
   	
   	
   	
   	
   cache_control:	
  {	
  public:	
  true,	
  max_age:	
  15,	
  s_maxage:	
  30	
  }	
  
Thanks!
david@driebit.nl
https://github.com/FriendsOfSymfony/
FOSHttpCache
https://github.com/FriendsOfSymfony/
FOSHttpCacheBundle
http://foshttpcache.readthedocs.org

HTTP Caching and PHP

  • 1.
    HTTP caching andPHP David de Boer
  • 2.
    Hello DPC! • I’mDavid • Lead developer at Driebit, Amsterdam • Open source enthusiast
  • 3.
    – Phil Karlton “Thereare only two hard things in Computer Science: cache invalidation and naming things.”
  • 4.
    Why we cache •Reduce response times • Reduce server load • Cheap
  • 5.
  • 6.
  • 7.
    Efficient caching • Maximizecache hits • TTL ∞
 

  • 8.
    GET  /apple  HTTP/1.1
 Host:  fresh-­‐fruit.com
 Connection:  close
 ...
 
 ! ! HTTP/1.1  200  OK
 Host:  fresh-­‐fruit.com
 X-­‐Cache:  HIT
 Age:  567648000
 ...
  • 9.
    • Maximize cachehits • TTL ∞ • Invalidation Efficient caching
  • 10.
  • 11.
  • 12.
    Varnish #  /etc/varnish/default.vcl   sub  vcl_hit  {
   if  (req.request  ==  "PURGE")  {
     purge;
          error  204  "Purged";
      }
 }   ! sub  vcl_miss  {
   if  (req.request  ==  "PURGE")  {
     purge;
     error  204  "Purged  (Not  in  cache)";
   }
 }
  • 13.
    $  composer  require  friendsofsymfony/http-­‐cache  @alpha FOSHttpCache App
  • 14.
    Connect and purge use  FOSHttpCacheProxyClientVarnish;
 
 $servers  =  ['10.0.0.1:6081'];
 $proxyClient  =  new  Varnish($servers);   $proxyClient
   -­‐>purge('/news/articles/42')
   -­‐>purge('/news')
   -­‐>flush();
  • 15.
    Level up sub  vcl_recv  {     if  (req.request  ==  "BAN")  {       ban("obj.http.x-­‐host  ~  "  +  req.http.x-­‐host               +  "  &&  obj.http.x-­‐url  ~  "  +  req.http.x-­‐url         +  "  &&  obj.http.content-­‐type  ~  "  +  req.http.x-­‐content-­‐type       );   !     error  200  "Banned";     }   }   ! sub  vcl_fetch  {        #  Set  ban  lurker-­‐friendly  custom  headers     set  beresp.http.x-­‐url  =  req.url;        set  beresp.http.x-­‐host  =  req.http.host;   }
  • 16.
    Level up use  FOSHttpCacheCacheInvalidator;   ! $invalidator  =  new  CacheInvalidator(
    $proxyClient
 );   ! $invalidator
    -­‐>invalidateRegex('.*',  'image/png')
    -­‐>invalidateRegex('^/admin')
   -­‐>flush();
  • 17.
    Get organized • Complexrelationships between URLs • Knowing when to invalidate what • Depends on application logic
  • 18.
    Tagging sub  vcl_recv  {     if  (req.request  ==  "BAN")  {       if  (req.http.x-­‐cache-­‐tags)  {
       ban("obj.http.x-­‐host  ~  "  +  req.http.x-­‐host           +  "  &&  obj.http.x-­‐url  ~  "  +  req.http.x-­‐url                      +  "  &&  obj.http.content-­‐type  ~  "    +  req.http.x-­‐content-­‐type                      +  "  &&  obj.http.x-­‐cache-­‐tags  ~  "    +  req.http.x-­‐cache-­‐tags         );   
       error  200  "Banned";       }  else  {         #  ...       }     }   }   ! #  ...
  • 19.
    Tagging //  overview.php
 header('Cache-­‐Control:  max-­‐age=3600');
 header('X-­‐Cache-­‐Tags:  art-­‐1,art-­‐2,art-­‐3');   ! ! //  article.php
 header('Cache-­‐Control:  max-­‐age=3600');
 header('X-­‐Cache-­‐Tags:  art-­‐2');   $invalidator-­‐>invalidateTags(['art-­‐2']);
  • 20.
    use  FOSHttpCacheTestsVarnishTestCase;   ! class  MyTest  extends  VarnishTestCase
 {
   public  function  testHit()
   {       $first  =  $this-­‐>getResponse('/cached.php');       $this-­‐>assertMiss($first);       $second  =  $this-­‐>getResponse('/cached.php');       $this-­‐>assertHit($second);
    }
 } OK  (1  test,  2  assertions) Test-driven invalidation
  • 21.
    Test-driven invalidation class  MyTest  extends  VarnishTestCase
 {
   public  function  testTags()
   {
     $miss  =  $this-­‐>getResponse('/article.php');
          $hit  =  $this-­‐>getResponse('/article.php');            $invalidator-­‐>invalidateTags(['art-­‐2']);
          $this-­‐>assertMiss($this-­‐>getResponse('/article.php'));
      }
 } OK  (1  test,  1  assertion)
  • 22.
    Symfony bundle $  composer  require  friendsofsymfony/http-­‐cache-­‐bundle  @alpha   ! /**
  *  @InvalidateRoute("fruits-­‐overview",  params={"type"  =  "latest"})    *  @InvalidateRoute("fruits",  params={"num"  =  "id"})    */
 public  function  editAction($id)
 {     #  ...
 
 /**
  *  @Tag(expression="'fruit-­‐'~$id")
  */
 public  function  showAction($id)
 {     #  ...
  • 23.
    Symfony bundle fos_http_cache:     cache_control:       rules:         -­‐           match:                           host:  ^login.example.com$           headers:             cache_control:               public:  false               s_maxage:  0               last_modified:  "-­‐1  hour"             vary:  [Accept-­‐Encoding,  Accept-­‐Language]         -­‐           match:             attributes:  {  _controller:  ^AcmeBundle:Default:.*  }             additional_cacheable_status:  [400]           headers:             cache_control:  {  public:  true,  max_age:  15,  s_maxage:  30  }  
  • 24.