Flink1.13-java版教程(高阶2)

文章目录

第 9 章 状态编程

在这里插入图片描述

9.1 Flink 中的状态

在流处理中,数据是连续不断到来和处理的。每个任务进行计算处理时,可以基于当前数
据直接转换得到输出结果;也可以依赖一些其他数据。这些由一个任务维护,并且用来计算输
出结果的所有数据,就叫作这个任务的状态。

9.1.1 有状态算子

在这里插入图片描述
在这里插入图片描述

9.1.2 状态的管理

在这里插入图片描述
在这里插入图片描述

9.1.3 状态的分类

1. 托管状态(Managed State)和 原始状态(Raw State)

在这里插入图片描述

2. 算子状态(Operator State)和 按键分区状态(Keyed State)

接下来我们的重点就是托管状态(Managed State)。
我们知道在 Flink 中,一个算子任务会按照并行度分为多个并行子任务执行,而不同的子任务会占据不同的任务槽(task slot)。由于不同的 slot 在计算资源上是物理隔离的,所以 Flink能管理的状态在并行任务间是无法共享的,每个状态只能针对当前子任务的实例有效。
而很多有状态的操作(比如聚合、窗口)都是要先做 keyBy 进行按键分区的。按键分区之后,任务所进行的所有计算都应该只针对当前 key 有效,所以状态也应该按照 key 彼此隔离。在这种情况下,状态的访问方式又会有所不同。
基于这样的想法,我们又可以将托管状态分为两类:算子状态和按键分区状态。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9.2 按键分区状态(Keyed State)

在实际应用中,我们一般都需要将数据按照某个 key 进行分区,然后再进行计算处理;所
以最为常见的状态类型就是 Keyed State。之前介绍到 keyBy 之后的聚合、窗口计算,算子所
持有的状态,都是 Keyed State。
另外,我们还可以通过富函数类(Rich Function)对转换算子进行扩展、实现自定义功能,
比如 RichMapFunction、RichFilterFunction。在富函数中,我们可以调用.getRuntimeContext()
获取当前的运行时上下文(RuntimeContext),进而获取到访问状态的句柄;这种富函数中自
定义的状态也是 Keyed State。

9.2.1 基本概念和特点

在这里插入图片描述

在这里插入图片描述

9.2.2 支持的结构类型

实际应用中,需要保存为状态的数据会有各种各样的类型,有时还需要复杂的集合类型,
比如列表(List)和映射(Map)。对于这些常见的用法,Flink 的按键分区状态(Keyed State)提供了足够的支持。接下来我们就来了解一下 Keyed State 所支持的结构类型.

1.值状态(ValueState)

在这里插入图片描述

2.列表状态(ListState)

将需要保存的数据,以列表(List)的形式组织起来。在 ListState接口中同样有一个
类型参数 T,表示列表中数据的类型。ListState 也提供了一系列的方法来操作状态,使用方式与一般的 List 非常相似。
在这里插入图片描述

3.映射状态(MapState)

在这里插入图片描述

4.归约状态(ReducingState)

在这里插入图片描述

5.聚合状态(AggregatingState)

与归约状态非常类似,聚合状态也是一个值,用来保存添加进来的所有数据的聚合结果。
与 ReducingState 不同的是,它的聚合逻辑是由在描述器中传入一个更加一般化的聚合函数
(AggregateFunction)来定义的;这也就是之前我们讲过的 AggregateFunction,里面通过一个累加器(Accumulator)来表示状态,所以聚合的状态类型可以跟添加进来的数据类型完全不
同,使用更加灵活。同样地,AggregatingState 接口调用方法也与 ReducingState 相同,调用.add()方法添加元素时,会直接使用指定的 AggregateFunction 进行聚合并更新状态。

调用示例
package com.scy.chapter09;

import com.scy.chapter01.ClickSource;
import com.scy.chapter01.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.*;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.time.Duration;

