Purgado de elementos de caché en Varnish

Purgar elementos de la caché de Varnish significa decirle a Varnish que "descarte" dichos elementos de la caché y que, por tanto, deje de servir la versión cacheada del elemento y que vuelva a tratar de cachearlo si llega una nueva petición del mismo.

La acción de purgado es importante porque puede permitir a la aplicación web (si la integramos con Varnish) el "caducar" contenido de la caché si ha sido "editado" y consideramos que el nuevo contenido debe de ser servido.

También puede permitir eliminar contenido estático (.js, .css) cacheado con tiempos muy altos si cambiamos el layout de la web.

El concepto en sí es referenciado en la documentación de Varnish como cache invalidation.

Concretamente, hay 2 métodos:


  • purge; → Elimina todas las variantes (diferentes Accept-Encoding) de un objeto concreto de la caché, liberando memoria. Debe usarse tanto en vcl_hit() como en vcl_miss(), dejando al siguiente cliente el refresco de contenido (aunque veremos un ejemplo con restart para provocar el refresco).
  • ban(); → Permite invalidar objetos (no sólo uno concreto, puede ser una expresión regular) añadiéndolos a una "lista de baneo" pero no libera específicamente memoria en el momento en que se ejecuta, sino que se invalidan los objetos cuando son solicitados de nuevo por un cliente HTTP. Permite mucha más flexibilidad que purge pero la memoria asignada a los objetos no se libera hasta que son solicitados de nuevo.


El purgado de contenidos "desde el cliente" se hace mediante el método HTTP "PURGE". Este método es similar a GET (lo hacemos vía protocolo HTTP) y hay principalmente 2 métodos de purgado: el estilo Squid (al PURGE sigue la URL a caducar) y el estilo Varnish (soporta expresiones regulares).

En ambos casos podemos realizar el purge con simple telnet al puerto HTTP de Varnish. Por ejemplo, para purgar /articles/whatsnew.php, haríamos:

$ telnet ip.servidor.varnish 80
PURGE /articles/whatsnew.php HTTP/1.0
Host: dominio.com
(enter + enter)

En cualquier caso, es recomendable habilitar ACLs para especificar orígenes válidos para solicitar purging.



Purgado de elementos estilo Squid

El purgado de elementos "estilo Squid" se llama así porque es el mismo método que utiliza Squid para solicitar purgado de elementos de la caché. Sólo soporta URLs completas (no expresiones regulares), es decir, deberemos especificar los elementos a purgar uno a uno:

acl purge_permitidos
{
    "localhost";
    "192.168.1.0"/24;
}


sub vcl_recv 
{

    # Agregar PURGE a las requests que tratara varnish sin un pipe
    if (req.request != "GET" && 
         req.request != "HEAD" && 
         req.request != "PUT" && 
         req.request != "POST" && 
         req.request != "PURGE" && 
         req.request != "TRACE" && 
         req.request != "OPTIONS" && 
         req.request != "DELETE") 
    {
        /* Non-RFC2616 or CONNECT which is weird. */
        return (pipe);
    }

    # O tambien:
    if (req.request != "GET" && req.request != "HEAD" && req.request != "PURGE") 
    {
        /* We only deal with GET and HEAD by default */
        return (pass);
    }

    # permitir PURGE solo desde origenes concretos:
    if (req.request == "PURGE") 
    {
        # IP no permitida: error
        if (!client.ip ~ purge_permitidos) 
        {
            error 405 "Not allowed.";
        }
        
        # IP permitida: seguir logica de busqueda en cache
        return (lookup);
    }

    (...)


}


sub vcl_hit 
{
    # En vcl_hit() entramos cuando hemos encontrado el elemento
    # en la cache. Aqui comprobamos si se ha pedido un PURGE:
    if (req.request == "PURGE") 
    {
        # Purgamos el elemento y devolvemos 200 OK:
        purge;
        error 200 "Purged.";
    }
}


