flink知识与理解


source, 源、上游
sink,汇,下游
flink流批一体,api都是同一套,在代码的runtime mode里指定stream或batch,或者提交命令行参数指定。
flink事件驱动,数据来了才动(比如窗口的创建)。
flink收/发kafka时,默认情况是不对key做序列化/反序列化,要key的话需要加几行代码。

以词频统计为例,flink program的基本步骤

  • 创建环境
    stream有stream的环境,dataset有dataset的环境,还可以创建local测试环境看webui。一般直接get,会自己判断需要创建的环境)
  • 准备datasource,1.13后流批一体,env.fromsource()
    读文件,socket,或者kafka连接器。生产环境一般是无界流。
    flink消费kafka时,偏移量策略有earliest(一定从最早消费,与kafka同名策略的含义不同)等。
    也可以用flink的datagen来作为测试数据源。
  • 写flatmap。
    输入是String,输出是(String, Integer)的二元组,实际上是(string, 1)。
  • 用group by方法将二元组按[0]分组。
  • 将二元组按[1]聚合。
  • 输出。
    新api是sinkto,大多数用连接器。可以输出到hdfs(过程类似log4j输出文件,sink的并行度决定inprogress文件的个数),JDBC(mysql),kafka。
    写kafka需要配置序列化器,kafka地址,topic名称,kafka一致性级别,如果是精准一次,要开checkpoint,配kafka事务前缀、超时时间(小于15min)。
    写JDBC需要配置攒batch,重试次数,。
  • execute启动。
    普通execute会阻塞,有executeAsync,不过一般一个类一个execute。

下面是代码,适合新建文件时复制。

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;

public class WindowWordCount {

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

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStream<Tuple2<String, Integer>> dataStream = env
                .socketTextStream("localhost", 9999)
                .flatMap(new Splitter())
                .keyBy(value -> value.f0)
                .window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
                .sum(1);

        dataStream.print();

        env.execute("Window WordCount");
    }

    public static class Splitter implements FlatMapFunction<String, Tuple2<String, Integer>> {
        @Override
        public void flatMap(String sentence, Collector<Tuple2<String, Integer>> out) throws Exception {
            for (String word: sentence.split(" ")) {
                out.collect(new Tuple2<String, Integer>(word, 1));
            }
        }
    }

}

上面用的flatmap,下面这个是map,匿名类写法,输入是String类型,输出是Integer类型。

DataStream<String> input = ...;

DataStream<Integer> parsed = input.map(new MapFunction<String, Integer>() {
    @Override
    public Integer map(String value) {
        return Integer.parseInt(value);
    }
});

常见的示例数据

上面的例子中,示例数据是一个个字符串,其实不太典型。
典型的示例数据有id,ts,vc三部分。ts是timestamp的缩写。

flink

Flink有四大基石,Checkpoint、State、Time、Window。

批流统一,同一套代码可以跑流也可以跑批。使用datastream API,放弃dataset API。
用流api做批处理时,提交任务时将execution.runtime-mode设为batch即可。

spark streaming与flink并列竞争。

可以与yarn、k8s集成。
一致性检查点,确保端到端精确一次。

数据流上的有状态计算。
无界流,比如kafka,必须马上处理。
有界流,比如文件,可以等完了再处理,也称为批。
状态,比如在路边数车,可以用正字计数(直接记录数据),也可以更新阿拉伯数字(中间的计算结果)。状态存在flink里的RockSDB,定期持久化。

算子可以连点,常用匿名实现类和匿名表达式。
匿名表达式可能会因为java泛型擦除报错,提供type hint即可,
如returns(Types.TUPLE(Types.INT, Types.STRING, Types.INT))。

常用算子

