Flink-DataStream快速上手

文章目录

1. 安装部署

安装

  • 第一步:将 flink-1.10.1-bin-scala_2.12.tgz 上传到服务器中并解压缩

  • 第二步:修改 conf/flink-conf.yaml 文件

    # 修改 jobmanager.rpc.address 参数,修改为 jobmanager 的机器
    jobmanager.rpc.address: hadoop151
    
  • 第三步:修改 conf/slaves 文件

    # slave 机器
    hadoop152
    hadoop153
    
  • 第四步:将 flink 整个目录分发到其他机器上

2. 执行任务

Standalone 模式

启动/停止

  • 命令

    # 启动
    bin/start-cluster.sh
    
    # 停止
    bin/stop-cluster.sh
    
  • 访问 web 页面

    • http://hadoop151:8081

执行任务

# =================== 启动任务 ===================
bin/flink run -c 全限定类名 –p 分区个数 jar包
# 示例
bin/flink run -c com.itfzk.flink.wordcount.KafkaStreamWordCount -p 3 FlinkStudyDemo-1.0-SNAPSHOT-jar-with-dependencies.jar


# =================== 停止任务 ===================
bin/flink cancel JobId
# 示例
bin/flink cancel f69fbd0650ae4202b2a46b3ad2089606

Yarn 模式

Session-cluster 模式

启动 yarn-session
  • 命令

    # =================== 启动 yarn-session ===================
    # -n(--container):TaskManager 的数量
    # -s(--slots): 每个 TaskManager 的 slot 数量,默认一个 slot 一个 core,默认每个 taskmanager 的 slot 的个数为 1,有时可以多一些 taskmanager,做冗余
    # -jm:JobManager 的内存(单位 MB)
    # -tm:每个 taskmanager 的内存(单位 MB)
    # -nm:yarn 的 appName(现在 yarn 的 ui 上的名字)
    # -d:后台执行
    bin/yarn-session.sh -n 2 -s 2 -jm 1024 -tm 1024 -nm test -d
    
    
    # =================== 停止 yarn-session ===================
    yarn application -kill Application-Id
    # 示例
    yarn application -kill application_1633171918776_0003
    
  • 访问 web 页面

    • 启动 yarn-session 后会出现 web 地址,例如:http://hadoop153:42189

执行任务
# =================== 启动任务 ===================
bin/flink run -c 全限定类名 –p 分区个数 jar包
# 示例
bin/flink run -c com.itfzk.flink.wordcount.KafkaStreamWordCount -p 3 FlinkStudyDemo-1.0-SNAPSHOT-jar-with-dependencies.jar


# =================== 停止任务 ===================
bin/flink cancel JobId
# 示例
bin/flink cancel f69fbd0650ae4202b2a46b3ad2089606

Per-Job-Cluster 模式

# =================== 启动任务 ===================
bin/flink run –m yarn-cluster -c 全限定类名 –p 分区个数 jar包
# 示例
bin/flink run –m yarn-cluster -c com.itfzk.flink.wordcount.KafkaStreamWordCount -p 3 FlinkStudyDemo-1.0-SNAPSHOT-jar-with-dependencies.jar


# =================== 停止任务 ===================
bin/flink cancel JobId
# 示例
bin/flink cancel f69fbd0650ae4202b2a46b3ad2089606

3. 执行环境

Environment

getExecutionEnvironment(常用)

  • 创建一个执行环境,表示当前执行程序的上下文。 getExecutionEnvironment 会根据查询运行的方式决定返回什么样的运行环境,是最常用的一种创建执行环境的方式

    // 普通运行环境
    ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
    
    // 流式运行环境(常用)
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    

createLocalEnvironment

  • 返回本地执行环境,需要在调用时指定默认的并行度

    LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(1);
    

createRemoteEnvironment

  • 返回集群执行环境,将 Jar 提交到远程服务器。需要在调用时指定 JobManager的 IP 和端口号,并指定要在集群中运行的 Jar 包

    StreamExecutionEnvironment env = StreamExecutionEnvironment.createRemoteEnvironment("jobmanage-hostname", 6123, "YOURPATH//WordCount.jar");
    

Source、Sink

Transform(算子)

map

DataStream<Integer> mapStram = dataStream.map(new MapFunction<String, Integer>() {
    public Integer map(String value) throws Exception {
    }
});

flatMap

DataStream<String> flatMapStream = dataStream.flatMap(new FlatMapFunction<String, String>() {
    public void flatMap(String value, Collector<String> out) throws Exception {
    }
});

Filter

DataStream<Interger> filterStream = dataStream.filter(new FilterFunction<String>() {
    public boolean filter(String value) throws Exception {
    }
});

KeyBy

  • DataStreamKeyedStream
  • 将一个流拆分成不相交的分区,每个分区包含具有相同 key 的元素,在内部以 hash 的形式实现的

滚动聚合算子

  • sum()
  • max()
  • min()
  • maxBy()
  • minBy()

Reduce

  • KeyedStreamDataStream
  • 一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果

Split 和 Select

Split

  • DataStream SplitStream
    • 根据某些特征把一个 DataStream 拆分成两个或者多个 DataStream

Select

  • SplitStreamDataStream
    • 从一个 SplitStream 中获取一个或者多个DataStream
public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    String filePath = "E:\\~fzk\\java\\IDEA\\bigdata\\FlinkStudyDemo\\test\\test1";
    DataStream<String> inputDataStream = env.readTextFile(filePath);
    
    DataStream<SensorsData> map = inputDataStream.map(new MapFunction<String, SensorsData>() {
        public SensorsData map(String value) throws Exception {
            String[] splits = value.split(" ");
            return new SensorsData(splits[0], new Long(splits[1]), new Double(splits[2]));
        }
    });
    
    KeyedStream<SensorsData, Tuple> keyedStream = map.keyBy("id");

    // split:分流
    SplitStream<SensorsData> splitStream = keyedStream.split(new OutputSelector<SensorsData>() {
        public Iterable<String> select(SensorsData value) {
            return value.getWendu() > 37 ? Collections.singletonList("h") : Collections.singletonList("d");
        }
    });

    // select:选择一个或多个 DataStream
    DataStream<SensorsData> resultDataStream = splitStream.select("d");

    env.execute();
}

Connect 和 CoMap

Connect

  • DataStream,DataStreamConnectedStreams:连接两个保持他们类型的数据流,两个数据流被 Connect 之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立

CoMap

  • ConnectedStreamsDataStream:作用于 ConnectedStreams 上,功能与 map 和 flatMap 一样,对 ConnectedStreams 中的每一个 Stream 分别进行 map 和 flatMap处理
public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    String filePath = "E:\\~fzk\\java\\IDEA\\bigdata\\FlinkStudyDemo\\test\\test1";
    DataStream<String> inputDataStream = env.readTextFile(filePath);

    DataStream<SensorsData> map = inputDataStream.map(new MapFunction<String, SensorsData>() {
        public SensorsData map(String value) throws Exception {
            String[] splits = value.split(" ");
            return new SensorsData(splits[0], new Long(splits[1]), new Double(splits[2]));
        }
    });

    KeyedStream<SensorsData, Tuple> keyedStream = map.keyBy("id");

    SplitStream<SensorsData> splitStream = keyedStream.split(new OutputSelector<SensorsData>() {
        public Iterable<String> select(SensorsData value) {
            return value.getWendu() > 37 ? Collections.singletonList("high") : Collections.singletonList("low");
        }
    });

    DataStream<SensorsData> highDataStream = splitStream.select("high");
    DataStream<SensorsData> lowDataStream = splitStream.select("low");

    // connect & CoMapFunction:合流
    ConnectedStreams<SensorsData, SensorsData> connectedStreams = highDataStream.connect(lowDataStream);
    /*
    	new CoMapFunction<SensorsData, SensorsData, Object>
        第一个参数:合流的第一个数据类型
        第二个参数:合流的第二个数据类型
        第三个参数:合流的返回类型
     */
    DataStream<Object> resultDataStream = connectedStreams.map(new CoMapFunction<SensorsData, SensorsData, Object>() {
        public Object map1(SensorsData value) throws Exception {
            return value;
        }

        public Object map2(SensorsData value) throws Exception {
            return value;
        }
    });

    env.execute();
}

