👌

Redisのアンチパターンを知る

に公開

はじめに

以下でRedisの勉強をしています。ちょろっと触ってみたので、次はアンチパターンの勉強をしておきます。

ちょっと古そうな情報ですが以下のアンチパターンの解説を読んでいきます。

2021年のStack Overflow Developer Surveyで5年連続でRedisが最も人気のあるデータベースであることがわかっています。一方でRedisのデフォルト設定がすべてのユーザに対して最適な設定ではないことを理解しておく必要があります。

1. 単一のシャード/Redisインスタンスで大規模データベースを展開する

単一シャード(Redisインスタンス)上で大規模なデータベースを運用すると、フェイルオーバー、バックアップ、リカバリに時間がかかる可能性があります。
そのため、シャードを推奨サイズ内に保つようにしましょう。

一般的で保守的な目安としては、
25GB または 1 秒あたり 25,000 オペレーション が基準です。

2. Redisインスタンスに直接接続する

クライアント数が多い場合、再接続の集中(リコネクトフラッド) が発生すると、シングルスレッドの Redis プロセスが簡単に圧倒され、フェイルオーバーを引き起こす可能性があります。

そのため、Redis サーバーへのオープン接続数を減らせる適切なツールを使いましょう。

Redis Enterprise の DMC プロキシ は、プロキシとして動作することでキャッシュサーバーへの接続数を減らすことができます。

他にも、Twemproxy のようなサードパーティーツールがあります。これは、高速で軽量なプロキシサーバーで、Redis サーバーへのオープン接続数を減らすために作られました。

Twemproxy は主に、バックエンドのキャッシュサーバーへの接続数を減らす目的で設計されています。これに加え、プロトコルのパイプライニング や シャーディング を組み合わせることで、分散キャッシュアーキテクチャを水平方向にスケールさせることができます。

3.RedisOSSで必要以上のセカンダリシャードを使う

Redis OSSクラスタでは、可用性を高めるためにレプリカ(セカンダリシャード)を配置します。しかし、必要以上にレプリカを増やすことは、コストとパフォーマンスの観点からアンチパターンです。

Redis OSS はシャードベースのクォーラムを使用しています。スプリットブレイン(分断脳)を防ぐために、少なくとも 3 つのデータコピー(マスターシャードに対して 2 つのレプリカシャード) を持つことが推奨されています。つまり、Redis OSS は「奇数のシャード構成(マスター + 2 つのレプリカ)」にすることで、クォーラムの課題を解決しています。

一方、Redis Cloud はノード数を奇数にすることでクォーラムの課題を解決しています。Redis Cloud では、2 つのデータコピーだけでスプリットブレインを回避できるため、よりコスト効率に優れています。さらに、「クォーラム専用ノード(quorum-only node)」 を使うことで、追加のデータノードを増やさずにクラスタ全体を奇数ノード構成にできるため、コストを抑えた運用が可能です。

4. 単一操作の実行

複数の操作を直列に(順番に)実行すると、接続オーバーヘッドが増加します。代わりに、Redis パイプライニング を活用しましょう。パイプライニングとは、各操作の応答を待たずに複数のメッセージをまとめて送信し、後でまとめて応答を処理する仕組み です。

パイプライニングは完全にクライアント側の実装です。これは、ネットワーク遅延が大きい環境におけるレスポンス遅延の問題を解決するためのものです。つまり、コマンド送信と応答受信にネットワークで費やす時間をできるだけ短くすることが重要です。これを効果的に実現するのがバッファリングです。クライアントは、送信前に TCP スタックでコマンドをバッファする場合もあれば、しない場合もあります。一度サーバーに送られると、サーバー側でコマンドを実行し、応答をバッファします。パイプライニングの利点は、プロトコル性能が劇的に向上することです。パイプライニングによる高速化は、ローカルホスト接続で最大約 5 倍、遅いインターネット接続では少なくとも 100 倍の改善が見込めます。

5. TTLのないキャッシング

Redis は主に キー・バリュー型ストア として機能します。これらのキーに タイムアウト値(有効期限) を設定することができます。タイムアウトが切れると、自動的にそのキーは削除されます。さらに、キーの内容を削除・上書きするコマンドを実行すると、そのタイムアウト設定もクリアされます。TTL コマンドを使うと、キーの残り有効時間(秒単位)を取得できます。TTL は、タイムアウトが設定されているキーが あと何秒で消えるか を確認する introspection(内部状態確認)手段を提供します。キーは放置するとどんどん蓄積され、最終的には削除(eviction)される可能性があります。そのため、キャッシュ用のキーには必ず TTL を設定しましょう。

6. 完了しないRedisレプリケーションループ

大きくてアクティブなデータベースを、遅い回線や混雑した回線経由でレプリケーションしようとすると、更新が絶えず発生するため、レプリケーションがいつまでも終わらないことがあります。そのため、スレーブ(レプリカ)やクライアントのバッファサイズを調整して、遅いレプリケーションに対応できるようにしましょう。詳細については、こちらのブログを参照してください。

7. ホットキー

Redis は、アプリの運用データの中心として使われやすく、貴重で頻繁にアクセスされる情報を保持できます。しかし、アクセスが特定の少数のデータに集中すると、ホットキー問題が発生します。

Redis クラスタでは、キーがデータの配置場所を決める役割を持ちます。データはキーをハッシュ化して決定された1 つのプライマリノードに保存されます。したがって、特定のキーに何度もアクセスすると、実際には同じノード(シャード)に繰り返しアクセスしていることになります。

