キャッシュが限界に達したとき
大規模なペイロードをキャッシュに送信している最中、突然すべてが停止してしまった場面を想像してください。アプリケーションログには、次のような特定のエラーが記録されているはずです。
(error) ERR string exceeds maximum allowed size
このエラーは通常、SET、APPEND、または GETSET 操作中に発生します。これは、保存しようとしている値が Redis の 1 つのキーで処理できるサイズを大幅に超えていることを意味します。
なぜこのエラーが発生するのか?
Redis の文字列には 512メガバイト (MB) という厳格な上限があります。これは redis.conf で変更できる設定ではなく、エンジンにハードコーディングされています。この制限には正当な理由があります。巨大で連続したメモリチャンクを処理するのはコストが高いからです。500MB の文字列に対する操作は、シングルスレッドのイベントループを 200ミリ秒以上ブロックする可能性があり、アプリケーションのレイテンシを急上昇させる原因となります。
512MB は十分な容量に思えるかもしれませんが、予想以上に早くこの上限に達することがあります。主な原因は以下の通りです。
- 巨大な JSON ブロブの保存(例:600MB の商品カタログのエクスポート)。
- 高解像度画像や 50 ページ以上の PDF ファイルなどの Base64 エンコードされたアセットのキャッシュ。
- 数日間にわたって
APPENDコマンドを使用し、ログを 1 つのキーに集約すること。
エラーを解決するための実証済みの戦略
上限を増やすことはできないため、アプリケーション側でのデータの準備方法を変更する必要があります。512MB の制限内に収めるための最も効果的な方法は以下の通りです。
1. データの圧縮
JSON や HTML などのテキストベースのデータは非常に冗長です。圧縮を行うことで、600MB のペイロードを 60MB(90% 削減)まで縮小できることがよくあります。Redis にデータを送信する前に、Gzip や Zstd などの高速なライブラリを使用してください。
以下は、Gzip を使用した Python のクイック例です:
import redis
import gzip
r = redis.Redis(host='localhost', port=6379)
# 上限を超える文字列をシミュレート
large_data = "{" + "a" * 550000000 + "}"
# 保存前にデータを圧縮
compressed_payload = gzip.compress(large_data.encode('utf-8'))
# バイナリブロブを Redis に保存
r.set('my_compressed_key', compressed_payload)
2. データのチャンク分割
圧縮してもデータが大きすぎる場合は、分割する必要があります。文字列を小さなセグメントに分け、:part:1 や :part:2 のようなサフィックスを使用して、複数のキーにまたがって保存します。
この Node.js のロジックはその概念を示しています:
const redis = require('redis');
const client = redis.createClient();
async function saveInChunks(baseKey, largeString) {
const CHUNK_SIZE = 100 * 1024 * 1024; // 安全な 100MB のチャンクを使用
const totalChunks = Math.ceil(largeString.length / CHUNK_SIZE);
for (let i = 0; i < totalChunks; i++) {
const chunk = largeString.substring(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
await client.set(`${baseKey}:part:${i}`, chunk);
}
// 後でいくつのパーツを取得すべきか判断できるようにメタデータを保存
await client.set(`${baseKey}:meta`, JSON.stringify({ total: totalChunks }));
}
3. Redis ハッシュへの移行
巨大な JSON オブジェクトに対して Redis をフラットなファイルシステムのように扱うのはやめましょう。Redis ハッシュ(Hashes)はメモリ効率が大幅に優れています。1 つの巨大な文字列を保存する代わりに、JSON のフィールドを個別のハッシュフィールドに分割します。これにより、500MB のブロブ全体に触れることなく、特定のフィールドのみを更新または取得できるようになります。
# 1 つの巨大なブロブを保存する代わりに:
SET user:data "{ 'bio': '...', 'history': '...', 'settings': '...' }"
# HSET を使用してセグメントを管理可能な状態に保つ:
HSET user:data bio "...text..." history "...text..." settings "...text..."
4. オブジェクトストレージへのオフロード
Redis は速度のために構築されており、大量保存用ではありません。頻繁に 512MB の制限にぶつかる場合は、その特定のタスクに Redis は適していない可能性があります。よりスケーラブルなアーキテクチャには以下が含まれます:
- 大容量ファイルを AWS S3 や Google Cloud Storage にアップロードする。
- 素早い検索のために、署名付き URL またはオブジェクトキーのみを Redis に保存する。
修正の確認
修正を実装したら、Redis CLI で結果を確認してください。STRLEN を使用して生の長さを確認するか、MEMORY USAGE を使用して RAM への実際の負荷を確認します。
# 文字列のバイト長を確認
STRLEN my_large_key
# 内部オーバーヘッドを含む実際の RAM 消費量を確認
MEMORY USAGE my_large_key
もし STRLEN が 536,870,912(512MB に相当するバイト数)を大幅に下回る値を返せば、問題は解決です。
将来のボトルネックを回避する
巨大なキーは「Stop-the-world」イベントを引き起こします。Redis が巨大なキーを処理している間、処理が終わるまで他のすべてのリクエストは無視されます。これはタイムアウトを引き起こし、ユーザーの不満につながります。再発を防ぐために、以下の対策を講じてください:
- ペイロードサイズの監視: 50MB を超える Redis の書き込みに対して、APM(Datadog や New Relic など)でアラートを設定する。
- データの事前検証: 大きな設定ファイルをキャッシュする場合、私はよく ToolCraft の YAML ↔ JSON Converter を使用します。キャッシュに保存する前にファイル形式が崩れていないか確認するのに便利です。ブラウザ上で動作するため、データはプライベートに保たれます。
- クライアント側のガード: 値が 256MB を超える場合にエラーをスローするチェックをコードに追加する。データベース全体をブロックするよりも、アプリケーションレイヤーで早期に失敗させる方が賢明です。

