Configurar Nginx para cachear respuestas del servidor de aplicaciones

Escrito por el , actualizado el .
planeta-codigo software software-libre web
Enlace permanente Comentarios

Las caches son un recurso utilizado para aumentar el rendimiento y evitar malgastar recursos. Si una petición es muy costosa pero que no cambia muy a menudo o no se necesitan que los datos estén totalmente actualizados cachear el resultado de esa petición evitará tener que recalcularla para cada petición, si se realizan muchas peticiones el aumento de rendimiento será drásticamente mejor usando además un menor número de recursos de los sistemas. Hay soluciones específicas para cacheo pero si nuestra necesidades no son extremadamente avanzadas el cacheo ofrecido por Nginx probablemente sea suficiente.

Nginx

Los servidores web son muy eficientes devolviendo al cliente archivos estáticos. Tradicionalmente el contenido estático formado por hojas de estilo, archivos JavaScript, imágenes o vídeos de una página o aplicación web han sido devueltos por un servidor web evitando que el servidor de aplicaciones tenga que procesar una gran cantidad de peticiones, aún hoy se sigue haciendo. En caso de que los recursos estáticos estén alojados en el servidor de aplicaciones en vez de en el servidor web, porque los recursos estáticos se empaquetan con la aplicación, el servidor web puede cachearlos evitando que peticiones lleguen al servidor de aplicaciones. En este artículo comentaré cual es la configuración necesaria a añadir en el servidor web Nginx que hace de proxy para que cachee el resultado de las peticiones así indicadas del servidor de aplicaciones, la aplicación genera el contenido y establece las cabeceras de cache indicando como quiere que se cachee el contenido devuelto.

Para que el servidor web realice el cacheo de los recursos en la aplicación esta ha de devolver en las cabeceras de respuesta como quiere que los recursos sean cacheados usando las cabeceras Last-Modified, Expires, Cache-Control quizá Etag. Las cabeceras de cache del protocolo HTTP establecen el comportamiento deseado para la cache.

Hay que modificar el archivo de configuración de Nginx para que cachee el contenido. La directiva proxy_cache_path indica donde se guardarán el contenido cacheado, cual es el tamaño de los metadatos de la caché y el tiempo de inactividad para cachear pasado el cual los recursos serán descartables. La directiva proxy_cache_key permite diferenciar los recursos en la cache, add_header X-Proxy-Cache añade una cabecera para la respuesta de Nginx con el resultado de cache que nos permite conocer si se produjo un acierto en la caché, un fallo o se ignoró la cache. Lo que es útil para depurar la aplicación u obtener información. Con proxy_pass hacemos que Nginx haga de proxy para el servidor de aplicaciones o la aplicación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
proxy_cache_path /tmp/nginx levels=1:2 keys_zone=localhost_zone:10m inactive=60m;
proxy_cache_key "$scheme$request_method$host$request_uri";

server {
	listen 80;

	location / {
		proxy_cache localhost_zone;
		add_header X-Proxy-Cache $upstream_cache_status;

		proxy_pass http://java:4567;
	}
}
nginx.conf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
java:
  image: java:alpine
  volumes:
    - ../build/libs/:/mnt/app/
  command: java -classpath "/mnt/app/*" io.github.picodotdev.blogbitix.cache.Main

nginx:
  image: nginx:alpine
  volumes:
    - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
  links:
    - java
  ports:
    - "80:80"
docker-compose.yml

La siguiente pequeña aplicación Java que usa el framework Spark expone dos recursos para probar el funcionamiento de cache de Nginx, un recurso añade cabeceras de cacheo para la respuesta y otro no añade las cabeceras de respuesta. Atendiendo a las cabeceras establecidas por la aplicación y Nginx configurado para hacer de proxy y cache devolverá el contenido deseado de su cache o solicitándolo a la aplicación y cacheándolo si así se indica en las cabeceras de respuesta.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.github.picodotdev.blogbitix.cache;

import java.time.LocalDateTime;
import java.util.UUID;

import static spark.Spark.get;

public class Main {

    public static void main(String[] args) {
        get("/nocache", (request, response) -> {
            return getMessage();
        });
        get("/cache", (request, response) -> {
            response.header("Cache-Control", "max-age=60");
            response.header("Etag", UUID.randomUUID().toString());
            return getMessage();
        });
    }

    private static String getMessage() {
        return String.format("¡Hola mundo! (%s)", LocalDateTime.now().toString());
    }
}
Main.java

La primera petición que se realiza al recurso cache devuelve un código de estado 200 de HTTP y Nginx en la cabecera X-Proxy-Cache indica que se ha producido un MISS o fallo en la cache, la segunda petición realizada antes de que pase el minuto del tiempo de expiración devuelve un código de estado 304 y Nginx en la cabecera X-Proxy-Cache un HIT o acierto en la cache, finalmente pasado más de un minuto del tiempo de expiración se devuelve un código de estado 200 y Nginx en la cabecera X-Proxy-Cache un EXPIRED. En las trazas de Nginx vemos las peticiones que se producen sus códigos de estado y después de este los bytes transferidos de contenido, nótese que en los casos de los 304 los bytes transferidos son 0, bytes de datos ahorrados y evitado que la petición llegue a la aplicación que son unos de los objetivos de las caches. En el recurso nocache de la aplicación Nginx no cachea el contenido devuelto ya que en este no se establecen las cabeceras de cache en la respuesta.

Fallo en la cache de Nginx Acierto en la cache de Nginx

Expiración en la cache de Nginx Fallo, acierto y expiración en la cache de Nginx con curl

Fallo, acierto y expiración que produce en la cache de Nginx al realizar peticiones
1
2
3
nginx_1  | 172.17.0.1 - - [30/Jul/2016:10:43:20 +0000] "GET /cache HTTP/1.1" 200 50 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0" "-"
nginx_1  | 172.17.0.1 - - [30/Jul/2016:10:43:56 +0000] "GET /cache HTTP/1.1" 304 0 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0" "-"
nginx_1  | 172.17.0.1 - - [30/Jul/2016:10:45:04 +0000] "GET /cache HTTP/1.1" 200 50 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0" "-"
nginx.out
1
2
$ curl -X GET -I http://localhost/cache

curl-cache.sh

Hay servidores específicos para realizar tareas de cache como Varnish con más opciones de las que ofrece Nginx. Para los casos no complicados usando Nginx evitamos añadir una nueva pieza a la arquitectura de la aplicación. Entre los productos que ofrece Amazon está Cloudfront que es una cache para recursos estáticos con el añadido de que de forma automática está distribuida geográficamente de forma que los recursos se sirven por un servidor más cercano al cliente evitando un mal rendimiento por la latencia. En el artículo servir recursos estáticos de un CDN en Apache Tapestry comento como usar esta red de distribución de contenido ofrecida por Amazon.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew build, cd docker, docker-compose up


Comparte el artículo: