Flink入门看这篇就够了(1.12)02

348 阅读21分钟

1.0 状态编程与容错机制

状态编程转存失败,建议直接上传图片文件
* TODO 1.什么是状态
         *  保存起来的数据,可以是历史的计算结果,也可以是数据本身
         *
         * TODO 2.状态分类
         *  1.算子状态
         *      =》 作用范围:算子
         *      =》 一个并行任务 维护 一个状态 =》 同一个算子的多个并行任务之间,状态不共享
         *      =》 实现方式: 继承 一个接口(CheckpointedFunction),重写两个方法(快照、初始化)
         *      =》 主要用在: source
         *      =》 数据结构: List
         *  2.键控状态
         *      =》 作用范围: 同一分组
         *      =》 每个分组 维护 一个状态 =》 即使 多个分组在同一个 并行任务中, 每个分组还是各自维护一个状态
         *      =》 数据结构: value、list、map、reducing、aggregating
         *      =》 使用步骤: 定义 =》 open里 创建 =》 使用
         *
         * TODO 3.状态后端干什么?
         *  1. 本地状态的管理
         *  2. 完成 checkpoint的远程存储
         *
         * TODO 4.状态后端的分类
         *  1.Memory
         *      =》 本地状态: TaskManager内存
         *      =》 checkpoint:JobManager内存
         *      =》 适用场景:本地测试
    		//单个state MaxSize 5M
    		//akka.frameSize 10M
         *  2.Fs
         *      =》 本地状态: TaskManager内存
         *      =》 checkpoint: 外部文件系统(HDFS)
         *      =》 适用场景:可以用于生产环境,主要是 分钟级窗口, 又大又长的状态;//需要开启HA
			//本地保存不超过内存
         *  3.RocksDB //需要引入依赖 唯一支持增量备份的方式
         *      =》 本地状态: TaskManager所在节点的RocksDB中(内存+磁盘)
         *      =》 checkpoint: 外部文件系统(HDFS)
         *      =》 适用场景:用于生产环境,可以支持 天级窗口,超大超长的状态
         *                   影响一点本地状态的读写效率
             //单key小于2G;总大小受限于内存+磁盘
         *
         * TODO 5.状态后端的指定
         *  1. 方式一: 配置文件 指定 默认的状态后端
         *  2. 方式二: 代码里指定, 执行环境 set ,如果是RocksDB,要先导入依赖
         *  3. 结合 开启 checkpoint,checkpoint默认是不打开的
         *
         * TODO 6.一致性级别
         *  1. at-most-once: 可能丢,不会重
         *  2. at-least-once:不会丢,可能重
         *  3. exactly-once: 不会丢,不会重
         *
         * TODO 7.端到端一致性
         *  1.source端: 可重置,可以重新获取数据
         *  2.flink内部: checkpoint保证
         *  3.sink端:    幂等写入、事务性写入(WAL、2PC)
         *      =》 事务性写入 都提供了 模板类

算子状态(operator state):继承了checkpointFunction

![img](E:\BIG DATA\知识总结\flink\pic\wps3.jpg)

键控状态(keyed state):每个key共享一个状态

![img](E:\BIG DATA\知识总结\flink\pic\wps4.jpg)

![img](E:\BIG DATA\知识总结\flink\pic\wps2.jpg)

状态后端 stateBackend

-------StateBackend 所有状态后端的父类:三个子类

  1. memoryStateBackEnd:
  2. FsStateBackend:
  3. RockDBBackEnd:本地需要序列化存储到本地的RockDB数据库;远程存储在文件系统

主要关注:

  1. 本地状态的管理
  2. 完成 checkpoint的远程存储

1.5 CheckPoint

Flink检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确。记住这一基本点之后,Flink为用户提供了用来定义状态的工具。

![img](E:\BIG DATA\知识总结\flink\pic\wps1.jpg)

  1. JM的 Checkpoint Coordinator(协调器) 向所有 source 节点 trigger Checkpoint;

  2. source 节点定期向下游广播 barrier,这个 barrier 就是实现 Chandy-Lamport 分布式快照算法的核心,下游的 task 只有收到所有 barrier(对齐式的) 的 barrier 才会执行相应的state状态存储于持久化(checkpoint);

    ----------当 task 完成 state 备份后,会将备份数据的地址(state handle)通知给 Checkpoint coordinator。

    ----------下游的 sink 节点收集齐上游两个 input 的 barrier 之后,会执行checkpoint照,完成后返回(state handle)通知给 Checkpoint coordinator。

  3. 所有节点的state本地存储完成后,认为完成了此checkpoint; Checkpoint Coordinator 将data meta持久化到远程存储,通知所有节点此次checkpoint完成;

  4. 当sink数据到达时,先把数据写入Kafka(此时不提交事务),当barrier到达时,sink状态进行保存并通知Kafka开启新的事务;当checkPoint完成时,收到通知提交事务彻底完成此次事务操作


    Barrier对齐:每一个算子要同时收到上游所有的checkpoint N后才会进行该算子的state存储,然后接受后面的数据。。。保证exactly once;

    Barrier不对齐:对齐就是指当还有其他流的 barrier 还没到达时,为了不影响性能,也不用理会,直接处理 barrier 之后的数据。等到所有流的 barrier 的都到达后,就可以对该 Operator 做 CheckPoint 了。 At Least Once。

Flink检查点算法的正式名称是异步分界线快照(asynchronous barrier snapshotting)

异步:数据的传输与checkpoint是异步执行的


分界线:barrier:用于分隔不同的checkpoint,对于每个任务而言,收到barrier就意味着要开始做state的保存 算子中需要对不同上游分区发来的barrier,进行对齐;

------barrier在数据处理上跟watermark是两套机制,完全没有关系;但是类似,都是插入数据流中的特殊数据结构;


轻量:checkpoint功能轻量化,基本不影响系统正常执行

该算法大致基于Chandy-Lamport分布式快照算法。检查点是Flink最有价值的创新之一,因为它使Flink可以保证exactly-once,并且不需要牺牲性能。

异常恢复

挂掉后从最近一次的checkpoint完成的节点开始恢复;

对于sink的事务性:要求可以有预提交状态的恢复功能;防止checkpoint完成到通知保存时挂了重启重新提交

背压

![缓存与背压机制](E:\BIG DATA\知识总结\flink\pic\image-20201225230708717.png)

当后面阻塞导致算子输出缓冲区阻塞------导致输入阻塞----最后一直到surce阻塞,导致背压

