Ejercicio Golang: Cálculo números primos con goroutines

Hace tiempo que voy dándole vueltas a la manera en que se desarrollan la mayoría de aplicaciones web, especialmente en php y cada vez más siento la necesidad de cambiar la manera de trabajar, en realidad, casi de cambiar mi lenguaje de programación habitual, php, por otra cosa.

Uno de los lenguajes que me interesa mucho es golang. Me gusta su sintaxis parecida al C, que es un lenguaje compilado, que está muy pensado para ayudar al programador y, sobretodo, me interesa lo fácil que es crear rutinas que corren en paralelo y que se pueden comunicar limpiamente mediante canales seguros.

Las CPUs de hoy en día tienen múltiples cores, cualquier ordenador doméstico o teléfono puede tener fácilmente entre 2 y 8 cpus corriendo y golang es posiblemente el lenguaje que permite usar esa potencia con más facilidad.

Hoy me he decidido a hacer un pequeño ejercicio, un cálculo de  los números primos en un rango dado de enteros. Quería ver si es fácil implementar un problema simple, y también quería probar si es fácil el uso del cálculo paralelo con Go. La verdad es que me he divertido mucho. Go es fácil de usar, el compilador te ayuda mucho a la hora de programar y la sintaxis es limpia y se entiende muy rápido.

El problema a resolver

El problema es sencillo. Dado un rango de números enteros, por ejemplo 1 a 1.000.000, queremos saber cuales de ellos son números primos.

En un lenguaje tradicional como php, o en java o C++ si no queremos complicarnos la vida, el algoritmo sería algo así como:

Para todos los enteros entre <inicio> y <fin>, si es primo, mételo a la lista y mira el siguiente.

Un número primo es aquel que sólo es divisible por 1 y por si mismo, o sea que es un problema que podría resolverse por fuerza bruta, intentando dividir ese número por todos los inferiores superiores a 1. Si el resto (módulo) de la división es cero en algún momento, el número no es primo. Hay otros métodos más óptimos de calcular un número primo, pero no es ese nuestro objetivo y ya nos va bien para el ejercicio usar un mecanismo de fuerza bruta.

¿Que ocurriría si en lugar de iterar sobre un bucle para cada posible candidato, lanzásemos todos los cálculos en paralelo? A fin de cuentas, ¿a quien no se le ha pasado por la cabeza lanzar un millón de procesos en paralelo y ver como la CPU salta por los aires 😉 ? La respuesta: No ha pasado nada. En mi máquina de pruebas, Go ha sido capaz de ejecutar 1 millón de procesos simultáneos sin pestañear, devolviendo la respuesta en menos de 10 segundos.

Te reto a hacer la prueba con php, java o incluso C++. Y en menos de 100 líneas de código.

El código fuente y cómo instalarlo

No es necesario que te bajes el código para seguir el tutorial, pero si quieres toquetear un poco y no te apetece copiar y pegar, lo tienes en https://github.com/x1m3/primeNumberTest.

Primero tendrás que instalar go, en mi caso un simple apt-get golang. Luego tendrás que crear un directorio de trabajo y apuntar la variable GOPATH a este. En mi caso, mkdir golang; cd golang; export GOPATH=pwd.

Finalmente, go get  github.com/x1m3/primeNumberTest bajará el código y te lo dejará dentro de la carpeta ./src. En ./bin encontrarás el ejecutable, que siempre podrás generar de nuevo ejecutando go install  github.com/x1m3/primeNumberTest

¿Fácil, verdad? y es que golang está pensado para que los desarrolladores sean felices.

La ineficiente rutina que nos dice si un número es primo

Vamos a comentar un poco el código, algo que nos vendrá muy bien si eres nuevo en go.

Lo primero que vemos es que hay pocos paréntesis y prácticamente ningún punto y coma. Los condicionales o bucles no necesitan los paréntesis, a fin de cuentas son obvios. Tampoco es necesario usar ; para indicar final de línea. Menos cosas a escribir (Veo que se me coló un ; en la línea 28… si es que salen automáticos ya).

