【无标题】

第一章 Flink基础
课程目标
了解什么是流式计算
了解Flink的简介
掌握Flink环境的搭建
掌握Flink的架构体系
掌握Flink的运行架构

  1. 课程说明
    1.1 框架版本
    https://flink.apache.org/blog/

官宣|Apache Flink 1.15 发布公告-阿里云开发者社区 (aliyun.com)
本课程基于2022年08月25日最新发布的Flink1.15.2版本进行讲解。Flink 1.15 包括了超过 200 名贡献者所提交的 1000 多项修复和优化。在这个版本中,对 Flink 的运维操作进行了简化,使用户能够更加轻松的进行运维。现在 Flink 明确了 Checkpoint 与 Savepoint 在不同作业之间的所属权;更加无缝支持完整的自动伸缩;通过 Watermark 对齐消除多个数据源产出速率不同带来的问题,并且初步支持了在不丢失状态的情况下升级 SQL 作业的能力。

传智教育积极拥抱开源社区,在该版本的升级中也贡献了部分PR:

1.2 编程语言
Flink官方提供了多语言的支持, 如:SQL、Java、Python、Scala 语言接口用以开发Flink应用程序,Flink框架的源码是使用Java语言进行开发的,少部分代码使用到了Scala语言,本次课程中将以SQL和Java语言为主进行Flink的学习讲解。
Apache Flink Documentation | Apache Flink

https://github.com/search?q=Flink

  1. [了解] -流式计算简介
    2.1 数据的时效性
    日常工作中,我们一般会先把数据存储在表,然后对表的数据进行加工、分析。既然先存储在表中,那就会涉及到时效性概念。
    如果我们处理以年,月为单位级别的数据处理,进行统计分析,个性化推荐,那么数据的的最新日期离当前有几个甚至上月都没有问题。但是如果我们处理的是以天为级别,或者一小时甚至更小粒度的数据处理,那么就要求数据的时效性更高了。比如:
    对网站的实时监控
    对异常日志的监控
    这些场景需要工作人员立即响应,这样的场景下,传统的统一收集数据,再存到数据库中,再取出来进行分析就无法满足高时效性的需求了。
    2.2 有界数据流和无界数据流
    有界数据流:有界数据流有明确定义的开始和结束,可以在执行任何计算之前通过获取所有数据来处理有界流,处理有界流不需要有序获取,因为可以始终对有界数据集进行排序。
    无界数据流:有一个开始但是没有结束,不会在生成时终止并提供数据,必须连续处理无界流,也就是说必须在获取后立即处理event。对于无界数据流我们无法等待所有数据都到达,因为输入是无界的,并且在任何时间点都不会完成。

2.3 批处理和流处理
批处理
对有界数据流的处理通常被称为批处理。批处理不需要有序地获取数据。在批处理模式下,首先将数据流持久化到存储系统(文件系统或对象存储)中,然后对整个数据集的数据进行读取、排序、统计或汇总计算,最后输出结果。
实时流处理
对于无界数据流,通常在数据生成时进行实时处理,即实时流处理。因为无界数据流的数据输入是无限的,所以必须持续地处理。数据被获取后需要立刻处理,不可能等到所有数据都到达后再进行处理。处理无界数据流通常要求以特定顺序(如事件发生的顺序)获取事件,以便能够保证推断结果的完整性。
批处理与流处理对比的特点
1)批处理程序的容错不使用检查点。因为数据有限,所以恢复可以通过“完全重播”(重新处理)来实现。不使用检查点的处理方式会降低常规处理的成本;而流处理因为数据是无限的,重新处理所有数据非常困难,所以一般会使用检查点。
2)批处理的特点是有界、持久、大量,非常适合需要访问全套记录才能完成的计算工作,一般用于离线统计;流处理的特点是无界、实时,无需针对整个数据集执行操作,而是对通过系统传输的每个数据项执行操作,一般用于实时统计。
2.4 流式计算和批量计算
上面说到的:统一收集数据->存储到DB->对数据进行批量处理,就是我们说到的批量计算。而流式计算,顾名思义,就是对数据流进行处理,是实时计算。
如下图:左边是Batch Analytics,右边是 Streaming Analytics。

Batch Analysis 就是传统意义上使用类似于 Map Reduce、Hive、Spark Batch 等,对作业进行分析、处理、生成离线报表。
Streaming Analytics 使用流式分析引擎如 Storm,Flink 实时处理分析数据,应用较多的场景如实时大屏、实时报表。

批量计算和流式计算的差别:
与批量计算那样慢慢积累数据不同,流式计算立刻计算,数据持续流动,计算完之后就丢弃。
批量计算是维护一张表,对表进行实施各种计算逻辑。流式计算相反,是必须先定义好计算逻辑,提交到流式计算系统,这个计算作业逻辑在整个运行期间是不可更改的。
计算结果上,批量计算对全部数据进行计算后传输结果,流式计算是每次小批量计算后,结果可以立刻实时化展现。
2.5 流式计算流程和特性
流程:
提交流计算作业
等待流式数据触发流计算作业
计算结果持续不断对外写出
特性:
实时,低延迟
无界,数据是不断输出无终止的
连续,计算连续进行,计算之后数据就会被丢弃
2.6 实时即未来

如今的我们正生活在新一次的信息革命浪潮中,5G、物联网、智慧城市、工业4.0、新基建……等新名词层出不穷,唯一不变的就是变化!对于我们所学习的大数据来说更是这样:数据产生的越来越快、数据量越来越大,数据的来源越来越千变万化,数据中隐藏的价值规律更是越来越被重视!数字化时代的未来正在被我们创造!
历史的发展从来不会一帆风顺,随着大数据时代的发展,海量数据和多种业务的实时处理需求激增,比如:实时监控报警系统、实时风控系统、实时推荐系统等,传统的批处理方式和早期的流式处理框架因其自身的局限性,难以在延迟性、吞吐量、容错能力,以及使用便捷性等方面满足业务日益苛刻的要求。在这种形势下,Flink 以其独特的天然流式计算特性和更为先进的架构设计,极大地改善了以前的流式处理框架所存在的问题。

扩展阅读:为什么说流处理即未来?
为什么说流处理即未来?-阿里云开发者社区 (aliyun.com)
3. [了解] - Flink简介
3.1 Flink的引入
这几年大数据的飞速发展,出现了很多热门的开源社区,其中著名的有 Hadoop、Storm,以及后来的 Spark,他们都有着各自专注的应用场景。Spark 掀开了内存计算的先河,也以内存为赌注,赢得了内存计算的飞速发展。Spark 的火热或多或少的掩盖了其他分布式计算的系统身影。就像 Flink,也就在这个时候默默的发展着。
在国外一些社区,有很多人将大数据的计算引擎分成了 4 代,当然,也有很多人不会认同。我们先姑且这么认为和讨论。
第1代——Hadoop MapReduce
首先第一代的计算引擎,无疑就是 Hadoop 承载的 MapReduce。它将计算分为两个阶段,分别为 Map 和 Reduce。对于上层应用来说,就不得不想方设法去拆分算法,甚至于不得不在上层应用实现多个 Job 的串联,以完成一个完整的算法,例如迭代计算。
批处理
Mapper、Reducer
第2代——DAG框架(Tez) + MapReduce
由于这样的弊端,催生了支持 DAG 框架的产生。因此,支持 DAG 的框架被划分为第二代计算引擎。如 Tez 以及更上层的 Oozie。这里我们不去细究各种 DAG 实现之间的区别,不过对于当时的 Tez 和 Oozie 来说,大多还是批处理的任务。
批处理
1个Tez = MR(1) + MR(2) + … + MR(n)
相比MR效率有所提升

第3代——Spark
接下来就是以 Spark 为代表的第三代的计算引擎。第三代计算引擎的特点主要是 Job 内部的 DAG 支持(不跨越 Job),以及强调的实时计算。在这里,很多人也会认为第三代计算引擎也能够很好的运行批处理的 Job。
批处理、流处理、SQL高层API支持
自带DAG
内存迭代计算、性能较之前大幅提升
第4代——Flink
随着第三代计算引擎的出现,促进了上层应用快速发展,例如各种迭代计算的性能以及对流计算和 SQL 等的支持。Flink 的诞生就被归在了第四代。这应该主要表现在 Flink 对流计算的支持,以及更一步的实时性上面。当然 Flink 也可以支持 Batch 的任务,以及 DAG 的运算。
批处理、流处理、SQL高层API支持
自带DAG
流式计算性能更高、可靠性更高
3.2 什么是Flink
Flink诞生背景
Flink起源于Stratosphere项目,Stratosphere是在2008~2014年由地处柏林的大学和欧洲的一些其他的大学共同进行的研究项目
2014年4月捐赠给了Apache软件基金会
2014年12月成为Apache软件基金会的顶级项目。
LOGO介绍
在德语中,Flink一词表示快速和灵巧,项目采用松鼠的彩色图案作为logo,Flink的松鼠logo尾巴的颜色与Apache软件基金会的logo颜色相呼应,也就是说,这是一只Apache风格的松鼠。

图 Flink Logo
官网地址:
https://flink.apache.org/

Flink概述

Flink是一款分布式的计算引擎,它可以用来做批处理;也可以用来做流处理。其主页在其顶部展示了该项目的理念:“Apache Flink是为分布式、高性能、随时可用以及准确的流处理应用程序打造的开源流处理框架“。

哪些公司在使用Flink

随着人工智能时代的降临,数据量的爆发,在典型的大数据的业务场景下数据业务最通用的做法是:选用批处理的技术处理全量数据,采用流式计算处理实时增量数据。在绝大多数的业务场景之下,用户的业务逻辑在批处理和流处理之中往往是相同的。但是,用户用于批处理和流处理的两套计算引擎是不同的。因此,用户通常需要写两套代码。毫无疑问,这带来了一些额外的负担和成本。阿里巴巴的商品数据处理就经常需要面对增量和全量两套不同的业务流程问题,所以阿里就在想,我们能不能有一套统一的大数据引擎技术,用户只需要根据自己的业务逻辑开发一套代码。这样在各种不同的场景下,不管是全量数据还是增量数据,亦或者实时处理,一套方案即可全部支持,这就是阿里选择 Flink 的背景和初衷。
2015 年阿里巴巴开始使用 Flink 并持续贡献社区(阿里内部还基于Flink做了一套Blink),2019年1月8日,阿里巴巴以 9000 万欧元(7亿元人民币)收购了创业公司 Data Artisans。从此Flink开始了新一轮的乘风破浪。

https://blog.csdn.net/dQCFKyQDXYm3F8rB0/article/details/86117374
3.3 Flink中的批和流
批处理的特点是有界、持久、大量,非常适合需要访问全部记录才能完成的计算工作,一般用于离线统计。 流处理的特点是无界、实时, 无需针对整个数据集执行操作,而是对通过系统传输的每个数据项执行操作,一般用于实时统计。
而在Flink中,一切都是由流组成的,Flink认为有界数据集是无界数据流的一种特例,离线数据是有界限的流,实时数据是一个没有界限的流,这就是所谓的有界流和无界流。
无界流
意思很明显,只有开始没有结束。必须连续的处理无界流数据,也即是在事件注入之后立即要对其进行处理。不能等待数据到达了再去全部处理,因为数据是无界的并且永远不会结束数据注入。处理无界流数据往往要求事件注入的时候有一定的顺序性,例如可以以事件产生的顺序注入,这样会使得处理结果完整。
有界流
也即是有明确的开始和结束的定义。有界流可以等待数据全部注入完成了再开始处理。注入的顺序不是必须的了,因为对于一个静态的数据集,我们是可以对其进行排序的。有界流的处理也可以称为批处理。

3.4 性能比较
首先,我们可以通过下面的性能测试初步了解两个框架的性能区别,它们都可以基于内存计算框架进行实时计算,所以都拥有非常好的计算性能。经过测试,Flink计算性能上略好。
测试环境: 
CPU:7000个; 
内存:单机128GB; 
版本:Hadoop 2.3.0,Spark 1.4,Flink 0.9 
数据:800MB,8GB,8TB; 
算法:K-means:以空间中K个点为中心进行聚类,对最靠近它们的对象归类。通过迭代的方法,逐次更新各聚类中心的值,直至得到最好的聚类结果。 
迭代:K=10,3组数据 
测试结果:
纵坐标是秒,横坐标是次数

Spark和Flink全部都运行在Hadoop YARN上,性能为Flink > Spark > Hadoop(MR),迭代次数越多越明显。性能上,Flink优于Spark和Hadoop最主要的原因是Flink支持增量迭代,具有对迭代自动优化的功能。
3.5 Flink流处理特性
支持高吞吐、低延迟、高性能的流处理
支持带有事件时间的窗口(Window)操作
支持有状态计算的Exactly-once语义
支持高度灵活的窗口(Window)操作,支持基于time、count、session,以及data-driven的窗口操作
支持具有Backpressure功能的持续流模型
支持基于轻量级分布式快照(Snapshot)实现的容错
一个运行时同时支持Batch on Streaming处理和Streaming处理
Flink在JVM内部实现了自己的内存管理
支持迭代计算
支持程序自动优化:避免特定情况下Shuffle、排序等昂贵操作,中间结果有必要进行缓存
3.6 发展历史

2008年,Flink 的前身已经是柏林理工大学一个研究性项目,原名 StratoSphere。
2014-04-16,Flink成为 ASF(Apache Software Foundation)的顶级项目之一,从Stratosphere 0.6开始,正式更名为Flink。由Java语言编写;
2014-11-04,Flink 0.7.0发布,介绍了最重要的特性:Streaming API
2016-03-08,Flink 1.0.0,支持Scala
2019-01-08,阿里巴巴以9000万欧元的价格收购了总部位于柏林的初创公司Data Artisans,也就是Flink的母公司
最新版本已经到了1.15.2

本次课程基于flink-1.15.2开发
3.7 Flink的优势
Flink 通过实现了 Google Dataflow 流式计算模型实现了高吞吐、低延迟、高性能兼具实时流式计算框架。
同时 flink 支持高度容错的状态管理,防止状态在计算过程中因为系统异常而丢失,flink 周期性地通过分布式快照技术 Checkpoints 实现状态的持久化维护,使得即使在系统停机或者异常情况下都能计算出正确的结果。
具体的优势有以下几点
同时支持高吞吐、低延迟、高性能
支持事件时间(Event Time)概念
支持有状态计算
支持高度灵活的窗口(Window)操作
基于轻量级分布式快照(Snapshot)实现的容错
基于 JVM 实现的独立的内存管理
Save Points 保存点
3.8 Flink用武之地
https://flink.apache.org/zh/usecases.html

从很多公司的应用案例发现,其实Flink主要用在如下三大场景:Event-driven Applications(事件驱动)、Data Analytics Applications(数据分析)、Data Pipeline Applications(数据管道)
3.8.1 Event-driven Applications【事件驱动】
事件驱动型应用是一类具有状态的应用,它从一个或多个事件流提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。
事件驱动型应用是在计算存储分离的传统应用基础上进化而来。在传统架构中,应用需要读写远程事务型数据库。相反,事件驱动型应用是基于状态化流处理来完成。在该设计中,数据和计算不会分离,应用只需访问本地(内存或磁盘)即可获取数据。
系统容错性的实现依赖于定期向远程持久化存储写入 checkpoint。下图描述了传统应用和事件驱动型应用架构的区别。

从某种程度上来说,所有的实时的数据处理或者是流式数据处理都应该是属于Data Driven,流计算本质上是Data Driven计算。应用较多的如风控系统,当风控系统需要处理各种各样复杂的规则时,Data Driven就会把处理的规则和逻辑写入到Datastream的API 或者是ProcessFunction 的API 中,然后将逻辑抽象到整个Flink引擎,当外面的数据流或者是事件进入就会触发相应的规则,这就是Data Driven的原理。在触发某些规则后,Data Driven会进行处理或者是进行预警,这些预警会发到下游产生业务通知,这是Data Driven 的应用场景,Data Driven 在应用上更多应用于复杂事件的处理。

