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)
Nie widzę, gdzie tworzyłeś nową instancję redis przy każdym wywołaniu funkcji. Czy to była tylko domyślna rzecz argumentowa? – jdi
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
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