La definición de la función es limpia y no tiene secretos.

La función se llama isPrime, acepta un parámetro de tipo int64 y devuelve un booleano. Primera sorpresa: Golang tiene tipos, algo que puede resultar un poco engorroso para los programadores de javascript o php pero que también puede ayudarnos mucho a la hora de evitar efectos indeseados.

Hay que definir las variables e indicar su tipo. 2 comentarios aquí. Golang no compilará si una variable se ha definido pero no se ha usado. Una buena ayuda para mantener nuestro código limpio. También es posible definir variables en marcha sin especificar el tipo, siempre que este pueda ser inferido por el programador. Para ello, en lugar de usar la asignación con =, escribiremos :=.

Por ejemplo:

Creará una variable llamada i de tipo integer, ya que 5 es un entero. Esto es muy útil para definir variables “tontas”, de aquellas que sólo tienen sentido en una pequeña parte del código. Por ejemplo una variable que sólo vaya a usarse en un bucle:

Crearía 3 variables i,j,z que se usarían dentro del bucle pero no tendríamos que definir.

WTF.. ¿Esto es necesario para calcular el módulo? No. Lo hemos hecho así para realizar los cálculos en coma flotante y darle más caña a la CPU. En realidad, podríamos haber usado el operador % sobre integers. Pero ya que estamos, expliquemos de que va.

math.Mod calcula el resto de una división entera entre sus dos parámetros, que son número flotantes de 64 bits. Para usar la función hemos tenido que hacer un import de la librería math. Como no tenemos conversión implícita de tipos, hemos tenido que forzar una conversión (cast) y convertir los enteros en flotantes. La sintaxis es limpia, float64() tiene forma de función, lo cual me parece más agradable que la sintaxis en C o php.

La versión secuencial de la función que calcula todos los primos en un rango

Tampoco hay mucho secreto aquí, aunque si que tendremos que explicar algunos detalles interesantes.

Atentos a los corchetes en el parámetro de retorno. Esta función acepta 2 enteros de 64 bits y devuelve una slice (una “lista”) de enteros de 64 bits. Los arrays son de longitud fija en Go, igual que en C. Los slices son un concepto que permite referirnos a un rango dentro de un array. Yo los interpreto con un puntero al array, en el que también sería posible definir la longitud. En el ejemplo que nos interesa a nosotros, podemos considerarlo como una lista de longitud variable.

Por lo demás, ningún secreto en esta función. Se itera desde el primero hasta el último con un bucle for y se va comprobando si es primo llamando a la función isPrime() que ya comentamos antes.

A comentar aquí, para añadir un elemento a la lista primeNumbers hemos de usar la función append, pasándole la lista original y el nuevo elemento. Esto no es muy óptimo, ya que append hace una copia del array y devuelve la nueva copia. Luego ya vendrá el garbage collector a salvarnos el culo, pero este mecanismo podría causarnos problemas de memoria.

La versión paralela. Empieza la fiesta

Hasta ahora, nada interesante. Un código que podríamos haber picado en cualquier lenguaje. Pero nos hemos dado cuenta de una cosa. Averiguar si cada número es primo o no es independiente de sus vecinos. ¿Por qué calcularlos de uno en uno si podríamos hacerlos todos a la vez? O, por lo menos, podríamos hacerlo de cuatro en cuatro u ocho en ocho en función del número de procesadores que tengamos.

La idea sería crear un thread para cada cálculo y recoger la respuesta de alguna manera. Hacer eso con php sería complicado y poco eficiente. Su librería de threads no es lo mejor que se ha inventado. Programadores de C++ o Java se atreverían a picar unas cuantas líneas de código y algún mutex para hacerlo. Un trabajo pesado. Los de javascript y node podrían tenerlo más fácil, usando las funciones de callback. Por desgracia node.js no es precisamente óptimo en operaciones que usan mucha cpu.