无状态算子,只要拿输入就能输出
map,一个输入一个输出,return输出值
flatmap,一个输入0到多个输出,输出次数取决于collect函数次数
filter,true则保留、输出,false则丢弃
keyby将数据分组,同一个组的数据会在同一个子任务里
有状态算子,
窗口,需要记录窗口中的每一个数据
sum/min/max/minBy/maxB。加by表示取所有字段,不加by只取比较字段。
reduce,两两聚合。注意第一条数据不会进reduce方法。

map算子的入参是一个map function,filter算子的入参是filter function,
这些function都有对应的rich版,
可以重写open/close方法,分别在子任务启动/结束时运行,
或调用getRuntimeContext方法,获取上下文信息,比如当前子任务编号,
从而让算子有状态。

分流与合流。
可以互斥filter多次(比如奇数偶数),实现分流,不过重复处理性能差;
可用process()和ctx.output(outputtag, value)来实现侧输出。print输出主流,getsideoutput输出测流。
union可合并多条流,如src1.union(src2).union(src3)。要求类型一致,仍得datastream。
connect可以实现不同类型的两条流的合并,得到connectedstream,处理时map1、map2各自处理。
可以使用connect来实现inner join。

process函数可以访问时间戳,timerService(可按处理/事件时间设闹钟,时间到了就触发onTimer函数。处理时间取决于系统时间,而事件时间的流逝依赖于新数据到来),以及侧输出。定时器会自动去重。
按场合不同有八种processFunction。比如有没有开窗、有没有keyby、有没有connect。

并行度

并行度是针对算子而言的,一个算子子任务的个数。每个算子的子任务个数可以不同。
对算子调用.setparallelism(num)可以设置并行度。也可以对env调用.setparallelism(num)设置全局。
提交作业时也可以指定并行度。
算子局部优先级>代码全局优先级>提交全局优先级>conf文件默认并行度。
作业的并行度是所有算子中最大并行度。

算子链

算子之间的连接有两种,
一种是one-to-one,如forward,直接相连,下游数据顺序与上游完全相同,类似spark窄依赖;
另一种是重分区,如keyby/rebalance轮询/shuffle随机/rescale局部轮询/broadcast广播,
重分区时一个上游发往多个下游,类似spark shuffle宽依赖。

如果两个算子并行度相同,且是one-to-one连接,则算子会连起来(合并),以减少线程切换,如果不连的话,箭头处就要切换线程。

如果计算任务很重,或者需要定位问题,就需要禁用算子链。
可以对env调用disable函数关闭operator chaining。也可以对某个算子关闭。对算子也可以startnewchain只断前面不断后面。

任务槽

taskmanager是jvm进程,它可以启动多个子任务(子线程)。
分槽主要是为了划分内存互不打扰,一个槽里的资源量是固定的,不能互相匀,严格划分。
虽然分槽不分cpu,但一般令单task manager的slot数=机器cpu数,可以避免不同任务竞争cpu。
在这里插入图片描述
同一个slot里同一个算子只能出现一次,比如上图,同一个slot里不会有两个source+map。
因此,slot总数决定了同一个算子出现的次数上限。如果任一算子并行度超过slot总数,standalone无法启动,yarn模式会动态多申请taskmanager,直到slot总数足够。
slot中的子任务,同时在运行。
各算子的默认slot共享组都是default,如果不在一个slot共享组则不会共享slot。
slot共享可以让需求不同的任务搭配,此外,单个task manager故障时也还能保障完整链路。

提交流程

在这里插入图片描述

逻辑流图就是算子计算顺序示意图,
作业流图合并算子,
执行流图按并行度展开。

状态和检查点

状态一般使用托管状态,不用原始状态。
检查点就是把状态存到外部存储。
barrier和watermark有点像,数据流示意图中,普通数据是圆形,barrier和watermark就是方型,特殊类型的数据。

算子状态按算子并行子任务实例维护。比如flink消费kafka,要给source记录偏移量状态。一般直接用连接器就行,不需要我们实现。
按键分区状态按keyby的key维护。keyby之后才能聚合,因为要先有每个组各自的状态。日常使用较多。

