2012-10-12 129 views
25

Quería crear un caché de redis en python, y como cualquier científico que se precie, hice un benchmark para probar el rendimiento.Rendimiento de Redis vs Disk en la aplicación de caché

Curiosamente, a Redis no le fue tan bien. O Python está haciendo algo mágico (almacenando el archivo) o mi versión de redis es tremendamente lenta.

No sé si esto se debe a la forma en que está estructurado mi código, o qué, pero esperaba que redis lo hiciera mejor de lo que lo hizo.

Para hacer una memoria caché redis, configuro mis datos binarios (en este caso, una página HTML) a una clave derivada del nombre de archivo con una caducidad de 5 minutos.

En todos los casos, el manejo de archivos se realiza con f.read() (esto es ~ 3x más rápido que f.readlines(), y necesito el blob binario).

¿Hay algo que me falta en mi comparación, o es realmente Redis no coincide con un disco? ¿Pitón está almacenando en caché el archivo en algún lugar y volviendo a procesarlo cada vez? ¿Por qué es esto mucho más rápido que el acceso a redis?

Estoy usando redis 2.8, python 2.7 y redis-py, todo en un sistema Ubuntu de 64 bits.

No creo que Python esté haciendo algo particularmente mágico, ya que hice una función que almacenaba los datos del archivo en un objeto python y los cedía para siempre.

tengo cuatro llamadas a funciones que Agrupé:

Leer el archivo X veces

Una función que se llama para ver si el objeto Redis sigue en la memoria, cargarlo, o almacenar en caché nuevo archivo (single y múltiples instancias redis).

Función que crea un generador que genera el resultado de la base de datos redis (con una o varias instancias de redis).

y finalmente, almacenar el archivo en la memoria y cederlo para siempre.

import redis 
import time 

def load_file(fp, fpKey, r, expiry): 
    with open(fp, "rb") as f: 
     data = f.read() 
    p = r.pipeline() 
    p.set(fpKey, data) 
    p.expire(fpKey, expiry) 
    p.execute() 
    return data 

def cache_or_get_gen(fp, expiry=300, r=redis.Redis(db=5)): 
    fpKey = "cached:"+fp 

    while True: 
     yield load_file(fp, fpKey, r, expiry) 
     t = time.time() 
     while time.time() - t - expiry < 0: 
      yield r.get(fpKey) 


def cache_or_get(fp, expiry=300, r=redis.Redis(db=5)): 

    fpKey = "cached:"+fp 

    if r.exists(fpKey): 
     return r.get(fpKey) 

    else: 
     with open(fp, "rb") as f: 
      data = f.read() 
     p = r.pipeline() 
     p.set(fpKey, data) 
     p.expire(fpKey, expiry) 
     p.execute() 
     return data 

def mem_cache(fp): 
    with open(fp, "rb") as f: 
     data = f.readlines() 
    while True: 
     yield data 

def stressTest(fp, trials = 10000): 

    # Read the file x number of times 
    a = time.time() 
    for x in range(trials): 
     with open(fp, "rb") as f: 
      data = f.read() 
    b = time.time() 
    readAvg = trials/(b-a) 


    # Generator version 

    # Read the file, cache it, read it with a new instance each time 
    a = time.time() 
    gen = cache_or_get_gen(fp) 
    for x in range(trials): 
     data = next(gen) 
    b = time.time() 
    cachedAvgGen = trials/(b-a) 

    # Read file, cache it, pass in redis instance each time 
    a = time.time() 
    r = redis.Redis(db=6) 
    gen = cache_or_get_gen(fp, r=r) 
    for x in range(trials): 
     data = next(gen) 
    b = time.time() 
    inCachedAvgGen = trials/(b-a) 


    # Non generator version  

    # Read the file, cache it, read it with a new instance each time 
    a = time.time() 
    for x in range(trials): 
     data = cache_or_get(fp) 
    b = time.time() 
    cachedAvg = trials/(b-a) 

    # Read file, cache it, pass in redis instance each time 
    a = time.time() 
    r = redis.Redis(db=6) 
    for x in range(trials): 
     data = cache_or_get(fp, r=r) 
    b = time.time() 
    inCachedAvg = trials/(b-a) 

    # Read file, cache it in python object 
    a = time.time() 
    for x in range(trials): 
     data = mem_cache(fp) 
    b = time.time() 
    memCachedAvg = trials/(b-a) 


    print "\n%s file reads: %.2f reads/second\n" %(trials, readAvg) 
    print "Yielding from generators for data:" 
    print "multi redis instance: %.2f reads/second (%.2f percent)" %(cachedAvgGen, (100*(cachedAvgGen-readAvg)/(readAvg))) 
    print "single redis instance: %.2f reads/second (%.2f percent)" %(inCachedAvgGen, (100*(inCachedAvgGen-readAvg)/(readAvg))) 
    print "Function calls to get data:" 
    print "multi redis instance: %.2f reads/second (%.2f percent)" %(cachedAvg, (100*(cachedAvg-readAvg)/(readAvg))) 
    print "single redis instance: %.2f reads/second (%.2f percent)" %(inCachedAvg, (100*(inCachedAvg-readAvg)/(readAvg))) 
    print "python cached object: %.2f reads/second (%.2f percent)" %(memCachedAvg, (100*(memCachedAvg-readAvg)/(readAvg))) 

