Flink 学习日志 01:前端转大数据的实战避坑指南

本文为前端开发者转型大数据的实战学习日志,系统梳理

  • Flink环境搭建(WSL2+IDEA+Maven)
  • 开发规范(项目结构、Maven管理)
  • 运行模式(本地彩排与集群提交)
  • 网络调试(nc、端口冲突排查)
  • JVM基础(内存模型、GC、类加载)

强调"先clean后package"、避免依赖冲突、理解FatJar与provided范围的重要性,并结合前端思维类比V8与JVM,助力高效跨域进阶。

这份学习日志专为**"前端转大数据"**量身定制,涵盖了从环境踩坑到底层原理的完整心路历程。按知识模块分门别类,适合作为备忘录或博客分享。



一、 环境搭建与版本控制(地基篇)

核心原则:脱离教程环境(Mac),构建统一的本地生态(Win11 + WSL2 + IDEA)。

  • 工具选型 :放弃 Docker(难调式)和 PyFlink(不符教程),选择 WSL2 本地安装。WSL2 的 Ubuntu 提供了与生产环境高度一致的 Linux 体验。

  • 版本对齐 :教程用 Flink 1.15,且后续包含 Hive 3.1.2(仅支持 Java 8),因此果断将 WSL2 默认 Java 切回 1.8,并下载 flink-1.15.4。

  • 环境隔离 :IDEA 是 Windows 应用,但配置其使用的 Maven 和 JDK 为 WSL2 内的版本,避免路径格式冲突(C:\ vs /mnt/)。

(AI总结版)从零搭建 Flink 开发环境:WSL2 + IDEA + Maven 完全指南(跑通一个 word count 应用程序)WSL2 中下载安装 Flink(Web UI)_ai搭建flink学习虚拟环境搭建-CSDN博客

二、 IDEA 开发实操与 Maven 管理

核心认知:IDEA 是强工程化工具,不是简单的文件夹打开器。

  • 层级关系 :必须理清 项目(Project) > 模块(Module) > 包(Package) > 类(Class)。写代码必须在 src/main/java 下建"Package",放配置在 resources 下建"Directory"。

  • Maven 依赖 :添加 flink-runtime-web 依赖后,必须点击 "重新加载所有 Maven 项目"(红色循环箭头) 强制同步。

  • 打包防坑 :执行 package 前,务必开启 "跳过测试模式" (闪电图标),并先执行 clean,确保打出纯净的 Fat Jar。

Fat Jar(胖包 / Uber Jar) ,就是把你自己写的代码 + 你依赖的所有第三方 jar 包(比如各种 connector、工具类) ,全部解压并合并塞进一个超级大的 jar 包里。


🍔 前端思维类比

  • 普通 Jar 包 :就像你只写了业务代码(几十 KB),没有打包 node_modules。扔到服务器上,一运行就报 Module not found。

  • Fat Jar(胖包) :就像你用 Webpack/Vite 跑了一遍 npm run build,把业务代码和所有依赖的库全部打包进了 dist 文件夹(几十 MB)。扔到任何地方,解压就能直接跑,不需要额外装环境。

⚖️ 普通 Jar vs Fat Jar

维度 普通 Jar 包 Fat Jar(胖包)
内容 只有你写的 .class 和配置文件 你的代码 + 所有第三方依赖包(如 Flink 核心包、Kafka 客户端等)
体积 极小(几十 KB) 很大(几十 MB 到上百 MB)
运行环境 必须配合完整的类路径(-cp)或部署在完整的框架里 随时随地 java -jar 独立运行

🔍 怎么看打出来的包是不是 Fat Jar?(3种方法)

方法一:看体积(最直观)

去 IDEA 的 target 目录下看打出来的 jar 包。如果只有几 KB 或十几 KB,那是普通包;如果是几十 MB 甚至更大,那就是 Fat Jar。


方法二:用解压软件打开看

Jar 包本质就是个 ZIP 压缩包。你可以用 Windows 的 7-Zip 或 Bandizip 打开它:

  • 如果里面只有 com/pk/flink/...(你写的代码),那就是普通包。

  • 如果里面除了你的代码,还有一大堆 org/apache/flink/...、com/google/...、org/slf4j/... 等文件夹,那它就是 Fat Jar。

方法三:看 META-INF/MANIFEST.MF 文件(最核心)

解压 jar 包,找到 META-INF/MANIFEST.MF 文件,用记事本打开:

  • 如果里面有 Main-Class: com.pk.flink.basic.FlinkSocketWCApp 这一行,说明它不仅是个 Fat Jar,还指定了入口类。这种包提交给 Flink 时,可以省略 -c 参数。

  • 如果没有这一行,说明没配置主类,提交时必须手动写 -c com.pk.flink.basic...。


💡 Flink 中的避坑大法