public class StateTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long l) {
                                return event.timestamp;
                            }
                        }));

        stream.keyBy(data -> data.user)
                .flatMap(new MyFlatMap())
                .print();

        env.execute();
    }
    //实现FlatMapFunction,用于Keyed State测试
    public static class MyFlatMap extends RichFlatMapFunction<Event,String>{
        //定义状态
        ValueState<Event> myValueState;
        ListState<Event> myListState;
        MapState<String,Long> myMapState;
        ReducingState<Event> myReducingState;
        AggregatingState<Event,String> myAggregating;
        //增加一个本地变量进行对比
        Long count = 0L;


        @Override
        public void open(Configuration parameters) throws Exception {
            myValueState = getRuntimeContext().getState(new ValueStateDescriptor<Event>("my-state",Event.class));
            myListState = getRuntimeContext().getListState(new ListStateDescriptor<Event>("my-list",Event.class));
            myMapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Long>("my-map",String.class,Long.class));
            myReducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<Event>("my-reducing",
                    new ReduceFunction<Event>() {
                        @Override
                        public Event reduce(Event value1, Event value2) throws Exception {
                            return new Event(value1.user,value1.url,value2.timestamp);
                        }
                    }, Event.class));
            myAggregating = getRuntimeContext().getAggregatingState(new AggregatingStateDescriptor<Event, Long, String>("my-agg",
                    new AggregateFunction<Event, Long, String>() {
                        @Override
                        public Long createAccumulator() {
                            return 0L;
                        }

                        @Override
                        public Long add(Event event, Long accumulator) {
                            return accumulator+1;
                        }

                        @Override
                        public String getResult(Long accumulator) {
                            return "count: " + accumulator;
                        }

                        @Override
                        public Long merge(Long a, Long b) {
                            return a+b;
                        }
                    }, Long.class));
        }

        @Override
        public void flatMap(Event value, Collector<String> collector) throws Exception {
            //访问和更新状态
            System.out.println(myValueState.value());
            myValueState.update(value);
            System.out.println("my value: " + myValueState.value());

            myListState.add(value);


            myMapState.put(value.user,myMapState.get(value.user) == null ? 1: myMapState.get(value.user) + 1);
            System.out.println("my map state: " + value.user + " " + myMapState.get(value.user));

            myAggregating.add(value);
            System.out.println("my agg state: " + myAggregating.get());

            myReducingState.add(value);
            System.out.println("reducing state: " + myReducingState.get());

            count++;
            System.out.println("count: " + count);

        }
    }
}

在这里插入图片描述

9.2.3 应用代码实现

了解了按键分区状态(Keyed State)的基本概念和类型,接下来我们就可以尝试在代码中使用状态了。

1. 值状态(ValueState)应用示例

我们这里会使用用户 id 来进行分流,然后分别统计每个用户的 pv 数据,由于我们并不想
每次 pv 加一,就将统计结果发送到下游去,所以这里我们注册了一个定时器,用来隔一段时
间发送 pv 的统计结果,这样对下游算子的压力不至于太大。具体实现方式是定义一个用来保
存定时器时间戳的值状态变量。当定时器触发并向下游发送数据以后,便清空储存定时器时间
戳的状态变量,这样当新的数据到来时,发现并没有定时器存在,就可以注册新的定时器了,
注册完定时器之后将定时器的时间戳继续保存在状态变量中。

package com.scy.chapter09;

import com.scy.chapter01.ClickSource;
import com.scy.chapter01.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

import java.time.Duration;

public class PeriodicPvExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long l) {
                                return event.timestamp;
                            }
                        }));

        stream.print("input");

        //统计每个用户的pv
        stream.keyBy(data -> data.user)
                .process(new PeriodicPvResult())
                .print();
        env.execute();
    }
    //实现自定义KeyedProcessFunction
    private static class PeriodicPvResult extends KeyedProcessFunction<String,Event,String> {
        //定义状态,保存当前pv统计值,以及当前有没有定时器
        ValueState<Long> countState;
        ValueState<Long> timerTsState;

        @Override
        public void open(Configuration parameters) throws Exception {
            countState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("count", Long.class));
            timerTsState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("timer-ts", Long.class));
        }

        @Override
        public void processElement(Event value, Context ctx, Collector<String> out) throws Exception {
            //每来一条数据,就更新对应的count值
            Long count = countState.value();
            countState.update(count==null ? 1 : count + 1);
            //如果没有定时器注册定时器
            if (timerTsState.value() ==null){
                ctx.timerService().registerEventTimeTimer(value.timestamp + 10*1000L);
                timerTsState.update(value.timestamp + 10*1000L);
            }

        }

        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
            //定时器触发输出一次结果
            out.collect(ctx.getCurrentKey() + "'s pv: " + countState.value());
            //清空状态
            timerTsState.clear();
//            ctx.timerService().registerEventTimeTimer(timestamp + 10*1000L); 类似滚动窗口的实现
//            timerTsState.update(timestamp + 10*1000L);
        }
    }
}

在这里插入图片描述

2. 列表状态(ListState)应用示例

在 Flink SQL 中,支持两条流的全量 Join,语法如下:
SELECT * FROM A INNER JOIN B WHERE A.id = B.id;
这样一条 SQL 语句要慎用,因为 Flink 会将 A 流和 B 流的所有数据都保存下来,然后进
行 Join。不过在这里我们可以用列表状态变量来实现一下这个 SQL 语句的功能。代码如下:

package com.scy.chapter09;


import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.CoProcessFunction;
import org.apache.flink.util.Collector;

import java.time.Duration;

