python - requerimientos - redis rendimiento
Rendimiento de Redis vs Disk en la aplicación de almacenamiento en caché (1)
Esta es una comparación de manzanas a naranjas. Ver http://redis.io/topics/benchmarks
Redis es un almacén de datos remoto eficiente. 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 sin procesar del medio (principalmente para la red)
- la latencia del programador del sistema operativo (no garantizado en Linux / Unix)
- los errores de caché de memoria son costosos, y la probabilidad de fallas en el caché aumenta mientras los procesos del cliente y del servidor están programados para entrar / salir.
- en cajas de gama alta, efectos secundarios NUMA
Ahora, repasemos los resultados.
Comparando la implementación usando generadores y el que usa llamadas de función, no generan la misma cantidad de viajes de ida y vuelta a Redis. Con el generador, simplemente tienes:
while time.time() - t - expiry < 0:
yield r.get(fpKey)
Entonces 1 ida y vuelta por iteración. Con la función, tienes:
if r.exists(fpKey):
return r.get(fpKey)
Entonces 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 de 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.
Quería crear un caché redis en python, y como cualquier científico que se precie, hice un benchmark para probar el rendimiento.
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 un caché redis, configuro mis datos binarios (en este caso, una página HTML) a una clave derivada del nombre del archivo con una caducidad de 5 minutos.
En todos los casos, el manejo de archivos se realiza con f.read () (esto es ~ 3 veces más rápido que f.readlines (), y necesito el blob binario).
¿Hay algo que me falta en mi comparación o Redis realmente no es rival para 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 he agrupado:
Leyendo el archivo X veces
Una función que se llama para ver si el objeto redis aún está en la memoria, lo carga o almacena en caché el nuevo archivo (instancias redis únicas y múltiples).
Una función que crea un generador que produce 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 las funciones de llamada cada vez, b) redis es más lento que la lectura desde el disco, yc) la lectura de los objetos de pitón es ridículamente rápida.
¿Por qué leer desde un disco sería mucho más rápido que leer desde un archivo en memoria de redis?
EDITAR: Más información y pruebas.
Reemplacé la función a
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)
Crear una nueva instancia de redis en cada llamada de función no tiene un efecto notorio en la velocidad de lectura, la variabilidad de prueba a prueba es mayor que la ganancia.
Sripathi Krishnan sugirió implementar lecturas aleatorias de archivos. 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)
Existe una gran cantidad de variabilidad en las lecturas de archivos, por lo que la diferencia porcentual no es un buen indicador de aceleración.
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)
Usé random.choice (fileList) para seleccionar aleatoriamente un nuevo archivo en cada pase a través de las funciones.
La esencia completa está aquí si alguien quisiera probarlo - https://gist.github.com/3885957
Editar edición: no me di cuenta de que estaba llamando a un solo archivo para los generadores (aunque el rendimiento de la llamada de 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)