在 Flink 开发中,打 Fat Jar 通常使用 maven-shade-plugin。


特别注意 :Flink 自己的核心依赖(如 flink-streaming-java、flink-clients)在你的 pom.xml 里必须加 <scope>provided</scope>。


这意味着:"打包的时候别把 Flink 的核心包打进去,集群上已经有了。"如果不加,你的 Fat Jar 会和集群自带的 Flink 发生严重冲突,导致启动失败。
在哪个 pom.xml 里加 <scope>provided</scope>,项目根目录的 pom.xml,还是模块的 pom.xml?


加在你实际写 Flink 业务代码的那个子模块的 pom.xml 里 (比如你之前截图的 pk-flink-basic/pom.xml)。


💡 为什么加在子模块,而不是根目录?

  • 子模块的 pom.xml :定义了当前模块具体需要什么依赖 。因为你是在 pk-flink-basic 里写了 Flink 代码,所以这个模块在编译时需要 Flink 的 API。

  • 根目录的 pom.xml(父工程) :通常只负责统一管理版本号 (比如定义 <flink.version>1.15.4</flink.version>),它自己不写业务代码,所以不需要引入具体的依赖。


⚠️ 为什么必须加 <scope>provided</scope>?(前端思维类比)

  • Flink 集群:就像是已经装好了 Node.js 运行环境(包含 Flink 核心包)的服务器。

  • 你的代码:就像是你的业务代码。

  • 不加 provided(默认是 compile) :你打包时会把 Flink 核心包也塞进你的 Fat Jar 里。提交到集群后,你的 Jar 包里的 Flink 类和集群自带的 Flink 类同名打架 ,直接报 ClassCastException 或 NoSuchMethodError。

  • 加了 <scope>provided :相当于告诉 Maven:"编译代码的时候需要用到它,但打包的时候千万别把它带上,因为集群上已经有了。"


🛠️ 具体怎么改?(推荐的企业级标准做法)

第一步:在父工程(根目录的 pom.xml)里统一管理

在根目录的 <dependencyManagement> 中定义好版本和 scope。这样所有子模块都不用重复写版本号了:

xml

XML 复制代码
<!-- 根目录的 pom.xml -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope> <!-- 在这里统一定义 -->
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

第二步:在子模块(pk-flink-basic/pom.xml)里引入依赖

在子模块里引入时,不需要写版本号和 scope,它会自动继承父工程的配置:

xml

XML 复制代码
<!-- pk-flink-basic/pom.xml -->
<dependencies>
    <!-- 不需要写 version,也不写 scope,自动继承父工程 -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-clients</artifactId>
    </dependency>

    <!-- 注意:你之前加的 flink-runtime-web 通常不用加 provided -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-runtime-web</artifactId>
        <version>${flink.version}</version>
    </dependency>
</dependencies>

🚨 特例:什么时候不 加 provided?

你代码里用来做本地测试的 WebUI 依赖(flink-runtime-web),通常不加 provided 。因为有些集群默认没带这个 UI 模块,你需要把它打进包里。但核心的 flink-clients、flink-streaming-java 等,一定要加 provided。

如果不开启"跳过测试模式",直接执行打包(比如双击 package 或 install),大概率会经历一次"打包失败"的折磨,但这取决于你的项目里有没有测试代码。


Maven 的设计非常健壮,它允许你一天打包 100 次。但在实际效果上,取决于你加没加 clean 命令,会有两种不同的情况:

情况一:不加 clean,直接重复打包(mvn package)

情况二:加上 clean 再打包(mvn clean package)------ 👑 标准做法


既然你在学 Flink,经常需要打包 jar 提交到集群。为了避免遇到灵异 Bug:

  1. 每次打包养成顺手点一下 clean 的习惯 。在 IDEA 的 Maven 面板里,先双击 clean,再双击 package。或者直接在终端里敲 mvn clean package -DskipTests。

  2. 看准 target 目录 :打包成功后,所有的产物(包括最终 fat-jar)都在 target 目录下。如果你觉得打出来的包有问题,直接右键把 target 文件夹删掉,重新打一次,能解决 80% 的玄学报错。

总结 :随意重复打包,电脑扛得住。但为了代码的纯净,"先 clean,后 package" 是每个 Java 开发者的肌肉记忆。


当你双击 package 后,注意看 IDEA 底部弹出的 Build 窗口 ,如果最后显示绿色的 BUILD SUCCESS,就说明打包成功了。


打出来的 jar 包通常在这里:

pk-flink-basic 目录下的 target 文件夹里(名字大概叫 pk-flink-basic-1.0.jar)。你后续提交到 Flink 集群的,就是这个东西。
Flink 学习需要具备的 Java 基础知识总结-CSDN博客


Java 3 天入门:"最小必要知识"的功利性学法-CSDN博客


