Redisのパフォーマンスボトルネックトップ5とその修正方法

Redisデプロイメントのピークパフォーマンスを引き出すための、一般的なボトルネックに関する必須ガイドです。遅いO(N)コマンド、過剰なネットワークラウンドトリップ、メモリプレッシャーと非効率なエビクションポリシー、永続化のオーバーヘッド、CPUバウンドな操作などの問題を特定し解決する方法を学びます。この記事では、パイプライン化や`SCAN`の活用からデータ構造や永続化の最適化まで、実践的な手順、具体例、ベストプラクティスを提供し、Redisインスタンスがキャッシング、メッセージング、データストレージのあらゆるニーズに対して高速で信頼性の高い状態を維持できるようにします。

Redisのパフォーマンスボトルネックトップ5とその修正方法

Redisのパフォーマンス問題は、通常、あることを思い出すまでは謎めいて見えます。Redisは高速ですが、作業から免除されているわけではありません。100万個のキーを走査するコマンドは、依然として100万個のキーを走査します。ネットワークラウンドトリップごとに1つのコマンドを送信するクライアントは、依然としてすべてのラウンドトリップに対してコストを支払います。メモリが不足したサーバーは、設定に応じて、エビクション、スワップ、書き込み拒否、またはダウンを余儀なくされます。

Redisが遅くなった場合、ランダムな設定を変更することから始めないでください。証拠から始めてください:

redis-cli INFO
redis-cli SLOWLOG GET 20
redis-cli LATENCY DOCTOR
redis-cli INFO commandstats
redis-cli INFO memory

これらのコマンドは通常、5つのボトルネックのいずれかを指し示します:遅いコマンド、ネットワークラウンドトリップ、メモリプレッシャー、永続化のオーバーヘッド、またはCPU飽和です。

1. 大規模データに対する遅いコマンド

Redisには多くの小さな定数時間操作がありますが、すべてのコマンドが小さいわけではありません。KEYS、大規模なLRANGESMEMBERSHGETALL、巨大な範囲のZRANGESORT、および長いLuaスクリプトなどのコマンドは、実行中に他のクライアントをブロックする可能性があります。

典型的なインシデントは、クリーンアップまたはデバッグコマンドから始まります:

KEYS *

小規模な開発インスタンスでは、すぐに戻ります。数百万のキーを持つ本番キースペースでは、アプリケーションリクエストが積み重なるのに十分な時間、サーバーを停止させる可能性があります。同じパターンが、「ユーザーごとにいくつかのフィールド」として始まり、静かに巨大なオブジェクトになったハッシュでも発生します。

証拠を見つける:

redis-cli SLOWLOG GET 20
redis-cli INFO commandstats
redis-cli LATENCY LATEST

SLOWLOGは、設定されたしきい値を超えたコマンドを記録します。INFO commandstatsは、コマンドごとの呼び出し回数と累積時間を示します。1つのコマンドが時間を支配している場合は、そこから始めてください。

アクセスパターンを修正する:

redis-cli --scan --pattern 'user:*'

キースペースの反復にはKEYSの代わりにSCANを使用してください。大きなハッシュ、セット、ソート済みセットにはHSCANSSCANZSCANを使用してください。構造全体ではなく、ページまたは範囲を取得します:

LRANGE feed:user:42 0 49
ZRANGE leaderboard 0 99 WITHSCORES

オブジェクトが大きくなりすぎた場合は、アプリケーションがそれを読み取る方法に基づいて分割します。数千の無関係なフィールドを持つ単一のuser:42ハッシュは、書き込みには便利かもしれませんが、プロファイル設定のみを必要とする読み取りには負担になります。user:42:profileuser:42:prefsuser:42:countersなどの個別のキーは、リクエストごとに触れるデータ量を減らすことができます。

削除には、値が大きい可能性がある場合はUNLINKを優先します:

UNLINK old:large:set

UNLINKはキースペースからキーを削除し、メモリを非同期に解放します。大きな値に対してはDELよりも安全ですが、バルククリーンアップには依然としてスロットリングが必要です。

2. ネットワークラウンドトリップが多すぎる

