轻松入门进阶Flink第一课 Flink基础

86 篇文章 49 订阅

开篇词:实时计算领域最锋利的武器 Flink

你好,欢迎来到 Flink 专栏,我是王知无,目前在某一线互联网公司从事数据平台架构和研发工作多年,算是整个大数据开发领域的老兵了。


我最早从 Release 版本开始关注 Flink,可以说是国内第一批钻研 Flink 的开发者,后来基于 Flink 开发过实时计算业务应用、实时数据仓库以及监控报警系统,在这个过程中积累了大量宝贵的生产实践经验。

面试是开发者永远绕不过去的坎

由于项目需要,我在工作中面试过很多 Flink 开发工程师,并且发现了一些普遍性问题,比如:

  • 对常用的 Flink 核心概念和原理掌握不牢,一旦参与到实战业务中必将寸步难行,一面直接被刷掉;

  • 能够通过简历筛选的人基本都有实时流计算开发的经验,可以从容应对典型场景下的问题,但对于非典型但常见的业务场景问题就会支支吾吾,无从应答;

  • 有些面试者自称参与过实时计算平台的架构设计、开发、发布和运维等全流程的工作,但稍微追问就会发现他在项目中的参与度其实很低,暴露出在上一家公司只是开发团队的一个“小透明”;

  • 我们现在招聘其实是偏向招有相关经验并熟悉底层原理的人,曾经有面试者能熟练回答在项目中是如何应用 Flink 的,但是不知道底层源码级别的实现


上面列举的这四个问题看似不同,但本质上都是在全方位考察你对技术原理的理解深度,以及在实际工作中解决问题的能力。


当然还有一类人,他们具备深厚的理论基础和丰富的实战经验,却往往因为缺乏面试经验,依然屡屡与大厂擦肩而过。很多开发者在学习完一个框架后,可以熟练地开发和排查问题,但是在面试的过程中却无法逻辑清晰地表述自己的观点。想象一下,当你在面试中被问到以下三个问题:

  • Flink 如何实现 Exactly-once 语义?

  • Flink 时间类型的分类和各自的实现原理?

  • Flink 如何处理数据乱序和延迟?

你将如何作答?面试官满意的答案究竟长什么样?上述问题的答案,你都可以在这个专栏中找到。

想进大厂,必须掌握 Flink 技术

随着大数据时代的发展、海量数据的实时处理和多样业务的数据计算需求激增,传统的批处理方式和早期的流式处理框架也有自身的局限性,难以在延迟性、吞吐量、容错能力,以及使用便捷性等方面满足业务日益苛刻的要求。在这种形势下,Flink 以其独特的天然流式计算特性和更为先进的架构设计,极大地改善了以前的流式处理框架所存在的问题。


越来越多的国内公司开始用 Flink 来做实时数据处理,其中阿里巴巴率先将 Flink 技术在全集团推广使用,比如 Flink SQL 与 Hive 生态的集成、拥抱 AI 等;腾讯、百度、字节跳动、滴滴、华为等众多互联网公司也已经将 Flink 作为未来技术重要的发力点。在未来 3 ~ 5 年,Flink 必将发展成为企业内部主流的数据处理框架,成为开发者进入大厂的“敲门砖”。




反观国外,在 2019 年 Flink 已经成为 Apache 基金会和 GitHub 社区最为活跃的项目之一。在全球范围内,越来越多的企业都在迫切地进行技术迭代和更新,无论是更新传统的实时计算业务,还是实时数据仓库的搭建,Flink 都是最佳之选。


毫无疑问,Flink 已经成为大数据开发、有实时数据需求的 Java 后端开发、数据仓库、数据挖掘等岗位必须掌握的技术。目前一名具有 3~5 年经验的 Flink 研发工程师,其薪资普遍在 30K 左右,而如果你是公司大数据实时计算领域的核心开发人员,在大数据实时计算领域有深厚的造诣,那么薪资还会更高。



然而从目前的市场状况来看,熟练掌握 Flink 的开发者仍然供不应求,大数据领域几乎 100% 的招聘 JD 上都要求开发者掌握 Flink。因此,熟练掌握 Flink 也会为求职中的开发者提供更大的议价空间。

你的困惑,我来解答

回顾过往,我在学习 Flink 的过程中也不是一帆风顺的,最初没有任何中文文档,比如在处理消息乱序问题时,在面对 Flink 复杂的窗口设计和水印生成时,不得不在晦涩的英文文档、社区邮件列表和源码中寻找答案。


目前市面上的 Flink 资源依然较少,而且由于 Flink 更新迭代过快、文档更新不及时,让我们在学习和实践过程中仍然面临诸多难点和各种问题:

  • 当开发者面对新增 API 的使用时,官网找不到答案;

  • Flink 的一些概念难以理解,文档又全部是英文的,进一步增加了理解难度;

  • Flink 在生产实践中也会遇到大量的问题,任何参数和 API 的不正确使用都会导致灾难性后果,虽然其中有些问题只有在大数据量、高并发条件下才会产生,比如数据倾斜、反压、多流 join 等,但是这部分正是我们学习进阶和面试大厂必须掌握的。


因此,真正掌握生产环境下的问题处理技能,才能称得上是掌握了 Flink。在 Flink 实践应用中由于 Flink 社区发展时间较短,版本迭代频繁,很多开发者不得不在摸索中前进,出现问题没有可以借鉴的经验,使得开发者束手无策。所以我在设计专栏时,把 Flink 相关的基础理论与实战案例相结合,基于 Flink 最新的版本,从基础概念入手,通过大量的实战代码演练,带你进入真实的生产环境,学习如何解决当下企业内部真实面临的生产实践问题,获得大厂一线生产实践的宝贵经验。

课程设计

专栏共划分 5 部分,合计 42 篇。

  • 入门篇:以讲解 Flink 的应用场景、编程模型,以及常用 API 的原理和使用为主,借此让你对 Flink 技术有一个更全面的认识。

  • 基础篇:重点讲解 Flink 的核心概念及其原理,比如流批一体、计算资源、State、重启策略、并行度、窗口、时间、水印、CEP 等内容,希望你能对 Flink 设计思想、架构模型有更深刻的认识。这些概念及其原理尤为重要,将是你进入生产环境进行开发和调优的基础,必须掌握牢固。

  • 进阶篇:是这个专栏的核心内容,我将帮助你拓展技术深度,深入到实际的生产环境中,探讨 Flink 的高可用配置,讲解排查反压和数据倾斜、资源配置、监控作业的方法,以及如何使用 Flink 高效地去重和建设实时数据仓库。

  • 实战篇:结合市面上应用较广的一些场景设计了多个实战项目,比如实时数据仓库、实时大屏、错误日志报警等,帮助你掌握 Flink 在不同场景中的使用,打破公司业务场景和应用的局限性。这里也会用到在基础篇和进阶篇中学习的知识,带你巩固所学,达到即学即用的效果。

  • 面试篇:假如你已经掌握了 Flink 的理论知识和实战应用,但还不够,还有最后一个关卡等待你去突破,那就是面试。本模块将从基础、进阶、源码、方案设计上,结合我的实践经历讲解面试中常见的问题,帮助你提前掌握面试的要点,透过题目领会面试技巧,真正成为企业正在寻找的那个人。


在这个专栏里,我希望将自己多年的大数据开发经验、经历的典型问题,以及遇到的经验教训梳理总结,给你带来一场独家定制的分享。核心目标就是,帮助你构建一套完整的 Flink 技术知识体系,打牢基础、完成进阶、稳赢实战、通关面试


通过本专栏的学习,你将获得:

  • 原理和实践结合的学习体验,知其然也知其所以然;

  • 大量的生产实践问题及其解决方案;

  • 多个实战项目,涵盖了目前各大公司经典的需求和解决方案;

  • 面试指南,面试环节是你拿到 Offer 的最终保障。

讲师寄语

我曾经就职于多家一二线互联网公司,深刻了解到大数据实时计算领域的应用和未来发展趋势。就在前段时间,我身边很多在大数据领域浸润多年的朋友,薪资已经很高了,还在跟我打听 Flink 的学习资源,要补充学习这块的知识。


我们知道,2013 年被称为“大数据元年”,发展到今天领域内的红利渐渐削弱,懂大数据的人越来越多,入坑早的开发者大有人在,而且很多已是大数据领域的资深专家。新人入行如果想实现弯道超车,学习和掌握新技术是加速的唯一捷径。Flink 作为出道仅仅一年的大数据框架,发展速度超乎想象,加之阿里巴巴的推波助澜,已经成为大数据和实时开发领域的开发者必须掌握的框架。