public class TwoStreamJoinExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        SingleOutputStreamOperator<Tuple3<String,String,Long>> stream1 = env.fromElements(
                 Tuple3.of("a", "stream-1", 1000L),
                 Tuple3.of("b", "stream-1", 2000L),
                Tuple3.of("a", "stream-1", 3000L)

        ).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple3<String,String,Long>>forMonotonousTimestamps()
        .withTimestampAssigner(new SerializableTimestampAssigner<Tuple3<String,String,Long>>() {
            @Override
            public long extractTimestamp(Tuple3<String, String, Long> element, long recordTimestamp) {
                return element.f2;
            }
        }));

        SingleOutputStreamOperator<Tuple3<String,String,Long>> stream2 = env.fromElements(
                Tuple3.of("a", "stream-2", 3000L),
                Tuple3.of("b", "stream-2", 4000L),
                Tuple3.of("a", "stream-2", 6000L)

        ).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple3<String,String,Long>>forMonotonousTimestamps()
                .withTimestampAssigner(new SerializableTimestampAssigner<Tuple3<String,String,Long>>() {
                    @Override
                    public long extractTimestamp(Tuple3<String, String, Long> element, long recordTimestamp) {
                        return element.f2;
                    }
                }));

        //自定义列表状态进行全外连接
        stream1.keyBy(data -> data.f0)
                .connect(stream2.keyBy(data -> data.f0))
                .process(new CoProcessFunction<Tuple3<String, String, Long>, Tuple3<String, String, Long>, String>() {
                    //定义列表状态,用于保存两条流中已经到达的所有数据
                    private ListState<Tuple2<String, Long>> stream1ListState;
                    private ListState<Tuple2<String, Long>> stream2ListState;

                    @Override
                    public void open(Configuration parameters) throws Exception {
                        stream1ListState = getRuntimeContext().getListState(new ListStateDescriptor<Tuple2<String, Long>>("stream1-list", Types.TUPLE(Types.STRING,Types.LONG)));
                        stream2ListState = getRuntimeContext().getListState(new ListStateDescriptor<Tuple2<String, Long>>("stream2-list", Types.TUPLE(Types.STRING,Types.LONG)));
                    }

                    @Override
                    public void processElement1(Tuple3<String, String, Long> left, Context ctx, Collector<String> out) throws Exception {
                        //获取另外一条流中所有数据,配对输出
                        for (Tuple2<String, Long> right: stream2ListState.get()){
                            out.collect(left.f0 + " " + left.f2 + " => " + right);
                        }
                        stream1ListState.add(Tuple2.of(left.f0,left.f2));
                    }

                    @Override
                    public void processElement2(Tuple3<String, String, Long> right, Context ctx, Collector<String> out) throws Exception {
                        for (Tuple2<String, Long> left: stream1ListState.get()){
                            out.collect(left + " => " + right.f0 + " " + right.f2);
                        }
                        stream2ListState.add(Tuple2.of(right.f0,right.f2));
                    }
                }).print();

        env.execute();
    }
}

在这里插入图片描述

3. 映射状态(MapState)应用示例

映射状态的用法和 Java 中的 HashMap 很相似。在这里我们可以通过 MapState 的使用来探
索一下窗口的底层实现,也就是我们要用映射状态来完整模拟窗口的功能。这里我们模拟一个
滚动窗口。我们要计算的是每一个 url 在每一个窗口中的 pv 数据。我们之前使用增量聚合和
全窗口聚合结合的方式实现过这个需求。这里我们用 MapState 再来实现一下。

package com.scy.chapter09;

import com.scy.chapter01.ClickSource;
import com.scy.chapter01.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.MapState;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;

import java.sql.Timestamp;
import java.time.Duration;

public class FakeWindowExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long l) {
                                return event.timestamp;
                            }
                        }));

        stream.print("input");

        stream.keyBy(data -> data.url)
                .process(new FakeWindowResult(10000L))
                .print();

        env.execute();
    }
    //实现自定义的keyedProcessFunction
    private static class FakeWindowResult extends KeyedProcessFunction<String,Event,String> {
        private Long windowSize; //窗口大小

        public FakeWindowResult(Long windowSize) {
            this.windowSize = windowSize;
        }

        //定义一个MapState,用来保存每个窗口中统计的count值
        MapState<Long,Long> windowUrlCountMapState;

        @Override
        public void open(Configuration parameters) throws Exception {
            windowUrlCountMapState = getRuntimeContext().getMapState(new MapStateDescriptor<Long, Long>("window-count", Long.class, Long.class));
        }

        @Override
        public void processElement(Event value, Context ctx, Collector<String> out) throws Exception {
            //每来一条数据应该判断时间戳属于哪个窗口(窗口分配器)
            Long windowStart = value.timestamp / windowSize * windowSize;
            Long windowEnd = windowStart +  windowSize;

            //注册end-1的定时器
            ctx.timerService().registerEventTimeTimer(windowEnd - 1);

            //更新状态,进行增量聚合
            if (windowUrlCountMapState.contains(windowStart)){
                Long count = windowUrlCountMapState.get(windowStart);
                windowUrlCountMapState.put(windowStart, count + 1);
            }else {
                windowUrlCountMapState.put(windowStart,1L);
            }
        }
        //定时器触发时输出计算结果
        @Override
        public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
            Long windowEnd = timestamp + 1;
            Long windowStart = windowEnd - windowSize;
            Long count = windowUrlCountMapState.get(windowStart);

            out.collect("窗口 " + new Timestamp(windowStart) + " ~ " + new Timestamp(windowEnd)
                    + " url: " + ctx.getCurrentKey() + " count: " + count);
            //模拟窗口的关闭,清除map中对应的key-value
            windowUrlCountMapState.remove(windowStart);
        }
    }
}

