2012-10-12 13 views
25

Ich wollte einen Redis-Cache in Python erstellen, und als jeder selbst respektierende Wissenschaftler machte ich einen Benchmark, um die Leistung zu testen.Leistung von Redis vs Festplatte in Caching-Anwendung

Interessanterweise ging es bei redis nicht so gut. Entweder macht Python etwas Magisches (Speichern der Datei) oder meine Version von Redis ist erstaunlich langsam.

Ich weiß nicht, ob dies wegen der Art ist, wie mein Code strukturiert ist, oder was, aber ich habe erwartet, dass redis es besser macht als es tat.

Um einen Redis-Cache zu erstellen, setze ich meine Binärdaten (in diesem Fall eine HTML-Seite) auf einen vom Dateinamen abgeleiteten Schlüssel mit einem Ablauf von 5 Minuten.

In allen Fällen wird Dateibearbeitung mit f.read() durchgeführt (das ist ~ 3x schneller als f.readlines(), und ich brauche den binären Blob).

Gibt es etwas, das ich in meinem Vergleich fehlt, oder ist Redis wirklich nicht für eine Festplatte geeignet? Python speichert die Datei irgendwo und nimmt sie jedes Mal wieder auf? Warum ist das so viel schneller als der Zugriff auf Redis?

Ich benutze Redis 2.8, Python 2.7 und Redis-Py, alle auf einem 64-Bit-Ubuntu-System.

Ich glaube nicht, dass Python etwas besonders magisches macht, als ich eine Funktion machte, die die Dateidaten in einem Python-Objekt speicherte und es für immer ergab.

Ich habe vier Funktionsaufrufe, dass ich gruppiert:

Lesen Sie die Datei X mal

Eine Funktion, wenn redis Objekt noch im Speicher zu sehen genannt wird, ist, laden Sie es, oder eine neue Datei-Cache (Einzel und mehrere Redis-Instanzen).

Eine Funktion, die einen Generator erstellt, der das Ergebnis aus der redis-Datenbank liefert (mit einzelnen und mehreren Instanzen von redis).

und schließlich, Speichern der Datei im Speicher und damit für immer.

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) 

Und nun die Ergebnisse:

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) 

Die Ergebnisse sind interessant, dass a) Generatoren sind schneller als Funktionen jedes Mal aufrufen, b) redis ist langsamer als von der Platte zu lesen, und c) Das Lesen von Python-Objekten ist lächerlich schnell.

Warum sollte das Lesen von einer Festplatte so viel schneller sein als das Lesen einer In-Memory-Datei von redis?

EDIT: Einige weitere Informationen und Tests.

ersetzte ich die Funktion

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

Die Ergebnisse auf jedem Funktionsaufruf eine neue redis Instanz unterscheiden sich nicht wesentlich von

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) 

Erstellen eigentlich kein noticable auf Lesegeschwindigkeit beeinflussen hat, die Variabilität von Test zu Test ist größer als die Verstärkung.

Sripathi Krishnan schlug vor, zufällige Dateilesevorgänge zu implementieren. Dies ist, wo Caching beginnt, um wirklich zu helfen, wie wir aus diesen Ergebnissen sehen kann.

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) 

Es gibt eine riesige Menge an Variabilität in der Datei liest, um die prozentuale Differenz nicht ein guter Indikator für Speedup ist.

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) 

I verwendet random.choice (filelist) eine neue Datei zufällig auszuwählen, auf die jeweils durch die Funktionen übergeben.

Der vollständige Kern ist hier, wenn jemand es auszuprobieren möchte - https://gist.github.com/3885957

bearbeiten edit: wusste nicht, dass ich für die Generatoren eine einzige Datei anrief (obwohl die Leistung der Funktionsaufruf und Generator war sehr ähnlich). Hier ist auch das Ergebnis verschiedener Dateien vom Generator.

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

Ich sehe nicht, wo Sie eine neue Redis-Instanz bei jedem Funktionsaufruf erstellen. War es nur die Standard-Argumentation? – jdi

+0