Union

  • DataStreamDataStream:对两个或者两个以上的 DataStream 进行 union 操作,产生一个包含所有 DataStream 元素的新 DataStream
  • 连接流的类型一样

广播(broadcast)

  • DataStreamDataStream:向每个分区广播元素

    dataStream.broadcast();
    

join

窗口连接
stream.join(otherStream)
    .where(<KeySelector>)
    .equalTo(<KeySelector>)
    .window(<WindowAssigner>)
    .apply(<JoinFunction>)
间隔加入
orangeStream
    .keyBy(<KeySelector>)
    .intervalJoin(greenStream.keyBy(<KeySelector>))
    .between(Time.milliseconds(-2), Time.milliseconds(1))
    .process (new ProcessJoinFunction<Integer, Integer, String(){
        @Override
        public void processElement(Integer left, Integer right, Context ctx, Collector<String> out) {
            out.collect(first + "," + second);
        }
    });

4. 时间语义与 Watermark

时间语义

  • Event Time:是事件创建的时间(默认时间语义)
  • Ingestion Time:是数据进入 Flink 的时间
  • Processing Time:是每一个执行基于时间操作的算子的本地系统时间

Watermark

  • Watermark 是一种衡量 Event Time 进展的机制
  • Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用 Watermark 机制结合 window 来实现
  • Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时 时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime 小于 maxEventTime - t 的所有数据都已经到达,如果有窗口的停止时间等于 maxEventTime – t,那么这个窗口被触发执行
  • ⚠️注意注意注意:在自定义数据源中发送了水位线以后,就不能在程序中使用 assignTimestampsAndWatermarks 方法 来 生 成 水 位 线 了 。 在 自 定 义 数 据 源 中 生 成 水 位 线 和 在 程 序 中 使 用assignTimestampsAndWatermarks 方法生成水位线二者只能取其一

老版本被弃用的使用方式(不推荐使用)

乱序时间的watermark实现方式

  • 接口:AssignerWithPeriodicWatermarks

  • 使用前设置时间语义

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    
        //设置时间语义
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        //周期性的生成 watermar,默认周期是 200 毫秒
        env.getConfig().setAutoWatermarkInterval(5000);
    
        String filePath = "E:\\~fzk\\java\\IDEA\\bigdata\\FlinkStudyDemo\\test\\test1";
        DataStream<String> inputDataStream = env.readTextFile(filePath);
    
        DataStream<SensorsData> map = inputDataStream.map(new MapFunction<String, SensorsData>() {
            public SensorsData map(String value) throws Exception {
                String[] splits = value.split(" ");
                return new SensorsData(splits[0], new Long(splits[1]), new Double(splits[2]));
            }
        });
    
        //乱序时间情况下的 watermark
        //Time.milliseconds(1000) :延迟时间,1000ms
        DataStream<SensorsData> eventTimeDataStream = map.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<SensorsData>(Time.milliseconds(1000)) {
            @Override
            public long extractTimestamp(SensorsData element) {
                return element.getTimestamp();
            }
        });
    
        env.execute();
    }
    
    
    // 类
    public class SensorsData {
        private String id;
        private Long timestamp;
        private double wendu;
    }
    

顺序时间的watermark实现方式

  • 接口:AssignerWithPunctuatedWatermarks

  • 使用前设置时间语义

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    
        //设置时间语义
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
    
        String filePath = "E:\\~fzk\\java\\IDEA\\bigdata\\FlinkStudyDemo\\test\\test1";
        DataStream<String> inputDataStream = env.readTextFile(filePath);
    
        DataStream<SensorsData> map = inputDataStream.map(new MapFunction<String, SensorsData>() {
            public SensorsData map(String value) throws Exception {
                String[] splits = value.split(" ");
                return new SensorsData(splits[0], new Long(splits[1]), new Double(splits[2]));
            }
        });
    
        //顺序时间情况下的 watermark
        DataStream<SensorsData> eventTimeDataStream = map.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<SensorsData>() {
            @Override
            public long extractAscendingTimestamp(SensorsData element) {
                return element.getTimestamp();
            }
        });
    
        env.execute();
    }
    
    
    // 类
    public class SensorsData {
        private String id;
        private Long timestamp;
        private double wendu;
    }
    

新版本的使用方式(推荐使用)

分配数据时间戳和水位线需实现的方法说明(自定义方式)

  • assignTimestampsAndWatermarks【分配数据时间戳和水位线】
    • WatermarkStrategy【水位线策略,需实现这两个方法:createWatermarkGenerator,createTimestampAssigner】
      • createWatermarkGenerator【水位线生成器,主要负责按照既定的方式,基于时间戳生成水位线,有以下两个方式】
        • onEvent【没条数据都调用】
        • onPeriodicEmit【周期性调用】
      • createTimestampAssigner【分配时间戳,主要负责从流中数据元素的某个字段中提取时间戳,并分配给元素。时间戳的分配是生成水位线的基础】
// 分配数据时间戳和水位线(SensorsData:自定义的实体类)
SingleOutputStreamOperator<SensorsData> watermarksData = map.assignTimestampsAndWatermarks(
    // 水位线策略
    new WatermarkStrategy<SensorsData>() {
        // 水位线生成器,主要负责按照既定的方式,基于时间戳生成水位线
        @Override
        public WatermarkGenerator<SensorsData> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
            return new WatermarkGenerator<SensorsData>() {
                // 每个事件(数据)到来都会调用的方法
                // 它的参数有当前事件、时间戳,以及允许发出水位线的一个 WatermarkOutput
                // 调用这个方法可以实现更新水位线:watermarkOutput.emitWatermark(new Watermark(long timestamp))
                @Override
                public void onEvent(SensorsData sensorsData, long l, WatermarkOutput watermarkOutput) {
                    watermarkOutput.emitWatermark(new Watermark(long timestamp));
                }
                // 周期性调用的方法,可以由 WatermarkOutput 发出水位线。
                // 调用这个方法可以实现更新水位线:watermarkOutput.emitWatermark(new Watermark(long timestamp))
                // 周期时间为处理时间,可以调用环境配置的 env.getConfig().setAutoWatermarkInterval()方法来设置,默认为200ms
                @Override
                public void onPeriodicEmit(WatermarkOutput watermarkOutput) {
                    watermarkOutput.emitWatermark(new Watermark(long timestamp));
                }
            };
        }

        // 分配时间戳,主要负责从流中数据元素的某个字段中提取时间戳,并分配给元素。时间戳的分配是生成水位线的基础
        @Override
        public TimestampAssigner<SensorsData> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
            return new TimestampAssigner<SensorsData>() {
                @Override
                public long extractTimestamp(SensorsData sensorsData, long l) {
                    return 0;
                }
            };
        }
    }
);

乱序时间的watermark实现方式

WatermarkStrategy.forBoundedOutOfOrderness

// 乱序时间情况下的 watermark(WatermarkStrategy.forBoundedOutOfOrderness)
SingleOutputStreamOperator<SensorsData> watermarksData = map.assignTimestampsAndWatermarks(
    WatermarkStrategy.<SensorsData>forBoundedOutOfOrderness(Duration.ofSeconds(10)).withTimestampAssigner(
        new SerializableTimestampAssigner<SensorsData>() {
            @Override
            public long extractTimestamp(SensorsData sensorsData, long l) {
                return sensorsData.getTimestamp();
            }
        }
    )
);
  • forMonotonousTimestamps 方法内部实现的水位线策略如下:

    // forBoundedOutOfOrderness
    public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>, WatermarkGeneratorSupplier<T> {
        static <T> WatermarkStrategy<T> forBoundedOutOfOrderness(Duration maxOutOfOrderness) {
            return (ctx) -> {
                return new BoundedOutOfOrdernessWatermarks(maxOutOfOrderness);
            };
        }
    }
    
    // BoundedOutOfOrdernessWatermarks
    public class BoundedOutOfOrdernessWatermarks<T> implements WatermarkGenerator<T> {
        private long maxTimestamp;
        private final long outOfOrdernessMillis;
    
        public BoundedOutOfOrdernessWatermarks(Duration maxOutOfOrderness) {
            Preconditions.checkNotNull(maxOutOfOrderness, "maxOutOfOrderness");
            Preconditions.checkArgument(!maxOutOfOrderness.isNegative(), "maxOutOfOrderness cannot be negative");
            this.outOfOrdernessMillis = maxOutOfOrderness.toMillis();
            this.maxTimestamp = -9223372036854775808L + this.outOfOrdernessMillis + 1L;
        }
    
        public void onEvent(T event, long eventTimestamp, WatermarkOutput output) {
            this.maxTimestamp = Math.max(this.maxTimestamp, eventTimestamp);
        }
    
        public void onPeriodicEmit(WatermarkOutput output) {
            output.emitWatermark(new Watermark(this.maxTimestamp - this.outOfOrdernessMillis - 1L));
        }
    }
    

顺序时间的watermark实现方式

WatermarkStrategy.forMonotonousTimestamps

//顺序时间情况下的 watermark(WatermarkStrategy.forMonotonousTimestamps)
SingleOutputStreamOperator<SensorsData> watermarksData = map.assignTimestampsAndWatermarks(
    WatermarkStrategy.<SensorsData>forMonotonousTimestamps().withTimestampAssigner(
        new SerializableTimestampAssigner<SensorsData>() {
            // 分配时间戳,主要负责从流中数据元素的某个字段中提取时间戳,并分配给元素。时间戳的分配是生成水位线的基础
            @Override
            public long extractTimestamp(SensorsData sensorsData, long l) {
                return sensorsData.getTimestamp();
            }
        }
    )
);
  • forMonotonousTimestamps 方法内部实现的水位线策略如下:

    // forMonotonousTimestamps
    public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>, WatermarkGeneratorSupplier<T> {
        static <T> WatermarkStrategy<T> forMonotonousTimestamps() {
            return (ctx) -> {
                return new AscendingTimestampsWatermarks();
            };
        }
    }
    
    // AscendingTimestampsWatermarks
    public class AscendingTimestampsWatermarks<T> extends BoundedOutOfOrdernessWatermarks<T> {
        public AscendingTimestampsWatermarks() {
            super(Duration.ofMillis(0L));
        }
    }
    
    // BoundedOutOfOrdernessWatermarks
    public class BoundedOutOfOrdernessWatermarks<T> implements WatermarkGenerator<T> {
        private long maxTimestamp;
        private final long outOfOrdernessMillis;
    
        public BoundedOutOfOrdernessWatermarks(Duration maxOutOfOrderness) {
            Preconditions.checkNotNull(maxOutOfOrderness, "maxOutOfOrderness");
            Preconditions.checkArgument(!maxOutOfOrderness.isNegative(), "maxOutOfOrderness cannot be negative");
            this.outOfOrdernessMillis = maxOutOfOrderness.toMillis();
            this.maxTimestamp = -9223372036854775808L + this.outOfOrdernessMillis + 1L;
        }
    
        public void onEvent(T event, long eventTimestamp, WatermarkOutput output) {
            this.maxTimestamp = Math.max(this.maxTimestamp, eventTimestamp);
        }
    
        public void onPeriodicEmit(WatermarkOutput output) {
            output.emitWatermark(new Watermark(this.maxTimestamp - this.outOfOrdernessMillis - 1L));
        }
    }
    

在自定义数据源中发送水位线

collectWithTimestamp(String var1, long var2)【发送水位线】

⚠️注意注意注意:在自定义数据源中发送了水位线以后,就不能在程序中使用 assignTimestampsAndWatermarks 方法 来 生 成 水 位 线 了 。 在 自 定 义 数 据 源 中 生 成 水 位 线 和 在 程 序 中 使 用assignTimestampsAndWatermarks 方法生成水位线二者只能取其一

DataStreamSource<String> sourceData = env.addSource(new SourceFunction<String>() {
    private boolean flag = true;

    @Override
    public void run(SourceContext<String> sourceContext) throws Exception {
        while (flag) {
            // 发送水位线
            sourceContext.collectWithTimestamp(String var1, long var2);
        }
    }

    @Override
    public void cancel() {
        flag = false;
    }
});

5. Window

  • 按照驱动类型分类
    • 时间窗口(Time Window)
      • 时间窗口以时间点来定义窗口的开始(start)和结束(end),所以截取出的就是某一时间段的数据。到达结束时间时,窗口不再收集数据,触发计算输出结果,并将窗口关闭销毁
    • 计数窗口(Count Window)
      • 计数窗口基于元素的个数来截取数据,到达固定的个数时就触发计算并关闭窗口
  • 按照窗口分配数据的规则分类
    • 滚动窗口(Tumbling Windows)
      • 滚动窗口有固定的大小,是一种对数据进行“均匀切片”的划分方式。窗口之间没有重叠,也不会有间隔,是“首尾相接”的状态
    • 滑动窗口(Sliding Windows)
      • 与滚动窗口类似,滑动窗口的大小也是固定的。区别在于,窗口之间并不是首尾相接的,而是可以“错开”一定的位置
    • 会话窗口(Session Windows)
      • 会话窗口顾名思义,是基于“会话”(session)来来对数据进行分组的。这里的会话类似Web 应用中 session 的概念,不过并不表示两端的通讯过程,而是借用会话超时失效的机制来描述窗口
    • 全局窗口(Global Windows)
      • 还有一类比较通用的窗口,就是“全局窗口”。这种窗口全局有效,会把相同 key 的所有数据都分配到同一个窗口中;说直白一点,就跟没分窗口一样

窗口分配器

  • 定义窗口分配器(Window Assigners)是构建窗口算子的第一步,它的作用就是定义数据应该被“分配”到哪个窗口
  • 窗口分配器最通用的定义方式,就是调用.window()方法。这个方法需要传入一个WindowAssigner 作为参数,返回 WindowedStream。如果是非按键分区窗口,那么直接调用.windowAll()方法,同样传入一个 WindowAssigner,返回的是 AllWindowedStream

时间窗口

滚动处理时间窗口(TumblingProcessingTimeWindows)
// 分配滚动窗口时间为 10s 
// TumblingProcessingTimeWindows.of()
map.keyBy(...)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
    .reduce(...)
滑动处理时间窗口(SlidingProcessingTimeWindows)
// 分配滑动窗口的 窗口大小:10s,步长:2s
// SlidingProcessingTimeWindows.of()
map.keyBy(...)
    .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(2)))
    .reduce(...)
处理时间会话窗口(ProcessingTimeSessionWindows)
// 分配会话窗口时间:10s,10秒没数据说明一个会话结束
// ProcessingTimeSessionWindows.withGap()
map.keyBy(...)
    .window(ProcessingTimeSessionWindows.withGap(Time.seconds(4)))
    .reduce(...)
