Repository: wangzhiwubigdata/God-Of-BigData
Branch: master
Commit: 4b83d906ac7b
Files: 250
Total size: 1.6 MB
Directory structure:
gitextract_brrx73xl/
├── Flink/
│ ├── 10-Flink集群的高可用(搭建篇补充).md
│ ├── 11-时间戳和水印.md
│ ├── 12-Broadcast广播变量.md
│ ├── 13-Flink-Kafka-Connector.md
│ ├── 14-Flink-Table-&-SQL.md
│ ├── 15-Flink实战项目之实时热销排行.md
│ ├── 16-Flink-Redis-Sink.md
│ ├── 17-Flink消费Kafka写入Mysql.md
│ ├── 6-Flink重启策略.md
│ ├── 7-Flink的分布式缓存.md
│ ├── 8-Flink中的窗口.md
│ ├── 9-Flink中的Time.md
│ ├── Flink从入门到放弃(入门篇1)-Flink是什么?.md
│ ├── Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.md
│ ├── Flink从入门到放弃(入门篇3)-DataSetAPI.md
│ ├── Flink从入门到放弃(入门篇4)-DataStreamAPI.md
│ └── Flink集群部署.md
├── Flink漫谈系列/
│ ├── Apache-Flink-漫谈系列(02)-Watermark.md
│ ├── Apache-Flink-漫谈系列(03)-State.md
│ ├── Apache-Flink漫谈系列(1)-概述.md
│ └── 我的Markdown笔记/
│ └── Apache-Flink-漫谈系列(03)-State.md
├── Hadoop/
│ ├── Hadoop极简入门.md
│ └── MapReduce编程模型和计算框架架构原理.md
├── JVM/
│ ├── HotSpot垃圾收集器.md
│ ├── HotSpot虚拟机对象探秘.md
│ ├── JVM 性能调优.md
│ ├── JVM内存结构.md
│ ├── jvm系列(一)java类的加载机制.md
│ ├── jvm系列(三)GC算法 垃圾收集器.md
│ ├── jvm系列(二)JVM内存结构.md
│ ├── jvm系列(五)Java GC 分析.md
│ ├── jvm系列(四)jvm调优-命令大全(jps jstat jmap jhat jstack jinfo).md
│ ├── 内存分配与回收策略.md
│ ├── 垃圾收集策略与算法.md
│ ├── 我的Markdown笔记/
│ │ └── jvm系列(五)Java GC 分析.md
│ ├── 类加载器.md
│ ├── 类加载的时机.md
│ ├── 类加载的过程.md
│ └── 类文件结构.md
├── Java高级特性增强/
│ ├── Java NIO之Buffer(缓冲区).md
│ ├── Java NIO之Channel(通道).md
│ ├── Java NIO之Selector(选择器).md
│ ├── Java NIO之拥抱Path和Files.md
│ ├── NIO概览.md
│ ├── 大数据成神之路-Java高级特性增强(HashMap).md
│ ├── 大数据成神之路-Java高级特性增强(HashSet).md
│ ├── 大数据成神之路-Java高级特性增强(LinkedHashMap).md
│ ├── 大数据成神之路-Java高级特性增强(Synchronized关键字).md
│ ├── 大数据成神之路-Java高级特性增强(volatile关键字).md
│ ├── 大数据成神之路-Java高级特性增强(多线程).md
│ ├── 大数据成神之路-Java高级特性增强(锁).md
│ ├── 大数据成神之路-Java高级特性增强(集合框架).md
│ ├── 大数据成神之路-Java高级特性增强-NIO.md
│ └── 大数据成神之路-Java高级特性增强.md
├── Kafka/
│ ├── Apache-Kafka安装和使用.md
│ ├── Apache-Kafka核心概念.md
│ ├── Apache-Kafka核心组件和流程(副本管理器).md
│ ├── Apache-Kafka核心组件和流程-协调器.md
│ ├── Apache-Kafka核心组件和流程-控制器.md
│ ├── Apache-Kafka核心组件和流程-日志管理器.md
│ ├── Apache-Kafka简介.md
│ └── Apache-Kafka编程实战.md
├── Linux基础/
│ └── Linux基础和命令.md
├── NIO/
│ ├── Java NIO之Buffer(缓冲区).md
│ ├── Java NIO之Channel(通道).md
│ ├── Java NIO之Selector(选择器).md
│ ├── Java NIO之拥抱Path和Files.md
│ └── NIO概览.md
├── Netty/
│ ├── Netty源码解析-概述篇.md
│ ├── Netty源码解析1-Buffer.md
│ ├── Netty源码解析2-Reactor.md
│ ├── Netty源码解析3-Pipeline.md
│ ├── Netty源码解析4-Handler综述.md
│ ├── Netty源码解析5-ChannelHandler.md
│ ├── Netty源码解析6-ChannelHandler实例之LoggingHandler.md
│ ├── Netty源码解析7-ChannelHandler实例之TimeoutHandler.md
│ ├── Netty源码解析8-ChannelHandler实例之CodecHandler.md
│ ├── Netty源码解析9-ChannelHandler实例之MessageToByteEncoder.md
│ └── 关于Netty我们都需要知道什么.md
├── README.md
├── RPC/
│ ├── RPC的原理和框架.md
│ ├── RPC简单介绍.md
│ └── 手把手教你实现一个简单的RPC.md
├── WechatArticles/
│ └── flink.md
├── pictures/
│ ├── bigdata-notes-icon.psd
│ └── 大数据技术栈思维导图.xmind
├── zookeeeper/
│ ├── ZooKeeper应用程序.md
│ ├── zk安装和运行.md
│ ├── zk开发实例.md
│ └── zk服务.md
├── 分布式理论/
│ ├── 分布式ID生成器解决方案.md
│ ├── 分布式事务的解决方案.md
│ ├── 分布式系统理论基础一: 一致性、2PC和3PC.md
│ ├── 分布式系统理论基础三-时间、时钟和事件顺序.md
│ ├── 分布式系统理论基础二-CAP.md
│ ├── 分布式系统理论进阶 - Paxos.md
│ ├── 分布式系统理论进阶 - Raft、Zab.md
│ ├── 分布式系统理论进阶:选举、多数派和租约.md
│ ├── 分布式系统的一些基本概念.md
│ ├── 分布式锁的解决方案(二).md
│ └── 分布式锁的解决方案.md
├── 大数据框架学习/
│ ├── Azkaban_Flow_1.0_的使用.md
│ ├── Azkaban_Flow_2.0_的使用.md
│ ├── Azkaban简介.md
│ ├── Flink_Data_Sink.md
│ ├── Flink_Data_Source.md
│ ├── Flink_Data_Transformation.md
│ ├── Flink_Windows.md
│ ├── Flink开发环境搭建.md
│ ├── Flink核心概念综述.md
│ ├── Flink状态管理与检查点机制.md
│ ├── Flume整合Kafka.md
│ ├── Flume简介及基本使用.md
│ ├── HDFS-Java-API.md
│ ├── HDFS常用Shell命令.md
│ ├── Hadoop-HDFS.md
│ ├── Hadoop-MapReduce.md
│ ├── Hadoop-YARN.md
│ ├── Hbase_Java_API.md
│ ├── Hbase_Shell.md
│ ├── Hbase协处理器详解.md
│ ├── Hbase容灾与备份.md
│ ├── Hbase的SQL中间层_Phoenix.md
│ ├── Hbase简介.md
│ ├── Hbase系统架构及数据结构.md
│ ├── Hbase过滤器详解.md
│ ├── HiveCLI和Beeline命令行的基本使用.md
│ ├── Hive分区表和分桶表.md
│ ├── Hive常用DDL操作.md
│ ├── Hive常用DML操作.md
│ ├── Hive数据查询详解.md
│ ├── Hive简介及核心概念.md
│ ├── Hive视图和索引.md
│ ├── Kafka消费者详解.md
│ ├── Kafka深入理解分区副本机制.md
│ ├── Kafka生产者详解.md
│ ├── Kafka简介.md
│ ├── Scala函数和闭包.md
│ ├── Scala列表和集.md
│ ├── Scala基本数据类型和运算符.md
│ ├── Scala数组.md
│ ├── Scala映射和元组.md
│ ├── Scala模式匹配.md
│ ├── Scala流程控制语句.md
│ ├── Scala简介及开发环境配置.md
│ ├── Scala类和对象.md
│ ├── Scala类型参数.md
│ ├── Scala继承和特质.md
│ ├── Scala隐式转换和隐式参数.md
│ ├── Scala集合类型.md
│ ├── SparkSQL_Dataset和DataFrame简介.md
│ ├── SparkSQL外部数据源.md
│ ├── SparkSQL常用聚合函数.md
│ ├── SparkSQL联结操作.md
│ ├── Spark_RDD.md
│ ├── Spark_Streaming与流处理.md
│ ├── Spark_Streaming基本操作.md
│ ├── Spark_Streaming整合Flume.md
│ ├── Spark_Streaming整合Kafka.md
│ ├── Spark_Structured_API的基本使用.md
│ ├── Spark_Transformation和Action算子.md
│ ├── Spark简介.md
│ ├── Spark累加器与广播变量.md
│ ├── Spark部署模式与作业提交.md
│ ├── Spring+Mybtais+Phoenix整合.md
│ ├── Sqoop基本使用.md
│ ├── Sqoop简介与安装.md
│ ├── Storm三种打包方式对比分析.md
│ ├── Storm和流处理简介.md
│ ├── Storm核心概念详解.md
│ ├── Storm编程模型详解.md
│ ├── Storm集成HBase和HDFS.md
│ ├── Storm集成Kakfa.md
│ ├── Storm集成Redis详解.md
│ ├── Zookeeper_ACL权限控制.md
│ ├── Zookeeper_Java客户端Curator.md
│ ├── Zookeeper常用Shell命令.md
│ ├── Zookeeper简介及核心概念.md
│ ├── installation/
│ │ ├── Azkaban_3.x_编译及部署.md
│ │ ├── Flink_Standalone_Cluster.md
│ │ ├── HBase单机环境搭建.md
│ │ ├── HBase集群环境搭建.md
│ │ ├── Hadoop单机环境搭建.md
│ │ ├── Hadoop集群环境搭建.md
│ │ ├── Linux下Flume的安装.md
│ │ ├── Linux下JDK安装.md
│ │ ├── Linux下Python安装.md
│ │ ├── Linux环境下Hive的安装部署.md
│ │ ├── Spark开发环境搭建.md
│ │ ├── Spark集群环境搭建.md
│ │ ├── Storm单机环境搭建.md
│ │ ├── Storm集群环境搭建.md
│ │ ├── Zookeeper单机环境和集群环境搭建.md
│ │ ├── 基于Zookeeper搭建Hadoop高可用集群.md
│ │ ├── 基于Zookeeper搭建Kafka高可用集群.md
│ │ └── 虚拟机静态IP及多IP配置.md
│ ├── 大数据学习路线.md
│ ├── 大数据常用软件安装指南.md
│ ├── 大数据应用常用打包方式.md
│ ├── 大数据技术栈思维导图.md
│ └── 资料分享与工具推荐.md
├── 实战系列文章/
│ ├── Flink实战.md
│ ├── Kafka实战.md
│ ├── OLAP.md
│ ├── Spark实战.md
│ ├── 数据仓库.md
│ └── 面试系列.md
├── 并发容器/
│ ├── 大数据成神之路-Java高级特性增强(ArrayBlockingQueue).md
│ ├── 大数据成神之路-Java高级特性增强(ConcurrentHashMap).md
│ ├── 大数据成神之路-Java高级特性增强(ConcurrentLinkedQueue).md
│ ├── 大数据成神之路-Java高级特性增强(ConcurrentSkipListMap).md
│ ├── 大数据成神之路-Java高级特性增强(ConcurrentSkipListSet).md
│ ├── 大数据成神之路-Java高级特性增强(CopyOnWriteArrayList).md
│ ├── 大数据成神之路-Java高级特性增强(CopyOnWriteArraySet).md
│ ├── 大数据成神之路-Java高级特性增强(LinkedBlockingDeque).md
│ ├── 大数据成神之路-Java高级特性增强(LinkedBlockingQueue).md
│ └── 大数据成神之路-Java高级特性增强(并发容器大纲).md
└── 面试系列/
├── Flume面试题整理/
│ └── Flume.md
├── HBase面试题整理/
│ └── HBase.md
├── Hadoop面试题总结/
│ ├── Hadoop面试题总结(一).md
│ ├── Hadoop面试题总结(三)——MapReduce.md
│ ├── Hadoop面试题总结(二)——HDFS.md
│ ├── Hadoop面试题总结(五)——优化问题.md
│ └── Hadoop面试题总结(四)——YARN.md
├── Hive面试题总结/
│ ├── Hive(一).md
│ └── Hive(二).md
├── Kafka面试题整理/
│ ├── Kafka(一).md
│ └── Kafka(二).md
├── Spark面试题整理/
│ ├── Spark调优/
│ │ ├── Shuffle配置调优.md
│ │ ├── 数据倾斜.md
│ │ ├── 程序开发调优.md
│ │ └── 资源调优.md
│ ├── Spark(一).md
│ ├── Spark(三).md
│ ├── Spark(二).md
│ └── Spark(四).md
├── Zookeeper面试题总结/
│ └── Zookeeper.md
└── pics/
├── Flume面试题Pics/
│ └── 1.md
├── HBase面试题Pics/
│ └── 1.md
├── Hadoop面试题Pics/
│ ├── 1.md
│ ├── HDFS文档-Pics/
│ │ └── 1.md
│ ├── MR-Pics/
│ │ └── 1.md
│ └── YARN-Pics/
│ └── 1.md
├── Hive面试题Pics/
│ └── 1.md
├── Kafka面试题Pics/
│ └── 1.md
├── Spark面试题Pics/
│ ├── 1.md
│ ├── 数据倾斜调优/
│ │ └── 1.md
│ ├── 程序开发调优/
│ │ └── 1.md
│ └── 资源调优/
│ └── 1.md
└── ZK面试题Pics/
└── 1.md
================================================
FILE CONTENTS
================================================
================================================
FILE: Flink/10-Flink集群的高可用(搭建篇补充).md
================================================
Flink的HA搭建并不复杂,本质来说就是配置2个jobmanager。
本文作为Flink集群部署的补充篇。
> 这篇文章来自网络,向作者尼小摩致敬,
## 概述
JobManager 协调每个 Flink 部署。它负责调度和资源管理。
默认情况下,每个 Flink 集群只有一个 JobManager 实例。 这会产生单点故障(SPOF):如果 JobManager 崩溃,则无法提交新作业并且导致运行中的作业运行失败。
使用 JobManager 高可用性模式,可以避免这个问题,从而消除 SPOF。您可以为Standalone和 YARN 集群配置高可用性。
## Standalone集群高可用性
针对 Standalone 集群的 JobManager 高可用性的一般概念是,任何时候都有一个 主 JobManager 和多个备 JobManagers,以便在主节点失败时有备 JobManagers 来接管集群。这保证了没有单点故障,一旦备 JobManager 接管集群,作业就可以正常运行。主备 JobManager 实例之间没有明显的区别。每个 JobManager 都可以充当主备节点。
例如,请考虑以下三个 JobManager 实例的设置:
.resources/6278EDED-A65A-4539-A16D-7BCD9FE77864.png)
### 配置
要启用 JobManager 高可用性,您必须将高可用性模式设置为 zookeeper,配置 zookeeper quorum 将所有 JobManager 主机及其 web UI 端口写入配置文件。
Flink利用 ZooKeeper 在所有正在运行的 JobManager 实例之间进行分布式协调。 ZooKeeper 是独立于 Flink 的服务,通过 Leader 选举和轻量级一致状态存储提供高可靠的分布式协调。
### Masters文件 (masters服务器)
要启动HA集群,请在以下位置配置Master文件
* conf/masters:masters文件:masters文件包含启动 jobmanager 的所有主机和 web 用户界面绑定的端口。
```
jobManagerAddress1:webUIPort1
[...]
jobManagerAddressX:webUIPortX
```
默认情况下,job manager选一个随机端口作为进程随机通信端口。您可以通过 high-availability.jobmanager.port 键修改此设置。此配置接受单个端口(例如50010),范围(50000-50025)或两者的组合(50010,50011,50020-50025,50050-50075)。
### 配置文件(flink-conf.yaml)
要启动HA集群,请将以下配置键添加到 conf/flink-conf.yaml:
* 高可用性模式(必需):在 conf/flink-conf.yaml 中,必须将高可用性模式设置为zookeeper,以打开高可用模式。或者将此选项设置为工厂类的 FQN,Flink 通过创建 HighAvailabilityServices 实例使用。
```
high-availability: zookeeper
```
* Zookeeper quorum(必需): ZooKeeper quorum 是 ZooKeeper 服务器的复制组,它提供分布式协调服务。
```
high-availability.zookeeper.quorum:address1:2181[,...],addressX:2181
```
每个 addressX:port 都是一个 ZooKeeper 服务器的ip及其端口,Flink 可以在指定的地址和端口访问zookeeper。
* ZooKeeper root (推荐): ZooKeeper 根节点,在该节点下放置所有集群节点。
```
high-availability.zookeeper.path.root: /flink
```
* ZooKeeper cluster-id(推荐): ZooKeeper的cluster-id节点,在该节点下放置集群的所有相关数据。
```
high-availability.cluster-id: /default_ns # important: customize per cluster
```
**重要:** 在运行 YARN 或其他群集管理器中运行时,不要手动设置此值。在这些情况下,将根据应用程序 ID 自动生成 cluster-id。 手动设置 cluster-id 会覆盖 YARN 中的自动生成的 ID。反过来,使用 -z CLI 选项指定 cluster-id 会覆盖手动配置。如果在裸机上运行多个 Flink HA 集群,则必须为每个集群手动配置单独的 cluster-id。
* 存储目录(必需): JobManager 元数据保存在文件系统 storageDir 中,在 ZooKeeper 中仅保存了指向此状态的指针。
```
high-availability.storageDir: hdfs:///flink/recovery
```
该storageDir 中保存了 JobManager 恢复状态所需的所有元数据。
配置 master 文件和 ZooKeeper quorum 之后,您可以使用提供的集群启动脚本。它们将启动 HA 群集。请注意,启动 Flink HA 集群前,必须启动 Zookeeper 集群,并确保为要启动的每个 HA 群集配置单独的 ZooKeeper 根路径。
**示例:具有2个 JobManager 的 Standalone 集群**
1. 在conf/flink-conf.yaml 中配置高可用模式和 ZooKeeper quorum:
```
high-availability: zookeeper
high-availability.zookeeper.quorum: localhost:2181
high-availability.zookeeper.path.root: /flink
high-availability.cluster-id: /cluster_one
high-availability.storageDir: hdfs:///flink/recovery
```
2. 在 conf/master 中配置 master:
```
localhost:8081
localhost:8082
```
3. 在 conf/zoo.cfg 中配置 ZooKeeper 服务(目前,每台机器只能运行一个 ZooKeeper 进程)
```
server.0=localhost:2888:3888
```
4. 启动 ZooKeeper quorum:
```
$ bin/start-zookeeper-quorum.sh
Starting zookeeper daemon on host localhost.
```
5. 启动 Flink HA 集群:
```
$ bin/start-cluster.sh
Starting HA cluster with 2 masters and 1 peers in ZooKeeper quorum.
Starting jobmanager daemon on host localhost.
Starting jobmanager daemon on host localhost.
Starting taskmanager daemon on host localhost.
```
6. 停止 Zookeeper quorum 和集群:
```
$ bin/stop-cluster.sh
Stopping taskmanager daemon (pid: 7647) on localhost.
Stopping jobmanager daemon (pid: 7495) on host localhost.
Stopping jobmanager daemon (pid: 7349) on host localhost.
$ bin/stop-zookeeper-quorum.sh
Stopping zookeeper daemon (pid: 7101) on host localhost.
```
## YARN 集群的高可用性
在运行高可用性 YARN 集群时,我们不会运行多个 JobManager (ApplicationMaster) 实例,而只运行一个,该JobManager实例失败时,YARN会将其重新启动。Yarn的具体行为取决于您使用的 YARN 版本。
### 配置
Application Master最大重试次数(yarn-site.xml)
在YARN 配置文件 yarn-site.xml 中,需要配置 application master 的最大重试次数:
```
yarn.resourcemanager.am.max-attempts
4
The maximum number of application master execution attempts.
```
当前 YARN 版本的默认值是2(表示允许单个JobManager失败两次)。
Application Attempts(flink-conf.yaml):
除了HA配置(参考上文)之外,您还必须配置最大重试次数 conf/flink-conf.yaml:
```
yarn.application-attempts: 10
```
这意味着在如果程序启动失败,YARN会再重试9次(9 次重试 + 1次启动)。如果 YARN 操作需要,如果启动10次作业还失败,yarn才会将该任务的状态置为失败。如果抢占,节点硬件故障或重启,NodeManager 重新同步等操作需要,YARN继续尝试启动应用。 这些重启不计入 yarn.application-attempts 个数中。重要的是要注意 yarn.resourcemanager.am.max-attempts 为yarn中程序重启上限。因此, Flink 中设置的程序尝试次数不能超过 YARN 的集群设置。
### 示例:高可用的YARN Session
1.配置 HA 模式和 ZooKeeper 集群在 conf/flink-conf.yaml 中:
```
high-availability: zookeeper
high-availability.zookeeper.quorum: localhost:2181
high-availability.storageDir: hdfs:///flink/recovery
high-availability.zookeeper.path.root: /flink
yarn.application-attempts: 10
```
2. 配置 ZooKeeper 服务在 conf/zoo.cfg 中(目前每台机器只能运行一个 ZooKeeper 进程):
```
server.0=localhost:2888:3888
```
3. 启动 ZooKeeper 集群:
```
$ bin/start-zookeeper-quorum.sh
Starting zookeeper daemon on host localhost.
```
4. 启动 HA 集群:
```
$ bin / yarn-session.sh -n 2
```
### 配置 Zookeeper 安全性
如果 ZooKeeper 使用 Kerberos 以安全模式运行,flink-conf.yaml 根据需要覆盖以下配置:
```
zookeeper.sasl.service-name: zookeeper
# 默认设置是 “zookeeper” 。如果 ZooKeeper 集群配置了
# 不同的服务名称,那么可以在这里提供。
zookeeper.sasl.login-context-name: Client
# 默认设置是 “Client”。该值配置需要匹配
# "security.kerberos.login.contexts"中的其中一个值。
```
有关 Kerberos 安全性的 Flink 配置的更多信息,请参阅 此处。您还可以在 此处 找到关于 Flink 内部如何设置基于 kerberos 的安全性的详细信息。
### Bootstrap ZooKeeper
如果您没有正在运行的ZooKeeper,则可以使用Flink程序附带的脚本。
这是一个 ZooKeeper 配置模板 conf/zoo.cfg。您可以为主机配置为使用 server.X 条目运行 ZooKeeper,其中 X 是每个服务器的唯一IP:
```
server.X=addressX:peerPort:leaderPort
[...]
server.Y=addressY:peerPort:leaderPort
```
该脚本 bin/start-zookeeper-quorum.sh 将在每个配置的主机上启动 ZooKeeper 服务器。 Flink wrapper 会启动 ZooKeeper 服务,该 wraper 从 conf/zoo.cfg 中读取配置,并设置一些必需的配置项。在生产设置中,建议您使用自己安装的 ZooKeeper。
================================================
FILE: Flink/11-时间戳和水印.md
================================================
本文作者为阿里巴巴高级技术专家:金竹,原文发表在云栖社区。
地址为:https://yq.aliyun.com/articles/666056?spm=a2c4e.11155435.0.0.106e1b10snGqMd
## 实际问题(乱序)
在介绍Watermark相关内容之前我们先抛出一个具体的问题,在实际的流式计算中数据到来的顺序对计算结果的正确性有至关重要的影响,比如:某数据源中的某些数据由于某种原因(如:网络原因,外部存储自身原因)会有5秒的延时,也就是在实际时间的第1秒产生的数据有可能在第5秒中产生的数据之后到来(比如到Window处理节点).选具体某个delay的元素来说,假设在一个5秒的Tumble窗口(详见Window介绍章节),有一个EventTime是 11秒的数据,在第16秒时候到来了。图示第11秒的数据,在16秒到来了,如下图:

