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

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


状态后端 stateBackend
-------StateBackend 所有状态后端的父类:三个子类
- memoryStateBackEnd:
- FsStateBackend:
- RockDBBackEnd:本地需要序列化存储到本地的RockDB数据库;远程存储在文件系统
主要关注:
- 本地状态的管理
- 完成 checkpoint的远程存储
1.5 CheckPoint
Flink检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确。记住这一基本点之后,Flink为用户提供了用来定义状态的工具。

JM的 Checkpoint Coordinator(协调器) 向所有 source 节点 trigger Checkpoint;
source 节点定期向下游广播 barrier,这个 barrier 就是实现 Chandy-Lamport 分布式快照算法的核心,下游的 task 只有收到所有 barrier(对齐式的) 的 barrier 才会执行相应的state状态存储于持久化(checkpoint);
----------当 task 完成 state 备份后,会将备份数据的地址(state handle)通知给 Checkpoint coordinator。
----------下游的 sink 节点收集齐上游两个 input 的 barrier 之后,会执行checkpoint照,完成后返回(state handle)通知给 Checkpoint coordinator。
所有节点的state本地存储完成后,认为完成了此checkpoint; Checkpoint Coordinator 将data meta持久化到远程存储,通知所有节点此次checkpoint完成;
当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完成到通知保存时挂了重启重新提交
背压

当后面阻塞导致算子输出缓冲区阻塞------导致输入阻塞----最后一直到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开源合入后迭代快速;
流的世界观:有界流和无界流
流-》动态表-》查询表-》动态表到流

基本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()。
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)。