別の言い方をすると、もし 99 ノードのクラスタがあって、1 つのキーに毎秒 100 万リクエストがあった場合、その 100 万リクエストは 1 ノードに集中し、残りの 98 ノードには分散されません。Redis にはホットキーの位置を特定するツールも用意されています。redis-cli コマンドの --hotkeys 引数を使うことで、接続に必要な他の引数と組み合わせてホットキーを調査できます。

可能であれば、この状況を生む開発パターン自体を避けることが最善の防御策です。データを複数のキーに分散して、異なるシャードに格納することで、同じデータに対するアクセス頻度を高めることができます。

要するに、すべてのクライアント操作でアクセスされる特定のキー(ホットキー)を持たないようにするということです。そのため、ハッシュアルゴリズムを使ってホットキーを分散(シャード化)しましょう。

また、ポリシーを LFU(Least Frequently Used) に設定し、redis-cli --hotkeys を実行してホットキーを特定することもできます。

8.KEYSコマンドを使用する

Redis では、KEYS コマンドを使ってすべてのキーに対してパターンマッチ検索を行うことができます。しかし、これは推奨されません。
大量のキーが存在するインスタンスでこれを実行すると、完了までに長時間かかり、Redis インスタンス全体のパフォーマンスが低下します。

リレーショナルデータベースの世界で言えば、WHERE 句なしの SELECT 文(例: SELECT ... FROM)を実行するのと同じです。このような操作を行う場合は慎重に実行し、テナント(利用者)がアプリケーションコード内で KEYS を使わないように対策を講じる必要があります。

代わりに、SCAN コマンドを使いましょう。SCAN は複数回に分けてキーを少しずつ返すため、サーバー全体を一度に占有しません。キー名でキー空間を走査する操作は非常に遅く、計算量は O(N)(N = キー数)になります。そのため、キー空間を繰り返し走査するのではなく、Redis Search を使ってデータ内容に基づいて情報を取得することが有効です。

9. Ephemeral Redis をプライマリデータベースとして使う

Redis はアプリケーションのプライマリストレージエンジンとして使われることがよくあります。キャッシュとして使う場合とは異なり、プライマリデータベースとして使うには追加で 2 つの機能が必要です。

どんなプライマリデータベースも、高可用性が重要です。キャッシュがダウンした場合、通常は「ブラウンアウト状態」(一部機能低下)になりますが、プライマリデータベースがダウンすると、アプリケーション全体が停止します。また、キャッシュがダウンして空の状態で再起動しても大した問題にはなりませんが、プライマリデータベースの場合は大問題です。

Redis はこうした状況に対応できますが、キャッシュとして動かす場合とは異なる設定が必要です。Redis をプライマリデータベースとして使うのは有効ですが、適切な機能を有効化してサポートする必要があります。

Redis オープンソース版では、高可用性を実現するために Redis Sentinel をセットアップする必要があります。Redis Cloud では、この機能はコア機能として組み込まれており、データベース作成時に有効化するだけで利用できます。

永続性については、Redis Cloud もオープンソース版も AOF(Append-Only File)やスナップショット方式を通じて提供しており、
インスタンスを再起動した際に元の状態で復元できます。

10. 文字列にJSONデータを保持する

複数の言語で書かれたマイクロサービスでは、JSON のシリアライズ/デシリアライズ(marshal/unmarshal)が一貫しない場合があります。アトミックに更新するには、アプリケーション側でキーをロックまたはウォッチするロジックが必要になります。さらに、JSON の操作は計算コストが高い処理になることが多いです。そのため、HASH データ構造や RedisJSON を活用しましょう。

11. クエリパターンを考慮せずにテーブルやJSONをそのままHASHに変換する

HASHの場合、唯一のクエリ手段は SCAN であり、これはデータ構造全体を読み込む必要があります。また同様に、フィルタリングは MATCH ディレクティブに限定されます。そのため、テーブルや JSON はHASHではなく文字列として保存することが推奨されます。インデックスは SET や SORTED SET を使って逆引きインデックスを作成し、文字列キーを参照するようにします。

その他にも以下のような場合に、クエリパターンを考慮していないと、パフォーマンスが低下します。

Redis の開発者 Salvatore は「1つの Redis インスタンス内で SELECT コマンドを使って複数のデータベースを利用する方法」をアンチパターン としています。各用途に専用の Redis インスタンスを使うことが推奨されます。特にマイクロサービス構成では、クライアント同士の干渉(ノイジーネイバー)、データベースのセットアップ/破棄の影響、メンテナンス、アップグレードなどの問題が起こりやすいためです。

Redis TimeSeries モジュールは、時系列データベースと競合できる強力な機能を提供します。しかし、単に順序に基づくクエリだけが必要な場合は、逆に複雑になりすぎます。その場合は、SORTED SET を使い、すべての値にスコア 0 を設定して追加するか、シンプルな時間ベースのクエリが必要なら タイムスタンプをスコアに使う方法 が推奨されます。

さいごに

この記事では、Redisを効果的に利用する上で避けるべき13のアンチパターンを紹介しました。Redisは実務利用したことがないので、勉強になりました。

これらのアンチパターンから見えてくる重要な教訓は、**「Redisのデータ構造を深く理解し、ユースケースに合わせて最適なものを選択すること」**の重要性です。

Redisは単なるキーバリューストアではなく、多様なデータ構造を駆使することで、その真価を発揮します。パフォーマンスを最大限に引き出し、安定した運用を実現するために、今回紹介したアンチパターンを常に意識し、適切な設計を心がけましょう。

参考書籍
実践Redis入門 技術の仕組みから現場の活用まで

Discussion