tencent cloud

Tencent Cloud Distributed Cache (Redis OSS-Compatible)

ドキュメントTencent Cloud Distributed Cache (Redis OSS-Compatible)ユースケースホットキーとラージキーの調査・最適化実践

ホットキーとラージキーの調査・最適化実践

ダウンロード
フォーカスモード
フォントサイズ
最終更新日: 2026-10-20 19:07:18
AI翻訳・品質チェック済み
本文では、クラウド分散キャッシュデータベース(Redis互換性)における、大KeyとホットKeyの定義、識別方法、ソリューションおよび予防策について説明します。

業務シナリオ

大KeyとホットKeyはキャッシュ型ビジネスで最も一般的な2種類のリスクであり、日常の運用保守において、ビジネス側は通常以下の状況に直面します:
問題を事前に発見することは困難です: インスタンスの各監視指標は正常ですが、ある業務ピーク時に突然大規模なアクセスタイムアウトが発生しました。
障害の根本原因を特定することは困難です: インスタンス全体のQPSは上限に達していませんが、特定のシャードではすでにCPUが飽和状態にあるか、接続数が枯渇しています。
拡張では問題を解決できません: シャード数を増やした後も、負荷は依然として既存ノードに集中し、アクセス遅延は改善されません。
上記の現象の多くは、単一のKeyのサイズまたはアクセス量がインスタンスの正常な水準から逸脱していることに起因します。本記事では、これら2種類の問題の判定方法、調査手段、および最適化ソリューションについて説明し、事前予防、事中発見、事後対処の各段階でクローズドループを形成できるようにします。

基本定義

ラージキーの定義

大Keyとは、Redis内の特定のKeyに対応するValueが大きすぎるか、コレクションのメンバーが多すぎることを指し、大量のメモリ領域を占有します。本質的には大きなValueの問題です。運用保守段階では、下表のしきい値に基づいてインスタンス内に大Keyが存在するかどうかを判定できます。
データ型
判定基準
String
単一のValueが10MBを超える
Hash
要素数が10,000を超えるか、または総容量が100MBを超える
List
要素数が10,000を超えるか、または総容量が100MBを超える
Set
要素数が10,000を超えるか、または総容量が100MBを超える
ZSet
要素数が10,000を超えるか、または総容量が100MBを超える
注意すべき点として、上表は既に存在する大Keyを識別するためのものです。設計段階では、より厳格な推奨制御値(Stringは10KB以下、コレクション型の要素数は5,000以下)に従う必要があります。詳細については、KeyとValueの設計原則をご参照ください。実際の判定では、ビジネスシナリオとインスタンス仕様に応じて調整する必要があります。クラスタアーキテクチャでは、各シャードにおけるKeyの分布が均一かどうかにも注意する必要があります。

ホットキーの定義

ホットKeyとは、一定期間内に特定のKeyへのアクセス量が他のKeyを大幅に上回り、大量のQPSまたは帯域幅が特定のRedisインスタンスまたはシャードに集中することを指します。
一般的な症状:
2,000個のFieldを含むHash型のKeyが、毎秒大量のHGETALL操作リクエストを受信します。
10,000個のメンバーを含むZSet型のKeyが、毎秒大量のZRANGE操作リクエストを受信します。
判定方法:
ホットKeyには絶対的なQPS数値基準はありません。同じ3,000 QPSの単一Keyでも、総QPSが5,000のインスタンスでは60%を占め、明らかなホットKeyとなります。一方、総QPSが50万のインスタンスでは1%未満となり、正常なアクセスに該当します。したがって、判定の鍵は単一Keyの絶対的なアクセス量ではなく、インスタンス全体に対する乖離度合いです。下表に、判定に使用できる3つの次元を示します。
判定ディメンション
判定方法
アクセス集中度
DBbrainのホットKey分析によりアクセス量ランキングを取得し、ランキング首位のKeyとその後のKeyの間に数量級の差があるかどうかを確認します。
相対比率
単一Keyのリクエスト数がインスタンスの総リクエスト数に占める割合、および単一Keyのトラフィックがインスタンスの出力帯域幅に占める割合を計算します。割合が高いほどリスクが大きくなります。
リソース余裕
インスタンス仕様に基づき、該当Keyが存在するノードのCPU、帯域幅、接続数が上限に近づいているかどうかを評価します。
見ての通り、3つの次元はいずれも「相対的な水準」を基準とし、固定数値を基準とはしていません。具体的なアラートしきい値はビジネス側で独自に決定する必要があります。ビジネスが安定している期間に、まず一定期間のアクセス分布データを収集してビジネスベースラインとし、その後、ベースラインからの大幅な乖離を判定基準とすることをお勧めします。同じしきい値は、異なるインスタンス仕様や異なるビジネスモデルでは汎用的ではありません。

