Flink 的数据结构是其区别于其他批处理和流处理框架(如 Spark)的核心之一。其设计哲学是 “有状态的流处理” ,即流处理是基础,批处理是流处理的一种特例。因此,它的数据结构也紧密围绕这一思想构建。

Flink 的数据结构主要可以分为两大类:

  1. 用于编程 API 的数据结构:即用户编写程序时直接操作的数据形式。
  2. 用于内部执行引擎的数据结构:即 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 负责:
      1. 序列化:将 JVM 对象转换为二进制字节数组。
      2. 反序列化:将二进制字节数组转换回 JVM 对象。
      3. 拷贝:高效地创建对象的副本。
  • 二进制数据格式 (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 有特殊优化:

  1. 类是 public 的。
  2. 有一个 public 的无参构造函数。
  3. 所有字段要么是 public 的,要么有对应的 getter 和 setter 方法。
    对于这样的 POJO,Flink 可以生成比通用 Java 序列化高效得多的序列化器。

总结与对比

数据结构层次主要类型特点与用途
编程 APIDataStream代表无界或有界数据流,是批流一体编程的核心抽象。
DataSet (已弃用)代表有界数据集,已被统一的 DataStream API 取代。
内部数据形式Java 对象 / POJOs易于使用,但性能开销较大,适合原型开发或简单作业。
Flink Value Types允许自定义序列化逻辑,平衡易用性和性能。
原生类型 & 元组开箱即用,Flink 能高效处理,适合简单场景。
核心:TypeInformation & SerializerFlink 高性能的基石。将数据转换为紧凑的二进制格式,用于网络传输和状态存储,极大减少了序列化开销和 GC 压力。
RowTable API 中的通用行类型,用于关系型操作。

核心思想:Flink 通过其强大的类型系统(TypeInformation)和自定义序列化框架,将用户友好的 API 层数据结构(如 DataStream)高效地映射到底层高性能的二进制数据结构上。这使得 Flink 既能提供简洁的编程接口,又能实现亚秒级延迟、高吞吐量和精确一次(Exactly-Once)语义的强大流处理引擎。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