Flink 实战:从零搭建大数据开发环境到用户行为分析

一篇文章带你搞定 Flink 开发环境搭建、作业提交、以及与 Kafka、MySQL 的完整交互

前言

最近在实践 Flink 实时计算,从环境搭建到作业开发,踩了不少坑。今天就把这些经验整理出来,希望能帮助到正在学习 Flink 的你。

本文会涵盖:

  • Docker Compose 一键搭建 Flink + Kafka + MySQL 开发环境

  • Flink 作业的打包与提交

  • 从 Kafka 消费数据,实时计算 PV/UV,写入 MySQL

  • 常见问题的解决方案


一、为什么选择 Docker Compose?

学习 Flink 最大的痛点就是环境搭建。要装 Hadoop、Kafka、MySQL、Flink 集群...想想就头大。

使用 Docker Compose,一个命令就能启动所有组件:

bash

XML 复制代码
docker-compose up -d

所有组件都在容器中运行,互不干扰,用完即焚,非常适合本地开发调试。


二、环境配置详解

2.1 docker-compose.yml 核心配置

yaml

XML 复制代码
services:
  # Flink JobManager
  jobmanager:
    image: flink:1.18.0-scala_2.12-java11
    ports:
      - "8081:8081"   # Flink Web UI
    command: jobmanager
    volumes:
      - ./target:/opt/flink/usrlib   # 挂载打包目录
    environment:
      FLINK_PROPERTIES: |
        jobmanager.rpc.address: jobmanager
        taskmanager.numberOfTaskSlots: 4
        parallelism.default: 4

  # Flink TaskManager
  taskmanager:
    image: flink:1.18.0-scala_2.12-java11
    depends_on:
      - jobmanager
    command: taskmanager
    scale: 1

  # Kafka (KRaft模式,无需Zookeeper)
  kafka:
    image: confluentinc/cp-kafka:7.5.0
    ports:
      - "9092:9092"
    environment:
      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092'

  # MySQL
  mysql:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: flink_db

关键点:

  • 所有服务在同一个 Docker 网络下,可以通过服务名互相访问

  • ./target 目录挂载到容器,这样 Flink 可以直接加载我们打的 JAR 包


三、Maven 项目配置

3.1 pom.xml 关键依赖

xml

XML 复制代码
<properties>
    <flink.version>1.18.0</flink.version>
    <hadoop.version>3.3.4</hadoop.version>
</properties>

<dependencies>
    <!-- Flink 核心依赖,scope 为 provided -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java</artifactId>
        <version>${flink.version}</version>
        <scope>provided</scope>
    </dependency>

    <!-- Kafka 连接器 -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-kafka</artifactId>
        <version>3.1.0-1.18</version>
    </dependency>

    <!-- JDBC 连接器(用于 MySQL) -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-jdbc</artifactId>
        <version>3.1.2-1.18</version>
    </dependency>

    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
</dependencies>

3.2 Shade 插件配置(Fat JAR)

xml

XML 复制代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.5.0</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <!-- 注意:ManifestResourceTransformer 直接用 mainClass,不要套 resource 标签 -->
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.boonya.flink.UserBehaviorAnalysisJob</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

踩坑提醒: ManifestResourceTransformer 的配置不要画蛇添足加 <resource> 标签,否则会报错 Cannot find 'resource' in class ManifestResourceTransformer


四、作业开发:用户行为分析

4.1 业务场景

假设我们有用户点击流数据(JSON格式)从 Kafka 进来:

json

java 复制代码
{
    "userId": "10001",
    "actionType": "click",
    "pageUrl": "/product/123",
    "productId": "P001",
    "province": "广东",
    "timestamp": 1700000000000
}

需要实时统计:

  • PV(页面浏览量):每分钟的访问次数

  • UV(独立访客):每分钟的去重用户数

  • 漏斗转化:点击 → 加购 → 支付的转化率

4.2 数据模型定义

java

java 复制代码
// 原始日志
public class UserActionLog {
    public String userId;
    public String actionType;  // click, add_cart, pay
    public String productId;
    public Long timestamp;
}

// 统计结果
public class BIStatResult {
    public String statType;    // PV, UV, FUNNEL
    public String dimension;
    public Long count;
    public Long windowStart;
    public Long windowEnd;
}

4.3 核心代码实现

java

