blog

高度に同時的な10億トラフィックシナリオで分散フロー制限を実現するにはどうすればいいのか?これを読んで、私は完全に理解した!

前に書く\nセオリー:「高同時性 10億トラフィック下で分散フロー制限を実現するには?これらの理論は必ずマスターしてください!\nアルゴリズム:「高同時性 10億レベルのトラフィックで分散フロー制限を...

Oct 30, 2020 · 10 min. read
シェア

前に書く

アルゴリズム:

プロジェクトのソースコードはgithubに投稿されました:

先に述べたフロー制限方式の欠点の一つは、グローバルでもなく、分散でもなく、分散シナリオにおける大トラフィックの影響にうまく対処できないことです。そこで次に、億単位規模のトラフィックに対して分散型のフロー制限を実現する方法を紹介します。

分散フロー制限の鍵は、フロー制限サービスをグローバルかつ統一的なものにする必要があることです。RedisとLuaの技術を利用することで、高い並行性と高性能なフロー制限を実現することができます。

Luaは、オープンソースのスクリプトとして標準的なC言語で書かれた軽量でコンパクトなスクリプト・プログラミング言語で、アプリケーションに組み込んで、アプリケーションの柔軟な拡張やカスタマイズができるように設計されています。

Redis+Luaスクリプトによる分散フロー制限のアイデア

Redia+Luaスクリプトは、分散システムの統一されたフルリミットストリーミングを実行するために使用することができます、Redis+LuaはLuaスクリプトを実装しました:

local key = KEYS[1] --フロー制限KEY
local limit = tonumber(ARGV[1]) --フローのサイズを制限する
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --フローリミットのサイズを超えた場合
 return 0
else -- +1,で、有効期限を2秒に設定する
 redis.call("INCRBY", key, "1")
 redis.call("expire", key "2")
 return 1
end

上記のLuaスクリプトのコードは、次のように理解できます。

Luaスクリプトでは、Redisアプリケーション側から渡されるキーやその他のパラメータを受け取るために、KEYSとARGVという2つのグローバル変数が使用されます;

アプリケーション側でKEYSを渡す場合は配列のリストとなり、配列内の値はLuaスクリプトの添え字によって取得されます。

アプリケーション側で ARGV を渡す場合、パラメータはより柔軟で、1 つまたは複数の独立したパラメータにすることができますが、ARGV 配列を受け取る Lua スクリプトに対応し、それを取得する方法も配列の添え字を使用します。

上記の操作はLuaスクリプトで、現在Redisのバージョン5.0を使っているので、実行されるリクエストはシングルスレッドで、Redis+Luaの処理はスレッドセーフでアトミックです。

ここで重要なのは、アトミック演算という知識です。演算が不可分でマルチスレッドセーフであれば、アトミック演算と呼ばれます。

次に、以下のJavaコードを使用して、フロー制限が必要かどうかを判断できます。

//ListLuaのKEYSを設定する[1]
String key = "ip:" + System.currentTimeMillis() / 1000;
List<String> keyList = Lists.newArrayList(key);
//ListLuaのARGVを設定する[1]
List<String> argvList = Lists.newArrayList(String.valueOf(value));
//Luaスクリプトを呼び出して実行する
List result = stringRedisTemplate.execute(redisScript, keyList, argvList)

ここまでは、Redis+Luaスクリプトを使用して分散フロー制限の全体的なアイデアを実現するための簡単な紹介で、LuaスクリプトのコアコードとLuaスクリプトのコアコードを呼び出すJavaプログラムを提供します。次は、Redis+Luaスクリプトを使って実装した分散フロー制限のケースを実際に書いてみましょう。

Redis+Luaスクリプトによる分散フロー制限の例

ここと、「同時多発的な10億トラフィックのシナリオでHTTPインターフェースのフローを制限するには?を読んで理解しました!記事の実装は、また、フローを制限するために、分散、高トラフィックフローシナリオを実現するためのカスタムアノテーションの形式を介して、似ていますが、ここではグローバルな統一されたフロー制限モードを実現するためにRedis + Luaスクリプトの使用。次に、このケースの手動実装と一緒に。

注釈の作成

まずプロジェクトで、以下のコードのようにMyRedisLimiterというアノテーションを定義します。

package io.mykit.limiter.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
 * 
 * @version 1.0.0
 * @description 分散フロー制限のためのカスタムアノテーション
 */
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRedisLimiter {
 @AliasFor("limit")
 double value() default Double.MAX_VALUE;
 double limit() default Double.MAX_VALUE;
}

MyRedisLimiter アノテーションの内部では、value 属性に limit というエイリアスが追加されます。MyRedisLimiter(limit=10)を使うことができます。

カッティング教室の開催

