Я хотел создать redis-кеш в python, и, как любой уважающий себя ученый, я сделал контрольный знак, чтобы проверить производительность.
Интересно, что redis не так хорошо себя чувствует. Либо Python делает что-то волшебное (сохраняя файл), либо моя версия redis значительно медленнее.
Я не знаю, связано ли это с тем, как мой код структурирован или что, но я ожидал, что redis будет лучше, чем это было.
Чтобы сделать кеш redis, я установил свои двоичные данные (в данном случае страницу HTML) на ключ, полученный из имени файла, с истечением 5 минут.
Во всех случаях обработка файлов выполняется с помощью f.read() (это ~ 3 раза быстрее, чем f.readlines(), и мне нужен двоичный blob).
Есть ли что-то, что мне не хватает в моем сравнении, или Redis действительно не подходит для диска? Является ли Python кэшированием файла где-то и каждый раз его обрабатывает? Почему это намного быстрее, чем доступ к redis?
Я использую redis 2.8, python 2.7 и redis-py, все в 64-битной системе Ubuntu.
Я не думаю, что Python делает что-то особенно волшебное, поскольку я создал функцию, которая хранила данные файла в объекте python и давала его навсегда.
У меня есть четыре функциональных вызова, которые я сгруппировал:
Чтение файла X раз
Функция, вызываемая, чтобы увидеть, остается ли объект redis в памяти, загружать его или кэшировать новый файл (один и несколько экземпляров redis).
Функция, которая создает генератор, который дает результат из базы данных redis (с одним и несколькими экземплярами redis).
и, наконец, сохранение файла в памяти и ведение его навсегда.
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)
И теперь результаты:
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)
Результаты интересны тем, что а) генераторы быстрее, чем вызывающие функции каждый раз, b) redis медленнее, чем чтение с диска, и c) чтение из объектов python является смехотворно быстрым.
Почему чтение с диска было бы намного быстрее, чем чтение из файла в памяти из redis?
EDIT: Дополнительная информация и тесты.
Я заменил функцию на
data = r.get(fpKey)
if data:
return r.get(fpKey)
Результаты не сильно отличаются от
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)
Создание нового экземпляра redis для каждого вызова функции фактически не оказывает заметного влияния на скорость чтения, изменчивость теста на тест больше, чем коэффициент усиления.
Шрипати Кришнан предложил реализовать случайные чтения файлов. Здесь кеширование начинает действительно помогать, как мы можем видеть из этих результатов.
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)
В файлах есть ОГРОМНОЕ количество изменчивости, поэтому процентная разница не является хорошим показателем ускорения.
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)
Я использовал random.choice(fileList) для случайного выбора нового файла на каждом проходе через функции.
Полный смысл есть здесь, если кто-нибудь захочет попробовать его - https://gist.github.com/3885957
Изменить править: Не понял, что я вызывал один файл для генераторов (хотя производительность вызова функции и генератора была очень похожа). Вот результат различных файлов из генератора.
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)