マイクロサービスは、大規模で複雑なアプリケーションを小さなアプリケーションに分割することです。そうすることの利点は、各アプリケーションが互いに影響を与えることなく、独立して開発、テスト、および起動されることです。ただし、サービスの分割後は多くの問題があり、マイクロサービスの正常な運用を確保し、サービスガバナンスを実施する必要があります。一般的な方法は、認証、電流制限、ダウングレード、融合などです。
中でも、電流制限とは、インターフェイスの呼び出し頻度が極端に速いためにスレッドリソースが使い果たされて、システム全体の応答が遅くなるのを防ぐために、インターフェイスの呼び出し頻度を制限することです。現在の制限は、スパイクやダブル11など、多くのビジネスで適用されます。ユーザーリクエストの量が多すぎると、システムの安定性を確保するために、後続のインターフェイスリクエストは拒否されます。
インターフェイスの電流制限を実装するという考えは、特定の期間内のインターフェイス呼び出しの数をカウントすることであり、呼び出しの数が設定されたしきい値を超えると、電流制限が実行されてインターフェイスアクセスが制限されます。
一般的な電流制限アルゴリズムには、固定時間ウィンドウアルゴリズム、スライディング時間ウィンドウアルゴリズム、トークンバケットアルゴリズム、リーキーバケットアルゴリズムなどがあります。以下では、各アルゴリズムの実装アイデアとコード実装を1つずつ紹介します。
1.固定時間ウィンドウ電流制限アルゴリズム
1.アルゴリズムの概要
固定時間ウィンドウの電流制限アルゴリズムのアイデアは、期間を決定し、この期間中のインターフェイス呼び出しの数をカウントして、電流を制限するかどうかを決定することです。
実装手順は次のとおりです。
インターフェイス要求が到着した時間の開始点を選択し、
- インターフェイスアクセス時間の数がしきい値よりも少ない場合、アクセスでき、インターフェイスアクセス時間の数は+1です。
- インターフェイスアクセス時間の数がしきい値よりも大きい場合、この期間内の後続のアクセスは電流制限のために拒否され、インターフェイスアクセス時間の数は変更されません。
- 次の時間枠に入ると、カウンタがクリアされ、時刻の開始点が現在の時刻に設定され、次の時間枠に入ります。
概略図は次のとおりです:(
画像ソース:https://time.geekbang.org/column/article/80388?utm_term = zeusNGLWQ&utm_source = xiangqingye&utm_medium = geektime&utm_campaign = end&utm_content = xiangqingyelink1104、次の図は上記と同じです)
この電流制限アルゴリズムの欠点は、2つの時間枠の臨界時間内にバーストトラフィックに対処できないことです。
次の図に示すように、1秒あたりのインターフェイス要求の数が100を超えないと仮定すると、1秒の時間ウィンドウのインターフェイス要求の数は100ですが、それらはすべて最後の10ミリ秒に集中しています。 2秒の時間枠も100であり、すべて最初の時間枠に集中しています。10ミリ秒以内。両方の時間枠のリクエスト数は100未満であり、要件を満たしています。ただし、2つの10ms以内のインターフェース要求の数= 200> 100。この数が200ではなく2000万の場合、システムがクラッシュする可能性があります。
2.コードの実装
public class FixedWindowRateLimitAlg implements RateLimitAlg {
// ms
private static final long LOCK_EXPIRE_TIME = 200L;
private Stopwatch stopWatch;
// 限流计数器
private AtomicInteger counter = new AtomicInteger(0);
private final int limit;
private Lock lock = new ReentrantLock();
public FixedWindowRateLimitAlg(int limit) {
this(limit, Stopwatch.createStarted());
}
public FixedWindowRateLimitAlg(int limit, Stopwatch stopWatch) {
this.limit = limit;
this.stopWatch = stopWatch;
}
@Override
public boolean tryAcquire() throws InterruptedException {
int currentCount = counter.incrementAndGet();
// 未达到限流
if (currentCount < limit) {
return true;
}
// 使用固定时间窗口统计当前窗口请求数
// 请求到来时,加锁进行计数器统计工作
try {
if (lock.tryLock(LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS)) {
// 如果超过这个时间窗口, 则计数器counter归零, stopWatch, 窗口进入下一个窗口
if (stopWatch.elapsed(TimeUnit.MILLISECONDS) > TimeUnit.SECONDS.toMillis(1)) {
counter.set(0);
stopWatch.reset();
}
// 不超过, 则当前时间窗口内的计数器counter+1
currentCount = counter.incrementAndGet();
return currentCount < limit;
}
} catch (InterruptedException e) {
System.out.println("tryAcquire() wait lock too long:" + LOCK_EXPIRE_TIME + " ms");
throw new InterruptedException("tryAcquire() wait lock too long:" + LOCK_EXPIRE_TIME + " ms");
} finally {
lock.unlock();
}
// 出现异常 不能影响接口正常请求
return true;
}
}
2.スライディングタイムウィンドウ電流制限アルゴリズム
1.アルゴリズムの概要
固定時間ウィンドウ電流制限アルゴリズムは、2つの時間ウィンドウの臨界値の突然の増加を処理できません。この問題を解決するために、固定時間ウィンドウの電流制限アルゴリズムをわずかに最適化できます。特定のしきい値を超えないように任意の時間ウィンドウ(たとえば、1S)のインターフェイス要求の数を制限することにより、この最適化されたアルゴリズムはスライディング時間と呼ばれます。ウィンドウ電流制限アルゴリズム。
スライディング時間ウィンドウ電流制限アルゴリズムは、大きな時間ウィンドウをより小さな粒度の時間ウィンドウに分割し、各サブウィンドウは独立して回数をカウントします。サブウィンドウが経過するたびに、ウィンドウ全体が1グリッド右にスライドします。
上の図に示すように、1分あたりのパス数が100を超えないと仮定すると、1分は10秒の6つのセルに分割されます。
最初の図では、最後の10秒間に渡された要求の数(シリアル番号:6)が100回であると想定され、2番目の図では、最初の10秒間の要求の数(シリアル番号: 7)も100回通過します。スライディングウィンドウであるため、最初のウィンドウを1グリッド右に移動した後、2番目のスライディングウィンドウでは、シーケンス番号6と7のリクエストの総数は200> 100であるため、現在の制限が解決され、次のように解決されます。固定時間ウィンドウ2つのウィンドウの臨界値の問題に対処できません。
スライディングタイムウィンドウアルゴリズムは、任意のタイムウィンドウでのインターフェイス要求の数がしきい値を超えないようにすることができますが、10秒以内にセル内のトラフィックが突然増加するなど、よりきめ細かいトラフィックサージシナリオを回避することはできません。すぐに制限することはできません。同時に、スライディングウィンドウアルゴリズムを使用した場合、フローカーブは次のようになり、スムーズな遷移の効果が得られず、流量を制御できません。
2.アルゴリズムの実装
循環キューを使用して、スライディングタイムウィンドウの電流制限アルゴリズムを実装できます。
現在の制限ルールは、インターフェイス要求の数が1秒でN回を超えないことであると想定します。
1S内の要求を記録するために、N + 1(循環キュー自体がストレージユニットを浪費するため、N + 1)循環キューを作成します。
新しいリクエストが来ると、
- 要求から1秒以上離れている要求をキューから削除します(ヘッドポインターを移動します)。
- 循環キューに空き位置があるかどうかを確認します。空き位置がある場合は、キューの最後(テールポインターが配置されている場所)に新しい要求を格納し、同時にテールポインターを移動します。
- 循環キューの最後に空き位置がない場合は、1秒以内の要求数が現在の制限数Nを超えていることを意味し、後続のサービスは拒否されます。
アルゴリズム実装の概略図は次のとおりです。
1S内のリクエスト数が6回を超えることはできないと仮定すると、キュー全体が(6 + 1)セルに分割されます。
- 18:060は、18秒60ミリ秒を意味します。最初の要求が来ると、この時点でキューは空であるため、最初のセル(つまり、ヘッドポインターが指す位置)に格納されます。
- 同様に、18:123、18:336、18:569、18:702、および18:906は、それぞれ2番目、3番目、4番目、5番目、および6番目の要求であり、これらはすべて1秒間隔内にあり、キューには空き位置があります。 、したがって、順番に対応するセルに保存します。
- 19:003リクエストが来ると、1秒間隔を超えるリクエストはないため、他のリクエストを削除する必要はありません。キューの最後に空き位置がないため、1秒以内のリクエスト数を意味します。 6回を超え、リクエストはアクセスを拒否されました。
- 19:406が到着すると、1S間隔を超える18:060、18:123、および18:336の要求があります。これらの3つの要求をキューから削除する必要があり(ヘッドポインターが反時計回りに3セル移動します)、テールポインターは反時計回りです。時針が1グリッド移動した後、現在の要求19:406が保存されます。
3.擬似コードの実装
/**
* @author: wanggenshen
* @date: 2020/6/29 21:56.
* @description: 循环队列实现滑动窗口限流算法
*/
public class SlidingWindowLimiter {
private int windowSize;
private CircularQueue queue;
public SlidingWindowLimiter(int windowSize) {
this.windowSize = windowSize;
queue = new CircularQueue(windowSize);
}
public boolean tryAcquire(long now) {
// 判断是否有间隔1s的请求, 有则移除队列
while (queue.prevNode() != -1 && now - queue.prevNode() > 1000) {
System.out.println("超过1S间隔, 移除超过间隔的节点: " + queue.prevNode() + "当前时间: " + now + ", 间隔: " + (now - queue.prevNode()));
queue.dequeue();
}
// 队列已满, 拒绝访问
if (queue.isFull()) {
System.out.println("队列已满, now: " + now);
return false;
}
queue.enqueue(now);
return true;
}
static class CircularQueue {
/**
* 每次请求的时间戳
*/
private long[] timeQueue;
/**
* 队列大小
*/
private int size;
/**
* 头指针
*/
private int headIndex;
/**
* 尾指针
*/
private int tailIndex;
public CircularQueue(int size) {
// 循环队列尾部指针多占用一个单元格
timeQueue = new long[size + 1];
this.size = size + 1;
}
/**
* 入队
*/
public void enqueue (long timestamp) {
// 队列已满
if (isFull()) {
throw new RuntimeException("Exceed queue size.");
}
timeQueue[tailIndex] = timestamp;
tailIndex = (tailIndex + 1) % size;
}
/**
* 出队
*
* @return
*/
public long dequeue () {
// 队列为空
if (isEmpty()) {
return -1;
}
long timestamp = timeQueue[headIndex];
headIndex = (headIndex + 1) % size;
return timestamp;
}
public long prevNode() {
// 队列为空
if (isEmpty()) {
return -1;
}
return timeQueue[headIndex];
}
public boolean isFull() {
return (tailIndex + 1) % size == headIndex;
}
public boolean isEmpty() {
return tailIndex == headIndex;
}
}
}
3つのリーキーバケットアルゴリズム
1.アルゴリズムの概要
実際、リクエストの数がしきい値を超えた場合、後続のすべてのトラフィックを制限する必要はありませんが、特定の速度内でトラフィックを制御する必要があります。
漏出バケットアルゴリズムは、フローを制御するためのフロー制御に基づいています。
次の図に示すように、発信者の要求は蛇口からの水に例えられ、バケツからの水はインターフェイスプロバイダーによって処理された要求に例えられます。蛇口からの水の流量がバケツ内の水の流量よりも大きい場合(インターフェース呼び出し要求の頻度が速すぎるのと同様)、水は直接オーバーフローします(同様の要求は直接制限されます)。この方法では、トラフィックがしきい値を超えないようにするだけでなく、インターフェイス上の要求の数が安定した速度で処理されるようにします。
リーキーバケットアルゴリズムの利点は、均一な速度で処理されるインターフェイスプロバイダーのインターフェイスを制御できることです。欠点は、レートの不適切な設定がインターフェイス処理の効率に影響を与えることです。
2.コードの実装
擬似コードは次のとおりです。
/**
* @author: wanggenshen
* @date: 2020/6/29 21:00.
* @description: 漏桶限流算法
*/
public class LeakyBucketLimiter {
/**
* 桶内剩余的水
*/
private long left;
/**
* 桶的容量
*/
private long capacity;
/**
* 一桶水漏完的时间
*/
private long duration;
/**
* 桶漏水的速率, capacity = duration*velocity
*/
private double velocity;
/**
* 上一次成功放入水桶的时间
*/
private long lastUpdateTime;
public boolean acquire() {
long now = System.currentTimeMillis();
// 剩余的水量 - 桶匀速漏出去的水
left = Math.max(0, left - (long)((now - lastUpdateTime) * velocity));
// 当前水桶再加一单位水没有溢出, 则可以继续访问
if (left++ <= capacity) {
lastUpdateTime = now;
return true;
} else {
return false;
}
}
}
第四に、トークンバケットアルゴリズム
1.アルゴリズムの概要
トークンバケットアルゴリズムの実装原理は次のとおりです。
一定の割合でトークンを生成し、トークンバケットに配置します。トークンバケットがいっぱいになったら、トークンを破棄し、トークンバケットに配置しなくなります。
リクエストを処理する場合は、トークンバケットからトークンをフェッチする必要があります。トークンを取り出せる場合はリクエストを処理し、トークンがない場合はリクエストを拒否します。
トークンバケットアルゴリズムは、リーキーバケットアルゴリズムと非常によく似ています。主な違いは次のとおりです。
-
リーキーバケットアルゴリズムの入力レートは可変ですが、出力レートは一定です。トークンバケットアルゴリズムの出力レートは、トラフィックの量に応じて調整できます。
-
インターフェースプロセッサの観点から、リーキーバケットアルゴリズムは固定頻度でのみリクエストを処理できます(たとえば、1秒あたり1リクエストしか処理できません。この時点で10リクエストが来ると、リーキーバケットの処理には10秒かかります)。バケットアルゴリズムは、20リクエストなどのバーストトラフィックを処理できます。トークンバケットに20以上のトークンがある場合、プロセッサは20リクエストすべてを一度に処理できます。
2.擬似コードの実装
/**
* @author: wanggenshen
* @date: 2020/6/29 21:00.
* @description: 令牌桶限流算法
*/
public class TokenBucketLimiter {
/**
* 令牌桶桶内剩余的令牌
*/
private long left;
/**
* 令牌桶的容量
*/
private long capacity;
/**
* 一桶水漏完的时间
*/
private long duration;
/**
* 令牌桶生产令牌的速率, capacity = duration*velocity
*/
private double velocity;
/**
* 上一次拿走令牌的时间
*/
private long lastUpdateTime;
public boolean acquire() {
long now = System.currentTimeMillis();
// 令牌桶余量 = 【上一次令牌桶剩余的令牌】+ 【(上一次拿走令牌到现在的时间段) * 每个单位时间生产令牌的速率 】
// 生产出的令牌 超过令牌桶的容量时, 则舍弃
left = Math.min(capacity, left + (long)((now - lastUpdateTime) * velocity));
// 若当前能够成功领取令牌, 则可以访问
if (left-- >= 0) {
lastUpdateTime = now;
return true;
} else {
return false;
}
}
}
実稼働環境では、Guavaが提供するトークンバケットアルゴリズム実装クラスの使用を検討できます。現在の制限にはRateLimiterであり、RateLimiterの実装はスレッドセーフです。
5、分散電流制限
実稼働環境では、サービスは基本的に分散方式で展開されるため、サービス電流を制限する場合は、分散電流制限を考慮する必要があります。
最も簡単な方法は、フロー制御しきい値を各アプリケーションサーバーに均等に分散し、分散された電流制限を単一マシンの電流制限に変換することです。合計トラフィックが1000回を超えない場合、5つのサービスインスタンスがあり、各インスタンスのリクエスト数は200回を超えることはできません。ただし、トラフィックが不均一な場合(たとえば、1台のマシンのトラフィックが常に10で、他のマシンのトラフィックが> 200)、または1台のマシンがダウンしている場合、他のマシンの平均は250> 200であり、あまり良くありません。 。
2つの一般的な実装のアイデアがあります。
- 一元化:サードパーティのサービスは、すべてのサービスインスタンスへの呼び出し数を一律に保存するために使用され、トラフィックを制限するかどうかを決定するのはそれ次第です。このように、サードパーティサービスのダウンタイムによって引き起こされる可用性の問題に注意を払う必要があります。このとき、単一マシンのフロー制御に縮退することができます。
- 分散化:各サービスは同じフロー制御データを個別に保存しますが、同じ状態、つまりCAPのCを維持することは困難です。
集中化のアイデアが一般的に使用されます。
1.TokenServerフロー制御
Sentinelは、TokenServerを独立したサービスとして提供し、総通話量をカウントして、単一のリクエストがアクセスを許可されているかどうかを判断します。アプリケーションサーバーは、要求を受信するたびに、TokenServerと1回通信して、要求にアクセスできるかどうかを判断する必要があります。
この実装方法の利点は次のとおりです。TokenServerは各サービスインスタンスの合計呼び出し量を一元管理し、サービスインスタンスはリクエストの統計的作業を気にする必要がありません。
欠点は、TokenServerがネットワーク上で通信する必要があるため、TokenServerのパフォーマンスに大きく依存することです。同時に、TokenServerサービスの単一ノード障害に関連している必要があります。
2.ストレージフロー制御
ストレージフロー制御とは、各サービスリクエストが来ると、サードパーティのストレージ(Redis、MySQLなど)からインターフェイスリクエストの数を読み取り、リクエストの数を更新してキャッシュに戻すことを意味します。
リクエストの数を受け取った後、各サービスインスタンスはフローを制限する必要があるかどうかを判断します。
総括する
高性能で信頼性の高い分散フロー制御のパフォーマンスを設計するには、ネットワーク通信やロック同期などがパフォーマンスに与える影響を考慮する必要があります。また、分散環境の信頼性も考慮する必要があります。
参照:https://mp.weixin.qq.com/s/joP22Z8zblcDBAV1keSdJw