推奨読書:
くそー、それはカウボーイです、プログラマーはこれらのアルゴリズムに47日間懸命に取り組んでおり、4サイドのバイトがオファーを獲得しました
崇拝!Byte Great Godによって要約された666ページのマスターレベルアルゴリズムブック、LeetCodeを数分で殺す
カウンター固定ウィンドウアルゴリズム
原理
カウンター固定ウィンドウアルゴリズムは、最も基本的で最も単純な電流制限アルゴリズムです。原則として、一定の時間枠内でリクエストをカウントします。リクエストの数がしきい値を超える場合、そのリクエストは破棄されます。設定されたしきい値に達しない場合、リクエストが受け入れられ、カウントが1増加します。時間枠が終了したら、カウンターを0にリセットします。
コードの実装とテスト
次のように、実装も比較的簡単です。
package project.limiter;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Project: AllForJava
* Title:
* Description:
* Date: 2020-09-07 15:56
* Copyright: Copyright (c) 2020
*
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: [email protected]
* @version 1.0
**/
public class CounterLimiter {
private int windowSize; //窗口大小,毫秒为单位
private int limit;//窗口内限流大小
private AtomicInteger count;//当前窗口的计数器
private CounterLimiter(){}
public CounterLimiter(int windowSize,int limit){
this.limit = limit;
this.windowSize = windowSize;
count = new AtomicInteger(0);
//开启一个线程,达到窗口结束时清空count
new Thread(new Runnable() {
@Override
public void run() {
while(true){
count.set(0);
try {
Thread.sleep(windowSize);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
//请求到达后先调用本方法,若返回true,则请求通过,否则限流
public boolean tryAcquire(){
int newCount = count.addAndGet(1);
if(newCount > limit){
return false;
}else{
return true;
}
}
//测试
public static void main(String[] args) throws InterruptedException {
//每秒20个请求
CounterLimiter counterLimiter = new CounterLimiter(1000,20);
int count = 0;
//模拟50次请求,看多少能通过
for(int i = 0;i < 50;i ++){
if(counterLimiter.tryAcquire()){
count ++;
}
}
System.out.println("第一拨50次请求中通过:" + count + ",限流:" + (50 - count));
//过一秒再请求
Thread.sleep(1000);
//模拟50次请求,看多少能通过
count = 0;
for(int i = 0;i < 50;i ++){
if(counterLimiter.tryAcquire()){
count ++;
}
}
System.out.println("第二拨50次请求中通过:" + count + ",限流:" + (50 - count));
}
}
テスト結果は次のとおりです。
渡された50のリクエストのうち20のみが制限され、30が制限されたため、予想された現在の制限効果が達成されたことがわかります。
特性分析
利点:実装が簡単で理解しやすい。
短所:下図に示すように、「スパー現象」が発生し、流動曲線が十分に滑らかでない場合があります。2つの問題があります。
-
システムサービスが一定期間利用できません(時間枠を超えていない)。たとえば、ウィンドウサイズが1秒で、現在の制限サイズが100の場合、ウィンドウの最初のmsで100リクエストが発生すると、2msから999msのリクエストは拒否されます。この間、ユーザーはシステムサービスが利用できないと感じます。
-
ウィンドウが切り替わると、しきい値トラフィックの2倍のリクエストが生成される場合があります。たとえば、ウィンドウサイズが1秒で、現在の制限サイズが100の場合、特定のウィンドウの999ミリ秒で100のリクエストが発生します。ウィンドウの初期段階ではリクエストがないため、100のリクエストすべてが通過します。次のウィンドウの最初のmsに100のリクエストが来て、すべてが通過した、つまり、2 ms以内に200のリクエストが通過し、設定したしきい値が100で、通過したリクエストがしきい値に達したということが偶然に起こります。ダブル。
カウンタースライディングウィンドウアルゴリズム
原理
カウンタースライディングウィンドウアルゴリズムは、固定ウィンドウが切り替えられたときにしきい値トラフィック要求の2倍の欠点を解決するカウンター固定ウィンドウアルゴリズムを改良したものです。
スライディングウィンドウアルゴリズムは、固定ウィンドウに基づいてタイミングウィンドウをいくつかの小さなウィンドウに分割し、各小さなウィンドウは独立したカウンターを維持します。要求された時間が現在のウィンドウの最大時間よりも長い場合、タイミングウィンドウは小さいウィンドウだけ前方にシフトされます。パンするとき、最初の小さなウィンドウのデータを破棄し、2番目の小さなウィンドウを最初の小さなウィンドウとして設定し、最後に小さなウィンドウを追加し、新しく追加された小さなウィンドウに新しいリクエストを配置します。 。同時に、ウィンドウ全体のすべての小さなウィンドウに対する要求の数が、後で設定されたしきい値を超えないようにする必要があります。
図から、スライディングウィンドウアルゴリズムが固定ウィンドウのアップグレードバージョンであることを確認することは難しくありません。タイミングウィンドウを小さなウィンドウに分割すると、スライディングウィンドウアルゴリズムは固定ウィンドウアルゴリズムに縮退します。スライディングウィンドウアルゴリズムは、実際にはリクエストの数をより細かく制限します。分割されるウィンドウが多いほど、現在の制限はより正確になります。
コードの実装とテスト
package project.limiter;
/**
* Project: AllForJava
* Title:
* Description:
* Date: 2020-09-07 18:38
* Copyright: Copyright (c) 2020
*
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: [email protected]
* @version 1.0
**/
public class CounterSildeWindowLimiter {
private int windowSize; //窗口大小,毫秒为单位
private int limit;//窗口内限流大小
private int splitNum;//切分小窗口的数目大小
private int[] counters;//每个小窗口的计数数组
private int index;//当前小窗口计数器的索引
private long startTime;//窗口开始时间
private CounterSildeWindowLimiter(){}
public CounterSildeWindowLimiter(int windowSize, int limit, int splitNum){
this.limit = limit;
this.windowSize = windowSize;
this.splitNum = splitNum;
counters = new int[splitNum];
index = 0;
startTime = System.currentTimeMillis();
}
//请求到达后先调用本方法,若返回true,则请求通过,否则限流
public synchronized boolean tryAcquire(){
long curTime = System.currentTimeMillis();
long windowsNum = Math.max(curTime - windowSize - startTime,0) / (windowSize / splitNum);//计算滑动小窗口的数量
slideWindow(windowsNum);//滑动窗口
int count = 0;
for(int i = 0;i < splitNum;i ++){
count += counters[i];
}
if(count >= limit){
return false;
}else{
counters[index] ++;
return true;
}
}
private synchronized void slideWindow(long windowsNum){
if(windowsNum == 0)
return;
long slideNum = Math.min(windowsNum,splitNum);
for(int i = 0;i < slideNum;i ++){
index = (index + 1) % splitNum;
counters[index] = 0;
}
startTime = startTime + windowsNum * (windowSize / splitNum);//更新滑动窗口时间
}
//测试
public static void main(String[] args) throws InterruptedException {
//每秒20个请求
int limit = 20;
CounterSildeWindowLimiter counterSildeWindowLimiter = new CounterSildeWindowLimiter(1000,limit,10);
int count = 0;
Thread.sleep(3000);
//计数器滑动窗口算法模拟100组间隔30ms的50次请求
System.out.println("计数器滑动窗口算法测试开始");
System.out.println("开始模拟100组间隔150ms的50次请求");
int faliCount = 0;
for(int j = 0;j < 100;j ++){
count = 0;
for(int i = 0;i < 50;i ++){
if(counterSildeWindowLimiter.tryAcquire()){
count ++;
}
}
Thread.sleep(150);
//模拟50次请求,看多少能通过
for(int i = 0;i < 50;i ++){
if(counterSildeWindowLimiter.tryAcquire()){
count ++;
}
}
if(count > limit){
System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit);
faliCount ++;
}
Thread.sleep((int)(Math.random() * 100));
}
System.out.println("计数器滑动窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数:" + faliCount);
System.out.println("===========================================================================================");
//计数器固定窗口算法模拟100组间隔30ms的50次请求
System.out.println("计数器固定窗口算法测试开始");
//模拟100组间隔30ms的50次请求
CounterLimiter counterLimiter = new CounterLimiter(1000,limit);
System.out.println("开始模拟100组间隔150ms的50次请求");
faliCount = 0;
for(int j = 0;j < 100;j ++){
count = 0;
for(int i = 0;i < 50;i ++){
if(counterLimiter.tryAcquire()){
count ++;
}
}
Thread.sleep(150);
//模拟50次请求,看多少能通过
for(int i = 0;i < 50;i ++){
if(counterLimiter.tryAcquire()){
count ++;
}
}
if(count > limit){
System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit);
faliCount ++;
}
Thread.sleep((int)(Math.random() * 100));
}
System.out.println("计数器滑动窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数:" + faliCount);
}
}
テストでは、スライディングウィンドウのサイズは1000/10 = 100msであり、次に、150ms間隔で50セットの100セットがシミュレーションされます。カウンタースライディングウィンドウアルゴリズムは、カウンター固定ウィンドウアルゴリズムと比較され、次の結果が表示されます。
固定ウィンドウアルゴリズムは、ウィンドウが切り替えられたときにしきい値トラフィック要求の2倍の問題を生成し、スライディングウィンドウアルゴリズムはこの問題を回避します。
特性分析
- 固定ウィンドウが切り替えられたときに、カウンター固定ウィンドウアルゴリズムがしきい値トラフィック要求の2倍を生成する可能性があるという問題を回避します。
- ファンネルアルゴリズムと比較して、新しいリクエストも処理できるため、ファンネルアルゴリズムの空腹の問題を回避できます。
漏斗アルゴリズム
原理
ファンネルアルゴリズムの原理も簡単に理解できます。リクエストが来ると、それは最初にファンネルに入り、次にファンネルが一定のレートでリクエストアウトフローを処理し、フローをスムーズにします。リクエストされたトラフィックが大きすぎる場合、最大キャパシティに到達すると漏斗がオーバーフローし、この時点でリクエストは破棄されます。システムの観点から見ると、リクエストがいつ発生するかはわかりません。また、リクエストがどれだけ速く到着するかわからないため、システムのセキュリティに隠れた危険が生じます。しかし、ファンネルアルゴリズムのレイヤーを追加して電流を制限すると、リクエストが一定のレートで流れるようにすることができます。システムの観点から見ると、リクエストは常にスムーズな送信レートで送信されるため、システムが保護されます。
コードの実装とテスト
package project.limiter;
import java.util.Date;
import java.util.LinkedList;
/**
* Project: AllForJava
* Title:
* Description:
* Date: 2020-09-08 16:45
* Copyright: Copyright (c) 2020
*
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: [email protected]
* @version 1.0
**/
public class LeakyBucketLimiter {
private int capaticy;//漏斗容量
private int rate;//漏斗速率
private int left;//剩余容量
private LinkedList<Request> requestList;
private LeakyBucketLimiter() {}
public LeakyBucketLimiter(int capaticy, int rate) {
this.capaticy = capaticy;
this.rate = rate;
this.left = capaticy;
requestList = new LinkedList<>();
//开启一个定时线程,以固定的速率将漏斗中的请求流出,进行处理
new Thread(new Runnable() {
@Override
public void run() {
while(true){
if(!requestList.isEmpty()){
Request request = requestList.removeFirst();
handleRequest(request);
}
try {
Thread.sleep(1000 / rate); //睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
/**
* 处理请求
* @param request
*/
private void handleRequest(Request request){
request.setHandleTime(new Date());
System.out.println(request.getCode() + "号请求被处理,请求发起时间:"
+ request.getLaunchTime() + ",请求处理时间:" + request.getHandleTime() + ",处理耗时:"
+ (request.getHandleTime().getTime() - request.getLaunchTime().getTime()) + "ms");
}
public synchronized boolean tryAcquire(Request request){
if(left <= 0){
return false;
}else{
left --;
requestList.addLast(request);
return true;
}
}
/**
* 请求类,属性包含编号字符串、请求达到时间和请求处理时间
*/
static class Request{
private int code;
private Date launchTime;
private Date handleTime;
private Request() { }
public Request(int code,Date launchTime) {
this.launchTime = launchTime;
this.code = code;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public Date getLaunchTime() {
return launchTime;
}
public void setLaunchTime(Date launchTime) {
this.launchTime = launchTime;
}
public Date getHandleTime() {
return handleTime;
}
public void setHandleTime(Date handleTime) {
this.handleTime = handleTime;
}
}
public static void main(String[] args) {
LeakyBucketLimiter leakyBucketLimiter = new LeakyBucketLimiter(5,2);
for(int i = 1;i <= 10;i ++){
Request request = new Request(i,new Date());
if(leakyBucketLimiter.tryAcquire(request)){
System.out.println(i + "号请求被接受");
}else{
System.out.println(i + "号请求被拒绝");
}
}
}
}
テストでは、ファンネル電流制限アルゴリズムの容量は5、ファンネルレートは1秒あたり2であり、1〜10の番号が付けられた10個の連続したリクエストがシミュレーションされ、結果は次のようになります。
リクエスト1〜5は受け入れられたが、リクエスト6〜10は拒否されたことがわかります。これは、現時点で目標到達プロセスがオーバーフローしていることを示しています。
受け入れられた5つの要求の処理に注目してみましょう。5つの要求は受け入れられますが、処理は1つずつ(特定の実装によっては必ずしも順序どおりではない)処理されることがわかります。 500ms 1を処理します。これは、ファネルアルゴリズムの特性を反映しています。つまり、リクエストフローは瞬時に生成されますが、リクエストは固定レートで流れて処理されます。ファネルレートを毎秒2に設定しているため、500ミリ秒ごとにファネルがリクエストをリークし、処理します。
特性分析
- 漏出バレルの漏出率は固定されており、これが整流の役割を果たすことができます。つまり、要求されたフローはランダム、大小にかかわらず、ファンネルアルゴリズムの後、固定レートで安定したフローになり、ダウンストリームシステムを保護します。
- 突然の交通の問題を解決することはできません。テストした例を見てみましょう。ファネルレートを毎秒2に設定したところ、突然10のリクエストが届きました。ファネルの容量により、5つのリクエストしか受け入れられず、残りの5つのリクエストは拒否されました。ファネルレートが1秒あたり2つで、5つのリクエストが即座に受け入れられると言うかもしれませんが、これは突然のトラフィックの問題を解決しませんか?いいえ、これらの5つの要求は受け入れられただけで、すぐには処理されませんでした。処理速度は設定どおり1秒あたり2のままなので、トラフィックバーストの問題は解決されません。また、これから説明するトークンバケットアルゴリズムは、トラフィックバーストの問題をある程度解決することができます。読者はそれを比較できます。
トークンバケットアルゴリズム
原理
トークンバケットアルゴリズムは、ファンネルアルゴリズムを改良したものであり、フローを制限するだけでなく、ある程度のトラフィックバーストも可能にします。トークンバケットアルゴリズムには、トークンバケットがあり、アルゴリズムには、一定のレートでトークンをトークンバケットに入れるメカニズムがあります。トークンバケットにも一定の容量があり、いっぱいになるとトークンを入れることができません。リクエストが来ると、最初にトークンバケットに移動してトークンを取得します。トークンが取得されると、リクエストが処理されてトークンが消費されます。トークンバケットが空の場合、リクエストは捨てる。
コードの実装とテスト
package project.limiter;
import java.util.Date;
/**
* Project: AllForJava
* Title:
* Description:
* Date: 2020-09-08 19:22
* Copyright: Copyright (c) 2020
*
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: [email protected]
* @version 1.0
**/
public class TokenBucketLimiter {
private int capaticy;//令牌桶容量
private int rate;//令牌产生速率
private int tokenAmount;//令牌数量
public TokenBucketLimiter(int capaticy, int rate) {
this.capaticy = capaticy;
this.rate = rate;
tokenAmount = capaticy;
new Thread(new Runnable() {
@Override
public void run() {
//以恒定速率放令牌
while (true){
synchronized (this){
tokenAmount ++;
if(tokenAmount > capaticy){
tokenAmount = capaticy;
}
}
try {
Thread.sleep(1000 / rate);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
public synchronized boolean tryAcquire(Request request){
if(tokenAmount > 0){
tokenAmount --;
handleRequest(request);
return true;
}else{
return false;
}
}
/**
* 处理请求
* @param request
*/
private void handleRequest(Request request){
request.setHandleTime(new Date());
System.out.println(request.getCode() + "号请求被处理,请求发起时间:"
+ request.getLaunchTime() + ",请求处理时间:" + request.getHandleTime() + ",处理耗时:"
+ (request.getHandleTime().getTime() - request.getLaunchTime().getTime()) + "ms");
}
/**
* 请求类,属性只包含一个名字字符串
*/
static class Request{
private int code;
private Date launchTime;
private Date handleTime;
private Request() { }
public Request(int code,Date launchTime) {
this.launchTime = launchTime;
this.code = code;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public Date getLaunchTime() {
return launchTime;
}
public void setLaunchTime(Date launchTime) {
this.launchTime = launchTime;
}
public Date getHandleTime() {
return handleTime;
}
public void setHandleTime(Date handleTime) {
this.handleTime = handleTime;
}
}
public static void main(String[] args) throws InterruptedException {
TokenBucketLimiter tokenBucketLimiter = new TokenBucketLimiter(5,2);
for(int i = 1;i <= 10;i ++){
Request request = new Request(i,new Date());
if(tokenBucketLimiter.tryAcquire(request)){
System.out.println(i + "号请求被接受");
}else{
System.out.println(i + "号请求被拒绝");
}
}
}
}
このテストでは、ファンネル電流制限アルゴリズムと比較するために、トークンバケットアルゴリズムの容量も5であり、トークン生成のレートは1秒あたり2であり、1〜10の番号が付けられた10個の連続したリクエストがシミュレーションされます。結果は次のとおりです。
10リクエストの場合、トークンバケットアルゴリズムはファネルアルゴリズムと同じであり、5リクエストが受け入れられ、5リクエストが拒否されます。ファネルアルゴリズムとは異なり、トークンバケットアルゴリズムはこれらの5つのリクエストをすぐに処理し、処理速度は毎秒5と見なすことができます。これは、毎秒2に設定したレートを超え、ある程度のトラフィックバーストを可能にします。。これは、じっくりと体験できる漏斗アルゴリズムとの主な違いでもあります。
特性分析
トークンバケットアルゴリズムは、リーキーバケットアルゴリズムの改良版であり、平均コールレートを制限するだけでなく、ある程度のトラフィックバーストを可能にします。
概要
上記の4つの電流制限アルゴリズムを簡単に要約しましょう。
カウンター固定ウィンドウアルゴリズムは、実装が簡単で理解しやすいものです。ファンネルアルゴリズムと比較して、新しいリクエストもすぐに処理できます。ただし、フロー曲線が十分に滑らかではなく、「スパー現象」が発生し、ウィンドウが切り替えられたときにしきい値フローの2倍の要求が生成される場合があります。カウンタスライディングウィンドウアルゴリズムは、ウィンドウが切り替えられたとき、カウンタの固定ウィンドウアルゴリズムの改善として、効果的に二倍閾値フロー要求の問題を解決します。
じょうごアルゴリズムは、トラフィックを修正して、ランダムで不安定なトラフィックを固定レートで流出させることができますが、突然のトラフィックの問題を解決することはできません。じょうごアルゴリズムの改善として、トークンバケットアルゴリズムはフローをスムーズにするだけでなく、ある程度のフローバーストを可能にします。
上記の4つの電流制限アルゴリズムには独自の特性があり、使用する場合は独自のシナリオに従って選択する必要があります。最適なアルゴリズムはなく、最適なアルゴリズムのみが存在します。たとえば、トークンバケットアルゴリズムは、通常、独自のシステムを保護し、呼び出し元のフローを制限し、バーストトラフィックから独自のシステムを保護するために使用されます。自システムの実際の処理能力が設定されたフロー制限よりも強い場合、ある程度のトラフィックバーストを許可できるため、実際の処理速度は設定された速度よりも高くなり、システムリソースが完全に利用されます。じょうごアルゴリズムは、一般にサードパーティシステムを保護するために使用されます。たとえば、独自のシステムがサードパーティのインターフェースを呼び出す必要があります。サードパーティシステムが独自の呼び出しに圧倒されないように保護するために、じょうごアルゴリズムを使用してフローを制限し、独自のフローを安定させることができます。サードパーティのインターフェースへ。
アルゴリズムは死んでおり、アルゴリズムの本質は学ぶ価値があります。実際のシナリオでは、柔軟に使用できますが、最適なアルゴリズムはなく、最適なアルゴリズムしかありません。