典型实例:
欺诈检测(Fraud detection)
异常检测(Anomaly detection)
基于规则的告警(Rule-based alerting)
业务流程监控(Business process monitoring)
Web应用程序(社交网络)

3.8.2 Data Analytics Applications【数据分析】
数据分析任务需要从原始数据中提取有价值的信息和指标。如下图所示,Apache Flink 同时支持流式及批量分析应用。

Data Analytics Applications包含Batch analytics(批处理分析)和Streaming analytics(流处理分析)
Batch analytics可以理解为周期性查询。就是传统意义上使用类似于Map Reduce、Hive、Spark Batch 等,对作业进行分析、处理、生成离线报表。比如Flink应用凌晨从Recorded Events中读取昨天的数据,然后做周期查询运算,最后将数据写入Database或者HDFS,或者直接将数据生成报表供公司上层领导决策使用。
Streaming analytics可以理解为连续性查询。比如实时展示双十一天猫销售GMV(Gross Merchandise Volume成交总额),用户下单数据需要实时写入消息队列,Flink 应用源源不断读取数据做实时计算,然后不断的将数据更新至Database或者K-VStore,最后做大屏实时展示。

典型实例
电信网络质量监控
移动应用中的产品更新及实验评估分析
消费者技术中的实时数据即席分析
大规模图分析

3.8.3 Data Pipeline Applications【数据管道】
什么是数据管道?
提取-转换-加载(ETL)是一种在存储系统之间进行数据转换和迁移的常用方法。
ETL 作业通常会周期性地触发,将数据从事务型数据库拷贝到分析型数据库或数据仓库。
数据管道和 ETL 作业的用途相似,都可以转换、丰富数据,并将其从某个存储系统移动到另一个。
但数据管道是以持续流模式运行,而非周期性触发。因此数据管道支持从一个不断生成数据的源头读取记录,并将它们以低延迟移动到终点。例如:数据管道可以用来监控文件系统目录中的新文件,并将其数据写入事件日志;另一个应用可能会将事件流物化到数据库或增量构建和优化查询索引。
和周期性 ETL 作业相比,持续数据管道可以明显降低将数据移动到目的端的延迟。
此外,由于它能够持续消费和发送数据,因此用途更广,支持用例更多。

下图描述了周期性ETL作业和持续数据管道的差异。

Periodic ETL:比如每天凌晨周期性的启动一个Flink ETL Job,读取传统数据库中的数据,然后做ETL,最后写入数据库和文件系统。
Data Pipeline:比如启动一个Flink 实时应用,数据源(比如数据库、Kafka)中的数据不断的通过Flink Data Pipeline流入或者追加到数据仓库(数据库或者文件系统),或者Kafka消息队列。
Data Pipeline 的核心场景类似于数据搬运并在搬运的过程中进行部分数据清洗或者处理,而整个业务架构图的左边是Periodic ETL,它提供了流式ETL 或者实时ETL,能够订阅消息队列的消息并进行处理,清洗完成后实时写入到下游的Database或File system 中。

典型实例
电子商务中的持续 ETL(实时数仓)
当下游要构建实时数仓时,上游则可能需要实时的Stream ETL。这个过程会进行实时清洗或扩展数据,清洗完成后写入到下游的实时数仓的整个链路中,可保证数据查询的时效性,形成实时数据采集、实时数据处理以及下游的实时Query。
电子商务中的实时查询索引构建(搜索引擎推荐)
搜索引擎这块以淘宝为例,当卖家上线新商品时,后台会实时产生消息流,该消息流经过Flink 系统时会进行数据的处理、扩展。然后将处理及扩展后的数据生成实时索引,写入到搜索引擎中。这样当淘宝卖家上线新商品时,能在秒级或者分钟级实现搜索引擎的搜索。

  1. 【重点】Flink架构体系
    4.1 [理解] - Flink中的重要角⾊

JobManager处理器
JobManager处理器也称之为Master,用于协调分布式执行,它们用来调度task,协调检查点,协调失败时恢复等。Flink运行时至少存在一个master处理器,如果配置高可用模式则会存在多个master处理器,它们其中有一个是leader,而其他的都是standby。
TaskManager处理器
TaskManager处理器也称之为Worker,用于执行一个dataflow的task(或者特殊的subtask)、数据缓冲和data stream的交换,Flink运行时至少会存在一个worker处理器。
Slot 任务执行槽位:
物理概念,一个TM(TaskManager)内会划分出多个Slot,1个Slot内最多可以运行1个Task(Subtask)或一组由Task(Subtask)组成的任务链。
多个Slot之间会共享平分当前TM的内存空间。Slot是对一个TM的资源进行固定分配的工具,每个Slot在TM启动后,可以获得固定的资源。比如1个TM是一个JVM进程,如果有6个Slot,那么这6个Slot平分这一个JVM进程的资源,但是因为在同一个进程内,所有线程之间共享TCP连接、内存数据等,效率更高(Slot之间交流方便)。
Task:
任务,每一个Flink的Job会根据情况(并行度、算子类型)将一个整体的Job划分为多个Task。
Subtask:
子任务,一个Task可以由一个或者多个Subtask组成。一个Task有多少个Subtask取决于这个Task的并行度,也就是,每一个Subtask就是当前Task任务并行的一个线程。如,当前Task并行度为8,那么这个Task会有8个Subtask(8个线程并行执行这个Task)。
并行度:
并行度就是一个Task可以分成多少个Subtask并行执行的一个参数。这个参数是动态的,可以在任务执行前进行分配,而非Slot分配,TM启动就固定了。
一个Task可以获得的最大并行度取决于整个Flink环境的可用Slot数量,也就是如果有8个Slot,那么最大并行度也就是8,设置的再大也没有意义。
如下图:
一个Job分为了3个Task来运行,分别是TaskA TaskB TaskC
其中TaskA设置为了6个并行度,也就是TaskA可以有6个Subtask,如图可见,TaskA的6个Subtask各自在一个Slot内执行
其中在Slot的时候说过,Slot可以运行由Task(或Subtask)组成的任务链,如图可见,最左边的Slot运行了TaskA TaskB TaskC 3个Task各自的1个Subtask组成的一个Subtask执行链

并行度是一个动态的概念,可以在多个地方设置并行度:
配置文件默认并行度:conf/flink-conf.yaml的parallelism.default
启动Flink任务,动态提交参数:比如:bin/flink run -p 3
在代码中设置全局并行度:env.setParallelism(3); 
针对每个算子进行单独设置:sum(1).setParallelism(3)
优先级:算子 > 代码全局 > 命令行参数 > 配置文件
4.2 [理解] - Flink数据流编程模型
Flink 提供了不同的抽象级别以开发流式或批处理应用。

最顶层:SQL/Table API 提供了操作关系表、执行SQL语句分析的API库,供我们方便的开发SQL相关程序
中层:流和批处理API层,提供了一系列流和批处理的API和算子供我们对数据进行处理和分析
最底层:运行时层,提供了对Flink底层关键技术的操纵,如对Event、state、time、window等进行精细化控制的操作API
4.3 [了解] - Libraries支持
支持机器学习(FlinkML)(Alink)
支持图分析(Gelly)
支持关系数据处理(Table)
支持复杂事件处理(CEP)
5. Flink集群搭建
Flink支持多种安装模式。
local(本地)——本地模式
standalone——独立模式,Flink自带集群,开发测试环境使用
standaloneHA—独立集群高可用模式,Flink自带集群,开发测试环境使用
yarn——计算资源统一由Hadoop YARN管理,生产环境测试

5.1 [了解] - Standalone - 伪分布环境(开发测试)
和Local模式不同的是,Standalone模式中Flink的各个角色都是独立的进程。
5.1.1 架构图

Flink程序需要提交给JobClient
JobClient将作业提交给JobManager
JobManager负责协调资源分配和作业执行。 资源分配完成后,任务将提交给相应的TaskManager
TaskManager启动一个线程以开始执行。TaskManager会向JobManager报告状态更改。例如开始执行,正在进行或已完成。
作业执行完成后,结果将发送回客户端(JobClient)
5.1.2 环境准备
jdk11及以上【配置JAVA_HOME环境变量】
ssh免密码登录【集群内节点之间免密登录】
5.1.3 下载安装包
https://dlcdn.apache.org/flink/flink-1.15.2/flink-1.15.2-bin-scala_2.12.tgz
5.1.4 服务器规划
服务器: node1(Master +Worker)
5.1.5 安装步骤
操作步骤 说明
1 上传Flink压缩包到指定目录
2 解压缩flink到 /export/server 目录
tar -zxvf flink-1.15.2-bin-scala_2.12.tgz -C /export/server/
3 改名或创建软链接:方便后期升级
cd /export/server
ln -s flink-1.15.2/ /export/server/flink
4 修改配置文件flink-conf.yaml
cd /export/server/flink
vim conf/flink-conf.yaml
将下面两个参数注释掉或者值改为node1(这里我们采取第一种注释掉)

5 启动Flink
bin/start-cluster.sh
6 通过jps查看进程信息

7 访问web界面
http://node1:8081

slot在flink里面可以认为是资源组,Flink是通过将任务分成子任务并且将这些子任务分配到slot来并行执行程序

8 运行测试任务
bin/flink run /export/server/flink/examples/batch/WordCount.jar
9 观察WebUI

10 日志的查看
JobManager 和 TaskManager 的启动日志可以在 Flink binary 目录下的 log 子目录中找到

log 目录中以“flink-${user}-standalonesession-${id}-${hostname}”为前缀的文件对应的即是 JobManager 的输出,其中有三个文件:
flink-${user}-standalonesession-${id}-${hostname}.log:代码中的日志输出 flink-${user}-standalonesession-${id}-${hostname}.out:进程执行时的 stdout 输出 

flink- u s e r − s t a n d a l o n e s e s s i o n − {user}-standalonesession- userstandalonesession{id}- h o s t n a m e − g c . l o g : J V M 的 G C 的 日 志 l o g 目 录 中 以 “ f l i n k − {hostname}-gc.log:JVM 的 GC 的日志 log 目录中以“flink- hostnamegc.logJVMGClogflink{user}-taskexecutor- i d − {id}- id{hostname}”为前缀的文件对应的是 TaskManager 的输出,也包括三个文件,和 JobManager 的输出一致。
11 日志的配置文件在 Flink binary 目录的 conf 子目录下:

log4j-cli.properties:用 Flink 命令行时用的 log 配置,比如执行“flink run”命令 

log4j-console.properties:JobManagers/TaskManagers 在前台模式运行时使用(例如 Kubernetes);
log4j-session.properties:是用 yarn-session.sh或Kubernetes session时启动时命令行执行时用的 log 配置
log4j.properties:无论是 standalone 还是 yarn 模式,JobManager 和 TaskManager 上用 的 log 配置都是 log4j.properties
这三个“log4j.*properties”文件分别有三个“logback.*xml”文件与之对应:
log4j-console.properties -> logback-console.xml
log4j-session.properties -> logback-session.xml
log4j.properties -> logback.xml
如果想使用 logback 的同学, 需要
从 lib 目录中移除 log4j-slf4j-impl jars;
向 lib 目录中添加 logback-core 和 logback-classic jars。
如果启用了 logback,则会自动使用”logback.*xml”这些文件
需要注意的是,“flink- u s e r − s t a n d a l o n e s e s s i o n − {user}-standalonesession- userstandalonesession{id}- h o s t n a m e ” 和 “ f l i n k − {hostname}”和“flink- hostnameflink{user}- taskexecutor- i d − {id}- id{hostname}”都带有“ i d ” , “ {id}”,“ id{id}”表示本进程在本机上该角色(JobManager 或 TaskManager)的所有进程中的启动顺序,默认从 0 开始。
5.2 [理解] - yarn集群环境(生产推荐)
Local模式:通过一个JVM进程中,通过线程模拟出各个Flink角色来得到Flink环境
Standalone模式:各个角色是独立的进程存在
YARN模式:Flink的各个角色,均运行在多个YARN的容器内,其整体上是一个YARN的任务

flink on yarn的前提是:hdfs、yarn均启动
在企业实际开发中,使用Flink时,更多的使用方式是Flink On Yarn模式,原因如下:
Yarn的资源可以按需使用,提高集群的资源利用率
Yarn的任务有优先级,根据优先级运行作业
基于Yarn调度系统,能够自动化地处理各个角色的 Failover(容错)
JobManager 进程和 TaskManager 进程都由 Yarn NodeManager 监控
如果 JobManager 进程异常退出,则 Yarn ResourceManager 会重新调度 JobManager 到其他机器
如果 TaskManager 进程异常退出,JobManager 会收到消息并重新向 Yarn ResourceManager 申请资源,重新启动 TaskManager
5.2.1 准备工作
jdk1.8及以上(推荐jdk11)【配置JAVA_HOME环境变量】
ssh免密码登录【集群内节点之间免密登录】
至少hadoop2.8.5
hdfs & yarn均启动
5.2.2 集群规划
服务器: node1(Master +Worker)
服务器: node2(Worker)
服务器: node3(Worker)
5.2.3 修改hadoop的配置参数
操作步骤 说明
1 打开yarn配置页面(每台hadoop节点都需要修改)
vim /export/server/hadoop/etc/hadoop/yarn-site.xml
添加

yarn.nodemanager.vmem-check-enabled
false

是否启动一个线程检查每个任务正使用的虚拟内存量,如果任务超出分配值,则直接将其杀掉,默认是true。
在这里面我们需要关闭,因为对于flink使用yarn模式下,很容易内存超标,这个时候yarn会自动杀掉job
2 分发yarn-site.xml到其它服务器节点
scp yarn-site.xml node2: P W D s c p y a r n − s i t e . x m l n o d e 3 : PWD scp yarn-site.xml node3: PWDscpyarnsite.xmlnode3:PWD
3 启动ZK、HDFS、YARN集群
[root@node1 ~]# /export/server/zookeeper/bin/zkServer.sh start
[root@node2 ~]# /export/server/zookeeper/bin/zkServer.sh start
[root@node3 ~]# /export/server/zookeeper/bin/zkServer.sh start

[root@node1 ~]#start-zookeeper-quorum.sh
start-all.sh
5.2.4 Flink on Yarn的运行机制

从图中可以看出,Yarn的客户端需要获取hadoop的配置信息,连接Yarn的ResourceManager。所以要有设置有 YARN_CONF_DIR或者HADOOP_CONF_DIR或者HADOOP_CONF_PATH,只要设置了其中一个环境变量,就会被读取。如果读取上述的变量失败了,那么将会选择hadoop_home的环境变量,读取成功将会尝试加载$HADOOP_HOME/etc/hadoop的配置文件。
当启动一个Flink Yarn会话时,客户端首先会检查本次请求的资源是否足够。资源足够将会上传包含HDFS配置信息和Flink的jar包到HDFS。
随后客户端会向Yarn发起请求,启动applicationMaster,随后NodeManager将会加载有配置信息和jar包,一旦完成,ApplicationMaster(AM)便启动。
当JobManager and AM 成功启动时,他们都属于同一个container,从而AM就能检索到JobManager的地址。此时会生成新的Flink配置信息以便TaskManagers能够连接到JobManager。同时,AM也提供Flink的WEB接口。用户可并行执行多个Flink会话。
随后,AM将会开始为分发从HDFS中下载的jar以及配置文件的container给TaskMangers.完成后Fink就完全启动并等待接收提交的job.
5.2.5 Flink on Yarn的三种部署方式介绍
5.2.5.1 Session模式
这种模式会预先在yarn或者或者k8s上启动一个flink集群,然后将任务提交到这个集群上,这种模式,集群中的任务使用相同的资源,如果某一个任务出现了问题导致整个集群挂掉,那就得重启集群中的所有任务,这样就会给集群造成很大的负面影响。

特点:需要事先申请资源,使用Flink中的yarn-session(yarn客户端),启动JobManager和TaskManger
优点:不需要每次递交作业申请资源,而是使用已经申请好的资源,从而提高执行效率
缺点:作业执行完成以后,资源不会被释放,因此一直会占用系统资源
应用场景:适合作业递交比较频繁的场景,小作业比较多的场景
5.2.5.2 Per-Job模式
考虑到集群的资源隔离情况,一般生产上的任务都会选择per job模式,也就是每个任务启动一个flink集群,各个集群之间独立运行,互不影响,且每个集群可以设置独立的配置。

特点:每次递交作业都需要申请一次资源
优点:作业运行完成,资源会立刻被释放,不会一直占用系统资源
缺点:每次递交作业都需要申请资源,会影响执行效率,因为申请资源需要消耗时间
应用场景:适合作业比较少的场景、大作业的场景
5.2.5.3 application模式
5.2.5.3.1 背景
flink-1.11 引入了一种新的部署模式,即 Application 模式。目前,flink-1.11 已经可以支持基于 Yarn 和 Kubernetes 的 Application 模式。
5.2.5.3.2 优势

Session模式:所有作业共享集群资源,隔离性差,JM 负载瓶颈,main 方法在客户端执行。
Per-Job模式:每个作业单独启动集群,隔离性好,JM 负载均衡,main 方法在客户端执行。
通过以上两种模式的特点描述,可以看出,main方法都是在客户端执行,社区考虑到在客户端执行 main() 方法来获取 flink 运行时所需的依赖项,并生成 JobGraph,提交到集群的操作都会在实时平台所在的机器上执行,那么将会给服务器造成很大的压力。尤其在大量用户共享客户端时,问题更加突出。
此外这种模式提交任务的时候会把本地flink的所有jar包先上传到hdfs上相应的临时目录,这个也会带来大量的网络的开销,所以如果任务特别多的情况下,平台的吞吐量将会直线下降。
因此,社区提出新的部署方式 Application 模式解决该问题。
5.2.5.3.3 原理

Application 模式下,用户程序的 main 方法将在集群中而不是客户端运行,用户将程序逻辑和依赖打包进一个可执行的 jar 包里,集群的入口程序 (ApplicationClusterEntryPoint) 负责调用其中的 main 方法来生成 JobGraph。Application 模式为每个提交的应用程序创建一个集群,该集群可以看作是在特定应用程序的作业之间共享的会话集群,并在应用程序完成时终止。在这种体系结构中,Application 模式在不同应用之间提供了资源隔离和负载平衡保证。在特定一个应用程序上,JobManager 执行 main() 可以节省所需的 CPU 周期,还可以节省本地下载依赖项所需的带宽。
5.2.6 Flink on Yarn的三种部署方式使用说明
5.2.6.1 第一种方式:YARN session
操作步骤 说明
1 yarn-session.sh(开辟资源)+flink run(提交任务)
这种模式下会启动yarn session,并且会启动Flink的两个必要服务:JobManager和Task-managers,然后你可以向集群提交作业。同一个Session中可以提交多个Flink作业。需要注意的是,这种模式下Hadoop的版本至少是2.2,而且必须安装了HDFS(因为启动YARN session的时候会向HDFS上提交相关的jar文件和配置文件)
通过./bin/yarn-session.sh脚本启动YARN Session
脚本可以携带的参数:
-n(–container):TaskManager的数量。(1.10 已经废弃)
-s(–slots): 每个TaskManager的slot数量,默认一个slot一个core,默认每个taskmanager的slot的个数为1,有时可以多一些taskmanager,做冗余。
-jm:JobManager的内存(单位MB)。
-q:显示可用的YARN资源(内存,内核);
-tm:每个TaskManager容器的内存(默认值:MB)
-nm:yarn 的appName(现在yarn的ui上的名字)。
-d:后台执行。
注意:
如果不想让Flink YARN客户端始终运行,那么也可以启动分离的 YARN会话。该参数被称为-d或–detached。
确定TaskManager数:
Flink on YARN时,TaskManager的数量就是:max(parallelism) / yarnslots(向上取整)。例如,一个最大并行度为10,每个TaskManager有两个任务槽的作业,就会启动5个TaskManager。
2 启动:
yarn-session.sh -tm 1024 -s 4 -d
上面的命令的意思是,每个 TaskManager 拥有4个 Task Slot(-s 4),并且被创建的每个 TaskManager 所在的YARN Container 申请 1024M 的内存,同时额外申请一个Container用以运行ApplicationMaster以及Job Manager。
TM的数量取决于并行度,如下图:
执行:flink run -p 8 examples/batch/WordCount.jar

3 启动成功之后,控制台显示:

4 去yarn页面:ip:8088可以查看当前提交的flink session

5 然后使用flink提交任务
flink run /export/server/flink/examples/batch/WordCount.jar
在控制台中可以看到wordCount.jar计算出来的任务结果

6 在yarn-session.sh提交后的任务页面中也可以观察到当前提交的任务:

7 点击查看任务细节:

8 停止当前任务(推荐第一种):
echo “stop” | yarn-session.sh -id application_1662342426082_0001
yarn application -kill application_1662342426082_0001
推荐一种关闭方式
会话模式将在/tmp中创建一个隐藏的 YARN 属性文件,提交作业时,命令行界面将选取该文件以进行群集发现。/tmp/.yarn-properties-。
采用一种方式,会自动删除该文件,如果采用kill直接杀掉该任务,不会删除该隐藏文件。
5.2.6.2 第二种方式:在YARN上运行一个Flink作业
上面的YARN session是在Hadoop YARN环境下启动一个Flink cluster集群,里面的资源是可以共享给其他的Flink作业。我们还可以在YARN上启动一个Flink作业,这里我们还是使用./bin/flink,但是不需要事先启动YARN session:
使用flink直接提交任务
flink run -m yarn-cluster /export/server/flink/examples/batch/WordCount.jar
常用参数:
–p 程序默认并行度
下面的参数仅可用于 -m yarn-cluster 模式
–yjm JobManager可用内存,单位兆
–ynm YARN程序的名称
–yq 查询YARN可用的资源
–yqu 指定YARN队列是哪一个
–ys 每个TM会有多少个Slot
–ytm 每个TM所在的Container可申请多少内存,单位兆
–yD 动态指定Flink参数
–yd 分离模式(后台运行,不指定-yd, 终端会卡在提交的页面上)
在8088页面观察:

停止yarn-cluster
该模式正常状态,完成任务后会自动关闭集群
手动关闭
yarn application -kill application的ID
注意:
在创建集群的时候,集群的配置参数就写好了,但是往往因为业务需要,要更改一些配置参数,这个时候可以不必因为一个实例的提交而修改conf/flink-conf.yaml;
可以通过:-yD Dynamic properties
来覆盖原有的配置信息:比如:
flink run
-m yarn-cluster
examples/batch/WordCount.jar
-yD fs.overwrite-files=true \
-yD taskmanager.network.numberOfBuffers=16368
5.2.6.3 第三种方式:Application Mode
application 模式使用 flink run-application 提交作业;通过 -t 指定部署环境,目前 application 模式支持部署在 yarn 上(-t yarn-application) 和 k8s 上(-t kubernetes-application);并支持通过 -D 参数指定通用的 运行配置,比如 jobmanager/taskmanager 内存、checkpoint 时间间隔等。
通过 flink run-application -h 可以看到 -D/-t 的详细说明:(-e 已经被废弃,可以忽略)
flink run-application -h 
参数:

下面列举几个使用 Application 模式提交作业到 yarn 上运行的命令:
第一种方式 带有 JM 和 TM 内存设置的命令提交:
flink run-application -t yarn-application
-Djobmanager.memory.process.size=1024m
-Dtaskmanager.memory.process.size=1024m
-Dyarn.application.name=“MyFlinkWordCount”
/export/server/flink/examples/batch/WordCount.jar --output hdfs://node1:8020/wordcount/output_51

查看hdfs

第二种方式 在上面例子的基础上自己设置 TaskManager slots 个数为3,以及指定并发数为3:
flink run-application -t yarn-application -p 3
-Djobmanager.memory.process.size=1024m
-Dtaskmanager.memory.process.size=1024m
-Dyarn.application.name=“MyFlinkWordCount”
-Dtaskmanager.numberOfTaskSlots=3
/export/server/flink/examples/batch/WordCount.jar --output hdfs://node1:8020/wordcount/output_52

当然,指定并发还可以使用 -Dparallelism.default=3,而且社区目前倾向使用 -D+通用配置代替客户端命令参数(比如 -p)。所以这样写更符合规范:
flink run-application -t yarn-application \

-Dparallelism.default=3
-Djobmanager.memory.process.size=1024m
-Dtaskmanager.memory.process.size=1024m
-Dyarn.application.name=“MyFlinkWordCount”
-Dtaskmanager.numberOfTaskSlots=3
/export/server/flink/examples/batch/WordCount.jar --output hdfs://node1:8020/wordcount/output_53
第三种方式 和 yarn.provided.lib.dirs 参数一起使用,可以充分发挥 application 部署模式的优势:我们看官方配置文档 对这个配置的解释:
yarn.provided.lib.dirs: A semicolon-separated list of provided lib directories. They should be pre-uploaded and world-readable. Flink will use them to exclude the local Flink jars(e.g. flink-dist, lib/, plugins/)uploading to accelerate the job submission process. Also YARN will cache them on the nodes so that they doesn’t need to be downloaded every time for each application. An example could be hdfs://$namenode_address/path/of/flink/lib
意思是我们可以预先上传 flink 客户端依赖包 (flink-dist/lib/plugin) 到远端存储(一般是 hdfs,或者共享存储),然后通过 yarn.provided.lib.dirs 参数指定这个路径,flink 检测到这个配置时,就会从该地址拉取 flink 运行需要的依赖包,省去了依赖包上传的过程,yarn-cluster/per-job 模式也支持该配置。在之前的版本中,使用 yarn-cluster/per-job 模式,每个作业都会单独上传 flink 依赖包(一般会有 180MB左右)导致 hdfs 资源浪费,而且程序异常退出时,上传的 flink 依赖包往往得不到自动清理。通过指定 yarn.provided.lib.dirs,所有作业都会使用一份远端 flink 依赖包,并且每个 yarn nodemanager 都会缓存一份,提交速度也会大大提升,对于跨机房提交作业会有很大的优化。
使用示例如下:
my-application.jar 是用户 jar 包
上传 Flink 相关 plugins 到hdfs
cd /export/server/flink/plugins
hdfs dfs -mkdir /flink/plugins
hdfs dfs -put
external-resource-gpu/flink-external-resource-gpu-1.15.2.jar
metrics-datadog/flink-metrics-datadog-1.15.2.jar
metrics-graphite/flink-metrics-graphite-1.15.2.jar
metrics-influx/flink-metrics-influxdb-1.15.2.jar
metrics-jmx/flink-metrics-jmx-1.15.2.jar
metrics-prometheus/flink-metrics-prometheus-1.15.2.jar
metrics-slf4j/flink-metrics-slf4j-1.15.2.jar
metrics-statsd/flink-metrics-statsd-1.15.2.jar
/flink/plugins

根据自己业务需求上传相关的 jar
cd /export/server/flink/lib

hdfs dfs -mkdir /flink/lib
hdfs dfs -put
./*
/flink/lib

上传用户需要运行的作业jar 到 hdfs
cd /export/server/flink

hdfs dfs -mkdir /flink/user-libs
hdfs dfs -put ./examples/batch/WordCount.jar /flink/user-libs
提交任务
flink run-application -t yarn-application
-Djobmanager.memory.process.size=1024m
-Dtaskmanager.memory.process.size=1024m
-Dtaskmanager.numberOfTaskSlots=2
-Dparallelism.default=2
-Dyarn.provided.lib.dirs=“hdfs://node1.itcast.cn:8020/flink/lib;hdfs://node1.itcast.cn:8020/flink/plugins”
-Dyarn.application.name=“batchWordCount”
hdfs://node1.itcast.cn:8020/flink/user-libs/WordCount.jar
–output hdfs://node1:8020/wordcount/output_54

也可以将 yarn.provided.lib.dirs 配置到 conf/flink-conf.yaml,这时提交作业就和普通作业没有区别了:
/flink run-application -t yarn-application \

-Djobmanager.memory.process.size=1024m
-Dtaskmanager.memory.process.size=1024m
-Dyarn.application.name=“MyFlinkWordCount” \
-Dtaskmanager.numberOfTaskSlots=3
/local/path/to/my-application.jar
注意:如果自己指定 yarn.provided.lib.dirs,有以下注意事项:
需要将 lib 包和 plugins 包地址用;分开,从上面的例子中也可以看到,将 plugins 包放在 lib 目录下可能会有包冲突错误
plugins 包路径地址必须以 plugins 结尾,例如上面例子中的 hdfs://node1.itcast.cn:8020/flink/plugins
hdfs 路径必须指定 nameservice(或 active namenode 地址),而不能使用简化方式(例如 hdfs://node1.itcast.cn:8020/flink/libs)
该种模式的操作使得 flink 作业提交变得很轻量,因为所需的 Flink jar 包和应用程序 jar 将到指定的远程位置获取,而不是由客户端下载再发送到集群。这也是社区在 flink-1.11 版本引入新的部署模式的意义所在。
Application 模式在停止、取消或查询正在运行的应用程序的状态等方面和 flink-1.11 之前的版本一样,可以采用现有的方法。

5.2.7 注意
如果使用的是flink on yarn方式,想切换回standalone模式的话,需要删除文件:【/tmp/.yarn-properties-】因为默认查找当前yarn集群中已有的yarn-session信息中的jobmanager
如果是分离模式运行的YARN JOB后,其运行完成会自动删除这个文件
但是会话模式的话,如果是kill掉任务,其不会执行自动删除这个文件的步骤,所以需要我们手动删除这个文件。

  1. [掌握] -入门案例
    6.1 Flink 数据流
    在 Flink 中,应用程序由数据流组成,这些数据流可以由用户定义的运算符(注:有时我们称这些运算符为“算子”)进行转换。这些数据流形成有向图,从一个或多个源开始,以一个或多个输出结束。

Flink 支持流处理和批处理,它是一个分布式的流批结合的大数据处理引擎。在 Flink 中,认为所有的数据本质上都是随时间产生的流数据,把批数据看作是流数据的特例,只不过流数据是一个无界的数据流,而批数据是一个有界的数据流(例如固定大小的数据集)。如下图所示:

因此,Flink 是一个用于在无界和有界数据流上进行有状态计算的通用的处理框架,它既具有处理无界流的复杂功能,也具有专门的运算符来高效地处理有界流。通常我们称无界数据为实时数据,来自消息队列或分布式日志等流源(如 Apache Kafka 或 Kinesis)。而有界数据,通常指的是历史数据,来自各种数据源(如文件、关系型数据库等)。由 Flink 应用程序产生的结果流可以发送到各种各样的系统,并且可以通过 REST API 访问 Flink 中包含的状态。

当 Flink 处理一个有界的数据流时,就是采用的批处理工作模式。在这种操作模式中,我们可以选择先读取整个数据集,然后对数据进行排序、计算全局统计数据或生成总结所有输入的最终报告。当 Flink 处理一个无界的数据流时,就是采用的流处理工作模式。对于流数据处理,输入可能永远不会结束,因此我们必须在数据到达时持续不断地对这些数据进行处理。
6.2 Flink 分层 API
Flink 提供了开发流/批处理应用程序的API供开发者使用,越往上抽象程度越高,使用起来越方便;越往下越底层,使用起来难度越大,如下图所示:

Flink 提供了三个分层的 API。每个 API 在简洁性和表达性之间提供了不同的权衡,并针对不同的应用场景。
Flink API 最底层的抽象为有状态实时流处理。
其抽象实现是 Process Function,并且 Process Function 被 Flink 框架集成到了 DataStream API 中来为我们使用。它允许用户在应用程序中自由地处理来自单流或多流的事件(数据),并提供具有全局一致性和容错保障的状态。此外,用户可以在此层抽象中注册事件时间(event time)和处理时间(processing time)回调方法,从而允许程序可以实现复杂计算。
Flink API 第二层抽象是 Core APIs。
实际上,许多应用程序不需要使用到上述最底层抽象的 API,而是可以使用 Core APIs 进行编程:其中包含 DataStream API(应用于有界/无界数据流场景)和 DataSet API(应用于有界数据集场景)两部分。Core APIs 提供的流式 API(Fluent API)为数据处理提供了通用的模块组件,例如各种形式的用户自定义转换(transformations)、联接(joins)、聚合(aggregations)、窗口(windows)和状态(state)操作等。此层 API 中处理的数据类型在每种编程语言中都有其对应的类。
Process Function 这类底层抽象和 DataStream API 的相互集成使得用户可以选择使用更底层的抽象 API 来实现自己的需求。DataSet API 还额外提供了一些原语,比如循环/迭代(loop/iteration)操作。
Flink API 第三层抽象是 Table API。
Table API 是以表(Table)为中心的声明式编程(DSL)API,例如在流式数据场景下,它可以表示一张正在动态改变的表。Table API 遵循(扩展)关系模型:即表拥有 schema(类似于关系型数据库中的 schema),并且 Table API 也提供了类似于关系模型中的操作,比如 select、project、join、group-by 和 aggregate 等。Table API 程序是以声明的方式定义应执行的逻辑操作,而不是确切地指定程序应该执行的代码。尽管 Table API 使用起来很简洁并且可以由各种类型的用户自定义函数扩展功能,但还是比 Core API 的表达能力差。此外,Table API 程序在执行之前还会使用优化器中的优化规则对用户编写的表达式进行优化。
表和 DataStream/DataSet 可以进行无缝切换,Flink 允许用户在编写应用程序时将 Table API 与 DataStream/DataSet API 混合使用。
Flink API 最顶层抽象是 SQL。
这层抽象在语义和程序表达式上都类似于 Table API,但是其程序实现都是 SQL 查询表达式。SQL 抽象与 Table API 抽象之间的关联是非常紧密的,并且 SQL 查询语句可以在 Table API 中定义的表上执行。

注意:
在Flink1.12时支持流批一体,DataSetAPI已经不推荐使用了,所以课程中除了个别案例使用DataSet外,后续其他案例都会优先使用DataStream流式API,既支持无界数据处理/流处理,也支持有界数据处理/批处理!当然Table&SQL-API会单独学习
概览 | Apache Flink
6.3 Flink流处理程序的一般流程
获取Flink流处理执行环境
构建source
数据处理
构建sink
6.4 搭建Flink工程
6.4.1 创建Maven项目
创建maven项目,项目名称:flinkbase

6.4.2 导入pom依赖

aliyun http://maven.aliyun.com/nexus/content/groups/public/
<repository>
    <id>apache</id>
    <url>https://repository.apache.org/content/repositories/snapshots/</url>
</repository>

<repository>
    <id>cloudera</id>
    <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
</repository>
org.scala-tools maven-scala-plugin ${scala.version}
<!-- Apache Flink 的依赖 -->
<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-scala_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

<!-- 用于通过自定义功能,格式等扩展表生态系统的通用模块-->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-cep</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-api-java</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-runtime</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-common</artifactId>
    <version>${flink.version}</version>
</dependency>

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

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-queryable-state-runtime</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-api-java-bridge</artifactId>
    <version>${flink.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-api-scala-bridge_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-planner_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

<!-- web ui的依赖 -->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-runtime-web</artifactId>
    <version>${flink.version}</version>
</dependency>

<!-- flink连接器-->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-csv</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-json</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-parquet</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-files</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-jdbc</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-sql-connector-kafka</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-base</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>${kafka.version}</version>
</dependency>


<!-- hadoop相关依赖 -->
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-hadoop-compatibility_2.12</artifactId>
    <version>${flink.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-client</artifactId>
    <version>${hadoop.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-shaded-hadoop-3-uber</artifactId>
    <version>${flink-shaded-hadoop.version}</version>
</dependency>


<!-- 日志 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${log4j.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>${log4j.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-1.2-api</artifactId>
    <version>${log4j.version}</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>${log4j.version}</version>
    <scope>test</scope>
</dependency>

<!--lombok插件-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
</dependency>
src/main/java org.apache.maven.plugins maven-compiler-plugin 3.5.1 1.8 1.8
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.18.1</version>
        <configuration>
            <useFile>false</useFile>
            <disableXmlReport>true</disableXmlReport>
            <includes>
                <include>**/*Test.*</include>
                <include>**/*Suite.*</include>
            </includes>
        </configuration>
    </plugin>

    <!-- 打包插件(会包含所有依赖) -->
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.3</version>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
                <configuration>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <!--
                                zip -d learn_spark.jar META-INF/*.RSA META-INF/*.DSA META-INF/*.SF -->
                                <exclude>META-INF/*.SF</exclude>
                                <exclude>META-INF/*.DSA</exclude>
                                <exclude>META-INF/*.RSA</exclude>
                            </excludes>
                        </filter>
                    </filters>
                    <transformers>
                        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <!-- 设置jar包的入口类(可选) -->
                            <mainClass></mainClass>
                        </transformer>
                    </transformers>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>
6.5 批处理的入门案例 6.5.1 示例 编写Flink程序,读取表中的数据,并根据表中的字段信息进行统计每个单词出现的数量。 6.5.2 开发步骤 创建流/批式处理的运行环境 构建数据源 对数据进行处理 对处理后的结果输出打印 启动执行 准备测试数据 wordcount.txt文件数据 Total,time,BUILD,SUCCESS Final,Memory,Finished,at Total,time,BUILD,SUCCESS Final,Memory,Finished,at Total,time,BUILD,SUCCESS Final,Memory,Finished,at BUILD,SUCCESS BUILD,SUCCESS BUILD,SUCCESS BUILD,SUCCESS BUILD,SUCCESS BUILD,SUCCESS order.csv文件数据 user_001,1621718199,10.1,电脑 user_001,1621718201,14.1,手机 user_002,1621718202,82.5,手机 user_001,1621718205,15.6,电脑 user_004,1621718207,10.2,家电 user_001,1621718208,15.8,电脑 user_005,1621718212,56.1,电脑 user_002,1621718260,40.3,家电 user_001,1621718580,11.5,家居 user_001,1621718860,61.6,家居

6.5.3 参考代码:基于DataStreamAPI编程
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.operators.*;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.util.Collector;

/**

  • 使用批的方式进行单词计数

  • 读取文件里面的数据,对单词进行空格拆分,对每个单词进行计数,然后分组累加

  • 使用flink1.12版本需要区分批处理和流处理的应用程序,flink1.12以后实现了流批一体
    /
    public class WordCountBatch {
    /
    *

    • 入口方法

    • @param args
      /
      public static void main(String[] args) throws Exception {
      /
      *

      • 实现步骤:
      • 1)初始化flink批处理的运行环境
      • 2)指定文件路径,获取数据
      • 3)对获取到的数据进行空格拆分
      • 4)对拆分后的单词计数,每个单词记一次数
      • 5)对相同单词的数据进行分组操作
      • 6)对分组后的数据进行累加操作
      • 7)打印输出
      • 8)启动作业,递交任务
        */
        //todo 1)初始化flink批处理的运行环境
        final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();

      //todo 2)指定文件路径,获取数据
      final DataSource dataSet = env.readTextFile(“./data/input/wordcount.txt”);

      //todo 3)对获取到的数据进行空格拆分
      //搜索类名:ctrl+n
      //提示参数:ctrl+p
      /**

      • 第一个String:传入值类型
      • 第二个String:返回值类型
        */
        final FlatMapOperator<String, String> words = dataSet.flatMap(new MyFlatMapFunction());
        //alt+回车
        //todo 4)对拆分后的单词计数,每个单词记一次数(Tuple)
        final MapOperator<String, Tuple2<String, Integer>> wordAndOne = words.map(new MapFunction<String, Tuple2<String, Integer>>() {
        @Override
        public Tuple2<String, Integer> map(String value) throws Exception {
        return Tuple2.of(value, 1);
        }
        });

      //todo 5)对相同单词的数据进行分组操作
      final UnsortedGrouping<Tuple2<String, Integer>> grouped = wordAndOne.groupBy(0);

      //todo 6)对分组后的数据进行累加操作
      final AggregateOperator<Tuple2<String, Integer>> sumed = grouped.sum(1);

      //todo 7)打印输出
      sumed.print();

      //todo 8)启动作业,递交任务
      //在flink1.12之前批处理开发的时候,print相当于行动算子,会触发作业的递交,因此不需要额外的触发提交
      //env.execute();
      }

    private static class MyFlatMapFunction implements FlatMapFunction<String, String>{
    /**
    * line:一行数据
    * out:收集器,是用来返回数据的
    * @param line
    * @param out
    * @throws Exception
    /
    @Override
    public void flatMap(String line, Collector out) throws Exception {
    //将一行数据根据空格进行拆分
    final String[] words = line.split(" ");
    //遍历每个单词
    for (String word : words) {
    out.collect(word);
    }
    }
    }
    }
    6.5.4 参考代码:基于TableAPI编程
    import org.apache.flink.table.api.
    ;
    import static org.apache.flink.table.api.Expressions.$;

/**

  • @author : zhang.jc

  • @date : 22.10.13 10:52

  • @desc : Table API 实现方式介绍如何开发一个简单的 WordCount 例子。
    **/
    public class WordCountTable {
    public static void main(String[] args) {
    //1:第一步创建流处理的执行环境
    final EnvironmentSettings settings =
    EnvironmentSettings.newInstance().inBatchMode().build();
    final TableEnvironment tEnv = TableEnvironment.create(settings);

     //2:第二步创建 datagen Connector 输入表 source_table,并指定字段 userid、timestamp、money 和 frequency:
     tEnv.createTemporaryTable("source_table",
             TableDescriptor.forConnector("filesystem").schema(
                             Schema.newBuilder()
                                     .column("userid", DataTypes.STRING())
                                     .column("timestamp", DataTypes.BIGINT())
                                     .column("money", DataTypes.DOUBLE())
                                     .column("category", DataTypes.STRING())
                                     .build()
                     )
                     .option("path","./data/input/order.csv")
                     .option("format","csv")
                     .build()
     );
    
     //3:第三步执行聚合查询计算每个 word 的出现次数 frequency:
     Table resultTable = tEnv.from("source_table")
             .groupBy($("userid"))
             .select($("userid"),$("money").sum().as("money"));
    
     //4:第四步创建 Print Connector 输出表 sink_table,并指定两个字段 userid 和 total_money:
     tEnv.createTemporaryTable("sink_table",
             TableDescriptor.forConnector("print").schema(
                             Schema.newBuilder()
                                     .column("userid", DataTypes.STRING())
                                     .column("total_money", DataTypes.DOUBLE())
                                     .build())
                     .build());
    
     //5:第五步查询结果 resultTable 输出到 Print 表 sink_table中:
     resultTable.executeInsert("sink_table");
    

    }
    }
    6.5.5 参考代码:基于SQL编程
    import org.apache.flink.table.api.EnvironmentSettings;
    import org.apache.flink.table.api.TableEnvironment;

/**

  • @author : zhang.jc

  • @date : 22.10.13 10:53

  • @desc : SQL 实现方式介绍如何开发一个简单的 WordCount 例子。
    **/
    public class WordCountSQL {
    public static void main(String[] args) throws Exception {
    // 1:第一步创建流处理的执行环境
    final EnvironmentSettings settings =
    EnvironmentSettings.newInstance().inBatchMode().build();
    final TableEnvironment tableEnv = TableEnvironment.create(settings);

     //2:第二步创建 datagen Connector 输入表 source_table,并指定字段 userid、timestamp、money 和 frequency:
     String sourceDdl = "CREATE TABLE file_source (\n" +
             "        `userid` STRING,\n" +
             "        `timestamp` BIGINT,\n" +
             "        `money` DOUBLE,\n" +
             "        `category` STRING\n" +
             "    ) WITH (\n" +
             "        'connector' = 'filesystem',\n" +
             "        'path' = 'file:///D:\\workspace\\itcast_flinkbase1.15\\data\\input\\order.csv',\n" +
             "        'format' = 'csv'\n" +
             "    )";
     tableEnv.executeSql(sourceDdl);
    
     //3:第三步创建 Print Connector 输出表 sink_table,并指定两个字段 userid 和 total_money:
     String sinkDdl = "CREATE TABLE print_sink (\n" +
             "         `userid` STRING,\n" +
             "         `category` STRING,\n" +
             "         `totalmoney` DOUBLE\n" +
             "     ) WITH (\n" +
             "       'connector' = 'print'\n" +
             "     )";
     tableEnv.executeSql(sinkDdl);
    
     //4:第四步查询结果 resultTable 输出到 Print 表 sink_table中:
     tableEnv.executeSql("INSERT INTO print_sink\n" +
             "                SELECT `userid`, `category`, sum(`money`) AS `totalmoney`\n" +
             "                FROM file_source\n" +
             "                GROUP BY `userid`, `category`");
    

    }
    }
    6.6 流处理的入门案例
    刚才提到自Flink1.12版本开始,有个重要的功能,批流一体,就是可以用流处理的API处理批数据。
    只需要调用setRuntimeMode(RuntimeExecutionMode executionMode)。
    Flink有3中运行模式,分别是STREAMING,BATCH和AUTOMATIC。
    STREAMING运行模式 是DataStream默认的运行模式
    BATCH运行模式 也可以在DataStream API上运行
    AUTOMATIC运行模式 是让系统根据source类型自动选择运行模式
    可以通过命令行来配置运行模式:
    bin/flink run -Dexecution.runtime-mode=BATCH examples/streaming/WordCount.jar
    也可以在代码中配置
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setRuntimeMode(RuntimeExecutionMode.BATCH);

注意:
我们建议用户不要在程序中设置运行模式,而是在提交应用程序时使用命令行进行设置。保持应用程序代码的无配置性可以使程序更加灵活,因为同一个程序可以在任何执行模式下执行。
6.6.1 示例
编写Flink程序,接收socket的单词数据,并以空格进行单词拆分打印。
6.6.2 开发步骤
获取流处理运行环境
构建socket流数据源,并指定IP地址和端口号
对接收到的数据进行空格拆分
对拆分后的单词,每个单词记一次数
对拆分后的单词进行分组
根据单词的次数进行聚合
打印输出
启动执行
在Linux中,使用nc -lk 端口号监听端口,并发送单词
安装nc: yum install -y nc
nc -lk 9999 监听9999端口的信息
6.6.3 参考代码:基于DataStreamAPI编程
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

/**

  • 使用流的方式进行单词计数

  • 读取socket里面的数据,对单词进行空格拆分,对每个单词进行计数,然后分组累加
    /
    public class WordCountStreaming {
    public static void main(String[] args) throws Exception {
    /
    *
    * 实现步骤:
    * 1)初始化flink流处理的运行环境
    * 2)指定socket路径,获取数据
    * 3)对获取到的数据进行空格拆分
    * 4)对拆分后的单词计数,每个单词记一次数
    * 5)对相同单词的数据进行分组操作
    * 6)对分组后的数据进行累加操作
    * 7)打印输出
    * 8)启动作业,递交任务
    */
    //todo 1)初始化flink流处理的运行环境
    final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

     //todo 2)指定socket路径,获取数据
     //nc -lk 9999
     final DataStreamSource<String> lines = env.socketTextStream("node1", 9999);
    
     //todo 3)对获取到的数据进行空格拆分
     //搜索类名:ctrl+n
     //提示参数:ctrl+p
     /**
      * 第一个String:传入值类型
      * 第二个String:返回值类型
      */
     final SingleOutputStreamOperator<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
         //ctrl+i
    
         /**
          * line:一行数据
          * out:收集器,是用来返回数据的
          * @param line
          * @param out
          * @throws Exception
          */
         @Override
         public void flatMap(String line, Collector<String> out) throws Exception {
             //将一行数据根据空格进行拆分
             final String[] words = line.split(" ");
             //遍历每个单词
             for (String word : words) {
                 out.collect(word);
             }
         }
     });
    
     //todo 4)对拆分后的单词计数,每个单词记一次数
     final SingleOutputStreamOperator<Tuple2<String, Integer>> wordAndOne = words.map(new MapFunction<String, Tuple2<String, Integer>>() {
         @Override
         public Tuple2<String, Integer> map(String value) throws Exception {
             return Tuple2.of(value, 1);
         }
     });
     //todo 5)对相同单词的数据进行分组操作
     final KeyedStream<Tuple2<String, Integer>, Tuple> keyedStream = wordAndOne.keyBy(0);
    
     //todo 6)对分组后的数据进行累加操作
     final SingleOutputStreamOperator<Tuple2<String, Integer>> sumed = keyedStream.sum(1);
    
     //todo 7)打印输出
     sumed.print();
    
     //todo 8)启动作业,递交任务
     //在flink1.12之前批处理开发的时候,print相当于行动算子,会触发作业的递交,因此不需要额外的触发提交
     //flink流开发作业必须要加execute
     env.execute();
    

    }
    }
    6.6.4 参考代码:基于TableAPI编程
    import org.apache.flink.connector.datagen.table.DataGenConnectorOptions;
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import org.apache.flink.table.api.*;
    import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

import static org.apache.flink.table.api.Expressions.$;

/**

  • @author : zhang.jc

  • @date : 22.10.13 9:58

  • @desc : Table API 实现方式介绍如何开发一个简单的 WordCount 例子。
    **/
    public class WordCountTable {
    public static void main(String[] args) {
    //1:第一步创建流处理的执行环境
    final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    final StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);

     //2:第二步创建 datagen Connector 输入表 source_table,并指定两个字段 word 和 frequency:
     //通过 option 设置 Connector 的一些行为:
     //fields.word.kind:指定 word 字段 kind 为 random,表示随机生成
     //fields.word.length:指定 word 字段为一个字符长度
     //fields.frequency.min:指定 frequency 字段最小值为 1
     //fields.frequency.max:指定 frequency 字段最大值为 9
     tEnv.createTemporaryTable("source_table",
             TableDescriptor.forConnector("datagen").schema(
                     Schema.newBuilder()
                             .column("word", DataTypes.STRING())
                             .column("frequency", DataTypes.BIGINT()).build()
                     )
                     .option(DataGenConnectorOptions.ROWS_PER_SECOND,1L)
                     .option("fields.word.kind","random")
                     .option("fields.word.length","1")
                     .option("fields.frequency.min","1")
                     .option("fields.frequency.max","9")
                     .build()
     );
    
     //3:第三步执行聚合查询计算每个 word 的出现次数 frequency:
     Table resultTable = tEnv.from("source_table")
             .groupBy($("word"))
             .select($("word"),$("frequency").sum().as("frequency"));
    
     //4:第四步创建 Print Connector 输出表 sink_table,并指定两个字段 word 和 frequency:
     tEnv.createTemporaryTable("sink_table",
             TableDescriptor.forConnector("print").schema(
                     Schema.newBuilder()
                             .column("word",DataTypes.STRING())
                             .column("frequency",DataTypes.BIGINT())
                             .build())
                     .build());
    
     //5:第五步查询结果 resultTable 输出到 Print 表 sink_table中:
     resultTable.executeInsert("sink_table");
    

    }
    }
    6.6.5 参考代码:基于SQL编程
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import org.apache.flink.table.api.EnvironmentSettings;
    import org.apache.flink.table.api.TableEnvironment;
    import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;

/**

  • @author : zhang.jc

  • @date : 22.10.13 10:53

  • @desc : SQL 实现方式介绍如何开发一个简单的 WordCount 例子。
    **/
    public class WordCountSQL {
    public static void main(String[] args) throws Exception {
    // 1:第一步创建流处理的执行环境
    final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    final StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);

     //2:第二步创建 datagen Connector 输入表 source_table,并指定字段 word 和 frequency:
     String sourceDdl = "CREATE TABLE file_source (\n" +
             "        `word` STRING,\n" +
             "        `frequency` BIGINT\n" +
             "    ) WITH (\n" +
             "        'connector' = 'datagen',\n" +
             "        'fields.word.kind' = 'random',\n" +
             "        'fields.word.length' = '1',\n" +
             "        'fields.frequency.min' = '1',\n" +
             "        'fields.frequency.max' = '9'\n" +
             "    )";
     tEnv.executeSql(sourceDdl);
    
     //3:第三步创建 Print Connector 输出表 sink_table,并指定两个字段 word 和 frequency:
     String sinkDdl = "CREATE TABLE print_sink (\n" +
             "         `word` STRING,\n" +
             "         `frequency` BIGINT\n" +
             "     ) WITH (\n" +
             "       'connector' = 'print'\n" +
             "     )";
     tEnv.executeSql(sinkDdl);
    
     //4:第四步查询结果 resultTable 输出到 Print 表 sink_table中:
     tEnv.executeSql("INSERT INTO print_sink\n" +
             "                SELECT `word`, sum(`frequency`) AS `frequency`\n" +
             "                FROM file_source\n" +
             "                GROUP BY `word`");
    

    }
    }
    +I,-U,+U 表示一行数据的 changelog,+I 表示是新增的数据,-U 表示之前的记录已经被更新,之前的记录要回撤,+U 表示本次更新的数据。
    可以看到,输出结果是以对于每行产生 changelog 的形式来表示的。
    6.6.6 参考代码:基于Lambda编程(扩展)
    import org.apache.flink.api.common.typeinfo.Types;
    import org.apache.flink.api.java.tuple.Tuple2;
    import org.apache.flink.streaming.api.datastream.DataStreamSource;
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import org.apache.flink.util.Collector;
    import java.util.Arrays;

/**

  • @author : zhang.jc

  • @date : 22.10.13 10:53

  • @desc : Lambda 实现方式介绍如何开发一个简单的 WordCount 例子。
    **/
    public class WordCountLambda {

    public static void main(String[] args) throws Exception {
    // 1、创建流式处理环境
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    // 2、并行度设置为1,运行模式为批处理
    env.setParallelism(1);
    // 3、将source添加到环境中,生成datastream对象
    final DataStreamSource dataSource = env.socketTextStream(“node1”, 9999);
    // 4、transform
    dataSource.flatMap((String input, Collector<String[]> collector) -> {
    collector.collect(input.split(“,”));
    })
    .returns(Types.OBJECT_ARRAY(Types.STRING))
    .flatMap((String[] words, Collector<Tuple2<String, Integer>> collector) -> {
    Arrays.stream(words).map(word -> new Tuple2<>(word, 1)).forEach(collector::collect);
    })
    .returns(Types.TUPLE(Types.STRING, Types.INT))
    .keyBy(0)
    .sum(1)
    .print();

     env.execute();
    

    }
    }

6.7 Flink程序提交部署
Flink程序递交方式有两种:
以UI的方式递交
以命令的方式递交
6.7.1 编写程序
写入HDFS如果存在权限问题:
进行如下设置:
hadoop fs -chmod -R 777 /

并在代码中添加:
System.setProperty(“HADOOP_USER_NAME”, “root”)

import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.util.Arrays;

/**

  • 编写Flink程序,读取文件中的字符串,并以空格进行单词拆分打印

*/
public class BatchWordCountToYarn {
public static void main(String[] args) throws Exception {
ParameterTool parameterTool = ParameterTool.fromArgs(args);
String output = “”;
if (parameterTool.has(“output”)) {
output = parameterTool.get(“output”);
System.out.println(“指定了输出路径使用:” + output);
} else {
output = “hdfs://node1:8020/wordcount/output47_”;
System.out.println(“可以指定输出路径使用 --output ,没有指定使用默认的:” + output);
}

    //TODO 0.env
    //ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    //env.setRuntimeMode(RuntimeExecutionMode.BATCH);//注意:使用DataStream实现批处理
    //env.setRuntimeMode(RuntimeExecutionMode.STREAMING);//注意:使用DataStream实现流处理
    //env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);//注意:使用DataStream根据数据源自动选择使用流还是批

    //TODO 1.source
    //DataSet<String> lines = env.fromElements("itcast hadoop spark", "itcast hadoop spark", "itcast hadoop", "itcast");
    DataStream<String> lines = env.fromElements("itcast hadoop spark", "itcast hadoop spark", "itcast hadoop", "itcast");

    //TODO 2.transformation
    //切割
    /*
    @FunctionalInterface
    public interface FlatMapFunction<T, O> extends Function, Serializable {
        void flatMap(T value, Collector<O> out) throws Exception;
    }
     */
    /*DataStream<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
        @Override
        public void flatMap(String value, Collector<String> out) throws Exception {
            //value就是每一行数据
            String[] arr = value.split(" ");
            for (String word : arr) {
                out.collect(word);
            }
        }
    });*/
    SingleOutputStreamOperator<String> words = lines.flatMap(
            (String value, Collector<String> out) -> Arrays.stream(value.split(" ")).forEach(out::collect)
    ).returns(Types.STRING);


    //记为1
    /*
    @FunctionalInterface
    public interface MapFunction<T, O> extends Function, Serializable {
        O map(T value) throws Exception;
    }
     */
    /*DataStream<Tuple2<String, Integer>> wordAndOne = words.map(new MapFunction<String, Tuple2<String, Integer>>() {
        @Override
        public Tuple2<String, Integer> map(String value) throws Exception {
            //value就是一个个单词
            return Tuple2.of(value, 1);
        }
    });*/
    DataStream<Tuple2<String, Integer>> wordAndOne = words.map(
            (String value) -> Tuple2.of(value, 1)
    ).returns(Types.TUPLE(Types.STRING, Types.INT));

    //分组:注意DataSet中分组是groupBy,DataStream分组是keyBy
    //wordAndOne.keyBy(0);
    /*
    @FunctionalInterface
    public interface KeySelector<IN, KEY> extends Function, Serializable {
        KEY getKey(IN value) throws Exception;
    }
     */
    KeyedStream<Tuple2<String, Integer>, String> grouped = wordAndOne.keyBy(t -> t.f0);

    //聚合
    SingleOutputStreamOperator<Tuple2<String, Integer>> result = grouped.sum(1);

    //TODO 3.sink
    //如果执行报hdfs权限相关错误,可以执行 hadoop fs -chmod -R 777  /
    System.setProperty("HADOOP_USER_NAME", "root");//设置用户名
    //result.print();
    //result.writeAsText("hdfs://node1:8020/wordcount/output47_"+System.currentTimeMillis()).setParallelism(1);
    result.writeAsText(output + System.currentTimeMillis()).setParallelism(1);

    //TODO 4.execute/启动并等待程序结束
    env.execute();
}

}

6.7.2 以UI的方式递交
操作步骤 说明
1 上传作业jar包
2 指定递交参数

3 查看任务运行概述

4 查看任务运行结果

6.7.3 以命令的方式递交
参考官网:执行模式(流/批) | Apache Flink
操作步骤 说明
1 上传作业jar包到linux服务器

2
3 flink run
-Dexecution.runtime-mode=BATCH
-m yarn-cluster
-yjm 1024
-ytm 1024
-c cn.itcast.day01.BatchWordCountToYarn /root/itcast_flinkbase-1.0-SNAPSHOT.jar --output hdfs://node1:8020/wordcount/output_50
3 查看任务运行概述

6.8 SqlClient工具的使用
Flink 的 Table & SQL API 可以处理 SQL 语言编写的查询语句,但是这些查询需要嵌入用 Java 或 Scala 或者Python 编写的表程序中。此外,这些程序在提交到集群前需要用构建工具打包。这或多或少限制了 Java/Scala/Python 程序员对 Flink 的使用。
SQL 客户端 的目的是提供一种简单的方式来编写、调试和提交表程序到 Flink 集群上,而无需写一行 Java 或 Scala 代码。SQL 客户端命令行界面(CLI) 能够在命令行中检索和可视化分布式应用中实时产生的结果。

6.8.1 入门
本节介绍如何在命令行里启动(setup)和运行你的第一个 Flink SQL 程序。
SQL 客户端捆绑在常规 Flink 发行版中,因此可以直接运行。它仅需要一个正在运行的 Flink 集群就可以在其中执行表程序。有关设置 Flink 群集的更多信息,请参见集群和部署部分。如果仅想试用 SQL 客户端,也可以使用以下命令启动本地集群:
./bin/start-cluster.sh
 
6.8.2 启动 SQL 客户端命令行界面
SQL Client 脚本也位于 Flink 的 bin 目录中。用户可以通过启动嵌入式 standalone 进程或通过连接到远程 SQL 客户端网关来启动 SQL 客户端命令行界面。目前仅支持 embedded,模式默认值embedded。可以通过以下方式启动 CLI:
./bin/start-cluster.sh
或者显式使用 embedded 模式:
./bin/sql-client.sh embedded
启动成功 会进入 flink sql> 命令行界面 (输入 quit; 退出)

6.8.3 执行 SQL 查询
命令行界面启动后,你可以使用 help命令列出所有可用的 SQL 语句。输入第一条 SQL 查询语句并按 Enter 键执行,可以验证你的设置及集群连接是否正确:

SELECT ‘Hello World’;

默认情况下输出默认采用的是表格模式,在上面的演示中该查询不需要 table source,因为只产生一行结果。CLI 将从集群中检索结果并将其可视化。按 Q 键退出结果视图。
CLI 为维护和可视化结果提供三种模式:
表格模式(table mode)在内存中实体化结果,并将结果用规则的分页表格可视化展示出来。执行如下命令启用:
SET sql-client.execution.result-mode=table;
变更日志模式(changelog mode)不会实体化和可视化结果,而是由插入(+)和撤销(-)组成的持续查询产生结果流。
SET sql-client.execution.result-mode=changelog;
Tableau模式(tableau mode)更接近传统的数据库,会将执行的结果以制表的形式直接打在屏幕之上。具体显示的内容会取决于作业 执行模式的不同(execution.type):
SET sql-client.execution.result-mode=tableau;
注意当你使用这个模式运行一个流式查询的时候,Flink 会将结果持续的打印在当前的屏幕之上。如果这个流式查询的输入是有限的数据集, 那么Flink在处理完所有的数据之后,会自动的停止作业,同时屏幕上的打印也会相应的停止。如果你想提前结束这个查询,那么可以直接使用 CTRL-C 按键,这个会停掉作业同时停止屏幕上的打印。
可以用如下查询来查看三种结果模式的运行情况:
SELECT name, COUNT(*) AS cnt FROM (VALUES (‘Bob’), (‘Alice’), (‘Greg’), (‘Bob’)) AS NameTable(name) GROUP BY name;

此查询执行一个有限字数示例:
变更日志模式 下,看到的结果应该类似于:
SET sql-client.execution.result-mode=changelog;

SELECT name, COUNT(*) AS cnt FROM (VALUES (‘Bob’), (‘Alice’), (‘Greg’), (‘Bob’)) AS NameTable(name) GROUP BY name;

表格模式 下,可视化结果表将不断更新,直到表程序以如下内容结束:
SET sql-client.execution.result-mode=table;

SELECT name, COUNT(*) AS cnt FROM (VALUES (‘Bob’), (‘Alice’), (‘Greg’), (‘Bob’)) AS NameTable(name) GROUP BY name;

Tableau模式 下,如果这个查询以流的方式执行,那么将显示以下内容:
SET sql-client.execution.result-mode=tableau;

SELECT name, COUNT(*) AS cnt FROM (VALUES (‘Bob’), (‘Alice’), (‘Greg’), (‘Bob’)) AS NameTable(name) GROUP BY name;

这几种结果模式在 SQL 查询的原型设计过程中都非常有用。这些模式的结果都存储在 SQL 客户端 的 Java 堆内存中。
为了保持 CLI 界面及时响应,变更日志模式仅显示最近的 1000 个更改。表格模式支持浏览更大的结果,这些结果仅受可用主内存和配置的最大行数(sql-client.execution.max-table-result.rows)的限制。
注意:在批处理环境下执行的查询只能用表格模式或者Tableau模式进行检索。
定义查询语句后,可以将其作为长时间运行的独立 Flink 作业提交给集群。
配置部分解释如何声明读取数据的 table source,写入数据的 sink 以及配置其他表程序属性的方法。

  1. [理解] – Flink概念透析
    7.1 Flink基石
    Flink之所以能这么流行,离不开它最重要的四个基石:Checkpoint、State、Time、Window。

7.2 系统架构
对于数据处理系统的架构,最简单的实现方式当然就是单节点。当数据量增大、处理计算更加复杂时,我们可以考虑增加 CPU 数量、加大内存,也就是让这一台机器变得性能更强大,从而提高吞吐量——这就是所谓的 SMP(Symmetrical Multi-Processing,对称多处理)架构。但是这样做问题非常明显:所有 CPU 是完全平等、共享内存和总线资源的,这就势必造成资源竞争;而且随着 CPU 核心数量的增加,机器的成本会指数增长,所以 SMP 的可扩展性是比较差的,无法应对海量数据的处理场景。
于是人们提出了“不共享任何东西”(share-nothing)的分布式架构。从以 Greenplum 为代表的 MPP(Massively Parallel Processing,大规模并行处理)架构,到 Hadoop、Spark 为代表的批处理架构,再到 Storm、Flink 为代表的流处理架构,都是以分布式作为系统架构的基本形态的。
我们已经知道,Flink 就是一个分布式的并行流处理系统。简单来说,它会由多个进程构成,这些进程一般会分布运行在不同的机器上。
正如一个团队,人多了就会难以管理;对于一个分布式系统来说,也需要面对很多棘手的问题。其中的核心问题有:集群中资源的分配和管理、进程协调调度、持久化和高可用的数据存储,以及故障恢复。
对于这些分布式系统的经典问题,业内已有比较成熟的解决方案和服务。所以 Flink 并不会自己去处理所有的问题,而是利用了现有的集群架构和服务,这样它就可以把精力集中在核心工作——分布式数据流处理上了。Flink 可以配置为独立(Standalone)集群运行,也可以方便地跟一些集群资源管理工具集成使用,比如 YARN、Kubernetes。Flink 也不会自己去提供持久化的分布式存储,而是直接利用了已有的分布式文件系统(比如 HDFS)或者对象存储(比如 S3)。而对于高可用的配置,Flink 是依靠 Apache ZooKeeper 来完成的。
我们所要重点了解的,就是在 Flink 中有哪些组件、是怎样具体实现一个分布式流处理系统的。如果大家对 Spark 或者 Storm 比较熟悉,那么稍后就会发现,Flink 其实有类似的概念和架构。
7.2.1 整体构成
Flink 的运行时架构中,最重要的就是两大组件:作业管理器(JobManger)和任务管理器(TaskManager)。对于一个提交执行的作业,JobManager 是真正意义上的“管理者”(Master),负责管理调度,所以在不考虑高可用的情况下只能有一个;而 TaskManager 是“工作者”(Worker、Slave),负责执行任务处理数据,所以可以有一个或多个。Flink 的作业提交和任务处理时的系统如图所示。

这里首先要说明一下“客户端”。其实客户端并不是处理系统的一部分,它只负责作业的提交。具体来说,就是调用程序的 main 方法,将代码转换成“数据流图”(Dataflow Graph),并最终生成作业图(JobGraph),一并发送给 JobManager。提交之后,任务的执行其实就跟客户端没有关系了;我们可以在客户端选择断开与 JobManager 的连接, 也可以继续保持连接。之前我们在命令提交作业时,加上的-d 参数,就是表示分离模式(detached mode),也就是断开连接。
当然,客户端可以随时连接到 JobManager,获取当前作业的状态和执行结果,也可以发送请求取消作业。我们在上一章中不论通过 Web UI 还是命令行执行“flink run”的相关操作,都是通过客户端实现的。

JobManager 和 TaskManagers 可以以不同的方式启动:
作为独立(Standalone)集群的进程,直接在机器上启动
在容器中启动
由资源管理平台调度启动,比如 YARN、K8S
这其实就对应着不同的部署方式。TaskManager 启动之后,JobManager 会与它建立连接,并将作业图(JobGraph)转换成可执行的“执行图”(ExecutionGraph)分发给可用的 TaskManager,然后就由 TaskManager 具体执行任务。接下来,我们就具体介绍一下 JobManger 和 TaskManager 在整个过程中扮演的角色。
7.2.2 作业管理器(JobManager)
JobManager 是一个 Flink 集群中任务管理和调度的核心,是控制应用执行的主进程。也就是说,每个应用都应该被唯一的 JobManager 所控制执行。当然,在高可用(HA)的场景下,可能会出现多个 JobManager;这时只有一个是正在运行的领导节点(leader),其他都是备用节点(standby)。JobManger 又包含 3 个不同的组件,下面我们一一讲解。

JobMaster
JobMaster 是 JobManager 中最核心的组件,负责处理单独的作业(Job)。所以 JobMaster和具体的 Job 是一一对应的,多个 Job 可以同时运行在一个 Flink 集群中, 每个 Job 都有一个自己的 JobMaster。需要注意在早期版本的 Flink 中,没有 JobMaster 的概念;而 JobManager的概念范围较小,实际指的就是现在所说的 JobMaster。
在作业提交时,JobMaster 会先接收到要执行的应用。这里所说“应用”一般是客户端提交来的,包括:Jar 包,数据流图(dataflow graph),和作业图(JobGraph)。
JobMaster 会把 JobGraph 转换成一个物理层面的数据流图,这个图被叫作“执行图”(ExecutionGraph),它包含了所有可以并发执行的任务。JobMaster 会向资源管理器(ResourceManager)发出请求,申请执行任务必要的资源。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的 TaskManager 上。而在运行过程中,JobMaster 会负责所有需要中央协调的操作,比如说检查点(checkpoints)的协调。

资源管理器(ResourceManager)
ResourceManager 主要负责资源的分配和管理,在 Flink 集群中只有一个。所谓“资源”,主要是指 TaskManager 的任务槽(task slots)。任务槽就是 Flink 集群中的资源调配单元,包含了机器用来执行计算的一组 CPU 和内存资源。每一个任务(Task)都需要分配到一个 slot 上执行。
这里注意要把 Flink 内置的 ResourceManager 和其他资源管理平台(比如 YARN)的ResourceManager 区分开。
Flink 的 ResourceManager,针对不同的环境和资源管理平台(比如 Standalone 部署,或者YARN),有不同的具体实现。在 Standalone 部署时,因为 TaskManager 是单独启动的(没有Per-Job 模式),所以 ResourceManager 只能分发可用 TaskManager 的任务槽,不能单独启动新TaskManager。
而在有资源管理平台时,就不受此限制。当新的作业申请资源时,ResourceManager 会将有空闲槽位的 TaskManager 分配给 JobMaster。如果 ResourceManager 没有足够的任务槽,它
还可以向资源提供平台发起会话,请求提供启动 TaskManager 进程的容器。另外,ResourceManager 还负责停掉空闲的 TaskManager,释放计算资源。

分发器(Dispatcher)
Dispatcher 主要负责提供一个 REST 接口,用来提交应用,并且负责为每一个新提交的作业启动一个新的 JobMaster 组件。Dispatcher 也会启动一个 Web UI,用来方便地展示和监控作
业执行的信息。Dispatcher 在架构中并不是必需的,在不同的部署模式下可能会被忽略掉。
7.2.3 任务管理器(TaskManager)
TaskManager 是 Flink 中的工作进程,数据流的具体计算就是它来做的,所以也被称为“Worker”。Flink 集群中必须至少有一个 TaskManager;当然由于分布式计算的考虑,通常会有多个 TaskManager 运行,每一个 TaskManager 都包含了一定数量的任务槽(task slots)。Slot是资源调度的最小单位,slot 的数量限制了 TaskManager 能够并行处理的任务数量。
启动之后,TaskManager 会向资源管理器注册它的 slots;收到资源管理器的指令后,TaskManager 就会将一个或者多个槽位提供给 JobMaster 调用,JobMaster 就可以分配任务来执行了。
在执行过程中,TaskManager 可以缓冲数据,还可以跟其他运行同一应用的 TaskManager交换数据。
7.3 作业提交流程
了解了 Flink 运行时的基本组件和系统架构,我们再来梳理一下作业提交的具体流程。
7.3.1 高层级抽象视角
Flink 的提交流程,随着部署模式、资源管理平台的不同,会有不同的变化。首先我们从一个高层级的视角,来做一下抽象提炼,看一看作业提交时宏观上各组件是怎样交互协作的。

具体步骤如下:
(1) 一般情况下,由客户端(App)通过分发器提供的 REST 接口,将作业提交给JobManager。
(2)由分发器启动 JobMaster,并将作业(包含 JobGraph)提交给 JobMaster。
(3)JobMaster 将 JobGraph 解析为可执行的 ExecutionGraph,得到所需的资源数量,然后向资源管理器请求资源(slots)。
(4)资源管理器判断当前是否由足够的可用资源;如果没有,启动新的 TaskManager。
(5)TaskManager 启动之后,向 ResourceManager 注册自己的可用任务槽(slots)。
(6)资源管理器通知 TaskManager 为新的作业提供 slots。
(7)TaskManager 连接到对应的 JobMaster,提供 slots。
(8)JobMaster 将需要执行的任务分发给 TaskManager。
(9)TaskManager 执行任务,互相之间可以交换数据。
如果部署模式不同,或者集群环境不同(例如 Standalone、YARN、K8S 等),其中一些步骤可能会不同或被省略,也可能有些组件会运行在同一个 JVM 进程中。比如我们在上一章实践过的独立集群环境的会话模式,就是需要先启动集群,如果资源不够,只能等待资源释放,而不会直接启动新的 TaskManager。
接下来我们就具体介绍一下不同部署环境下的提交流程。
7.3.2 独立模式(Standalone)
在独立模式(Standalone)下, TaskManager 都需要手动启动,所以当 ResourceManager 收到 JobMaster 的请求时,会直接要求 TaskManager 提供资源。提交的整体流程如图所示。

我们发现除去第 4 步不会启动 TaskManager,而且直接向已有的 TaskManager 要求资源,其他步骤与上一节所讲抽象流程完全一致。
7.3.3 YARN 集群
接下来我们再看一下有资源管理平台时,具体的提交流程。我们以 YARN 为例,分不同的部署模式来做具体说明。
7.3.3.1 会话(Session)模式
在会话模式下,我们需要先启动一个 YARN session,这个会话会创建一个 Flink 集群。

这里只启动了 JobManager,而 TaskManager 可以根据需要动态地启动。在 JobManager 内部,由于还没有提交作业,所以只有 ResourceManager 和 Dispatcher 在运行,如图所示。

接下来就是真正提交作业的流程,如上图所示:
(1)客户端通过 REST 接口,将作业提交给分发器。
(2)分发器启动 JobMaster,并将作业(包含 JobGraph)提交给 JobMaster。
(3)JobMaster 向资源管理器请求资源(slots)。
(4)资源管理器向 YARN 的资源管理器请求 container 资源。
(5)YARN 启动新的 TaskManager 容器。
(6)TaskManager 启动之后,向 Flink 的资源管理器注册自己的可用任务槽。
(7)资源管理器通知 TaskManager 为新的作业提供 slots。
(8)TaskManager 连接到对应的 JobMaster,提供 slots。
(9)JobMaster 将需要执行的任务分发给 TaskManager,执行任务。
可见,整个流程除了请求资源时要“上报”YARN 的资源管理器,其他与 7.5.1 节所述抽象流程几乎完全一样。
7.3.3.2 单作业(Per-Job)模式
在单作业模式下,Flink 集群不会预先启动,而是在提交作业时,才启动新的 JobManager。具体流程如下图所示。

(1)客户端将作业提交给 YARN 的资源管理器,这一步中会同时将 Flink 的 Jar 包和配置上传到 HDFS,以便后续启动 Flink 相关组件的容器。
(2)YARN 的资源管理器分配 Container 资源,启动 Flink JobManager,并将作业提交给JobMaster。这里省略了 Dispatcher 组件。
(3)JobMaster 向资源管理器请求资源(slots)。
(4)资源管理器向 YARN 的资源管理器请求 container 资源。
(5)YARN 启动新的 TaskManager 容器。
(6)TaskManager 启动之后,向 Flink 的资源管理器注册自己的可用任务槽。
(7)资源管理器通知 TaskManager 为新的作业提供 slots。
(8)TaskManager 连接到对应的 JobMaster,提供 slots。
(9)JobMaster 将需要执行的任务分发给 TaskManager,执行任务。
可见,区别只在于 JobManager 的启动方式,以及省去了分发器。当第 2 步作业提交给JobMaster,之后的流程就与会话模式完全一样了。
7.3.3.3 应用(Application)模式
应用模式与单作业模式的提交流程非常相似,只是初始提交给 YARN 资源管理器的不再是具体的作业,而是整个应用。一个应用中可能包含了多个作业,这些作业都将在 Flink 集群中启动各自对应的 JobMaster。
7.4 一些重要概念
我们现在已经了解 Flink 运行时的核心组件和整体架构,也明白了不同场景下作业提交的具体流程。但有些细节还需要进一步思考:一个具体的作业,是怎样从我们编写的代码,转换成 TaskManager 可以执行的任务的呢?JobManager 收到提交的作业,又是怎样确定总共有多少任务、需要多少资源呢?接下来我们就从一些重要概念入手,对这些问题做详细的展开讲解。
7.4.1 数据流图(Dataflow Graph)
Flink 是流式计算框架。它的程序结构,其实就是定义了一连串的处理操作,每一个数据输入之后都会依次调用每一步计算。在 Flink 代码中,我们定义的每一个处理转换操作都叫作“算子”(Operator),所以我们的程序可以看作是一串算子构成的管道,数据则像水流一样有序地流过。
比如在之前的 WordCount 代码中,基于执行环境调用的 socketTextStream()方法,就是一个读取文本流的算子;而后面的 flatMap()方法,则是将字符串数据进行分词、转换成二元组的算子。

所有的 Flink 程序都可以归纳为由三部分构成:
Source:表示“源算子”,负责读取数据源。
Transformation:表示“转换算子”,利用各种算子进行处理加工。
Sink:表示“下沉算子”,负责数据的输出。

在运行时,Flink 程序会被映射成所有算子按照逻辑顺序连接在一起的一张图,这被称为“逻辑数据流”(logical dataflow),或者叫“数据流图”(dataflow graph)。我们提交作业之后,打开 Flink 自带的 Web UI,点击作业就能看到对应的 dataflow,如上图所示。在数据流图中,可以清楚地看到 Source、Transformation、Sink 三部分。
数据流图类似于任意的有向无环图(DAG),这一点与 Spark 等其他框架是一致的。图中的每一条数据流(dataflow)以一个或多个 source 算子开始,以一个或多个 sink 算子结束。
在大部分情况下,dataflow 中的算子,和程序中的转换运算是一一对应的关系。那是不是说,我们代码中基于 DataStream API 的每一个方法调用,都是一个算子呢?
并非如此。除了 Source 读取数据和 Sink 输出数据,一个中间的转换算子(TransformationOperator)必须是一个转换处理的操作;而在代码中有一些方法调用,数据是没有完成转换的。可能只是对属性做了一个设置,也可能定义的是数据的传递方式而非转换,又或者是需要几个方法合在一起才能表达一个完整的转换操作。
例如,在之前的代码中,我们用到了定义分组的方法 keyBy,它就只是一个数据分区操作,而并不是一个算子。事实上,代码中我们可以看到调用其他转换操作之后返回的数据类型是 SingleOutputStreamOperator,说明这是一个算子操作;而 keyBy 之后返回的数据类型是 KeyedStream。感兴趣的读者也可以自行提交任务在 WebUI 中查看。
7.4.2 并行度(Parallelism)
我们已经清楚了算子和数据流图的概念,那最终执行的任务又是什么呢?容易想到,一个算子操作就应该是一个任务。那是不是程序中的算子数量,就是最终执行的任务数呢?

7.4.2.1 什么是并行计算
要解答这个问题,我们需要先梳理一下其他框架分配任务、数据处理的过程。对于 Spark而言,是把根据程序生成的 DAG 划分阶段(stage)、进而分配任务的。而对于 Flink 这样的流式引擎,其实没有划分 stage 的必要。因为数据是连续不断到来的,我们完全可以按照数据流图建立一个“流水线”,前一个操作处理完成,就发往处理下一步操作的节点。如果说 Spark基于 MapReduce 架构的思想是“数据不动代码动”,那么 Flink 就类似“代码不动数据流动”,原因就在于流式数据本身是连续到来的、我们不会同时传输所有数据,这其实是更符合数据流本身特点的处理方式。
在大数据场景下,我们都是依靠分布式架构做并行计算,从而提高数据吞吐量的。既然处理完一个操作就可以把数据发往别处,那我们就可以将不同的算子操作任务,分配到不同的节点上执行了。这样就对任务做了分摊,实现了并行处理。
但是仔细分析会发现,这种“并行”其实并不彻底。因为算子之间是有执行顺序的,对一条数据来说必须依次执行;而一个算子在同一时刻只能处理一个数据。比如之前 WordCount,一条数据到来之后,我们必须先用 source 算子读进来、再做 flatMap 转换;一条数据被 source读入的同时,之前的数据可能正在被 flatMap 处理,这样不同的算子任务是并行的。但如果多条数据同时到来,一个算子是没有办法同时处理的,我们还是需要等待一条数据处理完、再处理下一条数据——这并没有真正提高吞吐量。
所以相对于上述的“任务并行”,我们真正关心的,是“数据并行”。也就是说,多条数据同时到来,我们应该可以同时读入,同时在不同节点执行 flatMap 操作。

7.4.2.2 并行子任务和并行度
怎样实现数据并行呢?其实也很简单,我们把一个算子操作,“复制”多份到多个节点,数据来了之后就可以到其中任意一个执行。这样一来,一个算子任务就被拆分成了多个并行的“子任务”(subtasks),再将它们分发到不同节点,就真正实现了并行计算。在 Flink 执行过程中,每一个算子(operator)可以包含一个或多个子任务(operator subtask),这些子任务在不同的线程、不同的物理机或不同的容器中完全独立地执行。

一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism)。这样,包含并行子任务的数据流,就是并行数据流,它需要多个分区(stream partition)来分配并行任务。
一般情况下,一个流程序的并行度,可以认为就是其所有算子中最大的并行度。一个程序中,不同的算子可能具有不同的并行度。
如上图所示,当前数据流中有 source、map、window、sink 四个算子,除最后 sink,其他算子的并行度都为 2。整个程序包含了 7 个子任务,至少需要 2 个分区来并行执行。我们可以说,这段流处理程序的并行度就是 2。

7.4.2.3 并行度的设置
在 Flink 中,可以用不同的方法来设置并行度,它们的有效范围和优先级别也是不同的。
代码中设置
我们在代码中,可以很简单地在算子后跟着调用 setParallelism()方法,来设置当前算子的并行度:
stream.map(word -> Tuple2.of(word, 1L)).setParallelism(2);
这种方式设置的并行度,只针对当前算子有效。
另外,我们也可以直接调用执行环境的 setParallelism()方法,全局设定并行度:
env.setParallelism(2);
这样代码中所有算子,默认的并行度就都为 2 了。我们一般不会在程序中设置全局并行度,因为如果在程序中对全局并行度进行硬编码,会导致无法动态扩容。
这里要注意的是,由于 keyBy 不是算子,所以无法对 keyBy 设置并行度。
不建议在生产环境使用,生产环境调整并行度的时候需要修改代码,重新编译打包发布!
提交应用时设置
在使用 flink run 命令提交应用时,可以增加-p 参数来指定当前应用程序执行的并行度,它的作用类似于执行环境的全局设置:
bin/flink run –p 2 –c org.apache.flink.examples.java.wordcount.WordCount /export/server/flink/examples/batch/WordCount.jar
如果我们直接在 Web UI 上提交作业,也可以在对应输入框中直接添加并行度。
配置文件中设置
我们还可以直接在集群的配置文件 flink-conf.yaml 中直接更改默认并行度:
parallelism.default: 2
这个设置对于整个集群上提交的所有作业有效,初始值为 1。无论在代码中设置、还是提交时的-p 参数,都不是必须的;所以在没有指定并行度的时候,就会采用配置文件中的集群默认并行度。在开发环境中,没有配置文件,默认并行度就是当前机器的 CPU 核心数。这也就解释了为什么我们在第二章运行 WordCount 流处理程序时,会看到结果前有 1~4 的分区编号——运行程序的电脑是 4 核 CPU,那么开发环境默认的并行度就是 4。
我们可以总结一下所有的并行度设置方法,它们的优先级如下:
优先级:算子 > 代码全局 > 命令行参数 > 配置文件
7.4.3 算子链(Operator Chain)
关于“一个作业有多少任务”这个问题,现在已经基本解决了。但如果我们仔细观察 WebUI 上给出的图,如图所示,上面的节点似乎跟代码中的算子又不是一一对应的。

很明显,这里的一个节点,会把转换处理的很多个任务都连接在一起,合并成了一个“大任务”。这又是怎么回事呢?
7.4.3.1 算子间的数据传输
回到上一小节的例子,我们先来考察一下算子任务之间数据传输的方式。

如上图所示,一个数据流在算子之间传输数据的形式可以是一对一(one-to-one)的直通 (forwarding)模式,也可以是打乱的重分区(redistributing)模式,具体是哪一种形式,取决于算子的种类。
(1)一对一(One-to-one,forwarding)
这种模式下,数据流维护着分区以及元素的顺序。比如图中的 source 和 map 算子,source算子读取数据之后,可以直接发送给 map 算子做处理,它们之间不需要重新分区,也不需要调整数据的顺序。这就意味着 map 算子的子任务,看到的元素个数和顺序跟 source 算子的子任务产生的完全一样,保证着“一对一”的关系。map、filter、flatMap 等算子都是这种 one-to-one的对应关系。这种关系类似于 Spark 中的窄依赖。
(2)重分区(Redistributing)
在这种模式下,数据流的分区会发生改变。比图中的 map 和后面的 keyBy/window 算子之间(这里的 keyBy 是数据传输算子,后面的 window、apply 方法共同构成了 window 算子),以及 keyBy/window 算子和 Sink 算子之间,都是这样的关系。
每一个算子的子任务,会根据数据传输的策略,把数据发送到不同的下游目标任务。例如,keyBy()是分组操作,本质上基于键(key)的哈希值(hashCode)进行了重分区;而当并行度改变时,比如从并行度为 2 的 window 算子,要传递到并行度为 1 的 Sink 算子,这时的数据传输方式是再平衡(rebalance),会把数据均匀地向下游子任务分发出去。这些传输方式都会引起重分区(redistribute)的过程,这一过程类似于 Spark 中的 shuffle。
总体说来,这种算子间的关系类似于 Spark 中的宽依赖。
7.4.3.2 合并算子链
在 Flink 中,并行度相同的一对一(one to one)算子操作,可以直接链接在一起形成一个“大”的任务(task),这样原来的算子就成为了真正任务里的一部分,如图所示。每个 task会被一个线程执行。这样的技术被称为“算子链”(Operator Chain)。

比如在上图中的例子中,Source 和 map 之间满足了算子链的要求,所以可以直接合并在一起,形成了一个任务;因为并行度为 2,所以合并后的任务也有两个并行子任务。这样,这个数据流图所表示的作业最终会有 5 个任务,由 5 个线程并行执行。
Flink 为什么要有算子链这样一个设计呢?这是因为将算子链接成 task 是非常有效的优化:可以减少线程之间的切换和基于缓存区的数据交换,在减少时延的同时提升吞吐量。
Flink 默认会按照算子链的原则进行链接合并,如果我们想要禁止合并或者自行定义,也可以在代码中对算子做一些特定的设置:
// 禁用算子链
.map(word -> Tuple2.of(word, 1L)).disableChaining();
// 从当前算子开始新链
.map(word -> Tuple2.of(word, 1L)).startNewChain()
7.4.4 作业图(JobGraph)与执行图(ExecutionGraph)
至此,我们已经彻底了解了由代码生成任务的过程,现在来做个梳理总结。
由 Flink 程序直接映射成的数据流图(dataflow graph),也被称为逻辑流图(logicalStreamGraph),因为它们表示的是计算逻辑的高级视图。到具体执行环节时,我们还要考虑并行子任务的分配、数据在任务间的传输,以及合并算子链的优化。为了说明最终应该怎样执行一个流处理程序,Flink 需要将逻辑流图进行解析,转换为物理数据流图。
在这个转换过程中,有几个不同的阶段,会生成不同层级的图,其中最重要的就是作业图(JobGraph)和执行图(ExecutionGraph)。Flink 中任务调度执行的图,按照生成顺序可以分成
四层:
逻辑流图(StreamGraph)→ 作业图(JobGraph)→ 执行图(ExecutionGraph)→ 物理图(Physical Graph)。
我们可以回忆一下之前处理 socket 文本流的 StreamWordCount 程序:
如果提交时设置并行度为 2。那么根据之前的分析,除了 socketTextStream()是非并行的 Source 算子,它的并行度始终为 1,其他算子的并行度都为 2。
接下来我们分析一下程序对应四层调度图的演变过程,如下图所示。

  1. 逻辑流图(StreamGraph)
    这是根据用户通过 DataStream API 编写的代码生成的最初的 DAG 图,用来表示程序的拓扑结构。这一步一般在客户端完成。
    我们可以看到,逻辑流图中的节点,完全对应着代码中的四步算子操作:
    源算子 Source(socketTextStream())→ 扁平映射算子 Flat Map(flatMap()) → 分组聚合算子Keyed Aggregation(keyBy/sum()) →输出算子 Sink(print())。
  2. 作业图(JobGraph)
    StreamGraph 经过优化后生成的就是作业图(JobGraph),这是提交给 JobManager 的数据结构,确定了当前作业中所有任务的划分。
    主要的优化为: 将多个符合条件的节点链接在一起,合并成一个任务节点,形成算子链,这样可以减少数据交换的消耗。JobGraph 一般也是在客户端生成的,在作业提交时传递给 JobMaster。
    在上图中,分组聚合算子(Keyed Aggregation)和输出算子 Sink(print)并行度都为 2,而且是一对一的关系,满足算子链的要求,所以会合并在一起,成为一个任务节点。
  3. 执行图(ExecutionGraph)
    JobMaster 收到 JobGraph 后,会根据它来生成执行图(ExecutionGraph)。ExecutionGraph是 JobGraph 的并行化版本,是调度层最核心的数据结构。
    从上图中可以看到,与JobGraph最大的区别就是按照并行度对并行子任务进行了拆分,并明确了任务间数据传输的方式。
  4. 物理图(Physical Graph)
    JobMaster 生成执行图后, 会将它分发给 TaskManager;各个 TaskManager 会根据执行图部署任务,最终的物理执行过程也会形成一张“图”,一般就叫作物理图(Physical Graph)。这只是具体执行层面的图,并不是一个具体的数据结构。
    对应在上图中,物理图主要就是在执行图的基础上,进一步确定数据存放的位置和收发的具体方式。有了物理图,TaskManager 就可以对传递来的数据进行处理计算了。
    所以我们可以看到,程序里定义了四个算子操作:源(Source)->转换(flatMap)->分组聚合(keyBy/sum)->输出(print);合并算子链进行优化之后,就只有三个任务节点了;再考
    虑并行度后,一共有 5 个并行子任务,最终需要 5 个线程来执行。
    7.4.5 任务(Tasks)和任务槽(Task Slots)
    通过前几小节的介绍,我们对任务的生成和分配已经非常清楚了。上一小节中我们最终得到结论:作业划分为 5 个并行子任务,需要 5 个线程并行执行。那在我们将应用提交到 Flink集群之后,到底需要占用多少资源呢?是否需要 5 个 TaskManager 来运行呢?
    7.4.5.1 任务槽(Task Slots)
    之前已经提到过,Flink 中每一个 worker(也就是 TaskManager)都是一个 JVM 进程,它可以启动多个独立的线程,来并行执行多个子任务(subtask)。
    所以如果想要执行 5 个任务,并不一定非要 5 个 TaskManager,我们可以让 TaskManager多线程执行任务。如果可以同时运行 5 个线程,那么只要一个 TaskManager 就可以满足我们之前程序的运行需求了。
    很显然,TaskManager 的计算资源是有限的,并不是所有任务都可以放在一个 TaskManager上并行执行。并行的任务越多,每个线程的资源就会越少。那一个 TaskManager 到底能并行处理多少个任务呢?为了控制并发量,我们需要在 TaskManager 上对每个任务运行所占用的资源做出明确的划分,这就是所谓的任务槽(task slots)。
    slot 的概念其实在分布式框架中并不陌生。所谓的“槽”是一种形象的表达。如果大家见过传说中的“卡带式游戏机”,就会对它有更直观的认识:游戏机上的卡槽提供了可以运行游戏的接口和资源,我们把游戏卡带插入卡槽,就可以占用游戏机的计算资源,执行卡带中的游戏程序了。一台经典的小霸王游戏机一般只有一个卡槽,而在 TaskManager 中,我们可以设置多个 slot,只要插入“卡带”——也就是分配好的任务,就可以并行执行了。

每个任务槽(task slot)其实表示了 TaskManager 拥有计算资源的一个固定大小的子集。这些资源就是用来独立执行一个子任务的。

假如一个 TaskManager 有三个 slot,那么它会将管理的内存平均分成三份,每个 slot 独自占据一份。这样一来,我们在 slot 上执行一个子任务时,相当于划定了一块内存“专款专用”,就不需要跟来自其他作业的任务去竞争内存资源了。所以现在我们只要 2 个 TaskManager,就可以并行处理分配好的 5 个任务了,如上图所示。
7.4.5.2 任务槽数量的设置
我们可以通过集群的配置文件来设定 TaskManager 的 slot 数量:
taskmanager.numberOfTaskSlots: 8
通过调整 slot 的数量,我们就可以控制子任务之间的隔离级别。
具体来说,如果一个 TaskManager 只有一个 slot,那将意味着每个任务都会运行在独立的JVM 中(当然,该 JVM 可能是通过一个特定的容器启动的);而一个 TaskManager 设置多个slot 则意味着多个子任务可以共享同一个 JVM。它们的区别在于:前者任务之间完全独立运行,隔离级别更高、彼此间的影响可以降到最小;而后者在同一个 JVM 进程中运行的任务,将共享 TCP 连接和心跳消息,也可能共享数据集和数据结构,这就减少了每个任务的运行开销,在降低隔离级别的同时提升了性能。
注意:slot 目前仅仅用来隔离内存,不会涉及 CPU 的隔离。在具体应用时,可以将 slot 数量配置为机器的 CPU 核心数,尽量避免不同任务之间对 CPU 的竞争。这也是开发环境默认并行度设为机器 CPU 数量的原因。
7.4.5.3 任务对任务槽的共享
这样看来,一共有多少任务,我们就需要有多少 slot 来并行处理它们。不过实际提交作业进行测试就会发现,我们之前的 WordCount 程序设置并行度为 2 提交,一共有 5 个并行子任务,可集群即使只有 2 个 task slot 也是可以成功提交并运行的。这又是为什么呢?
我们可以基于之前的例子继续扩展。如果我们保持 sink 任务并行度为 1 不变,而作业提交时设置全局并行度为 6,那么前两个任务节点就会各自有 6 个并行子任务,整个流处理程序则有 13 个子任务。那对于 2 个 TaskManager、每个有 3 个 slot 的集群配置来说,还能否正常运行呢?

