2012-10-12 13 views
25

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) 
+1

Non vedo dove stavi creando una nuova istanza di redis su ogni chiamata di funzione. Era solo l'argomento di default? – jdi

+0

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

+2

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

risposta

28

Questo è un confronto tra mele e arance. Vedere http://redis.io/topics/benchmarks

Redis è un archivio dati remoto efficiente. Ogni volta che un comando viene eseguito su Redis, un messaggio viene inviato al server Redis e, se il client è sincrono, blocca l'attesa della risposta. Quindi, oltre al costo del comando stesso, pagherete un viaggio di andata e ritorno in rete o un IPC.

Su hardware moderno, andata e ritorno di rete o IPC sono sorprendentemente costosi rispetto ad altre operazioni. Ciò è dovuto a diversi fattori:

  • la latenza grezzo del mezzo (principalmente per la rete)
  • la latenza del scheduler sistema operativo (non garantita Linux/Unix)
  • cache miss memoria sono costosi e la probabilità di errori di cache aumenta mentre i processi client e server sono pianificati in/out.
  • su scatole di fascia alta, NUMA effetti collaterali

Ora, passiamo in rassegna i risultati.

Confrontando l'implementazione utilizzando i generatori e l'altra utilizzando le chiamate di funzione, non generano lo stesso numero di round trip verso Redis. Con il generatore hai semplicemente:

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

Quindi 1 andata e ritorno per iterazione. Con la funzione, hai:

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

Quindi 2 andata e ritorno per iterazione. Non c'è da stupirsi che il generatore sia più veloce.

Ovviamente si suppone di riutilizzare la stessa connessione Redis per prestazioni ottimali. Non ha senso eseguire un benchmark che si connetta/disconnette sistematicamente.

Infine, per quanto riguarda la differenza di prestazioni tra le chiamate Redis e il file letto, si sta semplicemente confrontando una chiamata locale con una remota.Le letture dei file sono memorizzate nella cache dal filesystem del sistema operativo, quindi sono veloci operazioni di trasferimento della memoria tra il kernel e Python. Non vi è alcun I/O del disco coinvolto qui. Con Redis, devi pagare il costo dei viaggi di andata e ritorno, quindi è molto più lento.

+4

Mi hai battuto in questo! Vorrei chiedere all'OP di eseguire i benchmark DOPO a) Rimuovere il controllo exist() per Redis, b) Utilizzare una connessione Redis persistente invece di ricrearla, e c) Leggere i file casuali invece di un singolo file hard coded. –

+0

Aggiunte ulteriori informazioni. Le letture casuali sono dove la cache aiuta davvero. Mi sembra strano che non ci sia molta differenza tra riutilizzare un'istanza redis e crearne di nuove. Non ci deve essere molto sovraccarico nella creazione (mi chiedo quanto cambierebbe con l'autenticazione). – MercuryRising

+0

Il costo di autenticazione è un roundtrip aggiuntivo che si verifica subito dopo la connessione. La creazione di una nuova istanza di Redis è economica solo perché il tuo client si trova sullo stesso host del tuo server. –

Problemi correlati