常用参数
// TODO Checkpoint常用配置(ck默认是禁用)
        env.enableCheckpointing(5000);  // 开启checkpoint  
****//执行间隔 默认500ms,一般生产中百万级秒数据 10分钟左右;看情况尽量小
//        env.enableCheckpointing(5000,CheckpointingMode.EXACTLY_ONCE);
//        env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
        env.getCheckpointConfig().setCheckpointTimeout(300000L);            // ck执行多久超时
        env.getCheckpointConfig().setMaxConcurrentCheckpoints(2);  
// 异步ck,同时有几个ck在执行;防止重叠过多,与下面的不冲突
        env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500L);        // 上一个ck结束后,到下一个ck开启,最小间隔多久-----上一次结束到下一次开始的间隔
        env.getCheckpointConfig().setPreferCheckpointForRecovery(false);        // 默认为 false,表示从 ck恢复;true,从savepoint恢复
        env.getCheckpointConfig().setTolerableCheckpointFailureNumber(3);        // 允许当前checkpoint失败的次数
端对端一致性:
幂等会出现暂时不一致:
是指一批数据回滚后,在发生故障前这批数据已经有写入sink的了,回滚会重新重播这部分数据,但是它是幂等操作,所以还是保证了Exactly-once。

---------------------------------------------------------------------------------------------------------------------------------------
预写日志(Write-Ahead-Log)
1)把结果数据先当成状态保存,然后收到checkpoint完成的通知时,一次性写入sink系统。
2)由于数据提前在状态后端(state backend)中做了缓存,所以无论什么 sink 系统,都能用这种方式一批搞定
3)DataStream API提供了一个模板类:GenericWriteAheadSink来实现这种事务性sink

---------------------------------------------------------------------------------------------------------------------------------------
两阶段提交two-parse commit
1)对于每个checkpoint,sink任务会启动一个事务,并将接下来所有接收的数据添加到事务里。
2)将这些数据写入外部sink系统,但是不提交它们,只是“预提交”。
3)当它收到checkpoint完成的通知时,才正式提交事务,实现结果的真正写入。
4)这种方式真正实现了exactly-once。
5)这种方式真正实现了exactly-once,它需要一个提供事务支持的外部sink系统,Flink提供了TwoPhaseCommitSinkFunction接口。

---------------------------------------------------------------------------------------------------------------------------------------
TwoPhaseCommitSink对外部sink系统的要求
1)外部 sink 系统必须提供事务支持,或者 sink 任务必须能够模拟外部系统上的事务
2)在 checkpoint 的间隔期间里,必须能够开启一个事务并接受数据写入
3)在收到 checkpoint 完成的通知之前,事务必须是“等待提交”的状态。
	在故障恢复的情况下,这可能需要一些时间。如果这个时候sink系统关闭事务(例如超时了),那么未提交的数据就会丢失
4)sink 任务必须能够在进程失败后恢复事务
5)提交事务必须是幂等操作

1.6 savepoint

#使用方法 ys是slot数量
flink -run -s xxxxxx -ys #提交
flink savepoint {jobID} {URL}

1、Flink 还提供了可以自定义的镜像保存功能,就是保存点(savepoints)

2、原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点

3、Flink不会自动创建保存点,因此用户(或外部调度程序)必须明确地触发创建操作

4、保存点是一个强大的功能。除了故障恢复外,保存点可以用于:有计划的手动备份,更新应用程序,版本迁移,暂停和重启应用,等等

2.0 实操案例练习

2.1 多久时间内统计多长时间内的TopN X

思路很重要:实时数仓的架构;使用的框架,分析的指标,建了哪些表,每个表的指标与流程

思路:

热门商品统计:实时窗口TopN

1.Environment 2.Source 3.Transform => 3.1 转换数据类型、指定watermark生成、事件时间提取 => 3.2 能过滤就先过滤 => 只需要pv行为,过滤出pv行为 => 3.3 考虑 统计维度 => 统计维度是 商品,按照商品分组 => 3.4 每隔5分钟输出最近一小时 => 滑动窗口,长度1小时,步长5分钟 => 3.5 统计求和 => 3.5.1 aggregate传两个参数 : 因为 聚合之后,就没有 窗口的 概念了 => 3.5.1.1 第一个参数: AggregateFunction 进行增量聚合 => 3.5.1.2 第二个参数: ProcessWindowFunction 对聚合后的结果,打上 窗口结束时间 的标签 => 3.6 按照 窗口结束时间 分组 : 让 同一个窗口的 统计结果 到一起,进行 TopN的计算 => 3.7 使用 process 进行排序 => processElement() 是一条一条处理数据的,所以要先把 同一个窗口的 统计结果 存起来 => 同一个窗口,考虑用 状态 来保存数据,因为按照 窗口结束时间 分组,那就等于按照 窗口 隔离 => 存到什么时候? => 注册一个定时器,注册时间 = 窗口结束时间 + 小延迟 => OnTimer() 进行排序 => 从状态里取出数据,放入到一个 List里 => 调用 list的 sort方法, 实现一个 Comparator接口,定义为 降序: 后减前 => 取前 N 个 : 如果要传参的方式,定义一个构造器 => 注意 传参 与 实际list 的大小,进行比较,取小的, 避免 数组下标越界问题 4.Sink

实现案例

/**
 * 接下来我们将实现一个“实时热门商品”的需求,可以将“实时热门商品”翻译成程序员更好理解的需求:
 * 每隔5分钟输出最近一小时内点击量最多的前N个商品
 */