状态有生存时间TTL,当状态在内存中存在的时间超出这个值时,就将他删除。

由状态后端管理状态的存储位置,默认是hashmapStateBackend,也可以是EmbeddedRocksDBStateBackend。
前者保存在taskmanager jvm堆上,快一个数量级。应用的状态可能会越来越大,内存不够用时不得不存硬盘。
后者保存在硬盘上,序列化和反序列化有些慢,配置中开启增量检查点,以提高保存效率。

增量检查点与检查点没有关联。changelog和state table都会持久化。

可以在各个job的env或者提交参数中指定状态后端。

chandy-lamport,分布式快照,不暂停。
类似水位线,jobmanager会把带barrier的数据发给所有source,source在ckpt完成后会回报。
上游-下游一对多,需要广播;
上游-下游多对一,需要对齐。
对齐的意思是,一个task在收齐同一编号的所有barrier后才开始后续处理。
精确一次:等待收齐的过程,缓存,停止处理。数据无法越过barrier。收齐开始备份。缺点是阻塞。
至少一次:等待收齐的过程不阻塞,数据可能越过barrier。缺点是可能导致重复计算。收齐开始备份。
非barrier的精准一次:在第一个barrier到达输入缓冲区时,马上开始备份,barrier直接跳到输出缓冲区末端,记录被其越过的输入缓冲区、输出缓冲区里面的数据。恢复时将被越过的数据恢复到原来的位置。最后一个barrier到达时停止备份。缺点是io开销和存储开销。

检查点保存过程中,每个任务互不影响。

默认情况下,Flink 只会保留一个成功的 Checkpoint。

检查点是自动的、定期的,针对的是意外停机(failure)。
保存点相当于用户手动保存。原理一样,仅触发时机不同。
借助savepoint,实时处理也几乎能实现热更新。
flink更新、代码更新、bug修复、不同版本应用的AB测试,集群迁移、集群扩展、并行度更新。

背压

反压可能是短暂的,比如,GC,检查点生成,任务重启时处理积压的数据等,都会造成反压,但这些场景通常可以忽略。

每个算子都有输入和输出缓冲区。

背压检测,JobManager 对taskmanager中的task调用Thread.getStackTrace(),
每50ms为每个任务触发100个堆栈跟踪,统计有多少堆栈跟踪被阻塞,
分为OK,low,high三档,10%和50%是两个分界点。
背压检测,缓冲池满了,发送者被阻塞。类似阻塞队列。下游会通知上游造成减速,使得反压传递。

解决方法,调大瓶颈的并发度,做成算子链。

时间,窗口与水位线

事件时间本身是有逻辑语义,而处理时间会随网络波动。flink1.12后,默认事件时间。

窗口,比如统计9点到10点的uv unique visitor,就需要窗口。窗口的结束,标志着处理的开始。如果说数据是水流,窗口就像水桶。见下面水位线的图。
窗口分为时间窗口(定点发车)和计数窗口(人齐发车)。
窗口也可以分为:
滚动窗口,窗口之间首尾衔接,每个数据只属于一个窗口;
滑动窗口,滑动步长可小于窗口大小,所以一个数据可能属于多个窗口;每隔步长输出当前最新窗;
会话窗口,会话有超时时间(静态/动态),如果没有超时就属于同一个会话,超过了就新会话;
全局窗口,需要自定义触发器来结算。也可以自定义移除器,在触发前/后移除一些数据。
窗口相关的逻辑一般分两步,开窗时window函数参数指定类型,写窗口aggregate处理函数。
口答时可以这样说:先key by,再开窗,然后reduce/aggregate。
开窗时无key by(api名字带all后缀)则只有一条逻辑流,同一个task,并行度为1。key by相反。
在这里插入图片描述
reduce是增量聚合,reduce函数每来一条数据就会执行一次。输入、累加器、输出的类型相同。
aggregate也是增量聚合,可以让输入、累加器、输出的类型不同。它更通用,代码量更大。
全窗口聚合一般用process,需要processWindowFunction,可以从入参context获得window等。
增量聚合和全窗口聚合可以嵌套使用,全窗口中只存当前的一条增量聚合结果,也有上下文。

聚合可以在窗上做,也可以直接key by不开窗就聚合。

窗口的getStart会取最近一个上界点,比如10s的滚动窗口,取getstart,总会是整十秒。

窗口会在属于此窗口的第一条数据到来时单例创建,窗口触发后延迟x秒销毁(x默认为0)。

提取每个区间中(区间大小可以为1)数据中事件时间的最大值,保持单调增(乱序数据),这就是水位线watermark。水位线能标志当前事件处理进度。
watermark = 当前最大事件时间 - 延迟时间 - 1ms
1ms是考虑区间开闭的问题,把压线数据也算进来。
watermark也是数据,也会插入流,且在原数据之后。因此process函数获取当前watermark时会有一条数据的延迟。

为了避免窗口错过乱序时的迟到数据,可以将水位线在所取事件时间的基础上统一减一个常量。有序流不需要这个延迟,顺序越乱,延迟应当更高。
这样做并不会错误处理早到数据,如下图:
在这里插入图片描述
水位线体现了低延时和准确性的权衡。
极致的低时延,可以直接用处理时间。
极致的准确性,要求水位线的延迟非常大。

写代码的时候,需要告知数据中哪部分代表事件时间,其它细节暂略。

分层API

在这里插入图片描述
官网中有这三层的demo。代码量,自然是上层少,下层多。

统计窗口内topN

先按统计量key by,
开滑动窗口,
增量聚合统计出现次数,
全窗口给数据打所属窗口的标签,
(aggregate函数可以有两个参数,一个aggregate function,一个processWindowFunction)
(聚合后变为普通DataStream,流中数据为(windowEnd, 统计量,出现次数))
按窗口标签keyby,
最后keyedProcessFunction,每来一条数据就加入list里并设一次定时器windowEnd+1,onTimer的动作就是先sort再取前n个。这里有说法是flink sql使用红黑树来做top n。

flink提供的库

CEP complex event processing:用于流处理中的模式识别
Gelly:可扩展的图处理和图分析,已经实现了label propagation,triangle enumeration和page rank。
也提供方便实现自定义图算法的API。

端到端精确一次

一致性,考虑重复和缺失,分为至少一次、至多一次、精确一次三种。
端到端包括flink的上游和下游。一致性等级有木桶效应,所以需要source和sink都能保证精准一次。

source如果是kafka,可以重置offset,这是精确一次的前提(上游可重放)。
检查点只可以保证flink内部精准一次。

kafka可以重置读取偏移量。因此输入端可以保证可重放。
两阶段提交,利用了flink的检查点机制和外部提供的事务:
第一阶段预提交,从第一个barrier到达开始新事务,各自提交,此时外部系统查不到此数据,
第二阶段,检查点完成之后,如果每一个都提交成功,则正式提交。否则回滚。
正式提交如果失败,从检查点恢复,重新启动外部事务,所以检查点也有必要保存事务的状态。
两阶段提交的逻辑也已经被封装在连接器里。
外部事务的超时时间需要大于flink检查点时间。

flink如果写hdfs,hdfs不提供事务,可以用sink任务模拟事务,比如预提交的log带一长串后缀,类似临时文件,正式提交时重命名文件删除这个后缀。

flink写kafka时注意,kafka默认read uncommitted,2pc的时候,即使是预提交,也会被消费,
所以要在此kafka的消费者(假设为flink)的隔离级别设为read comitted。

24/7

checkpoint和可重置stream source联合保证了恰好一次状态一致性。
异步、增量保证checkpoint对latency service layer agreements(SLAs)的影响很小。
对不同的存储系统,flink提供transactional sinks,保证即使failure也能恰好一次。
flink与cluster managers紧密集成。比如Hadoop YARN,Mesos,Kubernetes。如果一个进程fail,另一个新进程会自动接管。
HA-mode,高可用模式,基于ZooKeeper,消除所有单点failure。

重点作业指标

  • 延迟
    作业消费kafka的延迟时间/延迟条目数。延迟过大,说明作业性能有问题,flink消费kafka的能力不足。

  • 吞吐
    判断作业当前并发下总消费数据量,如果跟kafka实际数据量的差距较大,说明flink消费kafka的能力不足。

  • checkpoint大小
    state不应过大,如果checkpoint过大,说明数据流动在DAG中不顺畅,有堆积。

  • dag结点是否存在反压backpressure
    反压是一种现象,在数据流从上游生产者向下游消费者传输的过程中,上游生产速度大于下游消费速度,导致下游的 Buffer 溢出,这就是反压现象。关键是buffer溢出。
    在flink中,下游如果处理不过来,上游就会堆积,一直往下游找,第一个没有出现堆积的就是问题根源。

开发实践

map函数中不能包含类的成员,否则会报无法序列化的错。
不能访问closure中的属性。
闭包是js和函数式编程中比较重要的概念,在其它语言中也有。主要作用是让函数可以获取作用域外的变量(比如函数a内有函数b,b可以使用b外、a内的变量),还可以让这些变量的值始终保持在内存中。这可能会造成内存泄漏。所以闭包清理自成一个话题。flink closurecleaner可以自动化闭包清理的过程。
项目结构上,map、flatmap逻辑如果比较多,可以单独放在一个目录里。

看见一个项目中大量使用filter做分流。使用filter和sideoutput分流的辨析:https://www.cnblogs.com/chouc/p/17191452.html

运维实践

在这里插入图片描述
flink web ui上可以看见上面的图,四个部分都可以配置。
flink web ui上还可以看见jvm gc collector的次数和时间,共两种collector,ConcurrentMarkSweep,ParNew。

典型的flink jvm配置

-Xmx1530082096
-Xms1530082096
-XX:MaxMetaspaceSize=268435456
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:MaxHeapFreeRatio=70
-XX:+CMSClassUnloadingEnabled
-XX:OnOutOfMemoryError=kill -9 %p
-Dlog.file=/var/log/yarn/xxxx/jobmanager.log
-Dlog4j.configuration=file:log4j.properties
-Dlog4j.configurationFile=file:log4j.properties

flink用到akka。关键参数是client timeout,ask timeout,lookup timeout,设为了600s。

flink监控的关键指标:
jobmanager级别:
job是否反压,判断依据是subtask的反压平均值,超过0.7则认定为反压;
failed/inprogress/completed的检查点数量、最近一次检查点持续时间/大小;(ckpt由jobmanager发起)
flink jobmanager metric中,cpu(分为load和time)/内存/io属于基本,且内存并不简单分为很多metric参数,此外至少要答出GC(分为marksweep和scavenge,或者分为old和young)的次数和耗时;
slot的可用数/总数;
taskmanager的数量;(决定slot数,若减少可能是taskmanager挂掉重启)
running job的数量;
job的启动/停止/持续时间;
job的重启次数;(一直重启是资源无效浪费)
taskmanager级别:cpu/内存/GC;jvm启动以来加载/卸载类的个数
task级别:输入/出缓冲区使用量/排队量(输入端堆积说明是源头,输出端堆积说明还在下游);水位线;入/出流速(判断延迟、丢失)

flink jobmanager关注的jvm memory metric共12个,包括:
direct/mapped count/memory used/total capacity
heap/nonheap committed/max/used

flink和spark的对比

flink的基本数据结构是stream,spark的基本数据结构是RDD。
flink把批当成流,spark把流当成批。
flink的实时性更好,而spark则对SQL支持更好。
flink比较新,社区比较不成熟。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值