オープン ソース ライブ クラス丨ビッグ データ統合フレームワーク ChunJun クラス ローダー分離スキームの調査と実践

この号では, たゆまぬ同級生の生放送「ChunJun Class Loader Isolation」をレビューします. ChunJun Class Loader Isolation ソリューションは、私たちが最近調査した新しいソリューションです. このソリューションはまだ成熟していません. シェアします.新しいアイデアがある場合は、問題を提出するか、github で私のために PR してください。

1. クラス競合を解決するJavaクラスローダの基本的な考え方

プログラムを学習する前に、まず、クラスの競合を解決するための Java クラス ローダーの基本的な考え方を紹介します。

01 クラスパスとは?

クラスパスは、クラスの検索方法を JVM に指示するために JVM によって使用される環境変数です。

Java はコンパイル済み言語であるため、ソース コード ファイルは .java であり、コンパイル済みの .class ファイルは JVM によって実際に実行できるバイトコードです。したがって、com.dtstack.HelloWorld クラスをロードする場合、JVM は、対応する HelloWorld.class ファイルを検索する場所を認識する必要があります。

したがって、Classpath はディレクトリのコレクションであり、設定される検索パスはオペレーティング システムに関連しています。たとえば、次のようになります。

Windows システムでは、; を使用して区切り、スペースを含むディレクトリは "" で囲みます。これは次のようになります。

C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"

: で区切られた MacOS および Linux システムでは、次のようになります。

/usr/shared:/usr/local/bin:/home/wujuan/bin

JVM の起動時に Classpath 変数を設定すると、実際には -Classpath または -cp パラメータが java コマンドに渡されます。

java -クラスパス .;/Users/lzq/Java/a;/Users/lzq/Java/b com.dtstack.HelloWorld

システム環境変数が設定されておらず、-cp パラメータが渡されていない場合、JVM のデフォルトのクラスパスは現在のディレクトリになります。

Java com.dtstack.HelloWorld

02 Jar パッケージのクラスはいつロードされますか?

●ジャーパッケージ

Jar パッケージは、異なるサフィックス名を持つ zip パッケージです。分散した .class クラスを管理するために使用されます。

jar パッケージを生成するには、zip コマンド zip -r ChunJun.zip ChunJun を使用します。

java -cp ./ChunJun.zip com.dtstack.HelloWorld

●負荷

「ロード」段階は、「クラスのロード」プロセス全体の段階であり、読者がこれら 2 つの一見類似した用語を混同しないことを願っています。読み込みフェーズ中、Java 仮想マシンは次の 3 つのことを行う必要があります。

1. クラスの完全修飾名を使用して、このクラスを定義するバイナリ バイト ストリームを取得します。

2. このバイト ストリームによって表される静的ストレージ構造をメソッド領域のランタイム データ構造に変換します。

3. このクラスを表す java.lang.Class オブジェクトを、メソッド領域内のこのクラスのさまざまなデータへのアクセス エントリとして、メモリ内に生成します。

●分析

クラスまたはインターフェイスの解決

現在のコードが配置されているクラスが D であると仮定すると、未解決のシンボル参照 N がクラスまたはインターフェイス C への直接参照に解析される場合、仮想マシンは解析プロセス全体を完了するため、次の 3 つの手順を含める必要があります。

1. C が配列型でない場合、仮想マシンは N を表す完全修飾名を D のクラス ローダーに渡して、クラス C をロードします。

読み込みプロセス中に、メタデータの検証とバイトコードの検証が必要なため、親クラスやこのクラスの実装されたインターフェイスの読み込みなど、他の関連するクラス読み込みアクションがトリガーされる場合があります。このロード プロセスで何らかの例外が発生すると、解析プロセスは失敗します。

2. C が配列型で、配列の要素型がオブジェクトの場合、つまり、N の記述子はクラスになります。

"[Ljava/lang/Integer のように、最初のポイントのルールに従って配列要素の型をロードします。

N の記述子が上記で想定した形式の場合、読み込まれる要素の型は "java.lang.Integer" であり、仮想マシンは配列の次元と要素を表す配列オブジェクトを生成します。

3. 上記の 2 つの手順で例外がない場合、C は仮想マシンで実際に有効なクラスまたはインターフェイスになっていますが、解析が完了する前に、D が C にアクセスできるかどうかを確認するためにシンボル参照の検証が必要です。アクセス権がないことが判明した場合は、java.lang, llegalAccessError 例外がスローされます。

03 クラスのロードをトリガーするアクションは何ですか?

クラスのロード プロセスの第 1 段階を「ロード」する必要がある状況に関して、Java 仮想マシン仕様には強制的な制約はなく、自由に把握するために仮想マシンの特定の実装に任せることができます。しかし、初期化フェーズについては、"Java 仮想マシン仕様" は、クラスをすぐに "初期化" する必要があるのは 6 つの状況だけであることを厳密に規定しています (そして、ロード、検証、および準備は当然、この前に開始する必要があります)。ファイル

●シーン1

new、getstatic、putstatic、invokestatic の 4 つのバイトコード命令に遭遇したときに、型が初期化されていない場合は、その初期化フェーズを最初にトリガーする必要があります。これら 4 つの命令を生成できる典型的な Java コードのシナリオは次のとおりです。

1. new キーワードを使用してオブジェクトをインスタンス化する場合。

2. 型の静的フィールドを読み取りまたは設定する場合 (final によって変更された静的フィールドを除き、結果はコンパイル時に定数プールに入れられます)。

3. 型の静的メソッドを呼び出す場合。

●シーン2

java.lang.reflect パッケージのメソッドを使用して型へのリフレクション呼び出しを行う場合、型が初期化されていない場合は、最初にその初期化をトリガーする必要があります。

●シーン3

クラスを初期化するときに、親クラスが初期化されていないことが判明した場合は、最初に親クラスの初期化をトリガーする必要があります。

●シーン4

仮想マシンの起動時に、ユーザーは実行するメイン クラス (main() メソッドを含むクラス) を指定する必要があり、仮想マシンはこのメイン クラスを最初に初期化します。

●シナリオ5

JDK 7 で追加された新しい動的言語サポートを使用する場合、java.lang.invoke.MethodHandle インスタンスの最終的な解析結果が REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial の 4 種類のメソッド ハンドルであり、このメソッド ハンドルの対応するクラスが未処理 初期化が過剰な場合は、最初に初期化をトリガーする必要があります。

●シナリオ6

インタフェースがJDK 8によって追加された新しいデフォルト・メソッド(デフォルト・キーワードによって変更されたインタフェース・メソッド)を定義する場合、このインタフェースの実装クラスが初期化される場合、インタフェースはその前に初期化される必要があります。

型の初期化をトリガーする上記の 6 つのシナリオでは、Java 仮想マシン仕様は非常に強力な修飾子を使用します - 「存在するだけ」. それ以外の、初期化をトリガーしない型を参照するすべての方法は、パッシブ参照と呼ばれます。

04 親の委任メカニズムとは何ですか?

親委任メカニズムは、ローダーの階層関係に応じてレイヤーごとに委任することです. たとえば、下図のカスタム クラス ローダーがクラスをロードしたい場合、最初にそれ自体をロードしたくはありませんが、 Delegate は、カスタム クラス ローダー -> App ClassLoader -> Ext ClassLoader -> BootStrap ClassLoader から、読み込みたいクラスが BootStrap ClassLoader に見つからない場合は、逆のサイクルでロードされます。

ファイル

05 親の委任メカニズムを破る方法は?

では、親の委任メカニズムを破るにはどうすればよいのでしょうか? 実は、loadclass メソッドを書き換えることで実現できますが、具体的なプロセスについてはビデオで学習できるので、ここでは詳しく説明しません。

ファイル ファイル

2. Flink クラスローディング分離スキーム

次に、Flink クラス ローディング分離スキームを紹介しましょう. Flink には、Parent-First と Child-First の 2 つのクラス ローダーがあります. それらの違いは次のとおりです:

1.親優先

Java の親委任に似たクラス ローディング メカニズム。Parent First ClassLoader の実際のロジックは URL ClassLoader です。

2.子ども優先

最初に、classloader.parent-first-patterns.default と classloader.parent-first-patterns.additional で接合されたリストを使用して一致させます. クラス名のプレフィックスが一致する場合は、最初に親の委任に移動します. それ以外の場合は、ChildFirstClassLoader を使用して最初にロードします。

チャイルドファーストの問題

新しい ChildFirstClassLoader が作成されるたびに、それが長時間実行されると、Session のような TaskManager が常に閉じられるわけではありません。タスクが何度も実行されると、メタデータ スペースが爆発し、タスクが失敗します。

チャイルド ファースト ローディングの原則

ファイル

ファイル

ファイル ファイル

01 Flink はクラス漏れをどのように回避しますか?

Flink で jira を参照できます。これには、いくつかのバグと処理方法が含まれています。

https://issues.apache.org/jira/browse/FLINK-16245

https://issues.apache.org/jira/browse/FLINK-11205