public class Task1_TopN_SKU {
    public static void main(String[] args) throws Exception {
        //01.创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration()).setParallelism(4);
        //02.Get Source
        DataStreamSource<String> Ds = env.readTextFile("data/UserBehavior.csv", "utf-8");

        //03.Transform DataStream
        Ds.flatMap((String line, Collector<BehaviorBean> out) -> {
            String[] words = line.split(",");
            //3.1 error data ETL And transform to Bean
            if (words.length == 5) {
                out.collect(new BehaviorBean(Long.valueOf(words[0]), Long.valueOf(words[1]), Integer.valueOf(words[2]), words[3], Long.valueOf(words[4])));
            }
        })
                .returns(new TypeHint<BehaviorBean>() {
                })
                //3.2 set the watermark
                .assignTimestampsAndWatermarks(WatermarkStrategy.<BehaviorBean>forBoundedOutOfOrderness(Duration.ofSeconds(0))
                        .withTimestampAssigner((SerializableTimestampAssigner<BehaviorBean>) (element, recordTimestamp) -> {
                            return element.getTimestamp()*1000L;//ts 13 bytes
                        }))
                //3.2.5 ETL Data
                .filter(behavior -> (behavior.getBehavior().equals("pv")))
                //按照商品分类
                .keyBy(BehaviorBean::getItemId)
                //3.3 cut the working windows
                .window(SlidingEventTimeWindows.of(Time.hours(1),Time.minutes(5)))
                .aggregate(new SimpleAggregate<BehaviorBean>(), new ProcessWindowFunction<Long, SKUDataCountPV, Long, TimeWindow>() {
                    @Override
                    public void process(Long key, Context context, Iterable<Long> elements, Collector<SKUDataCountPV> out) throws Exception {
                        // 全窗口函数处理的数据量: 取决于有多少不同的商品, 中小规模的公司,商品数量也就在 十几万 到 小几十万(综合类的电商)
                        // 把商品的统计结果,添加上 窗口信息,窗口结束时间
                        // key: 进入process 是 一组一组进的, 那么这个key就是商品id
                        // count: 前面已经对每个商品做了聚合,每个商品一条聚合结果,所以elements里面只有一条统计结果
                        // windowEnd: 上下文获取
                        long endTs = context.window().getEnd();
                        Long count = elements.iterator().next();
                        out.collect(new SKUDataCountPV(key,count,endTs));
                    }
                })// can set two function including preMerge and window output
                .keyBy(SKUDataCountPV::getEndTs)
                //利用每个windows最后的时间戳作为分组隔离
                .process(new TopNGet(3))
                //sink
                .print();


        env.execute();
    }

    /**
     * 从每一个window中获取到所有的商品的count数,现在需要进行排序
     */
    static public class TopNGet extends KeyedProcessFunction<Long,SKUDataCountPV,String>{
        private ListState<SKUDataCountPV> listState;
        private ValueState<Long> clock;
        private int TopN;

        public TopNGet(int topN) {
            TopN = topN;
        }

        @Override
        public void open(Configuration parameters) throws Exception {
            listState=getRuntimeContext().getListState(new ListStateDescriptor<SKUDataCountPV>("listState",Types.POJO(SKUDataCountPV.class)));
            clock=getRuntimeContext().getState(new ValueStateDescriptor<Long>("clock",Types.LONG));
        }

        @Override
        public void processElement(SKUDataCountPV value, Context ctx, Collector<String> out) throws Exception {
            //创建一个当前endTs为基础的定时器作为收取数据的时间(5ms);到达定时器时开始聚合
            // 排序
            // 来一条存一条
            listState.add(value);
            if(clock.value() == null){
                ctx.timerService().registerEventTimeTimer(ctx.getCurrentKey()+5);
                clock.update(ctx.getCurrentKey()+5);
            }
        }

        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
            List<SKUDataCountPV> results = new ArrayList<>();
            for (SKUDataCountPV word : listState.get()) {
                results.add(word);
            }
            // 考虑性能和资源,把没用的释放掉
            listState.clear();
            //取出前N
            results.sort((o1, o2) -> (int) (o2.count-o1.count));
            //全部拼接解决线程乱序问题
            StringBuffer result = new StringBuffer();
            result.append("===================当前窗口结束时间为" + ctx.getCurrentKey() + "========================");
            for (int i = 0; i < Math.min(TopN, results.size()); i++) {
                result.append("No-" + (i + 1) + "为" + results.get(i)+"\n");
            }
            result.append("=============================当前窗口"+ctx.getCurrentKey()+"宣布结束==================");
            out.collect(result.toString());
        }
    }


}

2.2 实现黑名单过滤:

实现思路:

在进行开窗之前进行一个累计的过滤;

1.通过定时器实现每日黑名单数据的清空

2.通过IsAlarm开关来避免重复黑名单统计

3.通过侧输出流来输出黑名单

案例:

//黑名单
static public class myBlackListFilter extends KeyedProcessFunction<Tuple2<Long, Long>, AdsClickLog, AdsClickLog>{
        //a 首先统计出来每个key的点击截图按照状态保存
        ValueState<Long> clickCount;
        //b 加入一个告警清除闹钟
        ValueState<Long> clock;
        //c 加入一个告警开启记录
        ValueState<Boolean> isAlarm;

        @Override
        public void open(Configuration parameters) throws Exception {
            clickCount=getRuntimeContext().getState(new ValueStateDescriptor<Long>("clickCount",Types.LONG,0L));
            clock=getRuntimeContext().getState(new ValueStateDescriptor<Long>("clock",Types.LONG));
            isAlarm=getRuntimeContext().getState(new ValueStateDescriptor<Boolean>("isAlarm",Types.BOOLEAN,false));
        }

        @Override
        public void processElement(AdsClickLog value, Context ctx, Collector<AdsClickLog> out) throws Exception {
            Long ts = ctx.timestamp();
            //当点击数到达100后;需要停止计数并拒绝输出并加入黑名单
            if(clickCount.value()<100){
                clickCount.update(clickCount.value()+1);
                out.collect(value);
            }else if(!isAlarm.value()){
                //告警的放入侧输出流
                ctx.output(black,new BlackUser(value.getUserId(),value.getAdId(),ts));
                //更改报警状态,避免重复告警
                isAlarm.update(true);
            }
            //设置定时器第二日0点触发
            if(clock==null){
                clock.update((ts / (3600 * 24 * 1000) + 1)*3600*24*1000);
                ctx.timerService().registerEventTimeTimer(clock.value());
            }
        }

        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<AdsClickLog> out) throws Exception {
            clickCount.clear();
            clock.clear();
            isAlarm.clear();
        }
    }

