Diseñando una web ultra-rápida (V)

En el anterior post vimos cómo instalar y configurar nginx con caché y cómo comprobar el rendimiento. En este post me voy a centrar en cómo no servir contenido obsoleto.

Refrescando la caché

Lógicamente si la caché dura un día, los usuarios tardarán un día desde que envían un mensaje hasta que se pueda ver. Incluso ellos mismos no verían su mensaje, lo que es muy confuso.

Sin caché, estaríamos hablando entre 1000 y 2000 por segundo, que también es muy rápido. Como el backend sólo realiza una tarea muy básica, le permite entregar una cantidad de peticiones muy alta. Gracias a esto, podemos permitirnos que las cachés sean más pequeñas si queremos, ya que el tiempo de generar el contenido es realmente bajo.

Podemos establecer unos tiempos de caché distintos según el tipo de contenido que sea. Por ejemplo para los foros puede ser bastante largo. Para los hilos un tiempo intermedio y para los mensajes sería más corto.

Es más, como el backend puede devolver para cada petición unas cabeceras de control de caché distintas, podemos hacer que los hilos viejos y los mensajes viejos que tienen menos probabilidad de cambiar, que tengan una caché más grande. Especialmente las páginas de mensajes que tienen más de un par de días. Si tenemos bloqueadas las ediciones pasado un tiempo, ya tenemos garantizado que éstos no cambian.

El contenido que puede cambiar, la caché como mucho va a ser de escasos segundos. Y a veces incluso eso va a ser mucho en algunos casos ya que la aplicación podría pedir el post que se acaba de escribir y no lo vería.

Para solucionar eso lo ideal es que al realizar la petición PUT/POST desde javascript, el servidor devuelva el JSON entrada con él y lo integre. De ese modo una caché de unos pocos segundos no le afecta y ve el contenido instantáneamente. Los demás tardarán un poco, por ejemplo 5 segundos.

Pero esto quiere decir que si un hilo recibe mucha actividad, vamos a tener muchos usuarios leyendo la última página y refrescando. Cada poco, vamos a tener que generar una versión nueva. La ventaja es que la caché de 5 segundos va a enviar la misma copia a varias personas, por lo que la carga ya se va a reducir muchísimo.

Pero esto se puede mejorar aún más.

304 Revalidate con NGINX

En NGINX se puede habilitar una opción de caché llamada “revalidate“, que cuando la caché caduca, envía al servidor una petición con E-Tag y/o If-Modified-Since, y el servidor puede responder con HTTP 304 Not Modified. Si esto sucede, NGINX renueva la caché. Ésta respuesta “Not Modified” deberá tener también las cabeceras Cache-Control y en ellas podemos incluso definir nuevos parámetros de persistencia si queremos.

Supongo que sabéis cómo funciona: En cada petición en el backend generamos o una fecha de modificación, o un string (tipo una firma) llamado E-Tag. Cuando el cliente web nos envía una petición para la que ya tiene caché, incluye éstos parámetros y podemos comparar si su versión es aún válida o la hemos cambiado. Si ha cambiado o no provee éstas cabeceras, realizamos la petición normal y devolvemos HTTP 200. Si no ha cambiado, devolvemos HTTP 304, sin contenido.

Para hacer esto en Nginx es tan sencillo como activar cache_revalidate:

uwsgi_cache_revalidate on;

La ventaja de esto es que no tenemos que generar el contenido otra vez y no tenemos que enviarlo. El truco reside en encontrar la forma de saber si ha cambiado sin apenas usar recursos. Si en la base de datos tenemos una fecha de modificación, podemos usar esa. También se puede usar redis u otro sistema de caché en memoria para almacenar la versión y actualizarla cada vez que envíen contenido nuevo.

También está cache_lock que es interesante. Si se activa, cuando llegan dos peticiones para la misma página y no tiene caché, sólo lanza una petición al backend y la segunda petición se espera. Muy útil para evitar consumo de recursos extra a costa de que el cliente espere un poco. De todos modos es muy posible que la primera petición termine antes que la segunda, por lo que no debería haber ningún retraso percibido, con la ventaja de consumir menos:

uwsgi_cache_lock on;

Volvamos a revisar la gráfica que hice al principio:

grafica

En el extremo izquierdo hay 3000 peticiones por segundo para las consultas que no pesan casi nada, y en el derecho unas 300 por segundo para las que ya tienen un tamaño considerable. Esto quiere decir que para contenido grande, podemos hacer que NGINX lo mantenga en caché a un ritmo 10 veces más rápido que generarlo de nuevo! Es como una caché sin caché. O caché inteligente.

Incluso podría ser mejor. Puede que haya contenido que requiera mucha lógica para computarlo aunque no pese demasiado. Este contenido se beneficiaría aún más de ésta práctica.

Con éste sistema, la caché se puede bajar hasta 1 segundo y el ritmo de entrega sigue siendo muy bueno. Aún así, hay que seguir considerando el cachear más tiempo el contenido que no va a cambiar, porque aún es tres veces más rápido que el 304.

Hago la prueba con caché de 1 segundo, gestión del 304 y a medir con siege:

$ siege -f urls3.txt -c 8 -ib -t 60s
(...)
Lifting the server siege...
Transactions: 216952 hits
Availability: 100.00 %
Elapsed time: 59.61 secs
Data transferred: 242.55 MB
Response time: 0.00 secs
Transaction rate: 3639.52 trans/sec
Throughput: 4.07 MB/sec
Concurrency: 7.46
Successful transactions: 216952
Failed transactions: 0
Longest transaction: 0.27
Shortest transaction: 0.00

Como vemos, llego a las 3639 peticiones por segundo. Es decir, a pesar de servir un contenido que me daba 2500, se sirve a una velocidad altísima. Y como comentaba, si el tamaño de la respuesta aumenta, ésta velocidad no se reduce apenas porque estamos devolviendo sólo un 304. El truco es devolver el 304 rápidamente, en mi caso siempre lo hago sin ningún cálculo por lo que es de esperar que en una web real haya que calcular un poco.

Métodos de refresco forzado

Aún se puede mejorar más, pues NGINX tiene otras opciones de interés. Una es el stale-while-revalidate, que hace que entregue a los clientes una copia antigua enseguida mientras él la actualiza por debajo.

 proxy_cache_use_stale updating error timeout http_500 
       http_502 http_503 http_504;
 proxy_cache_background_update on;

Al hacer estos cambios y probar, ya veo un aumento en el rendimiento:

Transactions: 239757 hits
Availability: 100.00 %
Elapsed time: 59.72 secs
Data transferred: 267.79 MB
Response time: 0.00 secs
Transaction rate: 4014.69 trans/sec
Throughput: 4.48 MB/sec
Concurrency: 7.43
Successful transactions: 239758
Failed transactions: 0
Longest transaction: 0.23
Shortest transaction: 0.00

El problema de esto es que no podemos especificar cuan vieja puede llegar a ser la caché, por lo que aunque nuestra caché sea de 1 segundo, si durante una hora nadie la pide, el siguiente que la pida verá la de hace una hora y se actualizará en segundo plano.

Si queremos corregir esto, en lugar de usar estos comandos hay que usar las cabeceras http Cache-Control con stale-while-revalidate y stale-if-error, que permiten especificar el máximo de tiempo. Pero no estoy seguro de si Nginx sigue los tiempos también.

Otra de las opciones es purgar la caché a demanda. Se puede hacer que al recibir ciertas peticiones nginx vacíe la caché. Primero establecemos que cuando se reciba una petición tipo PURGE nos asigne una variable a 1:

    map $request_method $purge_method {
        PURGE 1;
        default 0;
    }

Luego asignamos este $purge_method como flag al comando cache_purge:

uwsgi_cache_purge $purge_method;

Esto resulta en que cuando se recibe una petición de tipo PURGE, la url especificada se elimina de la caché. También acepta patrones, por lo que se puede hacer:

$ curl -X PURGE -D – "https://www.example.com/api/forums/1/*"

Otra opción son URLs que no leen de caché, pero sí guardan. Lo normal es que si una URL guarda a la caché, también lea de la caché. Y que si no lee, tampoco guarde. Esto es así porque si una URL sirve contenido basado en cookies, podría estar cacheando contenido de un usuario autenticado, con información privada y luego servirla a anónimos.

Pero si hablamos de urls que siempre devuelven lo mismo, se puede pensar en una URL alternativa o una cabecera que hace que guarde en caché el contenido nuevo, sin leerlo nunca de la caché. Y sería muy útil para en lugar de purgar la caché, refrescarla con antelación cuando sabemos que ha cambiado.

En nginx esta función la realiza “cache_bypass” y se asigna a las variables que queramos. Si cualquiera de ellas devuelve algo distinto de vacío o cero, el contenido devuelto es nuevo. Si no se especifica “no_cache” la respuesta obtenida se guardará igualmente en la caché.

proxy_cache_bypass $bypass_request

Luego ya es lanzar un wget –mirror o similar con el método adecuado o cabeceras adecuadas, y nginx refrescará la caché.