主に次の 2 つの方法を使用して、Flink がクラスの漏れを回避する方法:

  1. デリゲート クラス ローダーのレイヤーを追加して、実際の UserClassloader をラップします。

  2. コールバック フックを追加して、タスクの終了時に解放されていないリソースを解放するインターフェイスをユーザーに提供します。

KinesisProducer はこのフックを使用します

最終的なランタイム コンテキスト ctx = getRuntimeContext();

ctx.registerUserCodeClassLoaderReleaseHookIfAbsent(

KINESIS_PRODUCER_RELEASE_HOOK_NAME、

()-> this.runClassLoaderReleaseHook

(ctx.getUserCodeClassLoader()));

02 Flink はユーザー コードで動的にロードされたクラスをアンロードします

ユーザー コードで動的にロードされたクラスをアンロードします。動的ユーザー コード クラスのロード (セッション) を含むすべてのシナリオは、再びアンロードされるクラスに依存します。

クラスのアンロードとは、クラスのオブジェクトが参照されなくなったことをガベージ コレクターが検出し、そのクラス (関連するコード、静的変数、メタデータなど) が削除されることを意味します。

TaskManager がタスクを開始または再起動すると、指定されたタスクのコードが読み込まれます. これらのクラスをアンロードできないと、更新されたバージョンのクラスが引き続き読み込まれ、時間の経過とともに蓄積される可能性があるため、メモリ リークが発生する可能性があります. この現象は、多くの場合、OutOfMemoryError: Metaspace の典型的な例外を引き起こします。

クラスリークの一般的な原因と推奨される修正:

● 余韻

アプリケーション コードの関数 /sources/sink がすべてのスレッドを閉じていることを確認します。遅延シャットダウン スレッドは、リソース自体を消費するだけでなく、オブジェクト参照を占有することで、ガベージ コレクションとクラスのアンロードを防ぎます。

●インターン生

関数/ソース/シンクの有効期間を超えて、特別な構造でオブジェクトをキャッシュしないようにします。Guava の Interner、または Avro のシリアライザーのクラスまたはオブジェクトなど。

●JDBC

JDBC ドライバーは、ユーザー クラス ローダーの外部で参照をリークします。これらのクラスが一度だけロードされるようにするために、ドライバー JAR パッケージを Flink の lib/ ディレクトリに配置するか、ドライバー クラスを classloader-parent-first-patterns-additional を介して親が最初にロードされるクラスのリストに追加することができます。

ユーザー コード クラスローダを解放するフックは、動的にロードされたクラスをアンロードするのに役立ちます. このようなフックは、クラスローダがアンロードされる前に実行されます. 通常は、通常の関数のライフサイクル操作 (典型的なclose()メソッドなど) の一部としてリソースを閉じてアンロードすることをお勧めします. 場合によっては (静的フィールドなど)、クラス ローダーが不要になったらすぐにアンロードすることをお勧めします。

クラスローダーを解放するためのフックは、次の方法で実行できます

RuntimeContext.registerUserCodeClassLoaderReleaseHookIfAbsent()登録する方法。

03 Flink は Classloader ソース コードをアンインストールします

BlobLibraryCacheManager$ResolvedClassLoader

プライベートボイドrunReleaseHooks(){

Set<map.entry> hooks = releaseHooks.entrySet();

if (!hooks.isEmpty()) {

    for (Map.EntryhookEntry : hooks) {

        try {

            LOG.debug("Running class loader shutdown hook: {}.", hookEntry.getKey());

            hookEntry.getValue().run();

        } catch (Throwable t) {

            LOG.warn(

                    "Failed to run release hook '{}' for user code class loader.",

                    hookEntry.getValue(),

                    t);

        }

    }

    releaseHooks.clear();

}

}

3. ChunJun がクラス ローディング分離を実装する方法

次に、ChunJun がどのようにクラス ローディング分離を実装しているかを紹介します。

01 Flink jar アップロードのタイミング

最初に、Jar パッケージをアップロードする必要があります. 全体的なプロセスは、次の図に示されています。

ファイル

●ヤーンパージョブ

タスクを送信するときに、jar パッケージをアップロードすると、配置されます

hdfs://flink03:9000/user/root/.flink/application_1654762357754_0140。

●ヤーンセッション

セッションを開始すると、Yarn のアプリは Jar パッケージ メカニズムをアップロードし、タスクをセッションに送信すると、Flink の Blob サーバーがタスクを受信します。

02 Yarn の分散キャッシュ

ファイル

03 Yarn の分散キャッシュ

分散キャッシュ メカニズムは各 NM によって実装されます。主な機能は、アプリケーションが必要とするファイル リソースを、後続のタスクで使用するためにローカルにキャッシュすることです。リソース キャッシュは時間によってトリガーされます。つまり、リソースを使用する最初のタスクがトリガーされ、後続のタスクはキャッシュする必要がなく、直接使用できます。

