Amazon ElastiCache for Valkey 9.1の「128バイト未満の文字列で最大20%削減」を検証してみた

Amazon ElastiCache for Valkey 9.1の「128バイト未満の文字列で最大20%削減」を検証してみた

ElastiCacheのValkey 9.1対応で告知された「128バイト未満の文字列のメモリ使用量を最大20%削減」を、9.0と9.1のクラスターを並べて検証しました。閾値は値単体ではなくキーとTTLを含む合計で、100万件投入では13.9%減、逆に増える区間もありました。
2026.08.26

こんにちは。サービス開発部の武田です。

Amazon ElastiCacheが2026年6月23日にValkey 9.1へ対応しました。告知には「128バイト未満の文字列のメモリ使用量を最大20%削減する」とあります。

https://aws.amazon.com/about-aws/whats-new/2026/06/amazon-elasticache-valkey-9-1/

この「128バイト未満」が具体的に何のサイズなのか、9.0と9.1のクラスターを並べて動かして確認しました。結論からいうと、128バイトは値の長さのことではありませんでした。

先に結論

  • 埋め込み判定の閾値は、値だけでなく「robj + キー + TTL + 値」の合計で決まる。キーを長くすると境界が下がる
  • 9.0は合計64バイト、9.1は合計128バイト。加えて9.1はptrフィールドの8バイトを再利用するので、許容される値長は最大72バイト広がる
  • MEMORY USAGEで見た1キー分では公称どおり20%削減される条件がある(80→64バイト)
  • ただし100万件投入時のused_memory増分で見ると13.9%減。ハッシュテーブル等のオーバーヘッドが両バージョン共通で乗るため
  • 9.1のほうがメモリを多く使う区間がある(キー16バイト・TTLなしでは値90〜92バイト)

検証環境

東京リージョンにValkey 9.0と9.1のレプリケーショングループを1つずつ作りました。

項目 設定
ノードタイプ cache.m7g.large × 1ノード
クラスターモード 無効
レプリカ なし
パラメーターグループ 9.0と9.1で共用のカスタムグループ
maxmemory-policy noeviction
activedefrag no

9.0と9.1はどちらもvalkey9パラメーターグループファミリのため、同じパラメーターグループを両方に割り当てられます。これによってエンジンバージョン以外の条件をそろえられます。

アロケーターは両方とも同じでした。

9.0: valkey_version:9.0.0 / mem_allocator:jemalloc-5.3.0
9.1: valkey_version:9.1.0 / mem_allocator:jemalloc-5.3.0

クライアントは同一AZのEC2です。

何が変わったのか

Valkeyは各キーの値をrobjという構造体で管理します。文字列が小さければrobjと同じメモリブロックに直接埋め込み(OBJECT ENCODINGで見るとembstr)、大きければ別にメモリを確保してポインターで指します(raw)。埋め込めばメモリ確保が1回で済み、確保ごとの管理オーバーヘッドが減ります。この「埋め込むかどうか」の判定が9.1で変わりました。

告知の「128バイト未満」を素直に読むと、値の長さだけで決まるように見えます。ところが9.1のクラスターで確かめると、キー16バイトのときembstrになる値の上限は97バイトでした。128バイトには届きません。値の長さだけでは決まっていないようです。

ソースを確認すると、理由が分かります。9.0のsrc/object.cです(コメントは一部省略)。