那么对于一个Count聚合的Tumble(5s)的window,上面的情况如何处理才能window2=4,window3=2 呢?Apache Flink的时间类型
开篇我们描述的问题是一个很常见的TimeWindow中数据乱序的问题,乱序是相对于事件产生时间和到达Apache Flink 实际处理算子的顺序而言的,关于时间在Apache Flink中有如下三种时间类型,如下图:

那么对于一个Count聚合的Tumble(5s)的window,上面的情况如何处理才能window2=4,window3=2 呢?
## Apache Flink的时间类型
开篇我们描述的问题是一个很常见的TimeWindow中数据乱序的问题,乱序是相对于事件产生时间和到达Apache Flink 实际处理算子的顺序而言的,关于时间在Apache Flink中有如下三种时间类型,如下图:

* ProcessingTime
是数据流入到具体某个算子时候相应的系统时间。ProcessingTime 有最好的性能和最低的延迟。但在分布式计算环境中ProcessingTime具有不确定性,相同数据流多次运行有可能产生不同的计算结果。
* IngestionTime
IngestionTime是数据进入Apache Flink框架的时间,是在Source Operator中设置的。与ProcessingTime相比可以提供更可预测的结果,因为IngestionTime的时间戳比较稳定(在源处只记录一次),同一数据在流经不同窗口操作时将使用相同的时间戳,而对于ProcessingTime同一数据在流经不同窗口算子会有不同的处理时间戳。
* EventTime
EventTime是事件在设备上产生时候携带的。在进入Apache Flink框架之前EventTime通常要嵌入到记录中,并且EventTime也可以从记录中提取出来。在实际的网上购物订单等业务场景中,大多会使用EventTime来进行数据计算。
开篇描述的问题和本篇要介绍的Watermark所涉及的时间类型均是指EventTime类型。
## 什么是Watermark
Watermark是Apache Flink为了处理EventTime 窗口计算提出的一种机制,本质上也是一种时间戳,由Apache Flink Source或者自定义的Watermark生成器按照需求Punctuated或者Periodic两种方式生成的一种系统Event,与普通数据流Event一样流转到对应的下游算子,接收到Watermark Event的算子以此不断调整自己管理的EventTime clock。 Apache Flink 框架保证Watermark单调递增,算子接收到一个Watermark时候,框架知道不会再有任何小于该Watermark的时间戳的数据元素到来了,所以Watermark可以看做是告诉Apache Flink框架数据流已经处理到什么位置(时间维度)的方式。 Watermark的产生和Apache Flink内部处理逻辑如下图所示:

## Watermark的产生方式
目前Apache Flink 有两种生产Watermark的方式,如下:
* Punctuated - 数据流中每一个递增的EventTime都会产生一个Watermark。
在实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力,所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成。
* Periodic - 周期性的(一定时间间隔或者达到一定的记录条数)产生一个Watermark。在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark,否则在极端情况下会有很大的延时。
所以Watermark的生成方式需要根据业务场景的不同进行不同的选择。
## Watermark的接口定义
对应Apache Flink Watermark两种不同的生成方式,我们了解一下对应的接口定义,如下:
* Periodic Watermarks - AssignerWithPeriodicWatermarks
```
/**
* Returns the current watermark. This method is periodically called by the
* system to retrieve the current watermark. The method may return {@code null} to
* indicate that no new Watermark is available.
*
* <p>The returned watermark will be emitted only if it is non-null and itsTimestamp
* is larger than that of the previously emitted watermark (to preserve the contract of
* ascending watermarks). If the current watermark is still
* identical to the previous one, no progress in EventTime has happened since
* the previous call to this method. If a null value is returned, or theTimestamp
* of the returned watermark is smaller than that of the last emitted one, then no
* new watermark will be generated.
*
* <p>The interval in which this method is called and Watermarks are generated
* depends on {@link ExecutionConfig#getAutoWatermarkInterval()}.
*
* @see org.Apache.flink.streaming.api.watermark.Watermark
* @see ExecutionConfig#getAutoWatermarkInterval()
*
* @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.
*/
@Nullable
Watermark getCurrentWatermark();
```
* Punctuated Watermarks - AssignerWithPunctuatedWatermarks
```
public interface AssignerWithPunctuatedWatermarks<T> extendsTimestampAssigner<T> {
/**
* Asks this implementation if it wants to emit a watermark. This method is called right after
* the {@link #extractTimestamp(Object, long)} method.
*
* <p>The returned watermark will be emitted only if it is non-null and itsTimestamp
* is larger than that of the previously emitted watermark (to preserve the contract of
* ascending watermarks). If a null value is returned, or theTimestamp of the returned
* watermark is smaller than that of the last emitted one, then no new watermark will
* be generated.
*
* <p>For an example how to use this method, see the documentation of
* {@link AssignerWithPunctuatedWatermarks this class}.
*
* @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.
*/
@Nullable
Watermark checkAndGetNextWatermark(T lastElement, long extractedTimestamp);
}
```
AssignerWithPunctuatedWatermarks 继承了TimestampAssigner接口 -TimestampAssigner
```
public interfaceTimestampAssigner<T> extends Function {
/**
* Assigns aTimestamp to an element, in milliseconds since the Epoch.
*
* <p>The method is passed the previously assignedTimestamp of the element.
* That previousTimestamp may have been assigned from a previous assigner,
* by ingestionTime. If the element did not carry aTimestamp before, this value is
* {@code Long.MIN_VALUE}.
*
* @param element The element that theTimestamp is wil be assigned to.
* @param previousElementTimestamp The previous internalTimestamp of the element,
* or a negative value, if noTimestamp has been assigned, yet.
* @return The newTimestamp.
*/
long extractTimestamp(T element, long previousElementTimestamp);
}
```
从接口定义可以看出,Watermark可以在Event(Element)中提取EventTime,进而定义一定的计算逻辑产生Watermark的时间戳。
## Watermark解决如上问题
从上面的Watermark生成接口和Apache Flink内部对Periodic Watermark的实现来看,Watermark的时间戳可以和Event中的EventTime 一致,也可以自己定义任何合理的逻辑使得Watermark的时间戳不等于Event中的EventTime,Event中的EventTime自产生那一刻起就不可以改变了,不受Apache Flink框架控制,而Watermark的产生是在Apache Flink的Source节点或实现的Watermark生成器计算产生(如上Apache Flink内置的 Periodic Watermark实现), Apache Flink内部对单流或多流的场景有统一的Watermark处理。
回过头来我们在看看Watermark机制如何解决上面的问题,上面的问题在于如何将迟来的EventTime 位11的元素正确处理。要解决这个问题我们还需要先了解一下EventTime window是如何触发的? EventTime window 计算条件是当Window计算的Timer时间戳 小于等于 当前系统的Watermak的时间戳时候进行计算。
* 当Watermark的时间戳等于Event中携带的EventTime时候,上面场景(Watermark=EventTime)的计算结果如下:

上面对应的DDL(Alibaba 企业版的Flink分支)定义如下:
```
CREATE TABLE source(
...,
Event_timeTimeStamp,
WATERMARK wk1 FOR Event_time as withOffset(Event_time, 0)
) with (
...
);
```
* 如果想正确处理迟来的数据可以定义Watermark生成策略为 Watermark = EventTime -5s, 如下:

上面对应的DDL(Alibaba 内部的DDL语法,目前正在和社区讨论)定义如下:
```
CREATE TABLE source(
...,
Event_timeTimeStamp,
WATERMARK wk1 FOR Event_time as withOffset(Event_time, 5000)
) with (
...
);
```
上面正确处理的根源是我们采取了 延迟触发 window 计算 的方式正确处理了 Late Event. 与此同时,我们发现window的延时触发计算,也导致了下游的LATENCY变大,本例子中下游得到window的结果就延迟了5s.
## 多流的Watermark处理
在实际的流计算中往往一个job中会处理多个Source的数据,对Source的数据进行GroupBy分组,那么来自不同Source的相同key值会shuffle到同一个处理节点,并携带各自的Watermark,Apache Flink内部要保证Watermark要保持单调递增,多个Source的Watermark汇聚到一起时候可能不是单调自增的,这样的情况Apache Flink内部是如何处理的呢?如下图所示:

Apache Flink内部实现每一个边上只能有一个递增的Watermark, 当出现多流携带Eventtime汇聚到一起(GroupBy or Union)时候,Apache Flink会选择所有流入的Eventtime中最小的一个向下游流出。从而保证watermark的单调递增和保证数据的完整性.如下图:

本节以一个流计算常见的乱序问题介绍了Apache Flink如何利用Watermark机制来处理乱序问题. 本篇内容在一定程度上也体现了EventTime Window中的Trigger机制依赖了Watermark(后续Window篇章会介绍)。Watermark机制是流计算中处理乱序,正确处理Late Event的核心手段。
================================================
FILE: Flink/12-Broadcast广播变量.md
================================================
## 广播变量简介
在Flink中,同一个算子可能存在若干个不同的并行实例,计算过程可能不在同一个Slot中进行,不同算子之间更是如此,因此不同算子的计算数据之间不能像Java数组之间一样互相访问,而广播变量Broadcast便是解决这种情况的。
我们可以把广播变量理解为是一个公共的共享变量,我们可以把一个dataset 数据集广播出去,然后不同的task在节点上都能够获取到,这个数据在每个节点上只会存在一份
## 用法
```
1:初始化数据
DataSet num = env.fromElements(1, 2, 3)
2:广播数据
.withBroadcastSet(toBroadcast, "num");
3:获取数据
Collection broadcastSet = getRuntimeContext().getBroadcastVariable("num");
注意:
1:广播出去的变量存在于每个节点的内存中,所以这个数据集不能太大。因为广播出去的数据,会常驻内存,除非程序执行结束
2:广播变量在初始化广播出去以后不支持修改,这样才能保证每个节点的数据都是一致的。
```
## 注意事项
### 使用广播状态,task 之间不会相互通信
只有广播的一边可以修改广播状态的内容。用户必须保证所有 operator 并发实例上对广播状态的 修改行为都是一致的。或者说,如果不同的并发实例拥有不同的广播状态内容,将导致不一致的结果。
### 广播状态中事件的顺序在各个并发实例中可能不尽相同
广播流的元素保证了将所有元素(最终)都发给下游所有的并发实例,但是元素的到达的顺序可能在并发实例之间并不相同。因此,对广播状态的修改不能依赖于输入数据的顺序。
### 所有operator task都会快照下他们的广播状态
在checkpoint时,所有的 task 都会 checkpoint 下他们的广播状态,随着并发度的增加,checkpoint 的大小也会随之增加
### 广播变量存在内存中
广播出去的变量存在于每个节点的内存中,所以这个数据集不能太大,百兆左右可以接受,Gb不能接受
## 案例
```
public class BroadCastTest {
public static void main(String[] args) throws Exception{
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
//1.封装一个DataSet
DataSet broadcast = env.fromElements(1, 2, 3);
DataSet data = env.fromElements("a", "b");
data.map(new RichMapFunction() {
private List list = new ArrayList();
@Override
public void open(Configuration parameters) throws Exception {
// 3. 获取广播的DataSet数据 作为一个Collection
Collection broadcastSet = getRuntimeContext().getBroadcastVariable("number");
list.addAll(broadcastSet);
}
@Override
public String map(String value) throws Exception {
return value + ": "+ list;
}
}).withBroadcastSet(broadcast, "number")
// 2. 广播的broadcast
.printToErr();//打印到err方便查看
}
}
```
输出结果:
```
a: [1, 2, 3]
b: [1, 2, 3]
```
================================================
FILE: Flink/13-Flink-Kafka-Connector.md
================================================
## 简介
Flink-kafka-connector用来做什么?
Kafka中的partition机制和Flink的并行度机制结合,实现数据恢复
Kafka可以作为Flink的source和sink
任务失败,通过设置kafka的offset来恢复应用
### kafka简单介绍
关于kafka,我们会有专题文章介绍,这里简单介绍几个必须知道的概念。
**1.生产者(Producer)**
顾名思义,生产者就是生产消息的组件,它的主要工作就是源源不断地生产出消息,然后发送给消息队列。生产者可以向消息队列发送各种类型的消息,如狭义的字符串消息,也可以发送二进制消息。生产者是消息队列的数据源,只有通过生产者持续不断地向消息队列发送消息,消息队列才能不断处理消息。
**2.消费者(Consumer)**
所谓消费者,指的是不断消费(获取)消息的组件,它获取消息的来源就是消息队列(即Kafka本身)。换句话说,生产者不断向消息队列发送消息,而消费者则不断从消息队列中获取消息。
**3.主题(Topic)**
主题是Kafka中一个极为重要的概念。首先,主题是一个逻辑上的概念,它用于从逻辑上来归类与存储消息本身。多个生产者可以向一个Topic发送消息,同时也可以有多个消费者消费一个Topic中的消息。Topic还有分区和副本的概念。Topic与消息这两个概念之间密切相关,Kafka中的每一条消息都归属于某一个Topic,而一个Topic下面可以有任意数量的消息。
### kafka简单操作
启动zk:nohup bin/zookeeper-server-start.sh config/zookeeper.properties &
启动server: nohup bin/kafka-server-start.sh config/server.properties &
创建一个topic:bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
查看topic:bin/kafka-topics.sh --list --zookeeper localhost:2181
发送数据:bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
启动一个消费者:bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
删除topic: bin/kafka-topics.sh --delete --zookeeper localhost:2181 --topic topn
## Flink消费Kafka注意事项
* setStartFromGroupOffsets()【默认消费策略】
默认读取上次保存的offset信息
如果是应用第一次启动,读取不到上次的offset信息,则会根据这个参数auto.offset.reset的值来进行消费数据
* setStartFromEarliest()
从最早的数据开始进行消费,忽略存储的offset信息
* setStartFromLatest()
从最新的数据进行消费,忽略存储的offset信息
* setStartFromSpecificOffsets(Map)
从指定位置进行消费
* 当checkpoint机制开启的时候,KafkaConsumer会定期把kafka的offset信息还有其他operator的状态信息一块保存起来。当job失败重启的时候,Flink会从最近一次的checkpoint中进行恢复数据,重新消费kafka中的数据。
* 为了能够使用支持容错的kafka Consumer,需要开启checkpoint
env.enableCheckpointing(5000); // 每5s checkpoint一次
## 搭建Kafka单机环境
我本地安装了一个kafka_2.11-2.1.0版本的kafka

启动Zookeeper和kafka server:
```
启动zk:nohup bin/zookeeper-server-start.sh config/zookeeper.properties &
启动server: nohup bin/kafka-server-start.sh config/server.properties &
```
创建一个topic:
```
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
```

## 实战案例
> 所有代码,我放在了我的公众号,回复**Flink**可以下载
* 海量【**java和大数据的面试题+视频资料**】整理在公众号,关注后可以下载~
* 更多大数据技术欢迎和作者一起探讨~

### Kafka作为Flink Sink
首先pom依赖:
```
org.apache.flink
flink-connector-kafka_2.11
1.7.0
```
向kafka写入数据:
```
public class KafkaProducer {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource text = env.addSource(new MyNoParalleSource()).setParallelism(1);
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
//new FlinkKafkaProducer("topn",new KeyedSerializationSchemaWrapper(new SimpleStringSchema()),properties,FlinkKafkaProducer.Semantic.EXACTLY_ONCE);
FlinkKafkaProducer producer = new FlinkKafkaProducer("test",new SimpleStringSchema(),properties);
/*
//event-timestamp事件的发生时间
producer.setWriteTimestampToKafka(true);
*/
text.addSink(producer);
env.execute();
}
}//
```
大家这里特别注意,我们实现了一个并行度为1的`MyNoParalleSource`来生产数据,代码如下:
```
//使用并行度为1的source
public class MyNoParalleSource implements SourceFunction {//1
//private long count = 1L;
private boolean isRunning = true;
/**
* 主要的方法
* 启动一个source
* 大部分情况下,都需要在这个run方法中实现一个循环,这样就可以循环产生数据了
*
* @param ctx
* @throws Exception
*/
@Override
public void run(SourceContext ctx) throws Exception {
while(isRunning){
//图书的排行榜
List books = new ArrayList<>();
books.add("Pyhton从入门到放弃");//10
books.add("Java从入门到放弃");//8
books.add("Php从入门到放弃");//5
books.add("C++从入门到放弃");//3
books.add("Scala从入门到放弃");//0-4
int i = new Random().nextInt(5);
ctx.collect(books.get(i));
//每2秒产生一条数据
Thread.sleep(2000);
}
}
//取消一个cancel的时候会调用的方法
@Override
public void cancel() {
isRunning = false;
}
}
```
代码实现了一个发送器,来发送书名等...
然后右键运行我们的程序,控制台输出如下:

开始源源不断的生产数据了。
然后我们用命令去查看一下 kafka `test`这个topic:
```
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
```
输出如下:

### Kafka作为Flink Source
直接上代码:
```
public class KafkaConsumer {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
FlinkKafkaConsumer consumer = new FlinkKafkaConsumer<>("test", new SimpleStringSchema(), properties);
//从最早开始消费
consumer.setStartFromEarliest();
DataStream stream = env
.addSource(consumer);
stream.print();
//stream.map();
env.execute();
}
}//
```
控制台输出如下:

将我们之前发往kafka的消息全部打印出来了。
================================================
FILE: Flink/14-Flink-Table-&-SQL.md
================================================
## 简介
Apache Flink具有两个关系API - 表API和SQL - 用于统一流和批处理。Table API是Scala和Java的语言集成查询API,允许以非常直观的方式组合来自关系运算符的查询,Table API和SQL接口彼此紧密集成,以及Flink的DataStream和DataSet API。您可以轻松地在基于API构建的所有API和库之间切换。例如,您可以使用CEP库从DataStream中提取模式,然后使用Table API分析模式,或者可以在预处理上运行Gelly图算法之前使用SQL查询扫描,过滤和聚合批处理表数据。
## Flink SQL的编程模型
### 创建一个TableEnvironment
TableEnvironment是Table API和SQL集成的核心概念,它主要负责:
1、在内部目录中注册一个Table
2、注册一个外部目录
3、执行SQL查询
4、注册一个用户自定义函数(标量、表及聚合)
5、将DataStream或者DataSet转换成Table
6、持有ExecutionEnvironment或者StreamExecutionEnvironment的引用
一个Table总是会绑定到一个指定的TableEnvironment中,相同的查询不同的TableEnvironment是无法通过join、union合并在一起。
TableEnvironment有一个在内部通过表名组织起来的表目录,Table API或者SQL查询可以访问注册在目录中的表,并通过名称来引用它们。
### 在目录中注册表
TableEnvironment允许通过各种源来注册一个表:
1、一个已存在的Table对象,通常是Table API或者SQL查询的结果
Table projTable = tableEnv.scan("X").select(...);
2、TableSource,可以访问外部数据如文件、数据库或者消息系统
TableSource csvSource = new CsvTableSource("/path/to/file", ...);
3、DataStream或者DataSet程序中的DataStream或者DataSet
//将DataSet转换为Table
Table table= tableEnv.fromDataSet(tableset);
### 注册TableSink
注册TableSink可用于将 Table API或SQL查询的结果发送到外部存储系统,例如数据库,键值存储,消息队列或文件系统(在不同的编码中,例如,CSV,Apache [Parquet] ,Avro,ORC],......):
```
TableSink csvSink = new CsvTableSink("/path/to/file", ...);
```
```
2、 String[] fieldNames = {"a", "b", "c"};
TypeInformation[] fieldTypes = {Types.INT, Types.STRING, Types.LONG};
tableEnv.registerTableSink("CsvSinkTable", fieldNames, fieldTypes, csvSink);
```
## 实战案例一
基于Flink SQL的WordCount:
```
public class WordCountSQL {
public static void main(String[] args) throws Exception{
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
BatchTableEnvironment tEnv = TableEnvironment.getTableEnvironment(env);
List list = new ArrayList();
String wordsStr = "Hello Flink Hello TOM";
String[] words = wordsStr.split("\\W+");
for(String word : words){
WC wc = new WC(word, 1);
list.add(wc);
}
DataSet input = env.fromCollection(list);
tEnv.registerDataSet("WordCount", input, "word, frequency");
Table table = tEnv.sqlQuery(
"SELECT word, SUM(frequency) as frequency FROM WordCount GROUP BY word");
DataSet result = tEnv.toDataSet(table, WC.class);
result.print();
}//main
public static class WC {
public String word;//hello
public long frequency;//1
// public constructor to make it a Flink POJO
public WC() {}
public WC(String word, long frequency) {
this.word = word;
this.frequency = frequency;
}
@Override
public String toString() {
return "WC " + word + " " + frequency;
}
}
}
```
输出如下:
```
WC TOM 1
WC Hello 2
WC Flink 1
```
## 实战案例二
本例稍微复杂,首先读取一个文件中的内容进行统计,并写入到另外一个文件中:
```
public class SQLTest {
public static void main(String[] args) throws Exception{
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
BatchTableEnvironment tableEnv = BatchTableEnvironment.getTableEnvironment(env);
env.setParallelism(1);
DataSource input = env.readTextFile("test.txt");
input.print();
//转换成dataset
DataSet topInput = input.map(new MapFunction() {
@Override
public Orders map(String s) throws Exception {
String[] splits = s.split(" ");
return new Orders(Integer.valueOf(splits[0]), String.valueOf(splits[1]),String.valueOf(splits[2]), Double.valueOf(splits[3]));
}
});
//将DataSet转换为Table
Table order = tableEnv.fromDataSet(topInput);
//orders表名
tableEnv.registerTable("Orders",order);
Table tapiResult = tableEnv.scan("Orders").select("name");
tapiResult.printSchema();
Table sqlQuery = tableEnv.sqlQuery("select name, sum(price) as total from Orders group by name order by total desc");
//转换回dataset
DataSet result = tableEnv.toDataSet(sqlQuery, Result.class);
//将dataset map成tuple输出
/*result.map(new MapFunction>() {
@Override
public Tuple2 map(Result result) throws Exception {
String name = result.name;
Double total = result.total;
return Tuple2.of(name,total);
}
}).print();*/
TableSink sink = new CsvTableSink("SQLTEST.txt", "|");
//writeToSink
/*sqlQuery.writeToSink(sink);
env.execute();*/
String[] fieldNames = {"name", "total"};
TypeInformation[] fieldTypes = {Types.STRING, Types.DOUBLE};
tableEnv.registerTableSink("SQLTEST", fieldNames, fieldTypes, sink);
sqlQuery.insertInto("SQLTEST");
env.execute();
}
/**
* 源数据的映射类
*/
public static class Orders {
/**
* 序号,姓名,书名,价格
*/
public Integer id;
public String name;
public String book;
public Double price;
public Orders() {
super();
}
public Orders(Integer id, String name, String book, Double price) {
this.id = id;
this.name = name;
this.book = book;
this.price = price;
}
}
/**
* 统计结果对应的类
*/
public static class Result {
public String name;
public Double total;
public Result() {}
}
}//
```
以上所有代码,大家在公众号回复`Flink`即可下载,可以直接本地运行,方便大家调试
================================================
FILE: Flink/15-Flink实战项目之实时热销排行.md
================================================
## 需求
某个图书网站,希望看到双十一秒杀期间实时的热销排行榜单。我们可以将“实时热门商品”翻译成程序员更好理解的需求:每隔5秒钟输出最近一小时内点击量最多的前 N 个商品/图书.
## 需求分解
将这个需求进行分解我们大概要做这么几件事情:
* 告诉 Flink 框架基于时间做窗口,我们这里用processingTime,不用自带时间戳
* 过滤出图书点击行为数据
* 按一小时的窗口大小,每5秒钟统计一次,做滑动窗口聚合(Sliding Window)
* 聚合,输出窗口中点击量前N名的商品
## 代码实现
### 向Kafka发消息模拟购买事件
```
public class KafkaProducer {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource text = env.addSource(new MyNoParalleSource()).setParallelism(1);
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "127.0.0.1:9092");
//new FlinkKafkaProducer("topn",new KeyedSerializationSchemaWrapper(new SimpleStringSchema()),properties,FlinkKafkaProducer.Semantic.EXACTLY_ONCE);
FlinkKafkaProducer producer = new FlinkKafkaProducer("topn",new SimpleStringSchema(),properties);
/*
//event-timestamp事件的发生时间
producer.setWriteTimestampToKafka(true);
*/
text.addSink(producer);
env.execute();
}
}//
```
其中的:`MyNoParalleSource` 是作者自己实现的一个并行度为1的发送器,用来向kafka发送数据:
```
public class MyNoParalleSource implements SourceFunction {//1
//private long count = 1L;
private boolean isRunning = true;
/**
* 主要的方法
* 启动一个source
* 大部分情况下,都需要在这个run方法中实现一个循环,这样就可以循环产生数据了
*
* @param ctx
* @throws Exception
*/
@Override
public void run(SourceContext ctx) throws Exception {
while(isRunning){
//图书的排行榜
List books = new ArrayList<>();
books.add("Pyhton从入门到放弃");//10
books.add("Java从入门到放弃");//8
books.add("Php从入门到放弃");//5
books.add("C++从入门到放弃");//3
books.add("Scala从入门到放弃");//0-4
int i = new Random().nextInt(5);
ctx.collect(books.get(i));
//每1秒产生一条数据
Thread.sleep(1000);
}
}
//取消一个cancel的时候会调用的方法
@Override
public void cancel() {
isRunning = false;
}
}
```
可见,我们每过1秒向Kafka的topn这个topic随机发送一本书的名字用来模拟购买行为。
整体实现代码如下:
```
public class TopN {
public static void main(String[] args) throws Exception{
/**
*
* 书1 书2 书3
* (书1,1) (书2,1) (书3,1)
*
*
*/
//每隔5秒钟 计算过去1小时 的 Top 3 商品
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime); //以processtime作为时间语义
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "127.0.0.1:9092");
FlinkKafkaConsumer input = new FlinkKafkaConsumer<>("topn", new SimpleStringSchema(), properties);
//从最早开始消费 位点
input.setStartFromEarliest();
DataStream stream = env
.addSource(input);
DataStream> ds = stream
.flatMap(new LineSplitter()); //将输入语句split成一个一个单词并初始化count值为1的Tuple2类型
DataStream> wcount = ds
.keyBy(0)
.window(SlidingProcessingTimeWindows.of(Time.seconds(600),Time.seconds(5)))
//key之后的元素进入一个总时间长度为600s,每5s向后滑动一次的滑动窗口
.sum(1);// 将相同的key的元素第二个count值相加
wcount
.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(5)))//(shu1, xx) (shu2,xx)....
//所有key元素进入一个5s长的窗口(选5秒是因为上游窗口每5s计算一轮数据,topN窗口一次计算只统计一个窗口时间内的变化)
.process(new TopNAllFunction(3))
.print();
//redis sink redis -> 接口
env.execute();
}//
private static final class LineSplitter implements
FlatMapFunction> {
public void flatMap(String value, Collector> out) {
// normalize and split the line
//String[] tokens = value.toLowerCase().split("\\W+");
// emit the pairs
/*for (String token : tokens) {
if (token.length() > 0) {
out.collect(new Tuple2(token, 1));
}
}*/
//(书1,1) (书2,1) (书3,1)
out.collect(new Tuple2(value, 1));
}
}
private static class TopNAllFunction
extends
ProcessAllWindowFunction, String, TimeWindow> {
private int topSize = 3;
public TopNAllFunction(int topSize) {
this.topSize = topSize;
}
public void process(
ProcessAllWindowFunction, String, TimeWindow>.Context arg0,
Iterable> input,
Collector out) throws Exception {
TreeMap> treemap = new TreeMap>(
new Comparator() {
@Override
public int compare(Integer y, Integer x) {
return (x < y) ? -1 : 1;
}
}); //treemap按照key降序排列,相同count值不覆盖
for (Tuple2 element : input) {
treemap.put(element.f1, element);
if (treemap.size() > topSize) { //只保留前面TopN个元素
treemap.pollLastEntry();
}
}
for (Map.Entry> entry : treemap
.entrySet()) {
out.collect("=================\n热销图书列表:\n"+ new Timestamp(System.currentTimeMillis()) + treemap.toString() + "\n===============\n");
}
}
}
}//
```
查看输出:
```
=================
热销图书列表:
2019-03-05 22:32:40.004{8=(Java从入门到放弃,8), 7=(C++从入门到放弃,7), 5=(Php从入门到放弃,5)}
===============
=================
热销图书列表:
2019-03-05 22:32:45.004{8=(Java从入门到放弃,8), 7=(C++从入门到放弃,7), 5=(Php从入门到放弃,5)}
===============
```
================================================
FILE: Flink/16-Flink-Redis-Sink.md
================================================
## 简介
流式计算中,我们经常有一些场景是消费Kafka数据,进行处理,然后存储到其他的数据库或者缓存或者重新发送回其他的消息队列中。
本文讲述一个简单的Redis作为Sink的案例。
后续,我们会补充完善,比如落入Hbase,Kafka,Mysql等。
## 关于Redis Sink
Flink提供了封装好的写入Redis的包给我们用,首先我们要新增一个依赖:
```
org.apache.flink
flink-connector-redis_2.10
1.1.5
```
然后我们实现一个自己的RedisSinkExample:
```
//指定Redis set
public static final class RedisSinkExample implements RedisMapper> {
public RedisCommandDescription getCommandDescription() {
return new RedisCommandDescription(RedisCommand.SET, null);
}
public String getKeyFromData(Tuple2 data) {
return data.f0;
}
public String getValueFromData(Tuple2 data) {
return data.f1.toString();
}
}
```
我们用最简单的单机Redis的SET命令进行演示。
完整的代码如下,实现一个读取Kafka的消息,然后进行WordCount,并把结果更新到redis中:
```
public class RedisSinkTest {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
env.enableCheckpointing(2000);
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
//连接kafka
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "127.0.0.1:9092");
FlinkKafkaConsumer consumer = new FlinkKafkaConsumer<>("test", new SimpleStringSchema(), properties);
consumer.setStartFromEarliest();
DataStream stream = env.addSource(consumer);
DataStream> counts = stream.flatMap(new LineSplitter()).keyBy(0).sum(1);
//实例化FlinkJedisPoolConfig 配置redis
FlinkJedisPoolConfig conf = new FlinkJedisPoolConfig.Builder().setHost("127.0.0.1").setPort("6379").build();
//实例化RedisSink,并通过flink的addSink的方式将flink计算的结果插入到redis
counts.addSink(new RedisSink<>(conf,new RedisSinkExample()));
env.execute("WordCount From Kafka To Redis");
}//
public static final class LineSplitter implements FlatMapFunction> {
@Override
public void flatMap(String value, Collector> out) {
String[] tokens = value.toLowerCase().split("\\W+");
for (String token : tokens) {
if (token.length() > 0) {
out.collect(new Tuple2(token, 1));
}
}
}
}
//指定Redis set
public static final class RedisSinkExample implements RedisMapper> {
public RedisCommandDescription getCommandDescription() {
return new RedisCommandDescription(RedisCommand.SET, null);
}
public String getKeyFromData(Tuple2 data) {
return data.f0;
}
public String getValueFromData(Tuple2 data) {
return data.f1.toString();
}
}
}//
```
预告,后续更新写入Hbase和Mysql案例代码。
================================================
FILE: Flink/17-Flink消费Kafka写入Mysql.md
================================================
本文介绍消费Kafka的消息实时写入Mysql
1. maven新增依赖:
```
mysql
mysql-connector-java
5.1.39
```
2.重写RichSinkFunction,实现一个Mysql Sink
```
public class MysqlSink extends
RichSinkFunction> {
private Connection connection;
private PreparedStatement preparedStatement;
String username = "";
String password = "";
String drivername = ""; //配置改成自己的配置
String dburl = "";
@Override
public void invoke(Tuple3 value) throws Exception {
Class.forName(drivername);
connection = DriverManager.getConnection(dburl, username, password);
String sql = "replace into table(id,num,price) values(?,?,?)"; //假设mysql 有3列 id,num,price
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, value.f0);
preparedStatement.setString(2, value.f1);
preparedStatement.setInt(3, value.f2);
preparedStatement.executeUpdate();
if (preparedStatement != null) {
preparedStatement.close();
}
if (connection != null) {
connection.close();
}
}
}
```
3. Flink主类
```
public class MysqlSinkTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
// 1,abc,100 类似这样的数据,当然也可以是很复杂的json数据,去做解析
FlinkKafkaConsumer consumer = new FlinkKafkaConsumer<>("test", new SimpleStringSchema(), properties);
env.getConfig().disableSysoutLogging(); //设置此可以屏蔽掉日记打印情况
env.getConfig().setRestartStrategy(
RestartStrategies.fixedDelayRestart(5, 5000));
env.enableCheckpointing(2000);
DataStream stream = env
.addSource(consumer);
DataStream> sourceStream = stream.filter((FilterFunction) value -> StringUtils.isNotBlank(value))
.map((MapFunction>) value -> {
String[] args1 = value.split(",");
return new Tuple3(Integer
.valueOf(args1[0]), args1[1],Integer
.valueOf(args1[2]));
});
sourceStream.addSink(new MysqlSink());
env.execute("data to mysql start");
}
}
```
================================================
FILE: Flink/6-Flink重启策略.md
================================================
## 概述
* Flink支持不同的重启策略,以在故障发生时控制作业如何重启
* 集群在启动时会伴随一个默认的重启策略,在没有定义具体重启策略时会使用该默认策略。
* 如果在工作提交时指定了一个重启策略,该策略会覆盖集群的默认策略默认的重启策略可以通过 Flink 的配置文件 flink-conf.yaml 指定。配置参数 restart-strategy 定义了哪个策略被使用。
* 常用的重启:
1.策略固定间隔 (Fixed delay)
2.失败率 (Failure rate)
3.无重启 (No restart)
* 如果没有启用 checkpointing,则使用无重启 (no restart) 策略。如果启用了 checkpointing,但没有配置重启策略,则使用固定间隔 (fixed-delay) 策略
* 重启策略可以在flink-conf.yaml中配置,表示全局的配置。也可以在应用代码中动态指定,会覆盖全局配置
## 固定间隔
第一种:全局配置 flink-conf.yaml
```
restart-strategy: fixed-delay
restart-strategy.fixed-delay.attempts: 3
restart-strategy.fixed-delay.delay: 10 s
```
第二种:应用代码设置:
```
env.setRestartStrategy(RestartStrategies.fixedDelayRestart( 3,// 尝试重启的次数
Time.of(10, TimeUnit.SECONDS) // 间隔 ));
```
## 失败率
* 失败率重启策略在Job失败后会重启,但是超过失败率后,Job会最终被认定失败。在两个连续的重启尝试之间,重启策略会等待一个固定的时间
**下面配置是5分钟内若失败了3次则认为该job失败,重试间隔为10s**
第一种:全局配置 flink-conf.yaml
```
restart-strategy: failure-rate
restart-strategy.failure-rate.max-failures-per-interval: 3
restart-strategy.failure-rate.failure-rate-interval: 5 min
restart-strategy.failure-rate.delay: 10 s
```
第二种:应用代码设置
```
env.setRestartStrategy(RestartStrategies.failureRateRestart( 3,//一个时间段内的最大失败次数
Time.of(5, TimeUnit.MINUTES), // 衡量失败次数的是时间段 Time.of(10, TimeUnit.SECONDS) // 间隔 ));
```
## 无重启策略
第一种:全局配置 flink-conf.yaml
```
restart-strategy: none
```
第二种:应用代码设置
```
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); env.setRestartStrategy(RestartStrategies.noRestart());
```
## 实际代码演示
```
public class RestartTest {
public static void main(String[] args) {
//获取flink的运行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 每隔1000 ms进行启动一个检查点【设置checkpoint的周期】
env.enableCheckpointing(1000);
// 间隔10秒 重启3次
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3,Time.seconds(10)));
//5分钟内若失败了3次则认为该job失败,重试间隔为10s
env.setRestartStrategy(RestartStrategies.failureRateRestart(3,Time.of(5,TimeUnit.MINUTES),Time.of(10,TimeUnit.SECONDS)));
//不重试
env.setRestartStrategy(RestartStrategies.noRestart());
}//
}
```
================================================
FILE: Flink/7-Flink的分布式缓存.md
================================================
## 分布式缓存
Flink提供了一个分布式缓存,类似于hadoop,可以使用户在并行函数中很方便的读取本地文件,并把它放在taskmanager节点中,防止task重复拉取。
此缓存的工作机制如下:程序注册一个文件或者目录(本地或者远程文件系统,例如hdfs或者s3),通过ExecutionEnvironment注册缓存文件并为它起一个名称。
当程序执行,Flink自动将文件或者目录复制到所有taskmanager节点的本地文件系统,仅会执行一次。用户可以通过这个指定的名称查找文件或者目录,然后从taskmanager节点的本地文件系统访问它。
## 示例
在ExecutionEnvironment中注册一个文件:
```
//获取运行环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
//1:注册一个文件,可以使用hdfs上的文件 也可以是本地文件进行测试
env.registerCachedFile("/Users/wangzhiwu/WorkSpace/quickstart/text","a.txt");
```
在用户函数中访问缓存文件或者目录(这里是一个map函数)。这个函数必须继承RichFunction,因为它需要使用RuntimeContext读取数据:
```
DataSet result = data.map(new RichMapFunction() {
private ArrayList dataList = new ArrayList();
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
//2:使用文件
File myFile = getRuntimeContext().getDistributedCache().getFile("a.txt");
List lines = FileUtils.readLines(myFile);
for (String line : lines) {
this.dataList.add(line);
System.err.println("分布式缓存为:" + line);
}
}
@Override
public String map(String value) throws Exception {
//在这里就可以使用dataList
System.err.println("使用datalist:" + dataList + "------------" +value);
//业务逻辑
return dataList +":" + value;
}
});
result.printToErr();
}
```
完整代码如下,仔细看注释:
```
public class DisCacheTest {
public static void main(String[] args) throws Exception{
//获取运行环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
//1:注册一个文件,可以使用hdfs上的文件 也可以是本地文件进行测试
//text 中有4个单词:hello flink hello FLINK env.registerCachedFile("/Users/wangzhiwu/WorkSpace/quickstart/text","a.txt");
DataSource data = env.fromElements("a", "b", "c", "d");
DataSet result = data.map(new RichMapFunction() {
private ArrayList dataList = new ArrayList();
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
//2:使用文件
File myFile = getRuntimeContext().getDistributedCache().getFile("a.txt");
List lines = FileUtils.readLines(myFile);
for (String line : lines) {
this.dataList.add(line);
System.err.println("分布式缓存为:" + line);
}
}
@Override
public String map(String value) throws Exception {
//在这里就可以使用dataList
System.err.println("使用datalist:" + dataList + "------------" +value);
//业务逻辑
return dataList +":" + value;
}
});
result.printToErr();
}
}//
```
输出结果如下:
```
[hello, flink, hello, FLINK]:a
[hello, flink, hello, FLINK]:b
[hello, flink, hello, FLINK]:c
[hello, flink, hello, FLINK]:d
```
================================================
FILE: Flink/8-Flink中的窗口.md
================================================
## 窗口
### 窗口类型
1. flink支持两种划分窗口的方式(time和count) 如果根据时间划分窗口,那么它就是一个time-window 如果根据数据划分窗口,那么它就是一个count-window
2. flink支持窗口的两个重要属性(size和interval)
* 如果size=interval,那么就会形成tumbling-window(无重叠数据)
* 如果size>interval,那么就会形成sliding-window(有重叠数据)
* 如果size 本文是例行介绍,熟悉的直接跳过 - 鲁迅
> 鲁迅: ...
# 大纲
**入门篇:**
-Flink是什么?.resources/1.png)
**放弃篇:**
-Flink是什么?.resources/A44BE2B6-FBC9-4143-9743-F097B9C0FDD6.png)
## Flink是什么
## 一句话概括
Apache Flink是一个面向分布式数据流处理和批量数据处理的开源计算平台,提供支持流处理和批处理两种类型应用的功能。
## 前身
Apache Flink 的前身是柏林理工大学一个研究性项目, 在 2014 被 Apache 孵化器所接受,然后迅速地成为了Apache Software Foundation的顶级项目之一。
## 特点
现有的开源计算方案,会把流处理和批处理作为两种不同的应用类型:流处理一般需要支持低延迟、Exactly-once保证,而批处理需要支持高吞吐、高效处理。
Flink是完全支持流处理,也就是说作为流处理看待时输入数据流是无界的;批处理被作为一种特殊的流处理,只是它的输入数据流被定义为有界的。
## Flink组件栈
-Flink是什么?.resources/6F963775-B91B-447F-959E-38B4029BE56D.png)
### Deployment层
主要涉及了Flink的部署模式,Flink支持多种部署模式:本地、集群(Standalone/YARN)、云(GCE/EC2)
-Flink是什么?.resources/F7406066-68CA-4BE7-9743-7FD65A0D722C.png)
### Runtime层
Runtime层提供了支持Flink计算的全部核心实现,比如:支持分布式Stream处理、JobGraph到ExecutionGraph的映射、调度等等,为上层API层提供基础服务
### API层
API层主要实现了面向无界Stream的流处理和面向Batch的批处理API,其中面向流处理对应DataStream API,面向批处理对应DataSet API
### Libaries层
* 在API层之上构建的满足特定应用的实现计算框架,也分别对应于面向流处理和面向批处理两类
* 面向流处理支持:CEP(复杂事件处理)、基于SQL-like的操作(基于Table的关系操作)
* 面向批处理支持:FlinkML(机器学习库)、Gelly(图处理)
## Flink的优势
* 支持高吞吐、低延迟、高性能的流处理
* 支持高度灵活的窗口(Window)操作
* 支持有状态计算的Exactly-once语义
* 提供DataStream API和DataSet API
-Flink是什么?.resources/3DE5BD22-BFE2-49C4-8DA8-C42EAD1948FB.png)
-Flink是什么?.resources/0E6F6341-5EB0-40FD-9953-70C3F0904043.png)
## Flink基本编程模型
> * Flink程序的基础构建模块是流(streams) 与 转换(transformations)
> * 每一个数据流起始于一个或多个 source,并终止于一个或多个 sink
下面是一个由Flink程序映射为Streaming Dataflow的示意图:
-Flink是什么?.resources/656C0986-42A7-4E76-B3CA-C0372395E451.png)
并行数据流示意图:
-Flink是什么?.resources/E6A4AF88-12D9-413A-A318-06A86ABDC1AF.png)
## Flink基本架构
> * Flink是基于Master-Slave风格的架构
> * Flink集群启动时,会启动一个JobManager进程、至少一个TaskManager进程
-Flink是什么?.resources/866EF50B-A9ED-461A-AC13-78BEBBDCCFC9.png)
### JobManager
* Flink系统的协调者,它负责接收Flink Job,调度组成Job的多个Task的执行
* 收集Job的状态信息,并管理Flink集群中从节点TaskManager
### TaskManager
* 实际负责执行计算的Worker,在其上执行Flink Job的一组Task
* TaskManager负责管理其所在节点上的资源信息,如内存、磁盘、网络,在启动的时候将资源的状态向JobManager汇报
### Client
* 用户提交一个Flink程序时,会首先创建一个Client,该Client首先会对用户提交的Flink程序进行预处理,并提交到Flink集群
* Client会将用户提交的Flink程序组装一个JobGraph, 并且是以JobGraph的形式提交的
## 最后
本文是例行介绍,熟悉的直接跳过。
================================================
FILE: Flink/Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用.md
================================================
## 本地安装单机版本Flink
一般来说,线上都是集群模式,那么单机模式方便我们测试和学习。
### 环境要求
本地机器上需要有 Java 8 和 maven 环境,推荐在linux或者mac上开发Flink应用:
如果有 Java 8 环境,运行下面的命令会输出如下版本信息:
-本地环境搭建&构建第一个Flink应用.resources/34F234C6-C9D6-46AB-A864-652BE177B4CA.png)
如果有 maven 环境,运行下面的命令会输出如下版本信息:
-本地环境搭建&构建第一个Flink应用.resources/1A1D2049-1042-43E1-BE0B-6D9FAA8224BE.png)
开发工具推荐使用 ItelliJ IDEA。
#### 第一种方式
来这里[https://flink.apache.org/](https://flink.apache.org/)
看这里:
-本地环境搭建&构建第一个Flink应用.resources/E0A8FC57-9184-4BE8-8D20-BDD91C3C44FD.png)
>注意:
```
An Apache Hadoop installation is not required to use Apache Flink. For users that use Flink without any Hadoop components, we recommend the release without bundled Hadoop libraries.
```
这是啥意思?
这个意思就是说Flink可以不依赖Hadoop环境,如果说单机玩的话,下载一个`only`版本就行了。
#### 第二种方式(不推荐)
```
git clone https://github.com/apache/flink.git
cd flink
mvn clean package -DskipTests
```
然后进入编译好的Flink中去执行 `bin/start-cluster.sh`
### 其他乱七八糟的安装办法
比如 Mac用户可以用`brew install apache-flink` ,前提是安装过 `brew`这个mac下的工具.
## 启动Flink
我们先到Flink的目录下来:
如下:
```
$ flink-1.7.1 pwd
/Users/wangzhiwu/Downloads/flink-1.7.1
```
-本地环境搭建&构建第一个Flink应用.resources/BE68C066-BD15-4FAF-B649-82D9B26F255D.png)
执行命令:
-本地环境搭建&构建第一个Flink应用.resources/C88AEAF7-42B7-4AD1-A793-3E89EBE751E2.png)
接着就可以进入 web 页面(http://localhost:8081/) 查看
-本地环境搭建&构建第一个Flink应用.resources/DAEECBBB-0FB7-4D4E-B338-B3181C23B6CB.png)
恭喜你,一个单机版的flink就跑起来了。
## 构建一个应用
当然了,我们可以用maven,一顿new,new出来一个过程,这里我们将使用 Flink Maven Archetype 来创建我们的项目结构和一些初始的默认依赖。在你的工作目录下,运行如下命令来创建项目:
```
mvn archetype:generate \
-DarchetypeGroupId=org.apache.flink \
-DarchetypeArtifactId=flink-quickstart-java \
-DarchetypeVersion=1.7.2 \
-DgroupId=flink-project \
-DartifactId=flink-project \
-Dversion=0.1 \
-Dpackage=myflink \
-DinteractiveMode=false
```
这样一个工程就构建好了。
还有一个更加牛逼的办法,看这里:
```
curl https://flink.apache.org/q/quickstart.sh | bash
```
直接在命令行执行上面的命令,结果如下图:
-本地环境搭建&构建第一个Flink应用.resources/A78DC26C-BD00-44A9-9481-FE67B9BAE9CF.png)
同样可以构建一个Flink工程,而且自带一些demo。
原理是什么?点一下它看看就明白了。
[https://flink.apache.org/q/quickstart.sh](https://flink.apache.org/q/quickstart.sh)
## 编写一个入门级的WordCount
```public class WordCount {
//
// Program
//
public static void main(String[] args) throws Exception {
// set up the execution environment
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// get input data
DataSet text = env.fromElements(
"To be, or not to be,--that is the question:--",
"Whether 'tis nobler in the mind to suffer",
"The slings and arrows of outrageous fortune",
"Or to take arms against a sea of troubles,"
);
DataSet> counts =
// split up the lines in pairs (2-tuples) containing: (word,1)
text.flatMap(new LineSplitter())
// group by the tuple field "0" and sum up tuple field "1"
.groupBy(0) //(i,1) (am,1) (chinese,1)
.sum(1);
// execute and print result
counts.print();
}
//
// User Functions
//
/**
* Implements the string tokenizer that splits sentences into words as a user-defined
* FlatMapFunction. The function takes a line (String) and splits it into
* multiple pairs in the form of "(word,1)" (Tuple2<String, Integer>).
*/
public static final class LineSplitter implements FlatMapFunction> {
@Override
public void flatMap(String value, Collector> out) {
// normalize and split the line
String[] tokens = value.toLowerCase().split("\\W+");
// emit the pairs
for (String token : tokens) {
if (token.length() > 0) {
out.collect(new Tuple2(token, 1));
}
}
}
}
}
```
类似的例子,官方也有提供的,可以在这里下载:
[WordCount官方推荐](https://github.com/apache/flink/blob/master/flink-examples/flink-examples-batch/src/main/java/org/apache/flink/examples/java/wordcount/WordCount.java)
### 运行
本地右键运行:
-本地环境搭建&构建第一个Flink应用.resources/8F0D8EF2-5C0B-4067-AA87-31D7A0DC16C7.png)
提交到本地单机Flink上
* 进入工程目录,使用以下命令打包
```
mvn clean package -Dmaven.test.skip=true
```
然后,进入 flink 安装目录 bin 下执行以下命令提交程序:
```
flink run -c org.myorg.laowang.WordCount /Users/wangzhiwu/WorkSpace/quickstart/target/quickstart-0.1.jar
```
分别制定main方法和jar包的地址。
在刚才的控制台中,可以看到:
-本地环境搭建&构建第一个Flink应用.resources/EB619900-BBDE-4E32-9089-0DC867FF9220.png)
我们刚才提交过的程序。
flink的log目录下有我们提交过的任务的日志:
-本地环境搭建&构建第一个Flink应用.resources/620369FB-ABCA-4184-AA90-C7FEDB114B07.png)
## 总结
一次简单的flink之旅就完成了。
================================================
FILE: Flink/Flink从入门到放弃(入门篇3)-DataSetAPI.md
================================================
## 编程结构
```
public class SocketTextStreamWordCount {
public static void main(String[] args) throws Exception {
if (args.length != 2){
System.err.println("USAGE:\nSocketTextStreamWordCount ");
return;
}
String hostName = args[0];
Integer port = Integer.parseInt(args[1]);
final StreamExecutionEnvironment env = StreamExecutionEnvironment
.getExecutionEnvironment();
DataStream text = env.socketTextStream(hostName, port);
DataStream> counts
text.flatMap(new LineSplitter())
.keyBy(0)
.sum(1);
counts.print();
env.execute("Java WordCount from SocketTextStream Example");
}
```
上面的`SocketTextStreamWordCount`是一个典型的Flink程序,他由一下及格部分构成:
* 获得一个execution environment,
* 加载/创建初始数据,
* 指定此数据的转换,
* 指定放置计算结果的位置,
* 触发程序执行
## DataSet API
分类:
* Source: 数据源创建初始数据集,例如来自文件或Java集合
* Transformation: 数据转换将一个或多个DataSet转换为新的DataSet
* Sink: 将计算结果存储或返回
### DataSet Sources
#### 基于文件的
* `readTextFile(path)/ TextInputFormat`- 按行读取文件并将其作为字符串返回。
* `readTextFileWithValue(path)/ TextValueInputFormat`- 按行读取文件并将它们作为StringValues返回。StringValues是可变字符串。
* `readCsvFile(path)/ CsvInputFormat`- 解析逗号(或其他字符)分隔字段的文件。返回元组或POJO的DataSet。支持基本java类型及其Value对应作为字段类型。
* `readFileOfPrimitives(path, Class)/ PrimitiveInputFormat`- 解析新行(或其他字符序列)分隔的原始数据类型(如String或)的文件Integer。
* `readFileOfPrimitives(path, delimiter, Class)/ PrimitiveInputFormat`- 解析新行(或其他字符序列)分隔的原始数据类型的文件,例如String或Integer使用给定的分隔符。
* `readSequenceFile(Key, Value, path)/ SequenceFileInputFormat`- 创建一个JobConf并从类型为SequenceFileInputFormat,Key class和Value类的指定路径中读取文件,并将它们作为Tuple2 返回。
#### 基于集合
* `fromCollection(Collection)` - 从Java Java.util.Collection创建数据集。集合中的所有数据元必须属于同一类型。
* `fromCollection(Iterator, Class)` - 从迭代器创建数据集。该类指定迭代器返回的数据元的数据类型。
* `fromElements(T ...)` - 根据给定的对象序列创建数据集。所有对象必须属于同一类型。
* `fromParallelCollection(SplittableIterator, Class) `- 并行地从迭代器创建数据集。该类指定迭代器返回的数据元的数据类型。
* `generateSequence(from, to)` - 并行生成给定间隔中的数字序列。
#### 通用方法
* `readFile(inputFormat, path)/ FileInputFormat`- 接受文件输入格式。
* `createInput(inputFormat)/ InputFormat`- 接受通用输入格式。
#### 代码示例
```
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// 从本地文件系统读
DataSet localLines = env.readTextFile("file:///path/to/my/textfile");
// 读取HDFS文件
DataSet hdfsLines = env.readTextFile("hdfs://nnHost:nnPort/path/to/my/textfile");
// 读取CSV文件
DataSet> csvInput = env.readCsvFile("hdfs:///the/CSV/file").types(Integer.class, String.class, Double.class);
// 读取CSV文件中的部分
DataSet> csvInput = env.readCsvFile("hdfs:///the/CSV/file").includeFields("10010").types(String.class, Double.class);
// 读取CSV映射为一个java类
DataSet> csvInput = env.readCsvFile("hdfs:///the/CSV/file").pojoType(Person.class, "name", "age", "zipcode");
// 读取一个指定位置序列化好的文件
DataSet> tuples =
env.readSequenceFile(IntWritable.class, Text.class, "hdfs://nnHost:nnPort/path/to/file");
// 从输入字符创建
DataSet value = env.fromElements("Foo", "bar", "foobar", "fubar");
// 创建一个数字序列
DataSet numbers = env.generateSequence(1, 10000000);
// 从关系型数据库读取
DataSet dbData =
env.createInput(JDBCInputFormat.buildJDBCInputFormat() .setDrivername("org.apache.derby.jdbc.EmbeddedDriver") .setDBUrl("jdbc:derby:memory:persons")
.setQuery("select name, age from persons")
.setRowTypeInfo(new RowTypeInfo(BasicTypeInfo.STRING_TYPE_INFO, BasicTypeInfo.INT_TYPE_INFO))
.finish());
```
### DataSet Transformation
> 详细可以参考官网:https://flink.sojb.cn/dev/batch/dataset_transformations.html#filter
* Map
采用一个数据元并生成一个数据元。
```
data.map(new MapFunction() {
public Integer map(String value) { return Integer.parseInt(value); }
});
```
* FlatMap
采用一个数据元并生成零个,一个或多个数据元。
```
data.flatMap(new FlatMapFunction() {
public void flatMap(String value, Collector out) {
for (String s : value.split(" ")) {
out.collect(s);
}
}
});
```
* MapPartition
在单个函数调用中转换并行分区。该函数将分区作为Iterable流来获取,并且可以生成任意数量的结果值。每个分区中的数据元数量取决于并行度和先前的 算子操作。
```
data.mapPartition(new MapPartitionFunction() {
public void mapPartition(Iterable values, Collector out) {
long c = 0;
for (String s : values) {
c++;
}
out.collect(c);
}
});
```
* Filter
计算每个数据元的布尔函数,并保存函数返回true的数据元。
重要信息:系统假定该函数不会修改应用谓词的数据元。违反此假设可能会导致错误的结果。
```
data.filter(new FilterFunction() {
public boolean filter(Integer value) { return value > 1000; }
});
```
* Reduce
通过将两个数据元重复组合成一个数据元,将一组数据元组合成一个数据元。Reduce可以应用于完整数据集或分组数据集。
```
data.reduce(new ReduceFunction {
public Integer reduce(Integer a, Integer b) { return a + b; }
});
```
如果将reduce应用于分组数据集,则可以通过提供CombineHintto 来指定运行时执行reduce的组合阶段的方式 setCombineHint。在大多数情况下,基于散列的策略应该更快,特别是如果不同键的数量与输入数据元的数量相比较小(例如1/10)。
* ReduceGroup
将一组数据元组合成一个或多个数据元。ReduceGroup可以应用于完整数据集或分组数据集。
```
data.reduceGroup(new GroupReduceFunction {
public void reduce(Iterable values, Collector out) {
int prefixSum = 0;
for (Integer i : values) {
prefixSum += i;
out.collect(prefixSum);
}
}
});
```
* Aggregate
将一组值聚合为单个值。聚合函数可以被认为是内置的reduce函数。聚合可以应用于完整数据集或分组数据集。
```
Dataset> input = // [...]
DataSet> output = input.aggregate(SUM, 0).and(MIN, 2);
```
您还可以使用简写语法进行最小,最大和总和聚合。
```
Dataset> input = // [...]
DataSet> output = input.sum(0).andMin(2);
```
* Distinct
返回数据集的不同数据元。它相对于数据元的所有字段或字段子集从输入DataSet中删除重复条目。
```
data.distinct();
```
使用reduce函数实现Distinct。您可以通过提供CombineHintto 来指定运行时执行reduce的组合阶段的方式 setCombineHint。在大多数情况下,基于散列的策略应该更快,特别是如果不同键的数量与输入数据元的数量相比较小(例如1/10)。
* Join
通过创建在其键上相等的所有数据元对来连接两个数据集。可选地使用JoinFunction将数据元对转换为单个数据元,或使用FlatJoinFunction将数据元对转换为任意多个(包括无)数据元。请参阅键部分以了解如何定义连接键。
```
result = input1.join(input2)
.where(0) // key of the first input (tuple field 0)
.equalTo(1); // key of the second input (tuple field 1)
```
您可以通过Join Hints指定运行时执行连接的方式。提示描述了通过分区或广播进行连接,以及它是使用基于排序还是基于散列的算法。
如果未指定提示,系统将尝试估算输入大小,并根据这些估计选择最佳策略。
```
// This executes a join by broadcasting the first data set
// using a hash table for the broadcast data
result = input1.join(input2, JoinHint.BROADCAST_HASH_FIRST)
.where(0).equalTo(1);
```
请注意,连接转换仅适用于等连接。其他连接类型需要使用OuterJoin或CoGroup表示。
* OuterJoin
在两个数据集上执行左,右或全外连接。外连接类似于常规(内部)连接,并创建在其键上相等的所有数据元对。此外,如果在另一侧没有找到匹配的Keys,则保存“外部”侧(左侧,右侧或两者都满)的记录。匹配数据元对(或一个数据元和null另一个输入的值)被赋予JoinFunction以将数据元对转换为单个数据元,或者转换为FlatJoinFunction以将数据元对转换为任意多个(包括无)数据元。请参阅键部分以了解如何定义连接键。
```
input1.leftOuterJoin(input2) // rightOuterJoin or fullOuterJoin for right or full outer joins
.where(0) // key of the first input (tuple field 0)
.equalTo(1) // key of the second input (tuple field 1)
.with(new JoinFunction() {
public String join(String v1, String v2) {
// NOTE:
// - v2 might be null for leftOuterJoin
// - v1 might be null for rightOuterJoin
// - v1 OR v2 might be null for fullOuterJoin
}
});
```
* CoGroup
reduce 算子操作的二维变体。将一个或多个字段上的每个输入分组,然后关联组。每对组调用转换函数。
```
data1.coGroup(data2)
.where(0)
.equalTo(1)
.with(new CoGroupFunction() {
public void coGroup(Iterable in1, Iterable in2, Collector out) {
out.collect(...);
}
});
```
* Cross
构建两个输入的笛卡尔积(交叉乘积),创建所有数据元对。可选择使用CrossFunction将数据元对转换为单个数据元
```
DataSet data1 = // [...]
DataSet data2 = // [...]
DataSet> result = data1.cross(data2);
```
注:交叉是一个潜在的非常计算密集型 算子操作它甚至可以挑战大的计算集群!建议使用crossWithTiny()和crossWithHuge()来提示系统的DataSet大小。
* Union
生成两个数据集的并集。
```
DataSet data1 = // [...]
DataSet data2 = // [...]
DataSet result = data1.union(data2);
```
* Rebalance
均匀地Rebalance 数据集的并行分区以消除数据偏差。只有类似Map的转换可能会遵循Rebalance 转换。
```
DataSet in = // [...]
DataSet result = in.rebalance()
.map(new Mapper());
```
* Hash-Partition
散列分区给定键上的数据集。键可以指定为位置键,表达键和键选择器函数。
```
DataSet> in = // [...]
DataSet result = in.partitionByHash(0)
.mapPartition(new PartitionMapper());
```
* Range-Partition
Range-Partition给定键上的数据集。键可以指定为位置键,表达键和键选择器函数。
```
DataSet> in = // [...]
DataSet result = in.partitionByRange(0)
.mapPartition(new PartitionMapper());
```
* Custom Partitioning
手动指定数据分区。
注意:此方法仅适用于单个字段键。
```
DataSet> in = // [...]
DataSet result = in.partitionCustom(Partitioner partitioner, key)
```
* Sort Partition
本地按指定顺序对指定字段上的数据集的所有分区进行排序。可以将字段指定为元组位置或字段表达式。通过链接sortPartition()调用来完成对多个字段的排序。
```
DataSet> in = // [...]
DataSet result = in.sortPartition(1, Order.ASCENDING)
.mapPartition(new PartitionMapper());
```
* First-n
返回数据集的前n个(任意)数据元。First-n可以应用于常规数据集,分组数据集或分组排序数据集。分组键可以指定为键选择器函数或字段位置键。
```
DataSet> in = // [...]
// regular data set
DataSet> result1 = in.first(3);
// grouped data set
DataSet> result2 = in.groupBy(0) .first(3);
// grouped-sorted data set
DataSet> result3 = in.groupBy(0) .sortGroup(1, Order.ASCENDING) .first(3);
```
### DataSet Sink
数据接收器使用DataSet用于存储或返回。使用OutputFormat描述数据接收器算子操作 。Flink带有各种内置输出格式,这些格式封装在DataSet上的算子操作中:
* writeAsText()/ TextOutputFormat- 按字符串顺序写入数据元。通过调用每个数据元的toString()方法获得字符串。
* writeAsFormattedText()/ TextOutputFormat- 按字符串顺序写数据元。通过为每个数据元调用用户定义的format()方法来获取字符串。
* writeAsCsv(...)/ CsvOutputFormat- 将元组写为逗号分隔值文件。行和字段分隔符是可配置的。每个字段的值来自对象的toString()方法。
* print()/ printToErr()/ print(String msg)/ printToErr(String msg)- 在标准输出/标准错误流上打印每个数据元的toString()值。可选地,可以提供前缀(msg),其前缀为输出。这有助于区分不同的打印调用。如果并行度大于1,则输出也将与生成输出的任务的标识符一起添加。
* write()/ FileOutputFormat- 自定义文件输出的方法和基类。支持自定义对象到字节的转换。
* output()/ OutputFormat- 大多数通用输出方法,用于非基于文件的数据接收器(例如将结果存储在数据库中)。
可以将DataSet输入到多个 算子操作。程序可以编写或打印数据集,同时对它们执行其他转换。
示例:
```
// text data
DataSet textData = // [...]
// write DataSet to a file on the local file system
textData.writeAsText("file:///my/result/on/localFS");
// write DataSet to a file on a HDFS with a namenode running at nnHost:nnPort
textData.writeAsText("hdfs://nnHost:nnPort/my/result/on/localFS");
// write DataSet to a file and overwrite the file if it exists
textData.writeAsText("file:///my/result/on/localFS", WriteMode.OVERWRITE);
// tuples as lines with pipe as the separator "a|b|c"
DataSet> values = // [...]
values.writeAsCsv("file:///path/to/the/result/file", "\n", "|");
// this writes tuples in the text formatting "(a, b, c)", rather than as CSV lines
values.writeAsText("file:///path/to/the/result/file");
// this writes values as strings using a user-defined TextFormatter object
values.writeAsFormattedText("file:///path/to/the/result/file",
new TextFormatter>() {
public String format (Tuple2 value) {
return value.f1 + " - " + value.f0;
}
});
```
使用自定义输出格式:
```
DataSet> myResult = [...]
// write Tuple DataSet to a relational database
myResult.output(
// build and configure OutputFormat
JDBCOutputFormat.buildJDBCOutputFormat()
.setDrivername("org.apache.derby.jdbc.EmbeddedDriver")
.setDBUrl("jdbc:derby:memory:persons")
.setQuery("insert into persons (name, age, height) values (?,?,?)")
.finish()
);
```
## 序列化器
* Flink自带了针对诸如int,long,String等标准类型的序列化器
* 针对Flink无法实现序列化的数据类型,我们可以交给Avro和Kryo
* 使用方法:ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
```
使用avro序列化:env.getConfig().enableForceAvro();
使用kryo序列化:env.getConfig().enableForceKryo();
使用自定义序列化:env.getConfig().addDefaultKryoSerializer(Class> type, Class extends Serializer>> serializerClass)
```
## 数据类型
* Java Tuple 和 Scala case class
* Java POJOs:java实体类
* Primitive Types
默认支持java和scala基本数据类型
* General Class Types
默认支持大多数java和scala class
* Hadoop Writables
支持hadoop中实现了org.apache.hadoop.Writable的数据类型
* Special Types
例如scala中的Either Option 和Try
================================================
FILE: Flink/Flink从入门到放弃(入门篇4)-DataStreamAPI.md
================================================
DataStream算子将一个或多个DataStream转换为新DataStream。程序可以将多个转换组合成复杂的数据流拓扑。
DataStreamAPI和DataSetAPI主要的区别在于Transformation部分。
## DataStream Transformation
### map
* DataStream→DataStream
用一个数据元生成一个数据元。一个map函数,它将输入流的值加倍:
```
DataStream dataStream = //...
dataStream.map(new MapFunction() {
@Override
public Integer map(Integer value) throws Exception {
return 2 * value;
}
});
```
### FlatMap
* DataStream→DataStream
采用一个数据元并生成零个,一个或多个数据元。将句子分割为单词的flatmap函数:
```
dataStream.flatMap(new FlatMapFunction() {
@Override
public void flatMap(String value, Collector out)
throws Exception {
for(String word: value.split(" ")){
out.collect(word);
}
}
});
```
### Filter
* DataStream→DataStream
计算每个数据元的布尔函数,并保存函数返回true的数据元。过滤掉零值的过滤器:
```
dataStream.filter(new FilterFunction() {
@Override
public boolean filter(Integer value) throws Exception {
return value != 0;
}
});
```
### KeyBy
* DataStream→KeyedStream
逻辑上将流分区为不相交的分区。具有相同Keys的所有记录都分配给同一分区。在内部,keyBy()是使用散列分区实现的。指定键有不同的方法。
此转换返回KeyedStream,其中包括使用被Keys化状态所需的KeyedStream。
```
dataStream.keyBy("someKey") // Key by field "someKey"
dataStream.keyBy(0) // Key by the first element of a Tuple
```
🌺注意:
如果出现以下情况,则类型不能成为key:
* 它是POJO类型但不覆盖hashCode()方法并依赖于Object.hashCode()实现
* 任何类型的数组
### Reduce
KeyedStream→DataStream
将当前数据元与最后一个Reduce的值组合并发出新值。
例如:reduce函数,用于创建部分和的流:
```
keyedStream.reduce(new ReduceFunction() {
@Override
public Integer reduce(Integer value1, Integer value2)
throws Exception {
return value1 + value2;
}
});
```
### Fold
KeyedStream→DataStream
具有初始值的被Keys化数据流上的“滚动”折叠。将当前数据元与最后折叠的值组合并发出新值。
折叠函数,当应用于序列(1,2,3,4,5)时,发出序列“start-1”,“start-1-2”,“start-1-2-3”,. ..
```
DataStream result =
keyedStream.fold("start", new FoldFunction() {
@Override
public String fold(String current, Integer value) {
return current + "-" + value;
}
});
```
### 聚合
* KeyedStream→DataStream
在被Keys化数据流上滚动聚合。min和minBy之间的差异是min返回最小值,而minBy返回该字段中具有最小值的数据元(max和maxBy相同)。
```
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");
```
### Window函数
关于Flink的窗口概念,我们会在后面有详细介绍。
* Window
KeyedStream→WindowedStream
可以在已经分区的KeyedStream上定义Windows。Windows根据某些特征(例如,在最后5秒内到达的数据)对每个Keys中的数据进行分组。
```
dataStream.keyBy(0)
.window(TumblingEventTimeWindows
.of(Time.seconds(5))); // Last 5 seconds of data
```
* Window Apply
WindowedStream→DataStream
AllWindowedStream→DataStream
将一般函数应用于整个窗口。下面是一个手动求和窗口数据元的函数。
注意:如果您正在使用windowAll转换,则需要使用AllWindowFunction。
```
windowedStream.apply (new WindowFunction, Integer, Tuple, Window>() {
public void apply (Tuple tuple,
Window window,
Iterable> values,
Collector out) throws Exception {
int sum = 0;
for (value t: values) {
sum += t.f1;
}
out.collect (new Integer(sum));
}
});
// applying an AllWindowFunction on non-keyed window stream
allWindowedStream.apply (new AllWindowFunction, Integer, Window>() {
public void apply (Window window,
Iterable> values,
Collector out) throws Exception {
int sum = 0;
for (value t: values) {
sum += t.f1;
}
out.collect (new Integer(sum));
}
});
```
* Window Reduce
WindowedStream→DataStream
将reduce函数应用于窗口并返回reduce后的值。
```
windowedStream.reduce (new ReduceFunction>() {
public Tuple2 reduce(Tuple2 value1, Tuple2 value2) throws Exception {
return new Tuple2(value1.f0, value1.f1 + value2.f1);
}
});
```
* 提取时间戳
>关于Time我们在后面有专门的章节进行介绍
DataStream→DataStream
从记录中提取时间戳,以便使用使用事件时间语义的窗口。
```
stream.assignTimestamps (new TimeStampExtractor() {...});
```
### Partition 分区
* 自定义分区
DataStream→DataStream
使用用户定义的分区程序为每个数据元选择目标任务。
```
dataStream.partitionCustom(partitioner, "someKey");
dataStream.partitionCustom(partitioner, 0);
```
* 随机分区
DataStream→DataStream
根据均匀分布随机分配数据元。
```
dataStream.shuffle();
```
* Rebalance (循环分区)
DataStream→DataStream
分区数据元循环,每个分区创建相等的负载。在存在数据倾斜时用于性能优化。
```
dataStream.rebalance();
```
* rescale
DataStream→DataStream
如果上游 算子操作具有并行性2并且下游算子操作具有并行性6,则一个上游 算子操作将分配元件到三个下游算子操作,而另一个上游算子操作将分配到其他三个下游 算子操作。另一方面,如果下游算子操作具有并行性2而上游 算子操作具有并行性6,则三个上游 算子操作将分配到一个下游算子操作,而其他三个上游算子操作将分配到另一个下游算子操作。
在不同并行度不是彼此的倍数的情况下,一个或多个下游 算子操作将具有来自上游 算子操作的不同数量的输入。
请参阅此图以获取上例中连接模式的可视化:

