Cacheando con Varnish un proyecto ASP.net MVC5

Una caché, por definición, es un almacenamiento a corto plazo de información y con una altísima velocidad de acceso, permitiéndonos mejorar el rendimiento de nuestros sistemas. En el caso de la web podemos diferenciar dos tipos:

  • Caché de cliente, que se hace en el navegador y que nos permite no tener que traer de internet imágenes o ficheros de estilo una vez hemos obtenido una copia.
  • Caché de servidor, que guarda una copia de manera temporal para evitar sobrecarga de nuestro servidor de aplicaciones.

Esto último tiene una justificación, y es que el acceso a las aplicaciones (PHP, ASP.net, Ruby) es un proceso relativamente lento que suele estar acompañado de un acceso a una base de datos, una aplicación de la lógica de negocio y la composición final de la página.

Existen diferentes sistemas y tipos de caché, como Memcached o Redis. En este artículo hablaremos de Varnish, un proyecto open-source que se instala sobre sistemas UNIX (aunque tiene soporte para Windows mediante Cygwin).

La principal peculiaridad que tiene este sistema, además de sus casi-infinitas opciones de configuración, es que permite dividir nuestra página en trozos y cachearlos de manera independiente, en función del intervalo de actualización de los mismos, un proceso conocido como Edge Side Includes (ESI) y que veremos a lo largo del artículo.

Instalación y configuración

La instalación de Varnish es un proceso tan simple como brew install varnish si usamos homebrew en mac. Una vez instalado, para ejecutarlo usamos el siguiente comando:

sudo /usr/local/opt/varnish/sbin/varnishd -n /usr/local/var/varnish -f /usr/local/etc/varnish/default.vcl -s malloc,1G -T 127.0.0.1:2000 -a 0.0.0.0:8082

pasando como parámetros las siguientes opciones:

  • Ruta del fichero de configuración
  • Puerto de registro de log
  • Máximo consumo de RAM
  • Puerto de escucha (en nuestro caso el 8082)

El fichero de configuración default.vcl, para nuestro ejemplo, contiene las siguientes directivas:

backend default {
    .host = "127.0.0.1";
    .port = "8087";
}

sub vcl_recv {
	if (req.http.cookie) {
		unset req.http.cookie;
	}
}

sub vcl_fetch {
	
	if (req.url == "/layout/*") {
       set beresp.ttl = 2m;      /* Sets a one minute TTL on        */
    } else {
       set beresp.do_esi = true; /* Do ESI processing               */
       set beresp.ttl = 30s;     /* Sets the TTL on the HTML above  */
    }
    unset beresp.http.set-cookie;
}

sub vcl_deliver {

    if (obj.hits > 0) {
            set resp.http.X-Cache = "HIT";
    } else {
            set resp.http.X-Cache = "MISS";
    }
}

Los comandos que hemos implementado son:

  • backend: Permite especificar qué servidores estarán funcionando al otro lado, se puede especificar una IP o un nombre DNS.

  • vcl_recv: En el caso de ASP.net, se genera una cookie de sesión al acceder al sitio, lo cual no siempre es lo deseable. En este ejemplo se borran todas las que provienen del servidor, de manera que se pueda cachear la página.

  • vcl_fetch: Se ejecuta cuando obtenemos un elemento desde el servidor, y en este caso especificamos que si estamos solicitando algún elemento de «layout» como el menú o la cabecera, ha de cachearlo durante 2 minutos, en caso contrario configuramos para que haga el procesamiento ESI (es decir, que intente unir los pedazos) y cachee el conjunto durante 30 segundos.

  • vcl_deliver: Nos permite editar la cabecera HTTP para saber si ha podido resolver el elemento en la caché o si ha tenido que acceder al servidor de aplicaciones.

Cacheando voy, cacheando vengo:

En este ejemplo hemos usado una plantilla de bootstrap que tiene el siguiente aspecto dentro de una aplicación prácticamente vacía en MVC5:

goal

Lo primero que podemos apreciar son tres zonas claramente diferenciadas, una cabecera, una barra lateral, y, finalmente, un área de contenido principal.

Estructura

Para mostrar la página, empleamos un controlador muy sencillo que genera una espera de 3 segundos para simular carga del servidor y almacena la fecha de generación en el ViewData:

public class HomeController : Controller
{
	public ActionResult Index ()
	{
		ViewData ["now"] = String.Format ("{0:HH:mm:ss}", DateTime.Now);
		System.Threading.Thread.Sleep (3000);
		return View ("Index");
	}
}

De manera adicional y para poder aplicar ESI, necesitaremos dividir nuestra vista en tres vistas diferentes que correspondan a acciones diferentes del controlador, que serán:

  • / <- que genera el contenido de la página
  • /layout/header <- que genera la cabecera
  • /layout/sidebar <- que genera la barra lateral

Las dos acciones del controlador Layout son idénticas, como muestra el código:

public class LayoutController : Controller
{
    public ActionResult Sidebar()
    {
		ViewData ["now"] = String.Format ("{0:HH:mm:ss}", DateTime.Now);
		System.Threading.Thread.Sleep (3000);
        return View ("Sidebar");
    }

	public ActionResult Header()
	{
		ViewData ["now"] = String.Format ("{0:HH:mm:ss}", DateTime.Now);
		System.Threading.Thread.Sleep (3000);
		return View ("Header");
	}
}

Nótese que por cada acción hemos agregado otros 3 segundos de retraso, teniendo un total de 9 segundos en el caso peor.

Para poder activar el ESI en las vistas, usamos etiquetas HTML que tienen el siguiente formato:

<esi:include src="/layout/sidebar" onerror="continue"/>

Además, a modo de prueba, podemos agregar algo de código javascript que se ejecute una vez cargada la página para apreciar la diferencia entre hora de generación y hora de renderización. Si todo ha ido bien, cuando carguemos por primera vez nuestra caché, se encontrará vacía, y sufriremos la espera de los 9 segundos.

En la imagen, G muestra la hora de generación en el servidor y R la hora de renderización en el cliente:

miss

Si volvemos a acceder, comprobaremos que el contenido de la página ya se ha cacheado, que las fechas de generación y de ejecución distan más de tres segundos, y que el inspector nos muestra el tiempo total de acceso a la página (700 milisegundos aprox):

hit

Conclusiones

Ha sido una prueba de concepto muy interesante, el hecho de descubrir que podemos tener nuestra web dividida en secciones con cachés diferentes nos da mucha movilidad al hacer portales con información dinámica. Varnish es una herramienta suficientemente compleja para ser tomada en serio y requiere que estudiemos a fondo sus características antes aplicarla a un sistema en producción.

Lecturas adicionales y enlaces

Probando módulos de NancyFx

En el artículo anterior de la serie de NancyFx hablábamos de diferentes tipos de respuesta así como el uso de plantillas en la plataforma. En este artículo veremos cómo crear pruebas unitarias para asegurarnos que nuestros módulos funcionan correctamente.

Creando nuestro primer test

Desde la documentación de Nancy se recomienda usar un proyecto diferente (es decir, un ensamblado diferente) para nuestros tests, ya que los módulos se descubren de manera automática por el «bootstrapper». Una vez creado nuestro proyecto, necesitamos agregar las siguientes referencias, via nuget o de manera manual:

  • NUnit (o XUnit, o el framework que deseemos)
  • Nancy
  • Nancy.Testing

Una vez tenemos las referencias, podemos crear nuestro primer test (que tiene el original nombre de RoutesTest.cs). Dicho test estará dividido en tres partes:

  • Arrange: Toda la preparación necesaria para poder llevar a cabo el test
  • Act: La ejecución de nuestro test
  • Assert: La fase de comprobación de los resultados
[Test ()]
public void TestPut ()
{
        //Arrange
	var bootstrapper = new DefaultNancyBootstrapper();
	var browser = new Browser(bootstrapper);

        //Act
	var result = browser.Put("/routes", with => {
		with.HttpRequest();
	});

        //Assert
	Assert.AreEqual (HttpStatusCode.OK, result.StatusCode);
	Assert.AreEqual ("Response with Put\n", result.Body.AsString());
}