我们从熟悉到真正掌握一门框架,这个过程并不是一帆风顺的,但只有在实际生产环境中不断地发现问题、解决问题,个人能力也才会出现质的飞跃,不断成为公司和团队的核心骨干。


独行者速,众行者远,我在这里,和很多朋友一起陪伴你的学习和成长,欢迎你在留言区分享你的困惑和成长。


第03讲:Flink 的编程模型与其他框架比较

本课时我们主要介绍 Flink 的编程模型与其他框架比较。

本课时的内容主要介绍基于 Flink 的编程模型,包括 Flink 程序的基础处理语义和基本构成模块,并且和 Spark、Storm 进行比较,Flink 作为最新的分布式大数据处理引擎具有哪些独特的优势呢?

Flink 的核心语义和架构模型

我们在讲解 Flink 程序的编程模型之前,先来了解一下 Flink 中的 Streams、State、Time 等核心概念和基础语义,以及 Flink 提供的不同层级的 API。

Flink 核心概念
  • Streams(流),流分为有界流和无界流。有界流指的是有固定大小,不随时间增加而增长的数据,比如我们保存在 Hive 中的一个表;而无界流指的是数据随着时间增加而增长,计算状态持续进行,比如我们消费 Kafka 中的消息,消息持续不断,那么计算也会持续进行不会结束。
  • State(状态),所谓的状态指的是在进行流式计算过程中的信息。一般用作容错恢复和持久化,流式计算在本质上是增量计算,也就是说需要不断地查询过去的状态。状态在 Flink 中有十分重要的作用,例如为了确保 Exactly-once 语义需要将数据写到状态中;此外,状态的持久化存储也是集群出现 Fail-over 的情况下自动重启的前提条件。
  • Time(时间),Flink 支持了 Event time、Ingestion time、Processing time 等多种时间语义,时间是我们在进行 Flink 程序开发时判断业务状态是否滞后和延迟的重要依据。
  • API:Flink 自身提供了不同级别的抽象来支持我们开发流式或者批量处理程序,由上而下可分为 SQL / Table API、DataStream API、ProcessFunction 三层,开发者可以根据需要选择不同层级的 API 进行开发。
Flink 编程模型和流式处理

我们在第 01 课中提到过,Flink 程序的基础构建模块是(Streams)和转换(Transformations),每一个数据流起始于一个或多个 Source,并终止于一个或多个 Sink。数据流类似于有向无环图(DAG)。

1.png

在分布式运行环境中,Flink 提出了算子链的概念,Flink 将多个算子放在一个任务中,由同一个线程执行,减少线程之间的切换、消息的序列化/反序列化、数据在缓冲区的交换,减少延迟的同时提高整体的吞吐量。

官网中给出的例子如下,在并行环境下,Flink 将多个 operator 的子任务链接在一起形成了一个task,每个 task 都有一个独立的线程执行。

2.png

Flink 集群模型和角色

在实际生产中,Flink 都是以集群在运行,在运行的过程中包含了两类进程。

  • JobManager:它扮演的是集群管理者的角色,负责调度任务、协调 checkpoints、协调故障恢复、收集 Job 的状态信息,并管理 Flink 集群中的从节点 TaskManager。
  • TaskManager:实际负责执行计算的 Worker,在其上执行 Flink Job 的一组 Task;TaskManager 还是所在节点的管理员,它负责把该节点上的服务器信息比如内存、磁盘、任务运行情况等向 JobManager 汇报。
  • Client:用户在提交编写好的 Flink 工程时,会先创建一个客户端再进行提交,这个客户端就是 Client,Client 会根据用户传入的参数选择使用 yarn per job 模式、stand-alone 模式还是 yarn-session 模式将 Flink 程序提交到集群。

3.png

Flink 资源和资源组

在 Flink 集群中,一个 TaskManger 就是一个 JVM 进程,并且会用独立的线程来执行 task,为了控制一个 TaskManger 能接受多少个 task,Flink 提出了 Task Slot 的概念。