¿Y que tal con Golang?

El código completo de este apartado está en rangeParallel.go

Vamos a ir poco a poco. Primero decir que la función se parece mucho a la anterior, salvo unos ligeros cambios. Lo primero que hacemos es definir un canal (chan) que acepta tipos PrimeResult.

PrimeResult es una estructura que hemos definido nosotros así:

es decir, un número y un booleano que nos dice si el número es primo o no.

Ya podemos intuir que eso llamado canal no es otra cosa que un canal de comunicación que usaremos para comunicar procesos. Piensa en ello como algo parecido a un pipe de unix.

Una línea interesante. El mecanismo defer es propio de Golang y sirve para especificar que ese comando debe ejecutarse cuando la función finalice. Así no nos olvidaremos nunca de cerrar el canal, algo que es obligatorio hacer. Por decirlo de manera coloquial, defer close(channel) significa “y cuando acabes, cierra el canal llamado channel”.

¡Guau! Nuestra primera llamada a go. A partir de este punto tendremos 2 hilos de ejecución, el actual, y uno nuevo que ejecutará la rutina firePrimeCalculations.

firePrimeCalculations() es muy sencillita, pero importante. Tiene un bucle en el que va lanzando ejecuciones paralelas de isPrimeAsync(). Fijaos en la palabra go antes de la llamada. Por cada vuelta del bucle creará un nuevo hilo de ejecución. ¿Y si el bucle tiene 1.000.000 de valores? Pues.. crearemos 1.000.000 de hilos..

Notar también que cada llamada a isPrimeAsync() recibe un número diferente y el canal por el que debe devolver el resultado.

Aquí tenemos el secreto de la comunicación entre los procesos. Primero creamos un objeto de tipo PrimeResult. Notad el uso del comando new. Luego asignamos sus dos valores number y prime. Para calcular si es primo llamamos a la función isPrime() que habíamos definido al principio.

Y la parte interesante. Devolvemos el resultado por el canal, para quien pueda estar interesado. Notad aquí ese asterisco, que huele a C de mala manera. result es en realidad un puntero, nosotros queremos devolver el valor al que apunta, por eso el *.

Vale, ¿Y quien diablos lee el canal?

Pues tienes la lectura del canal en la función primesInRangeParallel(), cuyo código mostramos bastante arriba. En concreto, en este bucle

La línea res= <- channel es la que nos interesa. Espera a que alguien escriba en el canal y recoge el resultado que llegue. Es importante señalar en que la lectura es bloqueante. Esperará a que cualquier hilo finalice y tomará el resultado.

Todo se ejecuta dentro de un bucle por que si habíamos lanzado n hilos de cálculo, tenemos que esperar n respuestas.

¿Y los benchmarks donde están?

La gracia de esta prueba era probar un poco las posibilidades de golang, y el paralelismo es posiblemente la más importante de ellas.  La verdad es que el ejemplo que hemos usado no es óptimo para probar paralelismo, porque un cálculo de módulo es tan rápido que el overhead introducido a la hora de crear y destruir hilos puede ser mayor que la ventaja obtenida con el paralelismo. Por eso hemos complicado un poco el cálculo del módulo forzando una operación en punto flotante.

No lo he explicado hasta ahora, pero si bajas el código fuente y lo instalas encontrarás un ejecutable llamado primeNumberTest que acepta 3 parámetros: valor inicial, valor final y parallel o sequential.

Con eso podrás hacer pruebas ajustadas a tu ordenador y probar que versión es la más rápida.

Por lo general, la versión secuencial será más rápida en máquinas con pocos nucleos y/o cálculos de números pequeños. Las máquinas con muchos cores se beneficiarán mucho más de la versión paralela.

Atención a la variable de entorno GOMAXPROCS. Le indica a go el número de procesadores que debe usar. Según la documentación se ajusta al número de cores disponibles automáticamente, pero yo me he encontrado con que no era así en un server virtual de linode. Para ello he debido asignarle el valor 4, para que use 4 cores de los 6 que tenía la máquina disponible