Redisはマイクロ秒でコマンドを処理するかもしれませんが、アプリケーションはネットワークの待機にミリ秒を費やしています。リクエストパスが50の連続したRedisコマンドを送信する場合、Redis自体が正常であっても、ネットワークが合計時間を支配する可能性があります。

これは、次のようなコードで一般的です:

for user_id in user_ids:
    profile = redis.get(f"user:{user_id}:profile")

GETは、次の開始前に独自の応答を待ちます。ネットワークを介すると、これは高コストです。

パイプラインを使用する:

pipe = redis.pipeline(transaction=False)
for user_id in user_ids:
    pipe.get(f"user:{user_id}:profile")
profiles = pipe.execute()

パイプライン化は、各応答を個別に待たずに複数のコマンドを送信します。Redisはコマンドを順番に実行しますが、クライアントはすべてのコマンドに対してラウンドトリップを支払うことを回避します。

適切な場合はマルチキーコマンドを使用する:

MGET user:1:profile user:2:profile user:3:profile

すべてのリクエストを1つの巨大なパイプラインにしないでください。大きなパイプラインはメモリ使用量を増やし、応答のバーストを引き起こす可能性があります。明らかなラウンドトリップの無駄を取り除くのに十分なバッチ処理を行い、その後測定します。

読み取りが多いパスでは、アプリケーションがリクエストの期間中に一度フェッチして再利用できる値をRedisに繰り返し要求していないかも確認してください。小さなローカルリクエストキャッシュは、Redisを変更せずに偶発的な重複読み取りを削除できます。

3. メモリプレッシャーとエビクションチャーン

Redisはメモリ中心です。メモリが逼迫すると、パフォーマンスはいくつかの点で悪化します:エビクションはCPUコストがかかり、noevictionでは書き込みが失敗する可能性があり、永続化フォークが困難になり、レプリカが遅延し、ホストが誤設定または過負荷の場合、オペレーティングシステムがスワップする可能性があります。

メモリを確認する:

redis-cli INFO memory
redis-cli INFO stats | grep evicted_keys
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy

重要な兆候:

  • used_memorymaxmemoryに近い。
  • evicted_keysが急速に増加している。
  • ホストがスワップしている。
  • 大きなキーが予想よりも多くのメモリを消費している。
  • 期限切れのキャッシュキーに実際にはTTLがない。

大きなキーを注意深く見つけてください。ピークトラフィック時に広範な高コストコマンドを実行しないでください。--bigkeysを使用したサンプリングが役立ちます:

redis-cli --bigkeys

キャッシュの場合は、メモリ制限とデータに一致するエビクションポリシーを設定します:

maxmemory 4gb
maxmemory-policy allkeys-lru

すべてのキーがキャッシュエントリである場合、allkeys-lruまたはallkeys-lfuが適切な場合があります。volatile-lruはTTLを持つキーのみをエビクションします。これは、永続キーがキャッシュキーとインスタンスを共有する場合に便利です。noevictionは、Redisをプライマリデータストアとして使用する場合に適切であることが多く、永続的なデータを静かにエビクションすることはエラーを返すよりも悪いためです。

書き込み時にTTLを設定します:

SET cache:product:123 "$json" EX 300

セッションストアの場合は、意図的に行ってください。有効期限のないセッションキーは通常バグです。レートリミッターの場合、カウンターはウィンドウで期限切れになる必要があります。ストリームの場合は、古いエントリをトリミングします。Redisのメモリリークは、多くの場合、ライフサイクルを受け取らなかったアプリケーションデータです。

また、重要な場合はキーと値のオーバーヘッドを削減します。数千の小さなキーは、予想よりも多くのメタデータコストがかかる可能性があります。コンパクトなハッシュが多数の個別キーよりも優れている場合もあります。読み取りが1つのフィールドのみを必要とするため、逆の場合もあります。実際のアクセスパターンで測定し、1つの形状が常に最適であると想定しないでください。

4. 永続化とディスクI/Oの停止

永続化はデータを保護しますが、理解する必要のあるフォークとディスクの動作を導入します。RDBスナップショットとAOF書き換えは通常バックグラウンド操作ですが、フォーク時間、コピーオンライトメモリプレッシャー、ディスクI/Oを介してレイテンシを引き起こす可能性があります。