我们可以简单的把 Task Slot 理解为 TaskManager 的计算资源子集。假如一个 TaskManager 拥有 5 个 slot,那么该 TaskManager 的计算资源会被平均分为 5 份,不同的 task 在不同的 slot 中执行,避免资源竞争。但是需要注意的是,slot 仅仅用来做内存的隔离,对 CPU 不起作用。那么运行在同一个 JVM 的 task 可以共享 TCP 连接,减少网络传输,在一定程度上提高了程序的运行效率,降低了资源消耗。

4.png

与此同时,Flink 还允许将不能形成算子链的两个操作,比如下图中的 flatmap 和 key&sink 放在一个 TaskSlot 里执行以达到资源共享的目的。

5.png

Flink 的优势及与其他框架的区别

Flink 在诞生之初,就以它独有的特点迅速风靡整个实时计算领域。在此之前,实时计算领域还有 Spark Streaming 和 Storm等框架,那么为什么 Flink 能够脱颖而出?我们将分别在架构、容错、语义处理等方面进行比较。

架构

Stom 的架构是经典的主从模式,并且强依赖 ZooKeeper;Spark Streaming 的架构是基于 Spark 的,它的本质是微批处理,每个 batch 都依赖 Driver,我们可以把 Spark Streaming 理解为时间维度上的 Spark DAG。

Flink 也采用了经典的主从模式,DataFlow Graph 与 Storm 形成的拓扑 Topology 结构类似,Flink 程序启动后,会根据用户的代码处理成 Stream Graph,然后优化成为 JobGraph,JobManager 会根据 JobGraph 生成 ExecutionGraph。ExecutionGraph 才是 Flink 真正能执行的数据结构,当很多个 ExecutionGraph 分布在集群中,就会形成一张网状的拓扑结构。

容错

Storm 在容错方面只支持了 Record 级别的 ACK-FAIL,发送出去的每一条消息,都可以确定是被成功处理或失败处理,因此 Storm 支持至少处理一次语义。

针对以前的 Spark Streaming 任务,我们可以配置对应的 checkpoint,也就是保存点。当任务出现 failover 的时候,会从 checkpoint 重新加载,使得数据不丢失。但是这个过程会导致原来的数据重复处理,不能做到“只处理一次”语义。

Flink 基于两阶段提交实现了精确的一次处理语义,我们将会在后面的课时中进行完整解析。

反压(BackPressure)

反压是分布式处理系统中经常遇到的问题,当消费者速度低于生产者的速度时,则需要消费者将信息反馈给生产者使得生产者的速度能和消费者的速度进行匹配。

Stom 在处理背压问题上简单粗暴,当下游消费者速度跟不上生产者的速度时会直接通知生产者,生产者停止生产数据,这种方式的缺点是不能实现逐级反压,且调优困难。设置的消费速率过小会导致集群吞吐量低下,速率过大会导致消费者 OOM。

Spark Streaming 为了实现反压这个功能,在原来的架构基础上构造了一个“速率控制器”,这个“速率控制器”会根据几个属性,如任务的结束时间、处理时长、处理消息的条数等计算一个速率。在实现控制数据的接收速率中用到了一个经典的算法,即“PID 算法”。

Flink 没有使用任何复杂的机制来解决反压问题,Flink 在数据传输过程中使用了分布式阻塞队列。我们知道在一个阻塞队列中,当队列满了以后发送者会被天然阻塞住,这种阻塞功能相当于给这个阻塞队列提供了反压的能力。

总结

本课时主要介绍了 Flink 的核心语义和架构模型,并且从架构、容错、反压等多方位比较了 Flink 和其他框架的区别,为后面我们学习 Flink 的高级特性和实战打下了基础。

以上就是本课时的内容。在下一课时中,我将介绍“Flink 常用的 DataSet 和 DataStream API”,下一课时见。

点击这里下载本课程源码


第04讲:Flink 常用的 DataSet 和 DataStream API

本课时我们主要介绍 Flink 的 DataSet 和 DataStream 的 API,并模拟了实时计算的场景,详细讲解了 DataStream 常用的 API 的使用。

说好的流批一体呢

现状

在前面的课程中,曾经提到过,Flink 很重要的一个特点是“流批一体”,然而事实上 Flink 并没有完全做到所谓的“流批一体”,即编写一套代码,可以同时支持流式计算场景和批量计算的场景。目前截止 1.10 版本依然采用了 DataSet 和 DataStream 两套 API 来适配不同的应用场景。