Qué hace el test?

  • Arrange: Para este test el primer paso es crear nuestro Bootstrapper que descubrirá los módulos existentes, seguido de un Browser con el que simularemos las llamadas a nuestros módulos.

  • Act: Una vez tenemos el browser, el siguiente paso es generar la peticion, en este caso Put, a una ruta especificada.

  • Assert: Con el resultado de la ejecución, comprobamos el estado de la respuesta (en este caso 200 OK) y que el texto corresponda con lo que necesitamos, que es «Response with Put\n».

Una vez tenemos las referencias, recordemos qué contenía el módulo que queríamos probar:

public class Routes : NancyModule
{
	public Routes ()
	{
		Get["/routes"] = _ => "Response with GET\n";
		Post["/routes"] = _ => "Response with POST\n";
		Put["/routes"] = _ => "Response with Put\n";
                ...
	}
}

Podemos identificar claramente una línea donde el verbo PUT devuelve «Response with Put\n», con lo cual nuestro test es correcto

Probando una ruta errónea

Al igual que podemos probar rutas correctas, los test son igual de válidos para comprobar que el acceso a una ruta no autorizada o no implementada se controla correctamente, como sucede a continuación:

[Test ()]
public void TestNotFound ()
{
	//Arrange
	var bootstrapper = new DefaultNancyBootstrapper();
	var browser = new Browser(bootstrapper);

	//Act
	var result = browser.Delete("/routes", with => {
		with.HttpRequest();
	});

	//Assert
	Assert.AreEqual (HttpStatusCode.MethodNotAllowed, result.StatusCode);
}

En este caso, la ruta no está implementada, con lo cual el resultado será un error de tipo «Method Not Allowed».

Conclusiones

Con pocas líneas de código podemos comenzar a probar nuestros módulos de NancyFx, podemos probar la respuesta de los mismos, así como códigos de error. El sistema de test permite, adicionalmente, pasar parámetros (simulando el uso de un formulario) o navegar a través del DOM del HTML devuelto, si usamos un ViewEngine.

Tipos de respuesta y plantillas en NancyFx

En los artículos anteriores de la serie, hacíamos un breve repaso a cómo crear módulos y rutas con NancyFx, en este veremos cómo responder a las peticiones usando texto plano, JSON o ficheros, así como usar un viewengine que nos permita, a través de plantillas, usar elementos generados por el servidor en documentos HTML.

Presentando al Content-Type

El Content-type es un mensaje que forma parte de los encabezados HTTP y especifica el tipo de recurso que soliticamos y que se devuelve en una respuesta para que el navegador lo pueda procesar. De esa manera, un content-type de tipo text/html se procesará en el cliente como un fichero HTML a renderizar, el tipo application/octet-stream se referirá habitualmente a un fichero binario, y así para ficheros de audio, vídeo u otros.

Nancy permite especificar el tipo de contenido que estamos devolviendo, de esta manera podemos rescatar el ejemplo anterior donde especificábamos el código de retorno, y modificarlo para que devuelva un contenido con un tipo mime específico:

public class ResponseTypes : NancyModule
{
	public ResponseTypes ()
	{
		Get["/responses/text"] = _ => {
			return ((Response)"<h1>Texto plano</h1>\n").
				WithContentType("text/plain");
		};

		Get["/responses/html"] = _ => {
			return ((Response)"<h1>Texto HTML</h1>\n").
				WithContentType("text/html");
		};

		Get["/responses/binary"] = _ => {
			return ((Response)"<h1>Texto en un fichero</h1>\n").
				WithContentType("application/octet-stream");
		};
	}
}

En este caso, la primera respuesta, aunque espefiquemos código HTML, mostrará el texto plano, en la segunda hemos especificado que se trata de html, con lo cual la respuesta se renderizará por pantalla. En la tercera respuesta, hemos establecido el tipo a binario, con lo cual la acción a realizar será la de descargar el contenido a un fichero en disco.

JSON

Javascript Object Notation, o JSON, es un viejo conocido de este blog, ya que lo hemos usado para acceder a Azure Mobile Services, a Twitter, y a otros. Es una representación ligera de datos que tiene un formato similar a este:

{
    "firstName": "John",
    "lastName": "Smith",
    "age": 25,
    "address": {
        "streetAddress": "21 2nd Street",
        "city": "New York",
        "state": "NY",
        "postalCode": 10021
    },
    "phoneNumbers": [
        {
            "type": "home",
            "number": "212 555-1234"
        },
        {
            "type": "fax",
            "number": "646 555-4567"
        }
    ]
}

