[マイクロサービス] Springboot の一般的な電流制限スキームの設計と実装

目次

1. 背景

2. 電流制限の概要

2.1 ダボサービスガバナンスモデル

2.1.1 ダボフレームレベルの電流制限

2.1.2 スレッドプールの設定

2.1.3 サードパーティコンポーネントの統合

2.2 SpringCloud サービスのガバナンス モデル

2.2.1 ヒストリックス

2.2.2 センチネル

2.3 ゲートウェイ層の電流制限

3. 一般的な電流制限戦略

3.1 電流制限に一般的に使用されるアルゴリズム

3.1.1 トークンバケットアルゴリズム

3.1.2 リーキーバケットアルゴリズム

3.1.3 スライディングタイムウィンドウ

4. 一般的な電流制限の実装スキーム

4.1 グアバ電流制限に基づく実現

4.1.1 guava 依存関係の導入

4.1.2 カスタム電流制限の注釈

4.1.3 電流制限 AOP クラス

4.1.4 テストインターフェイス

4.2 センチネル電流制限に基づく実現

4.2.1 Sentinel コア依存関係パッケージの導入

4.2.2 カスタム電流制限の注釈

4.2.3 電流制限を実装するカスタム AOP クラス

4.2.4 カスタム テスト インターフェイス

4.3 redis+luaに基づく電流制限の実装

4.3.1 Redis 依存関係の導入

4.3.2 カスタム注釈

4.3.3 カスタム Redis 構成クラス

4.3.4 カスタム電流制限 AOP クラス

4.3.5 lua スクリプトのカスタマイズ

4.3.6 テストインターフェースの追加

5. カスタムスターター電流制限の実装

5.1 事前準備

5.2 コード統合の完了手順

5.2.1 基本的な依存関係をインポートする

5.2.2 カスタム注釈

5.2.3 電流制限の AOP 実装

5.2.4 自動アセンブリ AOP 実装の構成

5.2.5 プロジェクトをインストール用の jar に作成する

5.2.6 上記SDKを他のプロジェクトに導入する

5.2.7 書き込みテストインターフェイス

5.2.8 機能テスト

六、文章の最後に書く


1. 背景

電流制限はマイクロサービス アーキテクチャ システムにとって非常に重要です。そうでないと、マイクロサービスの 1 つがシステム全体の隠れた雪崩要因になってしまいます。なぜそう言えるのでしょうか? たとえば、特定の SAAS プラットフォームには 100 を超えるマイクロサービス アプリケーションがありますが、基になるアプリケーションの 1 つまたは複数が、すべての上位レベルのアプリケーションによって頻繁に呼び出されます。ピークの営業時間中、基になるアプリケーションがストリーム処理に制限されていない場合、アプリケーションは、特に頻繁に呼び出されるインターフェイスの場合、多大なプレッシャーに直面することになります。最も直接的なパフォーマンスは、後続の新しい受信リクエストがブロックされ、キューに入れられ、応答がタイムアウトになることです...最終的には、サービスが JVM リソースを使い果たすまで。

2. 電流制限の概要

ほとんどのマイクロサービス アーキテクチャの設計の開始段階 (テクノロジーの選択段階など) では、アーキテクトはグローバルな観点からテクノロジー スタックの組み合わせを計画します。それとも春の雲?マイクロサービス ガバナンスの基礎となるフレームワークとして。迅速な立ち上げ、反復、配信に対応するためにも、開発は Springboot に直接基づいており、新しいテクノロジー スタックは後で導入されます。

したがって、特定のビジネス シナリオに対する具体的な技術的ソリューションについて語る場合、それを一般化するのではなく、製品やサービスの現状を総合的に評価する必要があります。以下の異なる技術アーキテクチャも同じです。

2.1 ダボサービスガバナンスモデル

基本的なサービス ガバナンスとして dubbo フレームワークを選択することは、内部プラットフォームに偏ったアプリケーションに適しています。Dubbo は最下層で netty を使用します。http プロトコルと比較して、特定のシナリオでは依然として利点があります。dubbo を選択する場合は、選択する必要があります電流制限 プランでは次の参照を行うことができます。

2.1.1 ダボフレームレベルの電流制限