Aunque igual es más sencillo purgarla por patrón y luego llenarla de nuevo consultando normalmente. Eso ya es cuestión de gustos y de las ventajas/inconvenientes para lo que queramos hacer exactamente.

Lo dejamos aquí por hoy. En el próximo post volvemos a la carga acelerando las páginas que requieren autenticación.

Nos vemos pronto!

PostgreSQL 11 beta 2 publicado! ¿Qué trae de nuevo?

Actualizo: acaba de salir la beta 2 con corrección de errores.

Hoy leo en Twitter con gran satisfacción que PostgreSQL 11 ha publicado su primera versión beta! Estoy muy contento por ellos. Lástima que en mi trabajo actual no lo usen, lo hecho mucho de menos.

PostgreSQL es una de las bases de datos más avanzadas que existen, ya lo dice su propio logo. No recomendaría ninguna otra base de datos.

Pero al lío, versión nueva, ¿qué trae esta versión para que queramos actualizar? Muchas cosas! Funcionalidades nuevas, y muchas optimizaciones!

Me he leído las release notes que hay propuestas a día de hoy y me he quedado flipando en colores. ¡La cantidad de optimizaciones y mejoras que hay es inmensa! No sé si exagero, pero creo que la mayor colección de mejoras que he visto desde la 9.4. O la mayor de todas, no lo sé.

Se han centrado mucho en la mejora de rendimiento de lo que ya había. En el paralelismo. Parece que ahora casi todo ya soporta paralelismo, con lo que se aprovecharán todos los núcleos. Y finalmente en las particiones de tablas, hay un gran esfuerzo en hacerlas muchísimo más rápidas y más convenientes.

En pocas palabras, sea cual sea tu servidor, tus datos o tu aplicación, actualizar a PostgreSQL 11 va a suponer un aumento de rendimiento claro.

Si ya exprimías PostgreSQL antes, ahora, sin tocar nada, va a funcionar bastante más rápido. Cuanto, no lo sé, pero por la cantidad de mejoras tiene que ser muy apreciable.

Veo que con las particiones PostgreSQL se está acercando muy peligrosamente al territorio de las NoSQL como MongoDB o Cassandra. Incluso Hadoop. Comenzando con esta versión comienza a ser realmente planteable montar un sistema de clustering/sharding con PostgreSQL. Viendo esto tengo muy claro que su objetivo en las próximas versiones es simplificar todo esto y hacer que sea realmente sencillo usar sharding y map/reduce en PostgreSQL. Si en la versión 9.6 ya había unos escenarios típicos de NoSQL donde PostgreSQL era mejor en todos los sentidos, ahora con la versión 11 en la mayoría de los escenarios nos podemos plantear PostgreSQL seriamente. Un diez para el equipo que hace PostgreSQL posible.

Otra cosa relacionada es el manejo de datos por encima del terabyte. Con las particiones y las nuevas funcionalidades PostgreSQL va a manejar los datos casi que mejor que los productos BigData, de forma más sencilla.

Tengo ganas de ver esas pruebas que hace la gente, con varios servidores y petabytes de datos siendo calculados en tiempos récord, convencido de que va a romper las reglas del juego.

Es imposible comentar todas las mejoras una por una. Así que voy a tomar unas pocas que me parecen especialmente relevantes y las comento por encima.

Los Foreign Data Wrappers pueden enviar los agregados (SUM, MAX, …) a las tablas foráneas que son particiones.

Esto sirve para montar un clúster de nodos postgresql. Con un servidor principal que contiene lo básico y un montón de servidores hijos que contienen terabytes de datos cada uno. El servidor principal cuando quiere sumar o contar sobre una tabla que está repartida en los otros servidores, encomienda a cada servidor contar o sumar por su cuenta, entregar el resultado al padre y el padre computa el resultado final.

Esto es lo que hacen los productos del BigData como Hadoop, Cassandra, etc. Permite computar con todas las CPU de todos los servidores a la vez. Tremendo.

Los índices únicos ahora pueden incluir columnas extra que no participan en el constraint pero sí en un index-only scan

Los index-only scans son ideales en tablas con muchas columnas grandes, donde ciertas consultas sólo necesitan unas pocas columnas ligeras. Este truco existe desde hace bastante, pero con los índices únicos sólo podía contar con las columnas de la restricción de unicidad (que es lógico, no?).

Pues ahora los índices únicos pueden incluir columnas extra aparte, para facilitar estos index-only scan sin tener que crear otro índice aparte.

Los índices recuerdan el valor más alto para futuras inserciones

Un patrón de acceso muy común es insertar al final de una tabla, donde el nuevo registro es superior/posterior a todos los demás. Pasa con las primary keys automáticas, con tablas que registran por fechas y con códigos de facturas entre otros.

Para un índice, acceder al último registro tiene un coste de log(n). Como ahora recordará el último registro, en estos casos de uso, el coste será 1. En otras palabras, para este caso de uso los tiempos no crecerán cuando la tabla sea gigantesca.

HOT updates para índices con expresiones

En un update normal, debido a que hay transacciones y el patrón MVCC, hay que crear un registro nuevo y actualizar todos los índices.

Si resulta que no tienes ningún índice y nadie está mirando el registro (no hay bloqueos de lectura), aunque hayan transacciones en curso, se puede simplemente reemplazar el valor y ya está. A esto se lo llama en PostgreSQL un HOT update.

En anteriores versiones esto ya se hacía incluso con índices en la tabla, siempre y cuando ningún índice cambie en el proceso.

Ahora además se agrega el soporte de HOT update para índices con expresiones, eso quiere decir que si indexamos (a+b+c), siempre y cuando no cambiemos ninguna de esas tres columnas, PostgreSQL realizará un HOT update.

Sobra decir que los HOT updates son increíblemente más rápidos que los tradicionales.

Las funciones de ventana ahora son 100% compatibles con el estándar SQL:2011

Concretamente han aprendido “PRECEDING” y “FOLLOWING” para especificar grupos de valores cercanos, saltándose un valor arbritrario.

Si habéis usado funciones de ventana sabréis lo potentes y rápidas que son. Más capacidades en las funciones de ventana implica que mucha lógica complicada en la aplicación se puede escribir de forma sencilla en SQL, facilitando mantener el código y además se ejecuta mucho más rápido.

Agregar columnas con valores por defecto sin reescribir la tabla

Antes, al agregar una tabla con un valor por defecto NULL era instantáneo, pero si tenía otro valor PostgreSQL reescribía la tabla para acomodar el nuevo valor.

Ahora ya no, por lo que con default o sin él, va a ser instantáeno. Big win para los que tienen tablas grandes y necesitan agregar columnas.

Estadísticas para índices con expresiones

Hasta ahora las estadísticas de datos, que son las que usa postgreSQL para decidir qué forma de hacer una consulta es la más rápida, sólo iban por columna de la tabla. Si dos columnas estaban relacionadas, PostgreSQL lo ignoraba. Si el resultado de un cálculo tiene un histograma peculiar, también lo ignoraba.

Ahora, con un ALTER INDEX específico, le podemos decir a PostgreSQL que obtenga estadísticas para un índice que tiene una expresión.

Con esto, PostgreSQL puede tomar mejores decisiones al ejecutar queries que filtran por índices con expresiones.

CREATE AGGREGATE puede especificar ahora el comportamiento de la función de finalización

Básicamente, más personalización a la hora de hacer nuestras funciones de agregado, permitiendo hacer cosas que antes eran imposible, como hacer que trabajen como funciones de ventana.

SQL Procedures

En el caso de PostgreSQL, son como funciones, pero pueden abrir y cerrar transacciones por su cuenta. Su uso primario es para código SQL que normalmente no es compatible con las funciones.

En fin, como veis es mucho. PostgreSQL 10 ya traía un montón de novedades. No he podido incluir ni el 5% de lo que hay de nuevo, así que os animo a que lo leáis vosotros mismos.

Nos seguimos leyendo!

Diseñando una web ultra-rápida (IV)

Después de haber instalado y configurado NGINX con uWSGI, en el post de hoy toca replantearse cosas y empezar con la caché.

(Re)diseño de la API

Después del test que realizamos en la entrada anterior me doy cuenta que mi diseño actual de la API está mal orientado. Para probar https/2 hice una API con muchas urls muy pequeñas para demostrar la cantidad de peticiones que el navegador puede consultar con HTTP2. (unas 100 por segundo, a través de internet)

Mi esquema de URLs (es un foro) era el siguiente:

  • /user/<user_id> ; Obtiene el perfil de un usuario por ID
  • /category/ ; Obtiene un listado inicial de categorías de foros
  • /category/<category_id>/forum ; Obtiene una lista de foros dentro de una categoría, la primera página
  • /category/<int:category_id>/forum/page/<int:page_id> ; lo anterior, para las siguientes páginas. (5 por página)
  • /category/<int:category_id>/forum/<int:forum_id>/topic/page/<int:page_id>
    obtiene una lista de hilos, por páginas. 10 por página.
  • /category/<int:category_id>/forum/<int:forum_id>/topic/<int:topic_id>/post/<int:post_id>
    obtiene un mensaje en particular

