本地 Flink on K8s + Iceberg + MinIO 实时数仓平台 — AI部署指南与踩坑实录

📖 目录


项目概述

本项目使用Qwen3.7模型,在 macOS 本地环境搭建了一套完整的轻量级实时数仓平台,技术栈包括 Minikube (K8s)、Apache Flink 1.19.1、Apache Iceberg 1.10.2、MinIO 对象存储和 Kafka 消息队列。所有组件部署在 Minikube 单集群的 data-platform namespace 中,资源控制在 4 CPU / 6 GB 内存以内。

最终实现的数据流为:Kafka → Flink (SQL 流式处理) → Iceberg (Parquet) → MinIO (S3)

架构总览

复制代码
┌─────────────────────────────────────────────────────────┐
│                   Minikube (4CPU/6GB)                    │
│  ┌──────────┐  ┌──────────┐  ┌───────────────────────┐  │
│  │  MinIO   │  │  Kafka   │  │   Iceberg REST        │  │
│  │  (S3)    │  │  (KRaft) │  │   (SQLite 元数据)      │  │
│  │  256Mi   │  │  512Mi   │  │   256Mi               │  │
│  └────┬─────┘  └────┬─────┘  └───────┬───────────────┘  │
│       │              │                │                  │
│  ┌────┴──────────────┴────────────────┴──────────────┐  │
│  │          Flink 1.19.1 (Standalone on K8s)          │  │
│  │   JobManager: 1400Mi/1000m                         │  │
│  │   TaskManager: 1400Mi/1000m                        │  │
│  │   连接器: Kafka SQL + Iceberg REST + S3 FileIO      │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
         │                     │
    NodePort 服务          port-forward
         │                     │
┌────────┴─────────────────────┴─────────────┐
│             macOS 宿主机                     │
│  StreamPark 2.1.5 (localhost:10000)         │
│  Flink Web UI (localhost:8081)              │
│  MinIO Console (localhost:9001)             │
└────────────────────────────────────────────┘

资源分配 (总计 ~3.4 GB):

组件 内存限制 CPU 限制 说明
MinIO 256Mi 200m S3 兼容对象存储
Kafka 512Mi 300m KRaft 模式 (无 ZooKeeper),堆内存限制 256m
Iceberg REST 256Mi 200m 内置 SQLite 做元数据后端
Flink JobManager 1400Mi 1000m 进程内存 1024m
Flink TaskManager 1400Mi 1000m 进程内存 1024m

一、环境准备

1.1 安装 Minikube 和 Helm

bash 复制代码
brew install minikube helm
minikube start --memory=6144 --cpus=4 --disk-size=60g

踩坑 1:资源规划要一步到位。 Minikube 创建后无法调整 CPU/内存,必须删除重建。建议在开始前就规划好 4CPU/6GB。

1.2 创建 namespace

bash 复制代码
kubectl create namespace data-platform

1.3 Docker 镜像准备

由于 Docker Hub 在国内网络不稳定,推荐先在本地拉取镜像,再通过 docker save + minikube cp + docker load 的方式导入 Minikube:

bash 复制代码
docker pull flink:1.19.1-scala_2.12-java11
docker pull apache/iceberg-rest-fixture:latest
docker pull apache/kafka:3.8.0
docker pull minio/minio:latest