if __name__ == "__main__": 
    fileToRead = "templates/index.html" 

    stressTest(fileToRead) 

Y ahora los resultados:

10000 file reads: 30971.94 reads/second 

Yielding from generators for data: 
multi redis instance: 8489.28 reads/second (-72.59 percent) 
single redis instance: 8801.73 reads/second (-71.58 percent) 
Function calls to get data: 
multi redis instance: 5396.81 reads/second (-82.58 percent) 
single redis instance: 5419.19 reads/second (-82.50 percent) 
python cached object: 1522765.03 reads/second (4816.60 percent) 

Los resultados son interesantes porque a) los generadores son más rápidos que llamar a funciones cada vez, b) Redis es más lento que la lectura del disco, y c) leer objetos de pitón es ridículamente rápido.

¿Por qué leer desde un disco sería mucho más rápido que leer desde un archivo en memoria de redis?

EDITAR: Algo más de información y pruebas.

que sustituyen la función de

data = r.get(fpKey) 
if data: 
    return r.get(fpKey) 

Los resultados no difieren mucho de

if r.exists(fpKey): 
    data = r.get(fpKey) 


Function calls to get data using r.exists as test 
multi redis instance: 5320.51 reads/second (-82.34 percent) 
single redis instance: 5308.33 reads/second (-82.38 percent) 
python cached object: 1494123.68 reads/second (5348.17 percent) 


Function calls to get data using if data as test 
multi redis instance: 8540.91 reads/second (-71.25 percent) 
single redis instance: 7888.24 reads/second (-73.45 percent) 
python cached object: 1520226.17 reads/second (5132.01 percent) 

Creación de una nueva instancia Redis en cada llamada a la función en realidad no tiene un notable efecto en la velocidad de lectura, la variabilidad de la prueba a la prueba es mayor que la ganancia.

Sripathi Krishnan sugirió la implementación de lecturas de archivos aleatorios. Aquí es donde el almacenamiento en caché comienza a ayudar realmente, como podemos ver a partir de estos resultados.

Total number of files: 700 

10000 file reads: 274.28 reads/second 

Yielding from generators for data: 
multi redis instance: 15393.30 reads/second (5512.32 percent) 
single redis instance: 13228.62 reads/second (4723.09 percent) 
Function calls to get data: 
multi redis instance: 11213.54 reads/second (3988.40 percent) 
single redis instance: 14420.15 reads/second (5157.52 percent) 
python cached object: 607649.98 reads/second (221446.26 percent) 

Hay una enorme cantidad de variabilidad en la lectura de ficheros por lo que la diferencia porcentual no es un buen indicador del aumento de velocidad.

Total number of files: 700 

40000 file reads: 1168.23 reads/second 

Yielding from generators for data: 
multi redis instance: 14900.80 reads/second (1175.50 percent) 
single redis instance: 14318.28 reads/second (1125.64 percent) 
Function calls to get data: 
multi redis instance: 13563.36 reads/second (1061.02 percent) 
single redis instance: 13486.05 reads/second (1054.40 percent) 
python cached object: 587785.35 reads/second (50214.25 percent) 

que utilizan random.choice (fileList) para seleccionar al azar un nuevo archivo en cada paso a través de las funciones.

La esencia completa está aquí, si alguien quiere probarlo - https://gist.github.com/3885957

