TraceIdを「取得する」ことを考える問題分析とプロセス

ミドルウェアを使いこなすことは、開発者の基本的なスキルであり、プロの開発者は、ミドルウェアを日常的に使用するだけでなく、本来の設計意図とその背後にあるロジックを探求し、システムがより安定して動作するようにします。開発作業がより効率的になります。

このテーマと組み合わせて、この記事はオンライン アラームの問題から始まり、最初に問題の根本原因を特定し、次に yu (社内で自己開発した yu などの分散リンク追跡システムの設計アイデアと実装方法につながります問題の本質に戻って、@Async のソース コードの基礎となる非同期ロジックと実装の特徴を分析し、MTrace クロススレッド転送の失敗の理由と解決策を示し、最後にステータスを整理します。開発者の日常的な使用と組み合わせた、現在の主流の分散追跡システムの現状 ミドルウェアのシナリオは、いくつかの考えと結論を提案します。

  • 1. 問題の背景と考え方

    • 1.1 問題の背景

    • 1.2 問題の再発と思考

  • 2.詳細な分析

    • 2.1 MTrace と Google Dapper

    • 2.2 @Async による非同期処理のトレーサビリティ

    • 2.3. TraceId を「失う」理由

  • 3. ソリューション

  • 4. 他のスキームとの比較

    • 4.1 ジプキン

    • 4.2 スカイウォーキング

    • 4.3 イーグルアイ

  • 5. まとめ

 1. 問題の背景と考え方 

1.1 問題の背景

オンラインアラームのトラブルシューティングの過程で、リンク情報が少し変わっていることに突然気付きました (ここでは、テストで再現されたコンテンツのみを示しています)。

a82166c4c6d8a49ff19d57360bc05080.png

マシンでは、ログ情報の行「2022-08-02 19:26:34.952 DXMsgRemoteService」に TraceId が含まれていないことが明確にわかり、通話リンク情報が突然停止し、通話を追跡できなくなります。その時。

1.2 問題の再発と思考

オンライン アラームを処理した後、「失われた」TraceId がどこに行ったのかを分析し始めました。最初に TraceId で追跡されていないコードの部分を見つけ、@Async アノテーションの下のメソッドで問題が発生していることを見つけ、無関係なビジネス情報コードを削除し、次のように MTrace 埋め込みポイント メソッドを追加した後に再現コードを追加します。

@SpringBootTest
@RunWith(SpringRunner.class)
@EnableAsync
public class DemoServiceTest extends TestCase {
 @Resource
  private DemoService demoService;
 @Test
  public void testTestAsy() {
  Tracer.serverRecv("test");
  String mainThreadName = Thread.currentThread().getName();
  long mainThreadId = Thread.currentThread().getId();
  System.out.println("------We got main thread: "+ mainThreadName + " - " +  mainThreadId + "  Trace Id: " + Tracer.id() + "----------");
  demoService.testAsy();
 }
}
@Component
public class DemoService {
 @Async
  public void testAsy(){
  String asyThreadName = Thread.currentThread().getName();
  long asyThreadId = Thread.currentThread().getId();
  System.out.println("======Async====");
  System.out.println("------We got asy thread: "+ asyThreadName + " - " +  asyThreadId + "  Trace Id: " + Tracer.id() + "----------");
 }
}

このコードを実行した後、コンソールの実際の出力を見てみましょう。

------We got main thread: main - 1  Trace Id: -5292097998940230785----------
======Async====
------We got asy thread: SimpleAsyncTaskExecutor-1 - 630  Trace Id: null----------

これまでのところ、@Async 非同期配信の過程で TraceId が失われていることがわかりました.この現象の理由を理解した後、次のように考え始めます:

  • MTrace (Meituan が開発した自社開発の分散リンク追跡システム) のような分散リンク追跡システムはどのように設計されていますか?

  • @Async 非同期メソッドはどのように実装されていますか?

  • InheritableThreadLocal、TransmittableThreadLocal、および TransmissibleThreadLocal の違いは何ですか?

  • MTrace のクロススレッド配信スキームが「効果がない」のはなぜですか?

  • @Async シナリオで「失われた」TraceId の問題を解決するにはどうすればよいですか?

  • 現在の分散リンク追跡システムは何ですか? クロススレッド配信の問題をどのように解決しますか?

2.詳細な分析 

| 2.1 MTrace と Google Dapper

MTrace は、Meituan が Google Dapper を参照してサービス間のコール チェーン情報を収集および整理する分散リンク トラッキング システムであり、開発者がシステム パフォーマンスを分析し、アラームの問題を迅速にトラブルシューティングできるようにすることを目的としています。MTrace が分散リンク トラッキング システムをどのように設計するかを理解するには、まず Google Dapper が大規模な分散環境で分散リンク トラッキングを実装する方法を見てください。次の図で完全な分散リクエストを見てみましょう。

ad443dd5cb4c2fa26c7f2c23a3f1108a.png

ユーザーがフロントエンド A にリクエストを送信すると、そのリクエストは 2 つの異なる中間層サービス B と C に分散され、サービス B はリクエストの処理後に結果を返し、サービス C は引き続きバックコールを行う必要があります。エンド サービス D および E を処理し、リクエストの結果が返され、最後にフロント エンド A がそれを要約してユーザーのリクエストに応答します。

この完全なリクエストを振り返ってみると、複数のサービスの分散リクエストを直感的かつ確実に追跡するために、クライアントとサーバーの各グループ間のリクエスト応答と応答時間に最も関心があることがわかります。 Dapper は、各要求と応答に識別子とタイムスタンプを設定することにより、リンク追跡を実装します. この設計思想に基づく基本的な追跡ツリー モデルを次の図に示します:

810716d9323b43f9d3d405dfd47026cc.png

トレース ツリー モデルはスパンで構成されており、それぞれにスパン名、スパン ID、親 ID、およびトレース ID が含まれています。トレース ツリー モデル内のスパン間の呼び出し関係をさらに分析すると、親 ID がなく、スパン ID は 1. ルート サービス呼び出しの場合、スパン ID が小さいほど、チェーンを呼び出すプロセスでサービスがルート サービスに近づきます. モデル内の比較的独立した各スパンをリンクすると、完全なリンク呼び出しレコードが構成されます.さらに見てみましょうスパン内の詳細を参照してください。

b9b0638304f56685f59b4aca00a527f5.png

最も基本的なスパン名、スパン ID、および親 ID に加えて、注釈は重要な役割を果たします。注釈には、RPC 要求を記録する <Strat>、Client Send、Server Recv、Server Send、Client Recv、および <End> 注釈が含まれます。クライアントからサーバーに送信される処理応答タイム スタンプ情報。ここで、foo 注釈はカスタマイズ可能なビジネス データを表し、これらもスパンに記録され、開発者に記録ビジネス情報を提供します。64 ビット整数があります。トレース ID は、グローバルに一意の識別子としてスパンに格納されます。

これまでのところ、Google Dapper は主に各リクエストでスパン情報を構成して分散システムを追跡することを学びましたが、これらの追跡情報を分散リクエストに埋め込むにはどうすればよいでしょうか?

低損失、アプリケーションの透明性、大規模な展開という設計目標を達成するために、Google Dapper は、アプリケーション開発者が少数の共通コンポーネント ライブラリに依存して分散リンクをほぼゼロの投資コストで追跡できるようにサポートします。リンク 他のサービスを呼び出す前に、このトレースのコンテキスト情報が ThreadLocal に保存され、主に軽量でコピーが容易な情報 (スパン ID およびトレース ID と同様) が含まれます。サービス スレッドが応答を受信した後、アプリケーションは開発者が渡すことができるコールバック関数は、サービス情報ログを出力します。

MTrace は、Meituan が Google Dapper のデザイン アイデアに基づいて独自のビジネスと組み合わせて改良および完成した自社開発製品です. 具体的な実装プロセスはここでは繰り返されません. MTrace によって行われた改善に焦点を当てましょう:

  • Meituan の各ミドルウェアにポイントを埋め込み、通話時間や通話結果などの情報を収集します. 埋め込みポイントのコンテキストには、主に転送情報、通話情報、マシン関連情報、およびカスタム情報が含まれます. 各通話リンクの間にグローバルがありますおよび一意の変数 TraceId を使用して、完全な通話とトレース データを記録します。

  • MTrace は、ネットワーク間のデータ伝送において、主に UUID と階層およびコンテキスト関係を表す SpanId の XOR によって生成された TraceId を伝送し、バッチ圧縮レポート、TraceId 集約および SpanId 構築フォームをサポートします。

  • 現在、製品はRPCサービス、HTTPサービス、MySQL、キャッシュ、MQをカバーしており、基本的にフルカバーを達成しています。

  • MTrace は、クロススレッド転送とプロキシをサポートして、埋め込みポイント メソッドを最適化し、開発者のコ​​ストを削減します。

| 2.2 @Asyncの非同期処理トレーサビリティ

@Async アノテーションは Spring3 から提供されていますが、このアノテーションの使用には以下の点に注意する必要があります。

  1. @EnableAsync アノテーションを構成クラスに追加する必要があります。

  2. @Async アノテーションは、非同期で実行されるメソッドをマークできます。また、クラスをマークして、クラスのすべてのメソッドが非同期で実行されることを示すためにも使用できます。

  3. Executor は @Async でカスタマイズできます。

@EnableAsync をエントリ ポイントとして非同期プロセスの分析を開始します. 基本的な構成方法に加えて, 構成クラス AsyncConfigurationSelector の内部ロジックに注目します. デフォルトで JDK インターフェイス プロキシを使用するため, ここではProxyAsyncConfiguration クラスのコード ロジック:

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {
 @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
  Assert.notNull(this.enableAsync, "@EnableAsync annotation metadata was not injected");
  //新建一个异步注解bean后置处理器
  AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
  //如果@EnableAsync注解中有自定义annotation配置则进行设置
  Class<? extends Annotation> customAsyncAnnotation = this.enableAsync.getClass("annotation");
  if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
   bpp.setAsyncAnnotationType(customAsyncAnnotation);
  }
  if (this.executor != null) {
   //设置线程处理器
   bpp.setExecutor(this.executor);
  }
  if (this.exceptionHandler != null) {
   //设置异常处理器
   bpp.setExceptionHandler(this.exceptionHandler);
  }
  //设置是否需要创建CGLIB子类代理,默认为false
  bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
  //设置异步注解bean处理器应该遵循的执行顺序,默认最低的优先级
  bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
  return bpp;
 }
}