for img in flink:1.19.1-scala_2.12-java11 apache/iceberg-rest-fixture:latest \
           apache/kafka:3.8.0 minio/minio:latest; do
    docker save $img -o /tmp/${img//[:\/]/_}.tar
    minikube cp /tmp/${img//[:\/]/_}.tar /tmp/
    minikube ssh "docker load -i /tmp/${img//[:\/]/_}.tar"
done

二、部署 MinIO + 创建 Bucket

MinIO 作为 S3 兼容的对象存储,存放 Iceberg 的 Parquet 数据文件。

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio
  namespace: data-platform
spec:
  replicas: 1
  selector:
    matchLabels: { app: minio }
  template:
    metadata:
      labels: { app: minio }
    spec:
      containers:
      - name: minio
        image: minio/minio:latest
        args: ["server", "/data", "--console-address", ":9001"]
        env:
        - { name: MINIO_ROOT_USER, value: "minioadmin" }
        - { name: MINIO_ROOT_PASSWORD, value: "minioadmin" }
        ports:
        - { containerPort: 9000, name: api }
        - { containerPort: 9001, name: console }
        resources:
          requests: { memory: "128Mi", cpu: "50m" }
          limits:   { memory: "256Mi", cpu: "200m" }
---
apiVersion: v1
kind: Service
metadata:
  name: minio
  namespace: data-platform
spec:
  selector: { app: minio }
  ports:
  - { port: 9000, targetPort: 9000, name: api }
  - { port: 9001, targetPort: 9001, name: console }

创建 warehouse bucket 用于 Iceberg 数据:

bash 复制代码
kubectl run mc-init --image=minio/mc --restart=Never -n data-platform -- \
  sh -c "mc alias set local http://minio:9000 minioadmin minioadmin && \
         mc mb --ignore-existing local/warehouse"

踩坑 2:MinIO 重启后 bucket 丢失。 MinIO 使用 emptyDir 存储,Pod 重启后数据丢失。每次重启后需要重新执行 mc mb --ignore-existing local/warehouse。生产环境应使用 PVC。


三、部署 Kafka (KRaft 模式)

使用 Kafka 3.8.0 的 KRaft 模式,无需 ZooKeeper,节省资源。

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kafka
  namespace: data-platform
spec:
  replicas: 1
  selector:
    matchLabels: { app: kafka }
  template:
    metadata:
      labels: { app: kafka }
    spec:
      containers:
      - name: kafka
        image: apache/kafka:3.8.0
        env:
        - { name: KAFKA_NODE_ID, value: "1" }
        - { name: KAFKA_PROCESS_ROLES, value: "broker,controller" }
        - { name: KAFKA_CONTROLLER_QUORUM_VOTERS, value: "1@kafka:9093" }
        - { name: KAFKA_LISTENERS, value: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:9094" }
        - { name: KAFKA_ADVERTISED_LISTENERS, value: "PLAINTEXT://kafka:9092,EXTERNAL://kafka:9094" }
        - { name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP, value: "PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT" }
        - { name: KAFKA_CONTROLLER_LISTENER_NAMES, value: "CONTROLLER" }
        - { name: KAFKA_INTER_BROKER_LISTENER_NAME, value: "PLAINTEXT" }
        - { name: KAFKA_HEAP_OPTS, value: "-Xmx256m -Xms256m" }
        - { name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR, value: "1" }
        - { name: CLUSTER_ID, value: "MkU3OEVBNTcwNTJENDM2Qk" }
        ports:
        - { containerPort: 9092, name: internal }
        - { containerPort: 9094, name: external }
        resources:
          requests: { memory: "256Mi", cpu: "100m" }
          limits:   { memory: "512Mi", cpu: "300m" }
---
apiVersion: v1
kind: Service
metadata:
  name: kafka
  namespace: data-platform
spec:
  selector: { app: kafka }
  ports:
  - { port: 9092, targetPort: 9092, name: internal }
  - { port: 9094, targetPort: 9094, name: external }
---
apiVersion: v1
kind: Service
metadata:
  name: kafka-external
  namespace: data-platform
spec:
  type: NodePort
  selector: { app: kafka }
  ports:
  - { port: 9094, targetPort: 9094, nodePort: 30094 }

踩坑 3:Kafka OOMKilled。 Kafka 默认堆内存 1GB,远超 512Mi 限制。必须通过 KAFKA_HEAP_OPTS=-Xmx256m -Xms256m 限制堆内存。
踩坑 4:Pod 重启后 Kafka topic 丢失。 每次重启后需要重新创建 topic:

bash 复制代码
kubectl exec -n data-platform $KAFKA_POD -- kafka-topics.sh \
  --create --if-not-exists --topic orders --bootstrap-server localhost:9092 \
  --partitions 2 --replication-factor 1

四、部署 Iceberg REST Catalog (SQLite 后端)

使用 apache/iceberg-rest-fixture 镜像,内置 SQLite JDBC 驱动,无需额外的 PostgreSQL 数据库。

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: iceberg-rest
  namespace: data-platform
spec:
  replicas: 1
  selector:
    matchLabels: { app: iceberg-rest }
  template:
    metadata:
      labels: { app: iceberg-rest }
    spec:
      containers:
      - name: iceberg-rest
        image: apache/iceberg-rest-fixture:latest
        env:
        - { name: AWS_ACCESS_KEY_ID, value: "minioadmin" }
        - { name: AWS_SECRET_ACCESS_KEY, value: "minioadmin" }
        - { name: AWS_REGION, value: "us-east-1" }
        - { name: CATALOG_CATALOG__IMPL, value: "org.apache.iceberg.jdbc.JdbcCatalog" }
        - { name: CATALOG_URI, value: "jdbc:sqlite:/tmp/iceberg_catalog.db" }
        - { name: CATALOG_WAREHOUSE, value: "s3a://warehouse/" }
        - { name: CATALOG_IO__IMPL, value: "org.apache.iceberg.aws.s3.S3FileIO" }
        - { name: CATALOG_S3_ENDPOINT, value: "http://minio:9000" }
        - { name: CATALOG_S3_PATH__STYLE__ACCESS, value: "true" }
        ports:
        - { containerPort: 8181 }
        resources:
          requests: { memory: "128Mi", cpu: "50m" }
          limits:   { memory: "256Mi", cpu: "200m" }

踩坑 5:Iceberg 元数据重启丢失。 SQLite 数据库存储在 /tmp/iceberg_catalog.db (容器临时文件系统),Pod 重启后丢失。每次重启后需要重新创建 namespace:

bash 复制代码
curl -X POST http://localhost:8181/v1/namespaces \
  -H 'Content-Type: application/json' \
  -d '{"namespace":["default"]}'

这是整个部署中最复杂的环节。Flink 官方 Docker 镜像不包含第三方连接器,需要手动准备并通过 hostPath 挂载注入 Pod。

5.1 下载 JAR 列表

在宿主机 /tmp/flink-connectors/ 目录下载以下 JAR:

JAR 名称 来源 说明
flink-sql-connector-kafka.jar Maven Central (3.2.0-1.19) Kafka SQL 连接器
iceberg-flink-runtime-1.19.jar Maven Central (1.10.2) Iceberg Flink 运行时 (需打补丁)
iceberg-aws-bundle.jar Maven Central (1.10.2) AWS SDK v2 (S3 访问)
flink-s3-fs-hadoop.jar Flink 1.19.1 发行包 S3 文件系统插件
hadoop-common.jar Maven Central (3.3.6) Hadoop 通用库
hadoop-auth.jar Maven Central (3.3.6) Hadoop 认证
hadoop-annotations.jar Maven Central (3.3.6) Hadoop 注解
hadoop-hdfs-client.jar Maven Central (3.3.6) Hadoop HDFS 客户端
hadoop-shaded-guava.jar Maven Central (1.1.1) Hadoop Shaded Guava (关键依赖)
hadoop-mapreduce-client-core.jar Maven Central (3.3.6) MapReduce 核心 (SELECT 查询需要)
woodstox-core.jar Maven Central (6.4.0) XML 解析
stax2-api.jar Maven Central (4.2.1) StAX API
commons-configuration2.jar Maven Central (2.8.0) 配置管理
commons-lang3.jar Maven Central (3.12.0) 字符串工具
re2j.jar Maven Central (1.7) 正则引擎
hive-metastore.jar Maven Central (2.3.9) Hive Metastore (FlinkCatalog 类引用)
libthrift.jar Maven Central (0.13.0) Thrift 库
libfb303.jar Maven Central (0.9.3) Facebook Thrift

5.2 导入 Minikube

bash 复制代码
# 将所有 JAR 复制到 Minikube 容器的 /opt/flink-connectors/
docker cp /tmp/flink-connectors/. minikube:/opt/flink-connectors/

踩坑 6:Init Container 网络失败。 最初尝试用 Init Container 在线下载 JAR,但 Minikube 内网络不稳定导致反复失败。最终方案:在宿主机下载好,通过 docker cp 导入 Minikube,再通过 K8s hostPath volume 挂载到 Flink Pod。


六、核心难点:PatchedFlinkCatalogFactory

这是本项目最关键的技术突破点。Iceberg 1.10.2 的 FlinkCatalogFactory 与 Flink 1.19.1 存在三个兼容性问题,需要通过自定义 Java 类解决。

问题 A:SPI 注册缺失

iceberg-flink-runtime-1.19.jarMETA-INF/services/org.apache.flink.table.factories.Factory 只注册了 FlinkDynamicTableFactory,缺少 FlinkCatalogFactory。Flink 1.19 的 FactoryUtil.discoverFactory() 通过此 SPI 文件发现 Catalog 工厂。

问题 B:factoryIdentifier() 返回 null 导致 NPE

Iceberg 的 FlinkCatalogFactory 继承自 CatalogFactory,其默认 factoryIdentifier() 方法返回 null。Flink 1.19 的 discoverFactory() 内部执行 null.equals(identifier) 抛出 NullPointerException

问题 C:Hive 依赖链

原始的 FlinkCatalogFactory.createCatalog() 方法会触发 Hive Metastore 相关类的加载,导致大量额外的依赖链。

解决方案:PatchedFlinkCatalogFactory

创建一个继承 FlinkCatalogFactory 的子类,重写关键方法:

java 复制代码
package org.apache.iceberg.flink;

import org.apache.flink.configuration.ConfigOption;
import org.apache.flink.table.catalog.Catalog;
import org.apache.flink.table.factories.CatalogFactory;
import org.apache.iceberg.catalog.Namespace;
import org.apache.hadoop.conf.Configuration;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

public class PatchedFlinkCatalogFactory extends FlinkCatalogFactory {

    @Override
    public String factoryIdentifier() {
        return "rest";  // 解决 NPE:返回非 null 标识符
    }

    @Override
    public Set<ConfigOption<?>> requiredOptions() {
        return Collections.emptySet();
    }

    @Override
    public Set<ConfigOption<?>> optionalOptions() {
        return Collections.emptySet();
    }

    @Override
    public Catalog createCatalog(CatalogFactory.Context context) {
        String name = context.getName();
        Map<String, String> options = context.getOptions();
        String defaultDb = options.getOrDefault("default-database", "default");
        Configuration hadoopConf = clusterHadoopConf();

        // 直接创建 REST catalog loader,绕过 Hive 依赖链
        CatalogLoader catalogLoader = CatalogLoader.rest(name, hadoopConf, options);

        // 关键:baseNamespace 必须用 Namespace.empty()
        // 如果用 Namespace.of(defaultDb),会导致 namespace 被拼接为
        // default%1Fdefault(两级),REST API 返回 400
        return new FlinkCatalog(
            name, defaultDb, Namespace.empty(),
            catalogLoader, options, true, 60000L
        );
    }
}

编译与注入流程:

bash 复制代码
# 1. 编译 (必须使用 --release 11,因为 Flink 使用 JRE 11)
FLINK_LIB="/path/to/flink-1.19.1/lib"
ICEBERG_JAR="$FLINK_LIB/iceberg-flink-runtime-1.19.jar"

javac --release 11 \
  -cp "$ICEBERG_JAR:$FLINK_LIB/flink-table-api-java-uber-1.19.1.jar:\
$FLINK_LIB/flink-table-runtime-1.19.1.jar:$FLINK_LIB/flink-dist-1.19.1.jar:\
$FLINK_LIB/flink-core-1.19.1.jar:$FLINK_LIB/hadoop-common.jar:\
$FLINK_LIB/hadoop-auth.jar" \
  org/apache/iceberg/flink/PatchedFlinkCatalogFactory.java

# 2. 注入到 iceberg-flink-runtime jar (不能用 jar uf,shaded jar 有重复条目)
python3 << 'PYEOF'
import zipfile, shutil
src = "/path/to/iceberg-flink-runtime-1.19.jar"
dst = src + ".new"
patch = "/path/to/PatchedFlinkCatalogFactory.class"

with zipfile.ZipFile(src, 'r') as zin:
    with zipfile.ZipFile(dst, 'w', zipfile.ZIP_DEFLATED) as zout:
        for entry in zin.namelist():
            if entry == "org/apache/iceberg/flink/PatchedFlinkCatalogFactory.class":
                with open(patch, 'rb') as f:
                    zout.writestr(entry, f.read())
            else:
                zout.writestr(entry, zin.read(entry))
shutil.move(dst, src)
PYEOF

踩坑 7:jar uf 无法用于 shaded uber-jar。 执行 jar uf iceberg-flink-runtime.jar PatchedFlinkCatalogFactory.class 会报 ZipException: duplicate entry: LICENSE。必须用 Python zipfile 模块逐条目重建 JAR。
踩坑 8:Namespace.of(defaultDb) 导致 namespace 双重编码。 FlinkCatalog 构造函数的第三个参数是 baseNamespace。如果传入 Namespace.of("default"),当 Flink 解析 iceberg_catalog.default.orders_iceberg 时,Iceberg 会将 baseNamespace 和 database 拼接为 ["default", "default"],REST URL 变成 default%1Fdefault,服务端返回 400 "Suspicious Path Character"。正确做法是传入 Namespace.empty(),让 database 名直接映射到 Iceberg namespace。


不使用 Flink Operator,直接用 K8s Deployment 部署 Flink Standalone 集群。核心设计:通过自定义 command 在启动脚本中完成 JAR 复制和 config.yaml 生成。

7.1 启动脚本设计

Flink Docker 镜像的 docker-entrypoint.sh 处理 FLINK_PROPERTIES 环境变量,但当自定义 command 时会绕过入口脚本。因此必须在启动脚本中手动写 config.yaml

bash 复制代码
# 复制连接器 JAR
cp /connectors/flink-sql-connector-kafka.jar $FLINK_HOME/lib/
cp /connectors/iceberg-flink-runtime.jar $FLINK_HOME/lib/
# ... (其余 JAR 同上)

# 生成 config.yaml
echo "jobmanager.rpc.address: flink-jobmanager" > $FLINK_HOME/conf/config.yaml
echo "taskmanager.numberOfTaskSlots: 2" >> $FLINK_HOME/conf/config.yaml
echo "state.backend: hashmap" >> $FLINK_HOME/conf/config.yaml
echo "rest.bind-address: 0.0.0.0" >> $FLINK_HOME/conf/config.yaml
echo "jobmanager.memory.process.size: 1024m" >> $FLINK_HOME/conf/config.yaml  # JM 需要
echo "blob.server.port: 6124" >> $FLINK_HOME/conf/config.yaml
echo "blob.fetch.num-retries: 10" >> $FLINK_HOME/conf/config.yaml

# 启动 (必须用 exec 替换 PID 1)
exec $FLINK_HOME/bin/jobmanager.sh start-foreground

踩坑 9:FLINK_PROPERTIES 环境变量不生效。 自定义 command: ["/bin/bash", "-c"] 会绕过 docker-entrypoint.shFLINK_PROPERTIES 不会被解析为 config.yaml。必须在脚本中用 echo 逐行写入配置文件。
踩坑 10:Flink 拒绝启动 --- 缺少内存配置。 手动写 config.yaml 时,如果不包含 jobmanager.memory.process.size (JM) 或 taskmanager.memory.process.size ™,Flink 会启动失败。容器内存限制 1400Mi,进程内存设为 1024m。
踩坑 11:TM 连接 localhost:6123。 如果不在 config.yaml 中设置 jobmanager.rpc.address: flink-jobmanager,TM 会默认连接 localhost,找不到 JM。
踩坑 12:Blob 传输超时。 Flink SQL Client 会将 lib/ 下所有 JAR (~80MB) 作为 blob 从 JM 传输到 TM。在资源受限环境中容易超时。设置 blob.server.port: 6124blob.fetch.num-retries: 10 可以显著提高稳定性。

7.2 YAML heredoc 缩进问题

踩坑 13:K8s YAML 中的 heredoc 缩进。 在 K8s args 字段中使用 cat << 'EOF' 时,YAML 的缩进空格会成为文件内容的一部分,导致 config.yaml 解析失败。推荐使用 echo "key: value" >> file 逐行写入。

7.3 S3 文件系统插件

flink-s3-fs-hadoop.jar 必须放在 $FLINK_HOME/plugins/s3-fs-hadoop/ 目录下(不是 lib/),Flink 的插件机制要求文件系统实现放在 plugins/ 子目录中。


8.1 SQL 定义

sql 复制代码
SET 'execution.checkpointing.interval' = '10s';

CREATE CATALOG iceberg_catalog WITH (
  'type' = 'rest',
  'uri' = 'http://iceberg-rest:8181',        -- K8s 内部服务名
  'warehouse' = 's3a://warehouse/',
  'io-impl' = 'org.apache.iceberg.aws.s3.S3FileIO',
  's3.endpoint' = 'http://minio:9000',        -- K8s 内部服务名
  's3.path-style-access' = 'true',
  's3.access-key-id' = 'minioadmin',
  's3.secret-access-key' = 'minioadmin'
);

USE CATALOG iceberg_catalog;

CREATE TEMPORARY TABLE kafka_orders (
  order_id BIGINT,
  user_id BIGINT,
  product_id INT,
  quantity INT,
  price DECIMAL(10,2),
  city STRING,
  status STRING,
  order_time TIMESTAMP(3),
  dt AS DATE_FORMAT(order_time, 'yyyy-MM-dd'),
  WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (
  'connector' = 'kafka',
  'topic' = 'orders',
  'properties.bootstrap.servers' = 'kafka:9092',  -- K8s 内部服务名
  'scan.startup.mode' = 'latest-offset',
  'format' = 'json',
  'json.timestamp-format.standard' = 'ISO-8601'
);

CREATE TABLE IF NOT EXISTS orders_iceberg (
  order_id BIGINT,
  user_id BIGINT,
  product_id INT,
  quantity INT,
  price DECIMAL(10,2),
  city STRING,
  status STRING,
  order_time TIMESTAMP(3),
  dt STRING
) PARTITIONED BY (dt) WITH (
  'format-version' = '2'
);

INSERT INTO orders_iceberg
SELECT * FROM kafka_orders;

8.2 提交方式

通过 Pod 内 Flink SQL Client 提交(使用 K8s 服务名):

bash 复制代码
JM_POD=$(kubectl get pods -n data-platform -l component=jobmanager \
  -o jsonpath='{.items[0].metadata.name}')

# 将 SQL 写入 Pod 内文件
kubectl exec -n data-platform $JM_POD -- bash -c 'cat > /tmp/submit.sql << "EOF"
... (上面的 SQL) ...
EOF'

# 提交
kubectl exec -n data-platform $JM_POD -- /opt/flink/bin/sql-client.sh -f /tmp/submit.sql

踩坑 14:CREATE TABLE IF NOT EXISTS 与 schema 不匹配。 如果 Iceberg 中已存在同名但 schema 不同的表,IF NOT EXISTS 会保留旧表,后续 INSERT INTO 时因列数/类型不匹配而失败。解决方法:先通过 REST API 删除旧表:

bash 复制代码
curl -X DELETE "http://localhost:8181/v1/namespaces/default/tables/orders_iceberg" \
  -H 'Content-Type: application/json' -d '{"purgeRequested": true}'

踩坑 15:Iceberg catalog 内的表不能指定 'connector' = 'iceberg' 当使用 Iceberg Catalog (而非 default_catalog) 创建表时,不需要也不能指定 connector 属性,因为 Catalog 本身已知道是 Iceberg 类型。
踩坑 16:default 是 Flink SQL 保留字。 引用 default 数据库名时需要用反引号:```default.orders_iceberg``。或者不写数据库名,因为 USE CATALOG后默认数据库就是default`。


九、StreamPark 集成

StreamPark 2.1.5 提供了 Web UI 和 API 来管理 Flink 应用。

9.1 添加 Flink 环境

bash 复制代码
TOKEN=$(curl -s -X POST "http://localhost:10000/passport/signin" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'username=admin&password=streampark&loginType=PASSWORD' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")

curl -X POST "http://localhost:10000/flink/env/create" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Authorization: $TOKEN" \
  -d 'flinkName=Flink-1.19.1-K8s&flinkHome=/path/to/flink-1.19.1&description=Flink 1.19.1 for K8s'

9.2 添加 Flink 远程集群

bash 复制代码
curl -X POST "http://localhost:10000/flink/cluster/create" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Authorization: $TOKEN" \
  -d 'clusterName=flink_02&executionMode=1&versionId=2&address=http://127.0.0.1:8081&description=Remote Flink cluster'

踩坑 17:StreamPark API 认证 --- 必须用 form-urlencoded。 POST /passport/signin 如果使用 Content-Type: application/json,会返回 {"code":0,"status":"success"}静默丢弃 token 。必须使用 application/x-www-form-urlencoded,且 loginType 必须是枚举名 "PASSWORD" 而非整数 0
踩坑 18:StreamPark REMOTE 模式的双向网络问题 (macOS)。 StreamPark 在宿主机编译 SQL 时需要访问 Iceberg REST 和 Kafka,但编译后的 JobGraph 在 K8s Pod 内运行时需要使用 K8s 服务名。macOS Docker Desktop 的网络隔离导致 minikube IP (192.168.49.2) 从宿主机不可达。最终方案:通过 Pod 内 Flink SQL Client 直接提交任务,StreamPark 用于集群监控。


十、端到端验证

10.1 发送测试数据

bash 复制代码
echo '{"order_id":9001,"user_id":1,"product_id":100,"quantity":2,"price":99.50,"city":"Beijing","status":"paid","order_time":"2026-06-11T16:40:00"}
{"order_id":9002,"user_id":2,"product_id":200,"quantity":1,"price":150.00,"city":"Shanghai","status":"paid","order_time":"2026-06-11T16:40:30"}
{"order_id":9003,"user_id":3,"product_id":100,"quantity":3,"price":75.80,"city":"Guangzhou","status":"pending","order_time":"2026-06-11T16:41:00"}' \
| kubectl exec -i -n data-platform $KAFKA_POD -- \
    kafka-console-producer.sh --bootstrap-server localhost:9092 --topic orders

10.2 验证结果

等待 10-20 秒(一个 checkpoint 周期)后,MinIO 中会出现新的 parquet 文件:

复制代码
warehouse/default/orders_iceberg/data/dt=2026-06-11/
├── 00000-0-ae1b9800-...-00001.parquet  (3 rows, batch 1)
└── 00000-0-ae1b9800-...-00002.parquet  (2 rows, batch 2)

Iceberg 元数据可通过 REST API 查看:

bash 复制代码
curl http://localhost:8181/v1/namespaces/default/tables/orders_iceberg \
  | python3 -c "import sys,json; d=json.load(sys.stdin); \
    print(d['metadata']['snapshots'][-1]['summary'])"

输出:

python 复制代码
{'operation': 'append',
 'flink.job-id': '335c0ddd7334653a2c937fbb817f11fa',
 'total-records': '3',
 'total-data-files': '1',
 'engine-version': '1.19.1',
 'iceberg-version': 'Apache Iceberg 1.10.2'}

十一、踩坑清单速查

# 问题 根因 解决方案
1 Minikube 无法调整资源 创建后不可变 创建时规划好 4CPU/6GB
2 MinIO bucket 重启丢失 emptyDir 存储 重启后 mc mb --ignore-existing
3 Kafka OOMKilled 默认 1GB 堆内存 KAFKA_HEAP_OPTS=-Xmx256m
4 Kafka topic 重启丢失 临时存储 重启后 --create --if-not-exists
5 Iceberg 元数据重启丢失 SQLite 在 /tmp 重启后重新创建 namespace
6 Init Container 下载 JAR 失败 网络不稳定 宿主机下载 + docker cp + hostPath
7 jar uf 在 shaded jar 上报错 重复 LICENSE 条目 用 Python zipfile 模块
8 default%1Fdefault namespace 双重编码 Namespace.of(defaultDb) 拼接 改用 Namespace.empty()
9 FLINK_PROPERTIES 不生效 自定义 command 绕过入口脚本 手动 echo 写 config.yaml
10 Flink 启动失败 --- 缺少内存配置 config.yaml 未包含内存参数 写入 *.memory.process.size
11 TM 连接 localhost:6123 缺少 rpc address 配置 jobmanager.rpc.address: flink-jobmanager
12 Blob 传输 JM→TM 超时 lib/ JAR 太大 blob.server.port: 6124, num-retries: 10
13 YAML heredoc 缩进污染文件内容 YAML 缩进成为文件内容 echo 逐行写入
14 CREATE TABLE IF NOT EXISTS schema 不匹配 旧表 schema 不同 先 DELETE 再 CREATE
15 Iceberg catalog 内不能指定 connector Catalog 已隐含类型 移除 'connector'='iceberg'
16 default 是 SQL 保留字 Flink SQL parser 用反引号或省略
17 StreamPark API 返回空 token JSON body + 整数 loginType form-urlencoded + loginType=PASSWORD
18 StreamPark REMOTE 模式双向网络不通 macOS Docker 网络隔离 Pod 内 SQL Client 提交
19 factoryIdentifier() NPE Iceberg FlinkCatalogFactory 未实现 PatchedFlinkCatalogFactory 返回 "rest"
20 SELECT 查询 FileInputFormat 缺失 缺少 hadoop-mapreduce-client-core 添加 JAR 到 lib/
21 UserGroupInformation crash hadoop-shaded-guava 缺失 添加 hadoop-shaded-guava-1.1.1.jar
22 javac 编译版本不兼容 Flink JRE 11 vs 宿主机 JDK 21 javac --release 11

十二、重启恢复流程

由于使用临时存储,每次 Docker Desktop / Minikube 重启后需要执行以下恢复步骤:

bash 复制代码
# 1. 启动基础设施
minikube start

# 2. 重建易失资源
kubectl exec -n data-platform $KAFKA_POD -- kafka-topics.sh \
  --create --if-not-exists --topic orders --bootstrap-server localhost:9092 \
  --partitions 2 --replication-factor 1

curl -X POST http://localhost:8181/v1/namespaces \
  -H 'Content-Type: application/json' -d '{"namespace":["default"]}'

kubectl exec -n data-platform $MINIO_POD -- mc alias set local http://localhost:9000 minioadmin minioadmin
kubectl exec -n data-platform $MINIO_POD -- mc mb --ignore-existing local/warehouse

# 3. 建立 port-forward
kubectl port-forward svc/flink-jobmanager 8081:8081 -n data-platform &
kubectl port-forward svc/iceberg-rest 8181:8181 -n data-platform &

# 4. 重新提交 Flink SQL 任务
kubectl exec -n data-platform $JM_POD -- /opt/flink/bin/sql-client.sh -f /tmp/submit.sql

十三、关键文件清单

复制代码
flink-iceberg-platform/
├── flink-iceberg-lite.yaml          # 主部署文件 (Iceberg REST + Flink JM/TM)
├── minio/minio-deployment.yaml      # MinIO 部署
├── kafka/kafka-deployment.yaml      # Kafka 部署
├── demo/flink-sql-orders.sql        # Flink SQL 流式任务
└── demo/generate-orders.sh          # Kafka 测试数据生成脚本

/tmp/flink-connectors/               # 连接器 JAR 目录 (hostPath 挂载源)
/tmp/iceberg_patch/                  # PatchedFlinkCatalogFactory 源码和编译产物

十四、效果

创建的flink任务

写入的元数据

相关推荐
蜂蜜黄油呀土豆1 小时前
ReWOO 与 Plan-and-Execute:解耦的规划
python·ai·大模型
金融RPA机器人丨实在智能1 小时前
工程线索工具合规避坑指南:使用开源爬虫抓取数据会触犯法规吗?实在Agent给出了安全答案
人工智能·爬虫·安全·ai·开源
阿坤带你走近大数据1 小时前
flink的架构介绍
大数据·架构·flink
Java知识技术分享1 小时前
node安装新版本,并解决opencode和claude code不能用问题
ai·个人开发·ai编程
2501_946786201 小时前
2026算法分级分类备案TOP5解读——吃透差异化监管,规避过度合规风险
大数据
IPDEEP全球代理1 小时前
TikTok为什么封号?应该怎么解决?(附IP环境解决方案)
大数据
我认不到你1 小时前
【开源、教程】RAG全流程实现(java+完整代码):第一弹
java·开发语言·人工智能·深度学习·ai·语言模型·开源
放下华子我只抽RuiKe51 小时前
FastAPI 全栈后端(五):后台任务与消息队列
前端·javascript·react.js·ai·前端框架·fastapi·ai编程
装不满的克莱因瓶2 小时前
自然语言处理中的词嵌入——从离散符号到语义向量空间
人工智能·python·深度学习·ai·自然语言处理·nlp