Flink原始碼剖析:Jar包任務提交流程

xingoo發表於2021-01-19

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包任務提交流程

總結來說,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。

總結起來流程如下:

Flink原始碼剖析:Jar包任務提交流程

3 程式設計實戰

通過閱讀上述原始碼,可以學習到:

1 classloader類載入的父類優先和子類優先問題
2 threadlocal執行緒級本地變數的使用
3 PackagedProgramUtils 利用列舉作為工具類
4 PackagedProgramUtils 利用重寫env,攔截異常獲取pipeline。

關於pipeline如何提交到叢集、如何執行,就後文再談了。

 

相關文章