Para que te hagas una idea de los tiempos que he obtenido, estos son los resultados, ejecutados con 4 procesadores, en un servidor que actualmente tiene 6 y que estaba trabajando con un apache y un varnish, sirviendo webs en producción (con un par…).

export GOMAXPROCS=4

time ./bin/primeNumberTest 2000000 3000000 parallel

real    0m8.355s
user    0m26.730s
sys     0m2.550s

time ./bin/primeNumberTest 2000000 3000000 sequential

real    0m20.681s
user    0m20.273s
sys     0m0.177s

Ignora el valor de user en el caso paralelo, porque suma el tiempo de cálculo en cada core. Como usamos 4 cores casi al 100% se ha ido a un valor de 26 segundos. Pero el tiempo que el usuario esperó no es ese.

Dicho a lo bruto, la versión paralela ha sido casi 2 veces y media más rápida que la secuencial. A cambio, hemos usado 4 cores en lugar de uno, por lo que hemos perdido casi un core y medio en la gestión del paralelismo. Si el problema hubiese sido diferente, por ejemplo, con conexiones de entrada/salida o esperas de usuario, la eficiencia habría sido superior.

Conclusiones

No me quiero enrollar más, que ya me he alargado mucho, pero quisiera hacer dos o tres comentarios.

Golang me ha parecido agradable de usar y creo que es un lenguaje robusto con mucho recorrido. La sintaxis es clara y los canales y las gorutinas son una maravilla que permite jugar con el paralelismo muy fácilmente.

Golang no es demasiado orientado a objetos. Pero tampoco es un lenguaje procedural. Para mi, es un lenguaje orientado al paralelismo. Quizás se echan a faltar algunas características de la orientación a objetos pero su simpleza sintáctica y las llamadas go permiten resolver problemas de manera sencilla que podrían ser un infierno en otros lenguajes.

Está muy orientado a los flujos de trabajo actuales, por ejemplo la instalación de vendors desde repositorios git. No lo he tratado aquí pero tiene soporte para tests unitarios de serie.

Verdaderamente, creo que golang ha llegado para quedarse. Personalmente, pienso meterme más a fondo con el. Seguro que seguiré dándoos la vara con el.

 

Acelerando WordPress con Varnish

Es sabido que WordPress no es un CMS especialmente rápido. Su inmenso ecosistema de plugins y la amplia posibilidad de personalización que permiten sus temas hace que muchas instalaciones de wordpress tiendan fácilmente a generar un elevado número de peticiones a la base de datos y/o a ejecutar mucho código php en cada petición.

Por suerte, WordPress cuenta con un sistema muy decente de cacheo que permite evita realizar cálculos idempotentes en cada una de las peticiones. Y algunos de esos plugins, incluso permiten comunicarse con proxies inversos como varnish que permiten cachear el resultado de las páginas.

En este artículo os vamos a explicar como configurar un varnish con WordPress. Para ello usaremos el plugin w3 total cache, aunque existen otras opciones posibles.

Instalando W3 total Cache

W3 Total Cache es uno de los plugins que siempre tengo en todos mis wordpress. Se integra perfectamente en WordPress y es capaz de realizar multitud de operaciones, por ejemplo cachear las consultas a la base de datos o los objetos internos de wordpress en diferentes contenedores de cache, por ejemplo disco, APC o un memcache o similar.

También es capaz de minizar el código o combinar javascripts y css.

Es un plugin muy recomendable aunque no vayamos a usar varnish.  Lo que lo hace muy interesante de cara a varnish es que es capaz de lanzar comandos “purge” contra nuestro varnish cuando lo considere conveniente. De esa manera, cuando añadamos una nueva entrada, por ejemplo, W3 Total Cache se encargará de hablar con nuestro varnish y decirle que páginas debe dejar de cachear.