Editar editar: no se dio cuenta de que estaba llamando a un solo archivo para los generadores (aunque el rendimiento de la llamada a la función y el generador fue muy similar). Aquí está el resultado de diferentes archivos del generador también.

Total number of files: 700 
10000 file reads: 284.48 reads/second 

Yielding from generators for data: 
single redis instance: 11627.56 reads/second (3987.36 percent) 

Function calls to get data: 
single redis instance: 14615.83 reads/second (5037.81 percent) 

python cached object: 580285.56 reads/second (203884.21 percent) 
+1

No veo dónde estaba creando una nueva instancia de redis en cada llamada de función. ¿Fue solo el argumento predeterminado? – jdi

+0

Sí, si no pasa una instancia de redis, la llamada de función tomará una nueva def cache_or_get (fp, expiry = 300, r = redis.Redis (db = 5)): – MercuryRising

+2

Eso no es verdad. Esos argumentos predeterminados solo se evalúan una vez cuando se carga el script y se guardan con la definición de la función. No son evaluados cada vez que lo llamas. Eso explicaría por qué no vio ninguna diferencia entre pasar uno o dejar que use el predeterminado. En realidad, lo que estaba haciendo es crear uno para cada definición de función, más uno para cada vez que lo estaba transfiriendo. 2 conexiones no utilizadas – jdi

Respuesta

28

Esta es una comparación de manzanas a naranjas. Ver http://redis.io/topics/benchmarks

Redis es un eficiente remoto almacén de datos. Cada vez que se ejecuta un comando en Redis, se envía un mensaje al servidor de Redis, y si el cliente es sincrónico, bloquea la espera de la respuesta. Por lo tanto, más allá del costo del comando en sí, pagará por una ida y vuelta de red o un IPC.

En el hardware moderno, los recorridos de ida y vuelta o los IPC son sorprendentemente caros en comparación con otras operaciones. Esto se debe a varios factores:

  • la latencia prima del medio (principalmente para la red)
  • la latencia del programador del sistema operativo (no garantizada en Linux/Unix)
  • fallos de caché de memoria son caros , y la probabilidad de fallas en la memoria caché aumenta mientras los procesos del cliente y del servidor están programados para entrar/salir.
  • en las cajas de gama alta, los efectos secundarios de la NUMA

Ahora, vamos a revisar los resultados.

Comparando la implementación usando generadores y la que usa llamadas de función, no generan la misma cantidad de viajes de ida y vuelta a Redis. Con el generador, simplemente tiene:

while time.time() - t - expiry < 0: 
     yield r.get(fpKey) 

Por lo tanto, 1 ida y vuelta por iteración. Con la función, tiene:

if r.exists(fpKey): 
    return r.get(fpKey) 

So 2 viajes de ida y vuelta por iteración. No es de extrañar que el generador sea más rápido.

Por supuesto, se supone que debes reutilizar la misma conexión Redis para un rendimiento óptimo. No tiene sentido ejecutar un punto de referencia que sistemáticamente conecte/desconecte.

Finalmente, con respecto a la diferencia de rendimiento entre las llamadas Redis y las lecturas de archivos, simplemente está comparando una llamada local a una remota.El sistema de archivos del sistema operativo almacena en caché las lecturas de archivos, por lo que son operaciones rápidas de transferencia de memoria entre el kernel y Python. No hay E/S de disco involucrado aquí. Con Redis, debe pagar el costo de los viajes de ida y vuelta, por lo que es mucho más lento.

+4

¡Me ganaste en esta! Le pediría al OP que ejecute los puntos de referencia DESPUÉS de a) Eliminar el control exists() de Redis, b) Usar una conexión Redis persistente en lugar de volver a crearlo, y c) Leer archivos aleatorios en lugar de un solo archivo codificado. –

+0

Se agregó más información. Las lecturas aleatorias son donde el almacenamiento en caché realmente ayuda. Me parece extraño que realmente no haya tanta diferencia entre reutilizar una instancia de redis y crear nuevas. No debe haber demasiados gastos generales en la creación (me pregunto cuánto cambiaría con la autenticación). – MercuryRising

+0

El costo de autenticación es una ida y vuelta adicional que ocurre justo después de la conexión. Crear una nueva instancia de Redis es barato porque su cliente está en el mismo host que su servidor. –

Cuestiones relacionadas