在这里插入图片描述

4. 聚合状态(AggregatingState)

我们举一个简单的例子,对用户点击事件流每 5 个数据统计一次平均时间戳。这是一个类
似计数窗口(CountWindow)求平均值的计算,这里我们可以使用一个有聚合状态的RichFlatMapFunction 来实现。

package com.scy.chapter09;

import com.scy.chapter01.ClickSource;
import com.scy.chapter01.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.AggregatingState;
import org.apache.flink.api.common.state.AggregatingStateDescriptor;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.time.Duration;

public class AverageTimeStampExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long l) {
                                return event.timestamp;
                            }
                        }));

        stream.print("input");

        //自定义实现平均时间戳的统计
        stream.keyBy(data -> data.user)
                .flatMap(new AvgTsResult(5L))
                .print();

        env.execute();
    }
    //实现自定义的RichFlatmapFunction
    private static class AvgTsResult extends RichFlatMapFunction<Event,String> {
        private Long count;

        public AvgTsResult(Long count) {
            this.count = count;
        }

        //定义聚合状态,用来保存平均时间戳
        AggregatingState<Event,Long> avgTsAggState;
        //定义一个值状态,保存用户访问次数
        ValueState<Long> countState;

        @Override
        public void open(Configuration parameters) throws Exception {
            avgTsAggState = getRuntimeContext().getAggregatingState(new AggregatingStateDescriptor<Event, Tuple2<Long,Long>, Long>(
                    "avg-ts",
                    new AggregateFunction<Event, Tuple2<Long, Long>, Long>() {
                        @Override
                        public Tuple2<Long, Long> createAccumulator() {
                            return Tuple2.of(0L,0L);
                        }

                        @Override
                        public Tuple2<Long, Long> add(Event event, Tuple2<Long, Long> accumulator) {
                            return Tuple2.of(accumulator.f0 + event.timestamp,accumulator.f1+1);
                        }

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

                        @Override
                        public Tuple2<Long, Long> merge(Tuple2<Long, Long> longLongTuple2, Tuple2<Long, Long> acc1) {
                            return null;
                        }
                    },
                    Types.TUPLE(Types.LONG, Types.LONG)
            ));

            countState = getRuntimeContext().getState(new ValueStateDescriptor<Long>("count",Long.class));

        }

        @Override
        public void flatMap(Event event, Collector<String> out) throws Exception {
            //每来一条数据,curr count 加 1
            Long currCount = countState.value();
            if (currCount == null){
                currCount = 1L;
            }else {
                currCount++;
            }
            //更新状态
            countState.update(currCount);
            avgTsAggState.add(event);

            //如果达到count次数就输出结果
            if (currCount.equals(count)){
                out.collect(event.user + "过去 " + count + " 次访问平均时间戳为 " +avgTsAggState.get() );
                //清理状态
                countState.clear();
                avgTsAggState.clear();
            }
        }
    }
}

在这里插入图片描述

9.2.4 状态生存时间(TTL)

在这里插入图片描述

ValueStateDescriptor<Event> valueStateDescriptor = new ValueStateDescriptor<>("my-state", Event.class);
myValueState = getRuntimeContext().getState(valueStateDescriptor);

//配置状态的TTL
StateTtlConfig ttlConfig = StateTtlConfig.newBuilder(Time.hours(1))
       .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
       .setStateVisibility(StateTtlConfig.StateVisibility.ReturnExpiredIfNotCleanedUp)
       .build();

valueStateDescriptor.enableTimeToLive(ttlConfig);

在这里插入图片描述

9.3 算子状态(Operator State)

除按键分区状态(Keyed State)之外,另一大类受控状态就是算子状态(Operator State)。
从某种意义上说,算子状态是更底层的状态类型,因为它只针对当前算子并行任务有效,不需
要考虑不同 key 的隔离。算子状态功能不如按键分区状态丰富,应用场景较少,它的调用方法
也会有一些区别。