現象と影響

ラージキーの影響

問題
具体的な現象
メモリ使用量が不均一です。
クラスタアーキテクチャにおいて、あるシャードのメモリ使用率が他のシャードを大幅に上回り、maxmemory の上限を引き起こす可能性があり、重要な Key が追い出され、さらには OOM に至る可能性があります。
リクエストのブロックによるタイムアウト
コマンド実行はシングルスレッドモデルを採用しており、大きなKeyの操作には時間がかかります(例:DEL、HGETALL)。その間、後続のリクエストはキューで待機するしかありません。
同期中断/主従切替
大きなKeyの削除またはRENAME操作は、マスターノードを長時間ブロックし、マスタースレーブ同期の中断やフェイルオーバーを引き起こす可能性があります。
ネットワーク輻輳
1MBの大きなKeyに毎秒1,000回アクセスすると、約1GB/sのトラフィックが発生し、インスタンスの帯域幅を占有する可能性があります。

ホットキーの影響

問題
具体的な現象
CPU使用率が継続的に高くなっています。
ホットリクエストが単一ノードに集中し、当該ノードのCPUがボトルネックとなり、サービス全体の性能が低下します。
アクセス傾斜
ホットKeyが存在するシャードは他のシャードをはるかに超えるアクセス圧力を受け、接続数の枯渇やノード異常が発生する可能性があります。アクセスが単一のKeyに集中しているため、シャードを水平拡張しても当該Keyの圧力を分散することはできません。
キャッシュブレイクダウン
ホットKeyの有効期限切れ、またはノード異常が発生した瞬間、高度に集中したトラフィックは直接バックエンドデータベースに到達します。データベースが受け止められない場合、連鎖障害を引き起こす可能性があります。

原因分析

ラージキー発生原因

1. Key-Valueの設定が不適切: String型を使用して大容量のバイナリファイルや大型のJSON/XMLを保存すると、Valueが大きくなりすぎます。
2. 無効データが適時にクリーンアップされていない: List、Setなどの型のメンバーが継続的に蓄積されますが、期限切れや無効なデータが定期的にクリーンアップされていません。
3. ビジネス分析が不正確: リリース前にデータ規模を十分に評価せず、Keyを適切に分割しなかったため、単一のKeyに含まれるメンバー数が多すぎる状態になりました。
4. コンシューマー側コードの異常: メッセージキュー(List)のコンシューマー側に障害が発生し、データが増加する一方で減少しません。

ホットキー発生原因

予期せぬアクセス量の急増:
ECシナリオ:突発的なヒット商品、タイムセールイベント。
コンテンツシナリオ:アクセス量が急増したホットニュース。
ライブ配信シナリオ:配信ルームでの大量の「いいね」、弾幕インタラクション。
ゲームシナリオ:特定エリアに多数のプレイヤーが集中してインタラクションを行います。

調査方法

方法1:DBbrainコンソールによる診断(推奨)

Tencent Cloud Distributed CacheはDBbrainの診断最適化機能に接続されており、インスタンス内の大KeyとホットKeyを迅速に検出するのに役立ちます:
大Keyの調査: メモリ分析を参照してください。
ホットKeyの調査: ホットKey分析を参照してください。

方法2:コマンドラインツールによる調査

