Amazon ElastiCache for Valkey 9.1の「128バイト未満の文字列で最大20%削減」を検証してみた
こんにちは。サービス開発部の武田です。
Amazon ElastiCacheが2026年6月23日にValkey 9.1へ対応しました。告知には「128バイト未満の文字列のメモリ使用量を最大20%削減する」とあります。
この「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点です。
- 閾値が64バイトから128バイトに倍増した
- 埋め込み時に
ptrフィールドを使わなくなるので、その8バイトを再利用する
合わせて、同じキー長・TTLなら、9.1では9.0より最大72バイト長い値まで埋め込みの対象になる計算です。そしてどちらのバージョンも、判定に使うのはキーとTTLと値の合計です。値単体の長さではありません。
境界は本当に「合計」で決まるのか
ソースのとおり合計で決まるなら、キーを長くすると埋め込める値の長さが縮むはずです。キー長を変えながら、OBJECT ENCODINGがembstrのままでいられる最大の値長を二分探索しました。
| キー長 | 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 ENCODINGとMEMORY 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.cのsetGenericCommand()でこの順に処理されます。
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/keyはused_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 ENCODINGとMEMORY USAGEを実際のキーと値の組み合わせで見るのが早いです。キー長を含めて測らないと意味がありません。
まとめ
「128バイト未満の文字列のメモリ使用量を最大20%削減する」という一文の中身を追いかけました。閾値はキーとTTLを含む合計で、オブジェクト単体とクラスター全体では削減率が違い、値の長さによっては逆に増える区間もあります。
とはいえだいぶエッジケースですので、基本的には効率よくなったなという理解でよさそうです。