IntelliJ IDEA 2026.1.3 极简入门指南(新建项目、新建模块、新建包、新建类):以构建 Flink 应用程序(IDEA+Maven、Flink官方提供的命令)为例-CSDN博客


IntelliJ IDEA 补充篇:环境与终端避坑指南:Win11 + WSL2(nc -lk 9527)+ IDEA,禁用Kotlin插件解决无法复制形参-CSDN博客
在 IDEA 本地运行环境中开启 Flink WebUI


只需三步:

1. 引入 WebUI 依赖(pom.xml)

必须引入运行时 Web 模块,否则启动会报错 NoClassDefFoundError:

xml

XML 复制代码
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-runtime-web</artifactId>
    <version>1.15.4</version>
</dependency>

2. 修改代码指定端口

不要用默认的 getExecutionEnvironment(),改用以下代码,并设定一个不冲突的端口(如 8082):

java

java 复制代码
import org.apache.flink.configuration.Configuration;

Configuration conf = new Configuration();
conf.setInteger("rest.port", 8082); // 避免与本地已启动的 8081 集群冲突
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(conf);

3. 运行并访问

运行程序后,在浏览器访问 http://localhost:8082 即可看到本地 MiniCluster 的 WebUI 界面。注意:程序停止后,UI 就无法访问了。

在 IDEA 里运行是"本地彩排",提交到集群是"正式演出"。


你用前端思维来理解:

  • IDEA 里运行(Run) :就像是你在本地用 npm run dev 起了一个本地服务,只要你在 IDEA 里点了红色方块(Stop),程序就彻底死掉了。

  • 提交到 Flink 集群(Submit) :就像是前端项目打包后,推送到正式的云服务器(比如 K8s 或 Docker)上跑。一旦提交成功,你就算把 IDEA 关掉,把电脑合上,只要集群没断电,这个 Flink 程序就会 24 小时一直跑着。

下面我们详细拆解一下这背后的区别:

1. 到底有什么区别?

维度 在 IDEA 中运行 / 取消 提交 / 取消 Flink 应用
运行位置 直接在你的 WSL2 内存里跑(使用的是本地的 MiniCluster)。 提交给独立的 JobManager 和 TaskManager 集群去跑。
生命周期 绑死在 IDEA 上。点停止,或者关掉 IDEA,程序就终止。 独立运行。提交后,哪怕你关掉 IDEA、甚至关掉电脑,集群里的程序依然在运行。
资源分配 只能使用你电脑当前的内存和 CPU。 由集群统一调度,可以使用几十台服务器的资源。
适用场景 本地开发、写代码、断点调试。 生产环境、服务器上真实的定时任务或流式任务。

2. 为什么要有"提交"这个概念?

你在 IDEA 里写完代码,点"运行"成功了,那只是测试通过了。真实的企业场景中:

  1. 你需要把代码打成 .jar 包(用我们之前说的 Maven package)。

  2. 把包提交给真正的集群,让成百上千个 TaskManager 去分布式的读取和处理数据。

3. 为什么要"取消"?

因为一旦提交到集群,程序就是 7x24 小时无休止运行的。如果你发现业务逻辑写错了,或者不需要跑了,如果不"取消",它就会一直占用集群的 CPU 和内存资源 ,把公司的服务器资源吃光。

所以,取消作业就是告诉 JobManager:"把这个任务的资源释放掉,别跑了。"

4. 什么时候需要用到?

  • 在 IDEA 里:写代码、改 Bug、跑个测试数据看看效果。

  • 在集群里(WebUI/命令行):把写好的完美代码打包扔上去,每天看它自动处理数据。如果你在集群的 WebUI 里发现数据算错了,就去点 Cancel 取消掉它。

总结 :你之前一直用 nc + IDEA 运行 来测试,是因为你还在开发阶段。跟着老师学到第 3 章,就是在教你怎么把写好的程序,脱离 IDEA,独立运行在真正的 Flink 集群里。

核心认知:IDEA 运行是"本地彩排",集群提交是"正式演出"。

  • 本地运行(IDEA) :使用 createLocalEnvironmentWithWebUI 启动 MiniCluster,指定 8082 端口(避开原集群 8081),随点随停,适合 Debug。

  • 集群提交(WebUI/命令行):

    • 命令行:flink run -d -c <主类> <jar包路径>(-d 为后台分离模式)。

    • WebUI:在 8081 端口上传 Jar 包,填入 Main Class 提交。

  • 作业取消 :用 flink cancel <JobID> 或 WebUI 点击 Cancel,释放集群资源。Jar 包不需要提前传到 HDFS,Flink 客户端会自动分发。

基于你之前写的代码(FlinkSocketWCApp),在 WebUI 上提交作业时,最关键的是要告诉 Flink 程序的入口在哪 ,以及需要什么参数。


按照下面的指南填写即可:

📝 核心填写指南

1. Entry Class(入口类) ------ 必填 🔴
  • 填什么 :com.pk.flink.basic.FlinkSocketWCApp

  • 说明 :告诉 Flink 从哪个类的 main 方法开始执行。必须写全限定名 (包名+类名)。如果不填,Flink 会去读 jar 包里的 META-INF/MANIFEST.MF,如果你打包时没配置主类,就会报错 No Main-Class found。

2. Program Arguments(程序参数) ------ 选填 🟡
  • 填什么 :--host localhost --port 9527

  • 说明 :这对应你代码里的 ParameterTool。

    • 如果你填了,Flink 会把这两个参数传给你的程序。

    • 如果你不填 ,你的代码里写了默认值,它也会自动用 localhost 和 9527,一样能跑通。建议填上,练习参数传递。

3. Parallelism(并行度) ------ 选填 🟡
  • 填什么 :1

  • 说明:因为你的本地集群默认只有 1 个 Task Slot(可以看左侧 Task Managers 里 Available Slots 是 1)。如果不填,默认就是 1;如果填了大于 1 的数字,会报资源不足的错误。

4. Savepoint Path(保存点路径) ------ 留空 ⚪
  • 说明:这是容错恢复用的。你第一次跑这个作业,没有历史状态,留空即可。
5. Allow Non Restored State ------ 不勾选 ⚪
  • 说明:新手不用管,留空。

⚠️ 提交前极其重要的前置动作

不要直接点 Submit!

  1. 打开你的 WSL2 终端。

  2. 确保你已经执行了 nc -lk 9527 并且光标在等待输入。

🚀 提交与验证

  1. 填好 Entry Class 和 Program Arguments 后,点击 Submit。

  2. 页面会自动跳到 Running Jobs 。你会看到一个状态为 RUNNING 的作业,名字叫 FlinkSocketWCApp--作业名字。

  3. 怎么查看输出? 因为是通过集群跑,IDEA 控制台不会有输出。你需要点击左边栏的 Task Managers -> 点击里面唯一的那个 TaskManager -> 点击 Stdout 标签页。

  4. 回到 WSL2 终端,输入 pk,pk,pk,你就能在 WebUI 的 Stdout 页面看到实时的词频统计输出了!

提交/取消自己开发的Flink应用程序,WebUI 和命令行两种方式


这里为你简略总结 Flink 应用程序的提交与取消方式(前提:已通过 Maven 打包生成 .jar 文件,假设集群 WebUI 端口为 8081)。


🚀 提交应用程序

1. 命令行方式

在 WSL2 终端执行 flink run 命令:

bash

复制代码
# 基础提交(前台运行,Ctrl+C 会取消作业)
flink run -c com.pk.flink.basic.FlinkSocketWCApp /path/to/pk-flink-basic-1.0.jar

# 分离模式提交(后台运行,最常用)
flink run -d -c com.pk.flink.basic.FlinkSocketWCApp /path/to/pk-flink-basic-1.0.jar

注:-c 指定主类全限定名;如果打的是 Fat Jar 且配置了 Main-Class,可省略 -c。


2. WebUI 方式

  1. 浏览器打开 http://localhost:8081。

  2. 点击左侧 Submit New Job -> 点击右上角 + Add New 上传 jar 包。

  3. 选中上传的 jar 包,在 Entry Class 填入主类全限定名。

  4. 在 Program Arguments 填入所需参数(如 --host localhost --port 9527)。

  5. 点击 Submit 提交。


🛑 取消应用程序

1. 命令行方式

需要先知道作业的 JobID(可通过 flink list 查看):

bash

复制代码
flink cancel <JobID>

2. WebUI 方式

  1. 浏览器打开 http://localhost:8081。

  2. 点击左侧 Running Jobs。

  3. 找到正在运行的作业,点击右侧的 Cancel 按钮。

Copy Reference(复制引用)是 IntelliJ IDEA 中一个非常实用的功能,用于快速复制代码元素(如类、方法、字段)的完全限定名(Fully Qualified Name)。


🎯 功能与效果

将光标放在类、方法或字段上执行此操作,它会将完整的引用路径复制到剪贴板:

  • Java 类 :com.pk.flink.basic.FlinkSocketWCApp(包名.类名)

  • Java 方法 :com.pk.flink.basic.FlinkSocketWCApp#main(java.lang.String[])(全类名#方法签名)

  • 文件行 :如果光标在非代码元素的行上,可能会复制出类似 路径/文件名.java:行号 的格式。

在 IDEA 顶部菜单栏点击 Edit ,在下拉菜单中直接选择 Copy Reference。
Flink 代码跑在 IDEA 里,尝试连接 Kafka?Kafka 装在 WSL2 中,怎么连接?-CSDN博客

四、 Linux 基础与网络测试

  • WSL2 重启大法 :遇到端口占用、网络不通或卡死,在 Windows PowerShell 执行 wsl --shutdown。

  • Netcat 神器 :nc -lk 9527 用于模拟 Socket 数据源。必须先启动监听,再运行 Flink,最后输入数据,否则数据会丢失(发给空气)。

  • 端口冲突排查 :使用 jps 查看残留 Java 进程,用 kill -9 <PID> 强杀。

Caused by: java.net.BindException: Could not start rest endpoint on any port in port range 8081


核心原因:端口冲突(8081 被占用了)。


🛠️ 解决步骤(按顺序执行)


第一步:杀掉所有残留的 Flink 进程

打开 WSL2 终端,把之前残留的进程全部干掉:

bash

复制代码
# 查看当前所有 Java 进程
jps

# 如果有 FlinkSocketWCApp、StandaloneSessionClusterEntrypoint、TaskManagerRunner
# 把它们对应的 PID 全部杀掉(比如你之前的 827359,或者其他看到的数字)
kill -9 <PID1> <PID2>

(如果嫌麻烦,可以直接在 Windows PowerShell 里执行 wsl --shutdown,然后重新打开终端,这招能瞬间清空所有后台进程。)


第二步:强制 IDEA 重新编译

回到 IDEA,点击顶部菜单栏:

Build -> Rebuild Project (重新构建项目)。
这一步绝对不能省,确保你改了 8082 的代码真正生效了。


第三步:换个"绝对冷门"的端口

为了防止再次被占用,建议直接把代码里的端口改大一点,比如 8888:

java

复制代码
Configuration configuration = new Configuration();
configuration.setInteger("rest.port", 8888); 
StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(configuration);

第四步:重新运行

再次在 IDEA 点击运行。如果看到 Starting rest endpoint. 并且没有报错,就成功了。浏览器访问 http://localhost:8888 就能看到 WebUI。


💡 验证到底是谁占用了端口(进阶排查)

如果你想知道 8081 到底被谁占用了,可以在 WSL2 终端敲:

bash

bash 复制代码
sudo lsof -i :8081

或者

bash

bash 复制代码
ss -tlnp | grep 8081

你会看到具体的进程名和 PID,找到后 kill -9 <PID> 即可。


总结 :这种 BindException 是大数据开发最常见的错误之一。养成一个好习惯:每次在 IDEA 里跑带 WebUI 的代码之前,先确保之前跑的程序已经点红色方块彻底停掉了。


如果遇到关不掉的,直接在终端 kill -9。

五、 Java 底层基础补充(进阶篇)

JVM(Java 虚拟机)


🎯 为什么 JVM 是学 Flink 的"重要基础"

Flink 的 JobManager 和 TaskManager,本质上都是 Java 进程,运行在 JVM 之上。

这意味着,你在 Flink 里做的很多事,最后都会落到 JVM 上:

  • 内存配置:你给 TaskManager 设置的内存大小,最终会转化为 JVM 的堆内存(Heap)和堆外内存(Off-Heap)。

  • 性能调优 :Flink 作业卡顿,很多时候需要调整 JVM 的垃圾回收(GC) 策略,或者排查内存泄漏。

  • 本地调试 :你现在在 IDEA 里直接 Run 的 Flink 程序,就是跑在一个本地 JVM 里的"迷你集群"。

如果不懂 JVM,遇到 OutOfMemoryError 或者 GC 导致的延迟飙升时,就很难定位问题。


📚 给你的学习路径建议

作为前端出身,你不需要像科班 Java 开发那样一上来就啃透 JVM 底层。跟着 Flink 课程,按需了解这几个模块就够了:

  1. 内存模型 :搞清楚堆(Heap)和栈(Stack)的区别,以及 Flink 的堆外内存(Direct Memory)为什么重要。

  2. 垃圾回收(GC):知道什么是 Minor GC / Full GC,以及为什么 Full GC 会导致 Flink 作业"卡住"甚至超时。

  3. 类加载机制 :了解 Flink 的 UserCodeClassLoader 是怎么隔离你的代码和 Flink 框架代码的(这对解决 ClassNotFoundException 很关键)。


建议你先从理解 JVM 内存模型 和 GC 基础开始,能把这些概念和你正在写的 Flink 程序对应起来,就达到老师说的"基础"要求了。

核心认知:Flink 底层跑在 JVM 上,理解内存是调优的前提。

Jar 包本质上就是一个 ZIP 压缩包 ,后缀名改成了 .jar(Java ARchive)。里面装的是你写的代码编译后的字节码(.class)、配置文件、以及可能包含的第三方依赖。


📦 Jar 包里到底装了什么?

你写的 .java 文件是给人看的,计算机不认识。Java 虚拟机(JVM)只认识 .class 字节码文件。

当你点击 Maven 的 package 打包时,Maven 会做这几件事:

  1. 把 src/main/java 下的所有 .java 代码编译 成 .class 文件。

  2. 把 src/main/resources 下的配置文件(如 log4j2.properties)拷贝过来。

  3. 把这一大堆东西,按照原本的目录结构(比如 com/pk/flink/basic/FlinkSocketWCApp.class),塞进一个压缩包里,命名为 pk-flink-basic-1.0.jar。


❓ 为什么必须打包?(为什么不直接跑代码?)

这就好比你问:"我前端写完代码,为什么不能直接把整个项目文件夹发给客户,非要跑一遍 npm run build 打成一个 dist 包?"


原因有三个:

1. 脱离开发环境运行

你在 IDEA 里能跑代码,是因为 IDEA 帮你做了编译 、找依赖 、配置启动参数 等脏活累活。

真实的企业集群(比如 Flink 的 TaskManager 节点)上,根本没有 IDEA,也没有你的源代码,只有一个纯纯的 Java 运行环境(JRE)。你只能把打包好的 Jar 扔给它,它才能直接运行。


2. 解决依赖地狱(这就是前端最熟悉的 node_modules)

你的 Flink 代码可能用到了 Jackson 解析 JSON,用到了 Log4j 打日志。打包时(特别是打 Fat Jar / Uber Jar ),Maven 会把所有你依赖的第三方 Jar 包,全部分解并合并到你自己的 Jar 包里 。

这样你提交作业时,只需要上传这一个大文件,集群就能正常跑了。


3. 统一交付标准

Jar 包是 Java 世界绝对的标准。无论你是用 Mac、Win11 还是 Linux,打出来的 Jar 包都一样。你可以把它当做一个可执行的工具 ,通过命令行随时运行(java -jar xxx.jar)。


🚀 结合 Flink 的实战场景

当你学完后面几章,准备把代码提交到真实的集群时:

  1. 你在 WSL2 里执行 mvn clean package,在 target 目录下生成了一个 pk-flink-basic-1.0.jar。

  2. 你在 Flink WebUI 上点击"上传",选中这个 Jar 包。

  3. 集群会下载这个 Jar 包,解压,找到里面的 FlinkSocketWCApp.class 运行。


💡 一个非常关键的避坑点:Fat Jar 与 provided

前端里,像 React、Vue 这种库,通常打包时会被抽离(不打进 dist 里),由 CDN 或者宿主机提供。


在 Java 里,Flink 自己的核心包(flink-streaming-java 等)你在 pom.xml 里必须加上 <scope>provided</scope>。


为什么? 因为如果你把 Flink 框架本身的类也打进了你的 Jar 包,提交到集群时,你的 Jar 包和集群自带的 Flink 类就会发生严重冲突,直接报 ClassNotFoundException 或 NoSuchMethodError。provided 的意思就是:"打包时别带上我,集群里有"。


总结 :Jar 包就是你的 Java 代码打完包后的"最终产物"。等你以后打包遇到红字 BUILD FAILURE 时,记得回来看这篇。

  • Jar 包本质 :就是 ZIP 压缩包(类似前端的 dist),包含编译后的 .class 和依赖。打包时需注意 provided 范围,避免与集群框架冲突。

  • JVM 内存模型:

    • 堆(Heap) :存 new 出来的对象(类似 V8 的引用类型)。

    • 栈(Stack):存方法局部变量(类似 JS 调用栈)。

    • 堆外内存(Direct Memory):🔥 Flink 重度使用,绕过 GC 提升性能。

  • STW 隐患:垃圾回收时的 Stop-The-World 会导致线程停顿,若超时会引起 Flink 心跳失败。G1 是 Flink 推荐的回收器。

V8 是 Google 开发的、用于执行 JavaScript 代码的高性能引擎。


简单来说,它就是 JavaScript 的"发动机":

  • 跑在哪里:Chrome 浏览器和 Node.js 底层用的都是它。

  • 干了什么:你写的 JS 代码计算机看不懂,V8 负责把它编译成机器能直接执行的机器码,并自动管理内存(也就是你在前端排查过的内存泄漏和垃圾回收)。

呼应前文 :前端代码由 V8 引擎 负责跑;Java/Flink 代码由 JVM(Java 虚拟机) 负责跑。两者都是"代码翻译官 + 内存大管家"。

🗺️ JVM 最小知识图谱


分支一:内存结构(东西存在哪?)
  • 堆 (Heap) :存放所有 new 出来的对象(如 Tuple2、字符串)。受 -Xmx 限制 。如果这里满了,报 OutOfMemoryError: Java heap space。

  • 栈 (Stack):存放方法的局部变量和调用记录。每个线程独立(Flink 的每个 Task 都在一个线程里跑)。

  • 元空间 (Metaspace):存放类的定义、方法代码。

  • 🌟 堆外内存 (Direct Memory) :Flink 最核心的优化点 。Flink 大量使用堆外内存做网络传输和状态存储,绕过 JVM 的垃圾回收,避免卡顿。如果这里满了,报 OutOfMemoryError: Direct buffer memory。