Dubbo は公式に包括的なサービス管理を提供しており、ほとんどの開発シナリオのニーズを満たすことができます。現在の制限シナリオについては、具体的には次のメソッドが含まれています。具体的な構成については、公式マニュアルを参照してください。

  • クライアント電流制限 
    セマフォ電流制限 (統計による) 
    接続数電流制限 (socket->tcp)
  • サーバー側電流制限 
    スレッドプール電流制限(分離手段) 
    セマフォ電流制限(非分離手段) 
    受信数電流制限(ソケット→tcp)

2.1.2 スレッドプールの設定

マルチスレッドの同時操作はスレッド プールと切り離せない必要があり、Dubbo 自体は 4 種類のスレッド プールをサポートしています。<dubbo:protocol>スレッド プールの主要なパラメータは、スレッド プール タイプ、ブロッキング キューのサイズ、コア スレッドの数など、プロデューサー タブで構成できます。プロダクション側でスレッド プールの数を構成することで、現在ある程度の制限効果は得られます。

2.1.3 サードパーティコンポーネントの統合

Springboot フレームワーク プロジェクトの場合は、hystrix、guava、sentinel ネイティブ SDK などのローカル コンポーネントまたは SDK を直接導入することを検討できます。技術力が十分に強い場合は、独自のホイールを構築することも検討できます。

2.2 SpringCloud サービスのガバナンス モデル

Springcloud または springcloud-alibaba をサービス ガバナンス フレームワークとして使用する場合、フレームワーク自体のエコロジーには、すぐに使用できる対応する電流制限コンポーネントがすでに含まれています。ここでは、springcloud フレームワークに基づいて一般的に使用される電流制限コンポーネントをいくつか示します。 。

2.2.1 ヒストリックス

Hystrix は、Netflix によってオープンソース化されたフォールト トレラント フレームワークであり、springcloud が初期段階で市場に投入されたとき、電流制限、融合、およびダウングレードのための springcloud エコシステムのコンポーネントとして使用されました。Hystrix は電流制限機能を提供しており、springcloud アーキテクチャ システムでは、ゲートウェイ上で Hystrix を有効にして電流制限処理を行うことができ、各マイクロサービスでも Hystrix を有効にして電流制限を行うことができます。

Hystrix はデフォルトでスレッド分離モードを使用し、スレッド数 + キュー サイズによって電流を制限できます。具体的なパラメータ設定については、公式 Web サイトの関連情報を参照してください。

2.2.2 センチネル

Sentinel は、分散システムのトラフィック ガードとして知られており、springcloud-alibaba エコロジーの重要なコンポーネントです。分散サービス アーキテクチャのトラフィック制御コンポーネントであり、ホットスポット保護など、開発者がマイクロサービスの安定性を確保するのに役立ちます。

2.3 ゲートウェイ層の電流制限

マイクロサービスの規模が大きくなり、システム全体の多くのマイクロサービスで電流制限を実装する必要がある場合、ゲートウェイ層での電流制限を検討できます。一般的に、ゲートウェイ層での電流制限は、悪意のあるリクエストなどの一般的なビジネス向けです。簡単に言えば、ゲートウェイ レベルでのトラフィック制限は、システム全体に保護層を提供します。

3. 一般的な電流制限戦略

3.1 電流制限に一般的に使用されるアルゴリズム

どのような種類の電流制限コンポーネントであっても、基礎となる電流制限実装アルゴリズムは似ています。理解のために、一般的に使用されるいくつかの電流制限アルゴリズムを以下に示します。

3.1.1 トークンバケットアルゴリズム

トークン バケット アルゴリズムは現在最も広く使用されている電流制限アルゴリズムであり、その名前が示すように、次の 2 つの重要な役割があります。

  • Token  : トークンを取得するリクエストは処理され、他のリクエストはキューに入れられるか、直接破棄されます。
  • Bucket  : トークンを保持するために使用される場所。すべてのリクエストはこのバケットからトークンを取得します。

è¿éæå¥å¾çæè¿°

 トークンバケットには主にトークン生成とトークン取得という 2 つのプロセスが含まれます

3.1.2 リーキーバケットアルゴリズム

リーキー バケット アルゴリズムの前半はトークン バケットに似ていますが、操作の対象が異なります。これは、次の図と併せて理解できます。