DataSet 和 DataStream 的区别和联系

在官网或者其他网站上,都可以找到目前 Flink 支持两套 API 和一些应用场景,但大都缺少了“为什么”这样的思考。

Apache Flink 在诞生之初的设计哲学是:用同一个引擎支持多种形式的计算,包括批处理、流处理和机器学习等。尤其是在流式计算方面,Flink 实现了计算引擎级别的流批一体。那么对于普通开发者而言,如果使用原生的 Flink ,直接的感受还是要编写两套代码。

整体架构如下图所示:

image.png
在 Flink 的源代码中,我们可以在 flink-java 这个模块中找到所有关于 DataSet 的核心类,DataStream 的核心实现类则在 flink-streaming-java 这个模块。

image (1).png

image (2).png

在上述两张图中,我们分别打开 DataSet 和 DataStream 这两个类,可以发现,二者支持的 API 都非常丰富且十分类似,比如常用的 map、filter、join 等常见的 transformation 函数。

我们在前面的课时中讲过 Flink 的编程模型,对于 DataSet 而言,Source 部分来源于文件、表或者 Java 集合;而 DataStream 的 Source 部分则一般是消息中间件比如 Kafka 等。

由于 Flink DataSet 和 DataStream API 的高度相似,并且 Flink 在实时计算领域中应用的更为广泛。所以下面我们详细讲解 DataStream API 的使用。

DataStream

我们先来回顾一下 Flink 的编程模型,在之前的课时中提到过,Flink 程序的基础构建模块是(Streams)和转换(Transformations),每一个数据流起始于一个或多个 Source,并终止于一个或多个 Sink。数据流类似于有向无环图(DAG)。

image (3).png

在第 02 课时中模仿了一个流式计算环境,我们选择监听一个本地的 Socket 端口,并且使用 Flink 中的滚动窗口,每 5 秒打印一次计算结果。

自定义实时数据源

在本课时中,我们利用 Flink 提供的自定义 Source 功能来实现一个自定义的实时数据源,具体实现如下:

public class MyStreamingSource implements SourceFunction<MyStreamingSource.Item> {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> isRunning = <span class="hljs-keyword">true</span>;

<span class="hljs-comment">/**
 * 重写run方法产生一个源源不断的数据发送源
 * <span class="hljs-doctag">@param</span> ctx
 * <span class="hljs-doctag">@throws</span> Exception
 */</span>
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">(SourceContext&lt;Item&gt; ctx)</span> <span class="hljs-keyword">throws</span> Exception </span>{
    <span class="hljs-keyword">while</span>(isRunning){
        Item item = generateItem();
        ctx.collect(item);

        <span class="hljs-comment">//每秒产生一条数据</span>
        Thread.sleep(<span class="hljs-number">1000</span>);
    }
}
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">cancel</span><span class="hljs-params">()</span> </span>{
    isRunning = <span class="hljs-keyword">false</span>;
}

<span class="hljs-comment">//随机产生一条商品数据</span>
<span class="hljs-function"><span class="hljs-keyword">private</span> Item <span class="hljs-title">generateItem</span><span class="hljs-params">()</span></span>{
    <span class="hljs-keyword">int</span> i = <span class="hljs-keyword">new</span> Random().nextInt(<span class="hljs-number">100</span>);

    Item item = <span class="hljs-keyword">new</span> Item();
    item.setName(<span class="hljs-string">"name"</span> + i);
    item.setId(i);
    <span class="hljs-keyword">return</span> item;
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Item</span></span>{
    <span class="hljs-keyword">private</span> String name;
    <span class="hljs-keyword">private</span> Integer id;

    Item() {
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">getName</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> name;
    }

    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">setName</span><span class="hljs-params">(String name)</span> </span>{
        <span class="hljs-keyword">this</span>.name = name;
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> Integer <span class="hljs-title">getId</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> id;
    }

    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">setId</span><span class="hljs-params">(Integer id)</span> </span>{
        <span class="hljs-keyword">this</span>.id = id;
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">toString</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-string">"Item{"</span> +
                <span class="hljs-string">"name='"</span> + name + <span class="hljs-string">'\''</span> +
                <span class="hljs-string">", id="</span> + id +
                <span class="hljs-string">'}'</span>;
    }
}

}