分支二:垃圾回收 GC(谁来打扫卫生?)
  • 分代回收:对象先放"新生代",活久了移到"老年代"。

  • 🌟 STW (Stop-The-World) :垃圾回收时,所有工作线程都要暂停。如果 STW 时间过长,Flink 任务会延迟,甚至导致 TaskManager 心跳超时被踢出集群。

  • G1 回收器:Flink 官方推荐的垃圾回收器,特点是 STW 时间可控。

分支三:类加载机制(代码怎么跑起来的?)
  • 双亲委派:Java 默认的类加载规则(先找爹加载,爹没有才自己加载)。

  • 🌟 Flink 的类加载隔离 (UserCodeClassLoader) :因为一个集群跑多个人的代码,Flink 打破了双亲委派,实现了隔离。如果你打包时依赖冲突(比如你引入了不同版本的 Kafka 客户端),就会在这里报 ClassNotFoundException。

分支四:核心调优参数(怎么调大小?)
  • -Xms 和 -Xmx:初始和最大堆内存(避免频繁扩容/缩容)。

  • -XX:+UseG1GC:启用 G1 回收器。

  • -XX:MaxDirectMemorySize:限制堆外内存大小。


💡 学习减负指南

你不需要了解:

  • JVM 字节码指令集(如 iload_0, astore_1)。

  • 垃圾回收底层的三色标记法、卡表(Card Table)。

  • 如何用 C++ 写一个 JVM。

你在 Flink 开发中会遇到的真实场景:

  1. Flink 作业卡顿:去 WebUI 看 GC 日志,发现是频繁 Full GC 导致的 STW。

  2. 打包冲突 :作业提交报错 ClassNotFoundException,知道是类加载隔离导致的依赖没打进去。

  3. 内存溢出 :看到 OOM,能分清是堆内存(Java heap space)不够,还是堆外内存(Direct buffer)漏了。


把这个图谱存下来,下次老师讲到 JVM 的时候,你脑海中直接映射到这几块,就不会觉得抽象了。

🗺️ 进阶版 JVM 与 Flink 知识图谱


模块一:运行时数据区(内存到底长啥样?)

你不仅要背出分区,还要知道它们在 Flink 里的特殊表现。

  1. 堆(Heap):

    • 结构:新生代(Eden + Survivor S0/S1)、老年代。

    • Flink 关联 :你代码里 new 的对象都在这里。注意,Flink 的 State(状态)不一定在堆里!如果使用 RocksDB 作为 State Backend,状态是存在堆外(甚至磁盘)的,只有读出来处理的那一瞬间才短暂进入堆内存。

  2. 虚拟机栈(VM Stack):

    • 结构:栈帧(局部变量表、操作数栈、动态链接、方法返回地址)。

    • Flink 关联 :Flink 的每个并行子任务(SubTask)都是一个独立的线程。你的业务代码是在某个线程的栈里执行的 。如果发生递归调用或嵌套过深,会抛 StackOverflowError。

  3. 方法区 / 元空间(Metaspace):

    • 结构:在 Java 8 之后,移到了本地内存(元空间),存放类元数据、常量池。

    • Flink 关联 :Flink 采用 UserCodeClassLoader 加载你的代码。如果你频繁提交带大量自定义类的作业,或者热更新作业,元空间会不断膨胀,导致 Metaspace OOM。

  4. 🔥 直接内存(Direct Memory / Off-Heap):

    • 前端类比 :Node.js 的 Buffer 或浏览器的 ArrayBuffer,不归 V8 管,不参与 V8 的 GC。

    • Flink 关联(极重要) :Flink 在网络传输(Netty Buffer)和管理状态时,为了避免 JVM 堆内对象过多导致 GC 停顿,重度使用了 Direct Memory 。如果分配超限,报错:java.lang.OutOfMemoryError: Direct buffer memory。


模块二:垃圾回收(GC:如何优雅地打扫战场?)

大数据场景下,内存通常很大(几十G),GC 策略极其重要。

  1. 对象生死判定:可达性分析算法(从 GC Roots 往下找,找不到的就可以回收)。

  2. 回收算法:

    • 复制算法:新生代使用。速度快,但浪费空间。

    • 标记-清除/整理:老年代使用。会产生内存碎片或导致移动对象耗时。

  3. STW(Stop-The-World):

    • 原理:无论哪种 GC,在某些阶段都要求所有用户线程暂停。

    • 🔥 Flink 关联:如果 STW 超过 10 秒,TaskManager 可能无法向 JobManager 发送心跳,导致 JM 认为 TM 挂了,从而触发 Failover(作业重启)。

  4. 垃圾收集器(重点懂 G1):

    • CMS:老一代回收器,已被淘汰。

    • G1(Garbage-First) :将堆划分为多个 Region,支持设置预期停顿时间(-XX:MaxGCPauseMillis)。Flink 官方推荐使用 G1,平衡吞吐量和延迟。

    • ZGC(进阶):JDK 11+ 引入的超低延迟回收器(停顿在 1ms 内),超大数据量场景(如几百G内存)下,Flink 生产环境越来越多地转向 ZGC。