java 复制代码
public class UserBehaviorAnalysisJob {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 1. Source: 从 Kafka 读取数据
        Properties kafkaProps = new Properties();
        kafkaProps.setProperty("bootstrap.servers", "kafka:9092");
        kafkaProps.setProperty("group.id", "flink-analysis");
        
        DataStream<UserActionLog> sourceStream = env
            .addSource(new FlinkKafkaConsumer<>(
                "user_action_logs",
                new SimpleStringSchema(),
                kafkaProps
            ))
            .map(json -> parseJson(json))  // 解析 JSON
            .assignTimestampsAndWatermarks(
                WatermarkStrategy.<UserActionLog>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                    .withTimestampAssigner((log, ts) -> log.timestamp)
            );
        
        // 2. 转换: 计算 PV
        DataStream<BIStatResult> pvStream = sourceStream
            .map(log -> Tuple2.of("pv", 1L))
            .keyBy(t -> t.f0)
            .window(TumblingEventTimeWindows.of(Time.minutes(1)))
            .reduce((t1, t2) -> Tuple2.of(t1.f0, t1.f1 + t2.f1))
            .map(t -> new BIStatResult("PV", null, t.f1, getWindowStart(), getWindowEnd()));
        
        // 3. 转换: 计算 UV
        DataStream<BIStatResult> uvStream = sourceStream
            .keyBy(log -> log.userId)
            .window(TumblingEventTimeWindows.of(Time.minutes(1)))
            .process(new ProcessWindowFunction<UserActionLog, BIStatResult, String, TimeWindow>() {
                @Override
                public void process(String key, Context ctx, 
                                   Iterable<UserActionLog> elements,
                                   Collector<BIStatResult> out) {
                    BIStatResult result = new BIStatResult();
                    result.statType = "UV";
                    result.count = 1L;
                    result.windowStart = ctx.window().getStart();
                    result.windowEnd = ctx.window().getEnd();
                    out.collect(result);
                }
            })
            .keyBy(r -> r.windowEnd)
            .windowAll(TumblingEventTimeWindows.of(Time.minutes(1)))
            .sum("count");
        
        // 4. Sink: 写入 MySQL
        pvStream.addSink(createMySQLSink("bi_pv_stats"));
        uvStream.addSink(createMySQLSink("bi_uv_stats"));
        
        env.execute("User Behavior Analysis");
    }
    
    private static SinkFunction<BIStatResult> createMySQLSink(String tableName) {
        return JdbcSink.sink(
            "INSERT INTO " + tableName + " (stat_type, count, window_start, window_end) VALUES (?, ?, ?, ?)",
            (stmt, result) -> {
                stmt.setString(1, result.statType);
                stmt.setLong(2, result.count);
                stmt.setLong(3, result.windowStart);
                stmt.setLong(4, result.windowEnd);
            },
            JdbcExecutionOptions.builder().withBatchSize(1000).build(),
            new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
                .withUrl("jdbc:mysql://mysql:3306/flink_db")
                .withUsername("root")
                .withPassword("root")
                .build()
        );
    }
}

五、作业提交的三种方式

5.1 方式一:Web UI 提交

访问 http://localhost:8081,进入 Submit New Job 页面:

  1. 点击 "Add New" 上传 JAR 包

  2. 填写 Entry Class(主类全限定名)

  3. 点击 Submit

5.2 方式二:docker exec 命令(推荐)

bash

bash 复制代码
# 复制 JAR 到容器
docker cp target/bigdata-flink-1.0.0-SNAPSHOT.jar jobmanager:/opt/flink/usrlib/

# 提交作业
docker exec jobmanager flink run \
  -c com.boonya.flink.UserBehaviorAnalysisJob \
  /opt/flink/usrlib/bigdata-flink-1.0.0-SNAPSHOT.jar

5.3 方式三:一次性提交容器

在 docker-compose.yml 中添加:

yaml

bash 复制代码
job-submitter:
  image: flink:1.18.0-scala_2.12-java11
  depends_on:
    jobmanager:
      condition: service_healthy
  volumes:
    - ./target:/opt/flink/usrlib
  command: >
    bash -c "
      sleep 10 &&
      flink run -d \
        -c com.boonya.flink.UserBehaviorAnalysisJob \
        /opt/flink/usrlib/bigdata-flink-1.0.0-SNAPSHOT.jar
    "

然后执行:

bash

bash 复制代码
docker-compose up job-submitter