robj *createStringObjectWithKeyAndExpire(const char *ptr, size_t len, const sds key, long long expire) {
    /* When to embed? Embed when the sum is up to 64 bytes. ... */
    size_t size = sizeof(robj);
    if (key) {
        size_t key_len = sdslen(key);
        size += sdsReqSize(key_len, sdsReqType(key_len)) + 1;
    }
    size += (expire != -1) * sizeof(long long);
    size += sdsReqSize(len, SDS_TYPE_8);
    if (size <= 64) {

9.1の同じ箇所です。判定が関数に切り出されています。

static bool shouldEmbedStringObject(size_t val_len, const_sds key, long long expire) {
    /* When to embed? Embed when the sum is up to 128 bytes. (2 cache lines on most systems) */
    size_t size = sizeof(robj) - sizeof(void *); /* reusing 'ptr' memory when embedding */
    ...
    return size <= 128;
}

変更は2点です。

  1. 閾値が64バイトから128バイトに倍増した
  2. 埋め込み時にptrフィールドを使わなくなるので、その8バイトを再利用する

合わせて、同じキー長・TTLなら、9.1では9.0より最大72バイト長い値まで埋め込みの対象になる計算です。そしてどちらのバージョンも、判定に使うのはキーとTTLと値の合計です。値単体の長さではありません。

境界は本当に「合計」で決まるのか

ソースのとおり合計で決まるなら、キーを長くすると埋め込める値の長さが縮むはずです。キー長を変えながら、OBJECT ENCODINGembstrのままでいられる最大の値長を二分探索しました。

キー長 9.0の上限 9.1の上限
8 33 105
16 25 97
32 7 79
64 埋め込み不可 47
100 埋め込み不可 11

キーを長くすると境界が下がりました。ソースのとおりです。

キー長8・16・32では、9.0と9.1の差がいずれも72バイトちょうどでした。ソースから計算した値(64→128の64バイト、ptr再利用の8バイト)と一致します。

キー長64以上では、9.0は値長0でも合計が64バイトを超えるため、そもそも埋め込みができません。表の「埋め込み不可」は「空文字列なら埋め込めた」という意味ではないので、この行の差は算術的な増分にはなりません。

TTLの有無では境界が動きませんでした。

1キーあたりのメモリを測る

キーを16バイト固定にして、値長を変えながらOBJECT ENCODINGMEMORY USAGEを見ます。

TTLなしの場合です。

値長 9.0 9.1
16 embstr / 64 embstr / 56 8
32 raw / 88 embstr / 80 8
40 raw / 96 embstr / 80 16
45 raw / 104 embstr / 96 8
60 raw / 112 embstr / 112 0
85〜89 raw / 144 embstr / 128 16
90〜92 raw / 144 embstr / 160 -16
93〜97 raw / 160 embstr / 160 0
100 / 127 / 128 / 129 / 256 raw raw 0

TTLありの場合です。

値長 9.0 9.1
16 80 64 16
32 96 80 16
80 152 128 24
84〜90 152 160 -8
94〜97 168 160 8
100 / 127 / 128 / 129 / 256 同じ 同じ 0

値16バイトにTTLを付けたケースは80バイトから64バイトになりました。ちょうど20%で、公称値と一致します。

9.1のほうがメモリを食う区間がある

TTLなしで1バイト刻みに測ると、段差の位置がはっきりしました。

値長 9.0 9.1
89 raw / 144 embstr / 128 16
90 raw / 144 embstr / 160 -16
91 raw / 144 embstr / 160 -16
92 raw / 144 embstr / 160 -16
93 raw / 160 embstr / 160 0

89バイトと90バイトの間で9.1が128から160に跳ねます。9.0のほうは144のままですので、90〜92バイトの区間だけ9.1が16バイト大きくなります。

理由はアロケーションのまとめ方の違いです。9.1は「オブジェクト + キー + 値」を単一のアロケーションにするので、丸め単位を越えると一段まるごと上がります。9.0はオブジェクトと値の文字列が別々のアロケーションですので、それぞれが小さい丸め単位に収まり、合計は144バイトで済んでいます。

アロケーションを1つにまとめれば常に小さくなるわけではありません。大きな単一アロケーションがアロケーターの丸め境界を越えると、複数アロケーションの合計より大きくなる区間が生まれます。

段差の位置はキー長とTTLが変われば動くので、「値90バイトが常に境界」ではありません。丸めの単位はアロケーター依存ですが、ElastiCacheなら同じエンジンバージョンで変わらないはずです。

TTLで境界が動かなかった理由

TTLの有無で埋め込み境界が変わらなかった件です。

SET key value PX ...は、t_string.csetGenericCommand()でこの順に処理されます。

setKey(c, c->db, key, &val, setkey_flags);
if (expire) val = setExpire(c, c->db, key, milliseconds);

オブジェクトの生成が先で、TTLの付与は後です。生成時点ではTTLが分からないので、埋め込み判定のTTL項は0として評価されます。

ただしこれだけでは説明が足りません。9.1にはTTL領域の先行予約があります。

/* If the allocation has enough space for an expire field, add it even if we
 * don't need it now. Then we don't need to realloc if it's needed later. */
if (!o->hasexpire && bufsize >= min_size + sizeof(long long)) {
    o->hasexpire = 1;
    min_size += sizeof(long long);
}

アロケーターから実際に得た領域に8バイト以上の余りがあれば、TTLがなくてもTTL用の領域を確保しておきます。予約済みなら、あとからTTLを付けても再確保が起きません。予約されていなければTTL込みで判定をやり直すので、rawに移る可能性があります。

PEXPIREで切り分けてみました。

値長 SETのみ SET後にPEXPIRE SET ... PX
90 / 94 / 96 / 97(embstr) 160 160 160
98 / 100(raw) 160 168 168

embstrのケースはTTLを付けても増えません。TTL領域が先行予約されているためです。rawのケースは予約がないので8バイト増えます。

「TTLを付けると必ず8バイト増える」とも「TTLでは境界が動かない」とも言えません。今回試した範囲ではTTLによる境界の変化は見られませんでしたが、内部のhasexpireや実確保サイズは外から観測できないので、全ケースで先行予約が行われていたかまでは確認できていません。RESTOREのように生成時点でTTLが分かる経路も検証していません。

100万キーで測る

1キー単位で差が出ることは分かったので、まとまった量で見ます。

キーを16バイト固定にして100万件投入し、投入前後のused_memoryの差を件数で割ります。値は整数エンコードを避けるため非数値のASCIIにしました。各測定でFLUSHALL SYNCしてから空状態のused_memoryを9回測って中央値を取り、投入後も同様に9回測っています。

確定した数字

値長 TTL 9.0 9.1 削減率
16 あり 114.9 98.9 13.9%減
90 なし 161.4 177.5 約10%増

いずれも中央値、n=3です。全測定でDBSIZEが1000000、evicted_keysが0であることを確認しています。

1キー単位で見えた傾向がそのまま出ました。値90バイトでは9.1のほうが約10%多く使います。

参考までに、各1回のみですが他の値長も測っています。値90バイトの初回測定(161.5)がABBA反復(161.4〜161.5)と0.1以内で一致していたので、これらはウォーム状態での測定と見てよさそうです。

値長 TTL 9.0 9.1 削減率
40 なし 113.5 97.5 14.1%
64 なし 145.5 129.5 11.0%
256 なし 385.5 385.5 0.0%

公称20%と13.9%の差はどこから来るのか

値16バイト・TTLありのケースで、2つの数字を並べます。

9.0 9.1
MEMORY USAGE(1キー) 80 64 16
used_memory増分(bytes/key) 114.9 98.9 16.0
差引 34.9 34.9 0

used_memory増分の差は16.0バイトで、MEMORY USAGEの差16バイトと、掲載している精度の範囲で一致しました。オブジェクト表現の差以外に追加の差分は観測されていません。

差引の34.9バイトは、MEMORY USAGEには出ない領域です。bytes/keyused_memoryの投入前後差から求めているので、オブジェクト本体に加えて、DBのハッシュテーブルなどキーの格納に伴って増える領域も含みます。この差引が両バージョンとも34.9バイトでそろっていました。

つまりこういうことです。

  • MEMORY USAGEで見た1キー分では80→64で、ちょうど20%減。公称値のとおり
  • 100万件投入時のused_memory増分では114.9→98.9で13.9%減。ハッシュテーブルなどの34.9バイトが両バージョン共通で乗るぶん、削減率が薄まる

容量を見積もるなら後者です。ただし13.9%という数字は、キー16バイト・値16バイト・TTLあり・100万件という今回のデータの形に限った値です。実環境では自分のキー長・値長・TTLの比率で測り直しましょう。

実運用でどう考えるか

9.1でメモリが減るのは、次のような場合です。

  • 値が小さい。キーとの合計が128バイトに収まる
  • 9.0の64バイトは超えていた(つまり合計65〜128バイトの帯)
  • キーが短い。キーが長いぶん、埋め込める値の長さは縮む

逆に、減らない・増えてしまうのはこちらです。

  • 値が大きい。今回試した100・127・128・129・256バイトではどちらもrawになり差がなくなった
  • アロケーターの丸め境界にかかる狭い区間。今回は値90〜92バイトで9.1が16バイト大きかった

自分のデータで判断するなら、OBJECT ENCODINGMEMORY USAGEを実際のキーと値の組み合わせで見るのが早いです。キー長を含めて測らないと意味がありません。

まとめ

「128バイト未満の文字列のメモリ使用量を最大20%削減する」という一文の中身を追いかけました。閾値はキーとTTLを含む合計で、オブジェクト単体とクラスター全体では削減率が違い、値の長さによっては逆に増える区間もあります。

とはいえだいぶエッジケースですので、基本的には効率よくなったなという理解でよさそうです。

この記事をシェアする

関連記事