//业务
 public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration());
        DataStreamSource<String> soc = env.readTextFile("data/AdClickLog.csv", "utf-8");
        KeySelector<AdsClickLog, Tuple2<Long, Long>> keySelector = new KeySelector<AdsClickLog, Tuple2<Long, Long>>() {
            @Override
            public Tuple2<Long, Long> getKey(AdsClickLog value) throws Exception {
                return Tuple2.of(value.getUserId(), value.getAdId());
            }
        };

        SingleOutputStreamOperator<AdsClickLog> blackResult = soc.flatMap((String line, Collector<AdsClickLog> out) -> {
            String[] words = line.split(",");
            if (words.length == 5) {
                out.collect(new AdsClickLog(Long.valueOf(words[0]), Long.valueOf(words[1]), words[2], words[3], Long.valueOf(words[4])));
            }
        })
                .returns(new TypeHint<AdsClickLog>() {
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<AdsClickLog>forBoundedOutOfOrderness(Duration.ofSeconds(10))
                        .withTimestampAssigner((value, ts) -> value.getTimestamp() * 1000L))
                .keyBy(keySelector)
//                .window(SlidingEventTimeWindows.of(Time.hours(1), Time.minutes(5)))不需要
                //需要在增量的时候就把黑名单搞出来
                .process(new myBlackListFilter());

        //打印黑名单
        blackResult.getSideOutput(black).print("blackList");

        //正常流输出;需要重新分区开窗
        blackResult.keyBy(keySelector)
                .window(SlidingEventTimeWindows.of(Time.hours(1),Time.minutes(5)))
                //增量聚合,把每一个window的每个user-adId的聚合到一起,避免全量直接聚合排序oom
                .aggregate(new SimpleAggregate<AdsClickLog>(), new ProcessWindowFunction<Long, AdWithBlackBean, Tuple2<Long, Long>, TimeWindow>() {
                    @Override
                    public void process(Tuple2<Long, Long> longLongTuple2, Context context, Iterable<Long> elements, Collector<AdWithBlackBean> out) throws Exception {
                        out.collect(new AdWithBlackBean(longLongTuple2.f0,longLongTuple2.f1,elements.iterator().next(),context.window().getEnd()));
                    }
                })
                .keyBy(AdWithBlackBean::getEndTs)
                .process(new KeyedProcessFunction<Long, AdWithBlackBean, String>() {
                    //定义几个状态储存同一个滑动窗口的数据
                    ListState<AdWithBlackBean> listState;
                    ValueState<Long> ClockToRk;

                    @Override
                    public void open(Configuration parameters) throws Exception {
                        listState=getRuntimeContext().getListState(new ListStateDescriptor<AdWithBlackBean>("listState",Types.POJO(AdWithBlackBean.class)));
                        ClockToRk=getRuntimeContext().getState(new ValueStateDescriptor<Long>("clock", Types.LONG));
                    }

                    @Override
                    public void processElement(AdWithBlackBean value, Context ctx, Collector<String> out) throws Exception {
                        listState.add(value);
                       Long ts=ctx.getCurrentKey();
                        if(ClockToRk.value()==null){
                           ctx.timerService().registerEventTimeTimer(ts+1);
                           ClockToRk.update(ts+1);
                        }
                    }

                    @Override
                    public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
                        List<AdWithBlackBean> results=new ArrayList<>();
                        for (AdWithBlackBean word : listState.get()) {
                            results.add(word);
                        }
                        ClockToRk.clear();
                        listState.clear();
                        Collections.sort(results);

                        StringBuffer result = new StringBuffer();
                        result.append("===================当前窗口结束时间为" + ctx.getCurrentKey() + "========================\n");
                        for (int i = 0; i < Math.min(3, results.size()); i++) {
                            result.append("No-" + (i + 1) + "为" + results.get(i)+"\n");
                        }
                        result.append("=============================当前窗口"+ctx.getCurrentKey()+"宣布结束==================\n\n\n");
                        out.collect(result.toString());
                    }
                })
                .print("主要输出" );

        env.execute();
    }

2.3 连续登录拉黑处理

**
 *当用户连续2s内发生连续两次登录失败认为恶意登录;
 * 待优化实现:使用时间戳进行判断
 */
public class Task4_FuckLoginCount {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration());
        DataStreamSource<String> soc = env.readTextFile("data/loginLog.csv", "utf-8");

        soc.flatMap((String line, Collector<LoginLOJO> out)->{
            String[] words = line.split(",");
            if (words.length==4){
                out.collect(new LoginLOJO(Long.valueOf(words[0]),words[1],words[2],Long.valueOf(words[3])));
            }
        })
                .returns(new TypeHint<LoginLOJO>() {})
                .assignTimestampsAndWatermarks(WatermarkStrategy.<LoginLOJO>forBoundedOutOfOrderness(Duration.ofSeconds(10))
                        .withTimestampAssigner((login,ts)->login.getTimeStamp()*1000L))
                .keyBy(LoginLOJO::getUserId)
                .process( new myKeyed())
                .print();
        env.execute();
    }

    static public class myKeyed extends KeyedProcessFunction<Long, LoginLOJO, String> {
        ValueState<Long> SuccessTimer;
        ValueState<Long> FailTimer;
        ListState<Long> FailList;

        @Override
        public void open(Configuration parameters) throws Exception {
            SuccessTimer=getRuntimeContext().getState(new ValueStateDescriptor<Long>("success", Types.LONG));
            FailTimer=getRuntimeContext().getState(new ValueStateDescriptor<Long>("FailTimer", Types.LONG));
            FailList=getRuntimeContext().getListState(new ListStateDescriptor<Long>("FailList",Types.LONG));
        }

        @Override
        public void processElement(LoginLOJO value, Context ctx, Collector<String> out) throws Exception {
            //收到数据后,首先进行登录状态确认
            if("fail".equals(value.getLoginState())){
                //如果有定时器存在且均大于当前时间戳,那说明直接加入就行了,事实上还要做时间超界限判断
                if (FailTimer.value() ==null && SuccessTimer.value() ==null){
                    ctx.timerService().registerEventTimeTimer(ctx.timestamp()+2*1000L);
                    FailTimer.update(ctx.timestamp()+2*1000L);
                    FailList.add(value.getTimeStamp());
                }else if((SuccessTimer.value() == null) || SuccessTimer.value() > ctx.timestamp()){
//                    (SuccessTimer.value() > ctx.timestamp()
                    System.out.println(SuccessTimer.value()+"-"+ctx.timestamp());
                    FailList.add(value.getTimeStamp());
                }
            }else if ("success".equals(value.getLoginState())){
                //如果失败时间戳不为null
                if(FailTimer.value() !=null){
                    ctx.timerService().deleteEventTimeTimer(FailTimer.value());
                    ctx.timerService().registerEventTimeTimer(ctx.timestamp());
                    SuccessTimer.update(ctx.timestamp());
                }else if(SuccessTimer.value()!=null && SuccessTimer.value()>value.getTimeStamp()){
                    ctx.timerService().deleteEventTimeTimer(SuccessTimer.value());
                    ctx.timerService().registerEventTimeTimer(ctx.timestamp());
                    SuccessTimer.update(ctx.timestamp());
                }
            }
        }

        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
//            System.out.println(ctx.getCurrentKey()+" " +FailList.get().spliterator().estimateSize());
            if(FailList.get().spliterator().estimateSize()>=2 ){
                Iterator<Long> iterator = FailList.get().iterator();
                Long No1 =iterator .next();
                Long No2 =iterator.next();
                if(No2-No1<2){
                    out.collect(ctx.getCurrentKey()+"连续登录失败2次"+(No2-No1));
                }
            }
            SuccessTimer.clear();
            FailTimer.clear();
            FailList.clear();
        }
    }
}

