Redis, memcached, Rack, Nginx… y lua

Quiero montar un sistema de polling (como AnyCable o Action Cable pero sin websockets. O bien polls de alta frecuencia o bien long polling) y si bien parece logico que los mensajes se sirvan desde alguna arquitectura basada en RAM, no esta claro que haya muchas formas de acceder rapidamente a un mensaje que este alojado en Redis.

En primera aproximacion, si todo estuviera corriendo en un solo nucleo, la velocidad del codigo nos la daria el numero de tareas por segundo. Si tardamos un milisegundo, cada nucleo podra servir mil solicitudes por segundo, cada una tendra que esperar del orden de un segundo a que le toque turno, modulo numero de hilos, multiplexado y demas.  Asi que cuando estamos en el orden del milisegundo, cualquier detallito que te aumente el tiempo de ejecucion te quita capacidad de servicio. Eso descarta servir desde el framework; Rails en un solo core no va a poder hacer mas de 80 o 100 queries por segundo, porque tiene demasiado que ejecutar.

Si queremos mantenernos dentro de ruby, lo mas rapido es incorporar una microapp de Rack en paralelo con el rails, algo asi como esta:

class RodaApp < Roda
        redis = Redis.new
        route do |r|
            r.on "get" do
                 response['Content-Type'] = 'application/text'
                 redis.get(r.params["key"])
            end
            r.on "set" do
                redis.set(r.params["key"],r.params["value"])
            end
        end
end

map "/RedisRoda" do
        run RodaApp.freeze.app
end

Instalado en un webserver Puma con dos cores y probando desde un servidor vecino (con la herramienta «hey», luego le doy un repaso con «wrk») esto parece aguantar unas 4500 conexiones por segundo, lo que significa que incluso con 1000 conexiones se puede contestar la clave en un tiempo razonable, de hecho una media de 0.2 segundos.

Pero todavia es mejorable. Es una capacidad similar a la que tiene puma para servir ficheros estaticos, una tarea para la que normalmente delegamos en el servidor web principal. Pero claro, si le decimos al nginx que llame a ruby o a python, ya no tenemos nada que ganar.

Ahora, en ubuntu viene una expansion para nginx que se llama lua-nginx-redis y que corresponde no a la distribucion oficial sino a otra bastante facil de encontrar online, openresty, que pivota sobre codigo en lua. Asi añade a nginx la capacidad para generar contenido directamente. Sin compilar a bytecode, un script metido a lo bruto en el nginx seria:

lua_package_path "/usr/share/lua/5.1/nginx/?.lua;;";
init_by_lua_block { local json require "cjson"
                   local redis = require "redis" }

location /luaRedis {
     content_by_lua '
local redis = require "redis"
local red = redis:new()
red:set_timeout(1000) 
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.say("failed to connect: ", err)
    ngx.exit(501)
    return
end
--local args = ngx.req.get_post_args()
local res, err = red:get("data")
ngx.say("data:",res)
--red:close()
local ok, err = red:set_keepalive(10000, 1024)
';
  }

En este caso es dificil estimar la velocidad, el script lua se ejecuta en el nginx, pero mientras llama y espera respuesta del redis puede atender otro de manera que no se bloquea. El resultado es que en realidad estamos empleando dos nucleos y medio, 200% para nginx, 50% para redis. Se sigue consiguiendo una respuesta media de 0.25 segundos por llamada, pero es posible encajar unas 25000-30000 llamadas por segundo.  Practicamente el mismo resultado que si estuviera sirviendo ficheros estaticos, aunque a costa de quemar CPU.

Una alternativa a Redis es memcached, bien tambien con el lua, bien utilizando los filtros en la configuracion del nginx. Esto ultimo tendria la ventaja de ser completamente out-of-the-box en el lado de lectura, dado que nginx viene ya con una instruccion memcached_pass. Simplemente hay que meter los datos, bien con el nginx de los de taobao, bien con llamadas desde algun otro cliente.

Por ultimo, se pueden considerar frameworks de comunicacion ya completos. Aqui estarian los de AccionCable y Anycable, si queremos ir al mundo de los websockets. Pero tambien habria que considerar nchan, que tiene modulo propio para nginx y con un nchan_redis_pass puede pivotar sobre rediris, asi que en cierto modo es la solucion ya hecha.

 

