flink 提供多种消费 Kafka 数据的方式,由于不同层级接口支持的功能范围、粒度不同,同时 flink 版本迭代频繁,接口也在不断发生变化,因此使用起来容易混淆。
当我们定义了一个 Kafka 的 DDL,例如:
CREATE TABLE MyUserTable ( ...) WITH ( 'connector.type' = 'kafka', 'connector.version' = '0.11' ...)
在 DDL 背后都做了什么,使得我们能够通过 SQL 读写这张表?flink 如何组织其代码结构,如何复用 streaming 相关代码的?接口从 API 到 SQL,方式更加简洁的同时,又有哪些功能被忽略掉了?
这些疑问,在刚接触 flink-SQL 时就非常好奇,但是阿里很多文章都是介绍其优点而不提缺点。这篇笔记,记录下代码的结构层次,SQL 与 DataStream API 接口的关系,梳理下上述的疑问。
1. Table API Connectors
flink 的 API 分为多层,越低级能表达的含义越多,越高级使用起来越简洁。
SQL、Table API 都属于高级接口,使用成本很低,在我看来,这是一种偏配置化的接口,通过配置表达其意图。DDL 描述 Source/Sink,DML 描述计算逻辑。离线的计算逻辑可以统一到 SQL,说明 SQL 语法足够强大,因此实时计算选择 SQL 描述也就顺理成章了。当然,也可以使用 YAML 来描述 Source/Sink。
定义一个Table API Kafka Connector,可以使用 DDL、TableAPI、YAML 多种形式
# 1. DDLCREATE TABLE MyUserTable ( ...) WITH ( 'connector.type' = 'kafka', 'connector.version' = '0.11', ...)# 2. TableAPI.connect( new Kafka() .version("0.11") ... ) # 3. YAMLconnector: type: kafka version: "0.11" ...
都是在表达一层含义,描述的属性,可以分为 3 类:
- connector:即要连接的物理存储本身,可以是文件系统,可以是 Kafka 这类消息队列,也可以是 Redis、ES 等存储,甚至是 Http-API 等,称为 connector.
- format:即数据的结构,可以是 csv、json 等明文,也可以是 Arvo 等序列化格式,称为 format.
- schema:即字段的格式,数据是由各个字段组成的,例如 id、 name、 age 等,称为 schema.
通过明确这 3 个元素,我们就可以将存储里相同格式的多条数据抽象为表,进而使用 SQL、Table API 来读写表。
2. Connectors - Kafka
如果要自定义 Source/Sink 的 Connector,需要继承TableFactory,实现requiredContext supportedProperties方法,指定在 DDL 里支持的参数(optional/required)。然后借助Java’s Service Provider Interfaces (SPI)让 flink 发现该子类。
例如对于 Kafka-0.11,实现Kafka011TableSourceSinkFactory记录到文件/resources/META-INF/services/TableFactory,如果 DDL 参数一致,就会通过该类一步步创建读写 Kafka 的对象。
从 DDL 到底层读写类,通过继承自不同的基类来实现:
- TableFactory: 入口类,声明支持的 DDL 参数
- StreamTableSource:根据表的描述构建出流
- RichParallelSourceFunction/SourceFunction:产出数据,作为流的输入源
以 1.9 版本代码为例,Kafka 0.11 源表的接口关系如图:
具体的:
- Kafka011TableSourceSinkFactoryBase声明支持的参数,例如required: (update-mode -> append), (connector.type -> k