3.0 CEP 复杂事务处理

  • 目标:从有序的简单事件流中发现一些高阶特征
  • 输入:一个或多个由简单事件构成的事件流
  • 处理:识别简单事件之间的内在联系,多个符合一定规则的简单事件构成复杂事件
  • 输出:满足规则的复杂事件

优点:高效,快速;自带乱序解决方案;输出符合条件的结果与超时的结果

Ø 输入的流数据,尽快产生结果

Ø 在2个event流上,基于时间进行聚合类的计算

Ø 提供实时/准实时的警告和通知

Ø 在多样的数据源中产生关联并分析模式

Ø 高吞吐、低延迟的处理

缺点:无法解决特殊数据异常;Map如果没有匹配上可能会造成资源持续占用耗费大量资源;

3.1 基本操作

/测试普通与或条件
    @Test
    public void test1() {
        //CEP API使用:
        // 1.先定义规则
        //where:指定条件,可以调用多次,每次调用默认为and关系
        //or:指定或的条件
        // 2.再应用规则
        // 3.处理匹配结果
        Pattern<LoginLOJO, LoginLOJO> pat = Pattern
                .<LoginLOJO>begin("start")
                .where(new IterativeCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value, Context<LoginLOJO> ctx) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                })
//                .where(new SimpleCondition<LoginLOJO>() )
                .or(new SimpleCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value) throws Exception {
                        return value.getTimeStamp() > 1558430853;
                    }
                });
        PatternStream<LoginLOJO> PS = CEP.pattern(ds, pat);
        PS.select(new PatternSelectFunction<LoginLOJO, String>() {
            @Override
            public String select(Map<String, List<LoginLOJO>> pattern) throws Exception {
                return pattern.toString();
            }
        }).print();
    }
//测试模式序列
    // 1. next: 严格近邻, 两个事件 必须 紧挨着,中间不能有其他事件(不能有小三)
    // 1.5 notNext:排除后面紧跟紧邻 [必须后面有数据才行]
    // 2. followedBy:宽松近邻, 两个事件 先后出现即可,不需要紧挨着, 只匹配上一次就行了 (一夫一妻) []
    // 2.5 notFollowedBy:不能作为作为最后一个事件,作用范围: 上一个事件之后,下一个事件之前
    // 3. followedByAny: 非确定性宽松近邻, 两个事件 先后出现即可,不需要紧挨着, 有多少个就匹配多少个 (一夫多妻) [至少一个]
    @Test
    public void Test2() {
        Pattern<LoginLOJO, LoginLOJO> Pat = Pattern
                .<LoginLOJO>begin("start")
                .where(new IterativeCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value, Context<LoginLOJO> ctx) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                })
/*                .notNext("notNext")
                .where(new SimpleCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                });
                .next("next")
                .where(new SimpleCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                })
                .followedBy("follow")
                .where(new SimpleCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                })*/
                .notFollowedBy("asdfdfd")
                .where(new SimpleCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                })
                .followedByAny("followAny")
                .where(new SimpleCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                });

        PatternStream<LoginLOJO> PS = CEP.pattern(ds, Pat);
        PS.select(new PatternSelectFunction<LoginLOJO, String>() {
            @Override
            public String select(Map<String, List<LoginLOJO>> pattern) throws Exception {
                return pattern.toString();
            }
        })
                .print();
    }
//量词:在本条件起到做作用
    @Test
    public void Test3() {
        Pattern<LoginLOJO, LoginLOJO> Pat = Pattern
                .<LoginLOJO>begin("start")
                .where(new IterativeCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value, Context<LoginLOJO> ctx) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                })
//                .times(2);//后面匹配一个勾陈两个的数组(集合)
                .times(1, 3)
                .next("next")
                .where(new IterativeCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value, Context<LoginLOJO> ctx) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                })
                .within(Time.seconds(10L));//增对匹配的最早时间和最晚时间差进行限制


        PatternStream<LoginLOJO> PS = CEP.pattern(ds, Pat);
        PS.select(new PatternSelectFunction<LoginLOJO, String>() {
            @Override
            public String select(Map<String, List<LoginLOJO>> pattern) throws Exception {
                return pattern.toString();
            }
        })
                .print();
    }
   //optional:可选的    greedy:饥渴的,尽可能多的匹配
    @Test
    public void Test4() {
        Pattern<LoginLOJO, LoginLOJO> Pat = Pattern
                .<LoginLOJO>begin("start")
                .where(new IterativeCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value, Context<LoginLOJO> ctx) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                })
//                .greedy()//饥渴的、尽可能多的匹配,必须搭配量词
                .next("next")
                .where(new IterativeCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value, Context<LoginLOJO> ctx) throws Exception {
                        return value.getLoginState().equals("false");
                    }
                })
                .optional();//可选的,前面的条件或者量词可以没有匹配上


        PatternStream<LoginLOJO> PS = CEP.pattern(ds, Pat);
        PS.select(new PatternSelectFunction<LoginLOJO, String>() {
            @Override
            public String select(Map<String, List<LoginLOJO>> pattern) throws Exception {
                return pattern.toString();
            }
        })
                .print();
    }

3.2 高效解决登录异常拉黑分析

/**
 *当用户连续2s内发生连续两次登录失败认为恶意登录;
 */