9.3.1 基本概念和特点

算子状态(Operator State)就是一个算子并行实例上定义的状态,作用范围被限定为当前
算子任务。算子状态跟数据的 key 无关,所以不同 key 的数据只要被分发到同一个并行子任务,就会访问到同一个 Operator State。算子状态的实际应用场景不如 Keyed State 多,一般用在Source 或 Sink 等与外部系统连接的算子上或者完全没有 key 定义的场景。比如 Flink 的 Kafka 连接器中,就用到了算子状态。在我们给 Source 算子设置并行度后,Kafka 消费者的每一个并行实例,都会为对应的主题(topic)分区维护一个偏移量, 作为算子状态保存起来。这在保证 Flink 应用“精确一次”(exactly-once)状态一致性时非常有用。关于状态一致性的内容,我们会在第十章详细展开。
当算子的并行度发生变化时,算子状态也支持在并行的算子任务实例之间做重组分配。根
据状态的类型不同,重组分配的方案也会不同。

9.3.2 状态类型

1. 列表状态(ListState)

在这里插入图片描述

2. 联合列表状态(UnionListState)

在这里插入图片描述

3. 广播状态(BroadcastState)

在这里插入图片描述
在这里插入图片描述

9.3.3 代码实现

在这里插入图片描述

1. CheckpointedFunction 接口

在 Flink 中,对状态进行持久化保存的快照机制叫作“检查点”(Checkpoint)。于是使用算子状态时,就需要对检查点的相关操作进行定义,实现一个 CheckpointedFunction 接口。
在这里插入图片描述
在这里插入图片描述

2. 示例代码

接下来我们举一个算子状态的应用案例。在下面的例子中,自定义的 SinkFunction 会在
CheckpointedFunction 中进行数据缓存,然后统一发送到下游。这个例子演示了列表状态的平
均分割重组(event-split redistribution)。

package com.scy.chapter09;

import com.scy.chapter01.ClickSource;
import com.scy.chapter01.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.ListState;
import org.apache.flink.api.common.state.ListStateDescriptor;
import org.apache.flink.runtime.state.FunctionInitializationContext;
import org.apache.flink.runtime.state.FunctionSnapshotContext;
import org.apache.flink.streaming.api.checkpoint.CheckpointedFunction;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

public class BufferingSinkExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
                .assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            @Override
                            public long extractTimestamp(Event event, long l) {
                                return event.timestamp;
                            }
                        }));

        stream.print("input");

        //批量缓存输出
        stream.addSink(new BufferingSink(10));
        env.execute();
    }
    // 自定义实现Sinkfunction
    public static class BufferingSink implements SinkFunction<Event>, CheckpointedFunction {
        //定义当前类的属性
        private final int threshold;

        public BufferingSink(int threshold) {
            this.threshold = threshold;
            this.bufferedElements = new ArrayList<>();
        }

        private List<Event> bufferedElements; //用于数据缓存本地

        //定义一个算子状态
        private ListState<Event> checkPointedState; //用于数据持久化

        @Override
        public void invoke(Event value, Context context) throws Exception {
            bufferedElements.add(value);  //缓存到列表
            //判断如果达到阈值,就批量写入
            if (bufferedElements.size() ==threshold){
                //用打印控制台模拟写入外部系统
                for (Event element: bufferedElements){
                    System.out.println(element);
                }
                System.out.println("========输出完毕=======");
                bufferedElements.clear();
            }
        }

        @Override
        public void snapshotState(FunctionSnapshotContext context) throws Exception {
            //清空状态,保证跟bufferedElements完全一样
            checkPointedState.clear();

            //对状态进行持久化,复制缓存的列表到列表状态
            for (Event element: bufferedElements){
                checkPointedState.add(element);
            }
        }

        @Override
        public void initializeState(FunctionInitializationContext context) throws Exception {
            //定义算子状态
            ListStateDescriptor<Event> descriptor = new ListStateDescriptor<>("buffered-elements", Event.class);
            checkPointedState = context.getOperatorStateStore().getListState(descriptor);

            //如果从故障恢复,需要讲ListState中所有元素复制到列表中
            if (context.isRestored()){
                for (Event element:checkPointedState.get())
                    bufferedElements.add(element);
            }
        }
    }
}

在这里插入图片描述

9.4 广播状态(Broadcast State)

算子状态中有一类很特殊,就是广播状态(Broadcast State)。从概念和原理上讲,广播状
态非常容易理解:状态广播出去,所有并行子任务的状态都是相同的;并行度调整时只要直接
复制就可以了。然而在应用上,广播状态却与其他算子状态大不相同。本节我们就专门来讨论
一下广播状态的使用。

9.4.1 基本用法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9.4.2 代码实例