滚动事件时间窗口(TumblingEventTimeWindows)
// 使用前需要分配 事件的时间和水位线:assignTimestampsAndWatermarks
// 分配滚动窗口时间为 10s 
// TumblingEventTimeWindows.of()
map.keyBy(...)
    .window(TumblingEventTimeWindows.of(Time.seconds(10)))
    .reduce(...)
滑动事件时间窗口(SlidingEventTimeWindows)
// 使用前需要分配 事件的时间和水位线:assignTimestampsAndWatermarks
// 分配滑动窗口的 窗口大小:10s,步长:2s
// SlidingEventTimeWindows.of()
map.keyBy(...)
    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(2)))
    .reduce(...)
事件时间会话窗口
// 使用前需要分配 事件的时间和水位线:assignTimestampsAndWatermarks
// 分配会话窗口时间:10s,10秒没数据说明一个会话结束
// EventTimeSessionWindows.withGap()
map.keyBy(...)
    .window(EventTimeSessionWindows.withGap(Time.seconds(4)))
    .reduce(...)

计数窗口(countWindow)

滚动计数窗口(countWindow)
// 滚动计数窗口,countWindow 传入一个参数
// 长度为 10 的窗口
map.keyBy(...)
    .countWindow(5)
    .reduce(...)
    
// 滚动计数窗口,countWindow 传入两个参数
// 长度为 10,滑动步长为 3 的窗口
map.keyBy(...)
    .countWindow(5, 2)
    .reduce(...)

窗口函数(Window Functions)

  • 定义了窗口分配器,我们只是知道了数据属于哪个窗口,可以将数据收集起来了;至于收集起来到底要做什么,其实还完全没有头绪。所以在窗口分配器之后,必须再接上一个定义窗口如何进行计算的操作,这就是所谓的“窗口函数”(window functions)

增量聚合函数(incremental aggregation functions)

归约函数(ReduceFunction)
sourceData.keyBy(data -> data.f0)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
    .reduce(new ReduceFunction<Tuple2<String, Long>>() {
        @Override
        public Tuple2<String, Long> reduce(Tuple2<String, Long> data, Tuple2<String, Long> t1) throws Exception {
            return new Tuple2<>(data.f0, data.f1 + t1.f1);
        }
    }).print("out");
聚合函数(AggregateFunction)

AggregateFunction 接口说明

// AggregateFunction<IN, ACC, OUT> : 输入类型(IN)、累加器类型(ACC)和输出类型(OUT)
public interface AggregateFunction<IN, ACC, OUT> extends Function, Serializable {
    // 创建一个累加器,这就是为聚合创建了一个初始状态,每个聚合任务只会调用一次
    ACC createAccumulator();

    // 将输入的元素添加到累加器中。这就是基于聚合状态,对新来的数据进行进一步聚合的过程。方法传入两个参数:当前新到的数据 value,和当前的累加器accumulator;返回一个新的累加器值,也就是对聚合状态进行更新。每条数据到来之后都会调用这个方法
    ACC add(IN var1, ACC var2);

    // 从累加器中提取聚合的输出结果。也就是说,我们可以定义多个状态,然后再基于这些聚合的状态计算出一个结果进行输出。比如之前我们提到的计算平均值,就可以把 sum 和 count 作为状态放入累加器,而在调用这个方法时相除得到最终结果。这个方法只在窗口要输出结果时调用
    OUT getResult(ACC var1);

    // 合并两个累加器,并将合并后的状态作为一个累加器返回。这个方法只在需要合并窗口的场景下才会被调用;最常见的合并窗口(Merging Window)的场景就是会话窗口(Session Windows)
    ACC merge(ACC var1, ACC var2);
}

实例:

sourceData.keyBy(data -> true)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(4)))
    .aggregate(new AggregateFunction<Tuple2<String, Long>, Tuple2<Long, HashSet<String>>, Double>() {
        @Override
        public Tuple2<Long, HashSet<String>> createAccumulator() {
            return Tuple2.of(0L, new HashSet<>());
        }

        @Override
        public Tuple2<Long, HashSet<String>> add(Tuple2<String, Long> inData, Tuple2<Long, HashSet<String>> accData) {
            accData.f0 += inData.f1;
            accData.f1.add(inData.f0);
            return Tuple2.of(accData.f0, accData.f1);
        }

        @Override
        public Double getResult(Tuple2<Long, HashSet<String>> accData) {
            return (double) accData.f0 / accData.f1.size();
        }

        @Override
        public Tuple2<Long, HashSet<String>> merge(Tuple2<Long, HashSet<String>> longHashSetTuple2, Tuple2<Long, HashSet<String>> acc1) {
            mergeData1.f1.addAll(mergeData2.f1);
            return Tuple2.of(mergeData1.f0 + mergeData2.f0, mergeData1.f1);
        }
    }).print("out");

全窗口函数(full window functions)

处理窗口函数(ProcessWindowFunction)

  • ProcessWindowFunction 是 Window API 中最底层的通用窗口函数接口。之所以说它“最底层”,是因为除了可以拿到窗口中的所有数据之外,ProcessWindowFunction 还可以获取到一个“上下文对象”(Context)。这个上下文对象非常强大,不仅能够获取窗口信息,还可以访问当前的时间和状态信息。这里的时间就包括了处理时间(processing time)和事件时间水位线(event time watermark)
  • 缺点:牺牲性能和资源
  • 接口
    • public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window> extends AbstractRichFunction
sourceData.keyBy(data -> true)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(4)))
    .process(new ProcessWindowFunction<Tuple2<String, Long>, String, Boolean, TimeWindow>() {
        /**
                     * @param aBoolean  keyBy的分组值
                     * @param context   上下文信息
                     * @param iterable  窗口数据
                     * @param collector 返回数据
                     * @throws Exception
                     */
        @Override
        public void process(Boolean aBoolean, ProcessWindowFunction<Tuple2<String, Long>, String, Boolean, TimeWindow>.Context context, Iterable<Tuple2<String, Long>> iterable, Collector<String> collector) throws Exception {
            // TODO
        }
    });

6. 状态管理

键控状态(keyed state)

  • 键控状态是根据输入数据流中定义的键(key)来维护和访问的。Flink 为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个 key 对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的 key。因此,具有相同 key 的所有数据都会访问相同的状态。Keyed State 很类似于一个分布式的 key-value map 数据结构,只能用于 KeyedStream(keyBy 算子处理之后)
  • 存储一份状态值

Keyed State 支持数据类型

  • ValueState<T>保存单个的值,值的类型为 T
    • get 操作: ValueState.value()
    • set 操作: ValueState.update(T value)
  • ListState<T>保存一个列表,列表里的元素的数据类型为 T
    • ListState.add(T value)
    • ListState.addAll(List<T> values)
    • ListState.get()返回 Iterable<T>
    • ListState.update(List<T> values)
  • MapState<K, V>保存 Key-Value 对
    • MapState.get(UK key)
    • MapState.put(UK key, UV value)
    • MapState.contains(UK key)
    • MapState.remove(UK key)
  • ReducingState<T>保留一个值,该值表示添加到状态的所有值的聚合
    • 类似于值状态(Value),不过需要对添加进来的所有数据进行归约,将归约聚合之后的值作为状态保存下来。ReducintState<T>这个接口调用的方法类似于 ListState,只不过它保存的只是一个聚合值,所以调用.add()方法时,不是在状态列表里添加元素,而是直接把新数据和之前的状态进行归约,并用得到的结果更新状态
    • public ReducingStateDescriptor(String name, ReduceFunction<T> reduceFunction, Class<T> typeClass) {…}
    • ReducingState.add(T value)
    • ReducingState.get()