リソース タイプとリソースの可視性に基づいて、NM はリソースをさまざまなタイプに分類できます。

リソースの可視性の分類

●公開

ノード上のすべてのユーザーがこのリソースを共有できます。1 人のユーザーのアプリケーションがこれらのリソースをローカルにキャッシュしている限り、他のすべてのユーザーのアプリケーションはそれを使用できます。

● 私立

ノード上の同じユーザーのすべてのアプリケーションがリソースを共有します. ユーザーのアプリケーションの1つがリソースをローカルにキャッシュしている限り, ユーザーのすべてのアプリケーションはそれを使用できます.

●お申し込み

このリソースは、ノード上の同じアプリケーションのすべてのコンテナによって共有されます

リソースタイプの分類

●アーカイブ

アーカイブ ファイルは、.jar、.zip、.tar.gz、.tgz、および .tar の 5 種類のアーカイブ ファイルをサポートします。

● ファイル

通常のファイル、NM はそのようなファイルを処理せずにローカル ディレクトリにダウンロードするだけです。

●柄

上記の 2 つのファイルの混合

YARN は、resource、type、timestamp、pattern の 4 つのフィールドが同じかどうかを比較することで、2 つのリソース要求が同じかどうかを判断します。各ノードにキャッシュされたファイルがユーザーによって変更された場合、次に HDFS からファイルを再ダウンロードするために使用されるときに、キャッシュの更新が自動的にトリガーされます。

分散キャッシュの主な機能はファイルのダウンロードであり、これには多数のディスクの読み取りと書き込みが含まれるため、プロセス全体で非同期同時実行モデルを採用してファイルのダウンロード速度を高速化し、同期モデルによってもたらされるパフォーマンスのオーバーヘッドを回避します。

04 Yarn の分散キャッシュ

NodeManager uses round-robin allocation strategy to store these three types of resources in the directory list specified by yarn.nodemanager.local-dirs. 各ディレクトリでは、リソースは次の方法で保存されます。

● 公共リソース

${yarn.nodemanager.local-dirs}/filecache/ ディレクトリに格納され、各リソースはランダムな整数で名前が付けられたディレクトリに個別に格納され、ディレクトリのアクセス許可は 0755 です。

● プライベート リソース

${yarn.nodemanager.local-dirs}/usercache/${user}/filecache/ ディレクトリ (${user} はアプリケーション サブミッターで、デフォルトでは NodeManager イニシエーター) に格納され、各リソースはランダムな整数で名前が付けられたディレクトリに個別に格納され、ディレクトリのアクセス許可は 0710 です。

● アプリケーション リソース

${yarn.nodemanager.local-dirs}/usercache/${user}/${appcache}/${appid}/filecache/ ディレクトリ (${appid} はアプリケーション ID) に保存され、各リソースは保存されますランダムな整数で名前が付けられたディレクトリに個別に配置され、ディレクトリのアクセス許可は0710です。

コンテナの作業ディレクトリは、主に jar パッケージ ファイルとディクショナリ ファイルに対応するソフト リンク。ファイル

05 フリンク BlobServer

ファイル

06 迅速に提出し、jar パッケージのアップロードを減らす方法

Flink libs の下の jar パッケージ、Flink プラグインの下の jar パッケージ、Flink タスクの jar パッケージ (ChunJun、すべてのコネクタおよびコア用)、Flink jar ユーザー定義の jar パッケージ。

● パージョブ

事前に HDFS にアップロードできる場合:

  1. Flink lib、Flink プラグイン、ChunJun jar を事前に HDFS にアップロードします。

  2. タスクを送信するときは、yarn.provided.lib.dirs を介して HDFS 上のパスを指定します。

事前に HDFS にアップロードできない場合:

  1. タスクが送信され、HDFS の固定された場所にアップロードされます. 送信するときに、HDFS に対応する jar (キャッシュ ポリシー付き) があるかどうかを確認し、ローカル パスをリモート パスに置き換えます。

  2. コールバック フックを使用して、異常なタスクを終了させるジャンク ファイルをクリアします。

●シーイオン

事前に HDFS にアップロードできる場合:

  1. Flink lib、Flink プラグイン、ChunJun jar を事前に HDFS にアップロードします。

  2. セッションを開始するときに、yarn.provided.lib.dirs を介して HDFS 上のパスを指定できます。

  3. タスクを送信するときにコア パッケージをアップロードする必要はありません。

