Nginx y las limitaciones de FastCGI Cache


Nginx es sin duda una de las mejores opciones en conjunto con FastCGI Cache en lo que a servidores de alojamiento se refiere y con WordPress liderando las estadísticas de gestores de contenido CMS a nivel mundial de manera apabullante, no es sorpresa que encontremos ambos términos ampliamente ligados.

Para ejemplificar usaremos una implementación de WordPress, pero el contenido aplica para casi cualquier otra implementación.

La gestión de contenidos dinámicos presenta grandes ventajas para algunos usuarios, pero sin duda son una gran carga en cuanto a recursos de hardware se refiere conforme el trafico del sitio aumenta. Desde siempre han existido soluciones tanto en forma de plugins en WordPress (W3 Total Cache, Super Cache, etc.), como del lado del servidor (FastCGI, Varnish, etc.).

Nginx Logo

Si estamos hablando de Nginx lo mas seguro es que te decantes por la opción de FastCGI Cache y en este artículo nos centraremos en los retos que presenta su implementación, que no es para nada trivial aunque a veces lo parezca.

Módulo ngx_cache_purge de Nginx

Dando por entendido que ya todos entendemos como funciona y el propósito del uso de un mecanismo para guardar y servir el contenido desde la cache, podemos entrar en materia y presentar los retos que al momento de actualizar el contenido de una página, como la lógica lo indica, el contenido en cache también debe ser actualizado.

La solución oficial es hacer uso del módulo ngx_cache_purge de Nginx, el problema es que dicho módulo solo está incluido en la versión comercial de Nginx y no en la gratuita que la mayoría usamos. Por lo tanto, si usted usa la versión de pago de Nginx no tiene ningún problema y no tiene sentido continuar leyendo este articulo, usted ya cuenta con todo lo necesario para lidiar de manera correcta con la cache de Nginx.

Para los que usamos la versión gratuita de Nginx existen algunas opciones para lidiar con este tema.

La solución mas común es ofrecida por algunos plugins de WordPress, el más conocido tal vez sea Nginx-Helper, que entre otras cosas, nos brinda dos maneras o métodos para purgar la cache de FastCGI, el primer método nos indica que es necesario contar con el modulo ngx_cache_purge y el segundo es el borrado de los archivos de cache en el servidor. Ambos métodos tienen ventajas y desventajas.

* Algunas PPA’s y paquetes en el repositorio oficial de Ubuntu por razones que desconozco incluyen dicho modulo (a través del sub-paquete “nginx-extras”), de igual manera desconozco si el código es el original o alguna variante de un tercero.

FRiCKLE / ngx_cache_purge

Existen módulos de código abierto (Open-Source) disponibles para integrar de manera gratuita un módulo similar con esta funcionalidad y gracias a esto podemos encontrar una cantidad de repositorios que ofrecen Nginx con esta característica ya incluida. El más conocido tal vez sea el creado por FRiCKLE, entre algunos otros, la mayoría forks o derivados del mismo.

  • Es necesario compilar tu propia versión de Nginx.
  • Otra opción es descargar una versión no oficial de un tercero.

En mi caso no me siento cómodo incluyendo software de terceros o de repositorios poco conocidos en mi servidor y por política algunas aplicaciones empresariales tampoco lo permiten. Además del dilema ético que se presenta por ser una funcionalidad oficial incluida en la versión comercial o de pago.

Básicamente, si cuentas con este modulo, no importa si es el original o uno similar Open-Source, puedes hacer uso de la directiva fastcgi_cache_purge que se describe en la documentación y de una manera muy fácil puedes gestionar el contenido de la cache de FastCGI.

set $skip_cache 0;

# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
	set $skip_cache 1;
}   
if ($query_string != "") {
	set $skip_cache 1;
}   

# Don't cache uris containing the following segments
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
	set $skip_cache 1;
}   

# Don't use the cache for logged in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
	set $skip_cache 1;
}

location / {
	try_files $uri $uri/ /index.php?$args;
}    

location ~ \.php$ {
	try_files $uri =404; 
	include fastcgi_params;
	fastcgi_pass 127.0.0.1:9000;

	fastcgi_cache_bypass $skip_cache;
	fastcgi_no_cache $skip_cache;

	fastcgi_cache WORDPRESS;
	fastcgi_cache_valid  60m;
}

location ~ /purge(/.*) {
    fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1";
}

El anterior tal vez sea el fragmento de código más utilizado y difundido entre los usuarios de Nginx y WordPress. Podemos observar el uso de la directiva fastcgi_cache_purge, pero vale la pena observar algunos problemas y desventajas en esta implementación:

  • Cualquier usuario externo desde su navegador puede purgar la cache de cualquiera de nuestras páginas. Esto puede representar un riesgo de seguridad en sitios con alto tráfico, imagina un script invalidando toda la cache en segundos, generando una carga excesiva en el servidor para rehacer todas las páginas en cada solicitud.
  • El método de solicitud POST no es necesario, de manera predeterminada Nginx solo guarda en cache las solicitudes GET y HEAD.

¿Cómo puedo saber si mi versión de Nginx cuenta con este modulo?

Si intentas usar la directiva fastcgi_cache_purge en la version gratuita oficial, sin el módulo, con nginx -t te aparecerá un error Unknown directive en Nginx.

También puedes ejecutar nginx -V desde tu terminal y ver en el listado si el módulo ngx_cache_purge está incluido en tu versión instalada.

Borrar los archivos de FastCGI cache

El plugin Nginx-Helper y algunos otros, nos ofrecen la alternativa de eliminar los archivos de la cache, de esta manera lo que se logra es forzar al servidor a servir una copia actualizada de la página al no encontrar la copia almacenada (borrada) en cache.