Una de las cosas que nos permite NancyFx, es codificar en formato Json un objeto (o un conjunto de ellos) para devolverlos al cliente. Veamos un ejemplo:

public class Person
{
	public string Name {
		get;
		set;
	}
	public string Surname {
		get;
		set;
	}
}public class JsonResponse : NancyModule
{
	public JsonResponse ()
	{
		Get["/json/single"] = _ => Response.AsJson(
			new Person()
			{
				Name = "Roberto",
				Surname = "Luis"
			}
		);
	}
}

En este ejemplo, creamos un objeto, y usando Response.AsJson codificamos su contenido en formato Json y lo devolvemos. La respuesta, si la viésemos en un navegador, tendría este aspecto:

{
    "Name": "Roberto",
    "Surname": "Luis"
}

Si examinamos cuidadosamente la cabecera de la respuesta (usando curl por ejemplo), veremos además el valor del campo Content-Type:

Content-Type: application/json; charset=utf8

Lo que ha ocurrido es que Nancy automáticamente ha gestionado las cabeceras por nosotros. Contamos además con métodos como AsXml, AsImage o AsFile para devolver nuestros objetos en otros formatos.

ViewEngines

Un view engine, o motor de vistas, es un componente que nos permite combinar plantillas HTML con llamadas al servidor, evitandonos el engorro de trabajar directamente con html dentro del código, y posiblitando una separación de responsabilidades. Nancy incluye uno por defecto, llamado «Super Simple View Engine», que permite tener una plantilla de este estilo:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"> <title>External template</title>
  </head>
  <body>
    <h1>Hello, @Model.Name!</h1>
    <ul>
    @Each.Model.Children
    <li>@Current</li>
      @EndEach
    </ul>
  </body>
</html>

Los campos @Model.Name y @Model.Children, nos pueden recordar a Razor, el motor de vistas de ASP.net MVC3, mientras el uso de @Each.Model nos pueden recordar a las plantillas erb de ruby. Estos elementos nos permiten agregar datos de manera dinámica a un documento HTML.

La variable @Model, a la que hemos accedido desde la vista, se define desde el controlador, y puede ser un objeto perteneciente a una clase, o como se muestra en el ejemplo, un objeto anónimo que contenga los datos que necesitamos:

public class ViewTemplates : Nancy.NancyModule
{
	public ViewTemplates ()
	{
		Get ["/ViewEngines/WithData"] = _ => {
			var model = new { 
				Name = "John Smith", 
				Children = new []{"Mike", "Bob", "James"}
			};
			return View["ViewEngines/TemplateWithData.html", model];
		};
	}
}

Finalmente, se devuelve la vista usando la ruta relativa de la misma, y el objeto que servirá de modelo.

Resumen

En este artículo se ha visto cómo podemos personalizar el tipo de respuesta, ya sea texto, binario, o JSON, así como usar un motor de vistas para separar la vista del controlador. Con este artículo terminamos esta serie de introducción a NancyFx, si deseas consultar más información, aquí están los artículos anteriores de la serie:

Enlaces adicionales

Módulos y rutas en NancyFx

Este artículo continúa la serie de introducción a Nancy, un framework para aplicaciones web escrito en C#. En el artículo anterior (Primer contacto con NancyFX) hacíamos una breve introducción a la sintaxis, y veíamos un ejemplo muy básico de rutas. En este veremos con un poco más de detalle qué es un módulo, cómo funciona el sistema de rutas, cómo definirlas, y qué tipo de respuestas podemos dar.

Módulos

using System;

namespace NancyFxSinatra
{
	public class SampleModule : Nancy.NancyModule
	{
		public SampleModule()
		{
			Get["/"] = _ => "Hello World!";
		}
	}
}

En el artículo anterior mostrábamos el siguiente código. Las rutas están definidas en el constructor de un módulo, que es a su vez la pieza clave que define el comportamiento de nuestra aplicación.

Un módulo hereda de la clase NancyModule y se puede definir en cualquier parte de la aplicación. No es necesario declararlos explícitamente ya que Nancy se encargará de buscarlos por nosotros. Esta búsqueda se realiza al iniciar la aplicación, quedando en caché una vez encontrados.

Esto nos permitiría, en una hipotética aplicación de gestión tener nuestro ProductModule, que gestionara productos, y nuestro CategoriesModule que gestionara categorías. Los módulos pueden encontrarse en diferentes carpetas, o incluso en diferentes ensamblados.

Rutas

Todos los caminos conducen a Roma

Una ruta se compone por:

Método + Patrón + Acción.

Método

El método es el verbo HTTP por el que accedemos a un recurso. A continuación podemos ver un ejemplo de acceso a diferentes recursos con el mismo patrón.

using System;

namespace NancyFxSinatra
{
	public class Routes : Nancy.NancyModule
	{
		public Routes ()
		{
			Get["/routes"] = _ => "Respuesta usando GET\n";
			Post["/routes"] = _ => "Respuesta usando POST\n";
		}
	}
}

Para probarlo en este caso usaremos la línea de comandos y la utilidad curl, que permite hacer peticiones a una pagina, y devuelve el resultado. Se encuentra disponible para Mac y Linux de manera nativa, y para Windows lo podemos obtener de su página oficial.

Si queremos probar la llamada por GET, usamos el comando:

curl -X GET http://127.0.0.1:8080/routes

Y la salida que obtendremos será:

Respuesta usando GET

Por otra parte, si queremos probar la llamada por POST, el comando será:

curl -X POST http://127.0.0.1:8080/routes

Con la correspondiente salida:

Respuesta usando POST

Así podemos probar el resto de métodos HTTP soportados, que son GET, POST, PUT, DELETE, PATCH, HEAD y OPTIONS.

Patrón

El patrón, por su parte, nos permite detectar una url específica, así como capturar elementos. Podemos destacar los siguiente patrones:

  • «/users»: Detecta la coincidencia exacta.
  • «/users/{id}»: Al caso anterior, captura además cualquier elemento que se pase a continuación. En el caso de «/users/roberto», id tendría el valor de roberto.
  • «/users/{id}/{format?}»: Además de lo anterior, detecta una ruta que puede contener, o no ese valor. En el caso de «/users/roberto/json», format tendría el valor de json. Si por lo contrario usamos una ruta de «/users/roberto» también será capturada, ya que el parámetro es opcional.
  • «users/{cur*}»: A diferencia del anterior, solamente detecta y captura entradas que comiencen por cur, por lo tanto «/users/roberto» no sería detectado, pero «/users/cur1239» sí.

Podemos encadenar patrones como hemos visto antes para crear rutas más complejas, y usar expresiones regulares para una captura más específica.

Acción

Las acciones son el contenido de nuestra llamada, es decir, el resultado de la misma, es de tipo Response, sin embargo, según la respuesta del método se puede devolver:

  • entero: Se interpreta entonces como código de estado HTTP (ejemplo, 404).
  • HttpStatusCode: Código de respuesta incluido en Nancy, que corresponde también a estados HTTP.
  • texto: Interpretado como el cuerpo de la respuesta.

Para demostrarlo, veamos 4 tipos de respuesta, las 3 primeras son las que hemos mostrado, mientras que la última devuelve un error y además de una respuesta en el cuerpo.

Get ["/actions/ok"] = _ => 202;
Get ["/actions/auth"] = _ => HttpStatusCode.Forbidden;
Get ["/actions/text"] = _ => "Hola Mundo";Get["/actions/error"] = _ => {
    Response res =  ((Response)"Error con mensaje\n");
    res.StatusCode = HttpStatusCode.InternalServerError;
    return res;
};

Usando curl (con la opción -v para que muestre también las cabeceras) podemos comprobar que el resultado de la petición corresponde con los códigos que hemos mostrado.

Resumen

En este artículo hemos visto qué son los módulos, cómo, usando comandos HTTP, podemos especificar la manera para acceder a un recurso. Hemos visto también, cómo capturar rutas específicas o que cumplan con algún criterio, y finalmente, hemos devuelto mensajes HTTP usando números y enumerados. En el próximo artículo veremos cómo usar ViewEngines y plantillas HTML para contenido.

Más información:

Primer contacto con NancyFX: Desarrollo web ligero con C#

Existen multitud de frameworks para el desarrollo web, y posiblemente hayas oído hablar de Symfony para PHP, Django para Python, ASP.net MVC para plataformas basadas en .NET. Estas herramientas contienen un gran número de características para crear aplicaciones, se basan en convenciones, y existe abundante literatura sobre ellos.

Por otro lado, hay otros que se caracterizan por ser más livianos, de tal manera que contienen un número más reducido de características que sus hermanos mayores y que nos pueden ser útiles para crear soluciones más específicas. En esta serie de artículos haremos una breve introducción a Nancy, un framework para aplicaciones web escrito en C#.

¿Por qué Nancy?

Una de las cosas que más me ha llamado la atención es que está inspirado en Sinatra, un DSL (Lenguaje específico de dominio) escrito en Ruby. Además, como la mayoría de frameworks, es Open Source, lo que nos permite ver bajo el capó e intentar aprender cómo sucede la «magia».

Sinatra es bastante conocido en la comunidad Ruby, y ha servido de ejemplo para otros, aparte de Nancy. Uno de los libros más interesantes y sencillos es Sinatra up and running. de Alan Harris y Konstantin Haase, este último miembro del equipo que da mantenimiento a Sinatra. Ellos han permitido amablemente que pueda portar los ejemplos del mismo de ruby a C#, y veremos algunos a lo largo de esta serie, que cubren temas como el ya clásico «Hola mundo», el uso de rutas, plantillas, viewengines, cookies y errores entre otras. El código de los ejemplos está disponible en github.

Instalación

Para instalar Nancy necesitamos Visual Studio 2010+ con nuget, o bien Xamarin Studio/Monodevelop 4.1+. Para el ejemplo crearemos un sitio web ASP.net vacío. Una vez creado, y usando nuget podemos usar el siguiente comando:

Install-Package Nancy

Esto agregará las dependencias de Nancy en nuestro proyecto. Un último detalle que necesitamos antes de poder empezar a escribir código, es editar nuestro fichero web.config para que tenga este aspecto:

 <?xml version="1.0" encoding="utf-8"?>
 <configuration>
   <system.web>
     <httpHandlers>
       <add verb="*" type="Nancy.Hosting.Aspnet.NancyHttpRequestHandler" path="*" />
     </httpHandlers>
     <compilation>
       <assemblies />
     </compilation>
   </system.web>
   <system.webServer>
     <validation validateIntegratedModeConfiguration="false" />
     <handlers>
       <add name="Nancy" verb="*" type="Nancy.Hosting.Aspnet.NancyHttpRequestHandler" path="*" />
     </handlers>
   </system.webServer>
 </configuration>

Una vez tengamos esta configuración inicial, podemos escribir nuestro «Hola mundo». En el código que se muestra a continuación, dentro del constructor definimos una ruta, así como el verbo HTTP que usaremos para la misma, y el resultado que devolveremos.

using System;

namespace NancyFxSinatra
{
	public class SampleModule : Nancy.NancyModule
	{
		public SampleModule()
		{
			Get["/"] = _ => "Hello World!";
		}
	}
}

Si compilamos y ejecutamos nuestro módulo (F5 en Visual Studio y cmd + Enter en Xamarin Studio) veremos algo así:

helloNancy

Puede que no parezca gran cosa, pero en pocas líneas hemos definido un módulo, y nos hemos asociado a una ruta específica con un verbo concreto. Si por ejemplo intentásemos hacer una petición a otra ruta tendríamos este divertido mensaje 404.

nancy404

Este es un ejemplo muy sencillo (y poco útil) de lo que se puede hacer. Podemos usar parámetros como parte de la ruta, lo cual nos da un poco más de juego, por ejemplo si al constructor anterior agregamos la siguiente línea:

Get ["/hola/{name}"] = _ => "Hola " + _.name;

Entonces la aplicación reconocerá también /name/nombre, siendo nombre cualquier texto. Con esto podemos ya diseñar una pequeña API REST, ya que tenemos, por una parte, soporte para verbos HTTP, rutas específicas, e indicadores de recursos.

En el próximo artículo veremos además cómo pasar (y procesar) argumentos vía Post, y cómo devolver errores específicos al navegador, entre otras cosas.

Happy hacking!

Más información: