🐿️

Apache Flink クラスローダーについて

に公開

Apache Flink クラスローダーについて

▫️概要

Apache Flink のクラスローディングは、ジョブごとに独立したクラスローダーを生成し、
ユーザコードと Flink 本体を安全に分離する仕組みです。
これにより、異なるジョブが異なる依存ライブラリを使っても衝突を防げます。
本記事では、Java の基本的なクラスローダー構造から始め、
Flink のクラスローダー設計、設定方法、メモリリーク対策までをわかりやすく解説します。


▫️前提

1. そもそも Java プログラムはどう動いているのか?

Java プログラムは次の流れで動作します。

  1. .java ファイルをコンパイルして .class(バイトコード)を生成
  2. JVM(Java 仮想マシン)が .class を読み込み実行
  3. 実行時に「必要なクラス」を動的にロード

JVM は 実行前にすべてのクラスを知っているわけではなく
必要になったタイミングでクラスを探してロードします。
この「クラスをどこから探すか・どう読み込むか」を担うのが クラスローダー(ClassLoader) です。

2. クラスローダーとは何者か?

クラスローダーの役割を一言で言うと:

「クラス名」から「クラスの実体(.class)」を見つけて JVM に渡す係

Java の世界では:

  • コードでは com.example.Foo のようなクラス名を使う
  • 実際のクラスは .class ファイルや .jar に格納
  • それを見つけて JVM に登録するのがクラスローダーです

3. クラスローダーの「親子関係」

クラスローダーは 階層構造(ツリー構造) を持ちます。
基本ルールは 親から順番にクラスを探す(Parent-First) です。

主な階層

名称 読み込む対象
最上位 Bootstrap ClassLoader Java 標準クラス(java.lang.String など)
中間層 Platform / Application ClassLoader JDKの追加APIやアプリ全体
最下層 Custom / Framework ClassLoader フレームワークやユーザジョブ用クラス

上に行くほど JVM に近く、
下に行くほどユーザコードに近い層になります。


Flink では、ジョブごとに独立した UserCodeClassLoader を作成します。
これにより、複数ジョブが異なる依存関係(例:Guava のバージョン違いなど)を持っていても
互いに干渉しないように設計されています。

2. クラスローダーの階層構造

Flink のクラスローダーは以下の2層構造が中心です。

階層 内容
Flink Core ClassLoader(親層) Flink 本体と共通ライブラリをロード flink-core, flink-runtime, guava
UserCodeClassLoader(子層) ジョブ固有の依存ライブラリ・ユーザコードをロード myjob.jar, mysql-connector-j.jar

3. クラスローディング順序設定

flink-conf.yaml で以下の設定が可能です:

classloader.resolve-order: child-first
モード 探索順序 特徴 主な用途
Parent-First 親 → 子 Flink 本体を優先。安定性重視。 標準API利用ジョブ
Child-First 子 → 親 ユーザ依存を優先。柔軟性重視。 JDBC, Guava, Jackson など外部依存あり

特に 外部ライブラリが Flink 本体とバージョン競合する場合
child-first 設定が推奨されます。

4. メモリリーク(ClassLoader Leak)と対策

Flink はジョブごとに新しい UserCodeClassLoader を作成し、
ジョブ終了時に破棄します。
ただし、参照が残ると GC に回収されず、
メモリリーク(ClassLoader Leak) が発生することがあります。

主な原因と対処

原因 説明 対応策
static 参照 static フィールドにユーザクラスを保持 static を避け、依存注入(DI)を使う
ThreadLocal 残留 ThreadLocal にユーザクラスを残す ThreadLocal.remove() を明示的に呼ぶ
DriverManager 登録残り JDBC ドライバが登録解除されない ジョブ終了時に DriverManager.deregisterDriver()
child-first による親参照 親ローダーが子クラスを参照 parent-first に戻す or Shade Plugin で名前空間を隔離

5. まとめ

観点 parent-first child-first
探索順序 親 → 子 子 → 親
安定性 高い 低い(衝突リスクあり)
柔軟性 低い 高い(外部依存優先可)
主な用途 長期ジョブ、標準API 外部依存利用ジョブ(JDBC等)
メモリリークリスク 大(親参照によるリーク)

▫️結論

Flink のクラスローディングは、
「安定性(parent-first)」と「柔軟性(child-first)」のバランス設計 が鍵です。
基本は安定性を重視して parent-first を使い、
外部依存を含む場合のみ child-first に切り替えるのが実践的です。
また、ジョブ終了後の クラスローダーリーク防止(static・ThreadLocal の解放)
運用上の重要なポイントです。

✅ 安定稼働を重視:parent-first
✅ 外部ライブラリ利用時:child-first
✅ メモリリーク防止:参照を適切に解放


📚 参考文献・引用元


Discussion