news 2026/10/16 13:23:31

Flink入门第一课:从流处理概念到本地运行第一个作业

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flink入门第一课:从流处理概念到本地运行第一个作业

这阵子后台私信里,问Flink相关问题的频率明显高了一截。不少朋友是从Spark转过来,或者在实时数仓项目里被点名要用Flink,但一翻官方文档又觉得概念满天飞:DataStream、窗口、水位线、状态后端、检查点……每个词都认识,连在一起就不知道在说什么。所以我决定开一个系列,按自己从零上手到在生产环境填坑的真实路线,把Flink掰开揉碎了讲清楚。这是第一章,先解决“Flink是什么”和“本地怎么跑起来”这两件事,适合完全没接触过流处理的新手,也适合已经写过几个作业但还停留在“照着模板改”阶段的同学。本章不会堆砌概念,所有原理都用生活化的类比拆开,每一步操作都能跟着落地。

1. 先说清楚:Flink到底解决了什么问题

1.1 从批处理到流处理:数据不是静止的

大多数人接触大数据的第一站是批量计算。每天凌晨跑一次任务,把昨天的日志统计一遍出报表,这是批处理的经典场景。但如果业务对数据时效的要求变成“秒级”甚至“毫秒级”,批处理就有点跟不上了。举几个很典型的场景:电商大促时的实时大屏,每秒钟都在刷新支付金额;风控系统要在一笔交易发生的一瞬间判断它是不是风险操作;监控告警等不及T+1,异常在几秒内就得被感知到。这些场景的共同特点是,数据像流水一样源源不断涌进来,永远没有“今天的数据完了”这个说法,只有“到目前为止的数据”。

流处理框架就是为这种场景设计的。它不再把数据先攒起来再统一计算,而是来一条处理一条,计算结果持续更新。Flink能在这类框架里脱颖而出,核心原因是它在做到低延迟的同时,还保住了吞吐量和数据一致性。很多人以为Flink只是“快”,其实真正值钱的不是快,而是“又快又准”。

1.2 Flink的核心能力:不只是“快”这么简单

Flink最常被提起的几个标签,我用自己的理解重新翻译一遍。

事件驱动。数据一来就触发计算,整个链路是持续的,不是“跑完就结束”。这和传统批处理“拉数据→算数据→写结果→退出”的程序生命周期有本质区别。Flink作业一旦启动,正常情况下会一直运行,直到你手动取消或发生致命错误。

状态化计算。流处理里的很多运算不是孤立的。比如你想统计每个用户的累计消费金额,那就要记住每个用户之前的数据。这个“记住”的能力在Flink里叫状态。Flink把状态管理做成了底层基础设施,你可以直接用keyed state、operator state,不用自己搞数据库记中间结果。这一点是Flink和很多早期流引擎拉开差距的关键。

流批一体。Flink的底层引擎同时支持无限流(实时数据)和有限流(文件等批量数据),API也是同一套。意思是你可以用同一套技能去写实时任务和离线任务,逻辑共通,心智负担小很多。这对公司和开发者都是好事,团队不用养两条技术栈。

精确一次语义。这是老生常谈,但搞清楚很简单。一个Flink任务在运行中挂了,重启后数据会不会重复算?Flink通过轻量级异步快照机制,保证结果“只算一次”,不会因为故障导致重复或丢失。当然这是有能力上限的,前提是下游Sink也支持幂等或事务写入,但这个概念到后面讲精确一次那一章再展开。

把这些能力放在一起看,你就能明白Flink为什么是今天实时计算领域绕不开的选择。它不只是一个“跑得快的工具”,而是一个把延迟、吞吐、状态恢复、一致性打包好的计算框架。

2. 想用好Flink,这几个概念必须弄明白

2.1 数据流和算子:一个简单的任务长什么样

Flink程序的基本结构可以理解为一条流水线:数据从一个地方进来,经过一道道加工,最后送到某个地方去。

这条流水线上的“数据”,在Flink里叫DataStream,也就是一个无穷无尽的事件序列。流水线上的每一道加工工序叫“算子”。常见的算子有map(一对一转换)、filter(过滤)、flatMap(一进多出)、keyBy(按某个字段分组)、window(开窗聚合)、sink(输出到外部系统)。