事前に HDFS にアップロードできない場合:

  1. セッションの開始時に、すべての jar を HDFS にアップロードします。ヤーンシップによる指定。

  2. Flink タスクがセッションに送信されるとき、jar パッケージを送信する必要はありません。

ファイル

07 クラスローディング分離で遭遇する問題の分析

●思考分析

  1. まず、異なるプラグイン (コネクタ) を異なるクラスローダーに配置します。

  2. 次に、子優先の読み込み戦略を使用します。

  3. x not cast x エラーが発生しないことを確認します。

  4. メタデータ スペースでメモリ リークが発生し、タスク エラーが発生することはありません。

  5. コネクタ jar パッケージをキャッシュします。

●遭遇した問題

  1. Flink ジョブには複数のオペレーターが含まれる場合があり、コネクターはオペレーターです。Flink はネイティブにジョブ レベル用に新しく生成されたクラスローダーであり、各コネクタを個別のクラスローダーに配置することはできません。

  2. 子優先ロード戦略では、セッション モードで毎回新しいクラスローダーが作成されるため、メタデータ スペースでメモリ リークが発生します。

  3. コネクタ間でパブリック クラスを使用すると、エラーが発生します。

  4. 質問 2 と同様に、主にいくつかのスレッド プールが原因で、デーモン スレッドはいくつかのクラス オブジェクト、またはクラス クラス オブジェクトへの参照を保持します。

  5. native-yarnship を使用してアップロードすると、アプリ クラスローダーに配置されます。次に、App Classloader でロードされることが想定されていないいくつかのクラスがロードされます。

ファイル

08 Flink JobGraph クラスパスの利用

/** このジョブの実行に必要な JAR ファイルのセット。*/

private final ListuserJars = new ArrayList();

/** このジョブを実行するために必要なカスタム ファイルのセット。*/

private final MapuserArtifacts = new HashMap<>();

/** このジョブを実行するために必要なクラスパスのリスト。*/

プライベート ListClasspaths = Collections.emptyList();

  1. クライアント側の処理である JobGraph は、userJars、userArtifacts、および Classpaths の 3 つの属性を処理します。

  2. クラスパスはコネクタの階層のみを残します。

  3. セッションの開始時に jar をアップロードすると、jar は Yarn のすべての NodeManager ノードにキャッシュされます。

  4. jobmanager と taskmanager が Classloader をビルドするときに、Classpath のパスを変更し、現在のノード NodeManager のキャッシュ パスに置き換えます。

  5. さまざまなコネクタに従って、Flink Job の Classloader をビルドします。

  6. ビルドされたクラスローダをキャッシュすると、同じクラスローダが次のタスクで使用できるようになります。メモリ リークを回避します。

  7. 新しい ChildFirstCacheClassloader の loadclass メソッドを書き直して、異なるコネクタ URL に従って別の Classloader を生成します。

4. 発生した問題とトラブルシューティングの計画は?

jar パッケージの競合の一般的な例外は、クラスが見つからない (java.lang.ClassNotFoundException)、特定のメソッドが見つからない (java.lang.NoSuchMethodError)、フィールド エラー (java.lang.NoSuchFieldError) またはクラス エラー (java.lang.LinkageError) ) です。 .

● 一般的な解決策は次のとおりです。

1. 最初の方法は、プロジェクト ファイルの依存関係ツリーを入力することです. jar パッケージの依存関係に従って、同じ jar パッケージが複数のバージョンに依存しているかどうかが判断されます. 問題が確認された場合は、間違った jar パッケージを直接除外されます。

2. 依存関係ツリーを見て特定の競合する jar パッケージを特定できない場合は、jvm パラメータを追加してプログラムを開始し、クラスによってロードされた特定の jar 情報を出力できます (-verbose:class)。

3. 上記の手順の後、jar パッケージの競合の問題は基本的に解決でき、特定の問題を詳細に分析する必要があります。

● 常用推奨工具

1.Mavenヘルパー

主にクラスの競合をトラブルシューティングするための IDEA プラグイン。

2.Jスタック

いくつかのデッドロックの問題は、このツール jstack call stack を使用して表示できます。

3.アーサス

いくつかのパフォーマンスの問題と Classloader のリークをトラブルシューティングします。

4.VisualVM

一部のオブジェクトのメモリ リーク、ダンプ ファイルの分析などのトラブルシューティングを行います。

Kangaroo Cloud Open Source Framework DingTalk Technology Exchange Group (30537511) は、ビッグデータ オープン ソース プロジェクトに関心のある学生を歓迎し、最新の技術情報に参加して交換します。オープン ソース プロジェクト ライブラリのアドレス: https://github.com/DTStack/Taier

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/3869098/blog/5583107