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 hilos tipo Total r/s Latencia Req/s???
30000/4000 ficheroNginx 80135.11 72.77ms 27.39
RedisNginx 43483.24 63.84ms 28.04
RedisRoda 14817.24 123.74ms 19.39
20000/2500 ficheroNginx 64567.08 79.07ms 31.14
RedisNginx 35710.03 79.41ms 31.26
RedisRoda 12034.15 132.66ms 23.36
10000/2500 ficheroNginx 54178.17 82.66ms 28.66
RedisNginx 15688.18 46.92ms 26.40
TrivialNginx 56044.39 116.54ms 30.69
RedisRoda 11898.33 148.82ms 18.31
TrivialRack 6550.91 233.04ms 11.22
5000/2500 ficheroNginx 48145.33 87.80ms 27.72
RedisNginx 28712.54 57.59ms 22.61
TrivialNginx 38812.61 60.89ms 29.14
RedisRoda 11325.65 158.92ms 12.57
TrivialRack 6552.57 281.89ms 8.03
5000/3000 Rails page 238.90 1.20s 0.00 (alto Timeout)
3000/3000 Rails Page 243.03 0.00 (alto Timeout)
300/300 Rails Page 1.23MB (alto Timeout)
1000/1000 Hijack3 5313.03 45.03ms 11.25
Hijack2 2400.95 45.40ms 10.85
Hijack1 1972.60 48.75ms 10.39
Rails Page 175.82 1.26s alto timeout
RedisRoda 11996.85 79.29ms 13.34
TrivialRack 6572.38 106.25ms 10.57
8000/4000 RedisNginx 32444.77 87.09ms 20.13
4000/4000 RedisNginx 32241.75 107.81ms 16.87
10000/2000 RedisNginx 33203.54 69.05ms 26.83
TrivialNginx 54505.66 117.90ms 33.29
30000/2800 fichero2Nginx 54863.56 186.82ms 21.67
20000/2800 fichero2Nginx 58768.45 157.51ms 23.32
10000/1800 fichero2Nginx 48682.03 144.22ms 31.24
4000/1800 fichero2Nginx 50704.06 63.21ms 34.33
4000/180 fichero2Nginx 46088.55 109.98ms 257.91
40000/1800 fichero2Nginx 96831.63 207.53ms 32.22
6000/1800 fichero2Nginx 47161.88 97.16ms 33.85
6000/1200 fichero2Nginx 50326.41 82.44ms 57.86
6000/450 fichero2Nginx 49396.65 96.93ms 419.36
4000/120 fichero2Nginx 49923.31 82.13ms 118.32
400/12 fichero2Nginx 7990.46 39.44ms 670.37
1000 hey fichero2Nginx 11821.1741 0.0427 secs
500 hey fichero2Nginx 11709.3041 0.0389
50 hey fichero2Nginx 1142.0410 0.0434
6000/1800 RedisNginx 29991.12 90.76ms 27.52
2000/800 RedisNginx 22359.20 42.49ms 34.84
600/200 RedisNginx 13775.93 39.95ms 68.99
600/200 TrivialNginx 23622.84 51.45ms 36.03
2000/1200 TrivialNginx 25016.90 39.68ms 25.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.