コンソールを使用しにくいシーンでは、Redis付属のコマンドラインツールで迅速に調査できます。パスワードがプロセスリストやShell履歴に表示されるのを避けるため、-aパラメータを使用するのではなく、REDISCLI_AUTH環境変数でパスワードを渡すことをお勧めします:
# 環境変数でパスワードを渡し、平文の露出を避けてください。
export REDISCLI_AUTH='<パスワード>'

# 大Keyの調査:データ型ごとに要素数が最も多いKeyをそれぞれ出力します。
redis-cli -h <インスタンスアドレス> -p <ポート> --bigkeys

# ホットKeyの調査:LFUアクセス頻度統計に基づいて高頻度Keyを出力します。
redis-cli -h <インスタンスアドレス> -p <ポート> --hotkeys
2つのコマンドはいずれもSCANカーソルに基づいてすべてのKeyを走査し、メインスレッドを長時間ブロックすることはありません。ただし、データ量が多い場合は追加のリクエストオーバーヘッドが発生するため、業務の低ピーク時に実行することをお勧めします。クラスタアーキテクチャでは、各シャードノードにそれぞれ接続して実行する必要があり、1回の接続では現在のノードのデータのみを統計できます。
2つのコマンドの統計口径の違いに注意する必要があります:--bigkeysが統計するのは要素数(Stringはバイト長)であり、実際のメモリ使用量ではありません。そのため、結果は要素規模のみを反映します。メモリ使用量に基づいて特定する必要がある場合は、方法1のメモリ分析機能を使用してください。--hotkeysはLFU淘汰ポリシーに依存するため、使用するには先にインスタンスの淘汰ポリシーパラメータを調整する必要があります。
説明:
--hotkeysを使用する前に、インスタンスパラメータmaxmemory-policyをallkeys-lfuまたはvolatile-lfuに設定する必要があります。そうしないと、コマンドは直接エラーを報告します。このパラメータはインスタンスのKey追い出し動作を決定します。変更後、既存データの淘汰順序が変わる可能性があるため、業務への影響を評価してから調整してください。

方法3:ビジネスレイヤーでの埋め込みポイント

業務コード内でRedisクライアントをラップし、各Keyのアクセス回数と所要時間を記録してから、スケジュールタスクによって定期的にTopNのホットKeyを出力します。以下はJava + Jedisのサンプルコンポーネントです。
使用時にjedis.get(key)をHotKeyDetector.get(jedis, key)に置き換えて埋め込みを完了し、ScheduledExecutorServiceを通じて60秒ごとにprintTopN(10)を1回呼び出して結果を出力し、reset()を実行してリセットします。その他のクライアント(Lettuce、Redisson)とコマンド(set、hgetなど)は、このコンポーネントを参照して拡張できます。
import redis.clients.jedis.Jedis;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class HotKeyDetector {

// Key → アクセス回数
private static final ConcurrentHashMap<String, AtomicLong> KEY_COUNT = new ConcurrentHashMap<>();
// Key → 累計所要時間(ナノ秒)
private static final ConcurrentHashMap<String, AtomicLong> KEY_TIME = new ConcurrentHashMap<>();

/**
* Jedis get操作をラップし、各Keyのアクセス回数と所要時間を自動的に統計します。
*/
public static String get(Jedis jedis, String key) {
long start = System.nanoTime();
try {
return jedis.get(key);
} finally {
record(key, System.nanoTime() - start);
}
}

private static void record(String key, long nanos) {
KEY_COUNT.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();
KEY_TIME.computeIfAbsent(key, k -> new AtomicLong()).addAndGet(nanos);
}

/**
* TopNのホットKeyを出力します。スケジュールタスク(例:60秒ごと)による呼び出しを推奨します。
*/
public static void printTopN(int n) {
System.out.println("===== Top " + n + " Hot Keys =====");
KEY_COUNT.entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get()))
.limit(n)
.forEach(e -> {
String key = e.getKey();
long count = e.getValue().get();
long totalNs = KEY_TIME.getOrDefault(key, new AtomicLong()).get();
double avgMs = count > 0 ? (totalNs / 1_000_000.0) / count : 0;
System.out.printf("Key: %s | Count: %d | Avg: %.2f ms%n",
key, count, avgMs);
});
}

