シングルスレッドが10w以上のQPSをサポートできるのはなぜですか?
Redisはシングルスレッドプログラムだとよく耳にします。正確には、Redisはマルチスレッドプログラムですが、リクエスト処理部分は1つのスレッドで実装されます。
RedisQPSでのAlibabaCloudのテスト結果は次のとおりです。Redisは
どのように単一スレッドを使用して毎秒10w +のQPSを達成しますか?
- IO多重化を使用する
- CPUを集中的に使用しないタスク
- 純粋なメモリ操作
- 効率的なデータ構造
1つのスレッドだけで複数のクライアント接続を処理するにはどうすればよいですか?
これは、IO多重化テクノロジー、つまりJavaのNIOに言及する必要があります。
ブロッキングIO(JavaではBIO)を使用する場合は、読み取り関数を呼び出してパラメーターnを渡します。これは、スレッドがnバイトを読み取った後に戻ることを意味します。それ以外の場合は、ブロックされます。書き込みメソッドは通常、書き込みバッファーがいっぱいでない限りブロックしません。バッファーにスペースが解放されるまで、書き込みはブロックされます。
IO多重化テクノロジを使用する場合、読み取りまたは書き込みするデータがない場合、クライアントスレッドはブロックせずに直接戻ります。このようにして、Redisは1つのスレッドを使用して複数のソケットを監視できます。ソケットが読み取り可能または書き込み可能である場合、Redisは要求を読み取り、メモリ内のデータを操作してから戻ります。
シングルスレッドを使用する場合、マルチコアCPUは使用できませんが、RedisのほとんどのコマンドはCPUを集中的に使用するタスクではないため、CPUはRedisのボトルネックではありません。
高い同時実行性と大量のデータ、Redisのボトルネックを広げてください主にメモリとネットワーク帯域幅に反映されるため、メモリを節約するためにRedisが表示され、基盤となるデータ構造が占めるメモリをできるだけ少なくすることができます。データの種類が異なるさまざまなシナリオでさまざまなデータ構造が使用されます。
そのため、Redisは単一のスレッドで多数のリクエストを処理できるため、複数のスレッドを使用する必要はありません。また、シングルスレッドを使用することには以下の利点があります。
- スレッドスイッチングのパフォーマンスオーバーヘッドなし
- さまざまな操作をロックする必要はありません(マルチスレッドを使用する場合は、共有リソースへのアクセスをロックする必要があり、オーバーヘッドが増加します)
- デバッグが容易で、保守性が高い
最後に、Redisはインメモリデータベースであり、さまざまなコマンドの読み取りおよび書き込み操作はメモリに基づいて行われます。メモリとディスクの操作の効率が数桁異なることは誰もが知っています。Redisは非常に効率的ですが、回避する必要のある遅い操作がいくつかあります
Redisの遅い操作は何ですか?
さまざまなRedisコマンドがスレッド内で順番に実行されます。コマンドがRedisで長時間実行されると、前のリクエストが処理されるまで後続のリクエストが処理されないため、全体的なパフォーマンスに影響します。これらの時間のかかる操作には、次の部分
Redisは、これらの時間のかかるコマンドをログに記録できます。次の構成を使用するだけです。
# 命令执行耗时超过 5 毫秒,记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500
次のコマンドを実行すると、最近記録された低速ログを照会できます
127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693 # 慢日志ID
2) (integer) 1593763337 # 执行时间戳
3) (integer) 5299 # 执行耗时(微秒)
4) 1) "LRANGE" # 具体执行的命令和参数
2) "user_list:2000"
3) "0"
4) "-1"
2) 1) (integer) 32692
2) (integer) 1593763337
3) (integer) 5044
4) 1) "GET"
2) "user_info:1000"
...
過度に複雑なコマンドを使用する
前回の記事では、Redisの基礎となるデータ構造を紹介しましたが、その時間計算量を次の表に示します。
名前 | 時間の複雑さ |
---|---|
dict(辞書) | O(1) |
ziplist(圧縮リスト) | オン) |
zskiplist(スキップリスト) | O(logN) |
クイックリスト(クイックリスト) | オン) |
intset(整数のセット) | オン) |
単一要素操作:コレクション内の要素の追加、削除、変更、およびクエリ操作は、基になるデータ構造に関連しています。たとえば、辞書の追加、削除、変更、およびクエリの時間計算量はO(1)、ジャンプテーブルの追加、削除、クエリの時間計算量はO(logN)です。
範囲操作:ハッシュ型のHGETALL、セット型のSMEMBERS、リスト型のLRANGE、ZSet型のZRANGEなどのコレクションをトラバースします。時間計算量はO(n)です。使用を避け、代わりにSCANシリーズコマンドを使用してください。(ハッシュの場合はHscan、セットの場合はsscan、zsetの場合はzscan)
集約操作:このタイプの操作の時間計算量は通常、SORT、SUNION、ZUNIONSTOREなどのO(n)よりも大きくなります。
統計演算:LLENやSCARDなどのセット内の要素の数を取得する場合、quicklist、dict、intsetなどの基礎となるデータ構造が要素の数を格納するため、時間計算量はO(1)です。
境界操作:リストの最下層はクイックリストによって実装されます。クイックリストはリンクリストのヘッドノードとテールノードを保存します。したがって、リンクリストのヘッドノードとテールノードでの操作の時間計算量はO(1)です。 、LPOP、RPOP、LPUSH、RPUSHなど
Redisでキーを取得する場合は、キーの使用を避けてください*。Redisに保存されているキーと値のペアは、キーのタイプである辞書(JavaのHashMapに似ていますが、配列+リンクリストを介して実装されます)に保存されます。すべて文字列であり、値のタイプは文字列、セット、リストなどです。
たとえば、次のコマンドを実行すると、redisディクショナリの構造は次のようになります。
set bookName redis;
rpush fruits banana apple;
以下に示すように、keysコマンドを使用してRedisの特定のキーをクエリできます
# 查询所有的key
keys *
# 查询以book为前缀的key
keys book*
keysコマンドの複雑さはO(n)です。dict内のすべてのキーをトラバースします。Redisに多数のキーがある場合、Redisの読み取りと書き込みのすべての命令が遅れるので、これを使用しないでください。実稼働環境で注文します(出発する準備ができている場合は、楽しい時間をお過ごしください)。
キーの使用は許可されていないため、代替手段、つまりスキャンが必要です。
キーと比較して、スキャンには次の特徴があります
- 複雑さもO(n)ですが、カーソル分散によって実行され、スレッドをブロックしません。
- キーと同じ、パターンマッチング機能を提供します
- 完全なトラバーサルの開始から完全なトラバーサルの終了まで、データセットに含まれていたすべての要素が完全なトラバーサルによって返されますが、同じ要素が複数回返される場合があります。
- 要素が反復中にデータセットに追加された場合、または反復中にデータセットから削除された場合、要素が返される場合と返されない場合があります。
- 返される結果が空であるということは、トラバーサルの終了を意味するのではなく、返されたカーソル値が0であるかどうかによって異なります。
興味のある友人は、スキャンソースコードの実装を分析して、これらの機能を理解できます
zscanを使用してzsetをトラバースし、hscanを使用してハッシュをトラバースし、sscanを使用してsetをトラバースします。これは、hash、set、およびzsetの基になるデータ構造にすべてdictがあるためです。
オペレーションビッグキー
キーに対応する値が非常に大きい場合、そのキーはビッグキーと呼ばれます。bigkeyへの書き込みは、メモリの割り当てに時間がかかります。同様に、ビッグキーを削除してメモリを解放するのにも時間がかかります
遅いログでSET / DELなどの複雑性の低いコマンドを見つけた場合は、それがbigkeyへの書き込みが原因であるかどうかを確認する必要があります。
ビッグキーを見つける方法は?
Redisはビッグキーをスキャンするコマンドを提供します
$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01
...
-------- summary -------
Sampled 829675 keys in the keyspace!
Total key length in bytes is 10059825 (avg len 12.13)
Biggest string found 'key:291880' has 10 bytes
Biggest list found 'mylist:004' has 40 items
Biggest set found 'myset:2386' has 38 members
Biggest hash found 'myhash:3574' has 37 fields
Biggest zset found 'myzset:2704' has 42 members
36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)
787393 lists with 896540 items (94.90% of keys, avg size 1.14)
1994 sets with 40052 members (00.24% of keys, avg size 20.09)
1990 hashs with 39632 fields (00.24% of keys, avg size 19.92)
1985 zsets with 39750 members (00.24% of keys, avg size 20.03)
コマンド入力には次の3つの部分があることがわかります
- メモリ内のキーの数、占有されている合計メモリ、各キーが占有している平均メモリ
- 各タイプが占める最大メモリ、キーの名前は
- 各データ型のパーセンテージ、および平均サイズ
このコマンドの原則は、redisが内部でスキャンコマンドを実行し、インスタンス内のすべてのキーをトラバースしてから、strlen、llen、hlen、scar、zcardコマンドを実行して、文字列タイプとコンテナタイプ(数値)の長さを取得することです。リスト、ハッシュ、セット、zset内の要素の数)
このコマンドを使用して、次の2つの問題に注意してください。
- オンラインインスタンスでビッグキースキャンを実行する場合、ops(1秒あたりの操作数)の突然の増加を回避するために、スリープパラメータを-iで追加できます。上記の意味は、100回のスキャン命令ごとに0.01秒間スリープすることです。
- コンテナタイプ(リスト、ハッシュ、セット、zset)の場合、スキャンされたキーが最も多くの要素を持つキーですが、キーには多数の要素が含まれているため、必ずしもより多くのメモリを消費するわけではありません。
ビッグキーによって引き起こされるパフォーマンスの問題を解決するにはどうすればよいですか?
- ビッグキーへの書き込みは避けてください
- redis4.0以降を使用している場合は、delの代わりにunlinkコマンドを使用できます。このコマンドは、キーメモリ操作をバックグラウンドスレッドに解放して実行できます。
- redis 6.0以降を使用している場合は、レイジーフリーメカニズム(lazyfree-lazy-user-del yes)をオンにできます。また、delコマンドを実行すると、バックグラウンドスレッドでも実行されます。
多数のキーが一元的に期限切れになりました
Redisでキーの有効期限を設定できます。キーの有効期限が切れると、いつ削除されますか?
Redisの有効期限戦略を書いてみると、次の3つの解決策が考えられます。
- 時限削除は、キーの有効期限を設定しながら、タイマーを作成します。キーの有効期限が来たら、すぐにキーを削除してください
- 遅延削除、キーを取得するたびに、キーの有効期限が切れているかどうかが判断され、有効期限が切れている場合はキーが削除され、有効期限が切れていない場合はキーが返されます
- 定期的に削除し、時々キーをチェックし、その中の期限切れのキーを削除します。
タイミング削除戦略はCPUに適していません。期限切れのキーがさらにある場合、Redisスレッドを使用して期限切れのキーを削除します。これは影響します。通常のリクエストの応答。
タイミング削除戦略はCPUに適していません。期限切れのキーが多数ある場合、Redisスレッドを使用して期限切れのキーを削除します。これは、通常のリクエストの応答に影響します。
読み取りCPUの遅延削除の方が優れていますが、多くのメモリを浪費します。キーが有効期限を設定してメモリに配置したが、アクセスされていない場合、キーは常にメモリに存在します
定期的な削除戦略は、CPUとメモリにより適しています
redisの有効期限が切れたキーの削除戦略では、次の2つを選択します
- 怠惰な削除
- 定期的に削除する
遅延削除
クライアントはキーにアクセスすると、キーの有効期限を確認し、有効期限が切れるとすぐに削除します。
Redisを定期的に削除するには、有効期限のあるキーを別のディクショナリに配置し、ディクショナリを定期的にトラバースして期限切れのキーを削除します。トラバーサル戦略は次のとおりです。
- 1秒あたり10回の期限切れスキャンを実行し、毎回期限切れの辞書からランダムに20個のキーを選択します
- 20個のキーの中から期限切れのキーを削除します
- 期限切れのキーの割合が1/4を超える場合は、手順1に進みます。
- スレッドジャムを回避するために、各スキャン時間の上限はデフォルトで25ms以下です。
Redisの期限切れのキーはメインスレッドによって削除されるため、ユーザーの要求をブロックしないために、期限切れのキーは数回削除されます。ソースコードは、expire.cのactiveExpireCycleメソッドを参照できます。
メインスレッドが常にキーを削除しないようにするために、次の2つのソリューションを使用できます。
- 同時に期限切れになるキーに乱数を追加して、有効期限を分割し、キーをクリアする圧力を軽減します
- redisバージョン4.0以降を使用している場合は、レイジーフリーメカニズムをオンにできます(lazyfree-lazy-expire yes)。期限切れのキーが削除されると、メモリ解放操作がバックグラウンドスレッドで実行されます。
メモリが上限に達し、除去戦略がトリガーされます
Redisはインメモリデータベースです。Redisが使用するメモリが物理メモリの制限を超えると、メモリデータがディスクと頻繁に交換され、交換によってRedisのパフォーマンスが大幅に低下します。そのため、実稼働環境では、パラメーターmaxmemoeyを構成することにより、使用されるメモリーの量を制限します。
使用される実際のメモリがmaxmemoeyを超える場合、Redisは次のオプションの戦略を提供します。
noeviction:書き込み要求はエラーを返します
volatile-lru:lruアルゴリズムを使用して有効期限のあるキーと値のペアを削除します
volatile-lfu:lfuアルゴリズムを使用して有効期限のあるキーと値のペアを削除します
volatile-random:キー値から
volatileをランダムに削除します有効期限が-ttlのペア:有効期限の順序に従って削除します。有効期限が早いほど、削除されます。
allkeys-lru:lruアルゴリズムを使用してすべてのキーと値のペアを削除します
allkeys-lfu:lfuアルゴリズムを使用してすべてのキーと値のペアを削除します
allkeys-random:すべてのキーと値のペアからランダムに削除します
Redisの除去戦略もメインスレッドで実行されます。ただし、メモリがRedisの上限を超えた後は、書き込むたびに一部のキーを削除する必要があるため、リクエスト時間が長くなります。
以下の方法で改善できます
- メモリを増やすか、データを複数のインスタンスに配置します
- 除去戦略はランダム除去に変更されます。一般的に、ランダム除去はlruよりもはるかに高速です。
- ビッグキーの保存を避け、メモリの解放にかかる時間を短縮します
AOFログを書き込む方法は常にです
Redisの永続化メカニズムには、RDBスナップショットとAOFログが含まれます。各コマンドが書き込まれた後、Redisは次の3つのブラッシングメカニズムを提供します。
常に:同期ライトバック、書き込みコマンドが
毎秒実行された後にディスクに同期:毎秒、各書き込みコマンドが実行された後、最初にaofファイルのメモリバッファにログを書き込み、バッファの内容を毎秒1秒ディスクへの書き込み
番号:オペレーティングシステムがライトバックを制御します。各書き込みコマンドが実行されると、ログがaofファイルのメモリバッファに書き込まれ、オペレーティングシステムがバッファの内容をいつ書き戻すかを決定します。ディスクに
AOFのフラッシュメカニズムが常にの場合、redisは書き込みコマンドを処理するたびに戻る前に書き込みコマンドをディスクにフラッシュします。プロセス全体がメインのRedisスレッドで実行されるため、redisのパフォーマンスが必然的に遅くなります。
AOFのフラッシュメカニズムが毎秒の場合、メモリへの書き込み後にredisが戻ります。フラッシュ操作はバックグラウンドスレッドで実行され、バックグラウンドスレッドはメモリ内のデータを1秒ごとにディスクにフラッシュします。
AOFの点滅メカニズムがない場合、ダウンタイム後に一部のデータが失われる可能性があるため、通常は使用されません。
通常の状況では、aofブラッシングメカニズムはeverysecとして構成されます。
フォークに時間がかかりすぎる
永続性のセクションでは、RedisがRDBファイルとAOFログの書き換えを生成することを説明しました。どちらもメインスレッドのフォークサブプロセスによって実行され、メインスレッドのメモリが大きいほど、ブロック時間が長くなります。
次のように最適化できます
- Redisインスタンスのメモリサイズを制御します。メモリが大きいほどブロック時間が長くなるため、10g以内で制御してみてください。
- スレーブノードでRDBスナップショットを生成するなど、適切な永続性戦略を構成します
リファレンスブログ
[1] http://kaito-kidd.com/2021/01/23/redis-slow-latency-analysis/
[2] https://draveness.me/whys-the-design-redis-single-thread/
scanコマンド
[3] http://jinguoxing.github.io/redis/2018/09/04/redis-scan/scan
命令設計
[4] https://juejin.cn/post/6844903688528461831