模块三:类加载机制(代码怎么变成可执行的字节码?)

懂这个,你就能解决所有 ClassNotFoundException 和 NoSuchMethodError。

  1. 双亲委派模型:

    • Bootstrap ClassLoader -> Extension ClassLoader -> App ClassLoader。目的是防止你写个 java.lang.String 把系统底层的覆盖了。
  2. 🔥 Flink 打破双亲委派:

    • 痛点:Flink 集群上可能同时跑着 A 团队(用 Kafka 2.0)和 B 团队(用 Kafka 3.0)的作业。如果默认全由 AppClassLoader 加载,就会冲突。

    • 解决 :Flink 设计了 FlinkUserCodeClassLoader 。它优先加载用户提交的 jar 包,最后才委派给系统加载器。实现了作业之间的依赖隔离。


模块四:JVM 调优参数与排错工具

你在大数据平台上看别人调优,调的就是这些参数。

  1. 常见参数:

    • -Xms / -Xmx:初始/最大堆内存(通常设成一样大,避免动态扩容引起抖动)。

    • -XX:MaxMetaspaceSize:限制元空间大小。

    • -XX:MaxDirectMemorySize:限制堆外内存大小。

    • -XX:+HeapDumpOnOutOfMemoryError:发生 OOM 时自动 Dump 出内存快照(救命神药)。

  2. 常用命令行工具:

    • jps:看当前有哪些 Java 进程(你之前用过)。

    • jstat -gc <PID> 1000:每秒钟打印一次 GC 状况(看 Young GC / Full GC 频率)。

    • jmap -histo:live <PID> | head -20:查看堆内存中哪个类的对象最多。

  3. 可视化分析工具:

    • JProfiler / MAT (Memory Analyzer Tool) :用来分析 HeapDump 文件,找出内存泄漏的元凶。

模块五:🌟 Flink 的统一内存模型(串联实战)

Flink 1.10 之后,引入了任务级内存管理。这就是 JVM 内存在 Flink 中的真实划分:

  • JVM Overhead:JVM 自身运行(线程栈、代码缓存、GC)所需内存。

  • JVM Metaspace:类加载的元数据。

  • Flink Total Memory:

    • Managed Memory(托管内存) :由 Flink 接管,完全在堆外。用于批处理算子(Sort、Hash Join)和 RocksDB。它不受 JVM GC 管理,用完直接释放,效率极高。

    • Network Memory(网络内存):用于数据传输缓冲区(如上游发给下游)。

    • Task Heap(任务堆内存) :留给你的代码 new 对象的。如果这里爆了,就是典型的用户代码 OOM。

前端转 Java 的优势:你懂事件循环、懂闭包、懂内存泄漏怎么用 Chrome DevTools 排查。把 V8 的思维平移过来,JVM 就是另一个引擎,只是它的垃圾回收更复杂、堆外内存操作更频繁而已。

💡 学习感悟 :

前端讲"所见即所得",后端讲"约定优于配置"。理清环境边界(哪台机器在跑)、搞对操作顺序(谁先连谁后发)、认准 Maven 的强管控,就能跨过大数据入门的门槛。

相关推荐
Thomas.Sir1 小时前
第1课:SpringBoot 4.x 快速上手:新特性、环境准备与第一个HelloWorld程序
java·spring boot
devilnumber2 小时前
Java 内部迭代 vs 外部迭代:从原理到实战避坑
java·开发语言
杨运交2 小时前
080-缓存模块-Spring Cache缓存键前缀设计与租户隔离实践
java·spring·缓存
\光辉岁月/2 小时前
5.math类
java·开发语言
文丑颜不良啊2 小时前
LeetCode76~100题目解析
java·leetcode
她的男孩2 小时前
页面已发布用户仍进不去:Forge Admin 门户入口与系统菜单挂载解耦
java·spring boot·低代码
不像程序员的程序媛2 小时前
mybatisplus insert dto未指定id时会如何
java·开发语言
我命由我123452 小时前
5-2.Python 编码进阶 - 字符串进阶(拼接、普通格式化、普通格式化数字精度控制、快速格式化、遍历、常用方法)
java·开发语言·后端·python·学习·java-ee·学习方法
Pniubi10 小时前
HashMap的扩容和树化边界
java·开发语言