Compresión GZIP y Redis

En mi antiguo trabajo hacíamos muchas peticiones a proveedores externos. De media eran unas 2K peticiones por segundo. Unos 170M peticiones al día. Cada una de éstas peticiones tenía su correspondiente respuesta, que acostumbraba a ser un JSON, pero podía ser un XML. El peso dependía mucho de la respuesta, pero variaba entre pocos KB hasta casi 1MB.

Guardábamos la respuesta en Redis, durante un breve periodo de tiempo (segundos). Al principio, guardábamos los datos «tal cual» y, obviamente, era un error.

GZIP

Comprimir texto es asquerosamente rápido. Hay muchos algoritmos de compresión, pero normalmente conseguiremos un 80% de compresión con respecto al tamaño original, y podemos comprimir entre 15 hasta 60MB/segundo.

En nuestro caso, no sólo era el tamaño en RAM que ocupaba éstos datos en el cluster de Redis, sino la transferencia de datos para guardar las claves en Redis, y leerlas seguidamente. Simplemente guardando información comprimida reducíamos dicho tráfico al 10-20% del original.

Demostración

En este caso usaremos Python para hacer la pequeña prueba:

import redis
import gzip

client = redis.Redis()

payload = "abcdef" * 100_000  # 600K string

client.set("no-compressed", payload)
print("non-compressed length:", client.strlen("no-compressed"))


payload_compressed = gzip.compress(bytes(payload, 'utf-8'))
client.set("compressed", payload_compressed)
print("compressed length:", client.strlen("compressed"))

payload_compressed_retrieved = client.get("compressed")
print("Compressed payloads are equal:", payload_compressed == payload_compressed_retrieved)
print("Uncompressed payload length:", len(gzip.decompress(payload_compressed_retrieved)))

si ejecutamos el script veremos el siguiente resultado:

non-compressed length: 600000
compressed length: 913
Compressed payloads are equal: True
Uncompressed payload length: 600000

En Redis pasamos a tener una clave de 600K a 0.9K. Es una mejora de 600x. Obviamente esto no es un caso real, puesto que el payload original lo repetimos 100.000 veces y justamente GZIP es especialmente eficiente en comprimir éstos tipos de datos.

En un caso bueno donde comprimas texto, quedarte con un 10% del original ya está muy bien, y le sacaremos gran provecho.

Redis

A Redis realmente le da igual lo que almacenemos como string.

Strings are the most basic kind of Redis value. Redis Strings are binary safe, this means that a Redis string can contain any kind of data, for instance a JPEG image or a serialized Ruby object.

https://redis.io/topics/data-types

Simplemente estamos usando el hecho de que podamos tipos de datos binarios, y Redis nos va a devolver tal cual lo que le hayamos guardado originalmente esté o no comprimido.

Redis Benchmark

¿Cuán rápido puede Reddit Correr?

Redis es muy rápido, ¿pero cuánto? Realmente si no medimos la capacidad de nuestro nodo o cluster no podemos saber en qué cifras nos movemos y por tanto saber cuánto aguantará nuestra aplicación.

Pista: seguramente Redis no sea tu cuello de botella.

Velocidad de los comandos

En mi caso, desde un MacBook Pro (Retina, 15-inch, Mid 2015) con un 2,2 GHz Quad-Core Intel Core i7, y usando redis en Docker (redis:5) los resultados son los siguientes:

$ docker exec -it myredis redis-benchmark -q
PING_INLINE: 31847.13 requests per second
PING_BULK: 33411.29 requests per second
SET: 31377.47 requests per second
GET: 31816.74 requests per second
INCR: 31017.37 requests per second
LPUSH: 32520.32 requests per second
RPUSH: 30731.41 requests per second
LPOP: 32362.46 requests per second
RPOP: 33760.97 requests per second
SADD: 32499.19 requests per second
HSET: 32905.56 requests per second
SPOP: 32123.36 requests per second
LPUSH (needed to benchmark LRANGE): 31515.91 requests per second
LRANGE_100 (first 100 elements): 20942.41 requests per second
LRANGE_300 (first 300 elements): 12217.47 requests per second
LRANGE_500 (first 450 elements): 9434.85 requests per second
LRANGE_600 (first 600 elements): 7601.09 requests per second
MSET (10 keys): 29559.56 requests per second

En un servidor de DigitalOcean (Droplet 1GB) los resultados son:

[email protected]:~# redis-benchmark -q
PING_INLINE: 36900.37 requests per second
PING_BULK: 38358.27 requests per second
SET: 38580.25 requests per second
GET: 41050.90 requests per second
INCR: 37750.09 requests per second
LPUSH: 39401.10 requests per second
RPUSH: 38491.14 requests per second
LPOP: 37397.16 requests per second
RPOP: 36483.04 requests per second
SADD: 38639.88 requests per second
HSET: 38417.21 requests per second
SPOP: 39370.08 requests per second
LPUSH (needed to benchmark LRANGE): 40371.42 requests per second
LRANGE_100 (first 100 elements): 22416.50 requests per second
LRANGE_300 (first 300 elements): 9600.61 requests per second
LRANGE_500 (first 450 elements): 7243.23 requests per second
LRANGE_600 (first 600 elements): 5317.17 requests per second
MSET (10 keys): 35198.87 requests per second

A mi me parecen impresionantes.

Detalles por comando

Si queremos resultados más detallados siempre podemos ir a por un caso concreto, como por ejemplo con el comando set:

[email protected]:~# redis-benchmark -t set
====== SET ======
  100000 requests completed in 2.73 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

71.05% <= 1 milliseconds
99.75% <= 2 milliseconds
99.95% <= 3 milliseconds
100.00% <= 3 milliseconds
36643.46 requests per second

Mejorar lectura en Redis mediante Pipelining

Si nuestra web tiene que ejecutar 10 comandos para cargar la home, y podemos leer a 30K req/segundo, parecería que sólo podemos servir la home a 3K usuario por segundo.

Pero en los benchmarks, realmente estamos lanzando un comando esperando la respuesta para lanzar el siguiente. Redis soporta «piepelining» que básicamente es lanzar varios comandos a la vez y esperar la respuesta de todos ellos. Esto acelera dramáticamente la velocidad. En éste caso vamos a usar comandos de 10 en 10 (suponiendo que podemos hacer todas las peticiones a Redis para cargar la home en una sola petición) y vemos los siguientes resultados:

[email protected]:~# redis-benchmark -n 1000000 -t get -P 10 -q
GET: 289519.41 requests per second

289K req/segundo. Dan para bastante juego. Serían 28K usuarios cagando la home por segundo.

Traefik en Docker para múltiples entornos de devel

Traefik es un proxy HTTP, desarrollado en Go, OpenSource y que se lleva realmente bien con Docker.

La problematica es la siguiente: Si tienes diferents aplicaciones web en las que quieres desarrollar en local, lo normal es crear un Docker-Compose en cada una de ellas, tener un servidor HTTP como nginx o Apache corriendo, y, claro, lo normal es tenerlos escuchando en el puerto HTTP/HTTPS. Puerto 80 o 443, respectivamente.

Sigue leyendo Traefik en Docker para múltiples entornos de devel

Dockerfile y las capas AUFS o Overlay2

Cuando hacemos una build de una nueva imagen de Docker nos basamos en un Dockerfile, fichero que define exactamente los pasos a seguir para conseguir nuestra nueva imagen.

Una imagen de Docker está basada en diferentes «capas«. Digamos que con Docker siempre nos basamos en imagenes ya hechas, y descargadas de DockerHub. Éstas pueden ser imágenes de sistemas operativos como Debian o Ubuntu.

En este artículo explicaré como funciona el sistema de capas, que al principio es un poco confuso.

Sigue leyendo Dockerfile y las capas AUFS o Overlay2

Docker Vs Vagrant + VirtualBox para entornos de desarrollo

Cuando tenemos un entorno de desarrollo mínimamente complejo, necesitamos un sistema fácil, simple y fiable para gestionarlo.

Muchos habréis empezado a usar stacks LAMPP, XAMPP o el equivalente en otros lenguajes como Python o Ruby. Aún así, estos entornos se acaban quedando cortos a nivel de configuración y portabilidad.

Cuando el equipo crece, o si te cambias el PC tienes que reinstalar todo y configurar de nuevo. Suele llevar mucho tiempo.

Si vas a trabajar en diferentes proyectos que necesitan diferentes versiones de un lenguaje (Python 2.x Vs Python 3.x, o PHP5.X Vs PHP7.x), es un poco un infierno.

La solución siempre es la misma: virtualización.

Sigue leyendo Docker Vs Vagrant + VirtualBox para entornos de desarrollo