【Flink系列二十一】深入理解 JVM的類型加載約束,解決 Flink 類型加載衝突問題的通用方法

class ByteArrayDeserializer is not an instance of org.apache.kafka.common.serialization.Deserializer

Debugging Classloading

類似的 X cannot be cast to X exceptions

如何理解這類異常?

這類異常可以歸納爲類型異常,按個人有限經驗,現象分爲兩種常見情況:

  • 類型賦值檢查:不能 assign、或者 is not instance
  • 類型轉換檢查:不能 cast

都是因爲同一種機制。JVM的類加載器,默認按照雙親委派模型進行加載。我們一般編寫的應用程序,不會打破這個機制。

類型加載順序反轉

像 Tomcat,Flink 這類運行時類型框架(Runtime Framework),尤其是具備 plugin 特性時,考慮到一些便利性,好處是允許用戶的庫能夠覆蓋框架自身的一些庫。

Flink 的文檔指出:大多數情況下都能正常工作,然而某些時候,免不了遇到類型衝突。

對 JVM 類型系統的理解

  1. 在不滿足雙親委派的情況下,不同的類加載器能夠同時分別加載同一個字節碼文件。
  2. 這兩個類加載器所對應的 class 被認爲不是同一種類型。

參考鏈接:
Loading Constraints

5.3.4 主要內容概括

檢查約束
在5.3.1,5.3.2,5.3.3 階段(也就是各種類加載器對類進行加載的過程),JVM會記錄這個類被哪個類加載器加載。

When a class or interface C = <N1, L1> makes a symbolic reference to a field or method of another class or interface D = <N2, L2>, the symbolic reference includes a descriptor specifying the type of the field, or the return and argument types of the method. It is essential that any type name N mentioned in the field or method descriptor denote the same class or interface when loaded by L1 and when loaded by L2.

當class或者interface C = <N1, L1> 符號引用了 class或者interface D = <N2, L2> 的 field 或者 method,符號引用包含了一個描述符,標明瞭 field 的類型,或者方法形參類型和返回值類型。基本地,當任何類型 N 出現在 field 或者 method 的描述符中,且存在相同的 class 或者 interface,被 L1加載 或者 L2加載。

The situations described here are the only times at which the Java Virtual Machine checks whether any loading constraints have been violated. A loading constraint is violated if, and only if, all the following four conditions hold:

  • There exists a loader L such that L has been recorded by the Java Virtual Machine as an initiating loader of a class C named N.

  • There exists a loader L' such that L' has been recorded by the Java Virtual Machine as an initiating loader of a class C ' named N.

  • The equivalence relation defined by the (transitive closure of the) set of imposed constraints implies NL = NL'.

  • C ≠ C '.

這裏描述的情形,僅發生在當 JVM 檢查是否存在違反加載約束時。當且僅當同時滿足下列四種條件時,違反加載約束:

  1. 存在一個 L,被記錄爲類 C 的初始加載器,命名爲 N。
  2. 存在一個 L', 被記錄爲類 C' 的初始加載器,命名爲 N。
  3. 強制約束(傳遞閉包)所定義的等價性關係隱含着 NL = NL'。
  4. C ≠ C '

老美有點囉嗦,咱可以“簡單”理解:

對於咱眼裏的某個類型,他被兩個類加載器分別加載了,但是咱是要求他們能互相賦值的,這樣就違反了類型加載約束。

再看錯誤

Caused by: org.apache.kafka.common.KafkaException: 
  class org.apache.kafka.common.serialization.ByteArrayDeserializer is not an instance of org.apache.kafka.common.serialization.Deserializer

這個時候就很明顯了,我們可以理解爲:

某個類裏面符號引用了一個Deserializer,這個類被 parent loader 加載了,同時 ByteArrayDeserializer 被另一個 Flink UserCode Classloader (child loader)加載了
然後某個地方,對他倆進行了賦值或者類型轉換,違反了類型加載約束

結論

網絡上關於這類問題的解決辦法很簡單,改變 flink 的 classloader 加載優先級策略。

官方指出的方法 classloader.resolve-order=parent-first

這裏記錄另一個方法。

細粒度調整類型加載順序

classloader.parent-first-patterns.additional=org.apache.commons.collections
它改變了 common-collections 庫的加載順序。

classloader.parent-first-patterns.additional=org.apache.kafka
它改變了 kafka-clients 庫的加載順序。

注意: classloader.parent-first-patterns.additional 爲正確寫法,classloader.parent-first-patterns-additional 爲錯誤寫法。

加深理解

實際上,爲什麼這個問題這麼常見?

查看 org.apache.kafka.common.serialization.Deserializer 這個類被誰符號引用了就能知道答案。

舉例說明:

這個類已知被 flink-kafka-connector 中的某個類符號引用了。

KafkaRecordDeserializationSchema 看 import 就知道:

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.serialization.Deserializer;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章