Flink源码学习-executeSql方法及返回值TableResult

今天主要来学习Flink中TableEnvironment的executeSql方法以及返回值TableResult对象,来看看源码中到底是怎样实现的,Flink的版本是1.14.5。话不多说,首先上一段Flink读取kafka中数据的示例。

//省略StreamTableEnvironment的创建过程,聚焦核心片段
protected void run(StreamTableEnvironment tenv) {
    //kafka的建表语句
    String aisKafka = KafkaUtil.getAisKafka("test");
	//执行建表语句
    tenv.executeSql(aisKafka);
	//执行查询语句
    TableResult tableResult = tenv.executeSql("select * from test");
	//打印查询结果
    tableResult.print();
}

这段代码的逻辑非常简单,主要就是kafka建表然后读取数据,接下来就让我们看看最为核心的executeSql方法。

public interface TableEnvironment {
    /**
    Executes the given single statement and returns the execution result.
The statement can be DDL/DML/DQL/SHOW/DESCRIBE/EXPLAIN/USE. For DML and DQL, this method returns TableResult once the job has been submitted. For DDL and DCL statements, TableResult is returned once the operation has finished.
If multiple pipelines should insert data into one or more sink tables as part of a single execution, use a StatementSet (see createStatementSet()).
By default, all DML operations are executed asynchronously. Use TableResult.await() or TableResult.getJobClient() to monitor the execution. Set TableConfigOptions.TABLE_DML_SYNC for always synchronous execution.
Returns: content for DQL/SHOW/DESCRIBE/EXPLAIN, the affected row count for `DML` (-1 means unknown), or a string message ("OK") for other statements.
    **/
    /**
    上面的注释大致意思就是该方法执行给定的语句并返回执行结果,然后写了一些注意事项。
    如果给的是DML或DQL语句,提交就会返回结果。如果给的是DDL或DCL语句,执行完才会返回结果。
    然后呢,如果在一次操作中要将数据插入多个表中,建议使用createStatementSet方法。
    所有的DML语句执行过程都是异步的,可以使用TableResult.await()来进行监控。
    看了这段描述,相信小伙伴应该清楚了怎样用Flink来写sql了。
    **/
	TableResult executeSql(String statement);
}

//接下来就来看看executeSql方法的实现。
public class TableEnvironmentImpl implements TableEnvironmentInternal {
    @Override
    public TableResult executeSql(String statement) {
        //将传入的语句解析为一个List<Operation>
        List<Operation> operations = getParser().parse(statement);
		//判断上一步解析语句的返回值,不为1则抛出异常,至于为什么要判断,从这段代码大致猜测Operation可能类似于执行计划,一个执行计划代表一个完整的sql,如果解析出来有两段sql,那肯定要抛出异常了。
        if (operations.size() != 1) {
            throw new TableException(UNSUPPORTED_QUERY_IN_EXECUTE_SQL_MSG);
        }
		//执行上面解析出来的Operation
        return executeInternal(operations.get(0));
    }
}

从上面的代码我们可以看出executeSql方法有三个步骤,首先解析sql得到Operation,然后进行判断不为1抛出异常,最后进行执行。整个过程还是比较清晰的,横向对比其他组件,sql的执行基本上都是解析->优化->执行。

接下来我们聚焦到第一个步骤,主要核心逻辑有三个点,getParser()、parse(statement)、以及弄清楚Operation(如果有了解的小伙伴明白这就是算子,一个task就由多个Operation组成)

public abstract class PlannerBase implements Planner {
    public Parser getParser() {
        /**这段代码逻辑其实十分简单,首先我们要了解Flink的SqlDialect,
        这其实是sql兼容模式选择(和sql解析器不是一个概念,sql解析器是将sql解析为语法树),
        目前1.14版本SqlDialect只有default和hive。
        如果通过建表语句获取的SqlDialect和默认的SqlDialect都为空或者相同,返回默认的parser。
        **/
        if (this.parser() != null) {
            //获取建表时指定的SqlDialect
            SqlDialect var10000 = this.getTableConfig().getSqlDialect();
            //获取初始化时的SqlDialect
            SqlDialect var1 = this.currentDialect();
            if (var10000 == null) {
                if (var1 == null) {
                    return this.parser();
                }
            } else if (var10000.equals(var1)) {
                return this.parser();
            }
        }
		//当不符合上述条件时,根据在建表语句处获取的SqlDialect来新建一个parser返回
        this.parser_$eq(this.createNewParser());
        this.currentDialect_$eq(this.getTableConfig().getSqlDialect());
        return this.parser();
    }
}

