Подтвердить что ты не робот

Производительность утилиты Redis vs Disk в кешировании

Я хотел создать 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)
4b9b3361

Ответ 1

Это сравнение яблок с апельсинами. См. http://redis.io/topics/benchmarks

Redis - это эффективное хранилище данных remote. Каждый раз, когда команда выполняется в Redis, на сервер Redis отправляется сообщение, и если клиент синхронно, он блокирует ожидание ответа. Таким образом, помимо стоимости самой команды, вы будете платить за сетевой обмен или IPC.

На современном оборудовании сетевые обоймы или IPC являются чрезвычайно дорогими по сравнению с другими операциями. Это связано с несколькими факторами:

  • необработанная латентность среды (в основном для сети)
  • время ожидания планировщика операционной системы (не гарантируется в Linux/Unix)
  • Недостатки кэширования памяти являются дорогостоящими, а вероятность промахов кэша увеличивается, когда клиентские и серверные процессы планируются в/из.
  • на high-end боксах, побочных эффектах NUMA

Теперь просмотрите результаты.

Сравнивая реализацию с использованием генераторов и тех, которые используют вызовы функций, они не генерируют одинаковое количество обращений к Redis. С генератором вы просто имеете:

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

Итак, один раунд за итерацию. С помощью функции у вас есть:

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

Итак, 2 раунда за итерацию. Неудивительно, что генератор работает быстрее.

Конечно, вы должны использовать одно и то же соединение Redis для оптимальной производительности. Нет смысла запускать эталон, который систематически связывает/отключает.

Наконец, что касается разницы в производительности между вызовами Redis и чтениями файлов, вы просто сравниваете локальный вызов с удаленным. Файловые чтения кэшируются файловой системой ОС, поэтому они выполняют оперативную передачу данных между ядром и Python. Здесь нет дискового ввода-вывода. С Redis вы должны заплатить за стоимость раундов, так что это намного медленнее.