トークンバケットはトークンをバケットに入れるもので、リーキーバケットはアクセス要求のデータパケットをバケットに入れるものです。同様に、バケットがいっぱいの場合、新しい受信パケットは破棄されます。

è¿éæå¥å¾çæè¿°

3.1.3 スライディングタイムウィンドウ

以下の図に従って、時間ウィンドウをスライドさせるプロセスを簡単に説明します。

  • 大きな黒い枠はタイムウィンドウで、ウィンドウの時間単位は5秒に設定でき、時間の経過とともに後ろにスライドします。ウィンドウ内の時間を 5 つの小さなグリッドに分割し、各グリッドは 1 秒を表します。このグリッドには、現在の時間内にアクセスされたリクエストの数を計算するカウンターも含まれています。この場合、この時間枠内の訪問の合計数は、すべてのグリッド カウンターの累積値になります。
  • たとえば、毎秒 5 人のユーザー訪問があり、5 秒以内に 10 人のユーザー訪問がある場合、0 から 5 秒までの時間枠内の訪問数は 15 になります。インターフェイスが時間枠内の訪問数の上限を 20 に設定すると、時間が 6 秒目に達すると、この時間枠内のカウントの合計は 10 になります。これは、1 秒のグリッドが時間枠から出たためです。 in 6 秒以内に受信できる訪問数は 20-10=10 です。

è¿éæå¥å¾çæè¿°

 スライディング ウィンドウは実際には計算アルゴリズムであり、タイム ウィンドウ スパンが長いほど電流制限効果が滑らかになるという優れた特徴を持っています。たとえば、現在のタイム ウィンドウが 2 秒しかなく、すべてのアクセス要求が最初の 1 秒に集中している場合、時間が 1 秒戻ると、現在のウィンドウのカウント数が大きく変化します。このようなことが起こる可能性を減らす

4. 一般的な電流制限の実装スキーム

ゲートウェイ層での電流制限はさておき、マイクロサービス アプリケーションでは、テクノロジー スタックの組み合わせ、チーム メンバーの開発レベル、メンテナンスの容易さなどの要素を考慮すると、より一般的なアプローチは、AOP テクノロジー + カスタム アノテーション実装を使用することです。特定のメソッドまたはインターフェイスの電流制限この考えに基づいて、以下では、一般的に使用されるいくつかの電流制限スキームの実装を紹介します。

4.1 グアバ電流制限に基づく実現

Guava は Google によってオープンソース化されている比較的実用的なコンポーネントです。このコンポーネントを使用すると、開発者は従来の電流制限操作を完了することができます。次に、具体的な実装手順を見ていきます。

4.1.1 guava 依存関係の導入

バージョンは上位または他のバージョンを選択できます


    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>23.0</version>
    </dependency>

4.1.2 カスタム電流制限の注釈

電流制限用のアノテーションをカスタマイズすると、電流制限が必要なメソッドまたはインターフェイスにこのアノテーションを追加するだけで済みます。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RateConfigAnno {

    String limitType();

    double limitCount() default 5d;
}

4.1.3 電流制限 AOP クラス

AOP 事前通知を通じて上記のカスタム電流制限アノテーションを追加するメソッドをインターセプトし、アノテーション内の属性値を解析し、その属性値を guava が提供する電流制限パラメータとして使用します。このクラスは、実装全体。

import com.alibaba.fastjson2.JSONObject;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Objects;

@Aspect
@Component
public class GuavaLimitAop {

    private static Logger logger = LoggerFactory.getLogger(GuavaLimitAop.class);

    @Before("execution(@RateConfigAnno * *(..))")
    public void limit(JoinPoint joinPoint) {
        //1、获取当前的调用方法
        Method currentMethod = getCurrentMethod(joinPoint);
        if (Objects.isNull(currentMethod)) {
            return;
        }
        //2、从方法注解定义上获取限流的类型
        String limitType = currentMethod.getAnnotation(RateConfigAnno.class).limitType();
        double limitCount = currentMethod.getAnnotation(RateConfigAnno.class).limitCount();
        //使用guava的令牌桶算法获取一个令牌,获取不到先等待
        RateLimiter rateLimiter = RateLimitHelper.getRateLimiter(limitType, limitCount);
        boolean b = rateLimiter.tryAcquire();
        if (b) {
            System.out.println("获取到令牌");
        }else {
            HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
            JSONObject jsonObject=new JSONObject();
            jsonObject.put("success",false);
            jsonObject.put("msg","限流中");
            try {
                output(resp, jsonObject.toJSONString());
            }catch (Exception e){
                logger.error("error,e:{}",e);
            }
        }
    }

    private Method getCurrentMethod(JoinPoint joinPoint) {
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method target = null;
        for (Method method : methods) {
            if (method.getName().equals(joinPoint.getSignature().getName())) {
                target = method;
                break;
            }
        }
        return target;
    }

    public void output(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(msg.getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            outputStream.flush();
            outputStream.close();
        }
    }
}

このうち、電流制限のコア API は RateLimiter のオブジェクトであり、関連する RateLimitHelper クラスは次のとおりです。

import com.google.common.util.concurrent.RateLimiter;

import java.util.HashMap;
import java.util.Map;

public class RateLimitHelper {

    private RateLimitHelper(){}

    private static Map<String,RateLimiter> rateMap = new HashMap<>();

    public static RateLimiter getRateLimiter(String limitType,double limitCount ){
        RateLimiter rateLimiter = rateMap.get(limitType);
        if(rateLimiter == null){
            rateLimiter = RateLimiter.create(limitCount);
            rateMap.put(limitType,rateLimiter);
        }
        return rateLimiter;
    }

}

4.1.4 テストインターフェイス

上記のコードが機能するかどうかをテストするには、以下のテスト インターフェイスを追加します。

@RestController
public class OrderController {

    //localhost:8081/save
    @GetMapping("/save")
    @RateConfigAnno(limitType = "saveOrder",limitCount = 1)
    public String save(){
        return "success";
    }

}

インターフェイスでの効果をシミュレートするために、パラメーターを非常に小さく設定します (つまり、QPS は 1)。1 秒あたりのリクエストが 1 を超えると、電流制限のプロンプトが表示されることが予想されます。プロジェクトを開始して、インターフェイスを 1 秒に 1 回リクエストして検証すると、結果は正常に取得でき、結果は次のようになります。

インターフェイスをすばやくブラシすると、次の効果が表示されます。

4.2 センチネル電流制限に基づく実現

多くの学生の意識では、通常、sentinel を実用化するには springcloud-alibaba フレームワークと組み合わせる必要があり、フレームワークと統合した後は、コンソールと併用してより良い結果を達成できます。ネイティブ SDK が利用可能であり、統合はこの方法で行われます。

4.2.1 Sentinel コア依存関係パッケージの導入

    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-core</artifactId>
        <version>1.8.0</version>
    </dependency>

4.2.2 カスタム電流制限の注釈

必要に応じてさらに属性を追加できます

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface SentinelLimitAnnotation {

    String resourceName();

    int limitCount() default 5;

}

4.2.3 電流制限を実装するカスタム AOP クラス

このクラスの実装アイデアは前述の guava の使用法と似ていますが、異なる点は、ここではオリジナルの Sentinel 電流制限関連 API が使用されていることです。プロパティが十分でない場合は、公式ドキュメントを参照してください。学習用なので、ここでは詳しく説明しません。


import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
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.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Aspect
@Component
public class SentinelMethodLimitAop {

    private static void initFlowRule(String resourceName,int limitCount) {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        //设置受保护的资源
        rule.setResource(resourceName);
        //设置流控规则 QPS
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //设置受保护的资源阈值
        rule.setCount(limitCount);
        rules.add(rule);
        //加载配置好的规则
        FlowRuleManager.loadRules(rules);
    }

    @Pointcut(value = "@annotation(com.congge.sentinel.SentinelLimitAnnotation)")
    public void rateLimit() {

    }