ProxyAsyncConfiguration は、親クラス AbstractAsyncConfiguration のメソッドを継承し、AsyncAnnotationBeanPostProcessor の非同期アノテーション Bean ポストプロセッサの定義に焦点を当てています。これを見ると、@Async は主にポスト プロセッサを介してプロキシ オブジェクトを生成し、非同期実行ロジックを実装していることがわかります。

24088b5387fef995af938fed691bf34d.png

クラス図から、AsyncAnnotationBeanPostProcessor が BeanFactoryAware インターフェースを同時に実装していることが直感的にわかるので、setBeanFactory() メソッドに入り、AsyncAnnotationAdvisor の非同期アノテーションの側面が構築されていることがわかり、次に buildAdvice() に入りますAsyncAnnotationAdvisor のメソッドを使用して AsyncExecutionInterceptor クラスを確認し、クラス ダイアグラムを見て、AsyncExecutionInterceptor が MethodInterceptor インターフェイスを実装し、MethodInterceptor が AOP のエントリ ポイントのプロセッサであることを確認します。であるため、invoke ロジックのコードに注目します。

public Object invoke(final MethodInvocation invocation) throws Throwable {
 Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
 Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
 final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
  //首先获取到一个线程池
 AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
 if (executor == null) {
  throw new IllegalStateException("No executor specified and no default executor set on AsyncExecutionInterceptor either");
 }
  //封装Callable对象到线程池执行
 Callable<Object> task = () -> {
  try {
   Object result = invocation.proceed();
   if (result instanceof Future) {
    return ((Future<?>) result).get();
   }
  }
  catch (ExecutionException ex) {
   handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
  }
  catch (Throwable ex) {
   handleError(ex, userDeclaredMethod, invocation.getArguments());
  }
  return null;
 };
  //任务提交到线程池
 return doSubmit(task, executor, invocation.getMethod().getReturnType());
}