/**
* 統計をリセットします(毎回出力後に呼び出し、データの蓄積を防ぎます)。
*/
public static void reset() {
KEY_COUNT.clear();
KEY_TIME.clear();
}
}

調査ツールの比較

上述の3つの方法は、カバー範囲、使用コスト、精度においてそれぞれ重点が異なります。以下の表に、それぞれの適用シーンと制限事項をまとめます。
方法
適用シナリオ
強み
デメリット
DBbrainコンソール
日常的な運用保守、定期的な調査
可視化、履歴データ分析に対応、業務コードの変更が不要です。
コンソールへのアクセス権限が必要です。
redis-cli --bigkeys
コマンドライン環境で大容量Keyを迅速に自己点検します。
追加のデプロイは不要で、すぐに利用できます。
要素数のみをカウントし、メモリ使用量は反映されません。
redis-cli --hotkeys
コマンドライン環境でホットKeyを迅速に自己点検します。
追加のデプロイは不要で、すぐに利用できます。
LFU淘汰ポリシーに依存するため、インスタンスパラメータを調整する必要があります。
業務層への埋め込み
正確な位置特定、長期的かつ継続的な監視
統計ディメンションはカスタマイズ可能で、ビジネスコンテキストと関連付けることができます。
コードの改修が必要であり、若干のパフォーマンスオーバーヘッドが生じます。
概括すると、日常のトラブルシューティングでは優先的にDBbrainを使用します。DBbrainは大KeyとホットKeyの両方をカバーし、業務の改修も不要です。一時的な緊急対応やコンソール権限がない場合は、コマンドラインによる自己チェックを使用します。ホットKeyの分布を長期的に監視する必要がある場合や、業務ディメンションの分析と関連付ける場合は、業務層の埋め込みを導入します。

解決方法

ラージキー最適化

案1:段階的な無効データのクリーンアップ

List、Set、Hashなどのコレクション型に大量の期限切れまたは無効なメンバーが蓄積しているシーンに適用されます。メインスレッドをブロックするDELコマンドの直接使用を避け、段階的なクリーンアップに切り替えます:
# Hash:HSCAN + HDELで期限切れメンバーを段階的に削除し、クリーンアップが完了するまでループ実行します。
HSCAN key cursor COUNT 100
HDEL key field1 field2 ...

# List:LTRIMでバッチ処理によりトリミングし、先頭の1,000件のみを保持します。
LTRIM key 0 999

# Redis 4.0+:UNLINKで大Key全体を非同期に削除し、ブロッキングを回避します。
UNLINK large_key

案2:ラージキーのValue圧縮

圧縮可能なテキストデータ(JSON、XML、長文テキスト)は、書き込み前に圧縮し、読み取り後に解凍します:
// 書き込み:圧縮後にバイナリ形式で保存します。
// gzipCompress / gzipDecompressは、java.util.zip.GZIPOutputStreamに基づいて独自に実装できます。
byte[] compressed = gzipCompress(rawJsonString);
jedis.set(key.getBytes(), compressed);

// 読み取り:バイナリデータを取り出した後、解凍して復元します。
byte[] data = jedis.get(key.getBytes());
String rawJson = gzipDecompress(data);
この案を採用する前に、3つのコストを評価する必要があります。圧縮は追加のCPUを消費し、圧縮効果はデータの重複度に依存します。フィールド構造が重複するJSON、XMLテキストでは効果が顕著ですが、圧縮済みの画像、動画などのバイナリデータではほとんど効果がありません。圧縮後のValueはバイナリデータとなるため、バイト配列形式の読み書きインターフェースを使用する必要があり、Redisコマンドで直接内容を確認できなくなるため、トラブルシューティングの難易度が高まります。圧縮後もValueがしきい値を超える場合は、データ規模が単一Keyの合理的な許容範囲を超えていることを示しており、分割案と組み合わせてさらに対応する必要があります。

案3:ラージキーの分割