El resultado es que la mayoría de los mensajes están entre 0.5Kb y 1Kb. Siguiendo la gráfica que vimos está claro que esto tiene un rendimiento muy inferior a lo que podría ser. Os la pongo de nuevo:

grafica

Hay que empaquetar más las llamadas, ya que el mejor rango de trabajo está entre los 4kB y los 16kB. Por desgracia algunas no se pueden, porque los usuarios no tiene sentido empaquetarlos en páginas. Las categorías sí se pueden, ya que se podría exportar prácticamente todo el listado de foros junto a las categorías. Los hilos se pueden hacer en 100 por página y los mensajes también se pueden paginar según los hilos de igual forma.

Esto hay que plantearlo correctamente, porque también es tirar mucho ancho de banda a la basura si terminamos sirviendo muchos datos que el cliente al final no necesita.

El principal problema es que, como queremos servirlo como estático, no queremos parámetros de consulta, por lo que el cliente no puede pasarnos un listado de los elementos que quiere. Para eso tiene que realizar consultas HTTP separadas, que al ser HTTP2 debería ser rápido.

El nuevo diseño podría ser como sigue:

  • /user/<user_id> ; Obtiene el perfil de un usuario por ID
  • /forum/ ; obtiene el listado de todas las categorías y todos sus foros. Incluye el número de hilos dentro de cada uno o el número de página final.
  • /forum/<int:forum_id>/page/<int:page_id> ; obtiene los hilos de un foro, a 200 por página. Incluye el foro otra vez, el número de mensajes/página final y también incluye nombres y url de avatares de los usuarios que aparecen.
  • /forum/<int:forum_id>/topic/<int:page_id>page/<int:page_id> ; obtiene los mensajes de hilo, a 50 por página. Incluye el topic otra vez y los nombres de usuario y sus avatares.