Paso 1: Descargar y activar W3 Total Cache.

Ningún secreto aquí. Hay que ir a la sección de plugins, pulsar el botón “Añadir plugin”, buscar “w3 total cache” en el buscador y pulsar el botón instalar. Como cualquier otro plugin.

Paso 2: Configurar w3 total cache, por el momento, sin varnish

De momento, vamos a configurar el plugin sin usar varnish aún. El plugin de por si tiene bastantes configuraciones y algunas como el minimizado de HTML pueden romper la apariencia con algunos temas. Por tanto, primero nos vamos a asegurar que todo funciona sin varnish.

Visitar el menú “performance/General Settings” y activar la siguientes preferencias:

Activar page cache: Si es posible, usar APC:Opcode para usar la memoria del servidor. Si no, usar disk:enhanced.

Activar minify: Yo lo dejo en auto y cache en disco. Esta opción puede causar conflictos con algunos temas, por lo que será uno de los puntos a tocar si más adelante algo se descuadra.

Database Cache: Yo lo desactivo, ya que la siguiente opción “Object Cache” guarda la misma información y puede cachear varias consultas a la base de datos en una única entrada. Al gusto. Activarlo no debería dar problemas.

Object Cache: Activado en APC:Opcode si es posible. Esto acelera mucho de por si cualquier instalación de wordpress, ya que ahorra muchísimas consultas a la base de datos.

Browser Cache Enabled: Activado. Sirve para indicarle a los clientes por cuanto tiempo deben cachear nuestra web, las imágenes, etc.. de manera que en siguientes visitas no vuelvan a pedir recursos que ya habían solicitado. Aligera mucho el servidor. NOTA: Estos valores serán usados más adelante por varnish para conocer los tiempos de cache de cada recurso.

Una vez configurado, activar la configuración y borrar las caches con el botón que nos mostrará arriba.

Si en este momento cerramos la sesión y visitamos nuestra web notaremos un importante aumento del rendimiento.

NOTA 1: Hay que cerrar la sesión, o usar otro navegador porque W3 Total Cache no se activa para los editores. Por tanto, apenas notaremos ninguna mejora.

NOTA 2: Probar que todo va bien, que no se descuadra ninguna página y que los enlaces funcionan. Algunos temas de wordpress muy avanzados (y mal programados) pueden tener problemas con la compresión de CSS y HTML.

Paso 2: Configurar W3 total Cache para usar Varnish

Volvemos a visitar la sección “Performance/general Settings” de nuestro panel de control y vamos a la sección “Reverse Proxy”. Lo activamos y entramos la dirección de nuestro varnish. En mi caso, corre en el mismo servidor que apache, por tanto, será 127.0.0.1.

Lo que hemos hecho aquí es decir a wordpress como puede invalidar páginas de wordpress cuando estas hayan cambiado, es decir, a quien debe enviarle los comandos purge.

Visitar la sección “Page Cache Settings” y configurar al gusto. Por lo general, las opciones por defecto son correctas. A mi me gusta aumentar el número de páginas que deben purgarse ante un cambio y bajar los tiempos, pero depende de tu servidor y el tráfico que tengas.

Paso 3: Configurar varnish para escuchar comandos purge

Aquí entramos en materia. Suponemos que ya tenemos un varnish corriendo y bien configurado como explicamos en la entrada Instalando Varnish.

Tenemos que editar el archivo de configuración de varnish, que generalmente se encuentra es /etc/varnish/default.vlc

Si no existe el método acl_purge, debemos crear uno con esta forma:

acl purge {
“localhost”;
“127.0.0.1”;
}

Luego buscaremos la función sub_vcl_recv y añadiremos el siguiente código, lo más al principio que sea posible.

if (req.method == “PURGE”) {
if (!client.ip ~ purge) {
return(synth(405,”Not allowed.”));
}
return (purge);
}

 