EDIT: he probado los tiempos desde una maquina de amazon y si bien la regla general es la misma, el escalado depende mucho de los hilos con los que se ataque.  Vease la tabla que sigue. Probando con otras herramientas y tal las conclusiones pueden ser:

  • En estatico uno puede contar con que nginx llegue a servir hasta 30000 conexiones por segundo y nucleo virtual (o hyperthread).
    • Quizas hasta 45000 segun carga y disponibilidad, o en una bare metal segun lo reciente que sea.
    • En general se acerca a golpear los anchos de banda de las tarjetas de red, asi que no todos los hilos de una maquina deberian dedicarse a servir ficheros.
    • La gente de techempower ya noto en el 2014 que su maquina de 24 hilos, la tipica Xeon E5 de la epoca, marcaba mas de un millon de requests por segundo, eso son 25000 por hilo.
  • Con lua dentro de nginx, se pueden servir hasta 20000. Quizas es prudente estimar que se sirve la mitad de lo que puede hacer nginx desde estaticos.
    • ¿esto significa que todavia hoy en dia se puede considerar modificar los estaticos como forma de comunicacion? Bueno, falta probar memcached, que es nativo de nginx.
  • Si se trata de ejecutar Ruby, una app bare desde puma podra hacer 4000 request por segundo, un poco menos, digamos 2500 r/s, si necesita llamar a algo como Redis.
  • Si se trata de ejecutar Rails sin ningun tipo de cache, solo vamos a tener 80-90 requests por segundo por nucleo virtual. Obviamente acelerables si se cachea parte del procesado de pagina.

Ademas, hay que considerar que la parte record de la velocidad de conexion se alcanza gracias a poder acumular el trafico y reusar el TCP, porque estamos ya en rangos de decimas o centesimas de milisegundo. Esta no es inhabitual porque los servidores estan detras del balanceador de carga, pero naturalmente deja la duda de cuantos usuarios desde IPs y puertos diferentes aguanta un haproxy.

conexiones e hilostipoTotal r/sLatencia Req/s???
30000/4000ficheroNginx80135.1172.77ms27.39
RedisNginx43483.2463.84ms28.04
RedisRoda14817.24123.74ms19.39
20000/2500ficheroNginx64567.0879.07ms31.14
RedisNginx35710.0379.41ms31.26
RedisRoda12034.15132.66ms23.36
10000/2500 ficheroNginx 54178.17 82.66ms 28.66
RedisNginx15688.1846.92ms26.40
TrivialNginx56044.39116.54ms30.69
RedisRoda11898.33148.82ms18.31
TrivialRack6550.91233.04ms11.22
5000/2500ficheroNginx48145.3387.80ms27.72
RedisNginx28712.5457.59ms22.61
TrivialNginx38812.6160.89ms29.14
RedisRoda11325.65158.92ms12.57
TrivialRack6552.57281.89ms8.03
5000/3000Rails page238.901.20s0.00 (alto Timeout)
3000/3000Rails Page243.030.00 (alto Timeout)
300/300Rails Page1.23MB(alto Timeout)
1000/1000Hijack35313.0345.03ms11.25
Hijack22400.9545.40ms10.85
Hijack11972.6048.75ms10.39
Rails Page175.821.26salto timeout
RedisRoda11996.8579.29ms13.34
TrivialRack6572.38106.25ms10.57
8000/4000RedisNginx32444.7787.09ms20.13
4000/4000RedisNginx32241.75107.81ms16.87
10000/2000RedisNginx33203.5469.05ms26.83
TrivialNginx54505.66117.90ms33.29
30000/2800fichero2Nginx54863.56186.82ms21.67
20000/2800fichero2Nginx58768.45157.51ms23.32
10000/1800fichero2Nginx48682.03144.22ms31.24
4000/1800fichero2Nginx50704.0663.21ms34.33
4000/180fichero2Nginx46088.55109.98ms257.91
40000/1800fichero2Nginx96831.63207.53ms32.22
6000/1800fichero2Nginx47161.8897.16ms33.85
6000/1200fichero2Nginx50326.4182.44ms57.86
6000/450fichero2Nginx49396.6596.93ms419.36
4000/120fichero2Nginx49923.3182.13ms118.32
400/12fichero2Nginx7990.4639.44ms670.37
1000 heyfichero2Nginx 11821.1741 0.0427 secs
500 heyfichero2Nginx11709.30410.0389
50 hey fichero2Nginx1142.04100.0434
6000/1800RedisNginx29991.1290.76ms27.52
2000/800RedisNginx22359.2042.49ms34.84
600/200RedisNginx13775.9339.95ms68.99
600/200TrivialNginx23622.8451.45ms36.03
2000/1200TrivialNginx25016.9039.68ms25.17

Comments

Deja una respuesta

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.