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)
No veo dónde estaba creando una nueva instancia de redis en cada llamada de función. ¿Fue solo el argumento predeterminado? – jdi
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
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