例子:ValueState
  • 我们可以利用 Keyed State,实现这样一个需求:检测传感器的温度值,如果连续的两个温度差值超过 10 度,就输出报警

    public class Test {
        public static void main(String[] args) throws Exception {
            //创建执行环节
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            env.setParallelism(1);
    
            DataStream<String> inputDataStream = env.socketTextStream("localhost", 9999);
    
            SingleOutputStreamOperator<MyBean> myBeanDataStream = inputDataStream.map(new MapFunction<String, MyBean>() {
                @Override
                public MyBean map(String s) throws Exception {
                    String[] split = s.split(" ");
                    return new MyBean(split[0], Double.valueOf(split[1]));
                }
            });
    
            SingleOutputStreamOperator<Tuple3<String, Double, Double>> resultDataStream = myBeanDataStream
                    .keyBy((KeySelector<MyBean, String>) data -> data.getId())
                    .flatMap(new MyRichFlatMapFunction(10.0));
    
            resultDataStream.print();
    
            env.execute();
        }
    }
    
    
    // 富方法:存储状态值
    public class MyRichFlatMapFunction extends RichFlatMapFunction<MyBean, Tuple3<String, Double, Double>> {
        private ValueState<Double> myValueState;
      
        private Double abs;
    
        public MyRichFlatMapFunction(Double abs) {
            this.abs = abs;
        }
    
        @Override
        public void open(Configuration parameters) throws Exception {
            // 创建状态值
            myValueState = getRuntimeContext().getState(new ValueStateDescriptor<Double>("my-flatmap", Double.class));
        }
    
        @Override
        public void flatMap(MyBean myBean, Collector<Tuple3<String, Double, Double>> collector) throws Exception {
            // 获取状态值
            Double lastWendu = myValueState.value();
            if(lastWendu != null){
                double absWebdu = Math.abs(myBean.getWendu() - lastWendu);
                if (absWebdu > abs){
                    collector.collect(new Tuple3<>(myBean.getId(), lastWendu, myBean.getWendu()));
                }
            }
            // 修改状态值
            myValueState.update(myBean.getWendu());
        }
    
        @Override
        public void close() throws Exception {
            // 清除状态值
            myValueState.clear();
        }
    }
    
    
    // 实体类
    public class MyBean {
        private String id;
        private Double wendu;
    }
    
状态创建的用法
private ValueState<Long> valueState;
private ListState<Long> listState;
private MapState<Long, Long> mapState;
private ReducingState<Long> reducingState;
private AggregatingState<Long, Long> aggregatingState;

@Override
public void open(Configuration parameters) throws Exception {
    valueState = getRuntimeContext().getState(
        new ValueStateDescriptor<Long>(
            "value-state",
            Long.class
        )
    );

    listState = getRuntimeContext().getListState(
        new ListStateDescriptor<Long>(
            "list-state",
            Long.class
        )
    );


    mapState = getRuntimeContext().getMapState(
        new MapStateDescriptor<Long, Long>(
            "map-state",
            Long.class,
            Long.class
        )
    );

    reducingState = getRuntimeContext().getReducingState(
        new ReducingStateDescriptor<Long>(
            "reduce-state",
            new ReduceFunction<Long>() {
                @Override
                public Long reduce(Long aLong, Long t1) throws Exception {
                    return aLong + t1;
                }
            },
            Long.class
        )
    );

    aggregatingState = getRuntimeContext().getAggregatingState(
        new AggregatingStateDescriptor<Long, Long, Long>(
            "agg-state",
            new AggregateFunction<Long, Long, Long>() {
                @Override
                public Long createAccumulator() {
                    return 0L;
                }

                @Override
                public Long add(Long aLong, Long aLong2) {
                    return aLong + aLong2;
                }

                @Override
                public Long getResult(Long aLong) {
                    return aLong;
                }

                @Override
                public Long merge(Long aLong, Long acc1) {
                    return null;
                }
            },
            Long.class
        )
    );
}