接下来我们举一个广播状态的应用案例。考虑在电商应用中,往往需要判断用户先后发生
的行为的“组合模式”,比如“登录-下单”或者“登录-支付”,检测出这些连续的行为进行统
计,就可以了解平台的运用状况以及用户的行为习惯。

package com.scy.chapter09;

import org.apache.flink.api.common.state.*;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.BroadcastStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.KeyedBroadcastProcessFunction;
import org.apache.flink.util.Collector;

public class BehaviorPatternDetectExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        //用户行为数据流
        DataStreamSource<Action> actionStream = env.fromElements(
                new Action("Alice", "login"),
                new Action("Alice", "pay"),
                new Action("Bob", "login"),
                new Action("Bob", "order")
        );


        //行为模式流,基于它构建广播流
        DataStreamSource<Pattern> patternStreamSource = env.fromElements(
                new Pattern("login", "pay"),
                new Pattern("login", "order")
        );

        //定义广播状态描述器
        MapStateDescriptor<Void, Pattern> descriptor = new MapStateDescriptor<>("pattern", Types.VOID, Types.POJO(Pattern.class));
        BroadcastStream<Pattern> broadcastStream = patternStreamSource.broadcast(descriptor);

        //连接两条流进行处理
        SingleOutputStreamOperator<Tuple2<String,Pattern>> matches = actionStream.keyBy(data -> data.userId)
                .connect(broadcastStream)
                .process(new PatternDetector());

        matches.print();

        env.execute();
    }

    //定义用户行为事件和模式的POJO类
    public static class Action{
        public String userId;
        public String action;

        public Action(String userId, String action) {
            this.userId = userId;
            this.action = action;
        }

        public Action() {
        }

        @Override
        public String toString() {
            return "Action{" +
                    "userId='" + userId + '\'' +
                    ", action='" + action + '\'' +
                    '}';
        }
    }

    public static class Pattern{
        public String action1;
        public String action2;

        public Pattern() {
        }

        public Pattern(String action1, String action2) {
            this.action1 = action1;
            this.action2 = action2;
        }

        @Override
        public String toString() {
            return "Pattern{" +
                    "action1='" + action1 + '\'' +
                    ", action2='" + action2 + '\'' +
                    '}';
        }
    }
    //实现自定义KeyedBroadcastProcessFunction
    private static class PatternDetector extends KeyedBroadcastProcessFunction<String,Action,Pattern,Tuple2<String,Pattern>> {
        //定义一个KeyState,保存用户的上一个行为
        ValueState<String> preActionState;

        @Override
        public void open(Configuration parameters) throws Exception {
            preActionState = getRuntimeContext().getState(new ValueStateDescriptor<String>("last-action",String.class));
        }

        @Override
        public void processElement(Action value, ReadOnlyContext ctx, Collector<Tuple2<String, Pattern>> out) throws Exception {
            //从广播状态中获取匹配规则
            ReadOnlyBroadcastState<Void, Pattern> patternState = ctx.getBroadcastState(new MapStateDescriptor<>("pattern", Types.VOID, Types.POJO(Pattern.class)));
            Pattern pattern = patternState.get(null);

            //获取用户上一次行为
            String preAction = preActionState.value();

            //判断是否匹配
            if (pattern != null && preAction != null){
                if (pattern.action1.equals(preAction) && pattern.action2.equals(value.action))
                    out.collect(new Tuple2<>(ctx.getCurrentKey(),pattern));
            }

            //状态更新
            preActionState.update(value.action);
        }

        @Override
        public void processBroadcastElement(Pattern value, Context ctx, Collector<Tuple2<String, Pattern>> out) throws Exception {
            //从上下文中获取广播状态,并更新当前数据状态
            BroadcastState<Void, Pattern> patternState = ctx.getBroadcastState(new MapStateDescriptor<>("pattern", Types.VOID, Types.POJO(Pattern.class)));
            patternState.put(null,value);
        }
    }
}

在这里插入图片描述

9.5 状态持久化和状态后端

在 Flink 的状态管理机制中,很重要的一个功能就是对状态进行持久化(persistence)保存,这样就可以在发生故障后进行重启恢复。Flink 对状态进行持久化的方式,就是将当前所有分布式状态进行“快照”保存,写入一个“检查点”(checkpoint)或者保存点(savepoint)保存到外部存储系统中。具体的存储介质,一般是分布式文件系统(distributed file system)。

9.5.1 检查点(Checkpoint)

在这里插入图片描述
所以不会由Flink自动创建,而需要用户手动触发。这在有计划地停止、重启应用时非常有用。

9.5.2 状态后端(State Backends)

在这里插入图片描述

1. 状态后端的分类

状态后端是一个“开箱即用”的组件,可以在不改变应用程序逻辑的情况下独立配置。Flink 中提供了两类不同的状态后端,一种是“哈希表状态后端”(HashMapStateBackend),另一种是“内嵌 RocksDB 状态后端”(EmbeddedRocksDBStateBackend)。如果没有特别配置,系统默认的状态后端是 HashMapStateBackend。