sub vcl_miss {

    # En vcl_miss() entramos cuando NO hemos encontrado el elemento
    # en la cache. Aqui comprobamos si se ha pedido un PURGE:
    if (req.request == "PURGE") 
    {
        purge;
        error 200 "Purged.";
    }

    
sub vcl_pass {
    if (req.request == "PURGE") {
        error 502 "PURGE on a passed object";
    }
}

Nótese que "purge;" hay que usarlo tanto en vcl_hit como en vcl_miss.


Purgado de elementos estilo Varnish

Purgar elementos uno a uno puede ser válido en algunos casos (como en los plugins de Wordpress o Mediawiki que veremos a continuación) pero no lo será si queremos, por ejemplo, invalidar en caché elementos masivamente sin recurrir a sus "paths".

Para eso, podemos usar en el PURGE expresiones regulares gracias al método de purge nativo de Varnish, mediante las funciones built-in purge_url() y purge_hash(). La primera sólo utiliza la URL para el matcheo, y la segunda lo hace contra la cadena hash que lo identifica en caché (la construída por vcl_hash(), lo que también tiene en cuenta el hostname).

Además, con el estilo Varnish no es necesario modificar vcl_hit() y vcl_miss().

acl purge_permitidos
{
    "localhost";
    "192.168.1.0"/24;
}


sub vlc_recv
{
    if( req.request == "PURGE" )
    {
        # IP no permitida: error
        if (!client.ip ~ purge_permitidos) 
        {
            error 405 "Not allowed.";
        }
        
        # IP permitida: purgar y devolver OK
        purge("req.url == " req.url " && req.http.host == " req.http.host);
        error 200 "Purged";
    }
}

En este caso, purge_url() nos permitiría purgar, por ejemplo, todos los .jpg de la caché pasando como URL la cadena "\.jpg$".

Para utilizar purge_hash(), reemplazaríamos "purge_url(req.url);" por:

purge_hash(req.http.purgestring);

Finalmente, destacar que también se pueden realizar purges en el interfaz de administración (CLI) de Varnish:

purge req.http.host == example.com && req.url ~ ^/somedirectory/.*$
purge obj.http.Cache-Control ~ max-age=3600
purge obj.http.Cache-Control ~ max-age ?= ?3600[^0-9]



Purge con restart para cachear de nuevo el contenido

El "Varnish Book" muestra un ejemplo de cómo soportar el método PURGE de forma que el propio Varnish realice un "restart" de la petición, forzando que tras el PURGE se vuelva a cachear el "contenido fresco".

El resultado es que un PURGE elimina el objeto de la caché y al mismo tiempo realiza la petición de fetch() al backend para regenerarlo.

acl purgers 
{
    "127.0.0.1";
    "192.168.0.0"/24;
}


sub vcl_recv 
{
    if (req.restarts == 0) 
    {
        unset req.http.X-purger;
    }
    
    if (req.request == "PURGE") 
    {
        if (!client.ip ~ purgers) 
        {
            error 405 "Method not allowed";
        }
        return (lookup);
    }
}


sub vcl_hit 
{
    if (req.request == "PURGE") 
    {
        purge;
        set req.request = "GET";
        set req.http.X-purger = "Purged";
        return (restart);
    }

}


sub vcl_miss 
{
    if (req.request == "PURGE") 
    {
        purge;
        set req.request = "GET";
        set req.http.X-purger = "Purged-possibly";
        return (restart);
    }
}


sub vcl_pass 
{
    if (req.request == "PURGE") 
    {
        error 502 "PURGE on a passed object";
    }
}


sub vcl_deliver 
{
    if (req.http.X-purger) 
    {
        set resp.http.X-purger = req.http.X-purger;
    }
}



Bans en Varnish

La última forma de invalidar contenido en Varnish son los bans. Los bans en Varnish 3.x son el equivalente de los purges en Varnish 2.x .

El ban es un filtro que se añade a la "ban list" (lista de baneos) que selecciona los elementos de la caché y "los invalida". Sólo actúa sobre los elementos en caché, no evitando que nuevo contenido entre en la misma. Invalidar un objeto no significa, al contrario que en el purge "manual", que se libere la memoria/espacio que ocupa; eso es algo que se hará en el momento en que el "worker" dedicado a esto lo haga.

Los bans se realizan en el interfaz de línea de comandos de Varnish (CLI interface). En la página oficial de Varnish se muestra el siguiente ejemplo de cómo banear todos los objetos \.png de la página "dominio.com":

ban req.http.host == "dominio.com" && req.http.url ~ "\.png$"

También podemos utilizar la variante "ban.url":

# Banear la página home (/) para que sea refrescada;
ban.url ^/$

# Banear todos los documentos de la caché (toda la web):
ban.url .

Internamente, ban_url(foo) es el equivalente de ban("req.url ~ " foo).



Integrando el purging con nuestra Web

Ahora que sabemos cómo realizar purgados en la caché, es importante integrar esta funcionalidad en nuestra aplicación Web.

El objetivo es cachear la mayor cantidad de contenido Web posible e invalidar de forma inmediata aquel contenido que ha cambiado y que queremos presentar "fresco" (no cacheado) a los clientes HTTP.

Para eso, podemos solicitar a nuestros desarrolladores Web que las partes de la web que cambian contenido (páginas administrativas, formularios, cajetines de chat, etc) realicen llamadas de PURGE al servidor de Varnish (conexiones al 80 + PURGE) de las URLs a invalidar. Esto permitirá mantener gran parte de la web cacheada pero mostrar la versión actualizada de la misma cuando los contenidos cambien.

En el caso de software de terceros, existen plugins o configuraciones específicas para hacer esto. Por ejemplo, Wordpress dispone de varios plugins para invalidar caché cuando los contenidos son editados o creados, y MediaWiki tiene parámetros específicos para indicar la ubicación de Varnish o Squid y así invalidar vistas de páginas wiki cuando son editadas.

De nuevo encontramos en el Varnish Book un excelente ejemplo de cómo purgar URLs de la caché desde código PHP, utilizando el módulo php-curl. Para eso, nuestro código PHP deberá hacer el purge; + error 200 "Purged" en vcl_hit() y vcl_miss(), y el return(lookup) en vcl_recv() si req.request == "PURGE" y se valida la IP origen:

#### purge_article.php ####
<?php
header( 'Content-Type: text/plain' );
header( 'Cache-Control: max-age=0' );

$hostname = 'localhost';
$port = 80;
$URL = '/article.php';
$debug = true;

print "Updating the article in the database ...\n";
purgeURL( $hostname, $port, $URL, $debug );

// Purge items in cache:
function purgeURL( $hostname, $port, $purgeURL, $debug )
{
    $finalURL = sprintf("http://%s:%d%s", $hostname, $port, $purgeURL);

    print( "Purging ${finalURL}\n" );

    $curlOptionList = array(
          CURLOPT_RETURNTRANSFER     => true,
          CURLOPT_CUSTOMREQUEST      => 'PURGE',
          CURLOPT_HEADER             => true,
          CURLOPT_NOBODY             => true,
          CURLOPT_URL                => $finalURL,
          CURLOPT_CONNECTTIMEOUT_MS  => 2000
    );

    $fd = false;
    if( $debug == true )
    {
        print "\n---- Curl debug -----\n";
        $fd = fopen("php://output", 'w+');
        $curlOptionList[CURLOPT_VERBOSE] = true;
        $curlOptionList[CURLOPT_STDERR] = $fd;
    }

    $curlHandler = curl_init();
    curl_setopt_array( $curlHandler, $curlOptionList );
    curl_exec( $curlHandler );
    curl_close( $curlHandler );
    
    if( $fd !== false )
    {
        fclose( $fd );
    }
}
?>



Mecanismos "de gracia" y always_miss



Flag hash_always_miss = true

Existe un parámetro llamado hash_always_miss que si lo establecemos a true dentro de vcl_recv(), produce el siguiente comportamiento:


  • Se ejecuta vcl_miss();
  • Se descarga el contenido del servidor backend HTTP.
  • Se ejecuta vcl_fetch() para descargar este contenido.
  • Se cachea la versión actualizada del contenido.
  • Si tenemos establecido hash_always_miss y el servidor backend http no responde o está caído, se mantiene la copia "antigua" en la caché.
  • Cada cliente que no utilice hash_always_miss = true; continuará recibiendo el contenido "viejo" como bueno, evitando así que se elimine contenido de caché hasta que sea seguro obtener el nuevo.




Ejemplos de purgado


Via telnet

# telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
 
PURGE /PATH/A/PURGAR/ HTTP/1.0
Host: www.eldominio.com


Via CURL

# curl -X PURGE http://www.eldominio.com/path/a/purgar
# curl -H "Host: www.eldominio.com" -X PURGE http://IP_DEL_SERVIDOR/path/a/purgar


Via varnishadm

# varnishadm -T 127.0.0.1:6082 purge req.url == "/PATH/A/PURGAR/"
# varnishadm -T 127.0.0.1:6082 purge.url "^/EXPRESION_REGULAR/$"


Directamente desde la aplicación

El siguiente código PHP de Alain Kelder (http://giantdorks.org/alain/exploring-methods-to-purge-varnish-cache/) muestra cómo realizar un PURGE desde la propia aplicación en PHP:

<?php
$fp = fsockopen("127.0.0.1", "80", $errno, $errstr, 2);
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
    $out = "PURGE /alain HTTP/1.0\r\n";
    $out .= "Host: giantdorks.org\r\n";
    $out .= "Connection: Close\r\n\r\n";
    fwrite($fp, $out);
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }
    fclose($fp);
}
?>