六、常见踩坑与解决方案

6.1 JobManager 无法启动(目录权限问题)

错误: Directory is in an inconsistent state: storage directory does not exist or is not accessible

解决:

bash

bash 复制代码
# 手动创建目录并格式化
docker exec -it jobmanager bash
mkdir -p /tmp/hadoop-root/dfs/name
hdfs namenode -format -force
exit
docker-compose restart jobmanager

6.2 Shade 插件报错

错误: Cannot find 'resource' in class ManifestResourceTransformer

原因: 配置了多余的 <resource> 标签

正确配置:

xml

XML 复制代码
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
    <mainClass>com.boonya.flink.YourJob</mainClass>
</transformer>

6.3 Git 推送失败(证书问题)

错误: error setting certificate verify locations

解决(Windows):

bash

bash 复制代码
git config --global http.sslBackend schannel

6.4 Git 推送失败(目录所有权问题)

错误: detected dubious ownership in repository

解决:

bash

bash 复制代码
git config --global --add safe.directory D:/code/bigdata

6.5 作业提交后容器退出

现象: job-submitter 容器运行后就退出

解释: 这是正常行为。一次性任务容器执行完命令就会退出,不影响已提交的 Flink 作业。

如果希望保留容器,可以添加循环:

yaml

bash 复制代码
command: bash -c "flink run ... && tail -f /dev/null"

七、验证与监控

7.1 查看作业状态

bash

bash 复制代码
# 查看 Flink Web UI
http://localhost:8081

# 查看容器日志
docker-compose logs -f jobmanager
docker-compose logs -f taskmanager

# 查看已提交的作业
docker exec jobmanager flink list

7.2 在 MySQL 中查询结果

sql

sql 复制代码
-- 进入 MySQL 容器
docker exec -it mysql mysql -uroot -proot

-- 切换数据库
USE flink_db;

-- 查看 PV 统计
SELECT * FROM bi_pv_stats ORDER BY window_start DESC LIMIT 10;

-- 查看 UV 统计
SELECT * FROM bi_uv_stats ORDER BY window_start DESC LIMIT 10;

八、总结

通过本文,我们完成了:

阶段 内容
环境搭建 Docker Compose 一键启动 Flink + Kafka + MySQL
项目配置 Maven 依赖配置和 Shade 插件打包
作业开发 实时计算 PV/UV,从 Kafka 读取,写入 MySQL
作业提交 Web UI、docker exec、一次性容器三种方式
踩坑记录 目录权限、插件配置、Git 问题等解决方案

核心要点:

  1. Source:从 Kafka 消费数据

  2. Transform:使用 KeyBy + Window 进行聚合计算

  3. Sink:将结果写入 MySQL

  4. 网络 :Docker 容器间通过服务名互通(如 kafka:9092mysql:3306

掌握了这些,你就具备了开发 Flink 实时计算任务的基础能力。接下来可以尝试更复杂的场景:

  • 多流 Join(如订单流和支付流关联)

  • 状态编程(如检测用户异常行为)

  • Flink SQL 简化开发


扩展阅读:

本文适合:大数据工程师 数据仓库 数据湖 实时计算

相关推荐
事变天下2 小时前
数智奔涌,让ICU从“生死门”走向“生命中枢”
大数据
zhojiew2 小时前
使用 Spark Connect 在 Amazon EMR on EC2 上实现远程 Spark开发
大数据·分布式·spark
跨境卫士苏苏2 小时前
欧盟固定收费临近轻小件卖家如何判断继续铺量还是收缩
大数据·人工智能·安全·跨境电商·亚马逊
数据皮皮侠2 小时前
上市公司内源与债权股权融资协同数据(2009-2025)
大数据·人工智能·算法·microsoft·百度
冯RI375II694872 小时前
CPC认证是什么。申请CPC认证流程
大数据
青岛前景互联信息技术有限公司2 小时前
企业专职消防队的数字化升级:物联网和大数据的结合
大数据·物联网
杰建云1672 小时前
多门店商城小程序怎么做
大数据·apache
yongyoudayee3 小时前
AI CRM架构深度解析:销售易NeoAgent 2.0如何打破“AI+套壳“的技术困局
大数据·人工智能·架构
SelectDB3 小时前
时间序列近邻关联性能实测:Doris ASOF JOIN 领先 ClickHouse、DuckDB
大数据·数据库·数据分析