    @Around("rateLimit()")
    public Object around(ProceedingJoinPoint joinPoint) {
        //1、获取当前的调用方法
        Method currentMethod = getCurrentMethod(joinPoint);
        if (Objects.isNull(currentMethod)) {
            return null;
        }
        //2、从方法注解定义上获取限流的类型
        String resourceName = currentMethod.getAnnotation(SentinelLimitAnnotation.class).resourceName();
        if(StringUtils.isEmpty(resourceName)){
            throw new RuntimeException("资源名称为空");
        }
        int limitCount = currentMethod.getAnnotation(SentinelLimitAnnotation.class).limitCount();
        initFlowRule(resourceName,limitCount);

        Entry entry = null;
        Object result = null;
        try {
            entry = SphU.entry(resourceName);
            try {
                result = joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } catch (BlockException ex) {
            // 资源访问阻止,被限流或被降级
            // 在此处进行相应的处理操作
            System.out.println("blocked");
            return "被限流了";
        } catch (Exception e) {
            Tracer.traceEntry(e, entry);
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
        return result;
    }

    private Method getCurrentMethod(JoinPoint joinPoint) {
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method target = null;
        for (Method method : methods) {
            if (method.getName().equals(joinPoint.getSignature().getName())) {
                target = method;
                break;
            }
        }
        return target;
    }
}

4.2.4 カスタム テスト インターフェイス

効果をシミュレートするために、ここでは QPS の数を 1 に設定します。

    //localhost:8081/limit
    @GetMapping("/limit")
    @SentinelLimitAnnotation(limitCount = 1,resourceName = "sentinelLimit")
    public String sentinelLimit(){
        return "sentinelLimit";
    }

プロジェクトの開始後、ブラウザーはテストするインターフェイスを呼び出します。1 秒あたり 1 つのリクエストが正常に通過します。

インターフェイスをすばやく更新します。1 秒あたり 1 回を超えると、次のような効果があります。

これは効果を示すためのものであり、実際のプロジェクトで使用する場合は、返された結果をカプセル化することをお勧めします。

4.3 redis+luaに基づく電流制限の実装

Redis はスレッドセーフであり、当然スレッドセーフ機能を備えており、アトミック操作をサポートしています。電流制限サービスは、超高QPSを実行する必要があるだけでなく、電流制限ロジックの実行レベルがスレッドセーフであることも保証します。 Redis のこれらの機能を電流制限に使用すると、スレッドの安全性とパフォーマンスの両方を確保できます。Redis ベースの電流制限実装の完全なフローは次のとおりです。

è¿éæå¥å¾çæè¿°

上記のフローチャートと組み合わせると、全体的な実装アイデアは次のようになります。

  • 入力パラメータの電流制限ルールを指定するluaスクリプトを作成することで、例えば、特定のインターフェースの電流を制限する場合、1つまたは複数のパラメータに基づいて判断し、インターフェースのリクエストを呼び出し、そのインターフェースの数を監視することができます。特定の時間枠内のリクエスト。
  • 電流制限であるため、普遍的であることが最善であり、電流制限ルールはどのインターフェイスにも適用できるため、最も適切な方法はカスタム アノテーションを介して割り込むことです。
  • Spring コンテナーによって管理される構成クラスを提供します。DefaultRedisScript Bean は redisTemplate で提供されます。
  • インターフェイス パラメーターを動的に分析し、インターフェイス パラメーターに従ってルールを照合した後に電流制限をトリガーできるクラスを提供します。

4.3.1 Redis 依存関係の導入

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

4.3.2 カスタム注釈

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisLimitAnnotation {

    /**
     * key
     */
    String key() default "";
    /**
     * Key的前缀
     */
    String prefix() default "";
    /**
     * 一定时间内最多访问次数
     */
    int count();
    /**
     * 给定的时间范围 单位(秒)
     */
    int period();
    /**
     * 限流的类型(用户自定义key或者请求ip)
     */
    LimitType limitType() default LimitType.CUSTOMER;

}

4.3.3 カスタム Redis 構成クラス

import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;

import java.io.Serializable;

@Component
public class RedisConfiguration {

    @Bean
    public DefaultRedisScript<Number> redisluaScript() {
        DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
        redisScript.setResultType(Number.class);
        return redisScript;
    }

    @Bean("redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        //设置value的序列化方式为JSOn
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        //设置key的序列化方式为String
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

}

4.3.4 カスタム電流制限 AOP クラス

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.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;

@Aspect
@Configuration
public class LimitRestAspect {

    private static final Logger logger = LoggerFactory.getLogger(LimitRestAspect.class);

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private DefaultRedisScript<Number> redisluaScript;


    @Pointcut(value = "@annotation(com.congge.config.limit.RedisLimitAnnotation)")
    public void rateLimit() {

    }

    @Around("rateLimit()")
    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        RedisLimitAnnotation rateLimit = method.getAnnotation(RedisLimitAnnotation.class);
        if (rateLimit != null) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ipAddress = getIpAddr(request);
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append(ipAddress).append("-")
                    .append(targetClass.getName()).append("- ")
                    .append(method.getName()).append("-")
                    .append(rateLimit.key());
            List<String> keys = Collections.singletonList(stringBuffer.toString());
            //调用lua脚本,获取返回结果,这里即为请求的次数
            Number number = redisTemplate.execute(
                    redisluaScript,
                    keys,
                    rateLimit.count(),
                    rateLimit.period()
            );
            if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
                logger.info("限流时间段内访问了第:{} 次", number.toString());
                return joinPoint.proceed();
            }
        } else {
            return joinPoint.proceed();
        }
        throw new RuntimeException("访问频率过快,被限流了");
    }

