メモリ最適化¶
このページは作業中です。現在のところ、メモリに関する問題が発生したときにチェックするべき項目のリストにすぎません。
小さな集約データ型のための特別なエンコーディング¶
Redis 2.2 以降、多くのデータ型は、ある一定のサイズ以下であれば、使用する空間が小さくなるように最適化がされています。Hash, List, 整数型からなる Set, および Sorted Set は、既定の上限より要素数が小さく、かつ個々の要素のサイズが上限を超えない場合は、非常にメモリ効率の良い方法でエンコードされます。このエンコーディングにより、メモリ使用量は 最大で 10 分の 1 、平均で 5 分の 1 程度に削減されます。
これは、ユーザーやAPIからは完全に隠蔽されます。また、CPU とメモリのトレードオフとなるため、エンコードを適用する最大要素数や各要素の最大サイズは redis.conf の以下の設定によりチューニングが可能です。
hash-max-zipmap-entries 64 (hash-max-ziplist-entries for Redis >= 2.6)
hash-max-zipmap-value 512 (hash-max-ziplist-value for Redis >= 2.6)
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512
特別にエンコードされた値が、設定された最大値をオーバーすると、Redis は自動的にそれらを通常のエンコーディングに変換します。この操作は小さな値に対しては非常に高速ですが、より大きな値に対して特別エンコーディングを適用するために、設定値をデフォルトから変更する場合は、ベンチマークをとり変換にかかる時間を確認しておくと良いでしょう。
32 bit インスタンスを使用する¶
32 bit アーキテクチャ向けにコンパイルされた Redis は、ポインタサイズが小さいためメモリ消費も少ないですが、最大メモリサイズが 4 GB までに制限されます。32 bit バイナリとして Redis をコンパイルするには make 32bit を指定してください。RDB および AOF ファイルは 32bit, 64 bit (もちろん、リトルエンディアン・ビッグエンディアンでも) で互換なため、32 bit から 64 bit (またはその逆) への切り替えは問題なく行えます。
Bit および Byte レベルの操作¶
Redis 2.2 から、bit および byte レベルを扱う新しい操作が導入されました: GETRANGE, SETRANGE, GETBIT, SETBIT これらのコマンドを利用することで、String 型をランダムアクセス可能な配列として扱うことができます。たとえばあなたが、ユーザーをユニークな整数値で識別するアプリケーションを扱う場合、ユーザーの性別情報を保持する bitmap を使うことができます。女性なら bit をセット、男性ならクリアする、というように。こうすることで、1 億ユーザーの情報は、たかだか 1 Redis インスタンス中の 12 MB に収まります。同様に、ユーザあたり 1 byte で表現される情報を GETRANGE と SETRANGE を使って保存することができます。これは単なる例ですが、この非常に省スペースで新しいプリミティブ値を使って、多くの問題を設計することが可能です。
なるべく Hash を使う¶
小さな Hash は非常に省スペースなやり方でエンコードされるため、可能なら Hash を使ってデータを表現すべきです。たとえばあなたが Web アプリケーションのユーザーを表現するオブジェクトを扱うとき、名前、姓、Eメール、パスワード、に別々のキーを割り当てる代わりに、必要なすべてのフィールドを定義したひとつの Hash を使ってください。
もし上記についてより詳しく知りたいなら、次の節を読んでください。
Hash を使って、メモリ効率の高い、抽象化されたキー・バリューストアを Redis 上に構築する¶
この節のタイトルはぞっとしないものですが、詳しく説明していきます。
簡単にいえば、バリューがただの String であるとき、普通の Redis キーのみならず memcached よりもメモリ効率の良いキー・バリューストアを Redis 上に構築できる、というものです。
ある事実から始めましょう: いくつかのキーは、いくつかのフィールドをもつ Hash を保持するひとつのキーよりも、多くのメモリを必要とします。どうしてそうようなことが起こるのでしょうか?私たちはあるトリックを使っています。理論上、定数時間でのルックアップを保証するためには(ビッグ・オー記法でいう O(1) として知られる)、ハッシュテーブルのように、平均的に定数時間の計算量をもつデータ構造が必要です。
しかし、しばしば Hash は少数のフィールドしか含みません。Hash が小さいときは、線形配列のように、長さが固定されたキー・バリューのペアからなる O(N) データ構造にエンコードすることができます。これは、HGET と HSET コマンドのならし計算時間が O(1) に収まる程度に N が小さい場合に限り行われます: Hash に含まれる要素数が大きくなりすぎたら(上限は redis.conf で設定できます)、Hash は即座に本物のハッシュテーブルに変換されます。
これは、時間計算量の観点からだけでなく、定数時間という観点からも有効です。バリューペアの線形配列は、CPU キャッシュとの相性が非常に良いためです(ハッシュテーブルよりもキャッシュの局所性が良い)。
しかしながら、Hash のフィールドと値はフル機能を備えた Redis オブジェクトではないため、Hash のフィールドは本物のキーのような time to live (expire) をもたず、また値として String しか持つことができません。これは Hash 型の API 設計において意図されたものであり、私たちはこれで良しとしています(私たちは、機能よりシンプルさを信頼しており、個々のフィールドの expire を考慮しないと同様に、ネストしたデータ構造を考慮していません)。
なるほど、Hash のメモリ効率は良い。関連のあるフィールドのまとまりがある場合には、オブジェクトを表現したり、モデリングするのには非常に便利でしょう。しかし、シンプルなキー・バリューを扱う場合はどうでしょう?
JSON エンコードされたオブジェクトや HTML フラグメント、単純なキーとブール値のペア、その他、沢山の小さなオブジェクトをキャッシュするのに Redis を使うことを考えてください。なんであれ基本的には小さなキーと小さな値からなる、String から String へのマップに集約されます。
私たちがキャッシュしたいオブジェクトが、以下のように番号づけされているとしましょう:
- object:102393
- object:1234
- object:5
私たちができるのは次のようなことです。新しい値をセットするための SET 操作が実行される都度、キーを 2 つに分解します。片方はキーとして使い、もう片方は Hash のフィールド名として使います。たとえば、”object:1234” という名前のオブジェクトは、以下のように分解されます:
object:12 という名前のキー
34 という名前のフィールド
すべての文字を使いますが、最後のパートの 2 文字はキー、末尾の 2 文字は Hash フィールド名になります。キーをセットするため、以下のコマンドを発行します:
HSET object:12 34 somevalue
すぐにわかるように、すべての Hash は、最終的には 100 個のフィールドを含むようになり、これは CPU とメモリ節減の最適な折衷ラインです。
このスキーマに則ると、キャッシュされるオブジェクトが全部でいくつあるかに関わらず、各 Hash はおおよそ 100 個のフィールドを含む、というのはもうひとつの注目すべき点です。これは、オブジェクト名が常に、ランダムな文字列ではなく数字で終わるためです。ある意味、最後の数字は暗黙的な pre-sharding の一種とみなすことができます。
小さな数字についてはどうでしょう? object:2 のような? このケースは、”object:” をキーとして使い、数字部分をすべて Hash のフィールド名として使うことで対応できます。つまり、 object:2 と object:10 は両方とも “object:” というキーに含まれ、一方は “2”, もう一方は “10” というフィールド名で参照されます。
この方針で、どれくらいのメモリを節約できるでしょう?
以下の Ruby プログラムを使ってテストを行いました:
require 'rubygems'
require 'redis'
UseOptimization = true
def hash_get_key_field(key)
s = key.split(":")
if s[1].length > 2
{:key => s[0]+":"+s[1][0..-3], :field => s[1][-2..-1]}
else
{:key => s[0]+":", :field => s[1]}
end
end
def hash_set(r,key,value)
kf = hash_get_key_field(key)
r.hset(kf[:key],kf[:field],value)
end
def hash_get(r,key,value)
kf = hash_get_key_field(key)
r.hget(kf[:key],kf[:field],value)
end
r = Redis.new
(0..100000).each{|id|
key = "object:#{id}"
if UseOptimization
hash_set(r,key,"val")
else
r.set(key,"val")
end
}
これは Redis 2.2 64 bit インスタンスで実行した際の結果です。
- UseOptimization set to true: 1.7 MB of used memory
- UseOptimization set to false; 11 MB of used memory
1 桁分の違いがあります。これにより、Redis は事実上もっともメモリ効率の良いキー・バリューストアであるといえます。
注意 : これが機能するためには、redis.conf が、たとえば次のように設定されていることを確認しておいてください。
hash-max-zipmap-entries 256
また同様に、キーと値の最大長に応じて、以下のフィールドも忘れずに設定してください。
hash-max-zipmap-value 1024
Hash は指定された最大要素数、または最大要素サイズを超えると、本物のハッシュテーブルに変換され、メモリ節減の効果は失われます。
キー・スペース内で暗黙的にやってくれたら、ユーザーが考える必要がなくなるのに、なぜそうしないのか?と疑問に思うかもしれません。それには 2 つの理由があります: ひとつは、トレードオフを明確にするためです。ここには CPU, メモリ, 最大要素数, の間にはっきりとしたトレードオフがあります。ふたつめは、トップレベルのキー・スペースには Expire や LRU データ、その他諸々といった、サポートしなければならない多くの関心事項があるためです。一般にこのやり方に対応するのは現実的ではありません。
Redis のやり方は、ユーザーは物事の仕組みを知るべきだ、というものです。そうすることで、ユーザーは最良の妥協策を選択でき、またシステムがどのように振る舞うかについて正確に理解することができるでしょう。
メモリ割り当て¶
ユーザーのキーを保存するために、Redis は最大で ‘maxmemory’ 設定が許す限りのメモリを割り当てます(いくぶんかの追加割り当ても可能ですが)。
正確な値は、設定ファイルに記述するか、または CONFIG SET で後から設定することも可能です(Using Redis as an LRU cache も参照してください)。Redis がどのようにメモリ管理をしているか、いくつか注意すべき点があります:
Redis は、キーが削除されたとき、常にメモリを開放して OS に返すわけではありません。これは Redis に限った話ではなく、ほとんどの malloc() の実装がそうなっているためです。たとえば、5GB のデータでインスタンスをいっぱいにし、その後 2GB に相当するデータを削除したとき、Resident Set Size (プロセスによって消費されているメモリページ数。RSS とも言われる) はまだ 5GB 程度のままでしょう。Redis がユーザーメモリは 3GB であると主張しているとしても、です。これは下層の allocator が簡単にはメモリを解放できないために起こります。たとえば、まだ存在しているキーと同じページ上にある、すでに削除されたキーの大部分は割りつけられたままです。
上述の点は、 ピークメモリ使用量 に基づいてメモリを用意しておく必要がある、ということを意味します。もしあなたのワークロードが時々 10GB を要求するなら、たとえほとんどの期間では 5GB しか必要としないとしても、10GB を準備しておく必要があります。
しかし、allocator は賢く、解放されたメモリのチャンクを再利用することができます。そのため、5GB データセットのうちの 2GB を解放した後、再びキーを追加していくと、2GB 分のキーが追加されるまでは RSS (Resident Set Size) は一定の状態を保ったままで増えないことが確認できるでしょう。
これらにより、ピークメモリ使用量が現在のメモリ使用量よりも非常に大きい場合、フラグメンテーション率は信頼できる値とはいえません。フラグメンテーションは、現在のメモリ使用量(Redis自身による割り当ての合計)を、実際に割り当てられている物理メモリ(RSS が示す値)で割った値です。RSS はピークメモリ使用量を反映しているため、すでに多くのキー / バリューが解放済みで、実際に使用されている(仮想)メモリ量は少ないにも関わらず、RSS は高いままです。結果として mem_used に対する RSS の配分が非常に高い状態となるでしょう。
もし ‘maxmemory’ が設定されていなければ、Redis は必要とするメモリを確保し続けようとするため、フリーなメモリ領域を(徐々に)すべて食いつぶしてしまう可能性があります。そのため、何らかの制限をかけることが一般に推奨されます。併せて、’maxmemory-policy’ を ‘noeviction’ (これは古いバージョンの Redis ではデフォルト値 ではありません ) に設定したいこともあるでしょう。
この設定を行うと、利用可能なメモリの制限に達した場合、書き込みコマンドを発行すると out of memory エラーが発生します。結果的にアプリケーションエラーとなりますが、メモリ枯渇によりマシン全体が停止してしまうことは防げます。
Work in progress¶
このドキュメントは作業中です...今後、より多くの tips が追加される予定です。