単一の大Keyを複数の小Keyに分割し、業務ディメンションに基づいてシャーディングして保存します:
# 分割前
user:profile:1001 → 2MB JSON

# 分割後(フィールドごとにグループ化)
user:profile:1001:basic → 基本情報
user:profile:1001:extend → 拡張情報
user:profile:1001:settings → 設定情報
# Hash分割:Fieldにハッシュを適用して剰余を求め、N個の子Hashに分散します。
# シャード番号 = hash(field) % N、書き込みと読み取りは同じルールで位置を特定します。
user:cart:1001:shard_0 → 剰余結果が0のFieldを格納します。
user:cart:1001:shard_1 → 剰余結果が1のFieldを格納します。
...
分割粒度は業務アクセスパターンに基づいて設計する必要があります。分割後に単一の業務操作で複数回のネットワーク往復が必要になり、かえって遅延が増加することを避けるためです。
クラスタアーキテクチャではハッシュスロットの分布も考慮する必要があります。分割によって生成された複数の子Keyは、ハッシュスロットが異なるため異なるシャードに配置されます。この場合、MGETなどの複数Keyコマンドはスロットをまたいで実行できない可能性があります。一括読み取りが必要な場合は、ハッシュタグを使用して子Keyを同じシャードに固定する必要があります。例えば、{user:profile:1001}:basic、{user:profile:1001}:extendのように、中括弧内の内容が同じであれば同じスロットに配置されることが保証されます。ただし、ハッシュタグによってこれらのKeyが単一のシャードに集中するため、そのKeyが同時にホットKeyである場合、アクセスの偏りがかえって悪化します。したがって、一括読み取りの要件とシャード分散の要件は、実際のシナリオに基づいてバランスを取る必要があります。

案4:非適合データの移行

String型を大容量ファイル(画像、動画BLOBなど)の保存に使用する場合は、データをCOSに移行し、RedisにはアクセスURLまたはメタデータのみを保持することをお勧めします。
# 転送前
file:report:2025-q4 → 15MB PDF BLOB

# 転送後
file:report:2025-q4:url → "https://bucket.cos.region.myqcloud.com/report-2025-q4.pdf"
file:report:2025-q4:meta → {"size": 15728640, "md5": "abc123..."}

ホットキー最適化

案1:読み書き分離

ホットKeyの負荷が読み取りリクエストによるものである場合、読み書き分離アーキテクチャを有効にし、読み取り専用ノードを追加して読み取りリクエストを読み取り専用レプリカに分散します。これは読み取りが多く書き込みが少ないシナリオに適しています。なお、読み取り専用レプリカのデータはマスタースレーブ同期によって取得されるため、一定の遅延が存在します。データ整合性に対する要件が非常に高い読み取りリクエストは、マスターノードにルーティングする必要があります。具体的な操作については、読み書き分離を参照してください。

案2:ローカルキャッシュ

アプリケーション層でローカルキャッシュ(Caffeine、Guavaなど)を使用してホットデータをキャッシュし、大部分のリクエストはアプリケーションプロセス内で直接返されるため、Redisにアクセスする必要はありません。
// Caffeine ローカルキャッシュのホットデータ
private static final LoadingCache<String, String> HOT_KEY_CACHE = Caffeine.newBuilder()
.maximumSize(1000) // 最大キャッシュエントリ数
.expireAfterWrite(50, TimeUnit.MILLISECONDS) // 有効期限、データ不整合の期間を制御します。
.build(key -> {
// キャッシュミス時にRedisへアクセスし、接続プールから接続を取得します。
try (Jedis jedis = JEDIS_POOL.getResource()) {
return jedis.get(key);
}
});
この方式は読み取り集中型のホットKeyに適しており、かつビジネスが短時間のデータ不整合を許容できる場合に有効です。ここで、有効期限はデータ不整合ウィンドウの長さを決定し、ビジネスの整合性に対する許容度に応じて設定する必要があります。時間が短いほど整合性は高まりますが、Redisへのアクセス頻度が高くなり、負荷軽減効果は弱まります。実際の値は両者のバランスを考慮して決定する必要があり、上記コードの50msはあくまで例です。