```
dataStream.rescale();
```
* 广播
DataStream→DataStream
向每个分区广播数据元。
```
dataStream.broadcast();
```
================================================
FILE: Flink/Flink集群部署.md
================================================
## 部署方式
一般来讲有三种方式:
* Local
* Standalone
* Flink On Yarn/Mesos/K8s…
## 单机模式
参考上一篇**Flink从入门到放弃(入门篇2)-本地环境搭建&构建第一个Flink应用**
## Standalone模式部署
我们基于CentOS7虚拟机搭建一个3个节点的集群:
角色分配:
```
Master: 192.168.246.134
Slave: 192.168.246.135
Slave: 192.168.246.136
```
```
192.168.246.134 jobmanager
192.168.246.135 taskmanager
192.168.246.136 taskmanager
```
假设三台机器都存在:
用户root 密码为123
```
192.168.246.134 master
192.168.246.135 slave1
192.168.246.136 slave2
```
三台机器首先要做ssh免登,具体方法很简单,可以百度。
下载一个包到本地:

这里我选择了1.7.2版本+Hadoop2.8+Scala2.11版本
然后,分发
```
scp flink-1.7.2-bin-hadoop28-scala_2.11.tgz root@192.168.246.13X:~
scp jdk-8u11-linux-x64.tar.gz root@192.168.246.13X:~
注意:X代表4、5、6,分发到3台机器
修改解压后目录属主:
Chown -R root:root flink/
Chown -R root:root jdk8/
export JAVA_HOME=/root/jdk8
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
```
分别修改master和slave的flink-conf.yaml文件
```
Vim flink/conf/flink-conf.yaml
##配置master节点ip
jobmanager.rpc.address: 192.168.1.100
##配置slave节点可用内存,单位MB
taskmanager.heap.mb: 25600
##配置每个节点的可用slot,1 核CPU对应 1 slot
##the number of available CPUs per machine
taskmanager.numberOfTaskSlots: 30
##默认并行度 1 slot资源
parallelism.default: 1
修改slave节点配置文件slaves:
192.168.246.135
192.168.246.136
```
启动集群:
```
##在master节点上执行此脚本,就可以启动集群,前提要保证master节点到slaver节点可以免密登录,
##因为它的启动过程是:先在master节点启动jobmanager进程,然后ssh到各slaver节点启动taskmanager进程
./bin/start-cluster.sh
停止集群:
./bin/stop-cluster.sh
```
## Flink on yarn集群部署
### Yarn的简介:

* ResourceManager
ResourceManager 负责整个集群的资源管理和分配,是一个全局的资源管理系统。 NodeManager 以心跳的方式向 ResourceManager 汇报资源使用情况(目前主要是 CPU 和内存的使用情况)。RM 只接受 NM 的资源回报信息,对于具体的资源处理则交给 NM 自己处理。
* NodeManager
NodeManager 是每个节点上的资源和任务管理器,它是管理这台机器的代理,负责该节点程序的运行,以及该节点资源的管理和监控。YARN 集群每个节点都运行一个NodeManager。
NodeManager 定时向 ResourceManager 汇报本节点资源(CPU、内存)的使用情况和Container 的运行状态。当 ResourceManager 宕机时 NodeManager 自动连接 RM 备用节点。
NodeManager 接收并处理来自 ApplicationMaster 的 Container 启动、停止等各种请求。
* ApplicationMaster
负责与 RM 调度器协商以获取资源(用 Container 表示)。
将得到的任务进一步分配给内部的任务(资源的二次分配)。
与 NM 通信以启动/停止任务。
监控所有任务运行状态,并在任务运行失败时重新为任务申请资源以重启任务
### Flink on yarn 集群启动步骤
* 步骤1 用户向YARN中提交应用程序,其中包括ApplicationMaster程序、启动ApplicationMaster的命令、用户程序等。
* 步骤2 ResourceManager为该应用程序分配第一个Container,并与对应的Node-Manager通信,要求它在这个Container中启动应用程序的ApplicationMaster。
* 步骤3 ApplicationMaster首先向ResourceManager注册,这样用户可以直接通过ResourceManager查看应用程序的运行状态,然后它将为各个任务申请资源,并监控它的运行状态,直到运行结束,即重复步骤4~7。
* 步骤4 ApplicationMaster采用轮询的方式通过RPC协议向ResourceManager申请和领取资源。
* 步骤5 一旦ApplicationMaster申请到资源后,便与对应的NodeManager通信,要求它启动任务。
* 步骤6 NodeManager为任务设置好运行环境(包括环境变量、JAR包、二进制程序等)后,将任务启动命令写到一个脚本中,并通过运行该脚本启动任务。
* 步骤7 各个任务通过某个RPC协议向ApplicationMaster汇报自己的状态和进度,以让ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务。 在应用程序运行过程中,用户可随时通过RPC向ApplicationMaster查询应用程序的当前运行状态。
* 步骤8 应用程序运行完成后,ApplicationMaster向ResourceManager注销并关闭自己
### on yarn 集群部署
设置Hadoop环境变量:
```
[root@hadoop2 flink-1.7.2]# vi /etc/profile
export HADOOP_CONF_DIR=这里是你自己的hadoop路径
```
bin/yarn-session.sh -h 查看使用方法:

在启动的是可以指定TaskManager的个数以及内存(默认是1G),也可以指定JobManager的内存,但是JobManager的个数只能是一个
我们开启动一个YARN session:
```
./bin/yarn-session.sh -n 4 -tm 8192 -s 8
```
上面命令启动了4个TaskManager,每个TaskManager内存为8G且占用了8个核(是每个TaskManager,默认是1个核)。在启动YARN session的时候会加载conf/flink-config.yaml配置文件,我们可以根据自己的需求去修改里面的相关参数.
YARN session启动之后就可以使用bin/flink来启动提交作业:
例如:
```
./bin/flink run -c com.demo.wangzhiwu.WordCount $DEMO_DIR/target/flink-demo-1.0.SNAPSHOT.jar --port 9000
```
flink run的用法如下:
```
用法: run [OPTIONS]
"run" 操作参数:
-c,--class 如果没有在jar包中指定入口类,则需要在这里通过这个参数指定
-m,--jobmanager 指定需要连接的jobmanager(主节点)地址
使用这个参数可以指定一个不同于配置文件中的jobmanager
-p,--parallelism 指定程序的并行度。可以覆盖配置文件中的默认值。
```
使用run 命令向yarn集群提交一个job。客户端可以确定jobmanager的地址。当然,你也可以通过-m参数指定jobmanager。jobmanager的地址在yarn控制台上可以看到。
值得注意的是:
上面的YARN session是在Hadoop YARN环境下启动一个Flink cluster集群,里面的资源是可以共享给其他的Flink作业。我们还可以在YARN上启动一个Flink作业。这里我们还是使用./bin/flink,但是不需要事先启动YARN session:
```
./bin/flink run -m yarn-cluster -yn 2 ./examples/batch/WordCount.jar \
--input hdfs://user/hadoop/input.txt \
--output hdfs://user/hadoop/output.txt
```
上面的命令同样会启动一个类似于YARN session启动的页面。其中的-yn是指TaskManager的个数,必须要指定。
#### 后台运行 yarn session
如果你不希望flink yarn client一直运行,也可以启动一个后台运行的yarn session。使用这个参数:-d 或者 --detached
在这种情况下,flink yarn client将会只提交任务到集群然后关闭自己。注意:在这种情况下,无法使用flink停止yarn session。
必须使用yarn工具来停止yarn session
```
yarn application -kill
```
#### flink on yarn的故障恢复
flink 的 yarn 客户端通过下面的配置参数来控制容器的故障恢复。这些参数可以通过conf/flink-conf.yaml 或者在启动yarn session的时候通过-D参数来指定。
* yarn.reallocate-failed:这个参数控制了flink是否应该重新分配失败的taskmanager容器。默认是true。
* yarn.maximum-failed-containers:applicationMaster可以接受的容器最大失败次数,达到这个参数,就会认为yarn session失败。默认这个次数和初始化请求的taskmanager数量相等(-n 参数指定的)。
* yarn.application-attempts:applicationMaster重试的次数。如果这个值被设置为1(默认就是1),当application master失败的时候,yarn session也会失败。设置一个比较大的值的话,yarn会尝试重启applicationMaster。
#### 日志文件查看
在某种情况下,flink yarn session 部署失败是由于它自身的原因,用户必须依赖于yarn的日志来进行分析。最有用的就是yarn log aggregation 。启动它,用户必须在yarn-site.xml文件中设置yarn.log-aggregation-enable 属性为true。一旦启用了,用户可以通过下面的命令来查看一个失败的yarn session的所有详细日志。
```
yarn logs -applicationId
```
完。
================================================
FILE: Flink漫谈系列/Apache-Flink-漫谈系列(02)-Watermark.md
================================================
## 实际问题(乱序)
在介绍Watermark相关内容之前我们先抛出一个具体的问题,在实际的流式计算中数据到来的顺序对计算结果的正确性有至关重要的影响,比如:某数据源中的某些数据由于某种原因(如:网络原因,外部存储自身原因)会有5秒的延时,也就是在实际时间的第1秒产生的数据有可能在第5秒中产生的数据之后到来(比如到Window处理节点).选具体某个delay的元素来说,假设在一个5秒的Tumble窗口(详见Window介绍章节),有一个EventTime是 11秒的数据,在第16秒时候到来了。图示第11秒的数据,在16秒到来了,如下图:
-Watermark.resources/E6EAF710-D569-4A11-A65D-687482D7B246.png)
那么对于一个Count聚合的Tumble(5s)的window,上面的情况如何处理才能window2=4,window3=2 呢?
## Apache Flink的时间类型
开篇我们描述的问题是一个很常见的TimeWindow中数据乱序的问题,乱序是相对于事件产生时间和到达Apache Flink 实际处理算子的顺序而言的,关于时间在Apache Flink中有如下三种时间类型,如下图:
-Watermark.resources/B28386CD-F1C2-4DED-AC1C-8983B7BB3C81.png)
**ProcessingTime**
是数据流入到具体某个算子时候相应的系统时间。ProcessingTime 有最好的性能和最低的延迟。但在分布式计算环境中ProcessingTime具有不确定性,相同数据流多次运行有可能产生不同的计算结果。
**IngestionTime**
IngestionTime是数据进入Apache Flink框架的时间,是在Source Operator中设置的。与ProcessingTime相比可以提供更可预测的结果,因为IngestionTime的时间戳比较稳定(在源处只记录一次),同一数据在流经不同窗口操作时将使用相同的时间戳,而对于ProcessingTime同一数据在流经不同窗口算子会有不同的处理时间戳。
**EventTime**
EventTime是事件在设备上产生时候携带的。在进入Apache Flink框架之前EventTime通常要嵌入到记录中,并且EventTime也可以从记录中提取出来。在实际的网上购物订单等业务场景中,大多会使用EventTime来进行数据计算。
开篇描述的问题和本篇要介绍的Watermark所涉及的时间类型均是指EventTime类型。
## 什么是Watermark
Watermark是Apache Flink为了处理EventTime 窗口计算提出的一种机制,本质上也是一种时间戳,由Apache Flink Source或者自定义的Watermark生成器按照需求Punctuated或者Periodic两种方式生成的一种系统Event,与普通数据流Event一样流转到对应的下游算子,接收到Watermark Event的算子以此不断调整自己管理的EventTime clock。 Apache Flink 框架保证Watermark单调递增,算子接收到一个Watermark时候,框架知道不会再有任何小于该Watermark的时间戳的数据元素到来了,所以Watermark可以看做是告诉Apache Flink框架数据流已经处理到什么位置(时间维度)的方式。 Watermark的产生和Apache Flink内部处理逻辑如下图所示:
-Watermark.resources/32C5E28E-7C48-4E69-A180-FF78815EF75E.png)
## Watermark的产生方式
目前Apache Flink 有两种生产Watermark的方式,如下:
* Punctuated - 数据流中每一个递增的EventTime都会产生一个Watermark。
在实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力,所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成。
* Periodic - 周期性的(一定时间间隔或者达到一定的记录条数)产生一个Watermark。在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark,否则在极端情况下会有很大的延时。
所以Watermark的生成方式需要根据业务场景的不同进行不同的选择。
## Watermark的接口定义
对应Apache Flink Watermark两种不同的生成方式,我们了解一下对应的接口定义,如下:
* Periodic Watermarks - AssignerWithPeriodicWatermarks
```
/**
* Returns the current watermark. This method is periodically called by the
* system to retrieve the current watermark. The method may return {@code null} to
* indicate that no new Watermark is available.
*
* <p>The returned watermark will be emitted only if it is non-null and itsTimestamp
* is larger than that of the previously emitted watermark (to preserve the contract of
* ascending watermarks). If the current watermark is still
* identical to the previous one, no progress in EventTime has happened since
* the previous call to this method. If a null value is returned, or theTimestamp
* of the returned watermark is smaller than that of the last emitted one, then no
* new watermark will be generated.
*
* <p>The interval in which this method is called and Watermarks are generated
* depends on {@link ExecutionConfig#getAutoWatermarkInterval()}.
*
* @see org.Apache.flink.streaming.api.watermark.Watermark
* @see ExecutionConfig#getAutoWatermarkInterval()
*
* @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.
*/
@Nullable
Watermark getCurrentWatermark();
```
* Punctuated Watermarks -AssignerWithPunctuatedWatermarks
```
public interface AssignerWithPunctuatedWatermarks<T> extendsTimestampAssigner<T> {
/**
* Asks this implementation if it wants to emit a watermark. This method is called right after
* the {@link #extractTimestamp(Object, long)} method.
*
* <p>The returned watermark will be emitted only if it is non-null and itsTimestamp
* is larger than that of the previously emitted watermark (to preserve the contract of
* ascending watermarks). If a null value is returned, or theTimestamp of the returned
* watermark is smaller than that of the last emitted one, then no new watermark will
* be generated.
*
* <p>For an example how to use this method, see the documentation of
* {@link AssignerWithPunctuatedWatermarks this class}.
*
* @return {@code Null}, if no watermark should be emitted, or the next watermark to emit.
*/
@Nullable
Watermark checkAndGetNextWatermark(T lastElement, long extractedTimestamp);
}
```
* AssignerWithPunctuatedWatermarks 继承了TimestampAssigner接口 -TimestampAssigner
```
public interfaceTimestampAssigner<T> extends Function {
/**
* Assigns aTimestamp to an element, in milliseconds since the Epoch.
*
* <p>The method is passed the previously assignedTimestamp of the element.
* That previousTimestamp may have been assigned from a previous assigner,
* by ingestionTime. If the element did not carry aTimestamp before, this value is
* {@code Long.MIN_VALUE}.
*
* @param element The element that theTimestamp is wil be assigned to.
* @param previousElementTimestamp The previous internalTimestamp of the element,
* or a negative value, if noTimestamp has been assigned, yet.
* @return The newTimestamp.
*/
long extractTimestamp(T element, long previousElementTimestamp);
}
```
从接口定义可以看出,Watermark可以在Event(Element)中提取EventTime,进而定义一定的计算逻辑产生Watermark的时间戳。
## Watermark解决如上问题
从上面的Watermark生成接口和Apache Flink内部对Periodic Watermark的实现来看,Watermark的时间戳可以和Event中的EventTime 一致,也可以自己定义任何合理的逻辑使得Watermark的时间戳不等于Event中的EventTime,Event中的EventTime自产生那一刻起就不可以改变了,不受Apache Flink框架控制,而Watermark的产生是在Apache Flink的Source节点或实现的Watermark生成器计算产生(如上Apache Flink内置的 Periodic Watermark实现), Apache Flink内部对单流或多流的场景有统一的Watermark处理。
回过头来我们在看看Watermark机制如何解决上面的问题,上面的问题在于如何将迟来的EventTime 位11的元素正确处理。要解决这个问题我们还需要先了解一下EventTime window是如何触发的? EventTime window 计算条件是当Window计算的Timer时间戳 小于等于 当前系统的Watermak的时间戳时候进行计算。
* 当Watermark的时间戳等于Event中携带的EventTime时候,上面场景(Watermark=EventTime)的计算结果如下:
-Watermark.resources/12F12191-492B-45E4-987E-8744D4235822.png)
上面对应的DDL(Alibaba 企业版的Flink分支)定义如下:
```
CREATE TABLE source(
...,
Event_timeTimeStamp,
WATERMARK wk1 FOR Event_time as withOffset(Event_time, 0)
) with (
...
);
```
* 如果想正确处理迟来的数据可以定义Watermark生成策略为 Watermark = EventTime -5s, 如下:
-Watermark.resources/EB72E25B-15BA-4651-BB26-177F1A71E692.png)
上面对应的DDL(Alibaba 内部的DDL语法,目前正在和社区讨论)定义如下:
```
CREATE TABLE source(
...,
Event_timeTimeStamp,
WATERMARK wk1 FOR Event_time as withOffset(Event_time, 5000)
) with (
...
);
```
上面正确处理的根源是我们采取了 延迟触发 window 计算 的方式正确处理了 Late Event. 与此同时,我们发现window的延时触发计算,也导致了下游的LATENCY变大,本例子中下游得到window的结果就延迟了5s.
## 多流的Watermark处理
在实际的流计算中往往一个job中会处理多个Source的数据,对Source的数据进行GroupBy分组,那么来自不同Source的相同key值会shuffle到同一个处理节点,并携带各自的Watermark,Apache Flink内部要保证Watermark要保持单调递增,多个Source的Watermark汇聚到一起时候可能不是单调自增的,这样的情况Apache Flink内部是如何处理的呢?如下图所示:
-Watermark.resources/C7873B00-89E2-4CE4-BB5E-584A1CF4767A.png)
Apache Flink内部实现每一个边上只能有一个递增的Watermark, 当出现多流携带Eventtime汇聚到一起(GroupBy or Union)时候,Apache Flink会选择所有流入的Eventtime中最小的一个向下游流出。从而保证watermark的单调递增和保证数据的完整性.如下图:
-Watermark.resources/8F75B306-8255-4D8D-BE79-BD2FC98113B8.png)
## 小结
本节以一个流计算常见的乱序问题介绍了Apache Flink如何利用Watermark机制来处理乱序问题. 本篇内容在一定程度上也体现了EventTime Window中的Trigger机制依赖了Watermark(后续Window篇章会介绍)。Watermark机制是流计算中处理乱序,正确处理Late Event的核心手段。
================================================
FILE: Flink漫谈系列/Apache-Flink-漫谈系列(03)-State.md
================================================
## 实际问题
在流计算场景中,数据会源源不断的流入Apache Flink系统,每条数据进入Apache Flink系统都会触发计算。如果我们想进行一个Count聚合计算,那么每次触发计算是将历史上所有流入的数据重新新计算一次,还是每次计算都是在上一次计算结果之上进行增量计算呢?答案是肯定的,Apache Flink是基于上一次的计算结果进行增量计算的。那么问题来了: "上一次的计算结果保存在哪里,保存在内存可以吗?",答案是否定的,如果保存在内存,在由于网络,硬件等原因造成某个计算节点失败的情况下,上一次计算结果会丢失,在节点恢复的时候,就需要将历史上所有数据(可能十几天,上百天的数据)重新计算一次,所以为了避免这种灾难性的问题发生,Apache Flink 会利用State存储计算结果。本篇将会为大家介绍Apache Flink State的相关内容。
## 什么是State
这个问题似乎有些"弱智"?不管问题的答案是否显而易见,但我还是想简单说一下在Apache Flink里面什么是State?State是指流计算过程中计算节点的中间计算结果或元数据属性,比如 在aggregation过程中要在state中记录中间聚合结果,比如 Apache Kafka 作为数据源时候,我们也要记录已经读取记录的offset,这些State数据在计算过程中会进行持久化(插入或更新)。所以Apache Flink中的State就是与时间相关的,Apache Flink任务的内部数据(计算数据和元数据属性)的快照。
## 为什么需要State
与批计算相比,State是流计算特有的,批计算没有failover机制,要么成功,要么重新计算。流计算在 大多数场景 下是增量计算,数据逐条处理(大多数场景),每次计算是在上一次计算结果之上进行处理的,这样的机制势必要将上一次的计算结果进行存储(生产模式要持久化),另外由于 机器,网络,脏数据等原因导致的程序错误,在重启job时候需要从成功的检查点(checkpoint,后面篇章会专门介绍)进行state的恢复。增量计算,Failover这些机制都需要state的支撑。
## State 实现
Apache Flink内部有四种state的存储实现,具体如下:
* 基于内存的HeapStateBackend - 在debug模式使用,不 建议在生产模式下应用;
* 基于HDFS的FsStateBackend - 分布式文件持久化,每次读写都产生网络IO,整体性能不佳;
* 基于RocksDB的RocksDBStateBackend - 本地文件+异步HDFS持久化;
* 还有一个是基于Niagara(Alibaba内部实现)NiagaraStateBackend - 分布式持久化- 在Alibaba生产环境应用;
## State 持久化逻辑
Apache Flink版本选择用RocksDB+HDFS的方式进行State的存储,State存储分两个阶段,首先本地存储到RocksDB,然后异步的同步到远程的HDFS。 这样而设计既消除了HeapStateBackend的局限(内存大小,机器坏掉丢失等),也减少了纯分布式存储的网络IO开销。
-State.resources/11AD7C2A-A1DD-4238-8226-AED47EF6F446.png)
## State 分类
Apache Flink 内部按照算子和数据分组角度将State划分为如下两类:
* KeyedState - 这里面的key是我们在SQL语句中对应的GroupBy/PartitioneBy里面的字段,key的值就是groupby/PartitionBy字段组成的Row的字节数组,每一个key都有一个属于自己的State,key与key之间的State是不可见的;
* OperatorState - Apache Flink内部的Source Connector的实现中就会用OperatorState来记录source数据读取的offset。
## State 扩容重新分配
Apache Flink是一个大规模并行分布式系统,允许大规模的有状态流处理。 为了可伸缩性,Apache Flink作业在逻辑上被分解成operator graph,并且每个operator的执行被物理地分解成多个并行运算符实例。 从概念上讲,Apache Flink中的每个并行运算符实例都是一个独立的任务,可以在自己的机器上调度到网络连接的其他机器运行。
Apache Flink的DAG图中只有边相连的节点🈶网络通信,也就是整个DAG在垂直方向有网络IO,在水平方向如下图的stateful节点之间没有网络通信,这种模型也保证了每个operator实例维护一份自己的state,并且保存在本地磁盘(远程异步同步)。通过这种设计,任务的所有状态数据都是本地的,并且状态访问不需要任务之间的网络通信。 避免这种流量对于像Apache Flink这样的大规模并行分布式系统的可扩展性至关重要。
如上我们知道Apache Flink中State有OperatorState和KeyedState,那么在进行扩容时候(增加并发)State如何分配呢?比如:外部Source有5个partition,在Apache Flink上面由Srouce的1个并发扩容到2个并发,中间Stateful Operation 节点由2个并发并扩容的3个并发,如下图所示:
-State.resources/60A3963C-B15F-456F-9B37-69C022B6491D.png)
在Apache Flink中对不同类型的State有不同的扩容方法,接下来我们分别介绍。
## OperatorState对扩容的处理
我们选取Apache Flink中某个具体Connector实现实例进行介绍,以MetaQ为例,MetaQ以topic方式订阅数据,每个topic会有N>0个分区,以上图为例,加上我们订阅的MetaQ的topic有5个分区,那么当我们source由1个并发调整为2个并发时候,State是怎么恢复的呢?
state 恢复的方式与Source中OperatorState的存储结构有必然关系,我们先看MetaQSource的实现是如何存储State的。首先MetaQSource 实现了ListCheckpointed,其中的T是Tuple2,我们在看ListCheckpointed接口的内部定义如下:
```
public interface ListCheckpointed; {
List snapshotState(long var1, long var3) throws Exception;
void restoreState(List<T> var1) throws Exception;
}
```
我们发现 snapshotState方法的返回值是一个List,T是Tuple2,也就是snapshotState方法返回List>,这个类型说明state的存储是一个包含partiton和offset信息的列表,InputSplit代表一个分区,Long代表当前partition读取的offset。InputSplit有一个方法如下:
```
public interface InputSplit extends Serializable {
int getSplitNumber();
}
```
也就是说,InputSplit我们可以理解为是一个Partition索引,有了这个数据结构我们在看看上面图所示的case是如何工作的?当Source的并行度是1的时候,所有打partition数据都在同一个线程中读取,所有partition的state也在同一个state中维护,State存储信息格式如下:
-State.resources/736E827E-E4C1-4412-8F0C-F51AECB68329.png)
如果我们现在将并发调整为2,那么我们5个分区的State将会在2个独立的任务(线程)中进行维护,在内部实现中我们有如下算法进行分配每个Task所处理和维护partition的State信息,如下:
```
List assignedPartitions = new LinkedList<>();
for (int i = 0; i < partitions; i++) {
if (i % consumerCount == consumerIndex) {
assignedPartitions.add(i);
}
}
```
这个求mod的算法,决定了每个并发所处理和维护partition的State信息,针对我们当前的case具体的存储情况如下:
-State.resources/436E7A11-0256-4373-A3D4-ED5A4363B0E3.png)
那么到现在我们发现上面扩容后State得以很好的分配得益于OperatorState采用了List的数据结构的设计。另外大家注意一个问题,相信大家已经发现上面分配partition的算法有一个限制,那就是Source的扩容(并发数)是否可以超过Source物理存储的partition数量呢?答案是否定的,不能。目前Apache Flink的做法是提前报错,即使不报错也是资源的浪费,因为超过partition数量的并发永远分配不到待管理的partition。
## KeyedState对扩容的处理
对于KeyedState最容易想到的是hash(key) mod parallelism(operator) 方式分配state,就和OperatorState一样,这种分配方式大多数情况是恢复的state不是本地已有的state,需要一次网络拷贝,这种效率比较低,OperatorState采用这种简单的方式进行处理是因为OperatorState的state一般都比较小,网络拉取的成本很小,对于KeyedState往往很大,我们会有更好的选择,在Apache Flink中采用的是Key-Groups方式进行分配。
## 什么是Key-Groups
Key-Groups 是Apache Flink中对keyed state按照key进行分组的方式,每个key-group中会包含N>0个key,一个key-group是State分配的原子单位。在Apache Flink中关于Key-Group的对象是 KeyGroupRange, 如下:
```
public class KeyGroupRange implements KeyGroupsList, Serializable {
...
...
private final int startKeyGroup;
private final int endKeyGroup;
...
...
}
```
KeyGroupRange两个重要的属性就是 startKeyGroup和endKeyGroup,定义了startKeyGroup和endKeyGroup属性后Operator上面的Key-Group的个数也就确定了。
## 什么决定Key-Groups的个数
key-group的数量在job启动前必须是确定的且运行中不能改变。由于key-group是state分配的原子单位,而每个operator并行实例至少包含一个key-group,因此operator的最大并行度不能超过设定的key-group的个数,那么在Apache Flink的内部实现上key-group的数量就是最大并行度的值。
GroupRange.of(0, maxParallelism)如何决定key属于哪个Key-Group
确定好GroupRange之后,如何决定每个Key属于哪个Key-Group呢?我们采取的是取mod的方式,在KeyGroupRangeAssignment中的assignToKeyGroup方法会将key划分到指定的key-group中,如下:
```
public static int assignToKeyGroup(Object key, int maxParallelism) {
return computeKeyGroupForKeyHash(key.hashCode(), maxParallelism);
}
public static int computeKeyGroupForKeyHash(int keyHash, int maxParallelism) {
return HashPartitioner.INSTANCE.partition(keyHash, maxParallelism);
}
@Override
public int partition(T key, int numPartitions) {
return MathUtils.murmurHash(Objects.hashCode(key)) % numPartitions;
}
```
如上实现我们了解到分配Key到指定的key-group的逻辑是利用key的hashCode和maxParallelism进行取余操作来分配的。如下图当parallelism=2,maxParallelism=10的情况下流上key与key-group的对应关系如下图所示:
-State.resources/90D10775-4713-43B6-B7FE-F77334D29212.png)
如上图key(a)的hashCode是97,与最大并发10取余后是7,被分配到了KG-7中,流上每个event都会分配到KG-0至KG-9其中一个Key-Group中。
每个Operator实例如何获取Key-Groups
了解了Key-Groups概念和如何分配每个Key到指定的Key-Groups之后,我们看看如何计算每个Operator实例所处理的Key-Groups。 在KeyGroupRangeAssignment的computeKeyGroupRangeForOperatorIndex方法描述了分配算法:
```
public static KeyGroupRange computeKeyGroupRangeForOperatorIndex(
int maxParallelism,
int parallelism,
int operatorIndex) {
GroupRange splitRange = GroupRange.of(0, maxParallelism).getSplitRange(parallelism, operatorIndex);
int startGroup = splitRange.getStartGroup();
int endGroup = splitRange.getEndGroup();
return new KeyGroupRange(startGroup, endGroup - 1);
}
public GroupRange getSplitRange(int numSplits, int splitIndex) {
...
final int numGroupsPerSplit = getNumGroups() / numSplits;
final int numFatSplits = getNumGroups() % numSplits;
int startGroupForThisSplit;
int endGroupForThisSplit;
if (splitIndex < numFatSplits) {
startGroupForThisSplit = getStartGroup() + splitIndex * (numGroupsPerSplit + 1);
endGroupForThisSplit = startGroupForThisSplit + numGroupsPerSplit + 1;
} else {
startGroupForThisSplit = getStartGroup() + splitIndex * numGroupsPerSplit + numFatSplits;
endGroupForThisSplit = startGroupForThisSplit + numGroupsPerSplit;
}
if (startGroupForThisSplit >= endGroupForThisSplit) {
return GroupRange.emptyGroupRange();
} else {
return new GroupRange(startGroupForThisSplit, endGroupForThisSplit);
}
}
```
上面代码的核心逻辑是先计算每个Operator实例至少分配的Key-Group个数,将不能整除的部分N个,平均分给前N个实例。最终每个Operator实例管理的Key-Groups会在GroupRange中表示,本质是一个区间值;下面我们就上图的case,说明一下如何进行分配以及扩容后如何重新分配。
假设上面的Stateful Operation节点的最大并行度maxParallelism的值是10,也就是我们一共有10个Key-Group,当我们并发是2的时候和并发是3的时候分配的情况如下图:
-State.resources/FBDBA73F-4927-4834-8284-4893707EA6FB.png)
如上算法我们发现在进行扩容时候,大部分state还是落到本地的,如Task0只有KG-4被分出去,其他的还是保持在本地。同时我们也发现,一个job如果修改了maxParallelism的值那么会直接影响到Key-Groups的数量和key的分配,也会打乱所有的Key-Group的分配,目前在Apache Flink系统中统一将maxParallelism的默认值调整到4096,最大程度的避免无法扩容的情况发生。
## 小结
本篇简单介绍了Apache Flink中State的概念,并重点介绍了OperatorState和KeyedState在扩容时候的处理方式。Apache Flink State是支撑Apache Flink中failover,增量计算,Window等重要机制和功能的核心设施。后续介绍failover,增量计算,Window等相关篇章中也会涉及State的利用,当涉及到本篇没有覆盖的内容时候再补充介绍。
================================================
FILE: Flink漫谈系列/Apache-Flink漫谈系列(1)-概述.md
================================================
-概述.resources/49D66A77-779B-468F-9BCB-6846609484DA.png)
摘要:Apache Flink 的命脉 "命脉" 即生命与血脉,常喻极为重要的事物。系列的首篇,首篇的首段不聊Apache Flink的历史,不聊Apache Flink的架构,不聊Apache Flink的功能特性,我们用一句话聊聊什么是 Apache Flink 的命脉?我的答案是:Apache Flink 是以"批是流的特例"的认知进行系统设计的。
"命脉" 即生命与血脉,常喻极为重要的事物。系列的首篇,首篇的首段不聊Apache Flink的历史,不聊Apache Flink的架构,不聊Apache Flink的功能特性,我们用一句话聊聊什么是 Apache Flink 的命脉?我的答案是:Apache Flink 是以"批是流的特例"的认知进行系统设计的。
我们经常听说 "天下武功,唯快不破",大概意思是说 "任何一种武功的招数都是有拆招的,唯有速度快,快到对手根本来不及反应,你就将对手KO了,对手没有机会拆招,所以唯快不破"。 那么这与Apache Flink有什么关系呢?Apache Flink是Native Streaming(纯流式)计算引擎,在实时计算场景最关心的就是"快",也就是 "低延时"。
就目前最热的两种流计算引擎Apache Spark和Apache Flink而言,谁最终会成为No1呢?单从 "低延时" 的角度看,Spark是Micro Batching(微批式)模式,最低延迟Spark能达到0.5~2秒左右,Flink是Native Streaming(纯流式)模式,最低延时能达到微秒。很显然是相对较晚出道的 Apache Flink 后来者居上。 那么为什么Apache Flink能做到如此之 "快"呢?根本原因是Apache Flink 设计之初就认为 "批是流的特例",整个系统是Native Streaming设计,每来一条数据都能够触发计算。相对于需要靠时间来积攒数据Micro Batching模式来说,在架构上就已经占据了绝对优势。
那么为什么关于流计算会有两种计算模式呢?归其根本是因为对流计算的认知不同,是"流是批的特例" 和 "批是流的特例" 两种不同认知产物。
Micro Batching 模式
Micro-Batching 计算模式认为 "流是批的特例", 流计算就是将连续不断的批进行持续计算,如果批足够小那么就有足够小的延时,在一定程度上满足了99%的实时计算场景。那么那1%为啥做不到呢?这就是架构的魅力,在Micro-Batching模式的架构实现上就有一个自然流数据流入系统进行攒批的过程,这在一定程度上就增加了延时。具体如下示意图:
-概述.resources/3BF00033-D856-49C2-A301-E6DB65B22EAE.png)
很显然Micro-Batching模式有其天生的低延时瓶颈,但任何事物的存在都有两面性,在大数据计算的发展历史上,最初Hadoop上的MapReduce就是优秀的批模式计算框架,Micro-Batching在设计和实现上可以借鉴很多成熟实践。
Native Streaming 模式
Native Streaming 计算模式认为 ""批是流的特", 这个认知更贴切流的概念,比如一些监控类的消息流,数据库操作的binlog,实时的支付交易信息等等自然流数据都是一条,一条的流入。Native Streaming 计算模式每条数据的到来都进行计算,这种计算模式显得更自然,并且延时性能达到更低。具体如下示意图:
-概述.resources/68B36353-346D-4E32-9AD8-8AE91F3FE461.png)
很明显Native Streaming模式占据了流计算领域 "低延时" 的核心竞争力,当然Native Streaming模式的实现框架是一个历史先河,第一个实现
Native Streaming模式的流计算框架是第一个吃螃蟹的人,需要面临更多的挑战,后续章节我们会慢慢介绍。当然Native Streaming模式的框架实现上面很容易实现Micro-Batching和Batching模式的计算,Apache Flink就是Native Streaming计算模式的流批统一的计算引擎。
Apache Flink 按不同的需求支持Local,Cluster,Cloud三种部署模式,同时Apache Flink在部署上能够与其他成熟的生态产品进行完美集成,如 Cluster模式下可以利用YARN(Yet Another Resource Negotiator)/Mesos集成进行资源管理,在Cloud部署模式下可以与GCE(Google Compute Engine), EC2(Elastic Compute Cloud)进行集成。
Local 模式
该模式下Apache Flink 整体运行在Single JVM中,在开发学习中使用,同时也可以安装到很多端类设备上。参考
Cluster模式
该模式是典型的投产的集群模式,Apache Flink 既可以Standalone的方式进行部署,也可以与其他资源管理系统进行集成部署,比如与YARN进行集成。Standalone Cluster 参考 YARN Cluster 参考
这种部署模式是典型的Master/Slave模式,我们以Standalone Cluster模式为例示意如下:
-概述.resources/79ACC2B0-BF5E-4DC4-99DE-9C4DF6007A3F.png)
其中JM(JobManager)是Master,TM(TaskManager)是Slave,这种Master/Slave模式有一个典型的问题就是SPOF(single point of failure), SPOF如何解决呢?Apache Flink 又提供了HA(High Availability)方案,也就是提供多个Master,在任何时候总有一个JM服役,N(N>=1)个JM候选,进而解决SPOF问题,示意如下:
-概述.resources/5F99E16C-1172-4E31-989B-DA7C0800D476.png)
在实际的生产环境我们都会配置HA方案,目前Alibaba内部使用的也是基于YARN Cluster的HA方案。
Cloud 模式
该模式主要是与成熟的云产品进行集成,Apache Flink官网介绍了Google的GCE 参考,Amazon的EC2 参考,在Alibaba我们也可以将Apache Flink部署到Alibaba的ECS(Elastic Compute Service)。
什么是容错
容错(Fault Tolerance) 是指容忍故障,在故障发生时能够自动检测出来并使系统能够自动回复正常运行。当出现某些指定的网络故障、硬件故障、软件错误时,系统仍能执行规定的一组程序,或者说程序不会因系统中的故障而中止,并且执行结果也不会因系统故障而引起计算差错。
容错的处理模式
在一个分布式系统中由于单个进程或者节点宕机都有可能导致整个Job失败,那么容错机制除了要保证在遇到非预期情况系统能够"运行"外,还要求能"正确运行",也就是数据能按预期的处理方式进行处理,保证计算结果的正确性。计算结果的正确性取决于系统对每一条计算数据处理机制,一般有如下三种处理机制:
At Most Once:最多消费一次,这种处理机制会存在数据丢失的可能。
At Least Once:最少消费一次,这种处理机制数据不会丢失,但是有可能重复消费。
Exactly Once:精确一次,无论何种情况下,数据都只会消费一次,这种机制是对数据准确性的最高要求,在金融支付,银行账务等领域必须采用这种模式。
Apache Flink的容错机制
Apache Flink的Job会涉及到3个部分,外部数据源(External Input), Flink内部数据处理(Flink Data Flow)和外部输出(External Output)。如下示意图:
-概述.resources/C810C9BC-45F5-4ADA-8408-7FF57C0C31DB.png)
目前Apache Flink 支持两种数据容错机制:
* At Least Once
* Exactly Once
其中 Exactly Once 是最严格的容错机制,该模式要求每条数据必须处理且仅处理一次。那么对于这种严格容错机制,一个完整的Flink Job容错要做到 End-to-End 的 容错必须结合三个部分进行联合处理,根据上图我们考虑三个场景:
系统内部容错
Apache Flink利用Checkpointing机制来处理容错,Checkpointing的理论基础 Stephan 在 Lightweight Asynchronous Snapshots for Distributed Dataflows 进行了细节描述,该机制源于有K. MANI CHANDY和LESLIE LAMPORT 发表的 Determining-Global-States-of-a-Distributed-System Paper。Apache Flink 基于Checkpointing机制对Flink Data Flow实现了At Least Once 和 Exactly Once 两种容错处理模式。
Apache Flink Checkpointing的内部实现会利用 Barriers,StateBackend等后续章节会详细介绍的技术来将数据的处理进行Marker。Apache Flink会利用Barrier将整个流进行标记切分,如下示意图:
-概述.resources/72919AF2-F467-4306-BE2B-042FA56E4DD4.png)
这样Apache Flink的每个Operator都会记录当前成功处理的Checkpoint,如果发生错误,就会从上一个成功的Checkpoint开始继续处理后续数据。比如 Soruce Operator会将读取外部数据源的Position实时的记录到Checkpoint中,失败时候会从Checkpoint中读取成功的position继续精准的消费数据。每个算子会在Checkpoint中记录自己恢复时候必须的数据,比如流的原始数据和中间计算结果等信息,在恢复的时候从Checkpoint中读取并持续处理流数据。
外部Source容错
Apache Flink 要做到 End-to-End 的 Exactly Once 需要外部Source的支持,比如上面我们说过 Apache Flink的Checkpointing机制会在Source节点记录读取的Position,那就需要外部数据提供读取的Position和支持根据Position进行数据读取。
外部Sink容错
Apache Flink 要做到 End-to-End 的 Exactly Once 相对比较困难,如上场景三所述,当Sink Operator节点宕机,重新恢复时候根据Apache Flink 内部系统容错 exactly once的保证,系统会回滚到上次成功的Checkpoin继续写入,但是上次成功Checkpoint之后当前Checkpoint未完成之前已经把一部分新数据写入到kafka了. Apache Flink自上次成功的Checkpoint继续写入kafka,就造成了kafka再次接收到一份同样的来自Sink Operator的数据,进而破坏了End-to-End 的 Exactly Once 语义(重复写入就变成了At Least Once了),如果要解决这一问题,Apache Flink 利用Two phase commit(两阶段提交)的方式来进行处理。本质上是Sink Operator 需要感知整体Checkpoint的完成,并在整体Checkpoint完成时候将计算结果写入Kafka。
批与流是两种不同的数据处理模式,如Apache Storm只支持流模式的数据处理,Apache Spark只支持批(Micro Batching)模式的数据处理。那么Apache Flink 是如何做到既支持流处理模式也支持批处理模式呢?
统一的数据传输层
开篇我们就介绍Apache Flink 的 "命脉"是以"批是流的特例"为导向来进行引擎的设计的,系统设计成为 "Native Streaming"的模式进行数据处理。那么Apache FLink将批模式执行的任务看做是流式处理任务的特殊情况,只是在数据上批是有界的(有限数量的元素)。
Apache Flink 在网络传输层面有两种数据传输模式:
* PIPELINED模式 - 即一条数据被处理完成以后,立刻传输到下一个节点进行处理。
* BATCH 模式 - 即一条数据被处理完成后,并不会立刻传输到下一个节点进行处理,而是写入到缓存区,如果缓存写满就持久化到本地硬盘上,最后当所有数据都被处理完成后,才将数据传输到下一个节点进行处理。
对于批任务而言同样可以利用PIPELINED模式,比如我要做count统计,利用PIPELINED模式能拿到更好的执行性能。只有在特殊情况,比如SortMergeJoin,这时候我们需要全局数据排序,才需要BATCH模式。大部分情况流与批可用统一的传输策略,只有特殊情况,才将批看做是流的一个特例继续特殊处理。
统一任务调度层
Apache Flink 在任务调度上流与批共享统一的资源和任务调度机制(后续章节会详细介绍)。
统一的用户API层
Apache Flink 在DataStremAPI和DataSetAPI基础上,为用户提供了流批统一的上层TableAPI和SQL,在语法和语义上流批进行高度统一。(其中DataStremAPI和DataSetAPI对流和批进行了分别抽象,这一点并不优雅,在Alibaba内部对其进行了统一抽象)。
求同存异
Apache Flink 是流批统一的计算引擎,并不意味着流与批的任务都走统一的code path,在对底层的具体算子的实现也是有各自的处理的,在具体功能上面会根据不同的特性区别处理。比如 批没有Checkpoint机制,流上不能做SortMergeJoin。
组件栈
我们上面内容已经介绍了很多Apache Flink的各种组件,下面我们整体概览一下全貌,如下:
-概述.resources/624AD4C0-351D-42C0-ACEA-E30851223B5F.png)
TableAPI和SQL都建立在DataSetAPI和DataStreamAPI的基础之上,那么TableAPI和SQL是如何转换为DataStream和DataSet的呢?
TableAPI&SQL到DataStrem&DataSet的架构
TableAPI&SQL最终会经过Calcite优化之后转换为DataStream和DataSet,具体转换示意如下:
-概述.resources/DC074C98-3D57-4997-AAF6-896BEF272F84.png)
对于流任务最终会转换成DataStream,对于批任务最终会转换成DataSet。
ANSI-SQL的支持
Apache Flink 之所以利用ANSI-SQL作为用户统一的开发语言,是因为SQL有着非常明显的优点,如下:
-概述.resources/7C067E43-57A2-4A83-BE9C-3C769DDFF6C6.png)
Declarative - 用户只需要表达我想要什么,不用关心如何计算。
Optimized - 查询优化器可以为用户的 SQL 生成最优的执行计划,获取最好的查询性能。
Understandable - SQL语言被不同领域的人所熟知,用SQL 作为跨团队的开发语言可以很大地提高效率。
Stable - SQL 是一个拥有几十年历史的语言,是一个非常稳定的语言,很少有变动。
Unify - Apache Flink在引擎上对流与批进行统一,同时又利用ANSI-SQL在语法和语义层面进行统一。
无限扩展的优化机制
Apache Flink 利用Apache Calcite对SQL进行解析和优化,Apache Calcite采用Calcite是开源的一套查询引擎,实现了两套Planner:
HepPlanner - 是RBO(Rule Base Optimize)模式,基于规则的优化。
VolcanoPlanner - 是CBO(Cost Base Optimize)模式,基于成本的优化。
Flink SQL会利用Calcite解析优化之后,最终转换为底层的DataStrem和Dataset。上图中 Batch rules和Stream rules可以根据优化需要无限添加优化规则。
Apache Flink 优秀的架构就像一座摩天大厦的地基一样为Apache Flink 持久的生命力打下了良好的基础,为打造Apache Flink丰富的功能生态留下无限的空间。
类库
CEP - 复杂事件处理类库,核心是一个状态机,广泛应用于事件驱动的监控预警类业务场景。
ML - 机器学习类库,机器学习主要是识别数据中的关系、趋势和模式,一般应用在预测类业务场景。
GELLY - 图计算类库,图计算更多的是考虑边和点的概念,一般被用来解决网状关系的业务场景。
算子
Apache Flink 提供了丰富的功能算子,对于数据流的处理来讲,可以分为单流处理(一个数据源)和多流处理(多个数据源)。
多流操作
如上通过UION和JOIN我们可以将多流最终变成单流,Apache Flink 在单流上提供了更多的操作算子。
单流操作
将多流变成单流之后,我们按数据输入输出的不同归类如下:
类型 输入 输出 Table/SQL算子 DataStream/DataSet算子
Scalar Function 1 1 Built-in & UDF, Map
Table Function 1 N(N>=0) Built-in & UDTF FlatMap
Aggregate Function N(N>=0) 1 Built-in & UDAF Reduce
如上表格对单流上面操作做简单归类,除此之外还可以做 过滤,排序,窗口等操作,我们后续章节会逐一介绍。
存在的问题
Apache Flink 目前的架构还存在很大的优化空间,比如前面提到的DataStreamAPI和DataSetAPI其实是流与批在API层面不统一的体现,同时看具体实现会发现DataStreamAPI会生成Transformation tree然后生成StreamGraph,最后生成JobGraph,底层对应StreamTask,但DataSetAPI会形成Operator tree,flink-optimize模块会对Batch Plan进行优化,形成Optimized Plan 后形成JobGraph,最后形成BatchTask。具体示意如下:
-概述.resources/1DEA43A8-381D-451C-9B99-9106B2B058B5.png)
这种情况其实 DataStreamAPI到Runtime 和 DataSetAPI到Runtime的实现上并没有得到最大程度的统一和复用。在这一点上面Aalibab 企业版的Flink在架构和实现上都进行了进一步优化。
组件栈
Alibaba 对Apache Flink进行了大量的架构优化,如下架构是一直努力的方向,大部分功能还在持续开发中,具体如下:
-概述.resources/928F452D-B175-4740-A139-8186EEDAC99C.png)
如上架构我们发现较大的变化是:
QP/QE/QO - 我们增加了QP/QE/QO层,在这一层进行统一的流和批的查询优化和底层算子的转换。
DAG API - 我们在Runtime层面统一抽象API接口,在API层对流与批进行统一。
TableAPI&SQL到Runtime的架构
Apache Flink执行层是流批统一的设计,在API和算子设计上面我们尽量达到流批的共享,在TableAPI和SQL层无论是流任务还是批任务最终都转换为统一的底层实现。这个层面最核心的变化是批最终也会生成StreamGraph,执行层运行Stream Task,如下:
-概述.resources/61F69F73-CEE1-43F4-BB88-177A115FC62E.png)
本篇概要的介绍了"批是流的特例"这一设计观点是Apache Flink的"命脉",它决定了Apache Flink的运行模式是纯流式的,这在实时计算场景的"低延迟"需求上,相对于Micro Batching模式占据了架构的绝对优势,同时概要的向大家介绍了Apache Flink的部署模式,容错处理,引擎的统一性和Apache Flink的架构,最后和大家分享了Apache Flink的优化架构。
本篇没有对具体技术进行详细展开,大家只要对Apache Flink有初步感知,头脑中知道Alibaba对Apache Flink进行了架构优化,增加了众多功能就可以了,至于Apache Flink的具体技术细节和实现原理,以及Alibaba对Apache Flink做了哪些架构优化和增加了哪些功能后续章节会展开介绍!
================================================
FILE: Flink漫谈系列/我的Markdown笔记/Apache-Flink-漫谈系列(03)-State.md
================================================
## 实际问题
在流计算场景中,数据会源源不断的流入Apache Flink系统,每条数据进入Apache Flink系统都会触发计算。如果我们想进行一个Count聚合计算,那么每次触发计算是将历史上所有流入的数据重新新计算一次,还是每次计算都是在上一次计算结果之上进行增量计算呢?答案是肯定的,Apache Flink是基于上一次的计算结果进行增量计算的。那么问题来了: "上一次的计算结果保存在哪里,保存在内存可以吗?",答案是否定的,如果保存在内存,在由于网络,硬件等原因造成某个计算节点失败的情况下,上一次计算结果会丢失,在节点恢复的时候,就需要将历史上所有数据(可能十几天,上百天的数据)重新计算一次,所以为了避免这种灾难性的问题发生,Apache Flink 会利用State存储计算结果。本篇将会为大家介绍Apache Flink State的相关内容。
## 什么是State
这个问题似乎有些"弱智"?不管问题的答案是否显而易见,但我还是想简单说一下在Apache Flink里面什么是State?State是指流计算过程中计算节点的中间计算结果或元数据属性,比如 在aggregation过程中要在state中记录中间聚合结果,比如 Apache Kafka 作为数据源时候,我们也要记录已经读取记录的offset,这些State数据在计算过程中会进行持久化(插入或更新)。所以Apache Flink中的State就是与时间相关的,Apache Flink任务的内部数据(计算数据和元数据属性)的快照。
## 为什么需要State
与批计算相比,State是流计算特有的,批计算没有failover机制,要么成功,要么重新计算。流计算在 大多数场景 下是增量计算,数据逐条处理(大多数场景),每次计算是在上一次计算结果之上进行处理的,这样的机制势必要将上一次的计算结果进行存储(生产模式要持久化),另外由于 机器,网络,脏数据等原因导致的程序错误,在重启job时候需要从成功的检查点(checkpoint,后面篇章会专门介绍)进行state的恢复。增量计算,Failover这些机制都需要state的支撑。
## State 实现
Apache Flink内部有四种state的存储实现,具体如下:
* 基于内存的HeapStateBackend - 在debug模式使用,不 建议在生产模式下应用;
* 基于HDFS的FsStateBackend - 分布式文件持久化,每次读写都产生网络IO,整体性能不佳;
* 基于RocksDB的RocksDBStateBackend - 本地文件+异步HDFS持久化;
* 还有一个是基于Niagara(Alibaba内部实现)NiagaraStateBackend - 分布式持久化- 在Alibaba生产环境应用;
## State 持久化逻辑
Apache Flink版本选择用RocksDB+HDFS的方式进行State的存储,State存储分两个阶段,首先本地存储到RocksDB,然后异步的同步到远程的HDFS。 这样而设计既消除了HeapStateBackend的局限(内存大小,机器坏掉丢失等),也减少了纯分布式存储的网络IO开销。
-State.resources/11AD7C2A-A1DD-4238-8226-AED47EF6F446.png)
## State 分类
Apache Flink 内部按照算子和数据分组角度将State划分为如下两类:
* KeyedState - 这里面的key是我们在SQL语句中对应的GroupBy/PartitioneBy里面的字段,key的值就是groupby/PartitionBy字段组成的Row的字节数组,每一个key都有一个属于自己的State,key与key之间的State是不可见的;
* OperatorState - Apache Flink内部的Source Connector的实现中就会用OperatorState来记录source数据读取的offset。
## State 扩容重新分配
Apache Flink是一个大规模并行分布式系统,允许大规模的有状态流处理。 为了可伸缩性,Apache Flink作业在逻辑上被分解成operator graph,并且每个operator的执行被物理地分解成多个并行运算符实例。 从概念上讲,Apache Flink中的每个并行运算符实例都是一个独立的任务,可以在自己的机器上调度到网络连接的其他机器运行。
Apache Flink的DAG图中只有边相连的节点🈶网络通信,也就是整个DAG在垂直方向有网络IO,在水平方向如下图的stateful节点之间没有网络通信,这种模型也保证了每个operator实例维护一份自己的state,并且保存在本地磁盘(远程异步同步)。通过这种设计,任务的所有状态数据都是本地的,并且状态访问不需要任务之间的网络通信。 避免这种流量对于像Apache Flink这样的大规模并行分布式系统的可扩展性至关重要。
如上我们知道Apache Flink中State有OperatorState和KeyedState,那么在进行扩容时候(增加并发)State如何分配呢?比如:外部Source有5个partition,在Apache Flink上面由Srouce的1个并发扩容到2个并发,中间Stateful Operation 节点由2个并发并扩容的3个并发,如下图所示:
-State.resources/60A3963C-B15F-456F-9B37-69C022B6491D.png)
在Apache Flink中对不同类型的State有不同的扩容方法,接下来我们分别介绍。
## OperatorState对扩容的处理
我们选取Apache Flink中某个具体Connector实现实例进行介绍,以MetaQ为例,MetaQ以topic方式订阅数据,每个topic会有N>0个分区,以上图为例,加上我们订阅的MetaQ的topic有5个分区,那么当我们source由1个并发调整为2个并发时候,State是怎么恢复的呢?
state 恢复的方式与Source中OperatorState的存储结构有必然关系,我们先看MetaQSource的实现是如何存储State的。首先MetaQSource 实现了ListCheckpointed,其中的T是Tuple2,我们在看ListCheckpointed接口的内部定义如下:
```
public interface ListCheckpointed; {
List snapshotState(long var1, long var3) throws Exception;
void restoreState(List<T> var1) throws Exception;
}
```
我们发现 snapshotState方法的返回值是一个List,T是Tuple2,也就是snapshotState方法返回List>,这个类型说明state的存储是一个包含partiton和offset信息的列表,InputSplit代表一个分区,Long代表当前partition读取的offset。InputSplit有一个方法如下:
```
public interface InputSplit extends Serializable {
int getSplitNumber();
}
```
也就是说,InputSplit我们可以理解为是一个Partition索引,有了这个数据结构我们在看看上面图所示的case是如何工作的?当Source的并行度是1的时候,所有打partition数据都在同一个线程中读取,所有partition的state也在同一个state中维护,State存储信息格式如下:
-State.resources/736E827E-E4C1-4412-8F0C-F51AECB68329.png)
如果我们现在将并发调整为2,那么我们5个分区的State将会在2个独立的任务(线程)中进行维护,在内部实现中我们有如下算法进行分配每个Task所处理和维护partition的State信息,如下:
```
List assignedPartitions = new LinkedList<>();
for (int i = 0; i < partitions; i++) {
if (i % consumerCount == consumerIndex) {
assignedPartitions.add(i);
}
}
```
这个求mod的算法,决定了每个并发所处理和维护partition的State信息,针对我们当前的case具体的存储情况如下:
-State.resources/436E7A11-0256-4373-A3D4-ED5A4363B0E3.png)
那么到现在我们发现上面扩容后State得以很好的分配得益于OperatorState采用了List的数据结构的设计。另外大家注意一个问题,相信大家已经发现上面分配partition的算法有一个限制,那就是Source的扩容(并发数)是否可以超过Source物理存储的partition数量呢?答案是否定的,不能。目前Apache Flink的做法是提前报错,即使不报错也是资源的浪费,因为超过partition数量的并发永远分配不到待管理的partition。
## KeyedState对扩容的处理
对于KeyedState最容易想到的是hash(key) mod parallelism(operator) 方式分配state,就和OperatorState一样,这种分配方式大多数情况是恢复的state不是本地已有的state,需要一次网络拷贝,这种效率比较低,OperatorState采用这种简单的方式进行处理是因为OperatorState的state一般都比较小,网络拉取的成本很小,对于KeyedState往往很大,我们会有更好的选择,在Apache Flink中采用的是Key-Groups方式进行分配。
## 什么是Key-Groups
Key-Groups 是Apache Flink中对keyed state按照key进行分组的方式,每个key-group中会包含N>0个key,一个key-group是State分配的原子单位。在Apache Flink中关于Key-Group的对象是 KeyGroupRange, 如下:
```
public class KeyGroupRange implements KeyGroupsList, Serializable {
...
...
private final int startKeyGroup;
private final int endKeyGroup;
...
...
}
```
KeyGroupRange两个重要的属性就是 startKeyGroup和endKeyGroup,定义了startKeyGroup和endKeyGroup属性后Operator上面的Key-Group的个数也就确定了。
## 什么决定Key-Groups的个数
key-group的数量在job启动前必须是确定的且运行中不能改变。由于key-group是state分配的原子单位,而每个operator并行实例至少包含一个key-group,因此operator的最大并行度不能超过设定的key-group的个数,那么在Apache Flink的内部实现上key-group的数量就是最大并行度的值。
GroupRange.of(0, maxParallelism)如何决定key属于哪个Key-Group
确定好GroupRange之后,如何决定每个Key属于哪个Key-Group呢?我们采取的是取mod的方式,在KeyGroupRangeAssignment中的assignToKeyGroup方法会将key划分到指定的key-group中,如下:
```
public static int assignToKeyGroup(Object key, int maxParallelism) {
return computeKeyGroupForKeyHash(key.hashCode(), maxParallelism);
}
public static int computeKeyGroupForKeyHash(int keyHash, int maxParallelism) {
return HashPartitioner.INSTANCE.partition(keyHash, maxParallelism);
}
@Override
public int partition(T key, int numPartitions) {
return MathUtils.murmurHash(Objects.hashCode(key)) % numPartitions;
}
```
如上实现我们了解到分配Key到指定的key-group的逻辑是利用key的hashCode和maxParallelism进行取余操作来分配的。如下图当parallelism=2,maxParallelism=10的情况下流上key与key-group的对应关系如下图所示:
-State.resources/90D10775-4713-43B6-B7FE-F77334D29212.png)
如上图key(a)的hashCode是97,与最大并发10取余后是7,被分配到了KG-7中,流上每个event都会分配到KG-0至KG-9其中一个Key-Group中。
每个Operator实例如何获取Key-Groups
了解了Key-Groups概念和如何分配每个Key到指定的Key-Groups之后,我们看看如何计算每个Operator实例所处理的Key-Groups。 在KeyGroupRangeAssignment的computeKeyGroupRangeForOperatorIndex方法描述了分配算法:
```
public static KeyGroupRange computeKeyGroupRangeForOperatorIndex(
int maxParallelism,
int parallelism,
int operatorIndex) {
GroupRange splitRange = GroupRange.of(0, maxParallelism).getSplitRange(parallelism, operatorIndex);
int startGroup = splitRange.getStartGroup();
int endGroup = splitRange.getEndGroup();
return new KeyGroupRange(startGroup, endGroup - 1);
}
public GroupRange getSplitRange(int numSplits, int splitIndex) {
...
final int numGroupsPerSplit = getNumGroups() / numSplits;
final int numFatSplits = getNumGroups() % numSplits;
int startGroupForThisSplit;
int endGroupForThisSplit;
if (splitIndex < numFatSplits) {
startGroupForThisSplit = getStartGroup() + splitIndex * (numGroupsPerSplit + 1);
endGroupForThisSplit = startGroupForThisSplit + numGroupsPerSplit + 1;
} else {
startGroupForThisSplit = getStartGroup() + splitIndex * numGroupsPerSplit + numFatSplits;
endGroupForThisSplit = startGroupForThisSplit + numGroupsPerSplit;
}
if (startGroupForThisSplit >= endGroupForThisSplit) {
return GroupRange.emptyGroupRange();
} else {
return new GroupRange(startGroupForThisSplit, endGroupForThisSplit);
}
}
```
上面代码的核心逻辑是先计算每个Operator实例至少分配的Key-Group个数,将不能整除的部分N个,平均分给前N个实例。最终每个Operator实例管理的Key-Groups会在GroupRange中表示,本质是一个区间值;下面我们就上图的case,说明一下如何进行分配以及扩容后如何重新分配。
假设上面的Stateful Operation节点的最大并行度maxParallelism的值是10,也就是我们一共有10个Key-Group,当我们并发是2的时候和并发是3的时候分配的情况如下图:
-State.resources/FBDBA73F-4927-4834-8284-4893707EA6FB.png)
如上算法我们发现在进行扩容时候,大部分state还是落到本地的,如Task0只有KG-4被分出去,其他的还是保持在本地。同时我们也发现,一个job如果修改了maxParallelism的值那么会直接影响到Key-Groups的数量和key的分配,也会打乱所有的Key-Group的分配,目前在Apache Flink系统中统一将maxParallelism的默认值调整到4096,最大程度的避免无法扩容的情况发生。
## 小结
本篇简单介绍了Apache Flink中State的概念,并重点介绍了OperatorState和KeyedState在扩容时候的处理方式。Apache Flink State是支撑Apache Flink中failover,增量计算,Window等重要机制和功能的核心设施。后续介绍failover,增量计算,Window等相关篇章中也会涉及State的利用,当涉及到本篇没有覆盖的内容时候再补充介绍。
================================================
FILE: Hadoop/Hadoop极简入门.md
================================================
其实Hadoop诞生至今已经十多年了,网络上也充斥着关于Hadoop相关知识的海量资源。但是,有时还是会使刚刚接触大数据领域的童鞋分不清hadoop、hdfs、Yarn和MapReduce等等技术词汇。
Hadoop是ASF(Apache软件基金会)开源的,根据Google开源的三篇大数据论文设计的,一个能够允许大量数据在计算机集群中,通过使用简单的编程模型进行分布式处理的框架。其设计的规模可从单一的服务器到数千台服务器,每一个均可提供局部运算和存储功能。Hadoop并不依赖昂贵的硬件以支持高可用性。Hadoop可以检测并处理应用层上的错误,并可以把错误转移到其他服务器上(让它错误,我在用别的服务器顶上就可以了),所以Hadoop提供一个基于计算机集群的、高效性的服务。
经过十年的发展,Hadoop这个名词的本身也在不断进化者,目前我们提到Hadoop大多是指大数据的生态圈,这个生态圈包括众多的软件技术(e.g. HBase、Hive和Spark等等)。
有如Spring框架有着最基础的几个模块Context、Bean和Core,其他的模块和项目都是基于这些基础模块构建。Hadoop与之一样,也有最基础的几个模块。
**Common**: 支持其他模块的公用工具包。
**HDFS**: 一个可高吞吐访问应用数据的分布式文件系统。
**YARN**: 一个管理集群服务器资源和任务调度的框架。
**MapReduce**: 基于Yarn对大数据集进行并行计算的系统。
其它的,像HBase、Hive等等不过在这几个基础模块上的高级抽象。另外Hadoop也不是目前大数据的唯一解决方案,像Amazon的大数据技术方案等等。
Common
Common模块是Hadoop最为基础的模块,他为其他模块提供了像操作文件系统、I/O、序列化和远程方法调用等最为基础的实现。如果想深入的了解Hadoop的具体实现,可以阅读一下Common的源码。
## HDFS
HDFS是“Hadoop Distributed File System”的首字母缩写,是一种设计运行在一般硬件条件(不需要一定是服务器级别的设备,但更好的设备能发挥更大的作用)下的分布式文件系统. 他和现有的其他分布式文件系统(e.g. RAID)有很多相似的地方。和其他分布式文件系统的不同之处是HDFS设计为运行在低成本的硬件上(e.g. 普通的PC机),且提供高可靠性的服务器. HDFS设计满足大数据量,大吞吐量的应用情况。
为了更好的理解分布式文件系统,我们先从文件讲起。
### 文件
文件这个词,恐怕只要是现代人都不会陌生。但是在不同行业中,文件有着不同的意义。在计算机科学领域,文件是什么呢?文件是可以在目录中看的见的图标么?当然不是。文件在存储设备时,是个N长的字节序列。而在一个计算机使用者的角度而言,文件是对所有I/O设备的抽象。每个I/O设备都可以视为文件,包括磁盘、键盘和网络等。文件这个简单而精致的概念其内涵是十分丰富的,它向应用程序提供了一个统一的视角,来看待系统中可能含有的各式各样的I/O设备。
### 文件系统
那么一台计算机上肯定不止一个文件,成千上万的文件怎么管理呢?因此需要我们需要一种对文件进行管理的东西,即文件系统。文件系统是一种在计算机上存储和组织数据的方法,它使得对其访问和查找变得容易,文件系统使用文件和树形目录的抽象逻辑概念代替了硬盘和光盘等物理设备使用数据块的概念,用户使用文件系统来保存数据而不必关心数据实际保存在硬盘的地址为多少的数据块上,只需要记住这个文件的所属目录和文件名。在写入新数据之前,用户不必关心硬盘上的那个块地址没有被使用,硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中即可。
### 分布式文件系统
相对于单机的文件系统而言,分布式文件系统(Distributed file system)。是一种允许文件通过网络在多台主机上分享的文件系统,可让多计算机上的多用户分享文件和存储空间。
在这样的文件系统中,客户端并非直接访问底层的数据存储区块和磁盘。而是通过网络,基于单机文件系统并借由特定的通信协议的帮助,来实现对于文件系统的读写。
分布式文件系统需要拥有的最基本的能力是通过畅通网络I/O来实现数据的复制与容错。也就是说,一方面一个文件是分为多个数据块分布在多个设备中。另一方面,数据块有多个副本分布在不同的设备上。即使有一小部分的设备出现离线和宕机等情况,整体来说文件系统仍然可以持续运作而不会有数据损失。
注意:分布式文件系统和分布式数据存储的界线是模糊的,但一般来说,分布式文件系统是被设计用在局域网,比较强调的是传统文件系统概念的延伸,并通过软件方法来达成容错的目的。而分布式数据存储,则是泛指应用分布式运算技术的文件和数据库等提供数据存储服务的系统。
### HDFS
HDFS正是Hadoop中负责分布式文件系统的。HDFS采用master/slave架构。一个HDFS集群是由一个Namenode和一定数目的Datanodes组成。Namenode是一个中心服务器,负责管理文件系统的命名空间以及文件的访问控制。集群中的Datanode一般是一个设备上部署一个,负责管理它所在节点上的存储。HDFS暴露了文件系统的命名空间,用户能够以文件的形式在上面存储数据。实际上,一个文件会被分成一个或多个数据块,这些块存储在一组Datanode上。Namenode执行文件系统的命名空间操作,比如打开、关闭、重命名文件或目录。它也负责确定数据块到具体Datanode设备的映射。Datanode负责处理文件系统客户端的读写请求。在Namenode的统一调度下进行数据块的创建、删除和复制。为了保证文件系统的高可靠,往往需要另一个Standby的Namenode在Actived Namenode出现问题后,立刻接管文件系统。
网络上有很多关于hdfs的安装配置手册,本文就不再复述。只提供一个以前项目中应用过的部署架构仅供大家参考。