Con esto le estamos diciendo a varnish que escuchará peticiones purge desde estas IPs. Cualquier petición desde una IP que no esté en la lista recibirá una respuesta 405:Not allowed.

Debe ser una lista de IPs seguras, para evitar que nadie pueda lanzarnos un ataque invalidando caches.

Paso 4: Configurar varnish para cachear un wordpress

Varnish no cachea ninguna petición que incluya cookies, para evitar liarla parda con las sesiones de cada usuario. WordPress tiene una manía inmensa de generar cookies para todo. Algunas de esas cookies pueden ignorarse, otras, como las cookies de sesión no.

Debemos configurar varnish para que borre aquellas cookies que son intrascendentes.

Por otro lado, puede ocurrir que tengamos varias webs corriendo en el mismo servidor, por lo que no podemos aplicar una configuración genérica de varnish para cada web. Personalmente, tengo agrupados todos los wordpress en una sección de la configuración y los identifico a partir del dominio. De esa manera, siempre puedo crear una regla específica para cada sitio.

Paso a pegar aquí algunos trozos de mi configuración que son significativos.

En la función vcl_recv

# Configuracion generica para todos los wordpress
# Añade una entrada en etc/hosts a 127.0.0.1 para cada web. Si no, no funciona el PURGE
if (req.http.host ~ “(www.jgimenez.info|otraweb.com|foro.otraweb.com)”) {
if (req.url ~ “\?(utm_(campaign|medium|source|term)|adParams|client|cx|eid|fbid|feed|ref(id|src)?|v(er|iew))=”) {
set req.url = regsub(req.url, “\?.*$”, “”);
}
if (req.url ~ “wp-(login|admin)” || req.url ~ “preview=true” || req.url ~ “xmlrpc.php”) {
return (pass);
}
if (req.http.cookie) {
if (req.http.cookie ~ “(wordpress_|wp-settings-)”) {
return(pass);
} else {
unset req.http.cookie;
}
}
}

Esto eliminará las cookies que vienen del cliente que no son necesarias para configurar la vista.

NOTA: Aquí os quisiera pedir ayuda. Esta lista de cookies cambia en función de la fuente que consultes e incluso con cada versión de wordpress. El trabajo de un sysadmin no acaba nunca. Si crees que falta o sobra algo, por favor, déjame un comentario.

Ahora haremos lo mismo, pero desde el lado de las cookies que se sirven desde el servidor web.

Buscaremos el método vcl_backend_response y añadiremos un código similar al anterior:

# Configuracion generica para todos los wordpress
if (bereq.http.host ~ “(www.jgimenez.info|otraweb.com|foro.otraweb.com)”) {
#Eliminamos todas las cookies que wordpress nos intenta colar
if ( (!(bereq.url ~ “(wp-(login|admin)|login)”)) || (bereq.method == “GET”) ) {
unset beresp.http.set-cookie;
#set beresp.ttl = 1h;
}
}

Aquí hemos de tener cuidado. Nos estamos cargando TODAS las cookies que wordpress nos entrega, salvo que estemos en las páginas de administración. Esto nos puede fastidiar algo, o puede romper funcionalidades extra de nuestro tema o plugins, por ejemplo, plugins de votos o contadores de visitas. Cada uno debería personalizarlo según sus necesidades borrando sólo las cookies que le interesan.

Reiniciar apache y varnish. (En realidad, apache no es necesario).

Probándolo todo

En mi caso, tuve que borrar todas las cookies antes de que funcionase. Probablemente la lista de cookies que borro es muy conservadora.

Importante recordar reiniciar varnish y probar la web sin estar logado.

Para ver que todo funciona, yo uso firebug y miro la cabecera age de la petición que entrega el código html. Si es mayor a cero, significa que la página se está cacheando.

Otra manera es consultar el log de apache y ver como a pesar de hacer peticiones, estas no llegan a apache.

Hay que estar atento al tema cookies de nuestra instalación de wordpress. Con un blog sencillo como el mio, no hay ningún problema. Con instalaciones más complicadas deberemos afinar más a la hora de filtrar cookies.