案3:マルチレプリカ分散

ホットKeyを内容が同一の複数のレプリカにコピーし、異なるシャードに分散させ、読み取り時にランダムに1つのレプリカを選択します:
# 元のホットKey
hot:product:1001

# 複数レプリカへの分散
hot:product:1001:0 → レプリカ0
hot:product:1001:1 → レプリカ1
hot:product:1001:2 → レプリカ2
// 読み取り時にランダムに1つのレプリカを選択し、リクエストを異なるシャードに分散させます。
int index = ThreadLocalRandom.current().nextInt(REPLICA_COUNT);
String key = "hot:product:" + productId + ":" + index;
try (Jedis jedis = JEDIS_POOL.getResource()) {
String value = jedis.get(key);
}
説明:
データ更新時には全レプリカへ同時に書き込む必要があります。いずれかのレプリカへの書き込みが失敗すると、読み取り結果の不整合が生じます。複数レプリカの読み書きロジックと整合性保証はすべてアプリケーション層で独自に維持する必要があるため、この方式はホットスポット突発時の一時的な応急手段として推奨し、長期的なアーキテクチャとしては推奨しません。

案4:多層キャッシュアーキテクチャ

超高同時実行シナリオに対して、多層キャッシュ体系を構築します:
ユーザーリクエスト → Nginxローカルキャッシュ → アプリケーションローカルキャッシュ(Caffeine) → Redis → データベース
大部分のホットリクエストは最初の2層のキャッシュで吸収され、Redisは少量のオリジンアクセス流量のみを処理します。各層のキャッシュ有効期限は層ごとに短く設定し、ユーザーに近い層ほど有効期限を短くすることで、バックエンドの負荷を軽減しながらデータ不整合の範囲を抑えます。

予防策

大KeyとホットKeyの最適な対策方法は事前予防であり、ビジネス設計段階でリスクを回避します:

設計段階

1. Key分割設計: リリース前にデータ規模と増加速度を評価し、大規模コレクションへ成長する可能性があるKeyに対して事前にシャーディング戦略を策定します。
2. 有効期限の設定: すべてのKeyに合理的な有効期限を設定し、データの無制限な蓄積を防ぐ必要があります。
3. データ構造の選定: データ特性に基づいて適切なデータ型を選択します:
Fieldごとに個別に有効期限を設定する必要がある説明情報 → Hash + 定期クリーンアップ
順序付けと均等な分散が必要なキー値 → ZSet(ページング読み取りでZRANGE 0 -1を代替)
多数のメンバーを必要とするリレーショナル記述情報 → 複数の小規模Setへの分割を検討します

運用段階

1. 監視アラート: メモリ使用率、帯域幅使用率、Key総数、CPU使用率などの指標に対してアラートを設定します。しきい値は、まず業務安定期の指標水準をベースラインとして収集し、そのベースラインに処理時間の余裕を上乗せしてアラートラインを決定することを推奨します。例えば、メモリ使用率のアラートラインは、アラート発生から上限到達までに拡張またはクリーンアップを完了する十分な時間を確保できるように設定します。アラート受信後は本文のトラブルシューティングフローに従って処理します。具体的な操作については監視機能(5秒粒度)およびアラート設定を参照してください。
2. 定期巡回: 定期的にDBbrainまたはコマンドラインツールを使用して大KeyとホットKeyが存在するかどうかを確認し、問題が業務に影響を与える前に対処します。
3. 容量計画: 定期的に業務の成長傾向を評価し、事前に拡張または分割を行います。

コーディング規約

1. KEYS *、SMEMBERS、HGETALLなどの全量走査コマンドの使用を禁止し、SCAN、SSCAN、HSCANを使用してバッチで取得してください。
2. バッチ操作ではPipelineまたはMGET/MSETを優先的に使用し、ループによる単一コマンドの実行を避けてください。
3. 大Keyの削除にはUNLINKを使用して非同期で削除します(Redis 4.0+)。

ヘルプとサポート

この記事はお役に立ちましたか?

フィードバック