Desde una página HTML con un formulario hacia PHP

De nuevo Alain Kelder en su página http://giantdorks.org/alain/exploring-methods-to-purge-varnish-cache/ nos muestra un ejemplo de formulario HTML + POST a PHP para purgar URLs:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 
<html>
<head>
        <title>Purge Varnish cache</title>
</head>
 
<style type="text/css">
  body {
        font-size: 10px;
  }
  h1 {
        font-weight: bold;
        color: #000000;
        border-bottom: 1px solid #C6EC8C;
        margin-bottom: 2em;
  }
  label {
        font-size: 160%;
        float: left;
        text-align: right;
        margin-right: 0.5em;
        display: block
  }
  input[type="text"] {
        width: 500px;
  }
  .submit input {
        margin-left: 0em;
        margin-bottom: 1em;
  }
</style>
 
<body>
 
  <h1>Makes Varnish purge the supplied URL from its cache</h1>
 
  <form action="vpurge.php" method="post">
        <p><label>URL</label> <input type="text" name="url"></p>
        <p><label>HOST</label> <input type="text" name="host"></p>
        <p class="submit"><input value="Submit" type="submit"></p>
  </form>
 
</body>
</html>
<?php
# get param
$url = $_POST["url"];
$host = $_POST["host"];
 
  $ip = "127.0.0.1";
  $port = "80";
 
  $timeout = 1;
  $verbose = 1;
 
  # inits
  $sock = fsockopen ($ip,$port,$errno, $errstr,$timeout);
  if (!$sock) { echo "connections failed $errno $errstr"; exit; }
 
  if ( !($url || $host) ) { echo "No params"; exit; }
 
  stream_set_timeout($sock,$timeout);
 
  $pcommand = "purge";
  # Send command
  $pcommand .= ".hash $url#$host#";
 
  put ($pcommand);
  put ("quit");
 
  fclose ($sock);
 
  function readit() {
    global $sock,$verbose;
    if (!$verbose) { return; }
    while ($sockstr = fgets($sock,1024)) {
      $str .= "rcv: " . $sockstr . "<br>";
    }
    if ($verbose) { echo "$str\n"; }
  }
 
  function put($str) {
    global $sock,$verbose;
    fwrite ($sock, $str . "\r\n");
    if ($verbose) { echo "send: $str <br>\n"; }
    readit();
  }
?>



<Volver a Página de VARNISH>