用流水线类比可能更好理解。想象一家工厂的传送带上不停送来产品,某个工位把产品贴上标签,这是map;某个工位检查质量不合格的直接拿走,这是filter;最后把产品装箱送走,这是sink。Flink的任务就是把这串工位串起来,让数据像产品一样流过去。生产环境里一个任务的拓扑可能几十个算子,但在代码层面,核心其实就是这些线性或分叉的数据流变换。

很多新手会犯一个概念性错误:以为定义好了这些算子,代码就已经开始算了。实际上不是。在Flink里,前期的代码只是“搭建蓝图”,真正的数据流动只有在你调用execute()之后才开始。这个点很重要,后面写代码时会有直观感受。

2.2 窗口、水位线、状态和检查点

这几个概念是流处理新手最容易晕的地方,我按自己最顺的理解方式讲一遍。

窗口。数据是无穷无尽的,但聚合操作总得有个范围。这个范围就是窗口。最常见的三种窗口:滚动窗口(固定长度,互不重叠)、滑动窗口(固定长度,但每隔一段就重新开一个)、会话窗口(基于事件活跃间隙切分)。对应反常理的点是:窗口的边界不一定按系统时间走,而是按数据自带的时间戳走,这就引出水位的概念。

水位线。它解决的核心问题是“乱序”。一个数据在源头产生的时间是12:00:01,但它经过网络传输到达Flink可能已经12:00:03了,甚至12:00:05。你怎么判断“12:00:00到12:00:01这个窗口的数据来齐了”?Flink用水位线来做这个“判定延迟”的标志。水位线可以理解成一句话:“我保证,水位线这个时刻之前的数据我都出发了(或者迟到了会被标记为迟到数据)”。它本质上是一个进度信号,不是真实数据。如果你设置了10秒的水位线延迟,那窗口会在“事件时间到达窗口末尾+10秒”才开始触发计算。不理解水位线,你写事件时间窗口时大概率会踩到“窗口永远不触发”的坑。

状态。很好理解,就是你算子内用来记录历史信息的变量。Flink把这个变量的存储、持久化、扩容迁移都接管了。比如你统计PV/UV,UV去重,你需要记住哪些用户已经来过,这些就是状态。状态和检查点绑定,Flink会周期性地把状态打入分布式快照。

检查点。这是Flink故障恢复的底牌。Source端有个机制定期往Flink发一个特殊标记,这个标记会跟着正常数据一起流经所有算子。Flink在每次特殊标记到达算子时,就把该算子的状态打一份快照。所有算子都打完,就形成了一致的全局快照。作业挂了之后从最近一次快照恢复,数据从上一次的Source位置重新读,因为状态和Source位置是匹配的,所以能做到精确一次。这里的特殊标记有一个专业名词叫Barrier,但你先记住“拍照”这个作用就够了。

这些概念你不需要第一次就全弄懂,但要先有印象。因为本章后面的实操里,你会实际看到“开窗”“状态”长什么样,回头再看这一段,会通得快很多。

3. 本地跑通第一个Flink程序(附完整代码)

3.1 环境准备与项目依赖

本机跑Flink不需要搭建集群,准备三样东西就行:JDK(建议8或11)、Maven(3.6以上)、一个集成开发环境(带Maven支持即可,流行的几个都行)。Flink版本我以当前主流稳定版1.17为基准,下面的代码在1.13到1.17之间基本都通用。

先创建一个Maven工程,在pom.xml里引入核心依赖:

<properties> <flink.version>1.17.0</flink.version> </properties> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-java</artifactId> <version>${flink.version}</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-java</artifactId> <version>${flink.version}</version> </dependency> <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-clients</artifactId> <version>${flink.version}</version> </dependency>

这三个依赖对应本地开发和提交作业所需的客户端能力。scope这里先不用provided,本地开发直接用默认就好。如果后面要打包上集群,再考虑把依赖作用域调整成provided,避免提交时依赖冲突。

另外要加一个打包插件。这一步常被新手漏掉,到集群提交时才发现打出来的jar少了临时依赖,报ClassNotFound。在Maven里最常用的就是shade插件,它能把你代码依赖的所有类重新打包进一个可执行的胖Jar里。配置贴在下面,后面提交集群直接复用。

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>你的主类全限定名</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build>

注意,mainClass改成你实际的主类路径。这个插件在本地不打包不影响,但到集群阶段是必须的。

3.2 一个能真正体现“流式”的WordCount