public class Task2_FuckLoginCount_CE {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration());

        KeyedStream<OrderEven, Long> OrKS = env.readTextFile("data/OrderLog.csv")
                .map(line -> {
                    String[] words = line.split(",");
                    return new OrderEven(Long.valueOf(words[0]), words[1], words[2], Long.valueOf(words[3]));
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<OrderEven>forBoundedOutOfOrderness(Duration.ofSeconds(0))
                        .withTimestampAssigner((ele, ts) -> ele.getEventTime() * 1000L))
                .keyBy(OrderEven::getOrderId);


        Pattern<OrderEven, OrderEven> pat = Pattern
                .<OrderEven>begin("start")
                .where(new SimpleCondition<OrderEven>() {
                    @Override
                    public boolean filter(OrderEven value) throws Exception {
                        return "create".equals(value.getEventType());
                    }
                })
                .next("付钱")
                .where(new SimpleCondition<OrderEven>() {
                    @Override
                    public boolean filter(OrderEven value) throws Exception {
                        return "pay".equals(value.getEventType());
                    }
                })
                .within(Time.minutes(15));

        PatternStream<OrderEven> pattern = CEP.pattern(OrKS, pat);

        OutputTag<String> outputTag = new OutputTag<String>("超时", Types.STRING) {        };
        SingleOutputStreamOperator<String> select = pattern
                .select(outputTag, new PatternTimeoutFunction<OrderEven, String>() {
                    @Override
                    public String timeout(Map<String, List<OrderEven>> pattern, long timeoutTimestamp) throws Exception {
                        return pattern.toString() + "<===========>" + timeoutTimestamp;
                    }
                }, new PatternSelectFunction<OrderEven, String>() {
                    @Override
                    public String select(Map<String, List<OrderEven>> pattern) throws Exception {
                        return pattern.toString();
                    }
                });

        //主流与超时流输出
        select.print();
        select.getSideOutput(outputTag).print();

        env.execute();
    }
}

3.3 高效匹配支付超时统计

/**
 *当支付未能在15分钟时输出;
 */
public class Task3_PayCount {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration());
        DataStreamSource<String> soc = env.readTextFile("data/OrderLog.csv", "utf-8");

        KeyedStream<LoginLOJO, Long> KS = soc.flatMap((String line, Collector<LoginLOJO> out) -> {
            String[] words = line.split(",");
            if (words.length == 4) {
                out.collect(new LoginLOJO(Long.valueOf(words[0]), words[1], words[2], Long.valueOf(words[3])));
            }
        })
                .returns(new TypeHint<LoginLOJO>() {
                })
                .assignTimestampsAndWatermarks(WatermarkStrategy.<LoginLOJO>forBoundedOutOfOrderness(Duration.ofSeconds(10))
                        .withTimestampAssigner((login, ts) -> login.getTimeStamp() * 1000L))
                .keyBy(LoginLOJO::getUserId);


        Pattern<LoginLOJO, LoginLOJO> pat = Pattern
                .<LoginLOJO>begin("第一次异常")
                .where(new SimpleCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value) throws Exception {
                        return value.getLoginState().equals("fail");
                    }
                })
                .next("第二次登陆失败")
                .where(new SimpleCondition<LoginLOJO>() {
                    @Override
                    public boolean filter(LoginLOJO value) throws Exception {
                        return value.getLoginState().equals("fail");
                    }
                })
                .within(Time.seconds(2L));

        PatternStream<LoginLOJO> pattern = CEP.pattern(KS, pat);

        pattern
                .select(new PatternSelectFunction<LoginLOJO, String>() {
                    @Override
                    public String select(Map<String, List<LoginLOJO>> pattern) throws Exception {
                        return pattern.toString();
                    }
                })
                .print();

        env.execute();
    }
}

4.0 FlinkSQL

从1.9开始Blink开源合入后迭代快速;

流的世界观:有界流和无界流

流-》动态表-》查询表-》动态表到流

![img](E:\BIG DATA\知识总结\flink\pic\Flinksql)

基本DSL语法使用

//todo Table API 基本使用
        //1.创建执行环境
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
        //2.流转换为表
//        tableEnv.fromDataStream(soc,"ts,vc,id");老版本写法
        Table sensorTable = tableEnv.fromDataStream(soc, $("id"), $("ts"), $("vc"));
        //使用DSL操作数据
        Table resultTable = sensorTable
//                .where("id ='sensor1'")
//                .select("id,vc,ts as tim");
                .where($("id").isEqual("sensor1"))
                .select($("id"), $("vc"), $("ts").as("water"));

        DataStream<Row> tupleDataStream = tableEnv.toAppendStream(resultTable, Row.class);//1.12版本-----追加的写法
//        DataStream<Tuple3<String, Integer, Long>> tupleDataStream = tableEnv.toAppendStream(resultTable, Types.TUPLE(Types.STRING, Types.INT, Types.LONG));
//        DataStream<Tuple3<String,Integer,Long>> tupleDataStream = tableEnv.toAppendStream(resultTable, TypeInformation.of(new TypeHint<Tuple3<String,Integer,Long>>() {}));

聚合使用

        //使用DSL操作数据
        Table resultTable = sensorTable
                .groupBy($("id"))
//                .aggregate($("id").count().as("xxxx"))  可以替代下面的聚合操作
                .select($("id"),$("id").count().as("sensor_count"));

  DataStream<Tuple2<Boolean, Row>> tupleDataStream = tableEnv.toRetractStream(resultTable, Row.class);//1.12版本
/*
(true,sensor1,1)
(false,sensor1,1)
(true,sensor1,2)
(true,sensor2,1)
(false,sensor2,1)// 标记为删除的数据
(true,sensor2,2)
(false,sensor1,2)
(true,sensor1,3)
(true,sensor3,1)
(false,sensor1,3)
(true,sensor1,4)
(true,sensor5,1)
(true,sensor4,1)
(false,sensor1,4)
(true,sensor1,5)
 */

DSL连接器

//1.创建执行环境
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        Table sensorTable = tableEnv.fromDataStream(soc, $("id"), $("ts"), $("vc"));
        //使用DSL操作数据
        Table resultTable = sensorTable
                .select($("id"), $("ts"), $("vc") );


        //连接外部系统抽象为一张表
        tableEnv.connect(new FileSystem().path("out/flink.csv"))
                .withFormat(new Csv().fieldDelimiter('|'))
                .withSchema(new Schema()
                        .field("fs_id", DataTypes.STRING())
                        .field("fs_ts", DataTypes.BIGINT())
                        .field("fs_vc", DataTypes.INT())
                )
                .createTemporaryTable("fsTable");

        //执行数据插入表格--不转换成流了
        resultTable.executeInsert("fsTable");
/*
Connect步骤:
1. connect(描述器)
2. withformat 指定存储格式 不同格式需要导依赖;
3. withSchema 指定 表的结构信息: 字段名、字段类型
4. createTemporaryTable 指定一个表名
只能插入数据不能更新数据!!!! 行分隔符不能瞎指定
 */

SQL 查询

        //todo Table API 基本使用
        //1.创建执行环境
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
        //2.流转换为表
//        tableEnv.fromDataStream(soc,"ts,vc,id");老版本写法
        Table sensorTable = tableEnv.fromDataStream(soc, $("id"), $("ts"), $("vc"));
        //创建映射
        tableEnv.createTemporaryView("tmp",sensorTable);
        //或者直接把数据源转换为临时映射,但是表格需要获取
        tableEnv.createTemporaryView("tmp2",soc);
        Table tmp2 = tableEnv.from("tmp2");


        //直接查询
        Table table = tableEnv//.sqlQuery("select * from tmp where id = 'sensor1' and vc >15");
                  .sqlQuery("select id,count(id) from tmp group by id"); // 分组查询
//                .sqlQuery("select * from sensor right join sensor1 on sensor.id=sensor1.id"); // 关联查询
//                .sqlQuery("select * from sensor where id not in (select id from sensor1)"); // 关联查询
tableEnv.toRetractStream(table, Row.class).print();//表示更新

SQL 连接器---推荐

        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        //创建临时表
        tableEnv.createTemporaryView("sensor",soc);

        //创建表格---参考官网

        TableResult tableResult = tableEnv.executeSql("create table mySensor "
                + "(user_id string,ts bigint,vc int) with ('connector' = 'filesystem',           -- required: specify the connector\n" +
                "  'path' = 'out/flink2.csv',  -- required: path to a directory\n" +
                "  'format' = 'csv')");
        //从临时表查询写入
        tableEnv.executeSql("insert into mySensor select * from sensor");

集成Hive:

创建 HiveCatLog->注册CataLog->指定CataLog->指定SQL方言->使用SQL操作表格

放置jar包;冲突很多;使用 use catalog myhive;切换catalog

1)配置 sql客户端配置文件sql-client-defaults.yaml

catalogs:

- name: myhive #指定catalog名

type: hive #指定类型

hive-conf-dir: /opt/module/hive/conf #指定hive配置文件目录

default-database: flinktest #指定默认的数据库(hive)

2)将flink-sql-connector-hive-3.1.2_2.11-1.12.0.jar、flink-shaded-hadoop-2-uber-3.1.3-9.0.jar、guava-27-jre.jar导入 ${flink_home}/lib下

3)启动kafka、flink、hive的metastore服务

4)启动flink 的sql 客户端

5)操作hive表

CREATE TABLE topic_products (
  id BIGINT,
  name STRING,
  description STRING,
  weight DECIMAL(10, 2)
) WITH (
 'connector' = 'kafka',
 'topic' = 'products_binlog',
 'properties.bootstrap.servers' = 'localhost:9092',
 'properties.group.id' = 'testGroup',
 'format' = 'canal-json'  -- using canal-json as the format
)

任务:TopN实现:sql和DSL实现---在官网Sql的Quries内查询

 //创建表格执行环境
        EnvironmentSettings settings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env,settings);
        //流转表
        Table behavior = tableEnv.fromDataStream(data, $("userId"), $("itemId"), $("categoryId"), $("behavior"), $("timestamp").rowtime());//标记时间格子

        Table select = behavior.where($("behavior").isEqual("pv"))
                .window(Slide.over(lit(1).hours()).every(lit(5).minutes()).on($("timestamp")).as("w"))//时间窗口
                .groupBy($("itemId"), $("w"))//此时进行group by必须要把窗口放入内容
                .select($("itemId"), $("itemId").count().cast(DataTypes.BIGINT()).as("itemCount"), $("w").end().as("windowsEnd"));//第一次聚合完成,把时间窗口转化为一个属性方便二次开窗

        tableEnv.createTemporaryView("tmpRe",select);
/*        select.groupBy($("windowsEnd"))
                .select($("itemId"),$("itemCount"))
                .window(Over.partitionBy($("itemId")).orderBy($("itemCount").desc()).as("w1"))//普通窗口:后面只能接select
                .select($("itemId"),$("").over($("w1")))*/
        //使用sql实现rk排序与取出topN;所有的关键词间保证空格的存在
        Table result = tableEnv.sqlQuery("select * " +
                "from(select *,row_number() over(partition by windowsEnd order by itemCount desc) as rk " +
                "from tmpRe) t1 " +
                "where rk <=3");

        tableEnv.toRetractStream(result, Row.class).print();

使用Flink的Bloom过滤器来进行uv过滤存储

/**
 * 统计没小时的uvcount
 * 此练习的主要目的为练习布隆过滤器的使用
 */
public class Practice2_UVCountByBloom {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //获取数据
        env.readTextFile("data/UserBehavior.csv", "utf-8")
                .flatMap((String line, Collector<BehaviorBean> out)->{
                    String[] words = line.split(",");
                    if (words.length == 5&& words[3].equals("pv")) {
                        out.collect(new BehaviorBean(Long.valueOf(words[0]), Long.valueOf(words[1]), Integer.valueOf(words[2]), words[3], Long.valueOf(words[4])));
                    }
                })
                .returns(new TypeHint<BehaviorBean>() {})
                .assignTimestampsAndWatermarks(WatermarkStrategy.<BehaviorBean>forBoundedOutOfOrderness(Duration.ofSeconds(10)).withTimestampAssigner((be,ts)->be.getTimestamp()*1000L))
               .keyBy(BehaviorBean::getBehavior)
                .window(TumblingEventTimeWindows.of(Time.hours(1)))
                .aggregate(new UvCountByBloom(), new ProcessWindowFunction<Long, String, String, TimeWindow>() {
                    @Override
                    public void process(String s, Context context, Iterable<Long> elements, Collector<String> out) throws Exception {
                        out.collect("uv值=" + elements.iterator().next() + ",窗口为[" + context.window().getStart() + "," +  context.window().getEnd() + ")");
                    }
                }).print();//使用Aggregate增量聚合中进行筛选

                env.execute();
    }


    //使用flink自带的布隆过滤器;
    public static class UvCountByBloom implements AggregateFunction<BehaviorBean, Tuple2<BloomFilter<Long>, Long>, Long> {

        @Override
        public Tuple2<BloomFilter<Long>, Long> createAccumulator() {
            // 第一个参数,指定数据类型; 第二个参数,指定预估的数据量; 第三个参数,默认值0.03,表示错误率
            BloomFilter<Long> longBloomFilter = BloomFilter.create(Funnels.longFunnel(), 10000000, 0.01D);
            return Tuple2.of(longBloomFilter,0L);
        }

        @Override
        public Tuple2<BloomFilter<Long>, Long> add(BehaviorBean value, Tuple2<BloomFilter<Long>, Long> accumulator) {
            Long userId=value.getUserId();
            BloomFilter<Long> bloom = accumulator.f0;
            // 通过 bloom判断是否存在,如果不存在 => count值 + 1, 把对应的格子置为 1
            Long count = accumulator.f1;
            if(!bloom.mightContain(userId)){
                count++;
                bloom.put(userId);
            }
            return Tuple2.of(bloom,count);
        }

        @Override
        public Long getResult(Tuple2<BloomFilter<Long>, Long> accumulator) {
            return accumulator.f1;
        }

        @Override
        public Tuple2<BloomFilter<Long>, Long> merge(Tuple2<BloomFilter<Long>, Long> a, Tuple2<BloomFilter<Long>, Long> b) {
            return null;
        }
    }

}