Si necesitas ayuda, o quieres comentar algo, no te cortes y deja un mensaje.

 

 

Instalando varnish

En un artículo anterior ya hicimos una introducción previa a varnish por lo que si quieres saber mejor cómo puede ayudarte te aconsejo que lo leas por encima.

En este artículo os voy a explicar los puntos mínimos que hemos de cubrir para tener una configuración mínima de varnish funcionando. Al acabar el tutorial tendrás una instalación mínima que funciona, que cachea en parte tu aplicación web y que será la base para construir sistemas mucho más complejos que se adapten a tus necesidades.

Consideraciones previas

El tema de los puertos

Como bien sabrás los servidores web funcionan habitualmente escuchando el puerto 80 para http y el 443 para https (En este artículo dejaremos de lado https ya que debido a que el tráfico está encriptado hay que tener algunas consideraciones adicionales).

Varnish es un acelerador web que se coloca delante del servidor web, entre este y el usuario. Es decir, las peticiones que antes se dirigían hacia el web server, ahora las atenderá varnish, y varnish se encargará de hablar con el servidor web.

Por tanto, varnish y apache (o nginx) no pueden estar escuchando en el mismo puerto.

Existen varias soluciones a este problema. Podríamos instalar varnish en otro servidor, siendo esta una de las soluciones habituales en entornos de alta demanda.

También podemos mover el puerto en el que apache atiende a otro diferente, por ejemplo el 8000. Entonces, dejaremos que varnish sirva sobre el 80 y realice las peticiones sobre el 8000, donde el servidor web le atenderá.

Varnish en el mismo servidor donde ya corren varias webs

En mi caso, me encontré con que tenía varias webs en el servidor, todas ellas corriendo con apache y, por supuesto, en el puerto 80. Yo quería instalar varnish en el mismo servidor, por lo que debía hacer varios cambios en las configuraciones que me iban a dejar las webs caídas por algo de tiempo.

Yo aproveché para hacer un cambio de servidor completo, gracias a las ventajas de los servidores virtuales. Creé una nueva instancia, instalé un sistema limpio y actualizado, configuré apache para correr en el puerto 8000 y varnish en el 80 moví sólo una web. Una vez estuvo todo correcto, empecé a mover webs del server antiguo al nuevo. Cuando finalicé la migración de todas las webs apagué el server antiguo. Sólo pagué por dos servidores durante los días que los tuve en marcha pero el proceso me sirvió para limpiar bien la casa.

Otra alternativa si no quieres, o no puedes levantar otra máquina virtual es instalar varnish en el puerto 8000 en lugar del 80. A partir de aquí, sólo tu sabes que corre en el 8000 (también puedes filtrar el tráfico por IP para que nadie haga el cotilla). Así puedes ir probando todas tus webs con varnish, accediendo a ellas a través del nuevo puerto. Cuando estés seguro de que todo funciona, configuras apache para servir en el 8000 y varnish en el 80, es decir, le das la vuelta a la instalación. Es bastante sencillo y seguro.

Memoria y CPU

Varnish es básicamente un consumidor de memoria y CPU. En realidad, básicamente de memoria, porque la CPU que usa la está ahorrando a otros servicios como el servidor web o la base de datos.

En mi experiencia,  con 500 MB o 1 GB es suficiente para que varnish suponga una mejora MUY importante en rendimiento del server. Pero estos números dependen de cada uno, tipo de web, comportamiento de las visitas, si usamos un CDN para estáticos o bien si los guardaremos en varnish, etc…

Si instalas varnish en tu servidor actual, tienes que asegurarte de que dispones de esa memoria adicional. Ten en cuenta también que cuando varnish esté correctamente configurado, tu apache correrá mas ligero por lo que necesitará menos recursos que pueden asignarse a varnish.

