Volevo creare una cache redis in python e, come ogni scienziato che si rispetti, ho fatto un benchmark per testare la performance.Prestazioni di Redis vs Disk nella domanda di memorizzazione nella cache
È interessante notare che i redis non sono andati così bene. O Python sta facendo qualcosa di magico (memorizzando il file) o la mia versione di redis è incredibilmente lenta.
Non so se questo è dovuto al modo in cui è strutturato il mio codice o cosa, ma mi aspettavo che i redis facessero meglio di quello che ha fatto.
Per creare una cache redis, ho impostato i miei dati binari (in questo caso, una pagina HTML) su una chiave derivata dal nome file con una scadenza di 5 minuti.
In tutti i casi, la gestione dei file avviene con f.read() (questo è ~ 3 volte più veloce di f.readlines(), e ho bisogno del blob binario).
C'è qualcosa che mi manca nel mio confronto, o Redis è davvero nessuna corrispondenza per un disco? Python memorizza il file nella cache da qualche parte e lo riaccede ogni volta? Perché è così molto più veloce dell'accesso ai redis?
Sto usando redis 2.8, python 2.7 e redis-py, tutto su un sistema Ubuntu a 64 bit.
Non penso che Python stia facendo qualcosa di particolarmente magico, poiché ho creato una funzione che memorizzava i dati del file in un oggetto python e l'ha resa per sempre.
ho quattro chiamate di funzione che ho raggruppato:
Leggere il file X volte
Una funzione chiamata per vedere se l'oggetto Redis è ancora in memoria, caricarlo, o nella cache nuovo file (singolo e più istanze di redis).
Una funzione che crea un generatore che produce il risultato dal database redis (con istanze singole e multiple di redis).
e infine, archiviando il file in memoria e cedendolo per sempre.
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)
e ora i risultati:
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)
I risultati sono interessanti in quanto a) generatori sono più veloce di chiamare funzioni di volta in volta, b) Redis è più lenta di lettura dal disco, e c) leggere da oggetti pitone è ridicolmente veloce.
Perché la lettura da un disco è molto più veloce della lettura da un file in memoria da redis?
MODIFICA: Altre informazioni e prove.
ho sostituito la funzione di
data = r.get(fpKey)
if data:
return r.get(fpKey)
I risultati non differiscono molto da
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)
Creazione di una nuova istanza Redis su ogni chiamata di funzione in realtà non ha un notevole effetto sulla velocità di lettura, la variabilità tra test e test è maggiore del guadagno.
Sripathi Krishnan ha suggerito di implementare letture di file casuali. È qui che il caching inizia ad aiutare davvero, come possiamo vedere da questi risultati.
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)
C'è una quantità enorme di variabilità nel file di letture in modo che la differenza percentuale non è un buon indicatore di aumento di velocità.
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)
Ho usato random.choice (fileList) per selezionare casualmente un nuovo file su ogni passaggio attraverso le funzioni.
Il succo completo è qui, se qualcuno volesse provarlo - https://gist.github.com/3885957
edit non mi resi conto che mi stava chiamando un unico file per i generatori (anche se le prestazioni della chiamata di funzione e generatore era molto simile). Ecco il risultato di diversi file anche dal generatore.
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)
Non vedo dove stavi creando una nuova istanza di redis su ogni chiamata di funzione. Era solo l'argomento di default? – jdi
Sì, se non si passa un'istanza redis, la chiamata alla funzione acquisirà una nuova cache_or_get (fp, scadenza = 300, r = redis.Redis (db = 5)): – MercuryRising
Questo non è vero. Questi argomenti predefiniti vengono valutati solo una volta quando viene caricato lo script e salvati con la definizione della funzione. Non vengono valutati ogni volta che lo chiami. Questo spiegherebbe perché non hai visto alcuna differenza tra il passarne uno o lasciarlo usare quello predefinito. In realtà ciò che stavi facendo è crearne uno per ogni funzione, più uno per ogni volta che lo stavi passando. 2 connessioni inutilizzate – jdi