@Async が使用するスレッド プールを見てみましょう。determinAsyncExecutor メソッドの getExecutorQualifier によって指定されたデフォルトのスレッド プールはどれであるかに注目してください。

@Override
@Nullable
protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
 Executor defaultExecutor = super.getDefaultExecutor(beanFactory);   
 return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor()); //其中默认线程池是SimpleAsyncTaskExecutor
}

ここまでで、スレッド プールを指定せずに @Async とマークされたメソッドを呼び出すと、Spring が自動的に SimpleAsyncTaskExecutor スレッド プールを作成してメソッドを実行し、非同期実行プロセスを完了することを学びました。

2.3. TraceId を「失う」理由

MTrace の以前の調査と理解を振り返ると、TraceId などの情報が ThreadLocal に送信されて保存されるため、非同期メソッドがスレッドを切り替えると、次の図のようなコンテキスト情報の送信損失の問題が発生します。

e166ed3cf86c8d2e73d04018931e811c.png

ThreadLocal が持つクロススレッド配信スキームについて調べてみましょう。MTrace はどのようなクロススレッド配信スキームを提供しますか? SimpleAsyncTaskExecutor の違いは何ですか? 「見つからない」TraceId の理由を段階的に見つけます。

2.3.1 InheritableThreadLocal、TransmittableThreadLocal、TransmissibleThreadLocal

前の分析で、クロススレッド シナリオのコンテキスト情報が ThreadLocal で失われることがわかったので、ThreadLocal とその拡張クラスの特性がこの問題を解決できるかどうかを見てみましょう。

  • ThreadLocal は、主に ThreadLocal オブジェクトごとに ThreadLocalMap を作成して、オブジェクトと値の間のマッピング関係をスレッドに保存します。ThreadLocal オブジェクトを作成するとき、get() または set() メソッドが呼び出されて、現在のスレッドの ThreadLocal オブジェクトに対応する Entry オブジェクトが検出されます。存在する場合は、Entry の値を取得または設定します。存在しない場合は、値を作成しますThreadLocalMap 新しい Entry オブジェクトで。ThreadLocal クラスのインスタンスは複数のスレッドで共有され、各スレッドには独自の ThreadLocalMap オブジェクトがあり、すべての ThreadLocal オブジェクトのキーと値のペアが独自のスレッドに格納されます。ThreadLocal の実装は比較的単純ですが、ThreadLocalMap の Entry オブジェクトが自動的に削除されないため、不適切に使用するとメモリ リークが発生する可能性があることに注意してください。

  • InheritableThreadLocal の実装は ThreadLocal と似ていますが、違いは、スレッドが子スレッドを作成するときに init() メソッドが呼び出されることです。

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,Boolean inheritThreadLocals) {
 if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  //拷贝父线程的变量
 this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 
 this.stackSize = stackSize;
 tid = nextThreadID();
}

これは、子スレッドが親スレッドの InheritableThreadLocal インスタンスにアクセスできることを意味し、set() メソッドが子スレッドで呼び出されると、子スレッドの InheritableThreadLocal インスタンスに影響を与えることなく、新しい Entry オブジェクトが子スレッド自身の inheritableThreadLocals フィールドに作成されます。親スレッド. エントリ オブジェクト. 同時に、ソース コードによると、Thread の init() メソッドがスレッド構築メソッドにコピーされており、スレッドの再利用のためにスレッド プールで使用する方法がないこともわかります。

  • TransmittableThreadLocal は、クロススレッド転送コンテキストを解決するために Alibaba によって提供される InheritableThreadLocal サブクラスです. スレッド間で転送する必要がある変数を保存するためのホルダーを導入します. 一般的なプロセスについては、以下に示すシーケンス図の分析を参照できます:

31fa965e55ed7bc6925122ee0a89c5ee.png

手順は次のように要約できます: ① Runnable を装飾し、メインスレッドの TTL を TtlRunnable の構築メソッドに渡します; ② サブスレッドの TTL 値をバックアップし、メインスレッドの TTL をサブスレッド (値はオブジェクト参照です, スレッドセーフの問題があるかもしれません); ③ サブスレッドロジックを実行します; ④ 新しく追加されたサブスレッドの TTL を削除し、バックアップをリセットしてサブスレッドの TTL に復元しますこれにより、マルチスレッド環境で ThreadLocal 値の推移性が保証されます。

TransmittableThreadLocal は InheritableThreadLocal の継承の問題を解決しますが、シリアライゼーションとデシリアライゼーション中に ThreadLocalMap を処理する必要があるため、オブジェクトの作成とシリアライゼーションのコストが増加し、サポートするシリアライゼーション フレームワークの数が少なくなり、柔軟性が十分ではありません。

  • TransmissibleThreadLocal は、InheritableThreadLocal クラスを継承し、get()、set()、および remove() メソッドを書き換えます。TransmissibleThreadLocal の実装は、TransmittableThreadLocal の実装に似ています。主な実行ロジックは、送信機のキャプチャ ( ) メソッド、非親スレッドのホルダー変数をフィルター処理する replay( ) メソッド、および replay() によってフィルター処理されたホルダー変数を復元する restore() メソッド:

public class TransmissibleThreadLocal<T> extends InheritableThreadLocal<T> {
 public static class Transmitter {
  public static Object capture() {
   Map<TransmissibleThreadLocal<?>, Object> captured = new HashMap<TransmissibleThreadLocal<?>, Object>();
      //获取所有存储在holder中的变量
   for (TransmissibleThreadLocal<?> threadLocal : holder.get().keySet()) { 
    captured.put(threadLocal, threadLocal.copyValue());
   }
   return captured;
  }
  public static Object replay(Object captured) {
   @SuppressWarnings("unchecked")
   Map<TransmissibleThreadLocal<?>, Object> capturedMap = (Map<TransmissibleThreadLocal<?>, Object>) captured;
   Map<TransmissibleThreadLocal<?>, Object> backup = new HashMap<TransmissibleThreadLocal<?>, Object>();
   for (Iterator<? extends Map.Entry<TransmissibleThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();iterator.hasNext(); ) {
    Map.Entry<TransmissibleThreadLocal<?>, ?> next = iterator.next();
    TransmissibleThreadLocal<?> threadLocal = next.getKey();
    // backup
    backup.put(threadLocal, threadLocal.get());
    // clear the TTL value only in captured
    // avoid extra TTL value in captured, when run task.
        //过滤非传递的变量
    if (!capturedMap.containsKey(threadLocal)) { 
     iterator.remove();
     threadLocal.superRemove();
    }
   }
   // set value to captured TTL
   for (Map.Entry<TransmissibleThreadLocal<?>, Object> entry : capturedMap.entrySet()) {
    @SuppressWarnings("unchecked")
    TransmissibleThreadLocal<Object> threadLocal = (TransmissibleThreadLocal<Object>) entry.getKey();
    threadLocal.set(entry.getValue());
   }
   // call beforeExecute callback
   doExecuteCallback(true);
   return backup;
  }
  public static void restore(Object backup) {
   @SuppressWarnings("unchecked")
   Map<TransmissibleThreadLocal<?>, Object> backupMap = (Map<TransmissibleThreadLocal<?>, Object>) backup;
   // call afterExecute callback
   doExecuteCallback(false);
   for (Iterator<? extends Map.Entry<TransmissibleThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
                       iterator.hasNext(); ) {
    Map.Entry<TransmissibleThreadLocal<?>, ?> next = iterator.next();
    TransmissibleThreadLocal<?> threadLocal = next.getKey();
    // clear the TTL value only in backup
    // avoid the extra value of backup after restore
    if (!backupMap.containsKey(threadLocal)) { 
     iterator.remove();
     threadLocal.superRemove();
    }
   }
   // restore TTL value
   for (Map.Entry<TransmissibleThreadLocal<?>, Object> entry : backupMap.entrySet()) {
    @SuppressWarnings("unchecked")
    TransmissibleThreadLocal<Object> threadLocal = (TransmissibleThreadLocal<Object>) entry.getKey();
    threadLocal.set(entry.getValue());
   }
  }
 }
}

TransmissibleThreadLocal はクロススレッド転送の問題を解決するだけでなく、サブスレッドとメインスレッド間の分離を保証しますが、スパンデータをスレッド間でコピーする場合、シャローコピーを使用するとデータ損失のリスクがあります。最後に、次の表に従って包括的な比較を行うことができます。

f8497570de9445a8facb4da6de52b0dd.png

TransmittableThreadLocal は標準の Java API ではなく、サードパーティ ライブラリによって提供されることを考慮すると、他のライブラリとの互換性の問題があり、実質的にコードの複雑さと使用の難しさが増します。したがって、MTrace が実装することを選択した TransmissibleThreadLocal クラスは、スレッドおよびサービス間でトレース情報を簡単に転送し、すべての非同期実行コンテキストのカスタマイズ可能で標準化されたキャプチャ転送を透過的かつ自動的に完了し、トレース情報全体をより完全かつ正確にします。