Os preguntaréis porqué me empeño en hacer las url jerárquicas, ya que un topic_id identifica un foro y no se repite en distintos foros. El motivo es que facilita después restringir el acceso por URL. Si no tienes acceso a un foro, se puede decir que te deniego el acceso a /forum/2/*, y no tengo que aplicar más reglas. Para eso, las urls jerárquicas tienen que comprobar que efectivamente estamos pidiendo el foro (o el hilo) que le hemos solicitado, pero se puede cachear en memoria fácilmente.

En cuanto al diseño de base de datos, paginar es bastante caro. Cuando pides a MySQL o PostgreSQL (o cualquier otra) que te devuelva la página 5 ordenando por id o fecha, no tiene más remedio que leer todos los mensajes hasta llegar a la página 5. Por esto, esta estrategia es poco eficiente con hilos de muchas páginas. Es recomendable agregar una columna de página o de nº de mensaje para poder filtrar con un Where. Otra opción es usar otra tabla que mantenga una relación de qué id es el primero de cada página.

Como MySQL cachea las consultas, esto no parece tener mucho efecto, pero en realidad MySQL no sabe muy bien cuando una modificación en una tabla debería purgar una caché, así que si alguien envía cualquier mensaje a cualquier foro, MySQL eliminará de la caché todas las consultas que lean mensajes. Agregar nosotros esta caché manualmente en forma de tabla o en REDIS nos permite purgarla o regenerarla cuando creamos conveniente. Tiene más trabajo, pero sacas más rendimiento.

Con el nuevo esquema, deberían haber muchísimas menos peticiones desde la web, éstas contendrán mucha más información, que a su vez es útil para el renderizado de la página que solicita el usuario.

Aún estoy pendiente de realizar estos cambios y van a ser un montón en el frontend Angular 6 que tengo. Así que no puedo sacar estadísticas de rendimiento aún. Esto tendrá que esperar más adelante.

De momento vamos a ver cómo funciona la API antigua con caché.

Configurando la caché en NGINX

Dependiendo de si habéis usado uwsgi, proxy o fastcgi, los comandos de caché tienen unos nombres ligeramente distintos, empiezan con el nombre del módulo y luego el comando. Pero la sintaxis es la misma. Yo lo hice con uwsgi, pero cambiar a otro es sólo reemplazar este nombre por el nombre de vuestro módulo.

Quedaría así:

uwsgi_cache_path /tmp/nginx-static levels=1:2 keys_zone=STATIC:10m
 inactive=1d max_size=1g;

upstream { 
 (...)
}

server {
 (...)
 location / {
  include uwsgi_params;
  uwsgi_pass backendpython;

  uwsgi_cache STATIC;
  uwsgi_cache_key backend_python_cache_v1_$request_uri;
 }
}

Con esto configuramos una caché. Es bastante sencillo. Hay que tener en cuenta que Nginx sigue las instrucciones de las cabeceras Cache-Control, Expires, etc. Para que la caché funcione nuestro backend tendrá que enviar las cabeceras adecuadas.

Si no queremos esto (aunque yo lo recomendaría), se le puede decir a Nginx que ignore las cabeceras y cachee las respuestas según su código HTTP:

uwsgi_cache_valid 200 15m;
uwsgi_ignore_headers "Cache-Control";

Reiniciamos NGINX:

sudo systemctl restart nginx

Y probamos otra vez con siege. Si la caché es lo suficientemente grande, al cabo de unas cuantas pruebas el rendimiento debería subir bastante:

$ siege -f urls3.txt -c 8 -ib -t 60s
(...)
Lifting the server siege...
Transactions: 503930 hits
Availability: 100.00 %
Elapsed time: 59.63 secs
Data transferred: 567.13 MB
Response time: 0.00 secs
Transaction rate: 8450.95 trans/sec
Throughput: 9.51 MB/sec
Concurrency: 6.70
Successful transactions: 503933
Failed transactions: 0
Longest transaction: 0.04
Shortest transaction: 0.00

Ahora en mi caso se alcanzan las 8450 peticiones por segundo. Esto es un muy buen rendimiento. La tasa de transferencia sigue siendo baja, lo que confirma que hay que intentar la nueva API que empaqueta más.

Esto es un avance muy grande. Con esto se pueden servir datos dinámicos a una velocidad igual que los estáticos. Queda ver cómo evitamos servir un contenido cacheado con datos antiguos, pero eso lo veremos en otro post.

Nos vemos en la próxima entrega!

Diseñando una web ultra-rápida (III)

En el anterior post vimos una visión general de la arquitectura necesaria. Si no lo has leído aún, haz clic aquí. Si ya lo has leído, es momento de pasar a la acción!

Instalación

Como ya sabéis algunos, todos mis servidores son Debian GNU/Linux. Si queréis seguir los pasos, serían para Debian 9 (Stretch). Para más detalle, recomiendo la guía de NixCraft (en inglés).

Primero, instalamos el paquete “nginx“:

$ sudo apt-get install nginx

Esto instalará el servidor y lo pondrá en marcha con una web por defecto.

Si navegamos a http://127.0.0.1/, deberíamos ver la página de bienvenida de Nginx:

welcome-screen-e1450116630667

Si no veis la página de bienvenida, posiblemente tengáis otro servidor web funcionando como Apache o Lighttpd en el puerto 80. Si es el caso, la instalación con apt-get seguramente habrá fallado al iniciar el servidor. Podéis probar a detener los otros servidores web o cambiar nginx de puerto. También es posible que tuvierais restos de una instalación de nginx anterior y haya cargado la configuración anterior. En este caso se puede probar a purgar el paquete e instalarlo de nuevo.

Nginx en Debian se puede parar e iniciar con systemctl:

$ sudo systemctl restart nginx

La configuración del servidor está en /etc/nginx/nginx.conf, pero normalmente allí sólo hay cosas de configuración general y no hace falta modificar este fichero para nada. En mis pruebas este fichero no lo he modificado ninguna vez.

Normalmente modificamos /etc/nginx/sites-enabled/default, que si os fijáis es un enlace simbólico a /etc/nginx/sites-available/default. Es lo mismo que se hace con Apache.

Por defecto sirve los ficheros en /var/www/html. En servidores web lo habitual es crear otros ficheros (uno por web) en lugar de cambiar default, porque esto permite servir múltiples dominios en un servidor web. Para pruebas locales usar default es lo más sencillo porque nos permite visitar localhost en lugar de tener que configurar un dominio.

En mi caso lo que hice es copiar “default” a mi repositorio git, y hacer un enlace simbólico a él. De este modo los cambios los tengo controlados por git.

Configurando un backend

Por defecto nginx sólo sirve estáticos. Para servir contenido dinámico hay básicamente tres formas:

  • Módulo proxy: Pasa todas las peticiones a otro servidor web. Por ejemplo si alzamos un servidor de desarrollo en el puerto 8080, le podemos decir que todas las peticiones que nos lleguen las reenvíe allí. FastCGI y uWSGI son mejores alternativas.
  • Módulo fastcgi: Pasa las peticiones a otro ejecutable de nuestra elección, usando el protocolo FastCGI. Ésta es la mejor opción para servir PHP. FastCGI puede comunicar con otros procesos a través de sockets TCP, por lo que incluso podemos tener Nginx en una máquina y PHP en otra.
  • Módulo uwsgi: Pasa las peticiones a otro ejecutable, usando el protocolo uWSGI. Esta es la mejor opción para servir Python. Al igual que FastCGI, se comunica con sockets por lo que Python puede ser ejecutado en otras máquinas.

uWSGI fue diseñado originalmente para Python, pero a día de hoy soporta muchos otros lenguajes como PHP o NodeJS. Además, de los tres es el más rápido.

Como ya imaginaréis, yo lo voy a instalar con uWSGI y olvidarme del resto. Si vais a usar cualquiera de los otros dos, la configuración es prácticamente idéntica, ya que los tres módulos tienen los mismos comandos y opciones, sólo hay que cambiar el prefijo uwsgi por proxy o fastcgi.

Primero agregaremos el upstream en la parte superior del fichero:

upstream backendpython {
 server unix:///tmp/backendpython.sock; # por fichero
 # server 127.0.0.1:8001; # por TCP
}

Esto crea una variable “backendpython” y le asocia un servidor. Se pueden poner varios a la vez y nginx distribuirá la carga. La conexión puede ser un socket unix (por fichero) o socket TCP. Los sockets unix sólo funcionan en Linux/MacOSX/BSD. Los TCP funcionan también en Windows. Los sockets unix son más rápidos y más seguros, pero no permiten conectar a otra máquina.

En mi caso lo he conectado al fichero /tmp/backendpython.sock. Este fichero lo tiene que crear la aplicación uWSGI. En mi caso es un programa bash muy sencillo:

#!/bin/bash
test -f config.sh && source config.sh
export SCRIPT_NAME=backend.py
uwsgi_python3 -p6 --uwsgi-socket /tmp/backendpython.sock \
 -C777 -i --manage-script-name \
 --mount /api=backendpython:app \
 --py-autoreload 1

Esto lanza 6 procesos (-p6), nos crea el fichero de socket unix y le da permiso a todos (-C777). Para local está bien, pero en servidores habría que restringir los permisos.

También se puede hacer por TCP, o incluso HTTP. Dadle un vistazo a la documentación de uwsgi_python3 para ver cómo hacerlo.

Ahora hay que decirle a Nginx dónde tiene que usar este nuevo upstream. En la sección “location /” sería:

 location / {
 include uwsgi_params;
 uwsgi_pass backendpython;
}

Con esto ya lo tendríamos configurado. Hay que tener en cuenta que con esta configuración dejamos de servir los ficheros en /var/www/html. Todas las peticiones pasan por el backend. Esto es lo que yo quiero en mi caso porque mi backend sólo responde JSON. Para servir el frontend en Angular 6 agrego esta porción:

location /angular {
 location ~* ^.+\.(css|js)$ {
  access_log off;
  expires max;
 }
 try_files $uri $uri/;
}

En mis pruebas locales no me hace falta porque Angular levanta un servidor en el puerto 4200. (podríamos usar proxy_pass para redirigir este tráfico y así tener el autoreloader funcionando en el puerto 80)

Además, si estáis usando PHP, lo normal es que queráis servir los ficheros *.php en lugar de enviarlo todo a uno solo. Esto tiene una configuración distinta. Aquí hay una guía: Instalar Nginx con Php-fpm

Os recomiendo leer Pitfalls and Common Mistakes – NGINX que explica muy bien qué errores se suelen cometer al configurar el servidor. Incluso como advierte, hay muchas guías que cometen errores bastante grandes. Es de lectura obligada aunque estéis siguiendo una guía paso a paso.

Con esto ya tendríamos nginx funcionando con nuestro backend, pero en este punto aún no hay caché. Lo configuraremos más adelante.

Comprobando la velocidad de nuestro backend

Con el framework Flask me hice un pequeño servidor web que se conecta a MySQL y convierte los datos a json.

Con http2 la cantidad de consultas no importa tanto y no es necesario empaquetar recursos. Así que mi diseño inicial de la API son muchas urls con poco contenido.

Conseguí unas 2650 peticiones por segundo. Como uWSGI está alzando seis procesos, usa toda la cpu disponible.

La velocidad es muy buena, pero las peticiones son de 1kbyte o menos. La pregunta recurrente es, ¿qué es mejor desde el punto de vista de la cpu usada en el servidor? ¿Pocas urls grandes o muchas pequeñas?

Hay que entender que lo que intentamos conocer aquí es cuan eficaz es nuestro servidor web, cuántos recursos usará y no estamos viendo qué tan rápida es la web desde el punto de vista del usuario. Esto es a propósito. El objetivo es no gastar dinero en los servidores y poder tener tantos usuarios como quiera. Y aprovecharnos de sus dispositivos moviendo allí la lógica de la app.

Suena un poco egoísta pero es lo que hay. Hay quien hace minar criptomonedas a sus usuarios, comparado con hacer que usen sus recursos para mover nuestra web, ¿no parece tan malo, verdad?

Volviendo al tema, ¿qué tamaño de petición es ideal para nuestro servidor? Pues lo suyo es probarlo!

Dependiendo que que backend sea, framework y servidor web el resultado será distinto, así que recomendaría que cada uno haga sus pruebas y saque sus conclusiones. Yo hice las mías y éstos son los resultados :

registros bytes trans/sec kB/sec MB/sec
1 285 3364.2 931.84 0.91
2 584 3151.65 1689.6 1.65
4 1131 2995.12 3307.52 3.23
8 2279 2653.26 5908.48 5.77
12 3471 2374.81 8048.64 7.86
16 4549 2171.1 9646.08 9.42
24 7003 1835.11 12554.24 12.26
32 9025 1600.84 14110.72 13.78
48 14048 1271.97 17448.96 17.04
64 18114 1039.71 18391.04 17.96
128 36978 603.89 21811.2 21.3
256 73938 346.12 24995.84 24.41
512 147917 178.72 25815.04 25.21
1024 293412 86.62 24821.76 24.24
2048 381540 69.36 25845.76 25.24

grafica

Como se puede ver la cantidad de kBytes/s crece constantemente hasta los 16Kb y después se estanca en los 25Mb/s. La cantidad de peticiones por segundo empieza estancada y luego empieza a bajar a buen ritmo a partir de los 16Kb. (La escala de la gráfica es logarítmica en ambos ejes para facilitar la visualización de los datos)

Parece bastante claro que el punto clave son los 16Kb, donde se maximiza la cantidad de mensajes y la tasa de transferencia a la vez.

Para medirlo yo uso Siege. Es bastante completo y la carga que genera se parece bastante a la carga real de usuarios. No es el más rápido (es de los más lentos), pero los demás montan escenarios bastante poco realistas o les faltan funciones. Un ejemplo de comando sería éste:

$ siege -f urls.txt -c 8 -ib -t 60s

Las opciones que uso son:

  • -f: (file) Ruta a un fichero con una lista de urls a solicitar. El fichero es de texto plano con una url cada línea.
  • -c 8: (connections) Número de conexiones simultáneas. Para pruebas en local normalmente con sobrepasar un poco los núcleos del ordenador es suficiente. Para pruebas contra un servidor remoto puede que haya que subir el número bastante.
  • -i: (internet) Realiza las llamadas en orden aleatorio. Por defecto sigue el orden del fichero.
  • -b: (benchmark) Elimina los tiempos de espera entre solicitudes. Ideal para realizar pruebas de rendimiento.
  • -t 60s: (time) Establece cuánto tiempo va a durar el test. Se puede parar antes con Control-C, pero para resultados consistentes entre llamadas lo mejor es establecer un tiempo definido.

Con esto ya tenemos todo lo básico configurado y hemos hecho algunas mediciones interesantes. En el próximo post veremos cómo configurar la caché en nginx y medir la diferencia de velocidad.

Os espero aquí para la siguiente entrega!

Cómo HTTP/2 acelera tu sitio web

English version – How HTTP/2 makes your site faster

La mayoría de gente no ha oído hablar de HTTP/2. Se publicó en 2015 y ha pasado desapercibido sin pena ni gloria. Han pasado tres años y el 99% de los sitios web siguen usando HTTP/1.1.

Esta nueva versión del protocolo HTTP trae muchísimas mejoras, casi todas ellas pensando en mejorar el rendimiento de las páginas web en el navegador. Configurar el servidor web para que use HTTP/2 es sencillo, tanto Apache como Nginx traen soporte de serie. Además los navegadores que no lo soporten pasarán automáticamente a HTTP/1.1 sin ningún problema.

El único obstáculo que hay para usar HTTP/2 es que funciona casi únicamente con SSL, es decir con HTTPS. Los distintos navegadores han decidido que no van a activar HTTP/2 a no ser que sea a través de HTTPS.

Para usar SSL hoy en día lo tenemos más fácil que nunca. Los certificados HTTPS normalmente hay que pagarlos anualmente, pero existe una iniciativa llamada letsencrypt que genera certificados HTTPS básicos de forma gratuita. Funciona en todos los navegadores. La principal limitación es que hay que renovarlos cada tres meses y hay que demostrar cada vez que tenemos control sobre nuestra web. Letsencrypt posee formas de automatizar esto si tenemos acceso al servidor.

Mejoras de rendimiento en HTTP/2

La principal diferencia con la anterior versión es que los navegadores solicitan los recursos sin ningún límite de paralelismo. Hasta ahora los navegadores pedían cuatro URLs a la vez como máximo y tenían que esperar a que alguno finalizara para empezar con la siguiente petición.

Esto se hace porque en HTTP/1.1 cada petición es una conexión TCP separada. La extensión keep-alive permite reusar la conexión cuando una petición termina, pero tiene que esperar a que termine para solicitar otro recurso. Para evitar denegaciones de servicio, se estableció que había que limitar cuantas conexiones en paralelo podían haber.

En TCP, las distintas conexiones luchan entre ellas para conseguir el ancho de banda. Las tarjetas de red, switches y routers distribuyen el ancho de banda entre las distintas conexiones. El problema es que si un programa lanza 2000 conexiones TCP, tendrá en total 2000 veces más prioridad que los programas que sólo lanzan una. Esto está mal visto y tiende a saturar las redes. Algunos recordaréis como eMule o BitTorrent saturaban las redes cuando alguien los ponía en marcha. Esto es porque los programas P2P lanzan cientos de conexiones a la vez y terminan generando una sobrecarga muy grande en la red, consumiendo recursos de un modo poco justo.

Para evitar el problema del límite de conexiones en los navegadores, los que hacemos las webs nos hemos visto obligados a empaquetar los recursos lo máximo posible, juntando distintas peticiones en una sola. Esto perjudica las otras ventajas de HTTP, porque las políticas de caché ahora afectan a todo el paquete, y cuando quieres cambiar una pequeña imagen o línea de CSS, se termina transfiriendo todo el bundle a todos los usuarios.

En HTTP/2 hay una única conexión TCP y ésta permite hacer diferentes peticiones en paralelo sin ningún límite. Los navegadores han aprovechado esto y cuando saben que están sobre HTTP/2 eliminan el límite de peticiones simultáneas, con lo que ahora obtienen más de 100 peticiones por segundo.

Ahora el empaquetar los recursos en un bundle deja de ser tan conveniente. Se puede volver al esquema anterior, aunque empaquetar sigue obteniendo algún beneficio marginal.

Si tu web ya está empaquetando Javascript, CSS e imágenes, HTTP/2 aún obtiene mejoras de rendimiento muy apreciables, porque aún así el navegador tiene que realizar conexiones simultáneas.

Otras mejoras de HTTP/2

Además de lo ya comentado, ahora http/2 es un protocolo binario y no de texto, que reduce mucho la CPU usada para procesar los mensajes. También elimina información redundante cuando hay varias peticiones seguidas por lo que se usa menos ancho de banda. La conexión TCP permanece abierta durante un período largo, por lo que los siguientes clics suelen tener tiempos de reacción muy rápidos.

También soporta ahora priorización de contenido. El navegador puede solicitar recursos con distinta prioridad, por ejemplo si sabe que una CSS está bloqueando la carga de una página, pero también tiene otras imágenes para cargar, puede emitir las peticiones de imágenes en prioridad baja, pero la CSS en prioridad alta. El servidor web seguirá las indicaciones y dará prioridad a enviar la CSS si es posible antes que las imágenes. Pero si la CSS pasa por PHP/Python y está tardando en calcularse, el servidor envía partes de las imágenes mientras espera.

Como último, han agregado compresión de cabeceras con un algoritmo llamado HPACK. Las cabeceras HTTP ocupan bastante en comparación con contenidos pequeños, y con esta mejora enviar códigos HTTP sin contenido (Como 304 Not Modified) se vuelve mucho más eficiente.

Finalmente está el nuevo “Server Push”. Esto permite desde el servidor enviar datos al cliente que no ha solicitado. Es muy útil para que cuando el cliente pida por primera vez una página enviarle enseguida los recursos CSS y JS que va a necesitar, antes de que el cliente tenga que pedirlos. Porque de otro modo, el HTML inicial tiene que llegar, el cliente lo tiene que procesar, y después pedir. Y a lo mejor un Javascript se inicia y pide más recursos. Con “Server Push”, le podemos enviar al navegador los recursos nosotros sin que los pida, y cuando el navegador se da cuenta que los necesita, ¡resulta que ya los tiene!. El resultado es que las cargas iniciales se pueden rebajar un segundo o dos.

Yo ya llevo años experimentando con HTTP/2 con grandes resultados. Lo único es el certificado SSL que se vuelve un poco engorroso de gestionar.

¿Qué os parece, lo estáis usando o pensáis usarlo?

Se espera la Nvidia GTX 1180 en Julio

Leo en Toms Hardware que hay bastantes rumores de que la nueva tarjeta de Nvidia salga a la venta en Julio. Sería la GTX 1180, aunque aún están por confirmar todos los detalles, incluido el número del modelo.

Voy a estar muy pendiente de las novedades, y sobretodo de la potencia de este nuevo modelo. La actual GTX 1080 fue toda una revolución y dejó a toda la competencia atrás. Dudo mucho que esta vez sea igual, ya que éstas tarjetas se siguen vendiendo muchísimo y me imagino que sólo será un poco más potente, lo suficiente como para que algunos gamers, mineros y entusiastas de la IA se planteen cambiarla.

Se cree que tendrá sobre 3500 núcleos CUDA y 8-16Gb de memoria. Consumirá aproximadamente 200W. Esto la haría un 50% más potente que la 1080 sobre el papel, aunque los demás parámetros no parecen cambiar demasiado, así que supongo que a la práctica sólo será un 30% más potente.

Me parece especialmente interesante, ya que podría facilitar mucho la vida a los entusiastas de la Inteligencia Artificial como yo. Tener una buena tarjeta a buen precio que no consuma demasiado es ideal, ya que los modelos IA tardan horas o días en computarse. Tener mayor potencia de cálculo hace que las iteraciones programando sean más cortas, un feedback mucho más rápido y por lo tanto aprendemos más rápido a programarlas.

El problema este último año han sido los mineros. La minería es una inversión a largo plazo y mucha gente se ha puesto a comprar tarjetas de éste estilo para minar criptomonedas. Y no son equipos con una o dos tarjetas. La gente se monta granjas en casa con 4 o 5 servidores que tienen unas 6 u 8 tarjetas cada una. Esto hizo que el precio de las tarjetas subiera, incluso las de segunda mano llegaban a ser más caras que las nuevas (ya que las nuevas no tenían ni stock).

Supongo que después del crash de Bitcoin la gente ha dejado de comprar más tarjetas y Nvidia ha pensado que ya es hora de sacar el nuevo modelo y seguir vendiendo. Las malas noticias para ellos es que los mineros no van a cambiar sus tarjetas, porque significaría invertir de nuevo en algo que acaba de caer en picado. Eso sí, habrá quien lo hará y como son tantos mineros con tantas tarjetas se puede esperar que el precio de segunda mano de las 1080 y 1070 baje en picado a principios del año que viene.

No creo que me compre una este año porque no estoy trabajando con la IA últimamente. Pero no lo descarto el año que viene, cuando los precios se normalicen, si es que vuelvo a interesarme por la inteligencia artificial.

También me parece muy interesante para los jugones, ya que ese empujoncito de la 1180 haría que jugar en resoluciones 4K fuese una realidad. Yo aún sigo con mi monitor 4:3 de 1600×1200, y he pensado en cambiarlo. Pero los 4K, que me parecen muy interesantes, son demasiado para mi GTX 1070. O eso dicen.

 

Diseñando una web ultra-rápida (II)

Esto es una continuación del post Diseñando una web ultra-rápida (I). Si no lo has leído aún, te lo recomiendo. En él me planteo la posibilidad de que el contenido dinámico sea diseñado de forma que pueda ser cacheado como estático y por lo tanto, servirlo a toda velocidad.

En esta entrega quiero analizarlo un poco más a fondo y plantear un diseño inicial.

Primer paso, el servidor web

Como nuestro diseño se va a basar en servir estáticos lo primero es buscar un servidor web que pueda cachear como un reverse proxy y que sirva una cantidad ingente de peticiones sin consumir apenas recursos.

Mi elección aquí es NGINX. Es uno de los servidores más rápidos y es software libre. La instalación es sencilla, pocas dependencias y muy estable.

Si alguien se pregunta, ¿y porqué no Apache?. Pues tuve muy malas experiencias con Apache más de diez años. Ha mejorado mucho, pero no quiero ni volverlo a ver. Apache se come recursos de la máquina, se colapsa con aluviones de visitas, etc. Si alguien quiere probarlo adelante, yo personalmente paso.

Un benchmark Apache vs NGINX: Web server performance comparison – DreamHost

En mi máquina NGINX puede servir más de 8000 peticiones por segundo y aún veo el 50% de CPU libre. Es difícil estimar para mí correctamente la velocidad que tiene porque la mayoría de herramientas de benchmark consumen más CPU que éste servidor web.

El diseño del back-end y la API

Si nuestra página funciona con Javascript y consume datos JSON, entonces podemos aplicar caché a nivel de cada consulta separada que realicemos. Luego, en el diseño de los datos que va a recibir ésta web vamos a separarlos en tres tipos que yo defino como “public”, “restricted” y “custom”:

  • public: (públicos) Los datos a los que cualquier usuario, registrado o anónimo, tiene acceso a ver. En un foro serían los mensajes en foros públicos (casi todos), en un e-commerce los artículos y el catálogo. Como son públicos, todos los usuarios van a ver este contenido igual.
  • restricted: (restringidos) Son los datos que, con las credenciales correctas, se tiene acceso a ver. Todo usuario con credencial ve el mismo dato, de la misma forma. Y los que no, obtienen un 403 Forbidden y no pueden verlo. Es decir, es igual que public + autorización.
  • custom: (personalizados) Esto sería el contenido completamente dinámico, que cada usuario ve distinto. Esto podría ser por ejemplo una API para preguntar nuestra cuenta de usuario.

La mayoría de webs deberían poder acoplar su modelo de datos a éste esquema. Es más, en la mayoría de casos el 90% de los datos transferidos serán públicos, un 9% restringidos y un 1% personalizados. (Dependiendo del tipo de web, en Facebook el 90% es restringido). El problema es que todas las peticiones hoy en día pasan por el backend principalmente para la autorización, aunque todo el mundo pueda acceder a un recurso en particular.

Incluso cosas como un servicio de mensajes privados o email se puede plantear como restringido en lugar de personalizado, básicamente por dos motivos: Al menos dos usuarios (emisor y receptor) ven el mismo mensaje, y segundo, el contenido no cambia en cada petición ni según que usuario lo pida.

La lógica de esto es que la caché no puede ser utilizada por igual para todos los tipos de datos. Si todos los datos fuesen cacheados, entonces los usuarios anónimos podrían ver contenido privado accidentalmente o si saben la url. Además para las peticiones personalizadas se verían datos antiguos o de otros usuarios.

Para que esto funcione, las urls de nuestra aplicación tienen que apuntar a un único contenido y hay que eliminar todo el dinamismo para convertirlas en algo estático. Esto significa que no se pueden pasar parámetros para personalizar la consulta (filtros, orden, etc). Siempre que se consulte a una URL con la autorización adecuada debería devolver el mismo resultado, independientemente de quién lo consulte. Menos las personalizadas (custom) que siempre hay alguna, pero deberían ser la excepción y no la norma.

El primer problema es cómo servir la mayoría de URLs que no necesitan autorización, sin pasar por el backend para que verifique que efectivamente no lo necesitan.

Por el momento veo tres soluciones: (Se aceptan sugerencias!!)

1) Separar todas las url

Las APIs públicas estarían en /api/public. El resto en /api/restricted y /api/custom. En nginx definiríamos el comportamiento de estas tres.

Ventajas:

  • Clara separación de los contextos de autenticación y caché.
  • La eficiencia es máxima.

Desventajas:

  • Normalmente en la base de datos estos conceptos también están mezclados.
  • Publicar correctamente los contenidos puede ser complejo a veces y un desliz te puede publicar datos que no querrías. Si esto se tiene en cuenta al diseñar la base de datos, puede que sea ideal.
  • Los restringidos, ¿cómo se tratan?

2) Usar JWT (JSON Web Tokens)

Algunos servidores pueden usar JWT firmados digitalmente, verificarlos y leer su contenido. Esto lo podemos usar para que el servidor web pueda determinar si se puede acceder o no.

Ventajas:

  • Flexible. Se puede hilar bastante más fino.

Desventajas:

  • La extensión JWT para nginx es de pago.
  • Gran parte de los permisos de tu aplicación tienen que estar programados en NGINX.

3) Usar un servicio http sólo para la autenticación

NGINX permite enviar a un servicio http (que puede ser Python u otra cosa) las peticiones para autenticación. Este servicio sólo tiene que devolver respuestas HTTP 200 o HTTP 403. Sin ningún dato.

Ventajas:

  • No necesitas separar ninguna URL.
  • Toda la lógica de autorización reside en tu aplicación.
  • El contenido puede seguir siendo cacheado.

Desventajas:

  • Más lento, pues todas las peticiones pasan por tu backend. Aunque el proceso es mínimo porque no servimos contenido.

A tener en cuenta:

  • Se puede usar otro lenguaje de programación que realice esta tarea más rápido.
  • Se puede separar parte del contenido público en /api/public para más rapidez.

Conclusiones

La primera opción planteada era la más evidente, pero la última usando un servicio aparte se vuelve cada vez más atractiva para mí. Además, que se pueden mezclar ambas libremente.

Al final he hecho algunas pruebas con Python+Flask y NodeJS.

Con Flask he conseguido 2400 peticiones por segundo para respuestas que apenas contenían el HTTP 200, usando los 4 núcleos físicos de mi i7. (Comparado con las 8000 de Nginx, no está mal. Pero Nginx no saturaba mi CPU y Python saturaba todos los núcleos a la vez)

Con NodeJS y un servidor muy simple he conseguido 3500 peticiones por segundo en condiciones similares. Pero usando sólo un único núcleo físico. He probado soluciones de multiproceso, pero al final me sale el mismo rendimiento pero usando más CPU. Parece que la comunicación entre procesos toma más tiempo de CPU del que gano. No obstante, es planteable con esa velocidad el usar sólo una CPU.

Otra opción para NodeJS sería abrir cuatro servidores HTTP en cuatro puertos distintos y enviar distintas URL (por patrón) a los distintos puertos. Un balanceo de carga bastante malo, pero funcionaría. Me he planteado también un balanceador de carga delante, pero a no ser que tengamos varios equipos para la web, me parece que agregar otra pieza de software no va a valer la pena. ¿Alguien recomienda un balanceador para que lo pruebe?

Y finalmente, cabe plantear que las respuestas de éstos backends también se pueden cachear por nginx. Tiene instrucciones para hacer caché en la autenticación. Habría que añadir las cookies o alguna cabecera para no mezclar usuarios. El principal problema es que muchas URL van a requerir la misma autenticación y cachear todas las URL multiplicado por todos los usuarios huele a ineficaz. Habrá que ver cómo hacer que Nginx entienda que ciertas URLs son iguales, y que en cuanto al usuario sólo tiene que mirar cierta credencial.

De momento lo dejo aquí, creo que va cogiendo forma y se vuelve más interesante. Espero publicar una continuación pronto!

Diseñando una web ultra-rápida (I)

¿Qué sucedería si pudiésemos hacer una web que sirviese 10.000 peticiones por segundo en un buen servidor? ¿Atraería más usuarios? ¿vendería más?

Posiblemente no haya casi ninguna diferencia, pues casi todas las webs sirven menos de 5 peticiones por segundo. Exceptuando las grandes, todas las demás no necesitan de grandes servidores ni complicados sistemas, hasta que se hacen muy grandes y necesitan más servidor. A partir de ahí, pagan más al mes por tener una mejor máquina y siguen funcionando.

Y ahí es donde reside la diferencia. Si nuestra web está bien hecha y es tan rápida, lo que pasaría es que dejaríamos de contratar un servidor de 100€ al mes a contratar uno virtual de 10€ al mes. A lo mejor sólo sirve 50 peticiones por segundo, pero el servidor es tremendamente barato. Y cuando hace falta más rendimiento, se sube un poco y enseguida el problema se soluciona de nuevo. Y hay servidores incluso más baratos, por lo que el coste de mantenimiento puede ser ridículo. Llega un punto que ya no sabes si estás pagando por dominios o servidores.

He estado estos días analizando cual sería la forma de poder montar una web que consuma muy pocos recursos y que pueda gestionar una gran cantidad de usuarios.

La receta es bien conocida y no tiene nada de nuevo. Como todo en programación, para que vaya más rápido hay que conseguir que trabaje menos. Cuantas más funcionalidades y más dinamismo, más trabajo se le da al servidor y por lo tanto, más lenta.

Por lo tanto la más rápida es un index.html estático que diga “Hola mundo” y ya está. Ni imágenes ni tonterías. ¿Cuántos usuarios puede servir? ¡Un porrón!.

mic-drop-2boom

Que sí, que lo sé.

Es obvio que si quieres vender o hacer cualquier cosa con una web hoy en día, publicar un html estático no es sólo ridículo, es que es obvio que no sirve para nada.

Pero no es tan obvio. El contenido estático es el que más rápido se puede servir desde una web. Si es pequeño mejor.

Hoy en día es una locura pensar que hayan usuarios que accedan a la web sin tener Javascript. Y el Javascript entre los distintos navegadores es bastante bueno; desde luego nada que ver con la época del Internet Explorer 6.

Piénsalo bien, por muy dinámica que sea la página, por mucho que cada usuario vea algo radicalmente distinto, ¿Cuántas veces sirves el mismo trozo de texto a todo el mundo? ¿Una o dos veces? Yo diría que cientos o miles. Incluso Facebook, donde cada usuario ve algo completamente distinto, el mismo post se ve cientos de veces y se envía incluso varias veces al mismo equipo en el transcurso de los días. Y la mayoría de las webs no son como Facebook. ¿Es ese post contenido estático o dinámico? Si cuando alguien lo escribe, lo guardo en disco y lo sirvo desde allí, ¿es dinámico o estático?

Por supuesto lo de grabar en disco cada mensaje y leerlo luego es una tontería bastante grande, sólo es un ejemplo. Si alguien se lo pregunta, es bastante más lento. Las bases de datos están mucho mejor preparadas que los sistemas de ficheros para ésta tarea y la realizan más rápido.

Pero si hablamos de webs SPA (Single Page Application) la cosa ya cambia. Si el contenido está en caché y el navegador consulta directamente éste, la carga para el servidor web es ínfima (con la excepción de que la cantidad de ficheros requerida es tan alta, que seguramente tampoco compense).

Esto es un poco la cruzada que llevo detrás bastante tiempo, poco a poco probando distintas cosas y viendo el resultado y sus posibilidades. Sólo es una prueba de concepto, nada más; pero es interesante y vale la pena compartirlo.

La idea es como sigue: En lugar de servir cada respuesta dinámicamente, cambiemos el diseño radicalmente para que se pueda servir estáticamente, desde una caché en el servidor. Esto significa que para casi todas las URL de la aplicación, cuando se realiza una petición GET, tiene que devolver siempre el mismo resultado independientemente de usuario o permisos. También significa que no se pueden pasar parámetros para personalizar consultas. Un recurso tiene que aparecer siempre en la misma URL con los mismos parámetros y no debe haber más que una forma (o dos) de acceder.

Al final se trata de mapear la base de datos que tienes a APIs REST. Habrá una cantidad finita de url’s que descargar, y si las descargas todas, acabas de exportar toda la web a disco.

Una de las grandes ventajas es que si el servicio de Python (o PHP, etc) se cae, la caché puede seguir teniendo efecto y los usuarios pueden seguir navegando. Excepto cuando intenten enviar cambios (POST/PUT/etc), la web parecerá plenamente funcional.

Hay un montón de problemas de diseño en esta idea, pero poco a poco parece que se pueden solventar. De momento he conseguido 8000 peticiones por segundo para contenido que puedo cachear, 3000 para el que requiere autenticación y 1000 para el que es completamente dinámico en un i7 920 @2.66Ghz. Iré actualizando en nuevas entradas cómo hacerlo, qué problemas he ido encontrando y qué soluciones me han parecido las más apropiadas para cada caso.

Hasta el próximo post!

Angular 7 podría ser el framework definitivo

Angular 6 acaba de ser publicado, después de ver las novedades que trae y algunas conferencias recientes sobre las nuevas tecnologías en que trabajan me he quedado bastante sorprendido.

Para los que no conozcáis Angular (a partir de su versión 2, ya que la versión 1 se llama AngularJS y es prácticamente un producto distinto), es un framework para realizar el frontend de páginas web de una única página (SPA). Se programa con Typescript, que es un superset de Javascript y hace la vida mucho más fácil.

Con Angular básicamente necesitamos un backend (en cualquier lenguaje) que tenga una API REST que devuelva JSON (aunque no es obligatorio, otros esquemas también funcionan). De este modo el backend es bastante ligero y es una mínima capa entre el frontend y la base de datos.

Ventajas de usar Angular 5

Hagamos un breve repaso de qué hace Angular en su versión 5 por nosotros. Primero, es un framework completo. No necesitamos apenas buscar librerías externas, dentro de Angular hay prácticamente todos los componentes necesarios para realizar una página web completa con enrutado de Url’s, sistema de plantillas y reemplaza en pantalla las variables conforme cambian con una detección automática. Además como las aplicaciones son SPA (de una única página) la interacción con el usuario es muy rápida. Es ideal para aplicaciones de móvil (con Cordova, por ejemplo), o en comunidades, intranets u otras páginas donde el usuario pasa tiempo navegando dentro de la aplicación.

Angular compila todo el proyecto y genera unos javacripts optimizados usando webpack que el navegador carga rápidamente. Los tiempos de compilado de Angular son extremadamente rápidos en los modos de desarrollo. Comparado con una página realizada PHP, Angular recarga la aplicación en el navegador automáticamente en menos del tiempo que se tarda con PHP en darle F5 al navegador. Además, como es javascript podemos conectar nuestra versión local con el backend del servidor pudiendo ver en real cómo queda la página sin tener que subir cada vez al servidor. Por supuesto lo recomendable es usar un servidor de desarrollo para estar tranquilos de que nuestra aplicación local no va a modificar datos por error de la web en producción.

Para compilar para producción, Angular trae una serie de opciones recomendables como el AOT (Ahead of time compiling) que hace que el navegador de los usuarios no tenga que sufrir tanto en CPU para arrancar la aplicación. Tarda algo más, posiblemente un minuto o dos dependiendo de la aplicación.

También hace uso de la librería RxJS para el manejo de todo el flujo de datos asíncrono. Aquí la curva de aprendizaje es algo más pronunciada, pero al final es algo que hay que aprender con cualquier aplicación seria en Javascript aunque no usemos Angular o no usemos RxJS. La programación asíncrona en Javascript es necesaria y cuesta al principio. RxJS facilita mucho los flujos de datos complejos.

En resumen, las cosas tienen una forma recomendada de hacerse, el set de librerías está estandarizado e integrado. Cuando buscas el resto de gente está usando esquemas muy similares por lo que las soluciones en StackOverflow casi siempre aplican a tu proyecto sin tener que inventar apenas nada. Está muy bien documentado.

Ventajas de una aplicación SPA

Quiero hacer especial énfasis en cómo una aplicación SPA es muchísimo mejor que una aplicación tradicional con páginas dinámicas servidas desde el backend.

He trabajado mucho con PHP. Las páginas dinámicas hacen que el servidor tenga que computar cada clic del usuario, aplicar las plantillas, la lógica de todo el programa y en definitiva renderizar todo cada vez. Cuando tienes muchos usuarios registrados el cachear no es fácil, porque al final cada usuario tiene una versión personalizada de la página. Y cuando se cachea, se realiza desde el backend con la lentitud habitual del lenguaje que hemos elegido, sea PHP o Python. En el caso de PHP además tiene que cargar librerías e inicializar el intérprete cada clic.

Las páginas SPA cargan unos recursos estáticos iniciales que son servidos eficientemente por Apache o Nginx. Luego éstas sólo piden una serie de datos JSON al servidor. Renderizar JSON es muy eficiente, la mayoría de lenguajes tienen librerías escritas en C por lo que la velocidad no se ve casi afectada. Además, como una SPA carga cada recurso por separado, hay muchas posibilidades de que hayan recursos públicos que puedan ser cacheados tanto en el navegador como en el servidor. Los reverse-proxy como Nginx o Varnish empiezan a cobrar sentido si nuestra aplicación separa correctamente lo público de lo privado. Nuestros usuarios pueden estar autenticados y recibir el contenido público desde la caché de Nginx que es tremendamente eficiente.

Después tenemos que considerar que el Backend pasa a realizar sólo las operaciones de negocio: autenticación, validación, lectura y escritura. La cantidad de lógica necesaria es ínfima y muchas veces se puede incluso abstraer con librerías haciendo que la programación del backend sea trivial. Y por tanto se ejecuta más rápido.

En resumen, lo que estamos haciendo es derivar la carga de CPU de nuestros servidores a los dispositivos de nuestros usuarios. Es como un P2P (Bittorrent) pero de CPU!. Esto significa que con un servidor mucho más pequeño vamos a poder servir a muchísimos usuarios.

Además, dado que el backend hace tan poco, casi todo el código puede ser stateless, y el poco estado que existe se puede mover a una base de datos sea en memoria (Redis) o de disco (MySQL/PostgreSQL). Con esto, cuando necesitamos más potencia lo único que hace falta es agregar más servidores y de una forma sencilla empezaremos a repartir la carga.

Desde el punto de vista del usuario la página se nota mucho más rápida, porque Javascript es uno de los lenguajes interpretados más rápidos de la actualidad (100 veces más rápido que Python), y los frameworks o librerías que usemos van a computar sólo las diferencias, realizando mucho menos trabajo que el servidor, ya que el servidor tendría que renderizarlo todo cada vez.

Desventajas de una aplicación SPA

Siendo realistas, hay pegas con el diseño de una SPA y como Angular sólo puede crear este tipo de aplicaciones, casi todos los problemas con Angular vienen derivados de las aplicaciones de una única página. Pero como explicaré después, prácticamente todos los problemas están ya solucionados o a punto de estar solucionados.

El primer problema y más obvio es el SEO. Los buscadores sólo ven en una SPA un HTML estático sin contenido, y por lo general son incapaces de indexar las diferentes páginas. En el caso de Google, están haciendo un gran trabajo para poder interactuar con la página desde el crawler para indexar gran parte del contenido. Hay algunos consejos por internet de qué cosas se pueden hacer en una SPA y qué no, para que Google las indexe. La solución es servir la página con enlaces cacheada para que cualquier buscador las pueda navegar. Pero esto supone renderizarlas todas desde el backend, lo cual implica o reprogramar los templates con dos lenguajes diferentes, o correr nodeJS.

Otro problema, que no tiene solución, es que una SPA no se puede usar sin Javascript. Un navegador muy antiguo no va a funcionar y los navegadores de consola van a ver lo mismo que el crawler, una página HTML sin contenido.

Lo siguiente que quiero comentar son los tiempos de carga inicial. Dado que hay un paquete inicial que descargar, si la mayoría de tus usuarios son anónimos y visitan tu página por primera vez, van a tener que descargar un par de megabytes y, lo peor, arrancar el sistema javascript que hay dentro. Como son sistemas complejos, el navegador tarda un poco en reaccionar, que puede ser desesperante para los usuarios que llegan a través de Google. Podríamos estar hablando de hasta unos 6 segundos hasta que la página es plenamente funcional. Unos 3 segundos hasta que se ve algo.

Un clic, en lugar de obtener una respuesta única y grande con todos los datos, tiende a producir múltiples llamadas para los diversos recursos necesarios. No tiene porqué ser necesariamente malo, pero en HTTP/1.1 los navegadores realizarán unas 4 conexiones a la vez como máximo (algunos hacen 8, depende del navegador). Esto lleva a que algunas veces el tiempo de reacción sea superior al que queremos.

En conclusión, con la situación actual las SPA son útiles para realizar apps móviles, o webs donde tiene sentido pasar tiempo dentro de ellas, o no tienen casi contenido público disponible, como Facebook o las aplicaciones que ofrece Google.

Novedades que hacen de Angular un framework idóneo para todo

En el último año he leído sobre muchas tecnologías en Angular que están llegando y estandarizándose que hacen que lo anterior deje de ser un problema.

Caché de páginas

En cuanto al problema del SEO y las visitas desde los buscadores, Angular ya tiene desde hace tiempo una solución para renderizar desde servidor. La gran noticia es que, a comparación de hace un año cuando lo revisé, ahora parece tener mucho soporte y es una solución bastante usada. Se llama “Angular Universal” y se encarga de renderizar las páginas bajo demanda con un servidor NodeJS. No lo he probado aún (pero lo tengo previsto).

La idea es que cuando un usuario llega a la web, nodeJS le sirve una versión ya lista, por lo que instantáneamente se ve todo el contenido y se puede empezar a leer. En segundo plano empieza la carga de Angular, y cuando está listo la web es usable. En los segundos que el usuario pasa leyendo la primera página, el servicio está listo y ha reemplazado la página sin que el usuario se dé cuenta de ello. Aún el problema es que parece que la interfaz y el scroll se quedan bloqueados hasta que completa.

Pero claro, no vamos a tener un backend corriendo para cada visita externa que llegue. A mí al menos no me parece demasiado correcto. Hay soluciones para compilar todo y servirlo de forma estática. Pero para eso hay que saber todas las URL.

La solución que más me atrae a mí es usar Nginx en reverse proxy hacia NodeJS. Cuando subimos una nueva versión, eliminamos la caché de Nginx para el proyecto (un simple “rm -rf” de una carpeta), y con un crawler pasamos a solicitar todas las URL que nos interesa pre-cachear. La mejora en este sistema es que si un usuario accede a cualquier URL que no habíamos pre-cacheado, nodeJS la servirá compilada y Nginx la cacheará hasta el siguiente deploy. Si revisamos los logs de Nginx podemos incluso coleccionar las URL’s que no teníamos y sumarlas a nuestra lista cuando querramos. Si un usuario accede a una que sí estaba en caché de Nginx, la petición nunca llega a NodeJS y se sirve de caché. (Y la caché de Nginx es tremendamente rápida, ¿lo he dicho ya?)

Reducir el tamaño de la primera petición

Como ya comenté, webpack crea los primeros recursos. Pueden llegar a ser algunos megabytes. Con conexiones 3G esto es un problema. Con cada versión de Angular este problema cada vez está más solucionado.

Angular y webpack llevan una tecnología llamada “tree shaking” que lo que hace es inspeccionar qué partes del código y de nuestras librerías realmente usamos, eliminando el código innecesario. Esto llega incluso al nivel de función, sólo cargando las funciones que requerimos. Cuando le sumamos el minificado y la compresión, los tamaños bajan mucho.

En el caso de Nginx es posible incluso comprimir de antemano, y nginx enviará el fichero GZ en lugar de comprimirlo cada vez. Dos ventajas, la primera el ahorro de CPU al no tener que comprimir cada vez. La segunda, que nuestra compresión Gzip puede ser con el programa y opciones que queramos (siempre que sea formato gzip), por lo que podemos ejecutar la compresión más agresiva posible.

Se espera una mejora muchísimo más agresiva en Angular 7 (si es que llegan a tiempo). Se va a reemplazar el motor de renderizado por Ivy, que realiza un tree-shaking muchísimo más mejorado. Llega a reducir el Hello-world de angular por debajo de los 10Kb! Aún falta por ver en aplicaciones reales qué tamaño será, pero tengo esperanzas de que pueda quedar por debajo de los 120kB, dejando una mejora notable.

Mejorar el tiempo de respuesta inicial

Aún con todo, el cliente tiene que esperar unos 4-6 segundos hasta poder interactuar. Lo cual es un mal resultado. Debería estar en el orden de los 1 o 2 segundos.

En una conferencia reciente ví el uso de Angular Elements (componentes aislados basados en Angular) para realizar una carga parcial. Esto es especialmente interesante, los resultados parecen indicar que en menos de 3 segundos la aplicación ya responde a algunos eventos de usuario que definimos, pudiendo hacer scroll y clicando algún botón que hemos preparado previamente.

Aún le queda en este aspecto, se puede realizar con el código actual, pero podría ser que en las próximas versiones esta técnica se estandarice, haciendo que sea sencillo de implementar.

Permitir cientos de peticiones paralelas con HTTP2

Respecto al problema de tener que servir tanta petición con HTTP en paralelo la solución existe y no tiene que ver nada con Angular. HTTP2 permite a la práctica realizar cientos de peticiones a la vez contra la misma web. Y no es teórico, yo mismo he realizado diversas pruebas con Angular y llego a ver un paralelismo de más de 500 peticiones para un solo clic, a través de internet.

Para HTTP2 hace falta usar HTTPS, y para ésto es tan sencillo como utilizar letsencrypt. Nginx soporta HTTP2 desde hace mucho y es sencillo de configurar.

Esto permite que desde el backend las peticiones sean más genéricas, menos bundles y menos consultas ad-hoc, lo que traduce en que todo el mundo consulta lo mismo, y por tanto se puede cachear en el mismo sitio. Mayor reuso de la caché y menos recursos a utilizar.

Soporte de aplicaciones progresivas

Angular 6 además estrena soporte para aplicaciones progresivas. Esto viene a decir que tenemos un código Javascript ejecutándose en segundo plano haciendo de proxy. Y podemos tenerlo como un reverse-proxy, como caché offline, o como enrutado de recursos. Incluso se pueden precargar recursos antes de usarlos, generarlos programáticamente, o implementar stale-while-revalidate.

Todo esto hace que la aplicación parezca que tenga todos los datos siempre locales y resiste los cortes de conexión muy bien. Esto es ideal para realizar aplicaciones de móvil o páginas web pensadas para móvil. No obstante en ordenadores de sobremesa también se va a notar la mejoría, pues vamos a poder trabajar como si fuese una aplicación de escritorio (o incluso realizar apps de escritorio)

Conclusión

Angular 6 trae tantas novedades que parece que los problemas originales casi son inexistentes. Los restantes, creo sinceramente que en las próximas versiones serán solucionados, haciendo que Angular sea el framework perfecto para casi todos los casos.

Una de las cosas que me preocupaba era la política de actualizaciones de Angular, ya que publican cada 6 meses una versión mayor nueva y el soporte es sólo seis meses. Actualizar de versión en cualquier librería siempre ha sido un problema.

Esto también deja de ser un problema, ya que ahora han anunciado que todas las versiones de Angular van a tener un soporte LTS de un año adicional, y además han publicado en la versión 6 toda una artillería de comandos para actualizar los proyectos automáticamente a la última versión, cambiando código si es necesario.

Además, nos comentan en la charla que muchísimas apps web de Google funcionan con Angular, y que antes de publicar versiones nuevas éstas se testean con toda la suite de apps de google. No va a haber ningún cambio que rompa nada ya que Google no puede permitirse modificar el código de 300 apps si no es de forma programática. Y cabe recordar que Angular es un proyecto hospedado por Google.

Así que, larga vida a Angular!