这个高可用的HDFS架构是由3台zookeeper设备、2台域名服务(DNS)和时间服务(NTP)设备、2台Namenode设备(如果必要Standby可以更多)、一个共享存储设备(NFS)和N个DataNode组成。
Zookeeper负责接受NameNode的心跳,当Actived namenode不向zookeeper报告心跳时,Standby Namenode的监控进程会收到这个消息,从而激活Standby NameNode并接管Active NameNode的工作。
NFS负责为2个NameNode存储EditLog文件,(NameNode 在执行 HDFS 客户端提交的创建文件或者移动文件这样的写操作时,会首先把这些操作记录在 EditLog 文件之中,然后再更新内存中的文件系统镜像,最终再刷新到磁盘。 EditLog 只是在数据恢复的时候起作用。记录在 EditLog 之中的每一个操作又称为一个事务,每个事务有一个整数形式的事务 id 作为编号。EditLog 会被切割为很多段,每一段称为一个 Segment)当发生NameNode切换的情况时,Standby NameNode接管后,会根据EditLog中把未完成的写操作继续下去并开使向EditLog写入新的写操作记录。(此外,hadoop还提供了另一种QJM的EditLog方案)
DNS&NTP分布负责整个系统的(包括客户端)域名服务和时间服务。这个在集群部署中是非常有必要的两个存在。首先说一下DNS的必要性,一、Hadoop是极力提倡用机器名作为在HDFS环境中的标识。二、当然可以在/etc/hosts文件中去标明机器名和IP的映射关系,可是请想想如果在一个数千台设备的集群中添加一个设备时,负责系统维护的伙伴会不会骂集群的设计者呢?其次是NTP的必要性,在刚刚开始接触Hadoop集群时我遇到的大概90%的问题是由于各个设备时间不一致导致的。各个设备的时间同步是数据一致性和管理一致性的一个基本保障。
### MapReduce
MapReduce是一个使用简单的软件框架,基于它写出来的应用程序能够运行在由上千个商用机器组成的大型集群上,并以一种可靠容错的方式并行处理上T级别的数据集。
一个MapReduce 作业(job)通常会把输入的数据集切分为若干独立的数据块,由 map任务(task)以完全并行的方式处理它们。框架会对map的输出先进行排序, 然后把结果输入给reduce任务。通常作业的输入和输出都会被存储在文件系统中。 整个框架负责任务的调度和监控,以及重新执行已经失败的任务。
通常,MapReduce框架和HDFS是运行在一相同的设备集群上的,也就是说,计算设备和存储设备通常在一起。这种配置允许框架在那些已经存好数据的设备上高效地调度任务,这可以使整个集群的网络带宽被非常高效地利用。
MapReduce框架由一个单独的master JobTracker 和每个集群设备一个slave TaskTracker共同组成。master负责调度构成一个作业的所有任务,这些任务分布在不同的slave上,master监控它们的执行,重新执行已经失败的任务。而slave仅负责执行由master指派的任务。
用户编写的MapReduce应用程序应该指明输入/输出的文件位置(路径),并通过实现合适的接口或抽象类提供map和reduce函数。再加上其他作业的参数,就构成了作业配置(job configuration)。然后,job client提交作业(jar包/可执行程序等)和配置信息给JobTracker,后者负责分发这些软件和配置信息给slave、调度任务并监控它们的执行,同时提供状态和诊断信息给job-client。