2.3.2 Mtrace のクロススレッド配信スキーム

MTrace は実際にこの問題の解決策を提供しています. 主な設計思想は、子スレッドが Runnable オブジェクトを初期化するとき、最初に親スレッドの ThreadLocal に移動して保存されたトレース情報を取得し、それを子スレッドに渡すことです.パラメータとしてスレッド. 損失を避けるためにトレース情報を設定すると、子スレッドが初期化されます. 以下の具体的な実装を見てみましょう。

次の図に示すように、親スレッドが新しいタスクを作成するときに、TransmissibleThreadLocal 内のすべての変数情報をキャプチャします。

4caa554bd7853a5df7416f318ad6a21f.png

子スレッドがタスクを実行すると、次の図に示すように、親スレッドによってキャプチャされた TransmissibleThreadLocal 変数情報がコピーされ、バックアップされた TransmissibleThreadLocal 変数情報が返されます。

2e085b8201a16f92fec0018a4d1c99ce.png

子スレッドがビジネス プロセスを実行すると、次の図に示すように、以前にバックアップされた TransmissibleThreadLocal 変数情報が復元されます。

a0f5a22e1125b744066d3e471db294bd.png

このソリューションは、スレッド間でコンテキストが失われる問題を解決できますが、コードレベルの開発が必要であり、開発者のワークロードが増加します. これは、分散追跡システムには最適なソリューションではありません:

TraceRunnable command = new TraceRunnable(runnable);
newThread(command).start();
executorService.execute(command);

そのため、MTrace は非侵入型の javaagent&instrument テクノロジも提供しており、クラスのロード時に単純に AOP 機能として理解できます. javaagent 構成を JVM パラメータに追加するだけで、Runnable やスレッド プールのコードを変更することなく、起動時に拡張できます 完全なパススレッド全体の問題。

この質問に戻ると、現在使用されている MDP 自体は MTrace-agent モードを統合していますが、なぜ TraceId を「失う」のでしょうか? MTrace の ThreadPoolTransformer クラスと ForkJoinPoolTransformer クラスを見ると、MTrace が ThreadPoolExecutor クラス、ScheduledThreadPoolExecutor クラス、および ForkJoinTask クラスのバイトコードを変更したことがわかります.この考え方に従って、@Async で使用される SimpleAsyncTaskExecutor スレッド プールを見てみましょう。

2.3.3 SimpleAsyncTaskExecutor とは

SimpleAsyncTaskExecutor のコードを深く掘り下げて、実行ロジックを確認しましょう。

public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator implements AsyncListenableTaskExecutor, Serializable {
 private ThreadFactory threadFactory;
 public void execute(Runnable task, long startTimeout) {
  Assert.notNull(task, "Runnable must not be null");
    //isThrottleActive是否开启限流(默认concurrencyLimit=-1,不开启限流)
  if(this.isThrottleActive() && startTimeout > 0L) {  
   this.concurrencyThrottle.beforeAccess();
   this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(task));
   this.concurrencyThrottle.beforeAccess();
   this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(task));
   this.concurrencyThrottle.beforeAccess();
   this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(task));
  } else {
   this.doExecute(task);
  }
 }
 protected void doExecute(Runnable task) {
    //没有线程工厂的话默认创建线程
  Thread thread = this.threadFactory != null?this.threadFactory.newThread(task):this.createThread(task);  
  thread.start();
 }
 public Thread createThread(Runnable runnable) {
    //和线程池不同,每次都是创建新的线程
  Thread thread = new Thread(getThreadGroup(), runnable, nextThreadName());
  thread.setPriority(getThreadPriority());
  thread.setDaemon(isDaemon());
  return thread;
 }
}

ここから、次の特徴を得ることができます。

  • SimpleAsyncTaskExecutor は、サブミットされたタスクを実行するたびに新しいスレッドを開始しますが、これは厳密にはスレッド プールではなく、スレッドの多重化の機能を実現することはできません。

  • 開発者が同時スレッドの上限 (concurrencyLimit) を制御できるようにすると、リソースの調整で特定の役割を果たすことができますが、デフォルトの concurrencyLimit 値は -1 です。つまり、リソースの調整は有効になっておらず、メモリ リークのリスクがあります。

  • Ali の技術的なコーディング規則では、ThreadPoolExecutor を使用してスレッド プールを作成し、リソースが枯渇するリスクを回避する必要があります。

前述の MTrace スレッド プール プロキシ モデルと組み合わせて、引き続き SimpleAsyncTaskExecutor のクラス図を見てみましょう。

bfa9befe0e3aa8af9c2d27ff97e9bca0.png

Spring の TaskExecutor インターフェイスを継承し、その本質は java.util.concurrent.Executor であることがわかります.今回の「失われた」TraceId の問題と組み合わせて、MTrace のクロス プロセスの「失敗」の理由を発見しました。スレッド配信スキーム : MTrace は、javaagent&instrument テクノロジを使用してトレース情報のクロススレッド転送を完了することができましたが、現在のところ、ThreadPoolExecutor、ScheduledThreadPoolExecutor、および ForkJoinTask のバイトコードのみをカバーしており、@Async は、スレッド プールが有効でない場合、デフォルトで SimpleAsyncTaskExecutor を有効にします。本質的には、java.util.concurrent.Executor がカバーされていないため、ThreadLocal の get メソッドが空の情報を取得し、最終的な TraceId 転送が失われます。

 3. ソリューション 