使用Redis的bitMap自定义bloom过滤器

**
 * 统计没小时的uvcount
 * 此练习的主要目的为练习布隆过滤器的使用
 */
public class Practice3_UVCountByBloomByRedis {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //获取数据
        env.readTextFile("data/UserBehavior.csv", "utf-8")
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy
                                .<String>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                                .withTimestampAssigner(new SerializableTimestampAssigner<String>() {
                                    @Override
                                    public long extractTimestamp(String element, long recordTimestamp) {
                                        String[] datas = element.split(",");
                                        return Long.valueOf(datas[4]) * 1000L;
                                    }
                                })
                )
                .flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
                    @Override
                    public void flatMap(String value, Collector<Tuple2<String, Long>> out) throws Exception {
                        String[] split = value.split(",");
                        BehaviorBean behavior = new BehaviorBean(Long.valueOf(split[0]), Long.valueOf(split[1]), Integer.valueOf(split[2]), split[3], Long.valueOf(split[4]));
                        if (behavior.getBehavior().equals("pv")) {
                            out.collect(Tuple2.of("uv", behavior.getUserId()));
                        }

                    }
                })
                .keyBy(t -> t.f0)
                .window(TumblingEventTimeWindows.of(Time.hours(1)))
                //trigger用于控制窗口函数的执行
                .trigger(
                        new Trigger<Tuple2<String, Long>, TimeWindow>() {
                            // 每来一条数据,执行一次这个方法
                            @Override
                            public TriggerResult onElement(Tuple2<String, Long> element, long timestamp, TimeWindow window, TriggerContext ctx) throws Exception {
                                // 每来一条数据,就触发,并且清除,不存数据
                                return TriggerResult.FIRE_AND_PURGE;
                            }

                            // 由处理时间,触发这个方法执行
                            @Override
                            public TriggerResult onProcessingTime(long time, TimeWindow window, TriggerContext ctx) throws Exception {
                                return TriggerResult.CONTINUE;
                            }

                            // 由事件时间,触发这个方法执行
                            @Override
                            public TriggerResult onEventTime(long time, TimeWindow window, TriggerContext ctx) throws Exception {
                                return TriggerResult.CONTINUE;
                            }

                            // 清空
                            @Override
                            public void clear(TimeWindow window, Trigger.TriggerContext ctx) throws Exception {

                            }
                        }
                )
                .process(
                        new ProcessWindowFunction<Tuple2<String, Long>, String, String, TimeWindow>() {
                            Jedis jedis;

                            @Override
                            public void open(Configuration parameters) throws Exception {
                                jedis = new Jedis("hadoop102", 6379);
                            }

                            @Override
                            public void process(String s, Context context, Iterable<Tuple2<String, Long>> elements, Collector<String> out) throws Exception {
                                // 结合 redis的 bitmap结构,实现布隆过滤    bitmap:一个bit位来表示某个元素对应的值或者状态,每一位的值为0/1  false/true
                                // 位图 : key = windowEnd, value=bitmap
                                // count值:key = 'uvCount',value=hash (hashkey=windowend,hashvalue=count)

                                // 1. 来一条数据,查redis,看是否存在
                                String userIdStr = String.valueOf(elements.iterator().next().f1);
                                String windowEndStr = String.valueOf(context.window().getEnd());
                                MyBloomHash myBloomHash = new MyBloomHash(61);
                                long offset = myBloomHash.getOffset(userIdStr, 2 << 31);
                                Boolean isExist = jedis.getbit(windowEndStr, offset);
                                if (!isExist) {
                                    // 不存在, count值 + 1, bitmap对应的格子置1
                                    String uvCount = jedis.hget("uvCount", windowEndStr);
                                    uvCount = String.valueOf(Long.valueOf(uvCount) + 1);
                                    // 置1
                                    jedis.setbit(windowEndStr, offset, true);
                                    jedis.hset("uvCount", windowEndStr, uvCount);
                                }
                            }
                        }
                ).print();

                env.execute();
    }

    public static class MyBloomHash {

        // 随机数种子,如果为 质数,碰撞率更低
        private int seed;

        public MyBloomHash(int seed) {
            this.seed = seed;
        }

        public long getOffset(String input, long bitmapSize) {
            int hash = 0;
            // 计算hash值
            for (char c : input.toCharArray()) {
                hash = hash * seed + c;
            }
            // 参考hashmap实现,当 为 2的n次方时,可以替换为 位运算,效率更高
            return hash & (bitmapSize - 1);
        }
    }
}

bloom的几个公式

m:位图长度 p:误判率 n:数据个数 k:哈希函数的个数

资源配置

JM 4-8G

TM 6-8G

-S 1slot 资源小的情况下 1core 1槽

监控:flink-metrics +普罗米修斯(监控框架)+Grafana (监控界面化)+webUI

背压实现

采样线程

背压监测通过反复获取正在运行的任务的堆栈跟踪的样本来工作,JobManager 对作业重复调用 Thread.getStackTrace()

img转存失败,建议直接上传图片文件

Sample

如果采样(samples)显示任务线程卡在某个内部方法调用中,则表示该任务存在背压。

默认情况下,JobManager 每50ms为每个任务触发100个堆栈跟踪,来确定背压。在Web界面中看到的比率表示在内部方法调用中有多少堆栈跟踪被阻塞,例如,0.01表示该方法中只有1个被卡住。状态和比率的对照如下: OK:0 <= Ratio <= 0.10 LOW:0.10 <Ratio <= 0.5 HIGH:0.5 <Ratio <= 1

为了不使堆栈跟踪样本对 TaskManager 负载过高,每60秒会刷新采样数据。

配置

可以使用以下配置 JobManager 的采样数:

  • web.backpressure.refresh-interval,统计数据被废弃重新刷新的时间(默认值:60000,1分钟)。
  • web.backpressure.num-samples,用于确定背压的堆栈跟踪样本数(默认值:100)。
  • web.backpressure.delay-between-samples,堆栈跟踪样本之间的延迟以确定背压(默认值:50,50ms)。