Si lo estás instalando en un servidor nuevo que hará de frontal, mi consejo es que empieces con una configuración mínima de 2GB o 4 GB que siempre puedes ampliar en el futuro, especialmente si corres con virtual servers. ¿Todavía con máquinas físicas? Tendrás que llamar a tu proveedor para le meta RAM, pero.. ¿De verdad estás usando aún máquinas físicas?

Instalando varnish

Ya he hablado antes de que existen diferentes configuraciones posibles. Aquí vamos a explicar como instalar varnish en un servidor nuevo, donde un apache escucha en el puerto 8000 y varnish correrá delante de el en el puerto 80.

Dependiendo de tu sistema operativo tendrás que descargar el paquete del repositorio adecuado. En mi caso, usé un debian y la instalación fue mi sencilla, simplemente un

sudo apt-get install varnish

El archivo de configuración de varnish es /etc/default/varnish. Allí configuré varnish para usar el puerto 80 y una memoria de 1024 MB. Esta es la parte que te interesa:

DAEMON_OPTS=”-a :80 \
-T localhost:6082 \
-f /etc/varnish/default.vcl \
-S /etc/varnish/secret \
-s malloc,1024m”

Un restart de apache y varnish, y todo está corriendo ya bajo varnish.

sudo service apache2 restart

sudo service varnish restart

NOTA: El orden es importante. A partir de ahora, siempre que quieras reiniciar tu servidor web debes reiniciar primero apache y luego varnish. Si lo haces al revés, varnish podría no reflejar parte de los cambios que hayan necesitado reiniciar tu apache.

Y ya lo tienes, si visitas tu web debería funcionar. El navegador ha pedido la página al puerto 80, allí varnish ha atendido la petición, ha visto que no tenía la página en memoria y la ha solicitado al apache. Cuando ha obtenido la respuesta, la ha entregado a tu navegador.

Puedes usar firebug u otro plugin que estudies las cabeceras de respuesta de la petición.  Verás que la respuesta incluye algunos campos como  Age=0, Via=1.1 varnish-v4 y X-varnish.

Varnish instalado, pero, ¿Está funcionando?

Técnicamente si. Está funcionado. Si el ejemplo que has usado es una página estática con algunos recursos pesados como imágenes notarás claramente como se sirve más rápido. Puedes verlo fácilmente con firebug, evaluando los tiempos de respuesta. Puedes comparar pidiendo la misma página al puerto 80 y al 8000 y mirando los tiempos.

También puedes fijarte en la cabecera http llamada age. Si tiene un valor diferente a cero y se incrementa en cada petición, significa que se está sirviendo desde la cache.

También puedes abrir tu log de apache y realizar múltiples peticiones. Verás como sólo aparece la primera. El resto, se entregan desde varnish y apache ni se entera.

También puedes usar la herramienta de consola varnishstat para ver que es lo que está pasando en varnish. main.cache_hit es un indicador que deberíamos mirar. Expresa el número de peticiones que se han servido desde la cache de varnish.

Mi varnish no cachea

Puede ocurrir, y en la realidad es muy frecuente, que después de una instalación básica de varnish no notemos ninguna mejora. Esto puede deberse a diferentes motivos que iremos exponiendo más adelante.

Uno de ellos puede ser la configuración de tiempos de expiración de tu apache. Si apache está sirviendo las páginas indicando que no se cacheen, o sin tiempos de expiración, varnish obedecerá a esas directivas y no cacheará salvo que se lo digamos explicitamente.

Otro motivo son las cookies. Como sirven para guardar estados dentro de una sesión concreta, varnish es muy conservador y por defecto decide ignorar cualquier petición que tenga cookies, para evitar riesgos y servir una página de un usuario a otro. ¡Imagina que ocurriría si consulto mi saldo bancario y me enseñan los datos de otro cliente!

En el siguiente artículo os explicaré como solucionar el tema de las cookies y os mostraré lo que debería ser un archivo básico de configuración que ofrece un buen rendimiento.