De esta manera no dependemos de ningún modulo externo y podemos utilizar prácticamente cualquier versión de Nginx oficial disponible. En mi opinión, esta es la opción que yo recomiendo en la mayoría de los casos.

Aunque también presenta sus desventajas:

  • Vamos a encontrar una cantidad de errores en los Logs de Nginx. Cuando Nginx trata de cargar el archivo de la cache y este ha sido eliminado, registrará un error.
  • Funciona sin problemas cuando Nginx y PHP se ejecutan con el mismo usuario. Para ambientes con múltiples usuarios no es tan sencillo, definitivamente deberías considerar pagar por la versión comercial de Nginx.
set $skip_cache 0;
if ($query_string != "") {
	set $skip_cache 1;
}

# Don't cache URL containing the following segments
if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|wp-.*.php|index.php|/feed/|.*sitemap.*\.xml|/feed/|/checkout|/add_to_cart/|/cart/|/my-account/|/checkout/|/logout/)") {
	set $skip_cache 1;
}

# Don't use the cache for logged in users or recent commenter or customer with items in cart
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|[a-z0-9]+_items_in_cart|[a-z0-9]+_cart_hash") {
	set $skip_cache 1;
}

# Use cached or actual file if they exists, Otherwise pass request to WordPress
location / {
	try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
	try_files $uri =404;
	include fastcgi_params;
	fastcgi_pass php;
	fastcgi_cache_bypass $skip_cache;
	fastcgi_no_cache $skip_cache;
	fastcgi_cache WORDPRESS;
}

Como vemos en el ejemplo anterior no es necesario hacer uso de la directiva fastcgi_cache_purge, por lo que garantizamos que funcionará en cualquier versión de Nginx y sin necesidad de hacer uso de módulos o código de terceros.

Si quieres conocer más a detalle sobre como se conforma el nombre de los archivos de cache o la fastcgi_cache_key, te recomiendo este tutorial. Para comprender como se localiza el archivo en cache de una determinada página y de esta manera borrar el archivo preciso. Muy útil si vas a desarrollar tu propio plugin o herramienta.

Otra alternativa: cache_bypass vs no_cache

Esta es simplemente la mejor opción si tienes conocimiento para hacer tu propio desarrollo, ya que no encontrarás ningún plugin para implementar en tu sitio esta solución.

  • fastcgi_cache_bypass – Le indica a Nginx que no debe buscar por un archivo en cache, por lo tanto debe servir el contenido directo del sitio.
  • fastcgi_no_cache – Le indica a Nginx que no debe guardar en cache la respuesta del servidor.

Con lo anterior, fácilmente podemos deducir que la manera más fácil de refrescar el contenido de la cache es haciendo un bypass, pero sin activar la directiva no_cache contrario a lo que se hacia en los ejemplos anteriores en donde ambas directivas tomaban el mismo valor, en este caso cada una puede tomar un valor distinto dependiendo el caso. Es decir, de esta manera no purgamos o eliminamos el contenido de la cache, solamente lo forzamos a actualizarse o refrescarse.

set $refresh_cache 0;
if ($http_cache_refresh) {
	set $refresh_cache 1;
}

set $skip_cache 0;
if ($query_string != "") {
	set $skip_cache 1;
}

# Don't cache URL containing the following segments
if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|wp-.*.php|index.php|/feed/|.*sitemap.*\.xml|/feed/|/checkout|/add_to_cart/|/cart/|/my-account/|/checkout/|/logout/)") {
	set $skip_cache 1;
}

# Don't use the cache for logged in users or recent commenter or customer with items in cart
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|[a-z0-9]+_items_in_cart|[a-z0-9]+_cart_hash") {
	set $skip_cache 1;
}

# Use cached or actual file if they exists, Otherwise pass request to WordPress
location / {
	try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
	try_files $uri =404;
	include fastcgi_params;
	fastcgi_pass php;
	fastcgi_cache_bypass $skip_cache $refresh_cache;
	fastcgi_no_cache $skip_cache;
	fastcgi_cache WORDPRESS;
}

En el ejemplo hacemos uso de una cabecera personalizada, cuando se recibe Cache-Refresh: true en el encabezado de la solicitud, Nginx responderá en modo bypass, pero guardará en cache la respuesta sobrescribiendo el contenido anterior en caso de existir.

curl https://example.com/test -H "Cache-Refresh: true"

Como puedes ver, son varias las opciones que podríamos tener para activar la instrucción de refrescar la cache. El uso de una cabecera es solo un ejemplo.

Al igual que los anteriores, también tiene sus desventajas:

  • En el caso de WordPress requiere hacer desarrollo propio, ya que no existe un plugin.
  • Originalmente esta opción era usada mediante una solicitud tipo PURGE, actualmente Nginx (versión gratuita) de manera predeterminada admite los métodos GET y HEAD, la directiva fastcgi_cache_methods bloquea y no admite la opción PURGE.
  • Con el uso de una cabecera personalizada, como en el ejemplo anterior, no he logrado limitar su uso para aceptar únicamente solicitudes internas o localhost. Por lo que existe el riesgo de que cualquier usuario externo pueda invalidar la cache.

Conclusiones

Sin duda la mejor opción siempre será contar con la versión oficial adecuada a nuestras necesidades, especialmente si estamos generando ganancias y hacemos negocio con las herramientas, es importante pagar por su uso y ofrecer a los clientes soluciones idóneas y a la altura de sus necesidades.

Es una cuestión de ética y profesionalismo el uso que hagamos del ingenio para resolver ciertas situaciones, por lo que es importante no aprovecharse y lucrar con el desconocimiento de los clientes para ofrecer soluciones que pueden ser ingeniosas, pero no las mas adecuadas en un ambiente profesional o empresarial.


Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *