2012-10-12 9 views
25

Chciałem stworzyć pamięć podręczną redis w pythoniu, a jak każdy szanujący się naukowiec zrobiłem benchmark, aby przetestować wydajność.Wydajność Redis kontra Dysk w aplikacji do buforowania

Co ciekawe, Redis nie przeszedł tak dobrze. Albo Python robi coś magicznego (przechowywanie pliku), albo moja wersja redis jest niesamowicie powolna.

ja nie wiem, czy to ze względu na sposób mojego kodu jest uporządkowany, czy co, ale spodziewałem Redis zrobić lepiej niż to miało miejsce.

Aby cache Redis, ustawić moich danych binarnych (w tym przypadku, strona html) do klucza pochodzącego z pliku z upływem 5 minut.

We wszystkich przypadkach, obsługa plików odbywa się f.read() (jest to ~ 3x szybciej niż f.readlines(), i muszę Blob binarny).

Czy jest coś, czego mi brakuje w moim porównaniu, czy też Redis naprawdę nie pasuje do dysku? Czy Python buforuje plik gdzieś i ponownie go za każdym razem? Dlaczego jest to o wiele szybsze niż dostęp do redis?

Używam redis 2.8, python 2.7 i redis-py, a wszystko to w 64-bitowym systemie Ubuntu.

Nie sądzę Python robi nic szczególnie magicznego, jak zrobiłem funkcję przechowywane dane pliku w obiekcie Python i przyniósł go na zawsze.

mam cztery wywołania funkcji, że zgrupowane:

Czytając plik razy x

Funkcja, która nazywa się aby sprawdzić, czy obiekt Redis jest wciąż w pamięci, załaduj go lub buforować nowy plik (single i wiele instancji redis).

Funkcja, która tworzy generator, który daje wynik z bazy REDIS (z pojedynczych, jak i wielu przypadkach REDiS).

i wreszcie przechowywanie pliku w pamięci i poddanie go na zawsze.

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) 

a teraz wyniki:

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) 

wyniki są interesujące, że a) generatorów są szybsze niż wywoływania funkcji za każdym razem, b) Redis jest wolniejszy niż odczyt z dysku, oraz c) czytanie z obiektów Pythona jest absurdalnie szybkie.

Dlaczego czytanie z dysku byłoby o wiele szybsze niż czytanie z pliku w pamięci z redisu?

EDIT: Niektóre więcej informacji i testy.

Wymieniłem funkcję

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

Wyniki nie odbiegają znacznie od

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) 

Tworzenie nowej instancji Redis na każdym wywołaniu funkcji faktycznie nie mają zauważalny wpływ na szybkość odczytu, zmienność od testu do testu jest większa niż zysk.

Sripathi Krishnan zasugerował wprowadzenie losowych odczytów plików. W tym momencie buforowanie zaczyna naprawdę pomóc, jak wynika z tych wyników.

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) 

Istnieje ogromna ilość zmienności plik czyta więc różnica procent nie jest dobrym wskaźnikiem SpeedUp.

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) 

Użyłem random.choice (fileList), aby losowo wybrać nowy plik przy każdym przejściu przez funkcje.

Pełne Istotą jest tutaj, jeśli ktoś chciałby go wypróbować - https://gist.github.com/3885957

edytuj: nie zdawali sobie sprawy, że dzwoni jeden plik dla generatorów (choć realizacja wywołania funkcji i generatora był bardzo podobny). Oto wynik różnych plików z generatora.

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

Nie widzę, gdzie tworzyłeś nową instancję redis przy każdym wywołaniu funkcji. Czy to była tylko domyślna rzecz argumentowa? – jdi

+0

Tak, jeśli nie przekażesz instancji redis, funkcja wywoła nowe polecenie def cache_or_get (fp, expiry = 300, r = redis.Redis (db = 5)): – MercuryRising

+2

To nie jest prawda. Te domyślne argumenty są oceniane tylko raz, gdy skrypt jest ładowany i zapisywane z definicją funkcji. Nie są one oceniane za każdym razem, gdy je wywołujesz. To wyjaśniałoby, dlaczego nie widzisz żadnej różnicy między przekazaniem jednej lub zezwoleniem na użycie domyślnej. Właściwie to, co robiłeś, to tworzenie dla każdej funkcji def, plus jeden za każdym razem, gdy go przekazywałeś. 2 nieużywane połączenia – jdi

Odpowiedz

28

To porównanie jabłek do pomarańczy. Zobacz http://redis.io/topics/benchmarks

Redis to wydajny magazyn danych zdalny. Za każdym razem, gdy polecenie jest wykonywane na Redis, wiadomość jest wysyłana do serwera Redis, a jeśli klient jest synchroniczny, blokuje czekanie na odpowiedź. Tak więc poza kosztem samego polecenia zapłacisz za połączenie w sieci lub IPC.

Na nowoczesnym sprzęcie, obieg sieci lub IPC są zaskakująco drogie w porównaniu do innych operacji. Wynika to z kilku czynników:

  • surowy latency medium (głównie dla sieci)
  • opóźnień w scheduler systemu operacyjnego (nie gwarantowanej na Linux/Unix)
  • Pamięć podręczna chybienia są drogie , a prawdopodobieństwo utraty pamięci podręcznej wzrasta, podczas gdy procesy klienta i serwera są planowane do wejścia/wyjścia.
  • na polach high-end, NUMA skutki uboczne

Teraz przyjrzyjmy się wyniki.

Porównując implementację za pomocą generatorów i tej, która korzysta z wywołań funkcji, nie generują one tej samej liczby połączeń z Redis. Z generatorem wystarczy posiadać:

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

So 1 podroż dookoła za iteracji. Dzięki tej funkcji, musisz:

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

SO2 roundtrips za iteracji. Nic dziwnego, że generator jest szybszy.

Oczywiście powinieneś ponownie użyć tego samego złącza Redis, aby uzyskać optymalną wydajność. Nie ma sensu prowadzić testu porównawczego, który systematycznie łączy/rozłącza.

Wreszcie, jeśli chodzi o różnicę w wydajności między połączeniami Redis i odczytami plików, po prostu porównujesz połączenie lokalne ze zdalnym.Odczyty plików są buforowane przez system plików OS, więc są to operacje szybkiego transferu pamięci między jądrem i Pythonem. W tym przypadku nie ma tu żadnych dyskowych operacji we/wy. W Redis musisz ponieść koszty podróży w obie strony, więc jest znacznie wolniej.

+4

Pokonałeś mnie w tym! Chciałbym poprosić OP o uruchomienie benchmarków PO a) Usunięcie istniejącego() sprawdzenia dla Redis, b) Użycie trwałego połączenia Redis zamiast odtworzenia go, oraz c) Odczytanie losowych plików zamiast pojedynczego zakodowanego pliku. –

+0

Dodano więcej informacji. Losowe odczyty pomagają w buforowaniu. Wydaje mi się dziwne, że tak naprawdę nie ma tak wielkiej różnicy między ponownym użyciem instancji redis a tworzeniem nowych. W kreacji nie może być zbyt wiele (zastanawiam się, jak bardzo by się to zmieniło wraz z uwierzytelnianiem). – MercuryRising

+0

Koszt uwierzytelnienia to jeden dodatkowy zwrot w obie strony występujący zaraz po połączeniu. Utworzenie nowej instancji Redis jest tanie, ponieważ klient jest na tym samym hoście, co serwer. –

Powiązane problemy