class StreamingDemo {
public static void main(String[] args) throws Exception {

    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    <span class="hljs-comment">//获取数据源</span>
    DataStreamSource&lt;MyStreamingSource.Item&gt; text = 
    <span class="hljs-comment">//注意:并行度设置为1,我们会在后面的课程中详细讲解并行度</span>
    env.addSource(<span class="hljs-keyword">new</span> MyStreamingSource()).setParallelism(<span class="hljs-number">1</span>); 
    DataStream&lt;MyStreamingSource.Item&gt; item = text.map(
            (MapFunction&lt;MyStreamingSource.Item, MyStreamingSource.Item&gt;) value -&gt; value);

    <span class="hljs-comment">//打印结果</span>
    item.print().setParallelism(<span class="hljs-number">1</span>);
    String jobName = <span class="hljs-string">"user defined streaming source"</span>;
    env.execute(jobName);
}

}

在自定义的数据源中,实现了 Flink 中的 SourceFunction 接口,同时实现了其中的 run 方法,在 run 方法中每隔一秒钟随机发送一个自定义的 Item。

可以直接运行 main 方法来进行测试:

image (4).png

可以在控制台中看到,已经有源源不断地数据开始输出。下面我们就用自定义的实时数据源来演示 DataStream API 的使用。

Map

Map 接受一个元素作为输入,并且根据开发者自定义的逻辑处理后输出。

image (5).png

class StreamingDemo {
    public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    <span class="hljs-comment">//获取数据源</span>
    DataStreamSource&lt;MyStreamingSource.Item&gt; items = env.addSource(<span class="hljs-keyword">new</span> MyStreamingSource()).setParallelism(<span class="hljs-number">1</span>); 
    <span class="hljs-comment">//Map</span>
    SingleOutputStreamOperator&lt;Object&gt; mapItems = items.map(<span class="hljs-keyword">new</span> MapFunction&lt;MyStreamingSource.Item, Object&gt;() {
        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> Object <span class="hljs-title">map</span><span class="hljs-params">(MyStreamingSource.Item item)</span> <span class="hljs-keyword">throws</span> Exception </span>{
            <span class="hljs-keyword">return</span> item.getName();
        }
    });
    <span class="hljs-comment">//打印结果</span>
    mapItems.print().setParallelism(<span class="hljs-number">1</span>);
    String jobName = <span class="hljs-string">"user defined streaming source"</span>;
    env.execute(jobName);
}

}

我们只取出每个 Item 的 name 字段进行打印。

image (6).png

注意,Map 算子是最常用的算子之一,官网中的表述是对一个 DataStream 进行映射,每次进行转换都会调用 MapFunction 函数。从源 DataStream 到目标 DataStream 的转换过程中,返回的是 SingleOutputStreamOperator。当然了,我们也可以在重写的 map 函数中使用 lambda 表达式。

SingleOutputStreamOperator<Object> mapItems = items.map(
      item -> item.getName()
);

甚至,还可以自定义自己的 Map 函数。通过重写 MapFunction 或 RichMapFunction 来自定义自己的 map 函数。

class StreamingDemo {
    public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    <span class="hljs-comment">//获取数据源</span>
    DataStreamSource&lt;MyStreamingSource.Item&gt; items = env.addSource(<span class="hljs-keyword">new</span> MyStreamingSource()).setParallelism(<span class="hljs-number">1</span>);
    SingleOutputStreamOperator&lt;String&gt; mapItems = items.map(<span class="hljs-keyword">new</span> MyMapFunction());
    <span class="hljs-comment">//打印结果</span>
    mapItems.print().setParallelism(<span class="hljs-number">1</span>);
    String jobName = <span class="hljs-string">"user defined streaming source"</span>;
    env.execute(jobName);
}

<span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyMapFunction</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">RichMapFunction</span>&lt;<span class="hljs-title">MyStreamingSource</span>.<span class="hljs-title">Item</span>,<span class="hljs-title">String</span>&gt; </span>{

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">map</span><span class="hljs-params">(MyStreamingSource.Item item)</span> <span class="hljs-keyword">throws</span> Exception </span>{
        <span class="hljs-keyword">return</span> item.getName();
    }
}

}

