Redis のスレッドモデル
Redis は高性能で知られていますが、その中核的な理由の一つが独特な スレッドモデルの設計 です。多くの人は Redis が「シングルスレッド」だと聞いたことがあるでしょうが、実際には Redis のスレッドモデルはバージョンを重ねる中で進化してきました。Redis のスレッドモデルを理解することは、その高性能の原理を理解するうえでも、高並行シーンでの使い方を理解するうえでも非常に重要です。
このテーマを独立した一本の記事として書こうと思ったのは、「Redis はシングルスレッドである」という言葉が面接や日常の議論であまりに頻繁に登場する一方で、大半の人の理解が字面のレベルにとどまっているからです。Redis プロセス全体にスレッドが 1 本しかないと思い込んでいるか、あるいは Redis 6 以降はコマンド実行もマルチスレッドになったと思い込んでいるか——どちらの理解も正確ではありません。そしてこのモデルは、私たちの Redis の使い方に直接影響します。どのコマンドを気軽に実行してはいけないのか、大きな Key がなぜ危険なのか、Pipeline がなぜ効果的なのか——その根源はすべてスレッドモデルにあります。
本記事では、Redis のシングルスレッド設計、イベント駆動モデル、IO 多重化、そして Redis 6 以降のマルチスレッド改善 という観点から紹介していきます。
Redis はなぜシングルスレッドを選んだのか
Redis の初期バージョン(Redis 6 より前)における中核的な実行モデルは、シングルスレッドでのコマンド処理 です。
つまり:
- すべてのクライアントリクエスト
- すべてのコマンド実行
- データの読み書き
これらすべてが 1 本のメインスレッドで完結します。
この設計は、非常に重要な性質をもたらします。任意の時点で実行されているコマンドは 1 つだけ ということです。そのため Redis の単一コマンドは本質的にアトミックであり、ロックを取る必要がなく、2 つのコマンドが交錯して同じ Key を変更するという問題も存在しません。INCR や SETNX といったコマンドをそのまま並行処理のプリミティブとして使えるのは、このモデルの副産物なのです。
ただし注意が必要です。
Redis のシングルスレッドとは、コマンド実行がシングルスレッドであることだけを指し、Redis プロセス全体にスレッドが 1 本しかないという意味ではありません。たとえば:
- RDB 永続化
- AOF rewrite
- 非同期削除
- BIO スレッド
これらは実際にはバックグラウンドスレッドです。
非同期削除を例にとると、UNLINK コマンドはメインスレッド上では 1 つのことしかしません——Key を辞書から取り外すことです。実際にメモリを解放する作業はバックグラウンドスレッドに委ねられ、ゆっくり処理されます。こうすることで、非常に大きなオブジェクトを削除する場合でもメインスレッドがブロックされません。RDB 永続化はさらに一歩進んでいて、直接子プロセスを fork してスナップショットを書き込み、オペレーティングシステムのコピーオンライト(copy-on-write)を利用してメインスレッドのブロックを回避します。
Redis のコアスレッドが担当するのは以下だけです。
- ネットワーク IO
- コマンドのパース
- コマンドの実行
- 結果の返却
シングルスレッドなのになぜ速いのか
多くの人が疑問に思うでしょう。シングルスレッドでどうして数十万 QPS を支えられるのか?
Redis の高性能は、主に以下の理由によるものです。
インメモリデータベース:Redis のすべてのデータはメモリ上にあり、ディスク IO を回避しています。メモリの速度とディスクの速度は桁が違います。通常の GET コマンド 1 つをとっても、本当の実行部分はハッシュテーブルの検索 1 回だけで、所要時間はマイクロ秒あるいはナノ秒レベル——ボトルネックはそもそも CPU の計算にはないのです。
スレッド切り替えのオーバーヘッドの回避:シングルスレッドでコマンドを実行することで、ロック、競合、複雑な並行制御を回避しています。マルチスレッドモデルでは、スレッドのコンテキストスイッチ、ロックの奪い合い、キャッシュの無効化といったオーバーヘッドは決して安くありません。各リクエスト自体の処理時間が極めて短い場合、これらの並行制御のコストはかえって利益を上回る可能性があります。Redis がシングルスレッドを選んだのは、本質的には「コマンド実行はボトルネックではなく、ネットワーク IO こそがボトルネックだ」という判断です。ならば並行性の問題は IO 層で解決し、実行層は最もシンプルな形に保とう、というわけです。
さらに、Redis 内部のデータ構造もこのモデルのために設計されています。SDS 文字列、スキップリスト、圧縮リストなどはいずれもメモリアクセスに最適化されており、単一操作のパスは非常に短くなっています。
IO 多重化
Redis は一度に 1 つの接続だけを処理しているわけではなく、IO 多重化モデル を使用しています。
IO 多重化とは、1 本のスレッドで複数の socket を同時に監視し、「どの接続に今読み書き可能なデータがあるか」をオペレーティングシステムに教えてもらい、スレッドは準備ができた部分だけを処理する仕組みです。「1 接続 1 スレッド」の従来モデルと比べて、接続ごとにスレッドのメモリとスケジューリングのコストを払う必要がなく、シングルスレッドで膨大な数の接続を支えられます。
Redis が使用する IO モデルには以下があります。
- epoll(Linux)
- kqueue(MacOS / BSD)
- select
- evport
Redis はコンパイル時にプラットフォームに応じて最適な実装を自動選択します。Linux では epoll です。この抽象化層はソースコード内で ae(A simple Event driven programming library)と呼ばれ、上位層に統一されたイベントインターフェースを公開しています。
Redis はイベントループを通じてすべてのクライアント接続を監視します。
┌───────────────┐
Client 1 ───▶│ │
Client 2 ───▶│ │
Client 3 ───▶│ IO Multiplex │
Client N ───▶│ │
└───────┬───────┘
│
▼
Redis EventLoop
│
▼
Command Execute
処理フロー:
- 複数のクライアント socket を監視する
- どの socket にデータの準備ができたかを検知する
- リクエストを読み取る
- コマンドを実行する
- 結果を返す
イベントループは毎周、多重化インターフェースから準備完了イベントを取り出し、1 つずつ処理します。読み取りイベントならコマンドをパースして実行し、書き込みイベントなら結果をクライアントへ送り返します。各イベントの処理が非常に速く、ループが十分な速さで回転するため、クライアントは自分が「順番待ち」していることに気づかないのです。
したがって Redis は シングルスレッドで大量の接続を同時に処理 できます。
Redis 6 のマルチスレッド改善
ハードウェアの発展に伴い CPU のコア数はますます増え、Redis のシングルスレッドモデルは特定のシーンでボトルネックに直面するようになりました。ボトルネックは主に ネットワーク IO にあります。
たとえば:
- 大量のクライアント接続
- ネットワークデータの送受信量が膨大な場合
コマンド実行自体は純粋なメモリ操作で非常に高速です。しかし、リクエストをカーネルバッファから読み込んだり、レスポンスを書き戻したりする read/write システムコールには実際のオーバーヘッドがあります。トラフィックが一定レベルに達すると、メインスレッドの時間の大半がデータの送受信に費やされ、実際にコマンドを実行する時間の割合はかえって小さくなります——これがシングルスレッドモデルの天井です。
そこで Redis 6 は IO マルチスレッド を導入しました。
実行フローは次のように変わります。
Client Request
│
▼
IO Threads (read)
│
▼
Main Thread Execute Command
│
▼
IO Threads (write)
考え方は明快です。読み取り・パースとレスポンスの書き戻しという 2 つの「運搬役」の仕事を複数の IO スレッドに分担させて並列に行い、コマンド実行は引き続きメインスレッドに集約してシリアルに処理します。こうすることでマルチコアを活用してネットワークスループットを向上させつつ、シングルスレッド実行のアトミック性のセマンティクスを完全に維持しています——上位のビジネスコードは何も変更する必要がありません。
したがって:
- コマンド実行は依然としてシングルスレッド
- IO 操作は並列化可能
補足すると、IO マルチスレッドはデフォルトで無効になっており、設定で有効化します。
# redis.conf
# IO スレッド数。CPU コア数より少なくすることを推奨
io-threads 4
# デフォルトでは書き戻しのみマルチスレッド。読み取り・パースも並列化したい場合はこちらも有効にする
io-threads-do-reads yes
ネットワークトラフィックが本当にボトルネックになっているインスタンスで有効化してこそ意味があります。トラフィックの少ないシーンで有効にすると、かえってスレッド間の調整というオーバーヘッドが一層増えるだけです。
Redis シングルスレッドの使用上のアドバイス
Redis のコマンド実行はシングルスレッドであるため、以下の点に注意が必要です。1 つの遅いコマンドが すべての クライアントのリクエストをブロックします。これはシングルスレッドモデルにおいて最も畏敬の念をもって扱うべきポイントです。
遅いコマンドを避ける
例:
# キー空間全体を走査する O(N) 操作。その間すべてのリクエストがブロックされる
KEYS *
このようなコマンドは Redis をブロックします。
代わりにこちらを使うべきです。
# カーソルベースでバッチごとにイテレートし、毎回少量ずつ処理するため長時間ブロックしない
SCAN
同様に、大きなコレクションに対して SMEMBERS、HGETALL、LRANGE 0 -1 などの全量読み取りコマンドを実行するのも、本質的には O(N) の仕事をシングルスレッドに押し込むことになります。
大きな Key を抑制する
大きな Key は以下を引き起こします。
- ネットワークのブロック
- CPU 実行時間の増大
さらに、大きな Key の削除と期限切れ処理も同様にメインスレッド上で行われます(UNLINK を使うか、遅延削除を有効にしない限り)。数百 MB の Key が期限切れになると、目に見えるリクエストのスパイクを引き起こす可能性があります。
推奨:
- データを分割する
- hash / set を使う
Pipeline を使う
Pipeline はネットワークの往復を減らせます。
client -> redis -> client
Pipeline を使わない場合、各コマンドは前のコマンドのレスポンスが返ってくるのを待ってから次を送信するため、スループットはネットワークの RTT に縛られてしまいます。Pipeline はコマンドをまとめて一度に送信し、すべてのレスポンスを一度に受け取ることで、N 回の往復を 1 回に圧縮します。
全体のスループットが向上します。
ハマりどころと注意点
スレッドモデルを踏まえると、実践で見落としがちなポイントがいくつかあります。
-
Lua スクリプトも同様にメインスレッドをブロックします。スクリプト実行中、Redis は他のいかなるコマンドも処理しません。スクリプト内でループを書いて大量のデータを処理すると、その効果は 1 本の超遅いコマンドと同じです。
-
Redis 6 の IO マルチスレッドを「コマンドの並行実行」と理解しないでください。トランザクション、Lua、
INCRのアトミック性のセマンティクスはまったく変わっておらず、これを理由にロックを追加する必要はありません。同様に、IO スレッドを有効にすれば遅いコマンドの問題が解決すると期待するのも誤りです——実行層で遅いものは、マルチスレッド IO では助けになりません。 -
ブロックの調査ではまずスローログを見ましょう。
SLOWLOG GETが記録するのはコマンドの実行所要時間(ネットワークを含まない)で、「Redis がたまに一瞬固まる」現象を突き止める最初の入り口です。latency関連のコマンドと組み合わせれば、コマンドが遅いのか、fork が遅いのか、ディスクが遅いのかをさらに切り分けられます。 -
単一インスタンスがマルチコアを使い切れないのは正常です。コマンド実行は 1 コアしか使いません。マルチコアマシン全体を活用したいなら、一般的なやり方は 1 台に複数インスタンスを立てるか Redis Cluster を導入することであって、Redis がマルチスレッド実行になるのを待つことではありません。
まとめ
Redis は メモリ操作、IO 多重化、イベント駆動(Event Loop)、そしてシングルスレッドでのコマンド実行 というアーキテクチャ設計により、ロック競合とスレッド切り替えのオーバーヘッドを回避しながら、極めて高い並行処理能力を実現しています。Redis 6 の IO マルチスレッドはネットワーク送受信を並列化しただけで、コマンド実行は依然としてシリアルです——この核心は変わらないため、それを取り巻く使用原則(遅いコマンドを避ける、大きな Key を抑制する、Pipeline を活用する)も常に成り立ちます。この点を理解すれば、Redis の多くのベストプラクティスは暗記すべき項目ではなく、モデルから導き出される必然的な結論になるのです。
COMMENTS