KafkaClient源码修改用于支持Flink提供的文件协议S3、HDFS、OSS、FILE等

1 背景

Flink的配置文件以及代码中会涉及到各种文件位置类配置,比如默认的检查点/保存点路径,高可用路径等。代码中,最常用的Flink数据源应该是KafkaSource。而使用Kafka,就需要用到鉴权,即需要在Kafka的连接属性中指定证书文件。

2 问题

然而,KafkaClient的配置中,证书文件的路径配置仅支持File协议,这让很多实际应用场景下变得非常不方便。比如:基于Yarn部署任务时需要确认任务部署到的机器的指定目录A下存在证书,同时配置的KafkaSource的配置中将证书路径设置为目录A下的证书文件。类似的场景还有很多,总结下来就是,Flink集群机器需要都有这个证书文件,并且需要保持文件路径统一。
事实上,既然是共享文件,就应该用共享文件系统(NFS、HDFS等),Flink很多配置也是这么支持的。但Kafka的证书配置却强制要求是File协议。笔者工作中遇到此问题,因此通过查看源码后决定简单调整源码解决该问题。

3 解决

经过简单的源码跟踪,配置的KafkaSSL证书路径这个参数最终被使用的位置是类 org.apache.kafka.common.security.ssl.SslFactory 类内的一个静态内部类 SecurityStore 中被使用的。对应2个方法,load 和 lastModifiedMs 方法,分别用于根据证书路径加载证书和根据证书路径返回文件最后修改时间戳,代码片段如下。

    static class SecurityStore {
        private final String type;
        private final String path;
        private final Password password;
        private final Password keyPassword;
        private Long fileLastModifiedMs;

        SecurityStore(String type, String path, Password password, Password keyPassword) {
            Objects.requireNonNull(type, "type must not be null");
            this.type = type;
            this.path = path;
            this.password = password;
            this.keyPassword = keyPassword;
        }

        // 此方法加载SSL证书
        KeyStore load() {
            try (InputStream in = Files.newInputStream(Paths.get(path))) {
                KeyStore ks = KeyStore.getInstance(type);
                char[] passwordChars = password != null ? password.value().toCharArray() : null;
                ks.load(in, passwordChars);
                fileLastModifiedMs = lastModifiedMs(path);
                log.debug("Loaded key store with path {} modification time {}", path, fileLastModifiedMs == null ? null : new Date(fileLastModifiedMs));
                return ks;
            } catch (GeneralSecurityException | IOException e) {
                throw new KafkaException("Failed to load SSL keystore " + path + " of type " + type, e);
            }
        }
        // 此方法获取SSL证书的最后修改时间戳
        private Long lastModifiedMs(String path) {
            try {
                return Files.getLastModifiedTime(Paths.get(path)).toMillis();
            } catch (IOException e) {
                log.error("Modification time of key store could not be obtained: " + path, e);
                return null;
            }
        }

        boolean modified() {
            Long modifiedMs = lastModifiedMs(path);
            return modifiedMs != null && !Objects.equals(modifiedMs, this.fileLastModifiedMs);
        }
    }

如上代码,最终只有2个方法使用,而且代码逻辑相对清晰简单,此处不做赘述。修改思路自然就是在这2个方法,使用新方法使得可接受非file协议的path参数即可。如果读者仅是计划使用HDFS,可以很简单的网上找个HDFS文件读取方式简单替换即可,但这么做未免太粗暴。笔者希望改造后可以支持原先的File协议,同时支持更多的其他协议。
同时由于笔者主要是在Flink中使用,而Flink的配置中涉及到文件路径的都会支持很多协议。因此,笔者决定基于Flink支持的协议去调整。通过简单跟踪和阅读源码后,发现Flink对这部分封装还算不错,我们只需要简单的照猫画虎即可。
下面先给出调整后的load和lastModifiedMs方法,其中文件系统访问相关的被笔者封装到了FlinkFileSystemUtils工具类中,相关访问后续介绍。

        KeyStore load() {
            log.info("Try to load kafka ssl files, path: {}.", path);
            try (InputStream in = FlinkFileSystemUtils.getInputStream(path)) { // 这行被调整为使用FlinkFileSystemUtils获取输入流。
                KeyStore ks = KeyStore.getInstance(type);
                char[] passwordChars = password != null ? password.value().toCharArray() : null;
                ks.load(in, passwordChars);
                fileLastModifiedMs = lastModifiedMs(path);
                log.debug("Loaded key store with path {} modification time {}", path, fileLastModifiedMs == null ? null : new Date(fileLastModifiedMs));
                return ks;
            } catch (GeneralSecurityException | IOException e) {
                throw new KafkaException("Failed to load SSL keystore " + path + " of type " + type, e);
            }

        }

        private Long lastModifiedMs(String path) {
            log.info("Try to get the lastModifiedTime of kafka ssl files, path: {}.", path);
            FileStatus fileStatus = FlinkFileSystemUtils.getFileStatus(path); // 此处调整为使用FlinkFileSystemUtils获取文件状态,然后下面根据判断决定怎么返回即可。
            if (fileStatus != null) {
                return fileStatus.getModificationTime();
            } else {
                log.error("Modification time of key store could not be obtained: " + path);
                return null;
            }
        }