Ja, wenn Sie eine redisse-Instanz nicht übergeben, wird der Funktionsaufruf eine neue nehmen def cache_or_get (fp, Ablauf = 300, r = redis.Redis (db = 5)): – MercuryRising

+2

Das ist eigentlich nicht wahr. Diese Standardargumente werden nur einmal beim Laden des Skripts ausgewertet und mit der Funktionsdefinition gespeichert. Sie werden nicht bei jedem Anruf ausgewertet. Das würde erklären, warum du keinen Unterschied zwischen der Weitergabe von einem oder der Verwendung des Standard-Werts gesehen hast. Eigentlich hast du eine für jede Funktion Def erstellt, plus eine für jedes Mal, wenn du sie übergeben hast. 2 unbenutzte Verbindungen – jdi

Antwort

28

Dies ist ein Vergleich von Äpfeln mit Orangen. Siehe http://redis.io/topics/benchmarks

Redis ist ein effizienter remote Datenspeicher. Jedes Mal, wenn ein Befehl auf Redis ausgeführt wird, wird eine Nachricht an den Redis-Server gesendet, und wenn der Client synchron ist, blockiert er das Warten auf die Antwort. Über die Kosten des Befehls hinaus zahlen Sie für einen Netzwerk-Roundtrip oder einen IPC.

Auf moderner Hardware sind Netzwerk-Roundtrips oder IPCs im Vergleich zu anderen Vorgängen überraschend teuer. Dies ist auf mehrere Faktoren zurückzuführen:

  • die rohe Latenz des Mediums (in erster Linie für Netzwerk)
  • die Latenzzeit des Betriebssystems Scheduler (nicht garantiert auf Linux/Unix)
  • Speicher-Cache-Misses sind teuer und die Wahrscheinlichkeit von Cache-Misses erhöht, während die Client- und Server-Prozesse in/out geplant sind.
  • auf High-End-Boxen, Nebenwirkungen NUMA

Jetzt wollen wir die Ergebnisse überprüfen.

Beim Vergleich der Implementierung mit Generatoren und derjenigen, die Funktionsaufrufe verwenden, wird nicht die gleiche Anzahl von Roundtrips für Redis generiert. Mit dem Generator haben Sie einfach:

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

Also 1 Roundtrip pro Iteration. Mit der Funktion haben Sie:

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

Also 2 Roundtrips pro Iteration. Kein Wunder, dass der Generator schneller ist.

Natürlich sollten Sie die gleiche Redis-Verbindung für optimale Leistung wiederverwenden. Es hat keinen Sinn, einen Benchmark zu betreiben, der systematisch verbindet/trennt.

schließlich in Bezug auf den Leistungsunterschied zwischen Redis ruft und die Datei liest, werden Sie einfach einen lokalen Anruf zu einem entfernten einem Vergleich.Dateilesevorgänge werden vom Dateisystem des Betriebssystems zwischengespeichert, sodass es sich um schnelle Speicherübertragungsoperationen zwischen dem Kernel und Python handelt. Hier ist keine Platten-E/A beteiligt. Bei Redis musst du die Kosten für die Roundtrips bezahlen, also ist es viel langsamer.

+4

Du schlägst mich in diesem! Ich würde das OP bitten, die Benchmarks AFTER a) Entfernen der Bestehenden() Prüfung für Redis, b) Verwenden einer dauerhaften Redis-Verbindung, anstatt sie neu zu erstellen, und c) Lesen von zufälligen Dateien anstelle von einer einzigen fest codierten Datei. –

+0

Weitere Informationen hinzugefügt. Random Reads ist, wo Caching wirklich hilft. Es kommt mir seltsam vor, dass es wirklich keinen großen Unterschied zwischen der Wiederverwendung einer Redis-Instanz und dem Erstellen neuer Instanzen gibt. Es muss nicht viel Aufwand in der Erstellung sein (ich frage mich, wie viel es sich mit der Authentifizierung ändern würde). – MercuryRising

+0

Authentifizierungskosten sind ein zusätzlicher Roundtrip kurz nach der Verbindung. Das Erstellen einer neuen Redis-Instanz ist nur billig, da sich Ihr Client auf demselben Host wie Ihr Server befindet. –