web-dev-qa-db-ja.com

キャッシングアプリケーションでのRedis vs Diskのパフォーマンス

私はPythonでredisキャッシュを作成したかったので、自尊心のある科学者として、パフォーマンスをテストするためのベンチマークを作成しました。

興味深いことに、redisはそれほどうまくいきませんでした。 Pythonが何か魔法をかけている(ファイルを保存している)か、私のバージョンのredisが途方もなく遅いです。

これが私のコードの構造のせいなのか、それとも何なのかはわかりませんが、redisの方がうまくいくと期待していました。

Redisキャッシュを作成するには、バイナリデータ(この場合はHTMLページ)を、有効期限が5分のファイル名から派生したキーに設定します。

すべての場合において、ファイル処理はf.read()を使用して行われます(これはf.readlines()より〜3倍高速であり、バイナリblobが必要です)。

比較で欠けているものがありますか、それともRedisは本当にディスクに一致しませんか? Pythonファイルをどこかにキャッシュし、毎回再アクセスするのですか?これがredisへのアクセスよりもはるかに高速なのはなぜですか?

私はredis2.8、python 2.7、redis-py、すべて64ビットUbuntuシステムで使用しています。

Pythonは、ファイルデータをpythonオブジェクトに格納し、永久に生成する関数を作成したため、特に魔法のようなことをしているとは思いません。

グループ化した4つの関数呼び出しがあります。

ファイルを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)

結果は、a)ジェネレーターが毎回関数を呼び出すよりも高速であり、b)redisがディスクからの読み取りよりも低速であり、c)pythonオブジェクトからの読み取りが途方もなく高速である)という点で興味深いものです。

ディスクからの読み取りが、redisからのメモリ内ファイルからの読み取りよりもはるかに高速なのはなぜですか?

編集:いくつかの詳細情報とテスト。

関数をに置き換えました

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インスタンスを作成しても、実際には読み取り速度に目立った影響はありません。テストごとのばらつきはゲインよりも大きくなります。

Sripathi Krishnanさんは、ランダムファイル読み取りの実装を提案しました。これらの結果からわかるように、ここからキャッシュが本当に役立ち始めます。

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

編集編集:ジェネレーター用に1つのファイルを呼び出していることに気づいていませんでした(関数呼び出しとジェネレーターのパフォーマンスは非常に似ていましたが)。これは、ジェネレーターからのさまざまなファイルの結果でもあります。

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)
30
MercuryRising

これはリンゴとオレンジの比較です。 http://redis.io/topics/benchmarks を参照してください

Redisは効率的なリモートデータストアです。 Redisでコマンドが実行されるたびに、メッセージがRedisサーバーに送信され、クライアントが同期している場合は、応答の待機がブロックされます。したがって、コマンド自体のコストを超えて、ネットワークラウンドトリップまたはIPCの料金を支払うことになります。

最新のハードウェアでは、ネットワークラウンドトリップまたはIPCは、他の操作に比べて驚くほど高価です。これはいくつかの要因によるものです。

  • メディアの生のレイテンシ(主にネットワーク用)
  • オペレーティングシステムスケジューラの待ち時間(Linux/Unixでは保証されません)
  • メモリキャッシュミスはコストがかかり、クライアントプロセスとサーバープロセスがスケジュールされている間、キャッシュミスの可能性が高くなります。
  • ハイエンドボックスでは、NUMAの副作用

それでは、結果を確認しましょう。

ジェネレーターを使用した実装と関数呼び出しを使用した実装を比較すると、Redisへのラウンドトリップ数は同じではありません。ジェネレーターを使用すると、次のことが可能になります。

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

したがって、反復ごとに1ラウンドトリップします。関数を使用すると、次のことができます。

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

したがって、反復ごとに2ラウンドトリップします。ジェネレーターの方が速いのも不思議ではありません。

もちろん、最適なパフォーマンスを得るために同じRedis接続を再利用することになっています。体系的に接続/切断するベンチマークを実行する意味はありません。

最後に、Redis呼び出しとファイル読み取りのパフォーマンスの違いについては、ローカル呼び出しとリモート呼び出しを比較しているだけです。ファイルの読み取りはOSファイルシステムによってキャッシュされるため、カーネルとPython間の高速なメモリ転送操作です。ここにはディスクI/Oは含まれていません。 Redisを使用すると、往復の費用を支払う必要があるため、はるかに遅くなります。

33
Didier Spezia