永続化の状態を確認する:

redis-cli INFO persistence
redis-cli LATENCY LATEST
iostat -xz 1

バックグラウンド保存の失敗、長いフォーク時間、AOF書き換えアクティビティ、ディスク飽和を探します。レイテンシスパイクがBGSAVEまたはBGREWRITEAOFと一致する場合、永続化のチューニングは優先リストに入れる必要があります。

AOFの場合、主な耐久性/パフォーマンス設定は次のとおりです:

appendfsync everysec

everysecは通常のバランスの取れた選択です。alwaysはすべての書き込みを同期し、非常に遅くなる可能性があります。noは同期をオペレーティングシステムに任せ、クラッシュ時のデータ損失リスクが高くなります。

RDBの場合、それが意図的でない限り、ビジーな書き込みワークロードで絶えず起動するスナップショットルールを避けてください:

save 900 1
save 300 10
save 60 10000

これらのデフォルトの例は、すべてのワークロードに自動的に適切であるとは限りません。使い捨てキャッシュとして使用される高書き込みRedisは、永続化をまったく必要としない場合があります。ジョブキューまたはセッションストアとして使用されるRedisインスタンスはおそらく必要ですが、許容可能な損失ウィンドウを明確にする必要があります。

永続化がアプリケーショントラフィックと競合する場合は、以下を検討してください:

  • より高速なローカルSSDストレージ。
  • Redisの永続化を他のディスク負荷の高いサービスから分離する。
  • プライマリがその設計に耐えられる場合、レプリカで永続化を実行する。
  • データセットサイズをホストが快適にフォークできる範囲内に保つ。
  • Redisがバックグラウンド保存に推奨するLinux vm.overcommit_memory=1を設定する。

データが本当に使い捨てでない限り、「パフォーマンスを修正する」ために盲目的に永続化を無効にしないでください。グラフは良く見えるかもしれませんが、再起動をデータ損失に変える可能性があります。

5. CPU飽和とシングルスレッドコマンド実行

Redisのコマンド実行は大部分がシングルスレッドですが、最新のRedisは一部のI/Oおよびバックグラウンド作業に追加のスレッドを使用します。1つのコアがRedisによって占有されている場合、同じインスタンスにアイドルコアを追加しても、ホットコマンドパスには役立たない可能性があります。

ホストとRedisコマンドミックスを確認する:

top -H -p $(pgrep redis-server)
redis-cli INFO commandstats
redis-cli SLOWLOG GET 20
redis-cli INFO clients

一般的なCPUの原因:

  • 大規模なセット、ソート済みセット、リスト、またはハッシュ操作。
  • 重いLuaスクリプト。
  • アプリケーションでの圧縮またはシリアル化のオーバーヘッドにより、予想よりも大きな値が発生する。
  • 非常に高いPub/Subファンアウト。
  • メモリプレッシャー下での高コストなエビクション。
  • 常に再接続したり、小さなコマンドを発行したりする接続が多すぎる。

作業を減らす、作業を分割する、または作業を分散することにより、CPUを修正します。

コマンドとデータ形状を変更して作業を削減します。50個のアイテムのみが必要な場合は、5,000個をフェッチしないでください。すべてのリクエストが500 KBのJSON blobを解析して1つのフラグを読み取る場合は、そのフラグをより小さなキーまたはフィールドに分割します。

インクリメンタルスキャンを使用して長いループをクライアントに移動することにより、作業を分割します:

HSCAN big:hash 0 COUNT 100

読み取り用のレプリカまたはシャーディング用のRedis Clusterを使用して作業を分散します。レプリカは読み取りが多いトラフィックに役立ちますが、プライマリでの書き込みを安くするわけではありません。Redis Clusterはキーをプライマリ間で分散し、CPUとメモリの総容量を増やすことができますが、運用の複雑さとキースロットの制約も追加します。

Pub/Subの場合は、出力バッファとファンアウトを監視します:

redis-cli PUBSUB NUMSUB events:updates
redis-cli CLIENT LIST

