【Hudi数据湖应用】Flink作业同名类强转异常ClassCastException修复

一、异常描述

近日升级到hudi 0.11后,在flink应用中遭遇了一个神级异常:java.lang.ClassCastException: org.apache.hudi.common.fs.HoodieWrapperFileSystem cannot be cast to org.apache.hudi.common.fs.HoodieWrapperFileSystem。
没看错吧?同名类转换失败?揉揉眼睛,逐字符再看一次,的确是同名类。同类对象转换还失败了?真是活久见。

2022-05-30 14:40:27
java.lang.ClassCastException: org.apache.hudi.common.fs.HoodieWrapperFileSystem cannot be cast to org.apache.hudi.common.fs.HoodieWrapperFileSystem
	at org.apache.hudi.io.storage.HoodieParquetWriter.<init>(HoodieParquetWriter.java:72)
	at org.apache.hudi.io.storage.HoodieFileWriterFactory.newParquetFileWriter(HoodieFileWriterFactory.java:84)
	at org.apache.hudi.io.storage.HoodieFileWriterFactory.newParquetFileWriter(HoodieFileWriterFactory.java:70)
	at org.apache.hudi.io.storage.HoodieFileWriterFactory.getFileWriter(HoodieFileWriterFactory.java:54)
	at org.apache.hudi.io.HoodieCreateHandle.<init>(HoodieCreateHandle.java:101)
	at org.apache.hudi.io.HoodieCreateHandle.<init>(HoodieCreateHandle.java:80)
	at org.apache.hudi.io.FlinkCreateHandle.<init>(FlinkCreateHandle.java:67)
	at org.apache.hudi.io.FlinkCreateHandle.<init>(FlinkCreateHandle.java:60)
	at org.apache.hudi.client.HoodieFlinkWriteClient.getOrCreateWriteHandle(HoodieFlinkWriteClient.java:479)
	at org.apache.hudi.client.HoodieFlinkWriteClient.upsert(HoodieFlinkWriteClient.java:143)
	at org.apache.hudi.sink.StreamWriteFunction.lambda$initWriteFunction$1(StreamWriteFunction.java:184)
	at org.apache.hudi.sink.StreamWriteFunction.lambda$flushRemaining$7(StreamWriteFunction.java:461)
	at java.util.LinkedHashMap$LinkedValues.forEach(LinkedHashMap.java:608)
	at org.apache.hudi.sink.StreamWriteFunction.flushRemaining(StreamWriteFunction.java:454)
	at org.apache.hudi.sink.StreamWriteFunction.endInput(StreamWriteFunction.java:151)
	at org.apache.hudi.sink.common.AbstractWriteOperator.endInput(AbstractWriteOperator.java:48)
	at org.apache.flink.streaming.runtime.tasks.StreamOperatorWrapper.endOperatorInput(StreamOperatorWrapper.java:91)
	at org.apache.flink.streaming.runtime.tasks.RegularOperatorChain.endInput(RegularOperatorChain.java:100)
	at org.apache.flink.streaming.runtime.io.StreamOneInputProcessor.processInput(StreamOneInputProcessor.java:68)
	at org.apache.flink.streaming.runtime.tasks.StreamTask.processInput(StreamTask.java:496)
	at org.apache.flink.streaming.runtime.tasks.mailbox.MailboxProcessor.runMailboxLoop(MailboxProcessor.java:203)
	at org.apache.flink.streaming.runtime.tasks.StreamTask.runMailboxLoop(StreamTask.java:809)
	at org.apache.flink.streaming.runtime.tasks.StreamTask.invoke(StreamTask.java:761)
	at org.apache.flink.runtime.taskmanager.Task.runWithSystemExitMonitoring(Task.java:958)
	at org.apache.flink.runtime.taskmanager.Task.restoreAndInvoke(Task.java:937)
	at org.apache.flink.runtime.taskmanager.Task.doRun(Task.java:766)
	at org.apache.flink.runtime.taskmanager.Task.run(Task.java:575)
	at java.lang.Thread.run(Thread.java:748)

二、异常分析

2.1 类的判同方法

一般而言,ClassCastException是不同类之间的对象强行转换时才会引起的。那么在java里,怎么判断一个类是否相同呢?
我们知道,在java里,万物皆对象,我们开发的每一个类,一般情况下,运行时都有唯一一个对应的Class类对象。这个类对象在类首次被使用时由java虚拟机加载、创建。我们看Class类的equal方法源码,两个类对象是否相同,取决于是否为同一个对象引用。

    public boolean equals(Object obj) {
        return (this == obj);
    }

很明显,类对象由谁创建和持有,就影响到了类是否相同的判断及类对象强转。而这就涉及到了jvm原理和类的加载机制。

2.2 类双亲委派加载机制

简单而言,jvm运行时会有一个专属的内存区域(方法区或meta space)存放类的信息。类信息由jvm通过类加载器ClassLoader来加载。熟悉jvm原理的人都知道,默认情况下,java虚拟机使用双亲委派机制进行类加载。

双亲委派机制简述如下:

1、如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。

2、如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终将到达顶层的启动类加载器。

3、如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
【Hudi数据湖应用】Flink作业同名类强转异常ClassCastException修复_第1张图片
由此可以看出,在双亲委派机制下,可以保证一个类只会被加载一次,避免类的重复加载。

2.3 两个class对象判同的必要条件

前面我们通过源码可以知道,类对象判同是通过类对象的指针是否相同来判断的。而类对象通过双亲委派机制由Classloader加载、创建。而类是通过全类名(Class.forName)来寻找、加载的。
由此可以推导出,在JVM中表示两个class对象是否是同一个类的两个必要条件:

1、类的全限制名一致(包名+类名)。

2、加载这个类的ClassLoader必须相同。

2.4 Flink的类加载机制

由于我们遇到的是同名类强转异常,可以肯定我们类的全限定名是一致的。那么问题就是出在类的加载器身上。
我们知道,flink有一个配置项,classloader.resolve-order,可以配置类的加载顺序。classloader.resolve-order的默认配置是child-first,这一配置打破了java默认的双亲委派机制。
child-first配置对应的类加载器是ChildFirstClassLoader,这个加载器除了指定的系统类、java lang、flink源等基础类是遵守双亲委派由父加载器加载,其他的类是直接由ChildFirstClassLoader直接加载。这样做的一个好处是,当用户依赖于flink依赖有冲突时,用户可以优先使用自己包内在类进行加载。
【Hudi数据湖应用】Flink作业同名类强转异常ClassCastException修复_第2张图片

2.5 ChildFirstClassLoader核心源码解析

ChildFirstClassLoader的构造器需要指定jar包资源urls、父加载器、父加载器加载的类模式、加载失败处理逻辑。
父加载器加载的类范围由assloader.parent-first-patterns.default指定,符合规则的由父加载器加载,否则由子加载器进行加载(loadClassWithoutExceptionHandling方法)。子加载器寻找jar包资源时(getResource方法),也优先寻找用户资源,再寻找父加载器的资源。
assloader.parent-first-patterns.default默认为:

java.;scala.;org.apache.flink.;com.esotericsoftware.kryo;org.apache.hadoop.;javax.annotation.;org.xml;javax.xml;org.apache.xerces;org.w3c;org.rocksdb。

ChildFirstClassLoader核心源码:

public final class ChildFirstClassLoader extends FlinkUserCodeClassLoader {

    /**
     * The classes that should always go through the parent ClassLoader. This is relevant for Flink
     * classes, for example, to avoid loading Flink classes that cross the user-code/system-code
     * barrier in the user-code ClassLoader.
     */
    private final String[] alwaysParentFirstPatterns;

    public ChildFirstClassLoader(
            URL[] urls,
            ClassLoader parent,
            String[] alwaysParentFirstPatterns,
            Consumer<Throwable> classLoadingExceptionHandler) {
        super(urls, parent, classLoadingExceptionHandler);
        this.alwaysParentFirstPatterns = alwaysParentFirstPatterns;
    }

    @Override
    protected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve)
            throws ClassNotFoundException {

        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);

        if (c == null) {
            // check whether the class should go parent-first
            for (String alwaysParentFirstPattern : alwaysParentFirstPatterns) {
                if (name.startsWith(alwaysParentFirstPattern)) {
                    return super.loadClassWithoutExceptionHandling(name, resolve);
                }
            }

            try {
                // check the URLs
                c = findClass(name);
            } catch (ClassNotFoundException e) {
                // let URLClassLoader do it, which will eventually call the parent
                c = super.loadClassWithoutExceptionHandling(name, resolve);
            }
        } else if (resolve) {
            resolveClass(c);
        }

        return c;
    }

    @Override
    public URL getResource(String name) {
        // first, try and find it via the URLClassloader
        URL urlClassLoaderResource = findResource(name);

        if (urlClassLoaderResource != null) {
            return urlClassLoaderResource;
        }

        // delegate to super
        return super.getResource(name);
    }

    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        // first get resources from URLClassloader
        Enumeration<URL> urlClassLoaderResources = findResources(name);

        final List<URL> result = new ArrayList<>();

        while (urlClassLoaderResources.hasMoreElements()) {
            result.add(urlClassLoaderResources.nextElement());
        }

        // get parent urls
        Enumeration<URL> parentResources = getParent().getResources(name);

        while (parentResources.hasMoreElements()) {
            result.add(parentResources.nextElement());
        }

        return new Enumeration<URL>() {
            Iterator<URL> iter = result.iterator();

            public boolean hasMoreElements() {
                return iter.hasNext();
            }

            public URL nextElement() {
                return iter.next();
            }
        };
    }

    static {
        ClassLoader.registerAsParallelCapable();
    }
 }

2.6 flink默认加载机制的衍生问题

由于flink默认使用ChildFirstClassLoader进行类加载,打破了源生的类加载机制,这可能会导致父加载类加载过的类,会被子加载器重复加载,进而导致同名类判同异常,影响对象强转,导致运行时出现同名类强转异常,抛ClassCastException。
它也会导致类的继承关系判断失效,导致子类转父类失败。因为父子类被不同的类加载器加载,同名父类被判断为不同类。异常示例如下:

Cannot cast org.apache.hudi.hive.HiveSyncConfig to org.apache.hudi.hive.replication.GlobalHiveSyncConfig
java.lang.ClassCastException: Cannot cast org.apache.hudi.hive.HiveSyncConfig to org.apache.hudi.hive.replication.GlobalHiveSyncConfig

三、异常解决方案

通过上述分析,在flink中遇到的同名类强转异常是由类加载器引起的。flink默认情况下,使用ChildFirstClassLoader进行加载。ChildFirstClassLoader加载类时,也优先使用用户的资源。一般情况下,这个机制是可以良好运行的。但有时也会出现同名类强转异常。这时在排除用户jar与flink运行依赖冲突后,若异常依然存在,可以修改flink的配置(conf/flink-conf.yaml),将classloader.resolve-order改为parent-first,这样类就只会加载一次,避免重复加载出现强转异常。

你可能感兴趣的:(Hudi,Flink,Java,flink,java,大数据)