1. Operators 是什么:DataStream 的"积木"
DataStream 的算子(Operators / Transformations)本质上就是:
输入一个或多个 DataStream,输出一个新的 DataStream。
你把这些算子串起来,就形成了 Flink 的数据流拓扑(DAG)。
常见链路长这样:
Source -> map -> flat_map -> filter -> key_by -> reduce/aggregate -> sink
2. Functions:算子里三种常见写法
在 PyFlink 里,算子需要"函数"来定义处理逻辑。官方文档强调了三种写法:
2.1 实现 Function 接口(推荐:可维护、可复用、可做 open 初始化)
例如 MapFunction:
python
from pyflink.datastream.functions import MapFunction
class MyMapFunction(MapFunction):
def map(self, value):
return value + 1
使用:
python
from pyflink.common.typeinfo import Types
data_stream = env.from_collection([1, 2, 3, 4, 5], type_info=Types.INT())
mapped_stream = data_stream.map(MyMapFunction(), output_type=Types.INT())
适合场景:
- 需要
open()里加载资源/初始化状态 - 逻辑复杂,想结构化代码
- 需要在类里保存变量、复用对象
2.2 Lambda(快速但有边界)
python
mapped_stream = data_stream.map(lambda x: x + 1, output_type=Types.INT())
注意官方的坑:
ConnectedStream.map()和ConnectedStream.flat_map()不支持 lambda- 它们必须分别接收
CoMapFunction/CoFlatMapFunction
结论:单流简单逻辑可以 lambda;涉及双流/连接流别用。
2.3 普通 Python function(兼顾可读性与轻量)
python
def my_map_func(value):
return value + 1
mapped_stream = data_stream.map(my_map_func, output_type=Types.INT())
3. Output Type:为什么你经常"必须显式写 output_type"
PyFlink DataStream 的一个关键机制是:
如果你不写 output_type,默认就是 Types.PICKLED_BYTE_ARRAY(),用 pickle 序列化。
这会带来两个问题:
1)很多下游算子/转换(尤其 DataStream -> Table)要求类型"可解释",而不是一坨 pickle
2)性能上 pickle 通常更慢、也更难跨语言/跨生态联动
官方给了两个典型场景:转 Table 与 写 Sink。
3.1 DataStream 转 Table 时必须是"复合类型(composite type)"
t_env.from_data_stream(ds) 需要 ds 的输出类型是 Row/Tuple 这类 composite type。
所以像你这个例子里:
flat_map(split, Types.TUPLE([...]))必须明确类型- 因为后面
reduce会"隐式继承这个输出类型" - 最终
from_data_stream(ds)才能知道 schema
示例(你给的例子我保持同风格整理一下):
python
from pyflink.common.typeinfo import Types
from pyflink.datastream import StreamExecutionEnvironment
from pyflink.table import StreamTableEnvironment
def data_stream_api_demo():
env = StreamExecutionEnvironment.get_execution_environment()
t_env = StreamTableEnvironment.create(stream_execution_environment=env)
t_env.execute_sql("""
CREATE TABLE my_source (
a INT,
b VARCHAR
) WITH (
'connector' = 'datagen',
'number-of-rows' = '10'
)
""")
ds = t_env.to_append_stream(
t_env.from_path('my_source'),
Types.ROW([Types.INT(), Types.STRING()])
)
def split(s):
splits = s[1].split("|")
for sp in splits:
yield s[0], sp
ds = ds.map(lambda i: (i[0] + 1, i[1])) \
.flat_map(split, Types.TUPLE([Types.INT(), Types.STRING()])) \
.key_by(lambda i: i[1]) \
.reduce(lambda i, j: (i[0] + j[0], i[1]))
t_env.execute_sql("""
CREATE TABLE my_sink (
a INT,
b VARCHAR
) WITH (
'connector' = 'print'
)
""")
table = t_env.from_data_stream(ds)
table_result = table.execute_insert("my_sink")
# 本地/mini-cluster 执行建议 wait,防止脚本提前退出
table_result.wait()
if __name__ == '__main__':
data_stream_api_demo()
一句话:你只要把 DataStream 结果要转 Table,当场就把 output_type 写死。
3.2 写 Sink 时也建议显式 output_type
某些 sink 只接受特定结构(例如 Row/Tuple),map 后不写类型,可能导致 sink 端拿到 pickle 字节数组,或者 schema 不匹配。
python
ds.map(lambda i: (i[0] + 1, i[1]), Types.TUPLE([Types.INT(), Types.STRING()])) \
.sink_to(...)
4. Operator Chaining:为什么 Flink 默认会"把你的算子粘在一起"
官方描述的核心是:
默认会把多个非 shuffle 的 Python 算子链在一起,减少序列化/反序列化与调用开销,提高吞吐。
这能显著提升性能,但也会在某些场景"适得其反":
- 比如 flat_map 一个输入吐出成千上万个输出,链在一起可能导致下游处理被单并行度拖死
- 或你希望在某个节点切开,单独调整并行度/slot 资源
- 或希望隔离 backpressure 传播范围
4.1 禁用 chaining 的几种方式(官方列举)
你可以理解为三大类:
A. 用"会引入 shuffle/重分区"的算子切断(禁用后续 chaining)
在某个算子后面加以下操作之一,通常会打断链路:
key_by(shuffle)shufflerescalerebalancepartition_custom
B. 在当前算子上显式控制链路边界
start_new_chain():只断开"前面到我"的链disable_chaining():断开"前后两边"的链
C. 通过资源配置把链路切断
- 给上下游设置不同
parallelism - 或不同
slot sharing group - 或全局配置:
python.operator-chaining.enabled = false
实战建议:
- 默认别动 chaining(先跑通)
- 发现某段链"CPU 拉满且 backpressure 一路传"时,再考虑拆链
- flat_map 爆炸式输出、或需要单独调并行度的节点,是最常见拆链点
5. 工程化必看:Bundling Python Functions(否则远程必踩 ModuleNotFoundError)
官方给了一个非常真实的生产坑:
如果 Python functions 不在 main 文件里,而你提交到非本地模式(YARN/Standalone/K8s),不打包 python-files 很容易报:
ModuleNotFoundError: No module named 'my_function'
解决思路(按官方):用 python-files 把你的函数定义文件一起带上。
经验补充(写博客时可强调):
- 本地 IDE/mini cluster 可能"看不出问题"
- 一到远程集群就炸
- 所以从第一天就按"可提交"方式组织代码和依赖
6. 在 Python Function 里加载资源:用 open() 做一次性初始化
典型场景:模型推理/大字典/大配置,只想加载一次。
官方示例思路是:继承 Function(例如 MapFunction),在 open() 里加载资源,然后 map 里重复使用。
python
from pyflink.datastream.functions import MapFunction, RuntimeContext
import pickle
class Predict(MapFunction):
def open(self, runtime_context: RuntimeContext):
with open("resources.zip/resources/model.pkl", "rb") as f:
self.model = pickle.load(f)
def map(self, x):
return self.model.predict(x)
要点:
open()每个并行子任务会执行一次(相当于每个 subtask 初始化一次)- 模型要能在 TaskManager 侧访问到(通常配合文件分发/依赖打包)
7. 最后给你一套"写作业时的快速检查清单"
1)你用了 lambda 吗?如果是 ConnectedStream,换 CoMapFunction/CoFlatMapFunction
2)你写 output_type 了吗?尤其是:
- flat_map / map 后要转 Table
- sink 需要 Row/Tuple/schema
3)你远程跑吗?函数分文件了吗?如果是:配置 python-files
4)flat_map 输出爆炸吗?考虑拆链、调并行度
5)需要加载模型/资源吗?放 open(),别每条数据都加载