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如何提交到叢集、如何執行,就後文再談了。