    /**
     * 获取请求的IP方法
     * @param request
     * @return
     */
    private static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }

}

このクラスが行うことは上記 2 つの電流制限対策と似ていますが、ここではコアの電流制限は lua スクリプトを読み取り、lua スクリプトにパラメータを渡すことによって実現されます。

4.3.5 lua スクリプトのカスタマイズ

プロジェクトのリソース ディレクトリに、次の lua スクリプトを追加します。



local key = "rate.limit:" .. KEYS[1]

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 current + 1
end

4.3.6 テストインターフェースの追加

@RestController
public class RedisController {

    //localhost:8081/redis/limit
    @GetMapping("/redis/limit")
    @RedisLimitAnnotation(key = "queryFromRedis",period = 1, count = 1)
    public String queryFromRedis(){
        return "success";
    }

}

効果をシミュレートするために、ここで QPS を 1 に設定します。プロジェクトを開始した後 (事前に Redis サービスを開始しておきます)、インターフェイスを呼び出します。通常の効果は次のとおりです。

インターフェイスをすばやく更新すると、1 秒あたり 1 つを超えるリクエストが発生した場合に次のような効果が見られます。

5. カスタムスターター電流制限の実装

上記では、一般的に使用されるいくつかの電流制限実装をケースを通して紹介しましたが、注意深い学生であれば、これらの電流制限実装が特定のエンジニアリング モジュールに埋め込まれていることがわかります。実際、実際のマイクロサービス開発では、プロジェクトには多くのマイクロサービス モジュールが含まれる場合があります。ホイールの繰り返し作成を減らし、各マイクロサービス モジュールでの個別の実装を避けるために、電流制限ロジックの実装を SDK にカプセル化することを検討できます。つまり、他のマイクロサービス モジュールが参照できるスプリングブート スターターとして使用できます。これは、現在、多くの本番環境で比較的一般的に行われている方法でもあります。次に、具体的な実装を見てみましょう。

5.1 事前準備

空の springboot プロジェクトを作成します。プロジェクトのディレクトリ構造は次の図に示すとおりです。ディレクトリの説明は次のとおりです。

  • 注釈: 電流制限に関連するカスタム注釈を保存します。
  • aop: グアバベースの AOP、センチネルベースの AOP 実装など、さまざまな電流制限実装を保存します。
  • spring.factories: アセンブルされる aop 実装クラスをカスタマイズします。

5.2 コード統合の完了手順

5.2.1 基本的な依存関係をインポートする