在抽象的层面上MapReduce是由两个函数Map和Reduce组成的。简单来说,一个Map函数就是对一些独立元素组成的概念上的列表的每一个元素进行指定的操作。事实上,每个元素都是被独立操作的,而原始列表没有被更改,因为这里创建了一个新的列表来保存操作结果。这就是说,Map操作是可以高度并行的。而Reduce函数指的是对Map函数的结果(中间经过洗牌的过程,会把map的结果进行分组)分组后多个列表的元素进行适当的归并。
注意:虽然Hadoop框架是用JavaTM实现的,但MapReduce应用程序则不一定要用 Java来写 。至少Scala是可以写的哟。
附上Scala实现的计算词频的Scala源码
```
import java.io.IOException
import java.util.StringTokenizer
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.Path
import org.apache.hadoop.io.{IntWritable, Text}
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat
import org.apache.hadoop.mapreduce.{Job, Mapper, Reducer}
import scala.collection.JavaConversions
object WordCount {
def main(args: Array[String]): Unit = {
val job = new Job(new Configuration(), "WordCount")
job.setJarByClass(classOf[WordMapper]);
job.setMapperClass(classOf[WordMapper]);
job.setCombinerClass(classOf[WordReducer]);
job.setReducerClass(classOf[WordReducer]);
job.setOutputKeyClass(classOf[Text]);
job.setOutputValueClass(classOf[IntWritable]);
job.setNumReduceTasks(1)
FileInputFormat.addInputPath(job, new Path(args(0)));
FileOutputFormat.setOutputPath(job, new Path(args(1)));
System.exit(job.waitForCompletion(true) match { case true => 0
case false => 1
});
}
}
class WordMapper extends Mapper[Object, Text, Text, IntWritable] {
val one = new IntWritable(1)
@throws[IOException]
@throws[InterruptedException]
override def map(key: Object, value: Text, context: Mapper[Object, Text, Text, IntWritable]#Context) = {
val stringTokenizer = new StringTokenizer(value.toString());
while (stringTokenizer.hasMoreTokens()) {
context.write(new Text(stringTokenizer.nextToken()), one);
}
}
}
class WordReducer extends Reducer[Text, IntWritable, Text, IntWritable] {
@throws[IOException]
@throws[InterruptedException]
override def reduce(key: Text, values: java.lang.Iterable[IntWritable], context: Reducer[Text, IntWritable, Text, IntWritable]#Context) = {
import JavaConversions.iterableAsScalaIterable
context.write(key, new IntWritable(values.map(x=>x.get()).reduce(_+_)));
}
}
```
### Yarn
YARN(Yet Another Resource Negotiator)是Hadoop的设备资源管理器,它是一个通用资源管理系统,MapReduce和其他上层应用提供统一的资源管理和调度,它为集群在利用率、资源统一管理和数据共享等方面提供了巨大的帮助。
Yarn由ResourceManager、NodeManager、ApplicationMaster和Containe四个概念构成。

ResourceManager是一个全局的资源管理器,负责整个系统的资源管理和分配。它主要由两个组件构成:调度器(Scheduler)和应用程序管理器(Applications Manager)。调度器根据容量、队列等限制条件,将系统中的资源分配给各个正在运行的MapReduce程序。应用程序管理器负责管理整个系统中所有MapReduce程序,包括提交、与调度器协商资源以启动ApplicationMaster、监控ApplicationMaster运行状态并在失败时重新启动它等。
用户提交的每个MapReduce程序均包含一个ApplicationMaster,主要功能包括:与ResourceManager调度器协商以获取资源(用Container表示);将得到的任务进一步分配给内部的任务(资源的二次分配);与NodeManager通信以启动/停止任务;监控所有任务运行状态,并在任务运行失败时重新为任务申请资源以重启任务。
NodeManager是每个设备上的资源和任务管理器,一方面,它会定时地向ResourceManager汇报本设备上的资源使用情况和各个Container的运行状态;另一方面,它接收并处理来自ApplicationMaster的Container启动/停止等各种请求。
Container是YARN中的资源抽象,它封装了某个设备上的多维度资源,如内存、CPU、磁盘、网络等,当AM向RM申请资源时,RM为AM返回的资源便是用Container表示。
## 结语
本文走马观花的介绍了Hadoop相关内容。文章的主要目的是给大家一个对大数据的分布式解决方案的感官印象,为后面的大数据相关文章提供一个基础的理解。最后要强调的是,思考大数据方向的问题是一定要记住分布式的概念,因为你的数据并不在一个设备中甚至不再一个集群中,而且计算也是分布的。所以在设计大数据应用程序时,要花时间思考程序和算法在单机应用和分布式应用所产生的不同(e.g. 加权平均值)。
================================================
FILE: Hadoop/MapReduce编程模型和计算框架架构原理.md
================================================
Hadoop解决大规模数据分布式计算的方案是MapReduce。MapReduce既是一个编程模型,又是一个计算框架。也就是说,开发人员必须基于MapReduce编程模型进行编程开发,然后将程序通过MapReduce计算框架分发到Hadoop集群中运行。我们先看一下作为编程模型的MapReduce。
### MapReduce编程模型
MapReduce是一种非常简单又非常强大的编程模型。
简单在于其编程模型只包含map和reduce两个过程,map的主要输入是一对值,经过map计算后输出一对值;然后将相同key合并,形成;再将这个输入reduce,经过计算输出零个或多个对。
但是MapReduce同时又是非常强大的,不管是关系代数运算(SQL计算),还是矩阵运算(图计算),大数据领域几乎所有的计算需求都可以通过MapReduce编程来实现。
我们以WordCount程序为例。WordCount主要解决文本处理中的词频统计问题,就是统计文本中每一个单词出现的次数。如果只是统计一篇文章的词频,几十K到几M的数据,那么写一个程序,将数据读入内存,建一个Hash表记录每个词出现的次数就可以了,如下图。

但是如果想统计全世界互联网所有网页(数万亿计)的词频数(这正是google这样的搜索引擎典型需求),你不可能写一个程序把全世界的网页都读入内存,这时候就需要用MapReduce编程来解决。
WordCount的MapReduce程序如下。
```
public class WordCount {
public static class TokenizerMapper
extends Mapper