状态生存时间(TTL)

  • 在实际应用中,很多状态会随着时间的推移逐渐增长,如果不加以限制,最终就会导致存储空间的耗尽。

    • 一个优化的思路是直接在代码中调用.clear()方法去清除状态,但是有时候我们的逻辑要求不能直接清除。
    • 这时就需要配置一个状态的“生存时间”(time-to-live,TTL),当状态在内存中存在的时间超出这个值时,就将它清除
  • 说明:

    • 配置状态的 TTL 时,需要创建一个 StateTtlConfig 配置对象,然后调用状态描述器的.enableTimeToLive()方法启动 TTL 功能

    • TTL默认配置

      • StateTtlConfig.UpdateType:更新类型。更新类型指定了什么时候更新状态失效时间
        • OnCreateAndWrite:表示只有创建状态和更改状态(写操作)时更新失效时间(默认
        • OnReadAndWrite:表示无论读写操作都会更新失效时间
      • StateTtlConfig.StateVisibility:状态的可见性。所谓的“状态可见性”,是指因为清除操作并不是实时的,所以当状态过期之后还有可能基于存在,这时如果对它进行访问,能否正常读取到就是一个问题了
        • NeverReturnExpired:表示从不返回过期值,也就是只要过期就认为它已经被清除了,应用不能继续读取(默认
        • ReturnExpireDefNotCleanedUp:是如果过期状态还存在,就返回它的值
      • StateTtlConfig.TtlTimeCharacteristic:时间类型
        • ProcessingTime:处理时间,目前只支持这一种时间(默认
      • isCleanupInBackground:是否在后台进行清理
      • strategies:策略
      • ttl:失效时间
      public Builder(@Nonnull Time ttl) {
          this.updateType = StateTtlConfig.UpdateType.OnCreateAndWrite;
          this.stateVisibility = StateTtlConfig.StateVisibility.NeverReturnExpired;
          this.ttlTimeCharacteristic = StateTtlConfig.TtlTimeCharacteristic.ProcessingTime;
          this.isCleanupInBackground = true;
          this.strategies = new EnumMap(StateTtlConfig.CleanupStrategies.Strategies.class);
          this.ttl = ttl;
      }
      
  • 使用举例:

    public static class TtlStateProcess extends ProcessFunction<Tuple2<String, Long>, String> {
    
        private ValueState<String> valueState;
    
        @Override
        public void open(Configuration parameters) throws Exception {
            // TTL配置
            // 失效时间:1h
            // 更新类型:OnCreateAndWrite
            // 状态的可见性:NeverReturnExpired
            StateTtlConfig stateTtlConfig = StateTtlConfig
                    .newBuilder(Time.hours(1L))
                    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
                    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
                    .build();
    
            // 创建 状态描述器
            ValueStateDescriptor<String> valueStateDescriptor = new ValueStateDescriptor<String>(
                    "value-state",
                    String.class
            );
    
            // 将 TTL 配置加入到 状态描述器中
            valueStateDescriptor.enableTimeToLive(stateTtlConfig);
    
            // 添加状态到上下文中
            valueState = getRuntimeContext().getState(valueStateDescriptor);
        }
    
        @Override
        public void processElement(Tuple2<String, Long> data, ProcessFunction<Tuple2<String, Long>, String>.Context context, Collector<String> collector) throws Exception {
    
        }
    }
    

算子状态(Operator State)

算子状态(Operator State)就是一个算子并行实例上定义的状态,作用范围被限定为当前算子任务。算子状态跟数据的 key 无关,所以不同 key 的数据只要被分发到同一个并行子任务,就会访问到同一个Operator State

Operator State支持的数据类型

  • ListState<T>保存一个列表,列表里的元素的数据类型为 T
    • ListState.add(T value)
    • ListState.addAll(List<T> values)
    • ListState.get()返回 Iterable<T>
    • ListState.update(List<T> values)
  • UnionListState<T>,与 ListState 类似,联合列表状态也会将状态表示为一个列表。它与常规列表状态的区别在于,算子并行度进行缩放调整时对于状态的分配方式不同
    • UnionListState.add(T value)
    • UnionListState.addAll(List<T> values)
    • UnionListState.get()返回 Iterable<T>
    • UnionListState.update(List<T> values)
  • BroadcastState<K, V>保存 Key-Value 对
    • BroadcastState.get(UK key)
    • BroadcastState.put(UK key, UV value)
    • BroadcastState.contains(UK key)
    • BroadcastState.remove(UK key)

广播状态(Broadcast State)

  • 广播状态非常容易理解:状态广播出去,所有并行子任务的状态都是相同的;并行度调整时只要直接复制就可以了

    public class BroadcastStateProcessFunction {
        private static MapStateDescriptor<String, String> mapStateDescriptor = new MapStateDescriptor<>("rule-state", Types.STRING, Types.STRING);
    
        public SingleOutputStreamOperator<String> processFunction(
                SingleOutputStreamOperator<Tuple2<String, Long>> sourceData,
                SingleOutputStreamOperator<String> ruleData){
    
            // 1、添加广播( .connect(BroadcastStream<R> broadcastStream) )
            return sourceData
                    .connect(ruleData.broadcast(mapStateDescriptor))
                    .process(new BroadcastStateProcessFunction.BroadcastStateProcess());
        }
    
    
        // 2、继承 BroadcastProcessFunction<IN1, IN2, OUT> 接口,并重写 processBroadcastElement 方法
        private class BroadcastStateProcess extends BroadcastProcessFunction<Tuple2<String, Long>, String, String> {
    
            @Override
            public void processElement(Tuple2<String, Long> stringLongTuple2, BroadcastProcessFunction<Tuple2<String, Long>, String, String>.ReadOnlyContext readOnlyContext, Collector<String> collector) throws Exception {
                collector.collect(stringLongTuple2.f0);
            }
    
            // 3、实现 processBroadcastElement 方法,并使用
            @Override
            public void processBroadcastElement(String s, BroadcastProcessFunction<Tuple2<String, Long>, String, String>.Context context, Collector<String> collector) throws Exception {
                // 添加广播信息
                context.getBroadcastState(mapStateDescriptor).put("fzk", s);
                // 获取广播
                String data = context.getBroadcastState(mapStateDescriptor).get("fzk");
            }
        }
    }
    

7. ProcessFunction API

  • DataStream API 提供了一系列的 Low-Level 转换算子。可以访问时间戳watermark 以及注册定时事件。还可以输出特定的一些事件,例如超时事件等
  • Flink 提供了 8 个 Process Function
    • ProcessFunction
    • KeyedProcessFunction
    • CoProcessFunction
    • ProcessJoinFunction
    • BroadcastProcessFunction
    • KeyedBroadcastProcessFunction
    • ProcessWindowFunction
    • ProcessAllWindowFunction

KeyedProcessFunction

  • KeyedProcessFunction 用来操作 KeyedStream。KeyedProcessFunction 会处理流的每一个元素,输出为 0 个、1 个或者多个元素。所有的 Process Function 都继承自RichFunction 接口,所以都有 open()、close()和 getRuntimeContext()等方法。

  • KeyedProcessFunction<K, I, O>还额外提供了两个方法

    • processElement
      • 流中的每一个元素都会调用这个方法,调用结果将会放在 Collector 数据类型中输出。Context 可以访问元素的时间戳,元素的 key,以及 TimerService 时间服务。Context 还可以将结果输出到别的流(side outputs)
    • onTimer
      • 回调函数。当之前注册的定时器触发时调用。参数 timestamp 为定时器所设定的触发的时间戳。Collector 为输出结果的集合。OnTimerContext 和processElement 的 Context 参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)
    class MyKeyedProcessFunction extends KeyedProcessFunction<Tuple, MyBean, MyBean> {
        @Override
        public void open(Configuration parameters) throws Exception {
        }
    
        @Override
        public void processElement(MyBean myBean, KeyedProcessFunction<Tuple, MyBean, MyBean>.Context context, Collector<MyBean> collector) throws Exception {
            collector.collect(myBean);
        }
    
        @Override
        public void onTimer(long timestamp, KeyedProcessFunction<Tuple, MyBean, MyBean>.OnTimerContext ctx, Collector<MyBean> out) throws Exception {
        }
    
        @Override
        public void close() throws Exception {
        }
    }
    

TimerService 和 定时器(Timers)

  • Context 和 OnTimerContext 所持有的 TimerService 对象拥有以下方法

    • long currentProcessingTime() :返回当前处理时间
    • long currentWatermark() :返回当前 watermark 的时间戳
    • void registerProcessingTimeTimer(long timestamp) :会注册当前 key 的processing time 的定时器。当 processing time 到达定时时间时,触发 timer。
    • void registerEventTimeTimer(long timestamp) :会注册当前 key 的 event time 定时器。当水位线大于等于定时器注册的时间时,触发定时器执行回调函数。
    • void deleteProcessingTimeTimer(long timestamp) :删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行
    • void deleteEventTimeTimer(long timestamp) :删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行
    class MyKeyedProcessFunction extends KeyedProcessFunction<Tuple, MyBean, MyBean> {
        @Override
        public void processElement(MyBean myBean, KeyedProcessFunction<Tuple, MyBean, MyBean>.Context context, Collector<MyBean> collector) throws Exception {
            long currentProcessingTime = context.timerService().currentProcessingTime();
            long currentWatermark = context.timerService().currentWatermark();
            context.timerService().registerProcessingTimeTimer(10000l);
            context.timerService().registerEventTimeTimer(10000l);
            context.timerService().deleteProcessingTimeTimer(10000l);
            context.timerService().deleteEventTimeTimer(10000l);
        }
    }
    

侧输出流(SideOutput)

  • process function 的 side outputs 功能可以产生多条流,并且这些流的数据类型可以不一样。一个 side output 可以定义为 OutputTag[X]对象,X 是输出流的数据类型。process function 可以通过 Context 对象发射一个事件到一个或者多个 side outputs

  • 事例:监控传感器温度值,将温度值低于 30 度的数据输出到 side output

    public class Test {
        public static void main(String[] args) throws Exception {
            //创建执行环节
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    
            env.setParallelism(1);
    
            DataStream<String> inputDataStream = env.socketTextStream("localhost", 9999);
    
            SingleOutputStreamOperator<Tuple2<String, Double>> myBeanDataStream = inputDataStream.map(new MapFunction<String, Tuple2<String, Double>>() {
                @Override
                public Tuple2<String, Double> map(String s) throws Exception {
                    String[] split = s.split(" ");
                    return new Tuple2<String, Double>(split[0], Double.valueOf(split[1]));
                }
            });
    
            //定义侧输出流
            OutputTag<Tuple2<String, Double>> outputTag = new OutputTag<Tuple2<String, Double>>("high-output") {};
    
            // 使用自定义算子:ProcessFunction
            SingleOutputStreamOperator<Tuple2<String, Double>> resultDataStream = myBeanDataStream
                    .process(new MyProcessFunction(30.0, outputTag));
    
            resultDataStream.print("low-wendu");
    
            // 获取侧输出流并输出
            resultDataStream.getSideOutput(outputTag).print("high-wendu");
    
            env.execute();
        }
    
    
    
        private static class MyProcessFunction extends ProcessFunction<Tuple2<String, Double>, Tuple2<String, Double>> {
            private Double wenduLimit;
            private OutputTag<Tuple2<String, Double>> outputTag;
    
            // 初始化
            public MyProcessFunction(Double wenduLimit, OutputTag<Tuple2<String, Double>> outputTag) {
                this.wenduLimit = wenduLimit;
                this.outputTag = outputTag;
            }
    
            @Override
            public void processElement(Tuple2<String, Double> myBean, ProcessFunction<Tuple2<String, Double>, Tuple2<String, Double>>.Context context, Collector<Tuple2<String, Double>> collector) throws Exception {
                // 温度高于限制温度就将数据加入到侧输出流,否则正常输出
                if(myBean.f1 > wenduLimit){
                    context.output(outputTag, myBean);
                }else {
                    collector.collect(myBean);
                }
            }
        }
    }
    

8. 检查点(CheckPoint)

  • 在执行流应用程序期间,Flink 会定期保存状态的一致检查点
  • 如果发生故障, Flink 将会使用最近的检查点来一致恢复应用程序的状态,并重新启动处理流程

检查点配置说明

  • 检查点模式(CheckpointingMode)

    • 设置检查点一致性的保证级别,有“精确一次”(exactly-once)和“至少一次”(at-least-once)两个选项。
    • 默认级别为 exactly-once,而对于大多数低延迟的流处理程序,at-least-once 就够用了,而且处理效率会更高
  • 超时时间(checkpointTimeout)

    • 用于指定检查点保存的超时时间,超时没完成就会被丢弃掉。传入一个长整型毫秒数作为参数,表示超时时间
    • 默认:600000毫秒(10分钟)
  • 最小间隔时间(minPauseBetweenCheckpoints)

    • 用于指定在上一个检查点完成之后,检查点协调器(checkpoint coordinator)最快等多久可以出发保存下一个检查点的指令。这就意味着即使已经达到了周期触发的时间点,只要距离上一个检查点完成的间隔不够,就依然不能开启下一次检查点的保存。这就为正常处理数据留下了充足的间隙。当指定这个参数时,maxConcurrentCheckpoints 的值强制为 1
    • 默认:0
  • 最大并发检查点数量(maxConcurrentCheckpoints)

    • 用于指定运行中的检查点最多可以有多少个。由于每个任务的处理进度不同,完全可能出现后面的任务还没完成前一个检查点的保存、前面任务已经开始保存下一个检查点了。这个参数就是限制同时进行的最大数量。
    • 如果前面设置了 minPauseBetweenCheckpoints,则 maxConcurrentCheckpoints 这个参数就不起作用了
    • 默认:1
  • 开启外部持久化存储(enableExternalizedCheckpoints)

    • 用于开启检查点的外部持久化,而且默认在作业失败的时候不会自动清理,如果想释放空间需要自己手工清理。里面传入的参数 ExternalizedCheckpointCleanup 指定了当作业取消的时候外部的检查点该如何清理
      • DELETE_ON_CANCELLATION:在作业取消的时候会自动删除外部检查点,但是如果是作业失败退出,则会保留检查点
      • RETAIN_ON_CANCELLATION:作业取消的时候也会保留外部检查点
  • 检查点异常时是否让整个任务失败(failOnCheckpointingErrors)

    • 用于指定在检查点发生异常的时候,是否应该让任务直接失败退出。
    • 默认: true,如果设置为 false,则任务会丢弃掉检查点然后继续运行
  • 不对齐检查点(enableUnalignedCheckpoints)

    • 不再执行检查点的分界线对齐操作,启用之后可以大大减少产生背压时的检查点保存时间。这个设置要求检查点模式(CheckpointingMode)必须为 exctly-once,并且并发的检查点个数为 1
public class CheckpointConfig implements Serializable {
    private static final long serialVersionUID = -750378776078908147L;
    private static final Logger LOG = LoggerFactory.getLogger(CheckpointConfig.class);
    public static final CheckpointingMode DEFAULT_MODE;
    public static final long DEFAULT_TIMEOUT = 600000L;
    public static final long DEFAULT_MIN_PAUSE_BETWEEN_CHECKPOINTS = 0L;
    public static final int DEFAULT_MAX_CONCURRENT_CHECKPOINTS = 1;
    public static final int UNDEFINED_TOLERABLE_CHECKPOINT_NUMBER = -1;
    private CheckpointingMode checkpointingMode;
    private long checkpointInterval;
    private long checkpointTimeout;
    private long minPauseBetweenCheckpoints;
    private int maxConcurrentCheckpoints;
    private boolean forceCheckpointing;
    private boolean forceUnalignedCheckpoints;
    private boolean unalignedCheckpointsEnabled;
    private Duration alignmentTimeout;
    private boolean approximateLocalRecovery;
    private CheckpointConfig.ExternalizedCheckpointCleanup externalizedCheckpointCleanup;
    /** @deprecated */
    @Deprecated
    private boolean failOnCheckpointingErrors;
    private boolean preferCheckpointForRecovery;
    private int tolerableCheckpointFailureNumber;
    private transient CheckpointStorage storage;
}

状态后端说明

  • 状态的存储、访问以及维护,都是由一个可插拔的组件决定的,这个组件就叫作状态后端(state backend)。状态后端主要负责两件事:一是本地的状态管理,二是将检查点(checkpoint)写入远程的持久化存储

状态后端分类

哈希表状态后端(HashMapStateBackend)
  • 把状态存放在内存里

  • 具体实现上:哈希表状态后端在内部会直接把状态当作对象(objects),保存在 Taskmanager 的 JVM 堆(heap)上

  • HashMapStateBackend 是将本地状态全部放入内存的,这样可以获得最快的读写速度,使计算性能达到最佳;代价则是内存的占用

    env.setStateBackend(new HashMapStateBackend());
    
内嵌 RocksDB 状态后端(EmbeddedRocksDBStateBackend)
  • RocksDB 是一种内嵌的 key-value 存储介质,可以把数据持久化到本地硬盘。

  • 配置EmbeddedRocksDBStateBackend 后,会将处理中的数据全部放入 RocksDB 数据库中,RocksDB默认存储在 TaskManager 的本地数据目录里

  • 数据被存储为序列化的字节数组(Byte Arrays),读写操作需要序列化/反序列化,因此状态的访问性能要差一些

    env.setStateBackend(new EmbeddedRocksDBStateBackend());
    

检查点使用

  1. 启动检查点

    // 开启检查点,每 60s 执行一次检查点
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.enableCheckpointing(60000);
    
  2. 检查点存储

    // 配置存储检查点到 JobManager 堆内存
    env.getCheckpointConfig().setCheckpointStorage(new JobManagerCheckpointStorage());
    // 配置存储检查点到文件系统
    env.getCheckpointConfig().setCheckpointStorage(new FileSystemCheckpointStorage("hdfs://namenode:8020/flink/checkpoints"));
    
  3. 检查点配置:需要什么配置参考上面的检查点配置说明

CheckpointedFunction 接口

对状态进行持久化保存的快照机制叫作“检查点”(Checkpoint)。于是使用算子状态时,就需要对检查点的相关操作进行定义,实现一个 CheckpointedFunction 接口,并实现一下两个方法:

  • initializeState方法:定义了初始化逻辑,也定义了恢复逻辑

    • 在算子任务进行初始化时,会调用. initializeState()方法。有两种情况:
      • 应用第一次运行,这时状态会被初始化为一个默认值(default value);
      • 应用重启,**从检查点(checkpoint)或者保存点(savepoint)**中读取之前状态的快照,并赋给本地状态
    • 拿到的是 FunctionInitializationContext,这是函数类进行初始化时的上下文,是真正的“运行时上下文”
  • snapshotState()方法:检查点的快照保存逻辑

    • 每次应用保存检查点做快照时,都会调用.snapshotState()方法,将状态进行外部持久化
    • 快照的上下文 FunctionSnapshotContext,它可以提供检查点的相关信息,不过无法获取状态句柄
    public interface CheckpointedFunction {
        void snapshotState(FunctionSnapshotContext var1) throws Exception;
    
        void initializeState(FunctionInitializationContext var1) throws Exception;
    }
    

9. 保存点(Savepoint)

  • 是一个存盘的备份,它的原理和算法与检查点完全相同,只是多了一些额外的元数据。事实上,保存点就是通过检查点的机制来创建流式作业状态的一致性镜像(consistent image)的
  • 保存点中的状态快照,是以算子 ID 和状态名称组织起来的,相当于一个键值对。从保存点启动应用程序时,Flink 会将保存点的状态数据重新分配给相应的算子任务

使用保存点

创建保存点

  • ${jobId} : 需要填充要做镜像保存的作业 ID

  • targetDirectory : 可选,表示保存点存储的路径,对于保存点的默认路径,可以通过配置文件 flink-conf.yaml 中的 state.savepoints.dir 项来设定:state.savepoints.dir: hdfs:///flink/savepoints

    • 不停止任务做保存点

      bin/flink savepoint ${jobId} [:targetDirectory]
      
    • 停止任务做保存点

      bin/flink stop --savepointPath :jobId [:targetDirectory]
      

从保存点重启应用

  • ${savepointPath} : 指定保存点的路径

  • runArgs : 可选,flink任务的参数

    bin/flink run -s ${savepointPath} [:runArgs]
    

10. 状态一致性

分类

  • AT-MOST-ONCE(最多一次)
    • 当任务故障时,最简单的做法是什么都不干,既不恢复丢失的状态,也不重播丢失的数据。最多处理一次事件
  • AT-LEAST-ONCE(至少一次)
    • 在大多数的真实应用场景,我们希望不丢失事件。所有的事件都得到了处理,而一些事件还可能被处理多次
  • EXACTLY-ONCE(精确一次)
    • 恰好处理一次语义不仅仅意味着没有事件丢失,还意味着针对每一个数据,内部状态仅仅更新一次

端到端 exactly-once

  • 内部保证 :checkpoint
  • source :可重设数据的读取位置
  • sink :从故障恢复时,数据不会重复写入外部系统
    • 幂等写入
    • 事务写入

事务写入

  • 构建的事务对应着 checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中
  • 实现方式
    • 预写日志
    • 两阶段提交

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

两阶段提交
  • 对于每个 checkpoint,sink 任务会启动一个事务,并将接下来所有接收的数据添加到事务里
  • 然后将这些数据写入外部 sink 系统,但不提交它们 —— 这时只是“预提交”
  • 当它收到 checkpoint 完成的通知时,它才正式提交事务,实现结果的真正写入
  • TwoPhaseCommitSinkFunction 接口,自定义实现两阶段提交的 SinkFunction 的实现,提供了真正端到端的 exactly-once 保证

Flink+Kafka 端到端状态一致性的保证

说明

  • 内部
    • checkpoint 机制,把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性
  • source
    • kafka consumer 作为 source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的时候可以由连接器重置偏移量,重新消费数据,保证一致性
  • sink
    • kafka producer 作为sink,采用两阶段提交 sink,需要实现一个 TwoPhaseCommitSinkFunction

使用

  • 必须启用检查点
  • 在 FlinkKafkaProducer 的构造函数中传入参数 Semantic.EXACTLY_ONCE
  • 配置 Kafka 读取数据的消费者的隔离级别
    • 这里所说的 Kafka,是写入的外部系统。预提交阶段数据已经写入,只是被标记为“未提交”(uncommitted),而 Kafka 中默认的隔离级别 isolation.level 是 read_uncommitted,也就是可以读取未提交的数据。这样一来,外部应用就可以直接消费未提交的数据,对于事务性的保证就失效了。所以应该将隔离级别配置
  • 事务超时配置
    • Flink 的 Kafka连接器中配置的事务超时时间 transaction.timeout.ms 默认是 1小时,而Kafka集群配置的事务最大超时时间 transaction.max.timeout.ms 默认是 15 分钟。所以在检查点保存时间很长时,有可能出现 Kafka 已经认为事务超时了,丢弃了预提交的数据;而 Sink 任务认为还可以继续等待。如果接下来检查点保存成功,发生故障后回滚到这个检查点的状态,这部分数据就被真正丢掉了。所以这两个超时时间,前者应该小于等于后者

Maven(pom.xml)

<properties>
    <flink.version>1.13.0</flink.version>
    <java.version>1.8</java.version>
    <scala.binary.version>2.12</scala.binary.version>
</properties>


<dependencies>
<!--        <dependency>-->
<!--            <groupId>org.apache.flink</groupId>-->
<!--            <artifactId>flink-java</artifactId>-->
<!--            <version>${flink.version}</version>-->
<!--        </dependency>-->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-clients_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
<!--            <scope>provided</scope>-->
    </dependency>
    <!-- fix start-->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-sql-connector-kafka_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>
    <!-- fix end -->

    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-kafka_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
        <exclusions>
            <exclusion>
                <artifactId>kafka-clients</artifactId>
                <groupId>org.apache.kafka</groupId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
<!--            <scope>provided</scope>-->
        <!--            local test-->
        <!--            <scope>compile</scope>-->
        <exclusions>
            <exclusion>
                <artifactId>slf4j-api</artifactId>
                <groupId>org.slf4j</groupId>
            </exclusion>
            <exclusion>
                <artifactId>commons-collections</artifactId>
                <groupId>commons-collections</groupId>
            </exclusion>
        </exclusions>
        <version>${flink.version}</version>
    </dependency>

    <!--加入下面两个依赖才会出现 Flink 的日志出来-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.25</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.7.25</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.13</version>
    </dependency>

    <!--工具包依赖-->
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>18.0</version>
    </dependency>
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.8.5</version>
    </dependency>

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.4</version>
    </dependency>

    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.2</version>
        <exclusions>
            <exclusion>
                <artifactId>commons-logging</artifactId>
                <groupId>commons-logging</groupId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.4</version>
    </dependency>

    <dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <version>2.4.0</version>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
        <version>2.9.9</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!--cglib-->
    <dependency>
        <groupId>asm</groupId>
        <artifactId>asm</artifactId>
        <version>3.3.1</version>
    </dependency>
    <dependency>
        <groupId>asm</groupId>
        <artifactId>asm-commons</artifactId>
        <version>3.3.1</version>
    </dependency>
    <dependency>
        <groupId>asm</groupId>
        <artifactId>asm-util</artifactId>
        <version>3.3.1</version>
    </dependency>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib-nodep</artifactId>
        <version>2.2.2</version>
    </dependency>

    <!--state backend-->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-statebackend-rocksdb_${scala.binary.version}</artifactId>
        <scope>compile</scope>
        <exclusions>
            <exclusion>
                <artifactId>slf4j-api</artifactId>
                <groupId>org.slf4j</groupId>
            </exclusion>
        </exclusions>
        <version>${flink.version}</version>
    </dependency>

    <dependency>
        <groupId>com.beust</groupId>
        <artifactId>jcommander</artifactId>
        <version>1.72</version>
    </dependency>

    <!-- 阿里巴巴连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.21</version>
    </dependency>
    <dependency>
        <groupId>commons-dbutils</groupId>
        <artifactId>commons-dbutils</artifactId>
        <version>1.7</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.72</version>
    </dependency>

    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka_${scala.binary.version}</artifactId>
        <version>${kafka.version}</version>
        <scope>compile</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-compress -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-collections4</artifactId>
        <version>4.1</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/com.github.oshi/oshi-core -->
    <dependency>
        <groupId>com.github.oshi</groupId>
        <artifactId>oshi-core</artifactId>
        <version>3.5.0</version>
    </dependency>
</dependencies>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值