(1)哈希表状态后端(HashMapStateBackend)

这种方式就是我们之前所说的,把状态存放在内存里。具体实现上,哈希表状态后端在内部会直接把状态当作对象(objects),保存在 Taskmanager 的 JVM 堆(heap)上。普通的状态,以及窗口中收集的数据和触发器(triggers),都会以键值对(key-value)的形式存储起来,所以底层是一个哈希表(HashMap),这种状态后端也因此得名。对于检查点的保存,一般是放在持久化的分布式文件系统(file system)中,也可以通过配置“检查点存储(CheckpointStorage)来另外指定。HashMapStateBackend 是将本地状态全部放入内存的,这样可以获得最快的读写速度,使计算性能达到最佳;代价则是内存的占用。它适用于具有大状态、长窗口、大键值状态的作业,对所有高可用性设置也是有效的。

(2)内嵌 RocksDB 状态后端(EmbeddedRocksDBStateBackend)

RocksDB 是一种内嵌的 key-value 存储介质,可以把数据持久化到本地硬盘。配置EmbeddedRocksDBStateBackend 后,会将处理中的数据全部放入 RocksDB 数据库中,RocksDB默认存储在 TaskManager 的本地数据目录里。
与 HashMapStateBackend 直接在堆内存中存储对象不同,这种方式下状态主要是放在RocksDB 中的。数据被存储为序列化的字节数组(Byte Arrays),读写操作需要序列化/反序列
化,因此状态的访问性能要差一些。另外,因为做了序列化,key 的比较也会按照字节进行,而不是直接调用.hashCode()和.equals()方法。
对于检查点,同样会写入到远程的持久化文件系统中。EmbeddedRocksDBStateBackend 始终执行的是异步快照,也就是不会因为保存检查点而阻塞数据的处理;而且它还提供了增量式保存检查点的机制,这在很多情况下可以大大提升保存效率。
由于它会把状态数据落盘,而且支持增量化的检查点,所以在状态非常大、窗口非常长、
键/值状态很大的应用场景中是一个好选择,同样对所有高可用性设置有效。

2. 如何选择正确的状态后端

在这里插入图片描述

3. 状态后端的配置

在不做配置的时候,应用程序使用的默认状态后端是由集群配置文件 flink-conf.yaml 中指
定的,配置的键名称为 state.backend。这个默认配置对集群上运行的所有作业都有效,我们可
以通过更改配置值来改变默认的状态后端。另外,我们还可以在代码中为当前作业单独配置状
态后端,这个配置会覆盖掉集群配置文件的默认值。

(1)配置默认的状态后端

在 flink-conf.yaml 中,可以使用 state.backend 来配置默认状态后端。
配置项的可能值为 hashmap,这样配置的就是 HashMapStateBackend;也可以是 rocksdb,这样配置的就是 EmbeddedRocksDBStateBackend。另外,也可以是一个实现了状态后端工厂StateBackendFactory 的类的完全限定类名。
在这里插入图片描述

(2)为每个作业(Per-job)单独配置状态后端

在这里插入图片描述

9.6 本章总结

有状态的流处理是 Flink 的本质,所以状态可以说是 Flink 中最为重要的概念。之前聚合算子、窗口算子中已经提到了状态的概念,而通过本章的学习,我们对整个 Flink 的状态管理机制和状态编程的方式都有了非常详尽的了解。
本章从状态的概念和分类出发,详细介绍了 Flink 中的按键分区状态(Keyed State)和算子状态(Operator State)的特点和用法,并对广播状态(Broadcast State)做了进一步的展开说明。最后,我们还介绍了状态的持久化和状态后端,引出了检查点(checkpoint)的概念。检查点是一个非常重要的概念,是 Flink 容错机制的核心,我们将在下一章继续进行详细的讨论。

第 10 章 容错机制

在这里插入图片描述

10.1 检查点(Checkpoint)

在这里插入图片描述
在这里插入图片描述

10.1.1 检查点的保存

什么时候进行检查点的保存呢?最理想的情况下,我们应该“随时”保存,也就是每处理
完一个数据就保存一下当前的状态;这样如果在处理某条数据时出现故障,我们只要回到上一
个数据处理完之后的状态,然后重新处理一遍这条数据就可以。这样重复处理的数据最少,完
全没有多余操作,可以做到最低的延迟。然而实际情况不会这么完美。

  1. 周期性的触发保存
    在这里插入图片描述
  2. 保存的时间点
    这里有一个关键问题:当检查点的保存被触发时,任务有可能正在处理某个数据,这时该怎么办呢?
    在这里插入图片描述
  3. 保存的具体流程
    在这里插入图片描述
    在这里插入图片描述

