这阵子后台私信里问Flink相关问题的频率明显高了一截。不少朋友是从Spark转过来或者在实时数仓项目里被点名要用Flink但一翻官方文档又觉得概念满天飞DataStream、窗口、水位线、状态后端、检查点……每个词都认识连在一起就不知道在说什么。所以我决定开一个系列按自己从零上手到在生产环境填坑的真实路线把Flink掰开揉碎了讲清楚。这是第一章先解决“Flink是什么”和“本地怎么跑起来”这两件事适合完全没接触过流处理的新手也适合已经写过几个作业但还停留在“照着模板改”阶段的同学。本章不会堆砌概念所有原理都用生活化的类比拆开每一步操作都能跟着落地。1. 先说清楚Flink到底解决了什么问题1.1 从批处理到流处理数据不是静止的大多数人接触大数据的第一站是批量计算。每天凌晨跑一次任务把昨天的日志统计一遍出报表这是批处理的经典场景。但如果业务对数据时效的要求变成“秒级”甚至“毫秒级”批处理就有点跟不上了。举几个很典型的场景电商大促时的实时大屏每秒钟都在刷新支付金额风控系统要在一笔交易发生的一瞬间判断它是不是风险操作监控告警等不及T1异常在几秒内就得被感知到。这些场景的共同特点是数据像流水一样源源不断涌进来永远没有“今天的数据完了”这个说法只有“到目前为止的数据”。流处理框架就是为这种场景设计的。它不再把数据先攒起来再统一计算而是来一条处理一条计算结果持续更新。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/UVUV去重你需要记住哪些用户已经来过这些就是状态。状态和检查点绑定Flink会周期性地把状态打入分布式快照。检查点。这是Flink故障恢复的底牌。Source端有个机制定期往Flink发一个特殊标记这个标记会跟着正常数据一起流经所有算子。Flink在每次特殊标记到达算子时就把该算子的状态打一份快照。所有算子都打完就形成了一致的全局快照。作业挂了之后从最近一次快照恢复数据从上一次的Source位置重新读因为状态和Source位置是匹配的所以能做到精确一次。这里的特殊标记有一个专业名词叫Barrier但你先记住“拍照”这个作用就够了。这些概念你不需要第一次就全弄懂但要先有印象。因为本章后面的实操里你会实际看到“开窗”“状态”长什么样回头再看这一段会通得快很多。3. 本地跑通第一个Flink程序附完整代码3.1 环境准备与项目依赖本机跑Flink不需要搭建集群准备三样东西就行JDK建议8或11、Maven3.6以上、一个集成开发环境带Maven支持即可流行的几个都行。Flink版本我以当前主流稳定版1.17为基准下面的代码在1.13到1.17之间基本都通用。先创建一个Maven工程在pom.xml里引入核心依赖properties flink.version1.17.0/flink.version /properties dependency groupIdorg.apache.flink/groupId artifactIdflink-java/artifactId version${flink.version}/version /dependency dependency groupIdorg.apache.flink/groupId artifactIdflink-streaming-java/artifactId version${flink.version}/version /dependency dependency groupIdorg.apache.flink/groupId artifactIdflink-clients/artifactId version${flink.version}/version /dependency这三个依赖对应本地开发和提交作业所需的客户端能力。scope这里先不用provided本地开发直接用默认就好。如果后面要打包上集群再考虑把依赖作用域调整成provided避免提交时依赖冲突。另外要加一个打包插件。这一步常被新手漏掉到集群提交时才发现打出来的jar少了临时依赖报ClassNotFound。在Maven里最常用的就是shade插件它能把你代码依赖的所有类重新打包进一个可执行的胖Jar里。配置贴在下面后面提交集群直接复用。build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.1/version executions execution phasepackage/phase goals goalshade/goal /goals configuration transformers transformer implementationorg.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端口 DataStreamString textStream env.socketTextStream(localhost, 9999); // 3. 计算逻辑按非字母字符切分单词每个单词记一次数 DataStreamTuple2String, Integer wordCountStream textStream .flatMap(new FlatMapFunctionString, Tuple2String, Integer() { Override public void flatMap(String line, CollectorTuple2String, 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把一个句子拆成若干个单词每个单词输出一个二元组单词,1keyBy指定按单词分组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的算子细节和实际数据源接入到时候用真实场景把窗口、水位线、状态串联起来跑一遍。如果你在照着操作时遇到了什么奇怪的报错欢迎留言把日志贴出来我会挑典型的下一章拆解。