これには、次の必要な依存関係が含まれます。その他の依存関係は、独自の状況に応じて合理的に選択できます。

  • スプリングブートスターター;
  • グアバ;
  • スプリングブート自動構成;
  • センチネルコア;
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- guava-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-core</artifactId>
            <version>1.8.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.22</version>
        </dependency>

    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/**</include>
                </includes>
            </resource>
        </resources>
    </build>

5.2.2 カスタム注釈

現在、SDK は 3 つの電流制限方法をサポートしています。つまり、後続の他のマイクロサービス プロジェクトでは、guava に基づくトークン バケット、センチネルに基づく電流制限、およびベースの電流制限の 3 種類のアノテーションを追加することで電流制限を実現できます。 Java 独自のセマフォ電流制限に基づいて、3 つのカスタム アノテーション クラスは次のとおりです。

トークンバケット

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)

public @interface TokenBucketLimiter {
    int value() default 50;
}

セマフォ

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ShLimiter {
    int value() default 50;
}

番兵

@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface SentinelLimiter {

    String resourceName();

    int limitCount() default 50;

}

5.2.3 電流制限の AOP 実装

特定の電流制限は AOP で実装され、考え方は前の章と同様です。つまり、周囲の通知の方法を通じて、まず電流制限の注釈が追加されたメソッドを分析し、次に内部のパラメーターを分析して電流制限を実現します。仕事。

Guava ベースの AOP 実装

import com.alibaba.fastjson2.JSONObject;
import com.congge.annotation.TokenBucketLimiter;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
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.springframework.cglib.core.ReflectUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Aspect
@Component
@Slf4j
public class GuavaLimiterAop {

    private final Map<String, RateLimiter> rateLimiters = new ConcurrentHashMap<String, RateLimiter>();

    @Pointcut("@annotation(com.congge.annotation.TokenBucketLimiter)")
    public void aspect() {
    }

    @Around(value = "aspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        log.debug("准备限流");
        Object target = point.getTarget();
        String targetName = target.getClass().getName();
        String methodName = point.getSignature().getName();
        Object[] arguments = point.getArgs();
        Class<?> targetClass = Class.forName(targetName);
        Class<?>[] argTypes = ReflectUtils.getClasses(arguments);
        Method method = targetClass.getDeclaredMethod(methodName, argTypes);
        // 获取目标method上的限流注解@Limiter
        TokenBucketLimiter limiter = method.getAnnotation(TokenBucketLimiter.class);
        RateLimiter rateLimiter = null;
        Object result = null;
        if (null != limiter) {
            // 以 class + method + parameters为key,避免重载、重写带来的混乱
            String key = targetName + "." + methodName + Arrays.toString(argTypes);
            rateLimiter = rateLimiters.get(key);
            if (null == rateLimiter) {
                // 获取限定的流量
                // 为了防止并发
                rateLimiters.putIfAbsent(key, RateLimiter.create(limiter.value()));
                rateLimiter = rateLimiters.get(key);
            }
            boolean b = rateLimiter.tryAcquire();
            if(b){
                log.debug("得到令牌,准备执行业务");
                result = point.proceed();
            }else {
                HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
                JSONObject jsonObject=new JSONObject();
                jsonObject.put("success",false);
                jsonObject.put("msg","限流中");
                try {
                    output(resp, jsonObject.toJSONString());
                }catch (Exception e){
                    log.error("error,e:{}",e);
                }
            }
        } else {
            result = point.proceed();
        }
        log.debug("退出限流");
        return result;
    }

    public void output(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(msg.getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            outputStream.flush();
            outputStream.close();
        }
    }
}

セマフォに基づくAOP 実装

import com.congge.annotation.ShLimiter;
import lombok.extern.slf4j.Slf4j;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cglib.core.ReflectUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;

@Aspect
@Component
@Slf4j
public class SemaphoreLimiterAop {

    private final Map<String, Semaphore> semaphores = new ConcurrentHashMap<String, Semaphore>();
    private final static Logger LOG = LoggerFactory.getLogger(SemaphoreLimiterAop.class);

    @Pointcut("@annotation(com.congge.annotation.ShLimiter)")
    public void aspect() {

    }

    @Around(value = "aspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        log.debug("进入限流aop");
        Object target = point.getTarget();
        String targetName = target.getClass().getName();
        String methodName = point.getSignature().getName();
        Object[] arguments = point.getArgs();
        Class<?> targetClass = Class.forName(targetName);
        Class<?>[] argTypes = ReflectUtils.getClasses(arguments);
        Method method = targetClass.getDeclaredMethod(methodName, argTypes);
        // 获取目标method上的限流注解@Limiter
        ShLimiter limiter = method.getAnnotation(ShLimiter.class);
        Object result = null;
        if (null != limiter) {
            // 以 class + method + parameters为key,避免重载、重写带来的混乱
            String key = targetName + "." + methodName + Arrays.toString(argTypes);
            // 获取限定的流量
            Semaphore semaphore = semaphores.get(key);
            if (null == semaphore) {
                semaphores.putIfAbsent(key, new Semaphore(limiter.value()));
                semaphore = semaphores.get(key);
            }
            try {
                semaphore.acquire();
                result = point.proceed();
            } finally {
                if (null != semaphore) {
                    semaphore.release();
                }
            }
        } else {
            result = point.proceed();
        }
        log.debug("退出限流");
        return result;
    }

}

センチネルに基づいたAOP 実装

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.congge.annotation.SentinelLimiter;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
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.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Aspect
@Component
public class SentinelLimiterAop {

    private static void initFlowRule(String resourceName,int limitCount) {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        //设置受保护的资源
        rule.setResource(resourceName);
        //设置流控规则 QPS
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //设置受保护的资源阈值
        rule.setCount(limitCount);
        rules.add(rule);
        //加载配置好的规则
        FlowRuleManager.loadRules(rules);
    }

    @Pointcut(value = "@annotation(com.congge.annotation.SentinelLimiter)")
    public void rateLimit() {

    }

    @Around("rateLimit()")
    public Object around(ProceedingJoinPoint joinPoint) {
        //1、获取当前的调用方法
        Method currentMethod = getCurrentMethod(joinPoint);
        if (Objects.isNull(currentMethod)) {
            return null;
        }
        //2、从方法注解定义上获取限流的类型
        String resourceName = currentMethod.getAnnotation(SentinelLimiter.class).resourceName();
        if(StringUtils.isEmpty(resourceName)){
            throw new RuntimeException("资源名称为空");
        }
        int limitCount = currentMethod.getAnnotation(SentinelLimiter.class).limitCount();
        initFlowRule(resourceName,limitCount);

        Entry entry = null;
        Object result = null;
        try {
            entry = SphU.entry(resourceName);
            try {
                result = joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } catch (BlockException ex) {
            // 资源访问阻止,被限流或被降级
            // 在此处进行相应的处理操作
            System.out.println("blocked");
            return "被限流了";
        } catch (Exception e) {
            Tracer.traceEntry(e, entry);
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
        return result;
    }

    private Method getCurrentMethod(JoinPoint joinPoint) {
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method target = null;
        for (Method method : methods) {
            if (method.getName().equals(joinPoint.getSignature().getName())) {
                target = method;
                break;
            }
        }
        return target;
    }

}

5.2.4 自動アセンブリ AOP 実装の構成

上記の spring.factories ファイルをリソース ディレクトリに作成します。内容は次のとおりです。このように構成した後は、現在の SDK の jar を導入した後、すぐに他のアプリケーション モジュールを使用できるようになります。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.congge.aop.SemaphoreLimiterAop,\
  com.congge.aop.GuavaLimiterAop,\
  com.congge.aop.SemaphoreLimiterAop

5.2.5 プロジェクトをインストール用の jar に作成する

このステップはスキップする方が簡単です

5.2.6 上記SDKを他のプロジェクトに導入する

        <dependency>
            <groupId>cm.congge</groupId>
            <artifactId>biz-limit</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

5.2.7 書き込みテストインターフェイス

他のプロジェクトでは、テスト インターフェイスを作成し、上記のアノテーションを使用します。ここでは、guava の電流制限アノテーションを例として説明します。

import com.congge.annotation.TokenBucketLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SdkController {

    //localhost:8081/query
    @GetMapping("/query")
    @TokenBucketLimiter(1)
    public String queryUser(){
        return "queryUser";
    }

}

5.2.8 機能テスト

現在のプロジェクトを開始した後、通常どおりインターフェイスを呼び出し、1 秒に 1 回リクエストし、通常どおり結果を取得します

インターフェイスをすばやく更新すると、QPS が 1 を超えると、電流制限がトリガーされ、次のような効果が見られます。

他の 2 つの電流制限アノテーションに興味のある学生は、引き続きテストと検証を続けることができ、スペースの理由は繰り返されません。

前述のスターター メソッドは、より洗練された電流制限統合メソッドを実装しています。これは実稼働環境でも推奨されるメソッドですが、現在のケースはまだ比較的大まかであり、それを使用する必要がある学生は、次に従ってロジックを改善する必要があります。より良い結果を得るためにさらにカプセル化します。

六、文章の最後に書く

本稿では、大規模な空間におけるマイクロサービスにおける電流制限の実装スキームを実際の事例と組み合わせて詳しく説明します. 電流制限はオペレーティングシステムの安定性にとって非常に重要です. サービスガバナンスの重要な側面と言えます.ご覧になった生徒の皆様、ご視聴ありがとうございました。

おすすめ

転載: blog.csdn.net/zhangcongyi420/article/details/131342759