一、Flink简介
- Flink是有状态的流式计算。
- Flink是一个框架和分布式处理引擎,用于在无边界和有边界数据流上进行有状态的计算。
- Flink可以部署在任意地方,Apache flink是一个分布式系统,集成了所有常见的集群资源管理器。如hadoop yarn,Apache mesos和kubernetes(k8s)。也可以作为独立集群运行(standalone)。
- 可以运行任意规模应用:每天处理数万亿的事件;可以维护几TB大小的状态;可以部署上千个节点的集群;异步和增量的检查点算法对处理延迟产生最小的影响,同时保证精准一次的状态一致性。
- 利用内存性能:有状态的flink程序针对本地状态访问进行了优化。任务的状态始终保留在内存中,如果状态大小超过可用内存,则会保存在能高效访问的磁盘数据结构中。任务通过访问本地(通常在内存中)状态来进行所有的计算,从而产生非常低的处理延迟。Flink通过定期和异步地对本地状态进行持久化存储来保证故障场景下的精确一次的状态一致性。
yarn和mesos部署区别:
- 都会对flink进行内存和cpu的资源调度;
- 大部分都是部署在yarn上。
- 区别:yarn是粗粒度的,mesos是细粒度的;
- yarn仅仅限于大数据框架部署,mesos可能还会部署别的框架,出问题较多,需要专业团队。
k8s部署有什么优势:
k8s可以部署任意平台,包括微服务等等(一统天下),把flink部署到K8s也是出于可以统一管理的意图。
二、Flink核心概念
- operator算子:flatmap,keyby,sum
- dataflow:Dataflow程序描述了数据如何在不同操作之间流动。Dataflow程序通常表现为有向无环图(DAG),图中顶点称为算子(Operator),表示计算。而边表示数据依赖关系。
- distributed:flink是主从架构,主节点叫jobManager,从节点叫taskManager。对于分布式执行,Flink将operator子任务链接到task中。每个任务由一个线程执行。将operator链接到task是一个有用的优化:它减少了线程到线程的切换和缓冲的开销,并在降低延迟的同时提高了总体吞吐量。
- slot:slot是指taskmanager的并发执行能力。集群运行的资源。并行度应该小于slot。
- parallelism并行度:parallelism是指taskmanager实际使用的并发能力。
- task:是一个阶段多个功能相同 subTask 的集合,类似于 Spark 中的 TaskSet。
- subtask:subTask 是 Flink 中任务最小执行单元,是一个 Java 类的实例,这个 Java 类中有属性和方法,完成具体的计算逻辑。
- flink提交任务命令:./flink run -c main函数路径 jar包名。
- operator chain的条件:数据传输策略是forward strategy,在同一个taskmanager中运行,并行度一样。
parallelism是可配置、可指定的:
- 可以通过修改$FLINK_HOME/conf/flink-conf.yaml文件的方式更改并行度。
- 可以通过设置$FLINK_HOME/bin/flink 的-p参数,即提交任务时指定参数-p来修改并行度。./flink run -p 4 -c main函数相对路径 jar包名
- 可以通过设置executionEnvironment的方法修改并行度。
- 可以通过设置flink的算子修改过并行度。
- 这些并行度优先级排序为api算子>env>-p>配置文件。
- 设置合适的并行度,能提高运算效率。
- parallelism不能多于slot个数。
slot和parallelism总结:
- slot是静态的概念,是指taskmanager具有的并发执行能力。
- parallelism是动态的概念,是指程序运行时实际使用的并发能力。
- 设置合适的parallelism能提高运算效率,太多了和太少了都不行。
- 设置parallelism有多种方式,优先级为api算子>env>-p>配置文件。
数据传输策略:
- forward strategy,一个task的输出只发给一个task作为输入,如果两个task都在一个jvm中的话,就可以避免网络开销;
- key based strategy(keyby),数据按照某个属性(我们称为key)进行分组(或分区);
- broadcast strategy广播策略,将上游的数据广播给下游的每一个算子,数据只会存在一份;
- random strategy随机策略,数据随机的从一个task中传输给下一个operator所有的subtask,保证数据能均匀的传输给所有的subtask;
三、flink分布式运行环境
- flink分布式四层模型(三层模型):flink代码开发要构建一个dataflow,这个dataflow运行需要经历如下四个阶段:stream graph,job graph,execution graph(可执行文件图),physical execution graph(物理执行图)。从stream graph到job graph就是做了一个operator chain的合并。
- flink任务分布式运行流程:flink是主从架构,有通信模型。
四、flinkStreaming(dataStream)
source:
source是程序的数据源输入,可以通过streamExecutionEnvironment.addSource(sourceFunction)来为你的程序添加一个source。flink提供了大量的已经实现好的source方法,如内存,文件,kafka,也可以自定义source(SourceFunction单并行度,ParallelSourceFunction多并行度)。
内存:
public class Source1_Collection {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
List<SensorReading> list = new ArrayList<>();
list.add(new SensorReading("sensor_1",System.currentTimeMillis(),37.5));
list.add(new SensorReading("sensor_2",System.currentTimeMillis(),37.2));
list.add(new SensorReading("sensor_3",System.currentTimeMillis(),37.4));
list.add(new SensorReading("sensor_4",System.currentTimeMillis(),36.9));
list.add(new SensorReading("sensor_5",System.currentTimeMillis(),36.8));
list.add(new SensorReading("sensor_6",System.currentTimeMillis(),37.1));
// 通过内存获取数据,当然是封装在对象中的
DataStreamSource<SensorReading> dss = env.fromCollection(list);
dss.print("sensor");
try {
env.execute("collectionSource");
} catch (Exception e) {
e.printStackTrace();
}
}
}
文件:
public class Source2_File {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
String path = "D:\\peixun\\soft\\projects\\Flink\\resource\\a.txt";
// 读文件方式
DataStreamSource<String> stringDataStreamSource = env.readTextFile(path);
stringDataStreamSource.print();
try {
env.execute("FileExample");
} catch (Exception e) {
e.printStackTrace();
}
}
}
kafka:
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer011;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import java.util.Properties;
public class Source3_Kafka {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//env.setParallelism(1);
// kafka 配置
Properties properties = new Properties();
properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.159.100:9092");
properties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG,"gro1");
properties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");
// 创建 FlinkKafkaConsumer (主题,SimpleStringSchema,kafka配置)
FlinkKafkaConsumer011<String> consumer = new FlinkKafkaConsumer011<>("flinkKafka", new SimpleStringSchema(), properties);
// 将创建好的 消费者放入 addSource
DataStreamSource<String> dss = env.addSource(consumer);
dss.print();
try {
env.execute("kafka-example");
} catch (Exception e) {
e.printStackTrace();
}
}
}
自定义source:
- 通过实现sourcefunction接口来自定义无并行度的source
- 通过实现parallelaSourceFunction接口或者继承RichParallelSourceFunction来自定义有并行度的source。
- 大多数情况下,使用自带的source即可。
import Flink.beans.SensorReading;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import java.util.Random;
public class Source4_MySource {
public static void main(String[] args) {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 利用 addSource 传入自定义类
DataStreamSource<SensorReading> ssd = env.addSource(new MySensorSource());
ssd.print();
try {
env.execute("MySource-example");
} catch (Exception e) {
e.printStackTrace();
}
}
// 自定义类 extends SourceFaction, 实现 run 和 cancel 方法
private static class MySensorSource implements SourceFunction<SensorReading> {
boolean flag = true;
@Override
public void run(SourceContext<SensorReading> sourceContext) throws Exception {
while (flag){ // 使一直保持输入状态,模拟一直接受数据
sourceContext.collect(new SensorReading("sensor_"+new Random().nextInt(10),System.currentTimeMillis(),new Random().nextInt(9)+30.0));
}
}
@Override
public void cancel() {
flag = false;
}
}
}
transformation:
- map和filter,flatmap,keyby和sum,connect,union,CoMapFunction和conFlatMapFunction。
- map是一对一,flatmap可以一对一和一对多。
- connect:可以允许数据源的数据类型不一样。
- union:对两个或者两个以上的相同类型的DataStream进行union操作,产生一个包含所有DataStream元素的新DataStream
- 代码如下:
StreamExecutionEnviroment env = StreamExecutionEnviroment.getExecutionEnviroment();
DataStreamSource<Long> text1 = env.addSource(new MyNoParalleSource());
DataStreamSource<Long> text2 = env.addSource(new MyNoParalleSource());
DataStreamSource<String> text3 = env.addSource(new MyNoParalleSource());
//union要求两个流的类型一致
DataStream<Long> text= text1.union(text2);
DataStream<Long> num = text.map(new MapFunction<Long,Long>)(){
@Override
public Long map(Long value) throws Exception{
System.out.println("原始接收到数据:"+value);
return value;
}
};
//connect允许两个数据源的数据类型不一样
ConnectStream<Long,String> connectStream = text1.connect(text3);
//这里如果是.map()方法就实现CoMapFunction,如果是.flatmap()方法就实现conFlatMapFunction
connectStream.map(new CoMapFunction<Long,String,Object>(){
//这个方法处理数据源1
@Override
public Object map1(Long value) throws Exception{
System.out.println("map1:"+value);
return value;
}
//这个方法处理数据源2
@Override
public Object map1(String value) throws Exception{
System.out.println("map2:"+value);
return value;
}
});
sink:
- 自带的sink: print()/printToErr(),writeAsText()。
- 扩展的sink:Redis,HBase,HDFS,Apache kafka(source/sink),Apache Cassandra(sink),Amazon Kinesis Streams(source/sink),elasticsearch(sink),hadoop filesystem(sink),RabbitMQ(source/sink),Apache NiFi(source/sink),Twitter Streaming API(source),Google pubSub(sourceSink)。
五、FlinkBatch(dataSet)
source:
- 基于文件:readTextFile(path)。
- 基于集合:fromCollection(collection)。
- 实时用keyby,离线用groupby。
transformation:
Map,FlatMap,MapPartition,Filter,Reduce,sum,max,min,distinct,join,outerJoin,cross,union,first-n,sortPartition,partitionByRange,withBroadcast()广播,counter累加器。
六、Flink State
- state:一般指一个具体的task/operator的状态。State可以被记录,在失败的情况下数据还可以恢复。
- Flink中有两种基本类型的state:keyed state和operator state,他们都以两种形式存在,原始状态(raw state)和托管状态(managed state)。
- 托管状态:由Flink框架管理的状态,我们通常使用的就是这种。
- 原始状态:由用户自行管理状态具体的数据结构,框架在做checkpoint的时候,使用byte[]来读写状态内容,对其内部数据结构一无所知。通常在dataStream上的状态推荐使用托管状态,当实现一个用户自定义的operator时会使用到原始状态。但工作中一般不常用,所以不考虑它。
- operator state特点:只有listState,broadcastState(MapState形式,实现控制流效果)。凡是没有keyby的state都是operator state。state是task级别的state,说白了就是每个task对应一个state。kafka connector source中的每个分区(task)都需要记录消费的topic的partition和offset等信息。
- keyed state:valueState,listState,mapState,reducingState(累加),aggregatingState(累加,规则较复杂);特点:一个key就是一个state。
- 注册状态调用open方法,是一个初始化的方法,只会调用一次。(不同算子中的状态描述的名字一样,会拿到状态吗?sum()方法内部也用到了state,查看源码)
- state存储位置,state与checkpoint,state源码。
- flink支持三种stateBackend:memoryStateBackend默认的state类型就是这种,FsStateBackend文件存储状态,RocksDBStateBackend数据库存储状态。
- memoryStateBackend:默认情况下,状态信息是存储在taskManager的堆内存中的,checkpoint的时候将状态保存到jobmanager的堆内存中。
缺点:只能保存数据量小的状态,状态数据有可能会丢失。优点:开发测试很方便。 - FsStateBackend:状态信息是存储在taskManager的堆内存中的,checkpoint的时候将状态保存到制定的文件中(HDFS等文件系统)。env.setStateBackend(new FsStateBackend("hdfs://192.168.xx.xx:xxxx/flink/checkpoint/xx"));不设置就是默认存到内存中。
缺点:状态大小受taskmanager的内存限制(默认支持5M)。优点:状态访问速度很快,状态信息不回丢失。用途:生产,可以存储超大量的状态信息。 - 状态信息存储在RocksDB数据库(key-value的数据存储服务),最终保存在本地文件中。checkpoint的时候将状态保存到指定的文件中(HDFS等文件系统)。flink中自带了rocksDB,使用的时候引入rocksDB依赖,然后设置代码,env.setStateBackend(new RocksDBStateBackend("hdfs://192.168.xx.xx:xxxx/flink/checkpoint/xx"));
缺点:状态访问速度有所下降。优点:可以存储超大量的状态信息,状态信息不会丢失。用途:生产,可以存储超大量的状态信息。
七、checkpoint原理
https://zhuanlan.zhihu.com/p/104601440
- checkpoint机制是flink可靠性的基石,可以保证flink集群在某个算子因为某些原因(如异常退出)出现故障时,能够将整个应用流图的状态恢复到故障之前的某一状态,保证应用流图状态的一致性。剖析chandy-lamport算法如何实现checkpoint原理。
- 设置checkpoint。
- 重启策略:固定延迟重启,失败率重启,无重启策略。两种方法设置(以fixed-delay为例):第一种全局配置,flink-conf.yaml文件设置restart-strategy为fixed-delay;设置重启次数和间隔周期。第二种应用代码配置,env.setRestartStrategy(RestartStrategy.fixedDelayRestart(3,Time.of(10,TimeUnit.SECONDS)));
- savepoint。停止程序:bin/flink cancel -s [要保存的检查点的目标路径] jobid [-yid yarnAppId](针对onyarn模式需要指定-yid参数)。从指定的savepoint启动job:bin/flink run -s savepointPath[runArgs]
八、flink容错机制
- source容错,flink本身容错,sink容错。kafka,Cassandra可以支持恰好一次和至少一次,Redis,HBASE支持至少一次,但是通过幂等也能支持恰好一次。
- 精准一次处理。
九、flink窗口
- 无论设置time窗口还是count窗口,都使用window()算子,而不使用timeWindow()和countWindow(),flink1.12新特性会对后面两个window划线。
- event time:事件产生的时间,通常由事件中的时间戳描述。ingestion time:事件进入flink的时间(一般不用)。processing time:事件被处理时当前系统的时间。
- sparkStreaming 用的是什么时间?processing time。sparkStreaming支持event time么?不支持。
- watermark,迟到太多的数据默认丢弃,也可以使用侧输出流。多并行度的watermark。理解watermark机制,掌握watermark编码。
- state:keyed state,non keyed state;stream:keyed stream,non keyed stream;window:keyed window,non keyed window;
- keyed window:.keyBy(),.window(),.trigger(),.evictor(),.allowedLateData(),.sideOutputLateData(),.reduce/aggregate/fold/apply(),.getSideOutput()
- non-keyed window:.windowAll(),.trigger(),.evictor(),.allowedLateness(),.sideOutputLateData(),.reduce/aggregate/fold/apply(),.getSideOutput()
- window增量聚合:窗口每进入一条数据,就进行一次计算,等时间到了展示结果。常用聚合算子:reduce(reduceFunction),aggregate(aggregateFunction),sum(),min(),max()
- window全量聚合:等属于窗口的数据全部到齐,才开始进行聚合运算。【可以实现对窗口内的数据进行排序等需求】常用算子:apply(windowFunction),process(processWindowFunction)。processWindowFunction比windowFunction提供了更多的上下文信息,类似于map和RichMap的关系。
- window join:两个window之间可以进行join,join操作只支持三种类型的window,滚动窗口,滑动窗口,会话窗口。使用方式:stream.join(otherStream)//两个流进行关联.where(
)//选择第一个流的key作为关联字段.equalTo( )//选择第二个流的key作为关联字段.window( )//设置窗口的类型.apply( )//对结果进行操作。
十、Flink案例
- 项目架构:用户行为->flume采集->hdfs、kafka、Redis->flink进行ETL处理->kafka、Redis、ES;注意:由于Redis分区和flink并行度的关系,需要将Redis的数据源用广播流处理才能保证取到完整的值。
- 计算PV,uv,广告点击量,黑名单(10分钟内点击超过100次的用户列为黑名单),计算DAU。
- 布隆过滤:直观的说,bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中。和一般的hashset不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。
- 算法:首先需要k个hash函数,每个函数可以把key散列成一个整数;初始化时,需要一个长度为n比特的数组,每个比特位初始化为0;某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位改成1;判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。
- 引入Redis:在我们开发过程中,会有一些bool类型数据需要存取,比如用户一年的签到记录,签了是1,没签是0,要记录365天。如果使用普通的key/value,每个用户要记录365个,当用户上亿的时候,需要的存储空间是惊人的。为了解决这个问题,Redis提供了位图数据结构,这样每天的签到记录只占据一个位,365天就是365个位,46个字节(一个字节有8位)就可以完全容纳下,这就大大节约了存储空间。位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是byte数组。我们可以通过普通的get/set直接获取和设置整个位图的内容,也可以使用位图操作getbit/setbit等将byte数组看成 位数组 来处理。
- 使用布隆过滤器实时统计UV:1.读取数据,2.添加水位,3.过滤用户行为,4.所有数据同一个task,5.滚动窗口,6.触发器,7.统计计算
实现思路:写一个布隆过滤器,计算当前用户编号hash值,计算在布隆过滤器的位置,根据上个步骤更新结果。 - 布隆过滤器的默认大小是32M,3210271024*8,2^5 2^10 210*23,
- flink实时监测:同一用户3秒之内连续失败2次,则进行风控。实现思路:1.读取数据,->2.添加水位,->3.按照用户分组,->4.逻辑计算(设置state,存储失败任务。3s之内失败两次,告警)
十一、CEP
- 什么是cep:复杂事件处理,complex event processing,cep
- 什么是复杂事件:检测和发现无界事件流中多个记录的关联规则。cep就是在无界事件流中检测事件模式,让我们掌握数据中重要的部分。flink cep是在flink中实现的复杂事件处理库。
- cep使用流程:1.读取数据
a1 longEventStream = env.addSource(...);
2.定义匹配规则val loginFailPattern = Pattern.begin[LoginEvent]("begin").where(_.eventType == "fail").next("next").where(_.eventType == "fail").within(Time.seconds(3));
3.在事件流上应用匹配规则val patternStream = CEP.pattern(loginEventStream,loginFailPattern)
4.提取匹配事件val loginFailDataStream = patternStream.select(new LoginFailMatch())