传统的入门样例一般读一个静态文件,但那跟批处理没区别,体会不到“流”的感觉。所以我用一个“Socket流”版本:你往终端里敲一行文字,Flink立刻统计这行里的单词频次,再和之前输入的所有行累计加总。这样你能亲眼看到数据是持续进出的,而不是一次性跑完。

在MySQL或终端工具之外,先准备一个发送数据的终端。Linux或macOS自带netcat,执行:

nc -lk 9999

这个命令的意思是监听本机的9999端口,你在终端输入的任何文字,回车后就会发出去。Windows用户没有原生netcat,可以搜索安装一个简易版本,或者用其他能往指定TCP端口发文本的工具,不影响后面的逻辑。

然后写主类,完整代码如下:

import org.apache.flink.api.common.functions.FlatMapFunction; import org.apache.flink.api.common.typeinfo.Types; import org.apache.flink.api.java.tuple.Tuple2; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.util.Collector; public class SocketWordCount { public static void main(String[] args) throws Exception { // 1. 创建流执行环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 2. 从Socket读取文本流,指定本机9999端口 DataStream<String> textStream = env.socketTextStream("localhost", 9999); // 3. 计算逻辑:按非字母字符切分单词,每个单词记一次数 DataStream<Tuple2<String, Integer>> wordCountStream = textStream .flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() { @Override public void flatMap(String line, Collector<Tuple2<String, Integer>> out) throws Exception { String[] words = line.split("\\W+"); for (String word : words) { if (word.length() > 0) { out.collect(Tuple2.of(word.toLowerCase(), 1)); } } } }) // 由于flatMap用了匿名类,返回类型可能被擦除,需要显式声明 .returns(Types.TUPLE(Types.STRING, Types.INT)) .keyBy(word -> word.f0) .sum(1); // 4. sink:直接打印到控制台 wordCountStream.print(); // 5. 启动任务,真正开始执行 env.execute("Socket WordCount Demo"); } }

来说说每个步骤在干什么。第一步创建执行环境,它会自动根据情况决定采用本地模式还是集群模式。当前我们在IDE里,就是本地环境。第二步,socketTextStream是一个Source算子,它不断从TCP端口接收文本。第三步是一串transform操作:flatMap把一个句子拆成若干个单词,每个单词输出一个二元组(单词,1);keyBy指定按单词分组;sum(1)对二元组的第二个字段做累加。这里sum(1)的“1”指的就是Tuple2的第二位,如果你是第一次写Flink,很容易把sum(1)理解成“对第一个字段求和”,其实下标从0开始,sum(1)才是第二位的计数器。

一个细节务必注意:flatMap用了匿名类,泛型信息在运行时可能被擦除,所以后面必须跟一个returns(...)显式指定返回类型。很多人第一次写类似代码时,去掉returns会直接报类型推断异常,别到了半夜才想起来是这个原因。

3.3 运行与观察

先保证netcat窗口在运行,就是执行了nc -lk 9999并且停留等待输入的状态。然后直接在IDE里运行SocketWordCount的main方法。控制台会先打印很多执行环境相关的日志,最后你会看到类似“Job has been submitted”的字样,任务就进入运行状态了。

这时回到netcat的终端,随便输入一行英文,比如:

flink is a streaming engine flink flink is cool

注意看IDE输出的日志窗口,你会看到类似这样的输出:

flink : 1 is : 1 a : 1 streaming : 1 engine : 1 flink : 2 flink : 3 is : 2 cool : 1

看到flink的计数从1变成2再变成3,说明Flink在处理新的数据之前,把之前的状态保存并累加上了。这就是状态化计算最直观的演示。再多输几行,你会意识到这个任务是“活”的,它不会停下,每来一条数据就更新一次结果。到这里你的第一个Flink流任务算是真正跑通了。

4. 从本地到集群:一次提交背后的故事

4.1 部署模式与提交命令

本地跑通只是第一步。Flink真正发挥价值肯定不是在IDE里,而是运行在集群上。Flink支持的部署方式比较多,独立集群、YARN、Kubernetes都有,但这章我不会把每种模式都展开讲,那是后面独立章节的任务。这里想让你先理解一件事:同一份代码,本地跑和集群跑有什么不同。

Flink提供了几种“运行环境”适配逻辑,核心就靠StreamExecutionEnvironment.getExecutionEnvironment()这一行。它在本地环境下自动创建LocalStreamEnvironment,在集群提交时自动识别上下文并切换成集群运行环境。这就是为什么你不需要在代码里写死“我在本地还是集群”,Flink会在提交时根据启动入口自动判断。

提交一个任务到集群,最朴素的命令长这样:

flink run -d \ -c com.example.SocketWordCount \ -p 2 \ my-job-1.0.jar

这里-c指定主类,-p指定并行度,-d表示后台运行。但请注意,socketTextStream这个source在集群上运行会有点尴尬,因为集群里的各节点不一定能访问到你的本机9999端口。如果你想在集群里试,最好把数据源换成Kafka或者其他消息队列,这一块在讲连接器时会专门介绍。

4.2 并行度与资源分配

并行度是Flink里特别重要的一个参数。可以把并行度理解成一个算子的“分身数量”。并行度等于3,意味着这个算子同时有3个任务在干活,每个处理不同的数据片段。

并行度的设置有三个层级,从低到高分别优先级递增:代码里通过env.setParallelism(1)设置全局默认;具体算子上通过算子名后调setParallelism()设置某个算子的并行度;提交时通过命令行覆盖所有。

实际运行中,Flink的每个并行子任务会占用一个Slot。Slot是TaskManager里划分出的资源单元,一个TaskManager有几个Slot,就最多同时运行几个任务。很多新手一上来就疯狂增加并行度,以为越大越快,结果发现资源没增加,任务反而因为调度开销变大而变慢。并行度不是越大越好,它受限于可用资源,也受数据源分区数的影响。

如果你读的是Kafka数据,并行度最好小于等于Kafka topic的分区数,否则多余的并行任务会处于空转状态,白白占用Slot。这里有个常见的推演逻辑:数据源的分区数目其实决定了吞吐上限,不是说你想并行100就并行100,瓶颈常常在Source。

4.3 为什么不建议生产环境直接用默认配置

本地跑demo时,Flink很多配置都是用默认值,方便快速启动,但拿到生产环境会出问题。我挑几个最常见的影响点。

默认不开启检查点。本地跑demo,作业挂了重启就行。生产环境作业挂了必须能从故障中恢复,所以必须显式打开checkpoint。打开方式是这样:

// 开启检查点,间隔3秒 env.enableCheckpointing(3000); // 设置检查点模式为精确一次 env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE); // 两个检查点之间最小间隔 env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500); // 检查点超时时间 env.getCheckpointConfig().setCheckpointTimeout(60000); // 允许检查点失败多少次 env.getCheckpointConfig().setTolerableCheckpointFailureNumber(3);

保存检查点的位置默认在本地临时目录,但生产环境通常跑在多节点上,本地临时路径各节点不共享,作业重启就找不到之前的状态了。所以你需要把状态后端的状态存储路径配置到一个共享文件系统,比如HDFS或S3,这样任何节点都能读取快照进行恢复。配置方式很简单:

// 把状态后端存储到指定目录,这里以文件系统为例 env.setStateBackend(new HashMapStateBackend()); env.getCheckpointConfig().setCheckpointStorage("hdfs:///flink/checkpoints");

如果不配置,默认的状态后端是内存型,一旦容器重启,状态全丢。所以“本地跑通”和“生产可用”之间隔着很多配置细节,这也是为什么很多新手从demo搬到集群后一脸懵:本地环境太宽容了,很多没配置的东西在集群上成了致命问题。

5. 新手阶段的常见坑与我的使用建议

5.1 三个最让我头疼的坑

第一个坑是“窗口不触发”。用事件时间开窗后,窗口迟迟不计算,数据一直堆积。根源几乎都是忘了配置水位线,或者水位线策略的延迟时间设得不对。水位线本质上是给数据流一个“判定迟到”的进度,如果没有它,系统不知道“窗口时间到了数据还没来”这个边界在哪里。解决办法就是明确指定水位线生成策略,后面我讲事件时间实操时会把这个坑彻底拆开讲。

第二个坑是“源码不改,集群提交却报类找不到”。这通常不是代码逻辑问题,而是打包遗漏或依赖冲突。本地依赖作用于compile,运行时不缺;集群环境里Flink自身带了Scala和部分基础库,如果你的胖Jar把这些也打进去了,就会版本冲突。规范做法是Flink核心依赖设置成provided,连接器等第三方组件在提交时再动态传递。这个调整可以避免大量ClassNotFoundException和NoSuchMethodError。

第三个坑是背压问题。很多新手对背压(BackPressure)没有概念,看到任务延迟增大就一味加资源。但Flink的背压本身是一种“信号”,说明下游处理速度跟不上上游,继续往下游塞数据只会堆积在缓存里甚至导致内存溢出。Flink Web UI里能看到每个算子的背压指标,如果某个算子背压很高,首先应该检查它的处理逻辑是否太重,其次检查并行度分配,最后才是加资源。背压是流处理里必须学会读懂的一种语言,不理解它,优化无从谈起。

5.2 给新手的实操建议

我的建议很简单:不要急着搭建集群,先在本地把基础概念逐个验证。这里有几条非常实用的学习路线。先在IDE里跑通SocketWordCount这类例子,再用env.fromElements(1,2,3)这种集合数据源快速验证某些算子行为,别每次都接外部系统。然后试着打开检查点杀掉作业,看看从最近一次快照恢复的关键日志表现。再把打印换成文件Sink,到本地目录里检查输出。每一步都亲眼看结果,概念就会落地。

配套的资料选择也有讲究。Flink官方文档的内容本身写得挺清楚,但信息量太大,新手容易看一会儿就走神。建议按章节选读,先看DataStream API介绍,再看Windows和Watermarks,最后才是状态与容错。零碎时间可以在某社区平台搜索真实案例,看别人在生产环境怎么排查,收获往往更大。

最后一点特别重要:把一个demo程序理解透,比复制十个模板强。很多新手上手就是抄一个Kafka到MySQL的作业,出了一堆问题,最后发现问题其实出在最基本的API理解上。先从最小场景开始,一个个算子的行为摸透,再组装成复杂拓扑,这是最稳的路线。

这一章只是系列的开头,先用“简介+简单使用”,帮大家把Flink的轮廓立起来。下一章我准备深入讲DataStream API的算子细节和实际数据源接入,到时候用真实场景把窗口、水位线、状态串联起来跑一遍。如果你在照着操作时遇到了什么奇怪的报错,欢迎留言,把日志贴出来,我会挑典型的下一章拆解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 13:23:23

TM1621段式LCD驱动实战:初始化顺序与常见坑解析

简介&#xff1a;这是一份针对TM1621数码管控制器的C语言驱动程序包&#xff0c;面向嵌入式开发者和电子爱好者&#xff0c;用于解决通过串行外设接口或并行接口控制七段数码管显示数字与字符的实际问题。该芯片常见于智能家居设备、仪器仪表和嵌入式系统的人机交互界面&#x…

作者头像 李华
网站建设 2026/10/16 13:23:22

手写单链表:从Node节点到五大核心方法的底层实现与避坑指南

1. 直击核心&#xff1a;单链表底层封装到底在封装什么先把这个标题翻译成大白话&#xff1a;别用现成的LinkedList&#xff0c;而是从Node节点开始&#xff0c;自己动手实现一个能存元素、能按索引/元素查找、能增删改查、能清空、能打印的自定义单链表类。很多朋友看到“底层…

作者头像 李华
网站建设 2026/10/16 13:23:03

低成本蓝牙SoC同声翻译耳机方案设计与踩坑实践

去年接到一个做同声翻译耳机的需求&#xff0c;要在某国产入门级蓝牙音频SoC方案上落地。这个方案芯片成本压得很低&#xff0c;SDK也比较成熟&#xff0c;很多做蓝牙耳机、带麦音箱的厂商都在用。同声翻译这个词听起来高级&#xff0c;落到嵌入式端其实就是一句话&#xff1a;…

作者头像 李华
网站建设 2026/10/16 13:21:20

context-mode实战:AI上下文管理的模式切换方案拆解

老新人入手“context-mode”&#xff1a;一个把AI上下文管理做成“模式切换”的实战复盘做AI应用开发这么久&#xff0c;我踩过最深的一个坑&#xff0c;就是上下文管理。你不把“上下文”当回事&#xff0c;它就会在关键时刻给你来一记回旋踢——要么AI忘了三十分钟前你交代的…

作者头像 李华
网站建设 2026/10/16 13:18:37

大模型Tool Use工程实践:从手写解析到插件化链路

1. 这不是写个“工具调用”Demo&#xff0c;而是现场验证工程直觉“面试官让我手写一个 Tool Use&#xff0c;我用了 3 种方案”——这句话在最近的开发者社群里被反复截图转发&#xff0c;不是因为炫技&#xff0c;而是因为它精准戳中了当前大模型应用开发中最真实、最高频、也…

作者头像 李华