如上所示,这2个方法的调整都非常简单,文件访问相关操作都被封装进了FlinkFileSystemUtils工具类下,下面是工具类源码。


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.flink.core.fs.FileStatus;  // 注意! 此处是使用 org.apache.flink.core.fs.*,不要引错。
import org.apache.flink.core.fs.FileSystem;  // 注意! 此处是使用 org.apache.flink.core.fs.*,不要引错。
import org.apache.flink.core.fs.Path;        // 注意! 此处是使用 org.apache.flink.core.fs.*,不要引错。

import java.io.IOException;
import java.io.InputStream;

/***
 * 通用文件操作工具。
 */
@Slf4j
public class FlinkFileSystemUtils {
    /***
     * 读取文件并返回。
     * @param path 完整文件路径(不推荐非绝对路径)。
     * @return 返回文件内容。
     */
    public static String readAll(String path) {
        String result = null;
        try {
            result = IOUtils.toString(getInputStream(path));
        } catch (Exception e) {
            e.printStackTrace();
            log.error("FlinkFileSystemUtils.ReadAll error. Path: {}, Cause: {}", path, e);
        }
        return result;
    }

    /***
     * 返回指定文件的输入流。
     * @param path 完整文件路径(不推荐非绝对路径)。
     * @return 返回文件输入流。
     */
    public static InputStream getInputStream(String path) {
        return getInputStream(new Path(path));
    }

    private static InputStream getInputStream(Path flinkPath) {
        InputStream inputStream = null;
        try {
            FileSystem flinkFileSystem = FileSystem.get(flinkPath.toUri());
            inputStream = flinkFileSystem.open(flinkPath);
        } catch (IOException e) {
            log.error("Error Call getInputStream, path: {}, Cause: {}.", flinkPath, e);
        }
        return inputStream;
    }

    /***
     * 返回指定文件的输入流。
     * @param path 完整文件路径(不推荐非绝对路径)。
     * @return 返回文件输入流。
     */
    public static boolean exist(String path) {
        return exist(new Path(path));
    }

    private static boolean exist(Path flinkPath) {
        boolean exist = false;
        try {
            FileSystem flinkFileSystem = FileSystem.get(flinkPath.toUri());
            exist = flinkFileSystem.exists(flinkPath);
        } catch (IOException e) {
            log.error("Error Call Exist, path: {}, Cause: {}.", flinkPath, e);
        }
        return exist;
    }

    /***
     * 返回指定文件的文件状态。
     * @param path 完整文件路径(不推荐非绝对路径)。
     * @return 返回文件的文件状态。
     */
    public static FileStatus getFileStatus(String path) {
        return getFileStatus(new Path(path));
    }

    private static FileStatus getFileStatus(Path flinkPath) {
        FileStatus fileStatus = null;
        try {
            FileSystem flinkFileSystem = FileSystem.get(flinkPath.toUri());
            return flinkFileSystem.getFileStatus(flinkPath);
        } catch (IOException e) {
            log.error("Error Call GetFileStatus, path: {}, Cause: {}.", flinkPath, e);
        }
        return fileStatus;
    }

    public static String normalizePath(String path) {
        return new Path(path).toUri().normalize().getPath();
    }
}

最后!说明,关于如何替换源码的问题读者就不需要去考虑了。因为,本文中调整的是KafkaClient的源码,这部分一般是随着项目引入的,不是框架自身的(比如Flink集群本身不提供)。因此,打包的Flink任务本身就应该打包进去KafkaClient的依赖,项目中通过使用maven或gradle引入依赖即可。然后通过相同包名类名覆盖被调整的源码类即可,默认情况下,用户代码会覆盖jar中代码。

4 总结

本文主要介绍了Flink任务中读取Kafka数据时涉及到Kafka SSl证书只能使用File协议的问题背景,由于其非常不便利,笔者决定调整源码并解决了该问题。

猜你喜欢

转载自blog.csdn.net/u013887254/article/details/107762079