遅いサブスクライバーはメモリプレッシャーに変わる可能性があります。数千のサブスクライバーは、1つのパブリッシュを大量のネットワーク出力に変える可能性があります。Pub/Subが重い場合は、別のRedisインスタンスに分離することを検討してください。

高速トリアージワークフロー

Redisのレイテンシが上昇した場合、これらのチェックを順番に実行します:

redis-cli --latency
redis-cli SLOWLOG GET 10
redis-cli LATENCY DOCTOR
redis-cli INFO memory
redis-cli INFO clients
redis-cli INFO persistence
redis-cli INFO commandstats

次に質問します:

  • 遅いコマンドが現れましたか?
  • メモリがmaxmemoryに達したか、エビクションを開始しましたか?
  • 永続化が保存または書き換えを開始しましたか?
  • 接続されたクライアントまたはブロックされたクライアントが急増しましたか?
  • 1つのコマンドの呼び出し回数または時間が爆発しましたか?
  • アプリケーションのデプロイによりRedisのアクセスパターンが変更されましたか?

ほとんどのRedisボトルネックは、1つの魔法の設定で解決されるわけではありません。それらは、ワークロードをより小さく、よりインクリメンタルに、よりバッチ処理され、より適切に分離することで解決されます。最高のRedisデプロイメントは退屈です:キーは期限が来たら期限切れになり、大きな操作はページングされ、クライアントは賢明にパイプライン化し、永続化設定はデータの価値に一致し、監視はユーザーが感じる前にトレンドをキャッチします。

修正の前後に測定すべきこと

パフォーマンスの修正は、重要なワークロードのグラフが変化した場合にのみ現実のものとなります。コードまたは設定を変更する前に、小さなベースラインをキャプチャします:

redis-cli INFO stats
redis-cli INFO commandstats
redis-cli INFO memory
redis-cli INFO persistence
redis-cli SLOWLOG GET 20

システムレベルでは、CPU、ディスク、メモリ、スワップ、ネットワークスループットをキャプチャします。Redisがコンテナで実行されている場合は、コンテナの制限とホストのプレッシャーの両方を確認します。Redisプロセスは、ホストが別のサービスからのディスクまたはCPUプレッシャー下にある場合でも、自身のメモリビュー内では正常に見えることがあります。

変更後、比較します:

  • Redisバックエンドリクエストのp50、p95、p99アプリケーションレイテンシ。
  • リクエストレイテンシだけでなく、Redisコマンドレイテンシ。
  • コマンド別のSlowlogエントリ。
  • エビクションレートとメモリヘッドルーム。
  • 接続されたクライアントと拒否された接続。
  • 永続化フォーク時間とAOF/RDBステータス。
  • レプリカが読み取りを提供するか耐久性を保護する場合のレプリカラグ。

痛みを移動するだけの修正には注意してください。たとえば、大きなパイプラインはリクエストレイテンシを削減するかもしれませんが、メモリスパイクを増加させる可能性があります。AOFを無効にすると、ディスクレイテンシがなくなるかもしれませんが、リカバリが弱まります。maxmemoryを増やすと、エビクションが遅れるかもしれませんが、マシンがすでに共有されている場合、ホストを枯渇させる可能性があります。

1つの有用なプラクティスは、変更した正確なRedisパターンを中心に小さなロードテストを作成することです。古いコードが40の連続したGETを行った場合、現実的なペイロードサイズで、連続したGETMGETまたはパイプライン化をテストします。古いコードがHGETALLを使用した場合、リクエストに実際に必要なフィールドに対してHGETをテストします。Redisのチューニングは、一般的な「Redis ops per second」の数値ではなく、実際に実行する形状をベンチマークするとはるかに簡単です。

最後に、ロールバックを簡単にしておきます。Redisのパフォーマンス変更は、多くの場合、アプリケーションコード、クライアント設定、サーバー設定に同時に影響します。可能な場合は1つのことを変更します。複数のことを変更する必要がある場合は、各変更がどの症状を改善することを意図しているかを書き留めてください。これにより、次のエンジニアが誰も削除したがらない謎の設定の山を引き継ぐのを防ぎます。