実際、@Async はカスタム スレッド プールの使用をサポートしています. 構成を手動でカスタマイズして ThreadPoolExecutor スレッド プールを構成し、アノテーションで Bean の名前を指定して、対応するスレッド プールに切り替えることができます.次のコード:

@Configuration
public class ThreadPoolConfig {
 @Bean("taskExecutor")
     public Executor taskExecutor() {
  ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
  //设置线程池参数信息
  taskExecutor.setCorePoolSize(10);
  taskExecutor.setMaxPoolSize(50);
  taskExecutor.setQueueCapacity(200);
  taskExecutor.setKeepAliveSeconds(60);
  taskExecutor.setThreadNamePrefix("myExecutor--");
  taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
  taskExecutor.setAwaitTerminationSeconds(60);
  taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
  taskExecutor.initialize();
  return taskExecutor;
 }
}

次に、注釈でこのスレッド プールをマークします。

@SpringBootTest
@RunWith(SpringRunner.class)
@EnableAsync
public class DemoServiceTest extends TestCase {
 @Resource
   private DemoService demoService;
 @Test
   public void testTestAsy() {
  Tracer.serverRecv("test");
  String mainThreadName = Thread.currentThread().getName();
  long mainThreadId = Thread.currentThread().getId();
  System.out.println("------We got main thread: "+ mainThreadName + " - " +  mainThreadId + "  Trace Id: " + Tracer.id() + "----------");
  demoService.testAsy();
 }
}
@Component
public class DemoService {
 @Async("taskExecutor")
   public void testAsy(){
  String asyThreadName = Thread.currentThread().getName();
  long asyThreadId = Thread.currentThread().getId();
  System.out.println("======Async====");
  System.out.println("------We got asy thread: "+ asyThreadName + " - " +  asyThreadId + "  Trace Id: " + Tracer.id() + "----------");
 }
}

出力テーブルの出力を見てください。

------We got main thread: main - 1  Trace Id: -3495543588231940494----------
======Async====
------We got asy thread: SimpleAsyncTaskExecutor-1 - 658  Trace Id: 3495543588231940494----------

最後に、この方法で @Async アノテーションの下でスレッド間で渡された「失われた」TraceId を「取得」できます。

 4. 他のスキームとの比較 

分散追跡システムの誕生からその実質的なブレークスルーまで、Google Dapper の影響を大きく受けています.現在、一般的な分散追跡システムには、Twitter の Zipkin、SkyWalking、Ali の EagleEye、PinPoint、および Meituan の MTrace が含まれます.これらのほとんどは、これらに基づいています. Google Dapper のデザイン アイデア. デザイン アイデアとアーキテクチャの特徴を考慮して、Zipkin、SkyWalking、EagleEye の基本的なフレームワークとクロススレッド ソリューションの紹介に焦点を当てています (以下の内容は、主に公式 Web サイトと著者の要約からのものであり、参考用です。のみであり、技術的なアドバイスを構成するものではありません)。

| | 4.1 ジプキン

Zipkin は、Twitter によって提供されたオープン ソースの分散追跡システムです. Finagle フレームワーク (Scala 言語) に基づくインターフェースを公式に提供しています. 他のフレームワークのインターフェースはコミュニティによって提供されています. 現在は Java, Python, Ruby, C# をサポートしています.など。主流の開発言語とフレームワークであり、その主な機能は、さまざまな異種システムからリアルタイムの監視データを収集することです。次の図に示すように、主に 4 つのコア コンポーネントで構成されます。

d7a052fa2d6a5793d553972092299e71.png

  • Collector : 外部システムから送信された追跡情報を処理し、Zipkin によって内部的に処理される Span 形式に情報を変換して、その後の保存、分析、表示、およびその他の機能をサポートするために主に使用されるコレクター コンポーネント。

  • ストレージ: ストレージ コンポーネントは、コレクターが受信した追跡情報を主に処理し、デフォルトで情報を保存し、ストレージ ポリシーの変更をサポートします。

  • API : API コンポーネント。主にクライアントへの追跡情報の表示や監視のための外部システムへのアクセスなど、外部アクセス インターフェイスを提供するために使用されます。

  • UI : UI コンポーネント、API コンポーネントに基づく上位レベルのアプリケーション。ユーザーは、UI コンポーネントを介して追跡情報を便利かつ直感的に照会および分析できます。

ユーザーが通話を開始すると、Zipkin クライアントは最初に入り口でこの要求に関連する追跡情報を記録し、追跡システムが送信するのを防ぐために、通話リンクで追跡情報を送信し、実際のビジネス プロセスを実行します。遅延と送信 障害はユーザー システムの遅延と中断につながり、トレース情報は非同期で Zipkin Collector に送信され、Zipkin Server は受信後にトレース情報を保存します。Zipkin の Web UI は、API アクセスを介してストレージからトレース情報を抽出し、分析して表示します。

c0a03d76dafd08f01af9c7d00541b0eb.png

最後に、Zipkin のクロススレッド配信スキームの長所と短所を見てみましょう。Zipkin は、単一のスレッド呼び出しで ThreadLocal<TraceContext> ローカルを定義して、スレッド実行プロセス全体で同じ Trace 値を取得しますが、新しいスレッドがこのシナリオでは、Zipkin は、スレッド プールが送信されないシナリオのために InheritableThreadLocal<TraceContext> を提供し、親スレッドと子スレッド間のトレース情報の伝達が失われる問題を解決します。