完全没有问题。这是因为默认情况下,Flink 是允许子任务共享 slot 的。
如上图所示,只要属于同一个作业,那么对于不同任务节点的并行子任务,就可以放到同一个 slot 上执行。所以对于第一个任务节点 source→map,它的 6 个并行子任务必须分到不同的 slot 上(如果在同一 slot 就没法数据并行了),而第二个任务节点 keyBy/window/apply 的并行子任务却可以和第一个任务节点共享 slot。
于是最终结果就变成了:每个任务节点的并行子任务一字排开,占据不同的 slot;而不同的任务节点的子任务可以共享 slot。一个 slot 中,可以将程序处理的所有任务都放在这里执行,
我们把它叫作保存了整个作业的运行管道(pipeline)。
这个特性看起来有点奇怪:我们不是希望并行处理、任务之间相互隔离吗,为什么这里又允许共享 slot 呢?
我们知道,一个 slot 对应了一组独立的计算资源。在之前不做共享的时候,每个任务都平等地占据了一个 slot,但其实不同的任务对资源的占用是不同的。例如这里的前两个任务,source/map 尽管是两个算子合并算子链得到的,但它只是基本的数据读取和简单转换,计算耗时极短,一般也不需要太大的内存空间;而 window 算子所做的窗口操作,往往会涉及大量的数据、状态存储和计算,我们一般把这类任务叫作“资源密集型”(intensive)任务。当它们被平等地分配到独立的 slot 上时,实际运行我们就会发现,大量数据到来时 source/map 和 sink任务很快就可以完成,但 window 任务却耗时很久;于是下游的 sink 任务占据的 slot 就会等待闲置,而上游的 source/map 任务受限于下游的处理能力,也会在快速处理完一部分数据后阻塞对应的资源开始等待(相当于处理背压)。这样资源的利用就出现了极大的不平衡,“忙的忙死,闲的闲死”。
解决这一问题的思路就是允许 slot 共享。当我们将资源密集型和非密集型的任务同时放到一个 slot 中,它们就可以自行分配对资源占用的比例,从而保证最重的活平均分配给所有的TaskManager。
slot 共享另一个好处就是允许我们保存完整的作业管道。这样一来,即使某个 TaskManager出现故障宕机,其他节点也可以完全不受影响,作业的任务可以继续执行。
另外,同一个任务节点的并行子任务是不能共享 slot 的,所以允许 slot 共享之后,运行作业所需的 slot 数量正好就是作业中所有算子并行度的最大值。这样一来,我们考虑当前集群需要配置多少 slot 资源时,就不需要再去详细计算一个作业总共包含多少个并行子任务了,只看最大的并行度就够了。
当然,Flink 默认是允许 slot 共享的,如果希望某个算子对应的任务完全独占一个 slot,或者只有某一部分算子共享 slot,我们也可以通过设置“slot 共享组”(SlotSharingGroup)手动指定:
.map(word -> Tuple2.of(word, 1L)).slotSharingGroup(“1”);
这样,只有属于同一个 slot 共享组的子任务,才会开启 slot 共享;不同组之间的任务是完全隔离的,必须分配到不同的 slot 上。在这种场景下,总共需要的 slot 数量,就是各个 slot共享组最大并行度的总和。
7.4.5.4 任务槽和并行度的关系
直观上看,slot 就是 TaskManager 为了并行执行任务而设置的,那它和之前讲过的并行度(Parallelism)是不是一回事呢?
Slot 和并行度确实都跟程序的并行执行有关,但两者是完全不同的概念。简单来说,taskslot 是 静 态 的 概 念 , 是 指 TaskManager 具 有 的 并 发 执 行 能 力 , 可 以 通 过 参 数taskmanager.numberOfTaskSlots 进行配置;而并行度(parallelism)是动态概念,也就是TaskManager 运行程序时实际使用的并发能力,可以通过参数 parallelism.default 进行配置。换句话说,并行度如果小于等于集群中可用 slot 的总数,程序是可以正常执行的,因为 slot 不一定要全部占用,有十分力气可以只用八分;而如果并行度大于可用 slot 总数,导致超出了并行能力上限,那么心有余力不足,程序就只好等待资源管理器分配更多的资源了。
下面我们再举一个具体的例子。假设一共有 3 个 TaskManager,每一个TaskManager 中的slot 数量设置为 3 个,那么一共有 9 个 task slot,如下图所示,表示集群最多能并行执行 9个任务。

而我们定义 WordCount 程序的处理操作是四个转换算子:source→ flatMap→ reduce→ sink。
当所有算子并行度相同时,容易看出 source 和 flatMap 可以合并算子链,于是最终有三个任务节点。
如果我们没有任何并行度设置,而配置文件中默认 parallelism.default=1,那么程序运行的默认并行度为 1,总共有 3 个任务。由于不同算子的任务可以共享任务槽,所以最终占用的 slot
只有 1 个。9 个 slot只用了 1 个,有 8 个空闲,如下图所示。

如果我们更改默认参数,或者提交作业时设置并行度为 2,那么总共有 6 个任务,共享任务槽之后会占用 2 个 slot,如图示例二所示。同样,就有 7 个 slot 空闲,计算资源没有充分利用。所以可以看到,设置合适的并行度才能提高效率。
那对于这个例子,怎样设置并行度效率最高呢?当然是需要把所有的 slot 都利用起来。考虑到 slot 共享,我们可以直接把并行度设置为 9,这样所有 27 个任务就会完全占用 9 个 slot。这是当前集群资源下能执行的最大并行度,计算资源得到了充分的利用,如图示例三所示。
另外再考虑对于某个算子单独设置并行度的场景。例如,如果我们考虑到输出可能是写入文件,那会希望不要并行写入多个文件,就需要设置 sink 算子的并行度为 1。这时其他的算子并行度依然为 9,所以总共会有 19 个子任务。根据 slot 共享的原则,它们最终还是会占用全部的 9 个 slot,而 sink 任务只在其中一个 slot 上执行,如图示例四所示。通过这个例子也可以明确地看到,整个流处理程序的并行度,就应该是所有算子并行度中最大的那个,这代表了运行程序需要的 slot 数量。
8. 【扩展】Flink VS Spark
8.1 数据处理架构
Micro Batching 模式(spark)
Micro-Batching 计算模式认为 “流是批的特例”, 流计算就是将连续不断的批进行持续计算,如果批足够小那么就有足够小的延时,在一定程度上满足了99%的实时计算场景。那么那1%为啥做不到呢?这就是架构的魅力,在Micro-Batching模式的架构实现上就有一个自然流数据流入系统进行攒批的过程,这在一定程度上就增加了延时。具体如下示意图:

从上面可以看到是把输入的数据,分成微笑的批次,然后一个批次一个批次的处理,然后也是一片批次的输出.很显然Micro-Batching模式有其天生的低延时瓶颈,但任何事物的存在都有两面性,在大数据计算的发展历史上,最初Hadoop上的MapReduce就是优秀的批模式计算框架,Micro-Batching在设计和实现上可以借鉴很多成熟实践。
Native Streaming 模式(flink)
Native Streaming 计算模式认为 "“批是流的特例”,这个认知更贴切流的概念,比如一些监控类的消息流,数据库操作的binlog,实时的支付交易信息等等自然流数据都是一条,一条的流入。Native Streaming 计算模式每条数据的到来都进行计算,这种计算模式显得更自然,并且延时性能达到更低。具体如下示意图:

从上图可以看到输入的数据过来一条处理一条,然后输出,几乎不存在延迟,很明显Native Streaming模式占据了流计算领域 “低延时” 的核心竞争力,当然Native Streaming模式的实现框架是一个历史先河,第一个实现Native Streaming模式的流计算框架是第一个吃螃蟹的人,需要面临更多的挑战,后续章节我们会慢慢介绍。当然Native Streaming模式的框架实现上面很容易实现Micro-Batching和Batching模式的计算,Apache Flink就是Native Streaming计算模式的流批统一的计算引擎。
8.2 数据模型

Spark 的数据模型
Spark 最早采用 RDD 模型,达到比 MapReduce 计算快 100 倍的显著优势,对 Hadoop 生态大幅升级换代。RDD 弹性数据集是分割为固定大小的批数据,RDD 提供了丰富的底层 API 对数据集做操作。为持续降低使用门槛,Spark 社区开始开发高阶 API:DataFrame/DataSet,Spark SQL 作为统一的 API,掩盖了底层,同时针对性地做 SQL 逻辑优化和物理优化,非堆存储优化也大幅提升了性能。
Spark Streaming 里的 DStream 和 RDD 模型类似,把一个实时进来的无限数据分割为一个个小批数据集合 DStream,定时器定时通知处理系统去处理这些微批数据。劣势非常明显,API 少、难胜任复杂的流计算业务,调大吞吐量而不触发背压是个体力活。不支持乱序处理,或者说很难处理乱序的问题。Spark Streaming 仅适合简单的流处理,这里稍微解释一下,因为spark的创始人在当时认为延迟不是那么的重要,他认为现实生活中没有那么多低延迟的应用场景,所以就没太注重延迟的问题,但是随着生活多样化场景的不断增加,对实时性的要求越来越高,所以spark也注意到了这个问题,开始在延迟方面发力,进而推出了Structured Streaming,相信很快sparkstreaming就会被Structured Streaming替代掉.
Spark Structured Streaming 提供了微批和流式两个处理引擎。微批的 API 虽不如 Flink 丰富,窗口、消息时间、trigger、watermarker、流表 join、流流 join 这些常用的能力都具备了。时延仍然保持最小 100 毫秒。当前处在试验阶段的流式引擎,提供了 1 毫秒的时延,但不能保证 exactly-once 语义,支持 at-least-once 语义。同时,微批作业打了快照,作业改为流式模式重启作业是不兼容的。这一点不如 Flink 做的完美。当然了现在还在优化阶段.
综上,Spark Streaming 和 Structured Streaming 是用批计算的思路做流计算。其实,用流计算的思路开发批计算才是最合理的。对 Spark 来讲,大换血不大可能,只有局部优化。其实,Spark 里 core、streaming、structured streaming、graphx 四个模块,是四种实现思路,通过上层 SQL 统一显得不纯粹和谐。

Flink 的基本数据模型是数据流,及事件(Event)的序列。数据流作为数据的基本模型可能没有表或者数据块直观熟悉,但是可以证明是完全等效的。流可以是无边界的无限流,即一般意义上的流处理。也可以是有边界的有限流,这样就是批处理。
Flink 采用 Dataflow 模型,和 Lambda 模式不同。Dataflow 是纯粹的节点组成的一个图,图中的节点可以执行批计算,也可以是流计算,也可以是机器学习算法,流数据在节点之间流动,被节点上的处理函数实时 apply 处理,节点之间是用 netty 连接起来,两个 netty 之间 keepalive,网络 buffer 是自然反压的关键。经过逻辑优化和物理优化,Dataflow 的逻辑关系和运行时的物理拓扑相差不大。这是纯粹的流式设计,时延和吞吐理论上是最优的。
8.3 运行时架构
Spark 运行时架构
批计算是把 DAG 划分为不同 stage,DAG 节点之间有血缘关系,在运行期间一个 stage 的 task 任务列表执行完毕,销毁再去执行下一个 stage;Spark Streaming 则是对持续流入的数据划分一个批次,定时去执行批次的数据运算。Structured Streaming 将无限输入流保存在状态存储中,对流数据做微批或实时的计算,跟 Dataflow 模型比较像。
Flink 运行时架构
Flink 有统一的 runtime,在此之上可以是 Batch API、Stream API、ML、Graph、CEP 等,DAG 中的节点上执行上述模块的功能函数,DAG 会一步步转化成 ExecutionGraph,即物理可执行的图,最终交给调度系统。节点中的逻辑在资源池中的 task 上被 apply 执行,task 和 Spark 中的 task 类似,都对应线程池中的一个线程。
在 DAG 的执行上,Spark 和 Flink 有一个比较显著的区别。在 Flink 的流执行模式中,一个事件在一个节点处理完后的输出就可以发到下一个节点立即处理。这样执行引擎并不会引入额外的延迟。与之相应的,所有节点是需要同时运行的。而 Spark 的 micro batch 和一般的 batch 执行一样,处理完上游的 stage 得到输出之后才开始下游的 stage。
在流计算的运行时架构方面,Flink 明显更为统一且优雅一些。
8.4 时延和吞吐
至于延迟和吞吐方面,sparkstreaming是秒级别的,Structured Streaming是毫秒级别的,Flink是亚秒级别的,其实这个没差多少,吞吐量的话,也相差不是太大。
8.5 反压
Flink 中,下游的算子消费流入到网络 buffer 的数据,如果下游算子处理能力不够,则阻塞网络 buffer,这样也就写不进数据,那么上游算子发现无法写入,则逐级把压力向上传递,直到数据源,这种自然反压的方式非常合理。Spark Streaming 是设置反压的吞吐量,到达阈值就开始限流,从批计算上来看是合理的。从这点看Flink的反压机制是要比spark好的。
8.6 状态存储
Spark 的快照 API 是 RDD 基础能力,定时开启快照后,会对同一时刻整个内存数据持久化。Spark 一般面向大数据集计算,内存数据较大,快照不宜太频繁,会增加集群计算量。spark的状态管理目前做的比较简单,只有两个对应的算子,具体可以看这篇文章.保存在checkpoint中的。
Flink 提供文件、内存、RocksDB 三种状态存储,可以对运行中的状态数据异步持久化。打快照的机制是给 source 节点的下一个节点发一条特殊的 savepoint 或 checkpoint 消息,这条消息在每个算子之间流动,通过协调者机制对齐多个并行度的算子中的状态数据,把状态数据异步持久化。

Flink 打快照的方式,Flink 支持局部恢复快照,作业快照数据保存后,修改作业,DAG 变化,启动作业恢复快照,新作业中未变化的算子的状态仍旧可以恢复。而且 Flink 也支持增量快照,面对内存超大状态数据,增量无疑能降低网络和磁盘开销。我们会发现Flink的状态存储也有较多的选择。
8.7 API方面
Spark 的初衷之一就是用统一的编程模型来解决用户的各种需求,在这方面一直很下功夫。最初基于 RDD 的 API 就可以做各种类型的数据处理。后来为了简化用户开发,逐渐推出了更高层的 DataFrame(在 RDD 中加了列变成结构化数据)和 Datasets(在 DataFrame 的列上加了类型),并在 Spark 2.0 中做了整合(DataFrame = DataSet[Row])。Spark SQL 的支持也比较早就引入了。在加上各个处理类型 API 的不断改进,比如 Structured Streaming 以及和机器学习深度学习的交互,到了今天 Spark 的 API 可以说是非常好用的,也是 Spark 最强的方面之一。
Flink 的 API 也有类似的目标和发展路线。Flink 和 Spark 的核心 API 可以说是可以基本对应的。今天 Spark API 总体上更完备一下,比如说最近一两年大力投入的和机器学习深度学习的整合方面。Flink 在流处理相关的方面还是领先一些,比如对 watermark、window、trigger 的各种支持要比spark好很多。
8.8 总结
spark和flink最大的区别还是在对流计算的支持上面,现在两者也都在做批流统一的相关工作,从spark最新的发展来看,会发展成一个和 Flink 的流处理模式比较相似的执行引擎。不过主要的功能都还在开发中或者待开发。对将来能做到什么程度,和 Spark 原来的 batch 执行引擎怎么结合,我们拭目以待。Flink也在做大量的完善工作,也逐步在ML,图计算等领域发力,使得Flink的生态系统越来越完善。
9. Flink 概念小结
在这一章,我们在之前部署运行的基础上,深入介绍了 Flink 的系统架构和不同组件,并进一步针对不同的部署模式详细讲述了作业提交和任务处理的流程。此外,通过展开讲解架构中的一些重要概念,解答了 Flink 任务调度的核心问题,并对分布式流处理架构的设计做了思考分析。
本章内容不仅是 Flink 架构知识的学习,更是分布式处理思想的入门。我们可以通过 Flink这样一个经典框架的学习,触摸到分布式架构的底层原理。
Flink 流处理架构设计还涉及事件时间、状态管理以及检查点等重要概念,保证分布式流处理系统的低延迟、时间正确性和状态一致性。我们将在后面的章节对这些内容做详细展开。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值