アノテーションを作成したら、ファセットクラス MyRedisLimiterAspect を作成します。 MyRedisLimiterAspect クラスの役割は、@MyRedisLimiter アノテーションを解析し、フローを制限するルールを適用することです。この方法では、フローを制限するための特定のロジックを実装する必要がある各メソッドのフローを制限する必要はなく、@MyRedisLimiterアノテーションを追加するメソッドのフローを制限するだけでよく、具体的なコードは次のとおりです。

package io.mykit.limiter.aspect;
import com.google.common.collect.Lists;
import io.mykit.limiter.annotation.MyRedisLimiter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.List;
/**
 * 
 * @version 1.0.0
 * @description MyRedisLimiter注釈付き切り抜き授業
 */
@Aspect
@Component
public class MyRedisLimiterAspect {
 private final Logger logger = LoggerFactory.getLogger(MyRedisLimiter.class);
 @Autowired
 private HttpServletResponse response;
 @Autowired
 private StringRedisTemplate stringRedisTemplate;
 private DefaultRedisScript<List> redisScript;
 @PostConstruct
 public void init(){
 redisScript = new DefaultRedisScript<List>();
 redisScript.setResultType(List.class);
 redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(("limit.lua"))));
 }
 @Pointcut("execution(public * io.mykit.limiter.controller.*.*(..))")
 public void pointcut(){
 }
 @Around("pointcut()")
 public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
 MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
 //リフレクションを使ってMyRedisLimiterアノテーションを取得する
 MyRedisLimiter myRedisLimiter = signature.getMethod().getDeclaredAnnotation(MyRedisLimiter.class);
 if(myRedisLimiter == null){
 //通常の実装方法
 return proceedingJoinPoint.proceed();
 }
 //アノテーションのパラメーターを取得して、コンフィギュレーションのレートを取得する
 double value = myRedisLimiter.value();
 //ListLuaのKEYSを設定する[1]
 String key = "ip:" + System.currentTimeMillis() / 1000;
 List<String> keyList = Lists.newArrayList(key);
 //ListLuaのARGVを設定する[1]
 List<String> argvList = Lists.newArrayList(String.valueOf(value));
 //Luaスクリプトを呼び出して実行する
 List result = stringRedisTemplate.execute(redisScript, keyList, String.valueOf(value));
 logger.info("Luaスクリプトの実行結果:" + result);
 //Luaこのスクリプトは、トラフィックサイズが超過した場合は0を返し、超過していない場合は1を返す。
 if("0".equals(result.get(0).toString())){
 fullBack();
 return null;
 }
 //トークンを手に入れ、ラインを進む
 return proceedingJoinPoint.proceed();
 }
 private void fullBack() {
 response.setHeader("Content-Type" ,"text/html;charset=UTF8");
 PrintWriter writer = null;
 try{
 writer = response.getWriter();
 writer.println("フォールバックに失敗しました、後でお読みください");
 writer.flush();
 }catch (Exception e){
 e.printStackTrace();
 }finally {
 if(writer != null){
 writer.close();
 }
 }
 }
}

上記のコードは、プロジェクトのクラスパスディレクトリにあるlimit.luaスクリプトファイルを読み込んで、流量制限演算を実行するかどうかを判断し、limit.luaファイルを呼び出して、0が返された結果を実行すると、流量制限ロジックの実装を意味し、それ以外の場合は、流量制限ロジックを実装していません。プロジェクトではLuaスクリプトを使用する必要があるため、次にプロジェクト内でLuaスクリプトを作成する必要があります。

クリエイト・リミット.luaスクリプトファイル

以下のように、プロジェクトのクラスパス・ディレクトリにlimit.luaスクリプト・ファイルを作成します。

local key = KEYS[1] --フロー制限KEY
local limit = tonumber(ARGV[1]) --フローのサイズを制限する
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --フローリミットのサイズを超えた場合
 return 0
else -- +1,で、有効期限を2秒に設定する
 redis.call("INCRBY", key, "1")
 redis.call("expire", key "2")
 return 1
end

limit.luaスクリプトファイルの内容は比較的単純なので、ここでは繰り返しません。

インターフェイスへの注釈の追加

アノテーションクラス、アノテーションをパースするカットオーバークラス、Lua スクリプトファイルの準備がすべて整いました。次に、PayControllerクラスのsendMessage2()メソッドに@MyRedisLimiterアノテーションを追加し、以下のようにlimitプロパティを10に設定します。

@MyRedisLimiter(limit = 10)
@RequestMapping("/boot/send/message2")
public String sendMessage2(){
 //レコード・リターン・インターフェース
 String result = "";
 boolean flag = messageService.sendMessage("成長おめでとう!+1");
 if (flag){
 result = "SMSは正常に送信された!";
 return result;
 }
 result = "おっと、サーバーが落ちたようだ。;
 return result;
}

ここでは、sendMessage2() メソッドは、1 秒間に最大 10 リクエストに制限されています。次に次に、sendMessage2() を JMeter を使ってテストします。