//parser有两个实现类,由于示例中的数据源为kafka,所以我们选择ParserImpl
public class ParserImpl implements Parser {
    @Override
    public List<Operation> parse(String statement) {
        //获取sql解析器,Flink的sql解析器使用了开源的calcite,至于calciteParserSupplier.get(),这里使用的是供应商模式
        CalciteParser parser = calciteParserSupplier.get();
        //获取FlinkPlannerImpl,作用主要是sql的校验
        FlinkPlannerImpl planner = validatorSupplier.get();

        /**在解析语句时,首先使用ExtendedParser解析语句。
        如果ExtendedParser解析失败,将会使用CalciteParser解析语句。
        ExtendedParser用于解析CalciteParser不支持的一些特殊命令,
        如SET key=value可能在key和value中包含特殊字符。Optional是一个容器,
        可以判断是空还是非空。ExtendedParser解析语句只能是比较简单的,
        比如clear、help、quit、reset、set这种类型语句。
        **/
        Optional<Operation> command = EXTENDED_PARSER.parse(statement);
        /**如果EXTENDED_PARSER能成功解析,就直接返回Operation了,
        singletonList是一个只包含一个对象的不可变列表。
        **/
        if (command.isPresent()) {
            return Collections.singletonList(command.get());
        }

        /** 解析sql将其转化为SqlNode,SqlNode是一种抽象语法树,这里面就不做深入了,
        实际上这里面是用的calcite的一些方法,如果不了解calcite这个框架,大概率是看不懂的。
        **/
        SqlNode parsed = parser.parse(statement);
        /**这一步其实没什么特别的逻辑在里面,就是用FlinkPlannerImpl校验一下上一步获得的SqlNode
        是否合法,然后将其转化为Operation。具体的转化过程为首先校验是否合法,
        然后判断SqlNode的类型,假如为查询sql,
        则由SqlToOperationConverter的convertSqlQuery方法将sqlNode转为Operation,
        里面具体过程为先由FlinkPlannerImpl将sqlNode转为relational tree
        (这块逻辑由calcite完成),
        然后将relational tree转为PlannerQueryOperation,
        PlannerQueryOperation是operation的一个实现类,
        里面重要的属性为之前解析的relational tree和表字段属性信息
        **/
        Operation operation =
                SqlToOperationConverter.convert(planner, catalogManager, parsed)
            .orElseThrow(() -> new TableException("Unsupported query: " + statement));
        //看到这里,应该清楚了Flink中的Operation对应着calcite中的RelNode,其实就是逻辑计划
        return Collections.singletonList(operation);
    }
}

第二个步骤没什么好解释的,一个sql语句解析为一个operation,Flink一次只允许执行一次sql。

接下来聚焦第三个步骤,就是executeInternal方法

public class TableEnvironmentImpl implements TableEnvironmentInternal {
    @Override
    public TableResult executeInternal(Operation operation) {
    	//完整的我就不贴出来了,选取查询语句
    	if (operation instanceof QueryOperation) {
        	return executeQueryOperation((QueryOperation) operation);
    	}
    }
    
    private TableResult executeQueryOperation(QueryOperation operation) {
        //这段逻辑主要就是创建一个标识符,然后获取当前任务目录名称和数据库名
        final UnresolvedIdentifier unresolvedIdentifier =
                UnresolvedIdentifier.of(
                        "Unregistered_Collect_Sink_" + CollectModifyOperation.getUniqueId());
        final ObjectIdentifier objectIdentifier =
                catalogManager.qualifyIdentifier(unresolvedIdentifier);
        //CollectModifyOperation的作用主要是将queryOperation的内容收集到本地
        CollectModifyOperation sinkOperation =
                new CollectModifyOperation(objectIdentifier, operation);
        /**获取Transformation,了解DataStream的小伙伴应该知道Transformation
        是组成DataStream的核心,主要代表创建数据流的操作
        **/
        List<Transformation<?>> transformations =
                translate(Collections.singletonList(sinkOperation));
        final String defaultJobName = "collect";
        //创建一个流水线
        Pipeline pipeline =
                execEnv.createPipeline(
                        transformations, tableConfig.getConfiguration(), defaultJobName);
        /**这段代码的逻辑咱就不深入去研究了,
        主要就是将StreamGraph转为jobGraph再提交到jobClient,
        最后转为TableResult,其中StreamGraph就是一种Pipeline
        **/
        try {
            //核心代码,有精力的小伙伴可以深入研究
            JobClient jobClient = execEnv.executeAsync(pipeline);
            
            CollectResultProvider resultProvider = sinkOperation.getSelectResultProvider();
            resultProvider.setJobClient(jobClient);
            return TableResultImpl.builder()
                    .jobClient(jobClient)
                    .resultKind(ResultKind.SUCCESS_WITH_CONTENT)
                    .schema(operation.getResolvedSchema())
                    .data(resultProvider.getResultIterator())
                    .setPrintStyle(
                            TableResultImpl.PrintStyle.tableau(
                                    PrintUtils.MAX_COLUMN_WIDTH,
                                    PrintUtils.NULL_COLUMN,
                                    true,
                                    isStreamingMode))
                    .setSessionTimeZone(getConfig().getLocalTimeZone())
                    .build();
        } catch (Exception e) {
            throw new TableException("Failed to execute sql", e);
        }
    }
}

到这里我们就基本明白了一个FlinkSQL是怎样执行的,最后总结下

1.首先解析sql语句,得到一个operation。
2.判断operation是不是只有一个,否则抛出异常。
3.将operation转为Transformation,再转为Pipeline也就是StreamGraph,再将StreamGraph转为jobGraph,提交到jobClient,最后转为TableResult输出。

如果有小伙伴存在疑问或者觉得哪里有问题,欢迎在评论区留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值