10.1.2 从检查点恢复状态

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10.1.3 检查点算法

在这里插入图片描述

1. 检查点分界线(Barrier)

我们现在的目标是,在不暂停流处理的前提下,让每个任务“认出”触发检查点保存的那个数据。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. 分布式快照算法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10.1.4 检查点配置

检查点的作用是为了故障恢复,我们不能因为保存检查点占据了大量时间、导致数据处理性能明显降低。为了兼顾容错性和处理性能,我们可以在代码中对检查点进行各种配置。

1. 启用检查点

默认情况下,Flink 程序是禁用检查点的。如果想要为 Flink 应用开启自动保存快照的功能,需要在代码中显式地调用执行环境的.enableCheckpointing()方法:
在这里插入图片描述

2. 检查点存储(Checkpoint Storage)

在这里插入图片描述

3. 其他高级配置

在这里插入图片描述
在这里插入图片描述

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 启用检查点,间隔时间 1 秒
env.enableCheckpointing(1000);
CheckpointConfig checkpointConfig = env.getCheckpointConfig();
// 设置精确一次模式
checkpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// 最小间隔时间 500 毫秒
checkpointConfig.setMinPauseBetweenCheckpoints(500);
// 超时时间 1 分钟
checkpointConfig.setCheckpointTimeout(60000);
// 同时只能有一个检查点
checkpointConfig.setMaxConcurrentCheckpoints(1);
// 开启检查点的外部持久化保存,作业取消后依然保留
checkpointConfig.enableExternalizedCheckpoints(
 ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
// 启用不对齐的检查点保存方式
checkpointConfig.enableUnalignedCheckpoints();
// 设置检查点存储,可以直接传入一个 String,指定文件系统的路径
checkpointConfig.setCheckpointStorage("hdfs://my/checkpoint/dir")

10.1.5 保存点(Savepoint)

在这里插入图片描述

1. 保存点的用途

在这里插入图片描述
在这里插入图片描述

2. 使用保存点

在这里插入图片描述

10.2 状态一致性

之前讲到检查点又叫作“一致性检查点”,是 Flink 容错机制的核心。接下来我们就对状态一致性的概念进行展开,结合理论和实际应用场景,讨论一下 Flink 流式处理架构中的应对机制。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10.3 端到端精确一次(end-to-end exactly-once)

在这里插入图片描述

10.3.1 输入端保证

在这里插入图片描述

10.3.2 输出端保证

在这里插入图片描述

1. 幂等(idempotent)写入

在这里插入图片描述
在这里插入图片描述

2. 事务(transactional)写入

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

10.3.3 Flink 和 Kafka 连接时的精确一次保证

在流处理的应用中,最佳的数据源当然就是可重置偏移量的消息队列了;它不仅可以提供数据重放的功能,而且天生就是以流的方式存储和处理数据的。所以作为大数据工具中消息队列的代表,Kafka 可以说与 Flink 是天作之合,实际项目中也经常会看到以 Kafka 作为数据源和写入的外部系统的应用。在本小节中,我们就来具体讨论一下 Flink 和 Kafka 连接时,怎样保证端到端的 exactly-once 状态一致性。

1. 整体介绍

在这里插入图片描述

2. 具体步骤

为了方便说明,我们来考虑一个具体的流处理系统,由 Flink 从 Kafka 读取数据、并将处理结果写入 Kafka,如图 10-14 所示。
在这里插入图片描述

(1)启动检查点保存

在这里插入图片描述

(2)算子任务对状态做快照

分界线(barrier)会在算子间传递下去。每个算子收到 barrier 时,会将当前的状态做个快照,保存到状态后端。

在这里插入图片描述

(3)Sink 任务开启事务,进行预提交

在这里插入图片描述

(4)检查点保存完成,提交事务

当所有算子的快照都完成,也就是这次的检查点保存最终完成时,JobManager 会向所有任务发确认通知,告诉大家当前检查点已成功保存,如图 10-18 所示。
在这里插入图片描述

3. 需要的配置

在这里插入图片描述

10.4 本章总结

Flink 作为一个大数据分布式流处理框架,必须要考虑系统的容错性,主要就是发生故障之后的恢复。Flink 容错机制的核心就是检查点,它通过巧妙的分布式快照算法保证了故障恢复后的一致性,并且尽可能地降低对处理性能的影响。
本章中我们详细介绍了 Flink 检查点的原理、算法和配置,并且结合一致性理论与Flink-Kafka 的实际互连系统,阐述了如何用 Flink 实现流处理应用的端到端 exactly-once 状态一致性。这既是 Flink 底层原理的深入,也与之前的状态管理、水位线机制有联系和相通之处;
相信通过本章内容的学习,读者会对 Flink 乃至分布式系统的容错机制有更加深刻的理解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值