分散フロー制限のテスト

この時点で、圧力テストのためにJMeterを使用します。ここでは、スレッド数を50に設定します。つまり、書き込まれたインターフェイスに同時にアクセスするスレッドが50あります。

以下のようにJemeterを保存して実行します。

実行が完了したら、以下のようにJMeterテストの結果を見に来てください。

テスト結果から、テストの途中で、いくつかのインターフェイスが "Oops, the server has deserted, please try again "というメッセージを返し、インターフェイスが制限されていることがわかります。その後、「SMSの送信に成功しました!これは、インターフェイスが1秒ごとにSMSを送信するように設定されているためです。これは、インターフェイスが1秒間に最大10回のリクエストを受け付けるように設定されているためで、最初の1秒間にインターフェイスにアクセスすると、最初の10回のリクエストは正常に「SMS Sent Successfully!というメッセージが返され、その後インターフェイスは「Oops, the server has deserted, please try again」というメッセージを返します。後のリクエストが "SMS sent successfully!というメッセージを返した場合、後のリクエストはすでに2番目のインターフェイスを呼び出したことになります。

Redis + Luaスクリプトのフロー制限方法を実装する方法を使用すると、クラスタ用のJavaプログラムを展開することができます、この方法は、グローバルな統一フロー制限を達成するために、クライアントがクラスタ内のどのノードにアクセスされているかに関係なく、アクセスがカウントされ、最終的なフロー制限効果を達成します。

Nginx+Lua分散フロー制限の実装

Nginx+Luaで分散フロー制限を実装する方法は、通常アプリケーションの入り口で使用されます。ここでは、分散フロー制限を実装するためにNginx + Luaを使用する方法を説明するための実用的な例の形でも。

まず、Luaスクリプトを作成する必要があり、スクリプトファイルの内容を以下に示します。

local locks = require "resty.lock"
 
local function acquire()
 local lock =locks:new("locks")
 local elapsed, err =lock:lock("limit_key") -- 
 local limit_counter =ngx.shared.limit_counter -- 
 
 local key = "ip:" ..os.time()
 local limit = 5 --フローのサイズを制限する
 local current =limit_counter:get(key)
 
 if current ~= nil and current + 1> limit then --フローリミットのサイズを超えた場合
 lock:unlock()
 return 0
 end
 if current == nil then
 limit_counter:set(key, 1, 1) --最初に有効期限を設定する必要がある場合、keyの値を1に設定すると、有効期限は1秒になる
 else
 limit_counter:incr(key, 1) --2回目からは1を足すだけでよい
 end
 lock:unlock()
 return 1
end
ngx.print(acquire())

実装では、アトミティシティ問題を解決するためにlua-resty-lock相互排他ロックモジュールと、カウンタを実装するためにngx.shared.DICT共有辞書を使用する必要があります。フロー制限が必要な場合は0を、そうでない場合は1を返します。これを使用する場合は、最初に2つの共有辞書を定義する必要があります。

次に、以下のようにNginxのnginx.conf設定ファイルでデータ辞書を定義する必要があります。

http {
  
 lua_shared_dict locks 10m;
 lua_shared_dict limit_counter 10m;
}

魂の拷問

アプリケーションの同時実行性が非常に高い場合、RedisやNginxで対応できるのでしょうか?

つまり、RedisとNginxは基本的に高性能なインターネットコンポーネントであり、一般的なインターネット企業の並行性の高いトラフィックにはまったく問題ありません。なぜそう言えるのですか?次に進みましょう。

アプリケーションのトラフィックが本当に非常に大きい場合は、一貫性ハッシュによって分散フロー制限をスライスすることができます、あなたはまた、アプリケーションレベルのフロー制限にフロー制限をダウングレードすることができます;ソリューションも非常に多く、あなたは実際の状況に応じてそれを調整することができます、フロー制限のためのRedis + Luaの使用は、高度に同時フロー制限の数億レベルを達成するために安定しています。

注意しなければならないのは、高同時性システム、特にこの種のトラフィック数千万、数億レベルの高同時性システムに直面した場合、フローを制限するトリックだけを使用することはできませんが、他のいくつかの対策を追加することもできます。

分散フロー制限については、これまで遭遇したシナリオは、フロー・エントリ・フロー制限ではなく、ビジネス・フロー制限です。トラフィック・エントリー・フローの制限については、アクセス層で行う必要があります。

メリット

Read next

機械学習による分類

通常、バイナリ分類タスクには、正常な状態に属するカテゴリと異常な状態に属するカテゴリが含まれます。 例えば、「非スパム」が正常状態、「スパム」が異常状態です。また、検診のタスクでは「がん未検出」が正常状態、「がん発見」が異常状態。 正常状態のクラスにはカテゴリラベル0が、異常状態のクラスには...

Oct 30, 2020 · 4 min read