@Async の使用シナリオについて、Zipkin は CurrentTraceContext クラスを提供して、最初に親スレッドのトレース情報を取得し、次にトレース情報を子スレッドにコピーします. 基本的な考え方は上記の MTrace と同じですが、コード開発が得意で、侵入セックスが強い。

| | 4.2 スカイウォーキング

SkyWalking は、Apache Foundation のオープン ソース アプリケーション パフォーマンス監視システムであり、クラウド ネイティブおよびコンテナー ベースの分散システムを明確に観察する簡単な方法を提供します。多言語プローブをサポート、マイクロカーネル + プラグイン アーキテクチャ、ストレージ、クラスタ管理、およびプラグイン コレクションの使用を自由に選択可能、アラームをサポート、優れた視覚化効果。次の図に示すように、主に 4 つのコア コンポーネントで構成されます。

c3076a23fd14bf382bfca726c83819d2.png

  • プローブ: 異なるソースに基づいて異なる場合がありますが、機能はデータを収集し、データを SkyWalking に適した形式にフォーマットすることです。

  • プラットフォーム バックエンド: データ集約、データ分析、およびプローブからユーザー インターフェイスへのデータ フローを駆動するプロセスをサポートします。分析には、Skywalking のネイティブ トレースとパフォーマンス メトリックのほか、Istio、Envoy テレメトリ、Zipkin トレース形式などのサードパーティ ソースが含まれます。

  • ストレージ: オープン プラグイン インターフェイスを介して SkyWalking データを保存します。ユーザーは、ElasticSearch、H2、または MySQL クラスター (Sharding-Sphere 管理) などの既存のストレージ システムを選択するか、ストレージ システムの実装を指定できます。

  • UI : インターフェイスに基づいて高度にカスタマイズされた Web システムで、ユーザーは SkyWalking データを視覚的に表示および管理できます。

SkyWalking の動作原理は Zipkin の動作原理に似ていますが、Zipkin がシステムにアクセスする方法と比較して、SkyWalking はプラグイン + javaagent の形式を使用して実装します: 仮想マシンによって提供されるインターフェイスを介してドットのコードを動的に追加します。 javaagent premain を介して Java クラスを変更し、システムの実行中にコードを操作するなど、コードを変更するため、ユーザーはコードを変更せずにリンクを追跡できます。これは、ビジネス コードに非侵襲的であり、バイトコード操作テクノロジ (Byte-Buddy ) と AOP の概念により、インターセプトと追跡コンテキスト トレース情報を実装するため、各ユーザーは自分のニーズに応じてインターセプト ポイントを定義するだけでよく、一部のモジュールの分散追跡を実装する必要があります。

69e361c4562385b6433748ef22995546.png

最後に、SkyWalking のクロススレッド転送スキームの長所と短所をまとめましょう: 主流の分散追跡システムと同様に、SkyWalking も ThreadLocal を使用してコンテキスト情報を保存し、クロススレッド転送に遭遇すると転送損失のシナリオにも直面します。親スレッドで ContextManager.capture() を呼び出してトレース情報を ContextSnapshot のインスタンスに保存して返し、ContextSnapshott がタスク オブジェクトの特定の属性にアタッチされ、子スレッドがタスク オブジェクトを処理するときに、最初に ContextSnapshott オブジェクトを取り出し、それを入力パラメーターとして使用して ContextManager.continued(contextSnapshot) を呼び出し、子スレッドに保存します。

全体的な考え方は、実際には主流の分散追跡システムに似ています. 現在、SkyWalking は、@TraceCrossThread で注釈が付けられた Callable、Runnable、および Supplier インターフェイスの実装クラスの拡張インターセプトのみを実装しています. xxxWrapper.of のパッケージ化メソッドを使用することにより、回避します開発または主要なコード変更が必要です。

| | 4.3 イーグルアイ

EagleEye Alibaba のオープン ソース アプリケーション パフォーマンス監視ツールは、多次元、リアルタイム、自動化されたアプリケーション パフォーマンス監視および分析機能を提供します。開発者がアプリケーションのパフォーマンス インジケーター、ログ、例外情報などをリアルタイムで監視し、対応するパフォーマンス分析とレポートを提供して、開発者が問題を迅速に特定して解決するのに役立ちます。主に以下の5つのパートで構成されています。

ded1aa7a92d837b61520642126a5ce0d.png

  • エージェント: エージェントは、Eagle Eye のデータ収集コンポーネントです. エージェントを通じて、アプリケーションのパフォーマンス インジケーター、ログ、および例外情報などのデータを収集し、Eagle Eye のストレージおよび分析コンポーネントに送信できます. プロキシは、HTTP、Dubbo、RocketMQ、Kafka などの複数のプロトコルをサポートしており、さまざまなシナリオでのデータ収集要件を満たすことができます。

  • ストレージ: ストレージは、Eagle Eye のデータ ストレージ コンポーネントであり、エージェントによって収集されたデータを格納し、高可用性、高性能、および信頼性の高いデータ ストレージ サービスを提供します。ストレージは、HBase、Elasticsearch、TiDB など、複数のストレージ エンジンをサポートしており、実際の状況に応じて選択および構成できます。

  • 分析: 分析は Eagle Eye のデータ分析コンポーネントで、エージェントによって収集されたデータのリアルタイム分析と処理を担当し、対応する監視指標とパフォーマンス レポートを生成します。Analysis は、Apache Flink、Apache Spark などの複数の分析エンジンをサポートしており、実際の状況に応じて選択および構成できます。

  • 視覚化: 視覚化は、Eagle Eye のデータ表示コンポーネントであり、分析によって生成された監視インジケーターとパフォーマンス レポートをグラフィカルに表示し、ユーザーがシステムの動作ステータスとパフォーマンス インジケーターを直感的に理解できるようにします。

  • アラーム: アラームは、Eagle Eye のアラーム コンポーネントであり、ユーザーの構成に応じた異常検出とアラーム、システム異常のタイムリーな発見と処理、およびシステム障害の防止を担当します。

