内容摘要
本文介绍如何使用阿里云实时计算 Flink 搭建一条"图片实时打标 + Embedding 计算"的事件驱动链路。通过 OSS CDC Connector 消费上传事件,在同一条 Flink 作业中并行调用百炼视觉大模型生成分类标签、调用本地 ResNet18 生成 512 维 Embedding 向量,实现图片上传即理解、即检索。
核心结论
Flink OSS CDC事件驱动替代定时扫描:图片上传后秒级触发处理,延迟从分钟级降至秒级。
一条Flink作业、两条支路、一次下载:FETCH_CONTENT 节点自动复用,图片只下载一次。
PyFlink DataFrame API 简化开发:几十行 Python 完成整条链路。
GPU 加速本地推理:Arrow batch 模式 + num_gpus 声明,充分利用 GPU。
01 为什么需要事件驱动的图片实时处理?
图像、视频、音频、文档等多模态数据正在越来越多地进入业务系统。对很多平台来说,文件上传到对象存储只是第一步,真正有价值的是尽快理解这些内容。以图片为例,用户在内容社区上传一张图片笔记或者商家在电商平台上传一张新品图片后,平台通常希望理解图片的大致内容:是宠物、美食、风景,还是需要进一步审核的异常。有了这些标签后,平台可以将其用于内容冷启动、推荐排序、运营分析和风控审核等下游场景。除此之外,平台往往也需要生成图片的 Embedding 并写入 Milvus 等向量数据库,从而支持后续的向量检索需求。
如果使用传统离线任务定时扫描对象存储(例如 OSS),新图片往往要等到下一个调度周期才会被处理。事件驱动链路无需反复调度:图片上传后,OSS 产生 ObjectCreated 事件并投递到 MNS Queue,Flink 作业通过 OSS CDC Connector 持续消费事件,根据 URI 下载一次图片内容,再并行调用百炼视觉 LLM 生成标签和置信度、调用本地模型生成 Embedding。
本文示例运行在阿里云实时计算 Flink 产品(www.aliyun.com/product/big…)之上。它是基于 Apache Flink 构建的全托管实时计算服务,提供作业开发、资源调度和运行维护能力,使用的阿里云实时计算 Flink 版本为 11.8。
02 整体架构是如何工作的?
这条图片处理链路本质上是一条读取图片内容、并行生成两类结果的实时流水线:OSS 上传事件进入作业后,OSS CDC Connector 读取新上传图片的 URL 和上传时间,统一为 uri 和 event_time。作业通过 FETCH_CONTENT 下载图片并增加 image_bytes 字段,随后分成两条支路:
- 支路一:将图片编码为 Base64 Data URL,调用百炼视觉 LLM 生成图片标签(category)和置信度(confidence)
- 支路二:将图片预处理为 tensor,使用 GPU 调用本地 ResNet18 模型生成经过 L2 归一化的 512 维 Embedding
最终两条支路分别输出图片分类结果和向量特征,供实时打标、相似度计算和向量检索等下游场景使用。
PyFlink DataFrame API 可以便捷地表达这样的实时链路:OSS CDC 事件读取、字段规范化、图片内容获取、处理分支和临时列裁剪,可以通过 read_generic、select 和 with_column 等 API 表达;图片解码、预处理和本地模型推理等自定义逻辑可以封装为 Python UDF;百炼视觉模型则通过 DataFrame LLM API 调用。
03 运行前需要准备哪些资源?
准备 OSS、MNS 和事件通知规则
Demo 需准备好以下资源:一个用于接收图片上传的 OSS Bucket;一个 MNS Queue(help.aliyun.com/zh/mns/gett…);一条 OSS 事件通知规则(help.aliyun.com/zh/mns/user…)——对象创建后,把 ObjectCreated:* 事件投递到这个 MNS Queue。投递的事件所含字段如下:
{
"eventName": "ObjectCreated:PutObject",
"eventTime": "2026-06-29T10:15:31Z",
"ossBucketName": "demo-bucket",
"ossObjectKey": "images/cat_001.jpg"
}
需要注意的是,MNS 里传的是"这张图片在哪里",不是图片内容本身。真正的图片内容会由 Flink 根据 oss://bucket/object_key 从 OSS 下载。
准备 ResNet18 权重文件
Demo 使用 torchvision 的 ResNet18 模型和 ImageNet 类别表。模型参数需预先准备并放到 TaskManager 可读取的本地路径,如 /flink/usrlib/resnet18-f37072fd.pth。线上作业也更推荐这种方式:模型文件提前准备好,worker 启动时直接加载本地文件,既不依赖外网,也更容易控制模型版本以及进行模型共享。
准备 Python 依赖
运行环境需要安装 pillow、torch、torchvision、CUDA 相关依赖。当前阿里云实时计算 Flink 11.8 版本已内置了相关依赖可以直接使用。如需自定义版本可参考使用 Python 依赖(help.aliyun.com/zh/flink/re…)文档进行配置。
开通 Flink AI 服务
调用百炼视觉 LLM 生成图片标签时需要 LLM 理解图片内容。本文推荐开通 Flink AI 服务(help.aliyun.com/zh/flink/re…),并选择内置模型,例如 qwen-vl-plus。Flink AI 服务提供托管式模型接入,使用内置模型时无需额外配置 API-Key、Endpoint 等参数,作业可以通过阿里云内网访问模型服务,简化模型接入和网络配置。
04 核心代码如何实现?
接下来按照数据流动顺序,依次展开主链路的几个关键位置:OSS CDC 事件读取、图片内容下载、调用百炼模型进行分类、调用本地模型进行 Embedding 计算和双 Sink 提交。完整代码见文末附录。
如何用 OSS CDC Connector 读取上传事件?
图片内容存储在 OSS,MNS Queue 只承载对象创建事件。Demo 使用 OSS CDC Connector 持续消费 MNS,并把事件转换成可直接处理的图片元信息:
df = pf.read_generic(
"oss-cdc",
schema={
"url": DataType.string(),
"modificationTime": DataType.timestamp_ltz(3),
},
options={
"endpoint": args.mns_endpoint,
"region": args.aliyun_region,
"queue-name": args.mns_queue_name,
"access-key-id": args.aliyun_access_key_id,
"access-key-secret": args.aliyun_access_key_secret,
"scan.startup.mode": "LATEST_OFFSET",
"oss-bucket": args.oss_bucket,
"path": args.oss_path,
},
)
url 是 Connector 构造好的完整 OSS URI,例如 oss://demo-bucket/images/cat.jpg;modificationTime 是对象上传时间。scan.startup.mode="LATEST_OFFSET" 表示作业启动后只处理新增事件。
如何用 fetch_content() 下载图片内容?
Connector 字段名属于外部接口。进入公共处理链后,Demo 将 modificationTime 和 url 统一重命名为业务字段 event_time 和 uri:
images = (
df.select(
event_time=pf.col("modificationTime"),
uri=pf.col("url"),
)
.with_column("image_bytes", pf.col("uri").fetch_content())
.select("event_time", "uri", "image_bytes")
)
fetch_content() 根据 oss:// URI 异步读取对象内容,并增加 image_bytes 字段。images 是两条推理支路的公共输入——百炼分类和本地 embedding 都消费同一列 image_bytes,因此执行计划可以复用公共的 FETCH_CONTENT 节点,而不必分别下载图片。
如何调用百炼视觉 LLM 生成图片标签?
作业启动时先注册百炼的 OpenAI 兼容模型,这里使用阿里云实时计算 Flink 内置的百炼模型推理服务。content_type="IMAGE_URL" 表示模型输入是图片 URL:
pf.set_model_provider(
pf.DashScopeProvider(
task="chat/completions",
model=args.bailian_model,
content_type="IMAGE_URL",
),
)
百炼推理分支将 image_bytes 进一步预处理为模型需要的 Base64 Data URL:
bailian_input = (
images
.with_column(
"image_data_url",
concat(
"data:",
pf.col("uri").mime_type(),
";base64,",
call_sql("TO_BASE64(`image_bytes`)"),
),
)
.select("event_time", "uri", "image_data_url")
)
标签集合使用 torchvision 提供的 ImageNet 类别表。ai_classify 调用百炼视觉 LLM,并返回 category 和 confidence:
def imagenet_labels():
from torchvision.models import ResNet18_Weights
return ResNet18_Weights.IMAGENET1K_V1.meta["categories"]
bailian_classified = bailian_input.llm.ai_classify(
"image_data_url",
imagenet_labels(),
provider="dashscope",
).select("event_time", "uri", "category", "confidence")
这一分支不运行本地分类模型。标签和置信度完全由百炼视觉 LLM 生成。
如何用本地 ResNet18 生成 512 维 Embedding?
本地推理分支先把图片解码为 RGB,再使用预训练权重处理图片生成 (3, 224, 224) 的 float32 tensor。阿里云 Flink 当前支持使用 TENSOR 类型表示多维数组:
@pf.udf(return_dtype=DataType.tensor("float32", shape=(3, 224, 224)))
class Preprocess(ScalarFunction):
"""Image bytes to a ResNet18 input tensor."""
def open(self, _function_context):
from torchvision.models import ResNet18_Weights
self.transform = ResNet18_Weights.IMAGENET1K_V1.transforms()
def eval(self, image_bytes):
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
return self.transform(image).numpy()
为了更好利用 GPU,Embedding UDF 用 Arrow batch 实现,并且通过 num_gpus、gpu_type 等参数指定 UDF 所需的 GPU 资源。它的返回类型是 ARRAY,便于写入向量数据库:
@pf.udf(
return_dtype=DataType.list(DataType.float32()),
func_type="arrow",
concurrency=1,
num_gpus=1.0,
gpu_type=os.environ.get("GPU_TYPE", DEFAULT_GPU_TYPE),
)
class ExtractEmbedding(ScalarFunction):
"""ResNet18 image tensor to an L2-normalized
512-dimensional embedding."""
def open(self, _function_context):
import torch
from torchvision.models import resnet18
checkpoint_path = os.environ.get(
"RESNET18_WEIGHTS_PATH",
DEFAULT_RESNET18_WEIGHTS_PATH
)
if not os.path.isfile(checkpoint_path):
raise FileNotFoundError(
f"ResNet18 weights not found: {checkpoint_path}"
)
self.device = torch.device(
"cuda" if torch.cuda.is_available() else "cpu"
)
self.model = resnet18(weights=None)
try:
state_dict = torch.load(
checkpoint_path,
map_location="cpu",
weights_only=True
)
except TypeError:
state_dict = torch.load(
checkpoint_path, map_location="cpu"
)
self.model.load_state_dict(state_dict)
self.model.fc = torch.nn.Identity()
self.model = self.model.eval().to(self.device)
def eval(self, image_tensor):
import torch
images = image_tensor.to_numpy(zero_copy_only=False)
with torch.inference_mode():
image_batch = torch.from_numpy(
images
).to(self.device)
embeddings = torch.nn.functional.normalize(
self.model(image_batch), dim=1
).cpu().numpy()
return pa.array(
embeddings.tolist(),
type=pa.list_(pa.float32())
)
最后该分支保留业务字段和 embedding 作为输出:
embeddings = (
images
.with_column("image_tensor", Preprocess(pf.col("image_bytes")))
.with_column("embedding", ExtractEmbedding(pf.col("image_tensor")))
.select("event_time", "uri", "embedding")
)
如何用 StatementSet 一次提交两个 Sink?
两个分支属于同一个 Flink 作业。Demo 使用 StatementSet 暂存两个写入操作,再调用一次 execute()。这里都采用了 print sink 用于验证链路。生产环境可以分别替换成 Kafka、Paimon、Hologres 或向量数据库 sink:
statement_set = pf.create_statement_set()
bailian_classified.write_generic(
"print",
options={
"print-identifier": "oss-cdc-bailian-image-classification",
"logger": "true",
},
statement_set=statement_set,
)
embeddings.write_generic(
"print",
options={
"print-identifier": "oss-cdc-resnet18-image-embedding",
"logger": "true",
},
statement_set=statement_set,
)
statement_set.execute()
05 如何启动和验证作业?
作业启动时需要提供 OSS CDC、OSS 路径和百炼模型参数,包括 mns-endpoint、aliyun-region、mns-queue-name、oss-bucket、oss-path 和 bailian-model 等。模型权重需要能从 TaskManager 本地访问,用户可以将权重文件上传到"附加依赖"中。阿里云实时计算 Flink 平台默认会把文件放到 TaskManager 的 /flink/usrlib 目录。
启动后,向配置了事件通知的 OSS Bucket 和指定前缀上传图片。由于启动模式是 LATEST_OFFSET,应在作业开始运行后再上传测试图片。运行时拓扑图如下:
对于这样一张测试图片:
print 输出如下,百炼模型识别出是"棕熊(brown bear)",且置信度较高,达到了 0.95;本地模型也打印了相关 Embedding 计算结果:
06 如何扩展到更多多模态场景?
Demo 使用百炼视觉 LLM 负责语义标签,使用本地 ResNet18 负责图片向量。两条支路相互独立,可以根据业务目标分别替换模型和 sink:
| 扩展方向 | 说明 |
|---|---|
| 使用业务标签 | 将 ImageNet 类别表替换为商品、内容审核或行业标签 |
| 更好的 Embedding | 将 ResNet18 替换为 DINO、CLIP 或领域微调模型 |
| 向量数据库 | 将 Embedding Sink 替换为 Milvus 等向量数据库 |
| OCR 与文档理解 | 在共享的 image_bytes 后增加 OCR、表格识别或信息抽取支路 |
| 视频关键帧分析 | 链路修改为"OSS 视频事件 → 抽帧 → 图片模型推理" |
对于图片、视频、音频和文档等非结构化数据,只要能把对象内容转换成模型输入,就可以沿用"事件触发、内容共享、多模型并行处理"的方式构建实时 AI 链路。
附录:完整 demo 代码
Python 实现:
import argparse
import io
import os
import pyarrow as pa
import pyflink.dataframe as pf
from PIL import Image
from pyflink.dataframe.datatype import DataType
from pyflink.table.expressions import call_sql, concat
from pyflink.table.udf import ScalarFunction
DEFAULT_RESNET18_WEIGHTS_PATH = "/flink/usrlib/resnet18-f37072fd.pth"
DEFAULT_GPU_TYPE = "V100"
DEFAULT_BAILIAN_MODEL = "qwen-vl-plus"
def imagenet_labels():
from torchvision.models import ResNet18_Weights
return ResNet18_Weights.IMAGENET1K_V1.meta["categories"]
@pf.udf(return_dtype=DataType.tensor("float32", shape=(3, 224, 224)))
class Preprocess(ScalarFunction):
"""Image bytes to a ResNet18 input tensor."""
def open(self, _function_context):
from torchvision.models import ResNet18_Weights
self.transform = ResNet18_Weights.IMAGENET1K_V1.transforms()
def eval(self, image_bytes):
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
return self.transform(image).numpy()
@pf.udf(
return_dtype=DataType.list(DataType.float32()),
func_type="arrow",
concurrency=1,
num_gpus=1.0,
gpu_type=os.environ.get("GPU_TYPE", DEFAULT_GPU_TYPE),
)
class ExtractEmbedding(ScalarFunction):
"""ResNet18 image tensor to an L2-normalized 512-dimensional embedding."""
def open(self, _function_context):
import torch
from torchvision.models import resnet18
checkpoint_path = os.environ.get(
"RESNET18_WEIGHTS_PATH", DEFAULT_RESNET18_WEIGHTS_PATH
)
if not os.path.isfile(checkpoint_path):
raise FileNotFoundError(
f"ResNet18 weights not found: {checkpoint_path}. "
"Download resnet18-f37072fd.pth and place it under /flink/usrlib, "
"or set RESNET18_WEIGHTS_PATH to the local file path."
)
self.device = torch.device(
"cuda" if torch.cuda.is_available() else "cpu"
)
self.model = resnet18(weights=None)
try:
state_dict = torch.load(
checkpoint_path, map_location="cpu", weights_only=True
)
except TypeError:
state_dict = torch.load(checkpoint_path, map_location="cpu")
self.model.load_state_dict(state_dict)
self.model.fc = torch.nn.Identity()
self.model = self.model.eval().to(self.device)
def eval(self, image_tensor):
import torch
images = image_tensor.to_numpy(zero_copy_only=False)
with torch.inference_mode():
image_batch = torch.from_numpy(images).to(self.device)
embeddings = torch.nn.functional.normalize(
self.model(image_batch), dim=1
).cpu().numpy()
return pa.array(embeddings.tolist(), type=pa.list_(pa.float32()))
def parse_args(argv=None):
parser = argparse.ArgumentParser()
parser.add_argument("--mns-endpoint", required=True)
parser.add_argument("--aliyun-region", required=True)
parser.add_argument(
"--mns-queue-name",
required=True,
help="MNS queue configured to receive only OSS ObjectCreated events.",
)
parser.add_argument("--aliyun-access-key-id", required=True)
parser.add_argument("--aliyun-access-key-secret", required=True)
parser.add_argument("--oss-bucket", required=True)
parser.add_argument(
"--oss-path",
required=True,
help="OSS key prefix to scan and monitor. Multiple prefixes can be comma-separated.",
)
parser.add_argument(
"--bailian-model",
default=DEFAULT_BAILIAN_MODEL,
help="Bailian model name.",
)
return parser.parse_args(argv)
def main(argv=None):
args = parse_args(argv)
pf.set_model_provider(
pf.DashScopeProvider(
task="chat/completions",
model=args.bailian_model,
content_type="IMAGE_URL",
),
)
pf.config.set("execution.checkpointing.interval", "20s")
df = pf.read_generic(
"oss-cdc",
schema={
"url": DataType.string(),
"modificationTime": DataType.timestamp_ltz(3),
},
options={
"endpoint": args.mns_endpoint,
"region": args.aliyun_region,
"queue-name": args.mns_queue_name,
"access-key-id": args.aliyun_access_key_id,
"access-key-secret": args.aliyun_access_key_secret,
"scan.startup.mode": "LATEST_OFFSET",
"oss-bucket": args.oss_bucket,
"path": args.oss_path,
},
)
images = (
df.select(
event_time=pf.col("modificationTime"),
uri=pf.col("url"),
)
.with_column("image_bytes", pf.col("uri").fetch_content())
.select("event_time", "uri", "image_bytes")
)
bailian_input = (
images
.with_column(
"image_data_url",
concat(
"data:",
pf.col("uri").mime_type(),
";base64,",
call_sql("TO_BASE64(`image_bytes`)"),
),
)
.select(
"event_time",
"uri",
"image_data_url",
)
)
bailian_classified = bailian_input.llm.ai_classify(
"image_data_url",
imagenet_labels(),
provider="dashscope",
).select(
"event_time",
"uri",
"category",
"confidence",
)
embeddings = (
images
.with_column("image_tensor", Preprocess(pf.col("image_bytes")))
.with_column(
"embedding", ExtractEmbedding(pf.col("image_tensor"))
)
.select(
"event_time",
"uri",
"embedding",
)
)
statement_set = pf.create_statement_set()
bailian_classified.write_generic(
"print",
options={
"print-identifier": "oss-cdc-bailian-image-classification",
"logger": "true",
},
statement_set=statement_set,
)
embeddings.write_generic(
"print",
options={
"print-identifier": "oss-cdc-resnet18-image-embedding",
"logger": "true",
},
statement_set=statement_set,
)
statement_set.execute()
if __name__ == "__main__":
main()
SQL 实现:
-- Local standalone prerequisites:
--
-- flink-conf.yaml:
-- execution.checkpointing.interval: 20s
--
-- TaskManager environment:
-- RESNET18_WEIGHTS_PATH=/path/to/resnet18-f37072fd.pth
--
-- The OSS-CDC source validates checkpointing from the cluster configuration.
-- FETCH_CONTENT initializes the OSS filesystem from the cluster configuration.
SET 'execution.checkpointing.interval' = '20s';
-- Python UDFs are defined in oss_cdc_bailian_image_classification_demo.py.
CREATE TEMPORARY FUNCTION Preprocess
AS 'oss_cdc_bailian_image_classification_demo.Preprocess'
LANGUAGE PYTHON;
CREATE TEMPORARY FUNCTION ExtractEmbedding
AS 'oss_cdc_bailian_image_classification_demo.ExtractEmbedding'
LANGUAGE PYTHON;
CREATE TEMPORARY MODEL image_model
INPUT (`input` STRING)
OUTPUT (`result` VARIANT)
WITH (
'provider' = 'dashscope',
'task' = 'chat/completions',
'model' = 'qwen-vl-plus',
'content-type' = 'IMAGE_URL'
);
CREATE TEMPORARY TABLE oss_events (
url STRING,
modificationTime TIMESTAMP_LTZ(3)
) WITH (
'connector' = 'oss-cdc',
'endpoint' = 'http://<mns-endpoint>.mns.cn-beijing.aliyuncs.com',
'region' = 'cn-beijing',
'queue-name' = 'your-mns-queue',
'access-key-id' = 'your-ak',
'access-key-secret' = 'your-sk',
'scan.startup.mode' = 'LATEST_OFFSET',
'oss-bucket' = 'your-bucket',
'path' = 'images/'
);
CREATE TEMPORARY TABLE classification_results (
event_time TIMESTAMP_LTZ(3),
uri STRING,
category STRING,
confidence DOUBLE
) WITH (
'connector' = 'print',
'print-identifier' = 'oss-cdc-bailian-image-classification',
'logger' = 'true'
);
CREATE TEMPORARY TABLE embedding_results (
event_time TIMESTAMP_LTZ(3),
uri STRING,
embedding ARRAY<FLOAT>
) WITH (
'connector' = 'print',
'print-identifier' = 'oss-cdc-resnet18-image-embedding',
'logger' = 'true'
);
-- FETCH_CONTENT is the shared upstream operation of both branches.
CREATE TEMPORARY VIEW images AS
SELECT
modificationTime AS event_time,
url AS uri,
FETCH_CONTENT(url) AS image_bytes
FROM oss_events;
CREATE TEMPORARY VIEW bailian_input AS
SELECT
event_time,
uri,
CONCAT(
'data:',
MIME_TYPE(uri),
';base64,',
TO_BASE64(image_bytes)
) AS image_data_url
FROM images;
CREATE TEMPORARY VIEW embeddings AS
SELECT
event_time,
uri,
ExtractEmbedding(Preprocess(image_bytes)) AS embedding
FROM images;
EXECUTE STATEMENT SET BEGIN
INSERT INTO classification_results
SELECT
event_time,
uri,
category,
confidence
FROM bailian_input,
LATERAL TABLE(
AI_CLASSIFY(
MODEL image_model,
image_data_url,
ARRAY['宠物', '美食', '风景', '商品', '票据', '其他']
)
);
INSERT INTO embedding_results
SELECT event_time, uri, embedding
FROM embeddings;
END;
07 FAQ
Q:Flink 事件驱动图片处理与传统的定时扫描方案有什么区别?
A:传统方案依赖定时任务轮询 OSS,新图片需等到下一个调度周期才被处理,延迟通常为分钟级。事件驱动方案通过Flink实时作业消费OSS ObjectCreated 事件,图片上传后秒级即可开始处理。
Q:百炼 LLM 和本地 ResNet18 各负责什么?
A:百炼视觉 LLM 负责语义层面的图片分类(输出人类可理解的标签和置信度),本地 ResNet18 负责生成 512 维向量 Embedding(用于相似度计算和向量检索)。两者互补,分别服务于"理解"和"检索"两类下游需求。
Q:如何替换为业务自定义的标签体系?
A:将 imagenet_labels() 函数返回的 ImageNet 1000 类替换为业务标签列表即可。ai_classify 接口接受任意字符串列表作为候选标签。
Q:生产环境应该用什么 Sink 替代 print?
A:分类结果可写入 Kafka、Paimon 或 Hologres 供下游实时消费;Embedding 结果可写入 Milvus 等向量数据库支持相似图片检索。
术语表
OSS:阿里云对象存储服务,用于存放图片、视频等非结构化文件
MNS:阿里云消息通知服务,支持将 OSS 事件投递到消息队列
OSS CDC Connector:Flink 连接器,持续消费 MNS 中的 OSS 对象变更事件
FETCH_CONTENT:Flink内置函数,根据 URI 异步下载对象内容
百炼(DashScope):阿里云大模型服务平台,提供 OpenAI 兼容 API
Embedding:将图片映射为固定维度的浮点向量,用于相似度计算和向量检索
L2 归一化:将向量缩放为单位长度(模长为 1),使余弦相似度等价于点积
参考资料
[1] OSS CDC Connector 官方文档:help.aliyun.com/zh/flink/re…
[2] 阿里云实时计算 Flink 产品:www.aliyun.com/product/big…
[3] Flink AI 服务:help.aliyun.com/zh/flink/re…
[4] 使用 Python 依赖:help.aliyun.com/zh/flink/re…
最后更新:2026 年 7 月 · 适用产品版本:阿里云实时计算 Flink 11.8