此外,在 RichMapFunction 中还提供了 open、close 等函数方法,重写这些方法还能实现更为复杂的功能,比如获取累加器、计数器等。

FlatMap

FlatMap 接受一个元素,返回零到多个元素。FlatMap 和 Map 有些类似,但是当返回值是列表的时候,FlatMap 会将列表“平铺”,也就是以单个元素的形式进行输出。

SingleOutputStreamOperator<Object> flatMapItems = items.flatMap(new FlatMapFunction<MyStreamingSource.Item, Object>() {
    @Override
    public void flatMap(MyStreamingSource.Item item, Collector<Object> collector) throws Exception {
        String name = item.getName();
        collector.collect(name);
    }
});

上面的程序会把名字逐个输出。我们也可以在 FlatMap 中实现更为复杂的逻辑,比如过滤掉一些我们不需要的数据等。

Filter

顾名思义,Fliter 的意思就是过滤掉不需要的数据,每个元素都会被 filter 函数处理,如果 filter 函数返回 true 则保留,否则丢弃。

image (7).png

例如,我们只保留 id 为偶数的那些 item。

SingleOutputStreamOperator<MyStreamingSource.Item> filterItems = items.filter(new FilterFunction<MyStreamingSource.Item>() {
    @Override
    public boolean filter(MyStreamingSource.Item item) throws Exception {
    <span class="hljs-keyword">return</span> item.getId() % <span class="hljs-number">2</span> == <span class="hljs-number">0</span>;
}

});

image (8).png

当然,我们也可以在 filter 中使用 lambda 表达式:

SingleOutputStreamOperator<MyStreamingSource.Item> filterItems = items.filter( 
    item -> item.getId() % 2 == 0
);
KeyBy

在介绍 KeyBy 函数之前,需要你理解一个概念:KeyedStream。 在实际业务中,我们经常会需要根据数据的某种属性或者单纯某个字段进行分组,然后对不同的组进行不同的处理。举个例子,当我们需要描述一个用户画像时,则需要根据用户的不同行为事件进行加权;再比如,我们在监控双十一的交易大盘时,则需要按照商品的品类进行分组,分别计算销售额。

image (9).png

我们在使用 KeyBy 函数时会把 DataStream 转换成为 KeyedStream,事实上 KeyedStream 继承了 DataStream,KeyedStream 中的元素会根据用户传入的参数进行分组。