SkyWalking のオープンソース コミュニティとは異なり、EagleEye は Alibaba の内部環境の開発に重点を置いており、大規模なリアルタイム モニタリングの問題点を解決するために、基盤となるストリーム コンピューティング、多次元タイミング インジケーター、およびインタラクティブ プラットフォームに対して多数の最適化が行われています。同時に、タイミング検出、根本原因分析、サービス リンク機能などのテクノロジが導入され、問題の発見と位置特定がパッシブからアクティブに変わりました。

EagleEye は StreamLib リアルタイム ストリーム処理技術を採用して、ストリーム コンピューティングのパフォーマンスを向上させ、収集したデータをリアルタイムで分析および処理します. e コマース Web サイトを監視する場合、ユーザーがアクセスしたログ データをリアルタイムで分析し、最適化することができます。分析結果に基づく Web サイト. パフォーマンスとユーザー エクスペリエンス; Apache Flink のスナップショット最適化完全性アルゴリズムを参照して、監視システムの決定性を確保します. さまざまな個々のニーズを満たすために、再利用可能なロジックを「ビルディング ブロック」に変換し、ユーザーが独自のニーズに従って、ストリーム コンピューティングのパイプラインを組み立てます。

151943426b4e63df1816ab0ea5ccd858.png

最後に、EagleEye のクロススレッド転送ソリューションの長所と短所をまとめましょう: EagleEye のソリューションは、ほとんどの分散追跡システムと一致しています. javaagent を使用してスレッド プールの実装を変更し、子スレッドは、スレッド プールからトレース情報を取得できます。 SkyWalking のようなオープン ソース システムで採用されているバイトコード拡張により、EagleEye のほとんどのシナリオは内部で使用されるため、直接エンコード方式もメンテナンスとパフォーマンス消費の点で非常に有利ですが、スケーラビリティとオープン性はあまり高くありません。フレンドリー。

5. まとめ 

この記事は、日常業務における非常に微妙な問題から始めて、主に次の側面を含む、設計のアイデアと分析の背後にある根本的な理由を探ることを目的としています。

  • 問題の本質を把握する: 業務システム アラームで問題のコア コードを把握し、問題の再現を試みて、実際に問題のあるモジュールを見つけます。

  • 設計思想の深い理解: 同社のミドルウェアの製品ドキュメントの参照に基づいて、引き続きソースをたどり、業界リーダーの初期の分散型リンク追跡システムの設計思想と実装方法を学びます。

  • 実際の問題に基づいて質問する: 学習した分散リンク追跡システムの実装プロセスと設計のアイデアと組み合わせて、解決したい TraceId 損失状況の最初に戻り、問題が発生した場所を分析します。

  • ソース コードを読んで基礎となるロジックを見つけます。@Async アノテーション、SimpleAsyncTaskExecutor、および ThreadLocal クラスのソース コードからレイヤーごとに追跡し、基礎となるレイヤーの実際の実装ロジックと特性を分析します。

  • 解決策を見つけるための比較分析: Mtrace のクロススレッド配信スキームが「失敗」した理由を分析し、その理由を見つけて解決策を提供し、他の分散トレーシング システムを要約します。

この記事からわかるように、ミドルウェアの出現は、システムの安定性を維持するために強力なサポートを提供するだけでなく、使用中に発生する可能性のある問題に対して、より効率的な解決策を提供してくれます。落ち着いて、実装ロジックと使用シナリオについて慎重に検討する必要があります. 頭を下げて理解せずに使用すると、特定の問題に対して非常に消極的になり、本当のことを演じることができないことがよくあります.ミドルウェアの価値 ミドルウェアがなくても効率的に問題を解決しながらサポートできない。

 6. この記事の著者 

Li Zhen、Meituan Daodian Business Group/Power Bank Business Department のエンジニア。

 7.参考文献 

[1]大規模な分散型システム トレース インフラストラクチャである Dapper

[2]スレッドローカル

[3]アノテーション インターフェイス非同期

[4] SkyWalking 8 公式中国語ドキュメント

[5] Zipkin アーキテクチャ

[6]アリババ イーグル アイ テクノロジーの復号化

- - - - - 終わり - - - - -

 推奨読書 

  |  Nettyヒープのメモリリーク調査とまとめ

  |  Mysterious Case Tracking: Spring Boot メモリ リークのトラブルシューティング ノート

  Pirate  Middleware: Meituan Service Experience Platform がビジネス データに接続するためのベスト プラクティス

おすすめ

転載: blog.csdn.net/MeituanTech/article/details/130278741