[[401361]] この記事では主に、カウンター アルゴリズム、リーキー バケット アルゴリズム、トークン バケット アルゴリズムなど、いくつかの一般的な電流制限アルゴリズムについて説明します。次に、Sentinel 1.8.0 についての私の理解に基づいて、Sentinel がソース コード内でフロー制御の判断にこれらのアルゴリズムをどのように使用するかについて説明します。 逆電流制限アルゴリズムカウンターを直接使用して、1 秒あたりに受信できるリクエストの数を制限できます。たとえば、qps が 1000 に設定されている場合、実装のアイデアは、最初に到着したリクエストからタイミングを開始することです。次の 1 秒間に、到着するリクエストごとにカウントが 1 ずつ増加します。累積数が 1000 に達すると、後続のすべてのリクエストは拒否されます。 1 秒が経過したら、カウントを 0 に戻し、再びカウントを開始します。 利点: シンプルな実装 デメリット: 1 秒のうち最初の 0.5 秒で 1,000 件のリクエストを受信した場合、次の 0.5 秒ではリクエストを拒否することしかできません。この現象を「スパート現象」と呼びます。 実装コードの例: - パブリッククラスCounter{
- パブリックlong timeStamp = getNowTime();
- 公共 要求カウント = 0;
- public final int limit = 100; // 時間枠内のリクエストの最大数
- public final long interval = 1000; // 時間ウィンドウ ms
-
- パブリックブール制限(){
- 長い今 = getNowTime();
- if (現在 <タイムスタンプ+ 間隔) {
- // 時間枠内
- 要求カウント++;
- // 現在の時間枠内で制御されるリクエストの最大数を超えているかどうかを判定します
- reqCount <= limitを返します。
- }それ以外{
- タイムスタンプ= 現在;
- // タイムアウト後にリセット
- 要求数 = 1;
- 戻る 真実;
- }
- }
-
- パブリックlong getNowTime() {
- System.currentTimeMillis()を返します。
- }
- }
スライディングタイムウィンドウアルゴリズムスライディング ウィンドウ、ローリング ウィンドウとも呼ばれます。カウンター アルゴリズムの欠点を解決するために、スライディング ウィンドウ アルゴリズムを導入しました。次の図は、スライディング ウィンドウ アルゴリズムを非常にわかりやすく説明しています。 上の図では、赤い四角形全体が時間枠を表しています。この例では、時間枠は 1 分です。次に、時間ウィンドウを分割します。たとえば、図では、スライディング ウィンドウを 6 つのグリッドに分割し、各グリッドは 10 秒を表します。 10 秒ごとに、時間ウィンドウは 1 グリッド右にスライドします。各グリッドには独立したカウンターがあります。例えば、0:35 秒にリクエストが到着すると、0:30 ~ 0:39 に対応するカウンターが 1 増加します。 では、スライディング ウィンドウはどのようにしてこの重大な問題を解決するのでしょうか。上の図からわかるように、0:59 に到着する 100 件のリクエストは灰色のグリッドに分類され、1:00 に到着するリクエストはオレンジ色のグリッドに分類されます。時間が 1:00 になると、ウィンドウは 1 グリッド右に移動します。すると、時間ウィンドウ内のリクエストの合計数は 200 となり、制限の 100 を超えます。したがって、現在の制限がトリガーされたことが検出されます。 ここで、カウンター アルゴリズムをもう一度確認してみましょう。カウンター アルゴリズムは、実際にはスライディング ウィンドウ アルゴリズムであることがわかります。時間ウィンドウをさらに分割しないので、グリッドは 1 つだけになります。 スライディング ウィンドウを分割するグリッドの数が増えるほど、スライディング ウィンドウのスクロールがスムーズになり、電流制限の統計がより正確になることがわかります。 実装コードの例: - パブリッククラスSlideWindow {
-
- /** キュー ID とキューのマッピング関係。キューは各パスのタイムスタンプを保存するため、プログラム内に複数の電流制限キューが存在する可能性があります */
- プライベート volatile静的Map<String, List<Long>> MAP = new ConcurrentHashMap<>();
-
- プライベートスライドウィンドウ() {}
-
- 公共 静的void main(String[] args)はInterruptedExceptionをスローします{
- (真)の間{
- // 10秒間に2回のパスのみ許可されます
- システム.out .println(LocalTime.now().toString() + SlideWindow.isGo( "ListId" , 2, 10000L));
- // 0~10 秒間スリープ
- スレッドをスリープ状態にします(1000 * 新しい Random().nextInt(10));
- }
- }
-
- /**
- * スライディングタイムウィンドウ電流制限アルゴリズム
- * 指定された時間枠と指定された回数内でアクセスを許可するかどうか
- *
- * @param listId キューID
- * @param count回数制限
- * @param timeWindow 時間ウィンドウのサイズ
- * @return通過を許可するかどうか
- */
- 公共 静的同期ブール値 isGo(String listId, int count 、長い時間ウィンドウ) {
- // 現在の時刻を取得する
- 長いnowTime = System.currentTimeMillis();
- //キューIDに応じて、対応する現在の制限キューを取り出し、存在しない場合は作成します
- リスト<Long> list = MAP.computeIfAbsent(listId, k -> new LinkedList<>());
- // キューがいっぱいでない場合は、キューを通過させ、現在のタイムスタンプをキューの先頭に追加します
- if (リスト.size () <カウント) {
- リストに0、現在時刻を追加します。
- 戻る 真実;
- }
-
- // キューがいっぱいの場合(制限に達した場合)、キューに追加された最も古いタイムスタンプを取得します
- Long farTime = list.get( count - 1 );
- // 現在のタイムスタンプから最も古い追加されたタイムスタンプを減算します
- if (nowTime - farTime <= timeWindow) {
- // 結果がtimeWindow以下の場合、timeWindow内で経過した回数がcountより大きいことを意味します
- // 通過は許可されません
- 戻る 間違い;
- }それ以外{
- // 結果がtimeWindowより大きい場合、timeWindow内で経過した回数がcount以下であることを意味します
- // 通過を許可し、最初に追加されたタイムスタンプを削除し、現在の時刻をキューの先頭に追加します
- リストを削除します(数- 1 );
- リストに0、現在時刻を追加します。
- 戻る 真実;
- }
- }
-
- }
Sentinel では、時間ウィンドウ アルゴリズムは LeapArray 構造を通じて実装されています。そのコア コードは次のとおりです (時間ウィンドウを取得する方法のみがリストされています)。
- /**
- * 現在の時間枠を取得する
- *
- *指定されたタイムスタンプでバケット項目を取得します。
- *
- * @param timeMillisは有効なタイムスタンプです ミリ秒単位
- * @戻る 指定されたタイムスタンプの現在のバケット項目 有効です。時間の場合はnullです 無効です
- */
- パブリックWindowWrap<T>現在のウィンドウ(長いtimeMillis) {
- (時間ミリ秒<0)の場合{
- 戻る ヌル;
- }
-
- int idx = calculateTimeIdx(timeMillis);
- //現在のバケットの開始時間を計算します。
- // ウィンドウの開始時間と各グリッドの開始時間を計算します
- 長いwindowStart = calculateWindowStart(timeMillis);
-
- /*
- *指定された時間にバケットアイテムを取得する 配列から。
- *
- * (1) バケットが存在しない場合は、新しいバケットを作成してCASを更新するだけです 円形配列にします。
- * (2) バケットが最新の状態であれば、バケットを返すだけです。
- * (3) バケットは非推奨なので、現在のバケットをリセットし、すべての非推奨のバケットをクリーンアップします。
- */
- (真)の間{
- WindowWrap<T> 古い = array.get(idx);
- // ペインがない場合は作成します
- (古い == null ) の場合 {
- /*
- * B0 B1 B2ヌルB4
- * ||_______|_______|_______|_______|_______||___
- * 200 400 600 800 1000 1200タイムスタンプ
- * ^
- *時間= 888
- * バケットは空なので、新しく作成して アップデート
- *
- * 古いバケットが存在しない場合は、 {@code windowStart}に新しいバケットを作成します。
- *試してみて CAS操作で循環配列を更新します。1つのスレッドのみが
- * 成功する 他のスレッドがタイムスライスを譲る間に、更新が行われます。
- */
- WindowWrap<T> ウィンドウ = 新しい WindowWrap<T>(windowLengthInMs、windowStart、newEmptyBucket(timeMillis));
- (array.compareAndSet(idx、 null 、window)の場合){
- // 正常に更新されました。作成されたバケットを返します。
- 返品期間;
- }それ以外{
- // 競合が失敗した場合、スレッドはタイムスライスを放棄して、バケットが使用可能になるまで待機します。
- スレッド.yield();
- }
- // 現在のペインが存在するので、履歴ペインに戻ります
- }そうでない場合 (windowStart == old.windowStart()) {
- /*
- * B0 B1 B2 B3 B4
- * ||_______|_______|_______|_______|_______||___
- * 200 400 600 800 1000 1200タイムスタンプ
- * ^
- *時間= 888
- * バケット3の開始時刻: 800なので最新です
- *
- *現在の{@code windowStart}が開始タイムスタンプと等しい場合 古いバケツの、
- * つまり時間 バケット内にあるため、バケットを直接返します。
- */
- 古いものを返す;
- //
- }それ以外の場合 (windowStart > old.windowStart()) {
- /*
- * (古い)
- * B0 B1 B2ヌルB4
- * |_______||_______|_______|_______|_______|_______||___
- * ... 1200 1400 1600 1800 2000 2200タイムスタンプ
- * ^
- *時間= 1676
- * バケット 2の開始時間: 400、非推奨、リセットする必要があります
- *
- * 開始タイムスタンプが 古いバケットは指定された時間より遅れています。つまり
- * バケットは非推奨です。バケットをリセットして 現在の{@code windowStart}。
- * リセットとクリーンアップ操作はアトミックに行うのが難しいことに注意してください。
- * したがって、バケット更新の正確性を保証するには更新ロックが必要です。
- *
- *更新ロックは条件付き(小さな範囲)であり、 いつ
- * バケットは非推奨であるため、ほとんどの場合、パフォーマンスの低下にはつながりません。
- */
- (updateLock.tryLock())の場合{
- 試す {
- //更新ロックを正常に取得しました。次にバケットをリセットします。
- // すべてのペインデータをクリアする
- resetWindowTo(old, windowStart)を返します。
- ついに
- 更新ロックを解除します。
- }
- }それ以外{
- // 競合が失敗した場合、スレッドはタイムスライスを放棄して、バケットが使用可能になるまで待機します。
- スレッド.yield();
- }
- // 時計が戻された場合は、タイムグリッドを再作成します
- }それ以外の場合 (windowStart < old.windowStart()) {
- //提供された時間のため、ここを通らないでください すでに遅れています。
- 新しいWindowWrap<T>(windowLengthInMs、windowStart、newEmptyBucket(timeMillis))を返します。
- }
- }
- }
リーキーバケットアルゴリズムリーキー バケット アルゴリズムは、ネットワークの世界ではトラフィック シェーピングやレート制限でよく使用されるアルゴリズムです。その主な目的は、ネットワークに注入されるデータのレートを制御し、ネットワーク上のバースト トラフィックを平滑化することです。リーキー バケット アルゴリズムは、バースト トラフィックを整形してネットワークに安定したフローを提供するメカニズムを提供します。実行プロセスを次の図に示します。 実装コードの例: - パブリッククラスLeakyBucket{
- public long timeStamp = System.currentTimeMillis(); // 現在の時刻
- public long capacity; // バケット容量
- public long rate; // 水が漏れる速度
- public long water; // 現在の水量(現在の累計リクエスト数)
-
- パブリックブールグラント(){
- 現在の長さ = System.currentTimeMillis();
- // 最初に水漏れを実行し、残りの水量を計算します
- 水 = Math.max (0, 水 - (現在 -タイムスタンプ) * 速度);
-
- タイムスタンプ= 現在;
- ((水 + 1) < 容量) の場合 {
- // 水を追加しようとしましたが、まだ水が満杯ではありません
- 水 += 1;
- 戻る 真実;
- }それ以外{
- // 水が満杯なので、水を追加しないでください
- 戻る 間違い;
- }
- }
- }
例: (1)タンクが満杯でない場合は水を追加します。コードwater +=1を使用して継続的に水を追加します。 (2)漏水:時間差を利用して漏水量を計算します。 (3)残水量:総水量-漏水量 Sentineでは、RateLimiterControllerがリーキーバケットアルゴリズムを実装しています。コアコードは次のとおりです。 - @オーバーライド
- パブリックブールcanPass(Node ノード、 int acquireCount、ブール値の優先順位) {
- //カウントを取得すると合格 0以下です。
- 取得カウント <= 0 の場合
- 戻る 真実;
- }
- // 拒否する場合 カウント 0以下です。
- // それ以外の場合、costTimeは最大になります longとwaitTimeのオーバーフローは いくつかのケース。
- (カウント<= 0)の場合{
- 戻る 間違い;
- }
-
- 現在の時刻が長い = TimeUtil.currentTimeMillis();
- // 2 つのリクエスト間の間隔を計算します。
- // 時間間隔を計算する
- 長いコスト時間 = Math.round(1.0 * (acquireCount) / count * 1000);
-
- // 予想通過時間 このリクエストの。
- // 予想される実行時間
- 長い予想時間 = コスト時間 + 最新の通過時間.get();
-
- // 現在の時刻 > 予想時刻
- 予想時刻 <= 現在の時刻の場合 {
- // ここで競合が発生する可能性がありますが、問題ありません。
- // 通過可能、最終通過時間の設定が可能
- 最新の経過時間を設定します(現在の時間) ;
- 戻る 真実;
- }それ以外{
- //時間を計算する 待つ。
- // 待ち時間 = 予想時間 - 前回の時間 - 現在時間
- 長い待機時間 = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
- // 待機時間 > 最大キュー時間
- 待機時間 > 最大キューイング時間ミリ秒の場合 {
- 戻る 間違い;
- }それ以外{
- // 最終時間 + 間隔時間
- 長い古い時間 = 最新の経過時間.addAndGet(コスト時間);
- 試す {
- // 待ち時間
- 待機時間 = 古い時間 - TimeUtil.currentTimeMillis();
- // 待機時間 > 最大キュー時間
- 待機時間 > 最大キューイング時間ミリ秒の場合 {
- 最新の経過時間。addAndGet(-コスト時間);
- 戻る 間違い;
- }
- // 競合状態ではwaitTime は 0 未満になる可能性がある
- // 寝て待つ
- 待機時間 > 0 の場合
- スレッドをスリープ状態にします。
- }
- // 待機後、解放
- 戻る 真実;
- } キャッチ (InterruptedException e) {
- }
- }
- }
- 戻る 間違い;
- }
トークンバケットアルゴリズムトークン バケット アルゴリズムは、ネットワーク トラフィック シェーピングとレート制限で最も一般的に使用されるアルゴリズムです。通常、トークン バケット アルゴリズムは、ネットワークに送信されるデータの量を制御し、データのバーストを送信できるようにするために使用されます。次の図に示すように: 簡単に言えば、一方がリクエスト時にバケット内のトークンを消費し、もう一方が一定の割合でバケットにトークンを入れます。消費されるリクエストが、入力されるレートよりも大きい場合は、待機または拒否などの適切な措置を講じます。 実装コードの例: - パブリッククラスTokenBucket {
- public long timeStamp = System.currentTimeMillis(); // 現在の時刻
- public long capacity; // バケット容量
- public long rate; //トークン挿入速度
- public long tokens; // 現在のトークン数
-
- パブリックブールグラント(){
- 現在の長さ = System.currentTimeMillis();
- // 最初にトークンを追加します
- トークン = Math.min (容量、トークン + (現在 -タイムスタンプ) * レート);
- タイムスタンプ= 現在;
- (トークン数<1)の場合{
- // トークンが1つ未満の場合は拒否します
- 戻る 間違い;
- }それ以外{
- // トークンもあります。トークンを取得します
- トークン -= 1;
- 戻る 真実;
- }
- }
- }
Sentinel は、WarmUpController のトークン バケット アルゴリズムを使用します。このアルゴリズムでは、システムを予熱し、予熱時間と水位を設定し、予熱期間中に過剰な要求を直接拒否することができます。 - パブリックブールcanPass(Node ノード、 int acquireCount、ブール値の優先順位) {
- 長い passQps = (長い) node.passQps();
-
- 長い previousQps = (long) node.previousPassQps();
- syncToken(前のQps);
-
- // 傾斜の計算を開始する
- // 警告ラインに入ったら、QPS の調整を開始します
- 長いrestToken = 格納されたTokens.get();
- (restToken >= 警告トークン)の場合{
- long aboveToken = restToken - warningToken;
- // 消費率は警告より速いが、
- //現在の間隔 = restToken*slope+1/ count
- ダブル警告Qps = Math.nextUp(1.0 / (aboveToken * 傾き + 1.0 /カウント));
- (passQps + acquireCount <= warningQps)の場合{
- 戻る 真実;
- }
- }それ以外{
- (passQps + acquireCount <= count )の場合{
- 戻る 真実;
- }
- }
-
- 戻る 間違い;
- }
電流制限アルゴリズムの概要カウンターと時間ウィンドウタイム ウィンドウ アルゴリズムの本質も、カウンター アルゴリズムを通じて実装されます。 時間ウィンドウ アルゴリズムが分割されるグリッドの数が多いほど、スライディング ウィンドウのスクロールがスムーズになり、現在の制限統計がより正確になりますが、メモリ ストレージの消費量も増加します。 リーキーバケットとトークンバケットリーキー バケット アルゴリズムとトークン バケット アルゴリズムは、本質的には、大量のトラフィックによるシステムのクラッシュを防ぐためのトラフィック シェーピングまたはレート制限を目的としています。ただし、この 2 つのアルゴリズムの主な違いは、フロー制限の方向が逆であることです。 リーキーバケット: 比較的固定されたトラフィックの流出率を制限します。 トークン バケット: トラフィックの平均流入速度を制限し、ある程度の突発的なトラフィックを許可します。最大速度はバケットの容量とトークンが生成される速度です。 いくつかのシナリオでは、リーキー バケットの漏洩率が比較的固定されているため、リーキー バケット アルゴリズムはネットワーク リソースを効果的に使用できません。そのため、ネットワークの状態が比較的良好で、混雑がない場合でも、リーキー バケットは制限されたままになり、その量を解放できないため、ネットワーク リソースを効果的に活用できません。トークン バケット アルゴリズムは異なります。平均レートを制限しながら、ある程度のバースト トラフィックをサポートします。 参照ドキュメントhttps://www.cnblogs.com/linjiqin/p/9707713.html https://www.cnblogs.com/dijia478/p/13807826.html |