我们在第 02 课时中讲解的 WordCount 程序,曾经使用过 KeyBy:

    // 将接收的数据进行拆分,分组,窗口计算并且进行聚合输出
        DataStream<WordWithCount> windowCounts = text
                .flatMap(new FlatMapFunction<String, WordWithCount>() {
                    @Override
                    public void flatMap(String value, Collector<WordWithCount> out) {
                        for (String word : value.split("\\s")) {
                            out.collect(new WordWithCount(word, 1L));
                        }
                    }
                })
                .keyBy("word")
                .timeWindow(Time.seconds(5), Time.seconds
                ....

在生产环境中使用 KeyBy 函数时要十分注意!该函数会把数据按照用户指定的 key 进行分组,那么相同分组的数据会被分发到一个 subtask 上进行处理,在大数据量和 key 分布不均匀的时非常容易出现数据倾斜和反压,导致任务失败。

image (10).png

常见的解决方式是把所有数据加上随机前后缀,这些我们会在后面的课时中进行深入讲解。

Aggregations

Aggregations 为聚合函数的总称,常见的聚合函数包括但不限于 sum、max、min 等。Aggregations 也需要指定一个 key 进行聚合,官网给出了几个常见的例子:

keyedStream.sum(0);
keyedStream.sum("key");
keyedStream.min(0);
keyedStream.min("key");
keyedStream.max(0);
keyedStream.max("key");
keyedStream.minBy(0);
keyedStream.minBy("key");
keyedStream.maxBy(0);
keyedStream.maxBy("key");

在上面的这几个函数中,max、min、sum 会分别返回最大值、最小值和汇总值;而 minBy 和 maxBy 则会把最小或者最大的元素全部返回。

我们拿 max 和 maxBy 举例说明:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//获取数据源
List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();
data.add(new Tuple3<>(0,1,0));
data.add(new Tuple3<>(0,1,1));
data.add(new Tuple3<>(0,2,2));
data.add(new Tuple3<>(0,1,3));
data.add(new Tuple3<>(1,2,5));
data.add(new Tuple3<>(1,2,9));
data.add(new Tuple3<>(1,2,11));
data.add(new Tuple3<>(1,2,13));

DataStreamSource<MyStreamingSource.Item> items = env.fromCollection(data);
items.keyBy(0).max(2).printToErr();

//打印结果
String jobName = “user defined streaming source”;
env.execute(jobName);

我们直接运行程序,会发现奇怪的一幕:

image (11).png

从上图中可以看到,我们希望按照 Tuple3 的第一个元素进行聚合,并且按照第三个元素取最大值。结果如我们所料,的确是按照第三个元素大小依次进行的打印,但是结果却出现了一个这样的元素 (0,1,2),这在我们的源数据中并不存在。

我们在 Flink 官网中的文档可以发现:

The difference between min and minBy is that min returns the minimum value, whereas minBy returns the element that has the minimum value in this field (same for max and maxBy).

文档中说:min 和 minBy 的区别在于,min 会返回我们制定字段的最大值,minBy 会返回对应的元素(max 和 maxBy 同理)

网上很多资料也这么写:min 和 minBy 的区别在于 min 返回最小的值,而 minBy 返回最小值的key,严格来说这是不正确的。

min 和 minBy 都会返回整个元素,只是 min 会根据用户指定的字段取最小值,并且把这个值保存在对应的位置,而对于其他的字段,并不能保证其数值正确。max 和 maxBy 同理。

事实上,对于 Aggregations 函数,Flink 帮助我们封装了状态数据,这些状态数据不会被清理,所以在实际生产环境中应该尽量避免在一个无限流上使用 Aggregations。而且,对于同一个 keyedStream ,只能调用一次 Aggregation 函数。

Reduce

Reduce 函数的原理是,会在每一个分组的 keyedStream 上生效,它会按照用户自定义的聚合逻辑进行分组聚合。
image (12).png

例如:

List data = new ArrayList<Tuple3<Integer,Integer,Integer>>();
data.add(new Tuple3<>(0,1,0));
data.add(new Tuple3<>(0,1,1));
data.add(new Tuple3<>(0,2,2));
data.add(new Tuple3<>(0,1,3));
data.add(new Tuple3<>(1,2,5));
data.add(new Tuple3<>(1,2,9));
data.add(new Tuple3<>(1,2,11));
data.add(new Tuple3<>(1,2,13));

DataStreamSource<Tuple3<Integer,Integer,Integer>> items = env.fromCollection(data);
//items.keyBy(0).max(2).printToErr();

SingleOutputStreamOperator<Tuple3<Integer, Integer, Integer>> reduce = items.keyBy(0).reduce(new ReduceFunction<Tuple3<Integer, Integer, Integer>>() {
@Override
public Tuple3<Integer,Integer,Integer> reduce(Tuple3<Integer, Integer, Integer> t1, Tuple3<Integer, Integer, Integer> t2) throws Exception {
Tuple3<Integer,Integer,Integer> newTuple = new Tuple3<>();

    newTuple.setFields(<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,(Integer)t1.getField(<span class="hljs-number">2</span>) + (Integer) t2.getField(<span class="hljs-number">2</span>));
    <span class="hljs-keyword">return</span> newTuple;
}

});

reduce.printToErr().setParallelism(1);

我们对下面的元素按照第一个元素进行分组,第三个元素分别求和,并且把第一个和第二个元素都置为 0:

data.add(new Tuple3<>(0,1,0));
data.add(new Tuple3<>(0,1,1));
data.add(new Tuple3<>(0,2,2));
data.add(new Tuple3<>(0,1,3));
data.add(new Tuple3<>(1,2,5));
data.add(new Tuple3<>(1,2,9));
data.add(new Tuple3<>(1,2,11));
data.add(new Tuple3<>(1,2,13));

那么最终会得到:(0,0,6) 和 (0,0,38)。

总结

这一课时介绍了常用的 API 操作,事实上 DataStream 的 API 远远不止这些,我们在看官方文档的时候要动手去操作验证一下,更为高级的 API 将会在实战课中用到的时候着重进行讲解。

点击这里下载本课程源码

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

办公模板库 素材蛙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值