質問:Redisは本当にどれほどの高速性を持っているのか?
Redisに付属のbenchmarkスクリプトを使用してテストする:
cd /usr/local/soft/redis-6.0.9/src
redis-benchmark -t set,lpush -n 100000 -q
結果:
- SET: 136239.78 requests per second — 秒あたり13万回以上のsetリクエストを処理
- LPUSH: 132626.00 requests per second — 秒あたり13万回以上のlpushリクエストを処理
redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')"
結果:
- script load redis.call('set','foo','bar'): 125628.14 requests per second — 秒あたり125628.14回のLuaスクリプト呼び出し
テスト結果から、RedisのQPSが10万は正確であることが示され、高性能サーバーではさらに高いパフォーマンスを発揮できることが確認できる。
Redisが高速な理由
要約すると、主に以下の3点が該当する:
- メモリのみでの構造
- リクエスト処理のシングルスレッド
- マルチプレクサメカニズム
メモリ
キーと値の構造を持つメモリデータベースであり、時間計算量はO(1)。
次に通常の考えでは、このような高并发性能を実現するために多数のスレッドを作成する必要があるのではないか?なぜRedisはシングルスレッドであるとされているのか?この「シングルスレッド」というのは何を意味しているのか?
シングルスレッド
ここで言及されるシングルスレッドとは、クライアントからのリクエスト処理を行うスレッドを指す。これをメインスレッドと呼ぶ。
バージョン4.0以降では、他のタスク(ゴミデータのクリーンアップ、未使用接続の解放、大規模キーの削除など)を処理するための追加スレッドも導入された。
リクエスト処理のメインスレッドをシングルスレッドに設定することにはどのような利点があるか?
- スレッドの作成・破棄によるコスト削減
- コンテキストスイッチによるCPU消費の回避
- スレッド間の競合問題(ロック・アンロック・デッドロックなど)の回避
しかし疑問が生じる。シングルスレッドが確かにこれらの利点を持つとしても、CPUリソースが無駄に使われていないか?つまり単一コアしか使えないのではないか?
公式の説明によると:
Redisにおいてシングルスレッドで十分であり、CPUはRedisのボトルネックではない。Redisのボトルネックは最も可能性が高いのがマシンのメモリ容量またはネットワーク帯域幅である。シングルスレッドであれば実装が容易であり、スレッドの並行処理問題を考慮する必要がないため、自然とシングルスレッドのアプローチを採用した。
注意:リクエスト処理がシングルスレッドであるため、本番環境では長時間かかるコマンド(keys, flushall, flushdbなど)を実行しないようにする。そうしないとリクエストがブロックされる可能性がある。
同期非ブロッキングI/O
同期非ブロッキングI/O、マルチプレクサによる並行接続処理 — マルチプレクサとは何か?
なぜシングルスレッドでも高速なのか?
Redisはメモリ操作に基づいているため高速である。
仮想記憶装置(仮想メモリ Virtual Memory)
コンピュータ内のメモリは主記憶装置(RAM)と呼ばれ、ハードディスクは補助記憶装置(Storage)と呼ばれる。
主記憶装置は非常に長い配列と考えることができ、各バイトごとに固有のアドレスを持ち、これは物理アドレス(Physical Address)と呼ばれる。
初期のコンピュータでは、CPUがメモリにアクセスする際に物理アドレスを使用し、直接主記憶装置にアクセスしていた。
しかし、この方式にはいくつかの欠点があった:
- 一般的なオペレーティングシステムはマルチユーザー・マルチタスクであり、すべてのプロセスが主記憶装置を共有する。各プロセスが物理アドレス空間を独占すると、すぐに主記憶装置が枯渇してしまう。異なる時間に異なるプロセスが同じ物理アドレス空間を共有できるようにしたい。
- すべてのプロセスが直接物理メモリにアクセスすると、あるプロセスが他のプロセスのメモリデータを変更できてしまうため、物理アドレス空間が破壊され、プログラムが異常終了する可能性がある。
どうすればよいか?物理メモリの使用を調整・管理する役割が必要であった。そのため、CPUと主記憶装置の間に中間層を設けるという方法を採用した。CPUは物理アドレスではなく仮想アドレスにアクセスし、この中間層がアドレスを物理アドレスに変換し、最終的にデータを取得する。この中間層はMMU(Memory Management Unit)と呼ばれる。
MMUへのアクセスは物理メモリへのアクセスと同様であるため、生成されたアドレスは仮想メモリ(Virtual Memory)と呼ばれる。各プロセスが起動する際、仮想アドレスが割り当てられ、仮想アドレスと物理アドレスのマッピングを通じて実際のデータにアクセスする。これにより、プロセスは物理アドレスに直接アクセスせず、どの物理アドレスのデータを参照しているかも知らぬままに動作する。
現在の多くのオペレーティングシステムは仮想メモリを使用しており、Windowsの仮想メモリやLinuxのスワップ領域などがその例である。Windowsの仮想メモリ(pagefile.sys)はディスク領域の一部である。
32ビットシステムでは仮想アドレス空間は232 = 4GB、64ビットシステムでは最大仮想アドレス空間は264 = 1024×1024TBではない。実際には使用可能な空間が限定されており、非常に大きな空間を必要とするわけではないため、システムのオーバーヘッドを増加させるために64ビットを活用していない。Linuxでは一般的に仮想アドレス空間を48ビットで表現しており、つまり2^48 = 256TBである。
cat /proc/cpuinfo
# address sizes : 43 bits physical, 48 bits virtual
実際の物理メモリは仮想メモリよりも遥かに小さい場合が多い。
仮想メモリの導入の目的:
- 同一の物理メモリを異なる仮想アドレス空間にマッピングすることでメモリ共有を実現
- 物理メモリを分離し、プロセス間の操作が互いに影響を与えないようにする
- より大きなアドレス空間を提供し、連続的なアドレス空間を確保することで、プログラムの記述やリンクを簡略化する
Linux/GNUの仮想メモリはさらに2つの領域に分割されている。
マルチプレクサの仕組み
まとめ
RedisはAEイベントモデルを抽象化し、IOイベントとタイマーイベントを統合した。また、マルチプレクサメカニズム(Linuxではepoll)のコールバック機能を利用し、IO読み書きを非ブロッキングに実現することで、高性能なネットワーク処理能力を実現している。
Redisの新バージョンにおける「マルチスレッド」機能について言及するが、これはサーバー側でのクライアントリクエスト受信がマルチスレッドになったことを意味するものではなく、依然としてシングルスレッドである。
厳密に言えば、Redisは4.0以降、処理に時間がかかるタスクやバックグラウンド作業を処理するためにマルチスレッドを導入した。さもなくば、単一スレッドだけでは、処理が重い操作によってクライアントリクエストがブロックされることになる。この「マルチスレッド」というのは、正確には「マルチスレッドI/O」と呼ばれるべきである。
なぜI/O処理にマルチスレッドを使うのか?
マルチスレッドI/O
サーバーからクライアントにデータを返す際、データはカーネル空間からユーザ空間へコピーされ、その後socket(write呼び出し)に書き戻される必要がある。このプロセスは非常に時間がかかる。したがって、マルチスレッドI/Oとは、このsocketへの書き込み処理をマルチスレッドで行うことを意味する。リクエスト処理自体はシングルスレッドのままであり、スレッドの並行安全性に関する問題は発生しない。