• Flink源码剖析:Jar包任务提交流程


    Flink基于用户程序生成JobGraph,提交到集群进行分布式部署运行。本篇从源码角度讲解一下Flink Jar包是如何被提交到集群的。(本文源码基于Flink 1.11.3)

    1 Flink run 提交Jar包流程分析

    首先分析run脚本可以找到入口类CliFrontend,这个类在main方法中解析参数,基于第二个参数定位到run方法:

    try {
        // do action
        switch (action) {
            case ACTION_RUN:
                run(params);
                return 0;
            case ACTION_RUN_APPLICATION:
                runApplication(params);
                return 0;
            case ACTION_LIST:
                list(params);
                return 0;
            case ACTION_INFO:
                info(params);
                return 0;
            case ACTION_CANCEL:
                cancel(params);
                return 0;
            case ACTION_STOP:
                stop(params);
                return 0;
            case ACTION_SAVEPOINT:
                savepoint(params);
                return 0;
            case "-h":
            case "--help":
                ...
                return 0;
            case "-v":
            case "--version":
                ...
            default:
                ...
                return 1;
        }
    }

    在run方法中,根据classpath、用户指定的jar、main函数等信息创建PackagedProgram。在Flink中通过Jar方式提交的任务都封装成了PackagedProgram对象。

    protected void run(String[] args) throws Exception {
        ...
        final ProgramOptions programOptions = ProgramOptions.create(commandLine);
        final PackagedProgram program = getPackagedProgram(programOptions);
    
        // 把用户的jar配置到config里面
        final List<URL> jobJars = program.getJobJarAndDependencies();
        final Configuration effectiveConfiguration = getEffectiveConfiguration(
                activeCommandLine, commandLine, programOptions, jobJars);
    
        try {
            executeProgram(effectiveConfiguration, program);
        } finally {
            program.deleteExtractedLibraries();
        }
    }

    创建PackagedProgram后,有个非常关键的步骤就是这个effectiveConfig,这里面会把相关的Jar都放入pipeline.jars这个属性里,后面pipeline提交作业时,这些jar也会一起提交到集群。

    其中比较关键的是Flink的类加载机制,为了避免用户自己的jar内与其他用户冲突,采用了逆转类加载顺序的机制。

    private PackagedProgram(
            @Nullable File jarFile,
            List<URL> classpaths,
            @Nullable String entryPointClassName,
            Configuration configuration,
            SavepointRestoreSettings savepointRestoreSettings,
            String... args) throws ProgramInvocationException {
    
        // 依赖的资源
        this.classpaths = checkNotNull(classpaths);
    
        // 保存点配置
        this.savepointSettings = checkNotNull(savepointRestoreSettings);
    
        // 参数配置
        this.args = checkNotNull(args);
    
        // 用户jar
        this.jarFile = loadJarFile(jarFile);
    
        // 自定义类加载
        this.userCodeClassLoader = ClientUtils.buildUserCodeClassLoader(
            getJobJarAndDependencies(),
            classpaths,
            getClass().getClassLoader(),
            configuration);
    
        // 加载main函数
        this.mainClass = loadMainClass(
            entryPointClassName != null ? entryPointClassName : getEntryPointClassNameFromJar(this.jarFile),
            userCodeClassLoader);
    }

    在类加载器工具类中根据参数classloader.resolve-order决定是父类优先还是子类优先,默认是使用子类优先模式。

    executeProgram方法内部是启动任务的核心,在完成一系列的环境初始化后(主要是类加载以及一些输出信息),会调用packagedProgram的invokeInteractiveModeForExecution的,在这个方法里通过反射调用用户的main方法。

    private static void callMainMethod(Class<?> entryClass, String[] args) 
        throws ProgramInvocationException {
        ...
        Method mainMethod = entryClass.getMethod("main", String[].class);
        mainMethod.invoke(null, (Object) args);
        ...
    }

    执行用户的main方法后,就是flink的标准流程了。创建env、构建StreamDAG、生成Pipeline、提交到集群、阻塞运行。当main程序执行完毕,整个run脚本程序也就退出了。

    总结来说,Flink提交Jar任务的流程是:
    1 脚本入口程序根据参数决定做什么操作
    2 创建PackagedProgram,准备相关jar和类加载器
    3 通过反射调用用户Main方法
    4 构建Pipeline,提交到集群

    2 通过PackagedProgram获取Pipeline

    有的时候不想通过阻塞的方式卡任务执行状态,需要通过类似JobClient的客户端异步查询程序状态,并提供停止退出的能力。

    要了解这个流程,首先要了解Pipeline是什么。用户编写的Flink程序,无论是DataStream API还是SQL,最终编译出的都是Pipeline。只是DataStream API编译出的是StreamGraph,而SQL编译出的Plan。Pipeline会在env.execute()中进行编译并提交到集群。

    既然这样,此时可以思考一个问题:Jar包任务是独立的Main方法,如何能抽取其中的用户程序获得Pipeline呢?

    通过浏览源码的单元测试,发现了一个很好用的工具类:PackagedProgramUtils。

    public static Pipeline getPipelineFromProgram(
            PackagedProgram program,
            Configuration configuration,
            int parallelism,
            boolean suppressOutput) throws CompilerException, ProgramInvocationException {
    
        // 切换classloader
        final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(program.getUserCodeClassLoader());
    
        // 创建env
        OptimizerPlanEnvironment benv = new OptimizerPlanEnvironment(
            configuration,
            program.getUserCodeClassLoader(),
            parallelism);
        benv.setAsContext();
        StreamPlanEnvironment senv = new StreamPlanEnvironment(
            configuration,
            program.getUserCodeClassLoader(),
            parallelism);
        senv.setAsContext();
    
        try {
            // 执行用户main方法
            program.invokeInteractiveModeForExecution();
        } catch (Throwable t) {
            if (benv.getPipeline() != null) {
                return benv.getPipeline();
            }
    
            if (senv.getPipeline() != null) {
                return senv.getPipeline();
            }
    
            ...
        } finally {
            // 重置classloader
        }
    }

    这个工具类首先在线程内创建了一个env,这个env通过threadload保存到当前线程中。当通过反射调用用户代码main方法时,内部的getEnv函数直接从threadlocal中获取到这个env。

    ThreadLocal<StreamExecutionEnvironmentFactory> factory = new ThreadLocal<>();
    
    public static StreamExecutionEnvironment getExecutionEnvironment() {
            return Utils.resolveFactory(factory , contextEnvironmentFactory)
                .map(StreamExecutionEnvironmentFactory::createExecutionEnvironment)
                .orElseGet(StreamExecutionEnvironment::createLocalEnvironment);
        }

    再回头看看env有什么特殊的。

    public class StreamPlanEnvironment extends StreamExecutionEnvironment {
    
        private Pipeline pipeline;
    
        public Pipeline getPipeline() {
            return pipeline;
        }
    
        @Override
        public JobClient executeAsync(StreamGraph streamGraph) {
            pipeline = streamGraph;
    
            // do not go on with anything now!
            throw new ProgramAbortException();
        }
    }

    原来是重写了executeAysnc方法,当用户执行env.execute时,触发异常,从而在PackagedProgramUtils里面拦截异常,获取到用户到pipeline。

    总结起来流程如下:

    3 编程实战

    通过阅读上述源码,可以学习到:

    1 classloader类加载的父类优先和子类优先问题
    2 threadlocal线程级本地变量的使用
    3 PackagedProgramUtils 利用枚举作为工具类
    4 PackagedProgramUtils 利用重写env,拦截异常获取pipeline。

    关于pipeline如何提交到集群、如何运行,就后文再谈了。

  • 相关阅读:
    语料和文本处理
    seq2seq+torch7聊天机器人bug处理
    unity3d inputfield标签控制台打印object
    多种语言tcp编程
    处理json中的空格
    安卓无法访问Azure服务器和微软API
    Xamarin/Unity3d无法访问Azure服务器或者微软API
    unity3d C# soket客户端接受失败
    unity3d之public变量引发错误
    unity3d,java,c#,python,rospy的socket通信测试
  • 原文地址:https://www.cnblogs.com/xing901022/p/14300093.html
Copyright © 2020-2023  润新知