Flink数据结构与类型系统详解
Flink 的数据结构是其区别于其他批处理和流处理框架(如 Spark)的核心之一。其设计哲学是 “有状态的流处理” ,即流处理是基础,批处理是流处理的一种特例。因此,它的数据结构也紧密围绕这一思想构建。
Flink 的数据结构主要可以分为两大类:
- 用于编程 API 的数据结构:即用户编写程序时直接操作的数据形式。
- 用于内部执行引擎的数据结构:即 Flink 运行时在内部处理和交换数据时使用的格式。
一、编程 API 数据结构
这是开发者最常接触的部分,主要分为两种:
1. DataStream / DataSet (已在 Flink 1.12 后统一为 DataStream API)
虽然现在官方推荐使用统一的 DataStream API 进行批流一体编程,但理解两者的区别有助于理解 Flink 的演进。
-
DataStream:
- 含义:代表一个无界的数据流。数据元素会持续不断地产生和流入。
- 来源:例如 Kafka、Kinesis、TCP Socket、实时日志文件等。
- 操作:应用于其上的转换(如
map,filter,keyBy,window)通常是持续进行的。 - 特点:处理永不停止(除非手动取消),结果会持续增量产出。
-
DataSet:
- 含义:代表一个有界的数据集合。所有要处理的数据在开始处理前就已经是已知且完整的。
- 来源:例如 HDFS、本地文件系统中的文件、数据库等。
- 操作:应用于其上的转换会生成一个执行计划,最终一次性计算出结果。
- 特点:处理有明确的开始和结束,最终产出完整结果。
- 现状:在批流一体的
Table API / SQL成为主流后,DataSet API已逐渐被弃用。
2. 统一后的 DataStream API (批流一体)
从 Flink 1.12 开始,官方推荐使用 DataStream API 来处理无界流和有界流。其核心思想是:批(有界流)是流(无界流)的一个子集。
- 当你对一个有界流(批数据)使用
DataStream API时,Flink 引擎会识别到数据源的有限性,并在处理完所有数据后自然地结束作业,并产出最终结果。 - 这意味着你只需学习一套 API,就可以同时处理实时流和批量数据。
二、内部执行引擎数据结构
这是 Flink 实现高性能和低延迟的关键。其核心是 Flink 序列化器 和 二进制数据格式。
1. Java 对象 (POJOs, Case Classes, etc.)
这是最直接的方式。用户定义的类(Java POJOs 或 Scala Case Classes)会被 Flink 识别并用作数据类型。
- 优点:易于使用和理解,符合面向对象编程习惯。
- 缺点:性能开销大。JVM 需要处理对象的创建、垃圾回收(GC),并且在网络传输、状态存储时需要先进行 Java 序列化,这个过程效率较低。
2. Flink Value Types
实现了 org.apache.flink.types.Value 接口(包含 read() 和 write() 方法)的类。允许自定义、更高效的序列化逻辑。
3. 原生类型 (Basic Types)
Flink 为常用的 Java 原生类型(如 String, Long, Integer, Double)以及它们的数组提供了开箱即用的支持。
4. 最核心:Flink Serializer 与 Binary Format
为了追求极致的性能,Flink 设计了自己的一套序列化框架和内存管理机制。其核心是 TypeInformation。
-
TypeInformation:
- 这是 Flink 的类型系统核心。它为 Flink 提供了关于数据处理过程中所有数据类型的详细信息。
- 用户在编写
dataStream.map(...)时,Flink 会通过泛型和类型提取机制自动推断出元素的TypeInformation。如果无法自动推断(例如使用了 Lambda 表达式),用户需要显式地通过.returns(TypeInformation.of(...))来提供。 TypeInformation的主要作用之一是为对应的数据类型创建一个高效的TypeSerializer。
-
TypeSerializer:
- 这是 Flink 的序列化器抽象。
TypeInformation会创建一个对应的TypeSerializer。 TypeSerializer负责:- 序列化:将 JVM 对象转换为二进制字节数组。
- 反序列化:将二进制字节数组转换回 JVM 对象。
- 拷贝:高效地创建对象的副本。
- 这是 Flink 的序列化器抽象。
-
二进制数据格式 (Binary Format):
- 经过
TypeSerializer序列化后的数据,在 Flink 内部(无论是在网络传输中、还是在状态后端(State Backend)如 RocksDB 中存储时)都以一种紧凑的二进制格式存在。 - 优势:
- 节省空间:二进制格式比 Java 原生序列化紧凑得多。
- 极致性能:避免了昂贵的 Java 对象创建和垃圾回收(GC)开销。操作可以直接在二进制数据上进行,无需反序列化成完整对象(例如,比较操作可以直接比较二进制字节)。
- 堆外内存:这些二进制数据可以存储在堆外内存(Off-Heap Memory)中,进一步减轻了 JVM GC 的压力,保证了大型状态下的稳定性和性能。
- 经过
5. Tuples & Case Classes
Flink 原生支持元组(Tuple)类型(如 Tuple2, Tuple3等)和 Scala 的 Case Classes。它们是复合类型,可以包含多个字段。Flink 能非常高效地处理它们,因为可以只访问元组中特定的字段而无需反序列化整个对象。
6. Row Type
主要用于 Table API / SQL。Row 对象是一个具有任意数量字段的可变数据结构。每个字段都可以为 null。它是在 API 层和 Table 层之间传递数据的通用数据结构。
7. POJOs 的特殊优化
虽然使用普通 Java 对象有开销,但 Flink 对符合以下规则的 POJO 有特殊优化:
- 类是 public 的。
- 有一个 public 的无参构造函数。
- 所有字段要么是 public 的,要么有对应的 getter 和 setter 方法。
对于这样的 POJO,Flink 可以生成比通用 Java 序列化高效得多的序列化器。
总结与对比
| 数据结构层次 | 主要类型 | 特点与用途 |
|---|---|---|
| 编程 API | DataStream | 代表无界或有界数据流,是批流一体编程的核心抽象。 |
| 代表有界数据集,已被统一的 DataStream API 取代。 | ||
| 内部数据形式 | Java 对象 / POJOs | 易于使用,但性能开销较大,适合原型开发或简单作业。 |
| Flink Value Types | 允许自定义序列化逻辑,平衡易用性和性能。 | |
| 原生类型 & 元组 | 开箱即用,Flink 能高效处理,适合简单场景。 | |
| 核心:TypeInformation & Serializer | Flink 高性能的基石。将数据转换为紧凑的二进制格式,用于网络传输和状态存储,极大减少了序列化开销和 GC 压力。 | |
| Row | Table API 中的通用行类型,用于关系型操作。 |
核心思想:Flink 通过其强大的类型系统(TypeInformation)和自定义序列化框架,将用户友好的 API 层数据结构(如 DataStream)高效地映射到底层高性能的二进制数据结构上。这使得 Flink 既能提供简洁的编程接口,又能实现亚秒级延迟、高吞吐量和精确一次(Exactly-Once)语义的强大流处理引擎。
更多推荐



所有评论(0)