Flink一套完结

Flink流处理API

image-20220215085451014

Environment

getExecutionEnvironment

创建一个执行环境,表示当前执行程序的上下文。如果程序是独立调用的,则此方法返回本地执行环境。如果从命令行客户端调用以提交到集群,则此方法返回此集群的执行环境。也就是说,getExecutionEnvironment会根据查询运行的方式决定返回什么样的运行环境。

// 批处理执行环境
ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// 流处理执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

如果没有设置并行度,会以flink-conf.yaml中的配置为准,默认是1

image-20220113092918686

createLocalEnvironment

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

LocalStreamEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(1);

createRemoteEnvironment

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

StreamExecutionEnvironment env =StreamExecutionEnvironment.createRemoteEnvironment("Jobmanage-hostname",6123,"youpath//wordcount.jar")

Source

从集合读取数据

  • env.fromCollection:封装成集合
  • env.fromElements:直接传入数据
DataStreamSource<SensorReading> dataStreamSource = env.fromCollection(Arrays.asList(
        new SensorReading("sensor_1", 12346L, 35.8),
        new SensorReading("sensor_2", 12346L, 36.8),
        new SensorReading("sensor_3", 12346L, 37.8)
));
DataStreamSource<Integer> integerDataStreamSource = env.fromElements(1, 2, 3, 4, 5, 6);

从文件读取数据

DataStreamSource<String> dataStreamSource = env.readTextFile("sensor.txt");

以Kafka消息队列的数据作为来源

String sourceTopic="sensor";
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "kafka_20220113");
DataStreamSource<String> dataStreamSource = env.addSource(new FlinkKafkaConsumer<String>(sourceTopic, new SimpleStringSchema(), properties));

自定义Source

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

    DataStreamSource<SensorReading> dataStreamSource = env.addSource(new MySensorSource());

    dataStreamSource.print();

    env.execute("MySensor");
}

// 实现自定义的SourceFunction
public static class MySensorSource implements SourceFunction<SensorReading> {

    // 定义一个标志位 用来控制数据的产生
    private boolean running = true;

    @Override
    public void run(SourceContext<SensorReading> sourceContext) throws Exception {
        Random random = new Random();
        HashMap<String, Double> sensorTempMap = new HashMap<>();
        for (int i = 1; i <= 10; i++) {
            sensorTempMap.put("sensor_" + i, 60 + random.nextGaussian() * 20);
        }
        while (running) {
            for (String sensorId : sensorTempMap.keySet()) {
                Double newtemp = sensorTempMap.get(sensorId) + random.nextGaussian();
                sensorTempMap.put(sensorId, newtemp);
                // source上下文 生成数据
                sourceContext.collect(new SensorReading(sensorId, System.currentTimeMillis(), newtemp));
            }
        }
        // 控制输出频率
        Thread.sleep(1000L);
    }

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

Transform

map

map的作用就是对数据集之中的元素逐一进行函数操作映射为另外一个数据集。一对一

SingleOutputStreamOperator<Integer> mapStream = inputStream.map(new MapFunction<String, Integer>() {
    @Override
    public Integer map(String s) throws Exception {
        return s.length();
    }
});

flatMap

flatMap的操作是将函数应用于数据集之中的每一个元素,将返回的迭代器的所有内容构成新的数据集。一对多

DataStream<String> flatMapStream = inputStream.flatMap(new FlatMapFunction<String, String>() {
    @Override
    public void flatMap(String s, Collector<String> collector) throws Exception {
        String[] fields = s.split(",");
        for (String field : fields) {
            collector.collect(field);
        }
    }
});

Filter

fliter的作用是过滤数据,return true不过滤 return false过滤

DataStream<String> filterStream = inputStream.filter(new FilterFunction<String>() {
    @Override
    public boolean filter(String s) throws Exception {
        return s.startsWith("sensor_1");
    }
});

KeyBy

DataStream->KeyedStream逻辑地将一个流拆分成不相交的分区,每个分区包含具有相同key的元素,在内部以hash的形式实现

// 分组
KeyedStream<SensorReading, String> keyedStream = dataStream.keyBy(SensorReading::getId);

滚动聚合算子

这些算子可以针对KeyedStream的每一个支流做聚合。

  • sum()
  • min()
  • max()
  • minBy()
  • maxBy()
public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setParallelism(1);

    // 从文件读取数据
    DataStreamSource<String> inputStream = env.readTextFile("E:\\java\\flink\\src\\main\\resources\\sensor.txt");

    // 转化成SensorReading
    DataStream<SensorReading> dataStream = inputStream.map(line -> {
        String[] fields = line.split(",");
        return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
    });

    // 分组
    KeyedStream<SensorReading, String> keyedStream = dataStream.keyBy(SensorReading::getId);

    // 滚动聚合 取最大值
    DataStream<SensorReading> resultStream = keyedStream.max("temperature");

    // 打印输出
    resultStream.print("resultStream>>>>>");

    env.execute("RollingAggregationTransform");
}

Reduce

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

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

    // 从文件读取数据
    DataStreamSource<String> inputStream = env.readTextFile("E:\\java\\flink\\src\\main\\resources\\sensor.txt");

    // 转化成SensorReading
    DataStream<SensorReading> dataStream = inputStream.map(line -> {
        String[] fields = line.split(",");
        return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
    });

    // 分组
    KeyedStream<SensorReading, String> keyedStream = dataStream.keyBy(SensorReading::getId);

    // reduce聚合 最大的温度值以及当前最新的时间戳
    SingleOutputStreamOperator<SensorReading> resultStream = keyedStream.reduce((curData, newData) -> new SensorReading(curData.getId(), newData.getTimestamp(), Math.max(curData.getTemperature(), newData.getTemperature())));

    resultStream.print("resultStream>>>>>");

    // 执行任务
    env.execute("ReduceTransform");
}

Split和Select

image-20220215090324674

Split:DataStream->SplitStream,根据某些特征把一个DataStream拆分成两个或者多个DataStream

image-20220215090331984

Select:SplitStream→DataStream:从一个SplitStream中获取一个或者多个DataStream

SplitStream<SensorReading> splitStream = dataStream.split(new OutputSelector<SensorReading>() {
    @Override
    public Iterable<String> select(SensorReading value) {
        return (value.getTemperature() > 30) ? Collections.singletonList("high") : Collections.singletonList("low");
    }
});

splitStream.select("high").print("high>>>>>");
splitStream.select("low").print("low>>>>>");

Connect和CoMap

只能连接两条流,但是数据类型可以不一样

image-20220215090653367

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

image-20220215090756425

CoMap,CoFlatMap:ConnectedStream->DataStream,作用于ConnectedStream上,功能与map和flatmap一样,对ConnectedStream中的每一个stream分别进行map和flatmap处理

DataStream<Tuple2<String, Double>> warningStream = dataStream1.map(new MapFunction<SensorReading, Tuple2<String, Double>>() {
    @Override
    public Tuple2<String, Double> map(SensorReading sensorReading) throws Exception {
        return new Tuple2<>(sensorReading.getId(), sensorReading.getTemperature());
    }
});
ConnectedStreams<Tuple2<String, Double>, SensorReading> connectedStreams = warningStream.connect(dataStream2);

DataStream<Object> resultStream = connectedStreams.map(new CoMapFunction<Tuple2<String, Double>, SensorReading, Object>() {
    @Override
    public Object map1(Tuple2<String, Double> value) throws Exception {
        return new Tuple3<>(value.f0, value.f1, "high temp warning");
    }

    @Override
    public Object map2(SensorReading value) throws Exception {
        return new Tuple2<>(value.getId(), "normal");
    }
});

Union

可以连接多条流,但是数据类型必须一样

image-20220215090859064

DataStream->DataStream:对两个或者两个以上的DataStream进行union操作,产生一个包含所有DataStream元素的新DataStream

DataStream<SensorReading> dataStream1 = inputStream.map(line -> {
    String[] fields = line.split(",");
    return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
});

DataStream<SensorReading> dataStream2 = inputStream.map(line -> {
    String[] fields = line.split(",");
    return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
});

DataStream<SensorReading> dataStream3 = inputStream.map(line -> {
    String[] fields = line.split(",");
    return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
});

// 合流 union
DataStream<SensorReading> unionStream = dataStream1.union(dataStream2, dataStream3);

ProcessFunction

image-20220215154924135

Flink提供了8个ProcessFunction,以KeyedProcessFunction为例说明。KeyedProcessFunction用来操作KeyedStream。KeyedProcessFunction会处理流的每一个元素,输出为0个、1个或者多个元素。所有的ProcessFunction都继承自RichFunction接口,所以都有open()、close()和getRuntimeContext()等方法。而KeyedProcessFunction还额外提供了两个方法processElement和onTimer

processElement(I value, Context ctx, Collector out):流中的每一个元素都会调用这个方法,调用结果将会放在Collector数据类型中输出。Context 可以访问元素的时间戳,元素的key,以及TimerService时间服务。Context还可以将结果输出到别的流

@Override
public void processElement(SensorReading value, KeyedProcessFunction<Tuple, SensorReading, Integer>.Context ctx, Collector<Integer> out) throws Exception {
    out.collect(value.getId().length());
    // context
    ctx.timestamp();
    ctx.getCurrentKey();
    ctx.timerService().currentProcessingTime();
    ctx.timerService().currentWatermark();
    ctx.timerService().registerProcessingTimeTimer(10000L);

    TimerService timerService = ctx.timerService();
}

onTimer(long timestamp, OnTimerContext ctx, Collector out):是一个回调函数。当之前注册的定时器触发时调用。参数timestamp为定时器所设定的触发时间戳。Collector为输出结果的集合,OnTimerContext 和processElement的Context参数一样,提供了上下文的一些信息,如定时器触发的时间信息(事件时间或者处理时间)

@Override
public void onTimer(long timestamp, KeyedProcessFunction<Tuple, SensorReading, Integer>.OnTimerContext ctx, Collector<Integer> out) throws Exception {
    System.out.println(timestamp + "定时器触发");
}

TimerService和定时器Timers

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

image-20220215160111789

currentProcessingTime:返回当前的处理时间
currentWatermark:返回当前watermark的时间戳
deleteEventTimeTimer:删除之前注册的事件时间定时器,如果没有此时间戳的定时器,则不执行
deleteProcessingTimeTimer:删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行
registerEventTimeTimer:会注册当前key的eventtime定时器。当水位线大于等于定时器的注册时间时,触发定时器执行回调函数
registerProcessingTimeTimer:会注册当前key的processtime的定时器。当水位线大于等于定时器的注册时间时,触发定时器执行回调函数

侧输出流SideOutput

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

// 定义一个OutputTag 用来表示测输出低温流
OutputTag<SensorReading> lowTempTag = new OutputTag<SensorReading>("lowTemp") {

};
// 自定义测输出流实现分流操作
SingleOutputStreamOperator<SensorReading> higtTempStream = dataStream.keyBy("id").process(new ProcessFunction<SensorReading, SensorReading>() {
    @Override
    public void processElement(SensorReading value, ProcessFunction<SensorReading, SensorReading>.Context ctx, Collector<SensorReading> out) throws Exception {
        // 判断大于30高温主流小于低温测输出流
        if (value.getTemperature() > 30) {
            out.collect(value);
        } else {
            ctx.output(lowTempTag, value);
        }
    }
});
higtTempStream.print("high-temp>>>>>");
higtTempStream.getSideOutput(lowTempTag).print("low-temp>>>>>");

Flink支持的数据类型

Flink流应用程序处理的是以数据对象表示的事件流,所以在Flink内部需要能够处理这些对象,它们就需要被序列化和反序列化。因此Flink需要明确知道应用程序所处理的数据类型。Flink使用类型信息的概念来表示数据类型,并为每个数据类型生成特定的序列化器、反序列化器、比较器。Flink还具有一个类型提取系统,该系统分析函数的输入和返回类型,以自动获取类型信息,从而获得序列化器和反序列化器。但是,在某些情况下,例如lambda 函数或泛型类型,需要显式地提供类型信息,才能使应用程序正常工作或提高其性能

Flink支持所有的Java和Scala基础数据类型:Int,Double,Long,String。以及Arrays、List、Maps、Tuples元组等。

可以通过BasicTypeInfo和Types指定复杂的类型

MapStateDescriptor<String, List<TableHandle>> mapStateDescriptor = new MapStateDescriptor<>("map-state", BasicTypeInfo.STRING_TYPE_INFO, Types.LIST(Types.POJO(TableHandle.class)));

实现UDF函数-更细粒度的控制流

Flink暴露了所有udf函数的接口(实现方式为接口或抽象类),例如MapFunction、FilterFunction、ProcessFunction等等

实现接口或者抽象方法的方式

public static class MyFlatMapper implements FlatMapFunction<String, Tuple2<String, Integer>> {
    @Override
    public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
        String[] words = s.split(" ");
        for (String word : words) {
            collector.collect(new Tuple2<>(word, 1));
        }
    }
}

匿名类方式实现

DataStream<String> filterStream = inputStream.filter(new FilterFunction<String>() {
    @Override
    public boolean filter(String s) throws Exception {
        return s.startsWith("sensor_1");
    }
});

匿名函数方式实现

DataStream<SensorReading> dataStream = inputStream.map(line -> {
    String[] fields = line.split(",");
    return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
});

富函数

富函数是DataStreamAPI提供的一个函数类的接口,所有Flink函数类都有其Rich版本,它与常规函数不同在于可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。

  • RichMapFunction
  • RichFlatMapFunction
  • RichFilterFunction

RichFunction有一个生命周期的概念,典型的生命周期方法有:

  • open方法是richFunction的初始化方法,当一个算子如map或者filter被调用之前open方法会被调用
  • close方法是生命周期中的最后一个调用的方法,做一些清理操作
  • getRuntimeContext()方法提供了函数的RuntimeContext的一些信息,例如函数执行的并行度,任务的名字,以及state状态
public static class MyMapper extends RichMapFunction<SensorReading, Tuple2<String, Integer>> {

    @Override
    public void open(Configuration parameters) throws Exception {
        // 初始化工作,定义状态 或者建立连接
        System.out.println("open");
    }

    @Override
    public void close() throws Exception {
        // 关闭连接 清空状态 收尾工作
        System.out.println("close");
    }

    @Override
    public Tuple2<String, Integer> map(SensorReading value) throws Exception {
        return new Tuple2<>(value.getId(), getRuntimeContext().getIndexOfThisSubtask());
    }
}

Sink

官方提供了一部分的框架sink,其他用于Flink的流连接器正在通过Apache Bahir发布,除此之外,需要用户自定义实现sink

image-20220215093237617

image-20220215093545913

stream.addSink(new MySink(xxxx)){}

KafkaSink

引入依赖

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka_2.12</artifactId>
    <version>1.12.0</version>
</dependency>

写入kafka

dataStream.addSink(new FlinkKafkaProducer<String>("hadoop102:9092", "kafkaSink", new SimpleStringSchema()));

RedisSink

引入依赖

<dependency>
    <groupId>org.apache.bahir</groupId>
    <artifactId>flink-connector-redis_2.11</artifactId>
    <version>1.0</version>
</dependency>

写入redis

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

    // 从文件读取数据
    DataStreamSource<String> inputStream = env.readTextFile("E:\\java\\flink\\src\\main\\resources\\sensor.txt");

    // 转化成SensorReading
    DataStream<SensorReading> dataStream = inputStream.map(line -> {
        String[] fields = line.split(",");
        return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
    });

    // 定义jedis连接配置
    FlinkJedisPoolConfig config = new FlinkJedisPoolConfig.Builder()
            .setHost("hadopp102")
            .setPort(6379)
            .build();

    // 写入redis
    dataStream.addSink(new org.apache.flink.streaming.connectors.redis.RedisSink<>(config, new MyRedisMapper()));


    env.execute("KafkaSink");
}

// 自定义RedisMapper
public static class MyRedisMapper implements RedisMapper<SensorReading> {

    // 定义保存数据到redis的命令,存成hash表 hset sensor_temp id temperature
    @Override
    public RedisCommandDescription getCommandDescription() {
        return new RedisCommandDescription(RedisCommand.HSET, "sensor_temp");
    }

    @Override
    public String getKeyFromData(SensorReading sensorReading) {
        return sensorReading.getId();
    }

    @Override
    public String getValueFromData(SensorReading sensorReading) {
        return sensorReading.getTemperature().toString();
    }
}

ElasticSearchSink

引入依赖

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-elasticsearch7_2.11</artifactId>
    <version>1.13.0</version>
</dependency>

写入es

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

    // 从文件读取数据
    DataStreamSource<String> inputStream = env.readTextFile("E:\\java\\flink\\src\\main\\resources\\sensor.txt");

    // 转化成SensorReading
    DataStream<SensorReading> dataStream = inputStream.map(line -> {
        String[] fields = line.split(",");
        return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
    });

    // 定义es连接配置
    ArrayList<HttpHost> httpHosts = new ArrayList<>();
    httpHosts.add(new HttpHost("localhost", 9200));

    dataStream.addSink(new ElasticsearchSink.Builder<SensorReading>(httpHosts, new MyEsSinkFunction()).build());

    env.execute("ESSink");
}

public static class MyEsSinkFunction implements ElasticsearchSinkFunction<SensorReading> {

    @Override
    public void process(SensorReading sensorReading, RuntimeContext runtimeContext, RequestIndexer requestIndexer) {
        // 定义写入的数据source
        HashMap<String, String> dataSource = new HashMap<>();
        dataSource.put("id", sensorReading.getId());
        dataSource.put("temp", sensorReading.getTemperature().toString());
        dataSource.put("ts", sensorReading.getTimestamp().toString());

        // 创建请求 写入es
        IndexRequest indexRequest = Requests.indexRequest().index("sensor").source(dataSource);

        // 用index发送请求
        requestIndexer.add(indexRequest);
    }
}

JDBC自定义sink

引入依赖

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

写入mysql

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

    // 从文件读取数据
    DataStreamSource<String> inputStream = env.readTextFile("E:\\java\\flink\\src\\main\\resources\\sensor.txt");

    // 转化成SensorReading
    DataStream<SensorReading> dataStream = inputStream.map(line -> {
        String[] fields = line.split(",");
        return new SensorReading(fields[0], new Long(fields[1]), new Double(fields[2]));
    });

    dataStream.addSink(new MyJdbcSink());

    env.execute("JdbcSink");
}

public static class MyJdbcSink extends RichSinkFunction<SensorReading> {
    // 声明连接和预编译sql语句
    private Connection connection = null;
    private PreparedStatement insertStmt = null;
    private PreparedStatement updateStmt = null;

    // 初始化
    @Override
    public void open(Configuration parameters) throws Exception {
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
        insertStmt = connection.prepareStatement("insert into sensor_tmp(id,temp)values(?,?)");
        updateStmt = connection.prepareStatement("update sensor_tmp set temp=? where id=?");
    }

    // 每来一条数据,调用连接,执行sql
    @Override
    public void invoke(SensorReading value, Context context) throws Exception {
        // 直接执行更新语句,如果没有更新那么就插入
        updateStmt.setDouble(1, value.getTemperature());
        updateStmt.setString(2, value.getId());
        updateStmt.execute();
        if (updateStmt.getUpdateCount() == 0) {
            insertStmt.setString(1, value.getId());
            insertStmt.setDouble(2, value.getTemperature());
            insertStmt.execute();
        }
    }

    // 关闭连接
    @Override
    public void close() throws Exception {
        insertStmt.close();
        updateStmt.close();
        connection.close();
    }
}

Flink-窗口window

window概念

streaming流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而window是一种切割无限数据为有限块进行处理的手段。Window是无限数据流处理的核心,Window将一个无限的stream拆分成有限大小的buckets桶,我们可以在这些桶上做计算操作.

Keyed vs Non-Keyed Windows

在键控流的情况下,传入事件的任何属性都可以用作键。拥有一个键控流将允许您的窗口计算被多个任务并行执行,因为每个逻辑键控流可以独立于其他处理。所有指向相同键的元素将被发送到相同的并行任务。在非键流的情况下,你的原始流将不会被分割成多个逻辑流,所有的窗口逻辑将由一个任务执行,即并行度为1

window类型

window可以分为两类:时间窗口(timewindow)和计数窗口(countwindow)。对于TimeWindow,可以根据窗口实现原理的不同分成三类:滚动窗口(Tumbling Window)、滑动窗口(SlidingWindow)和会话窗口(SessionWindow)

滚动窗口-Tumbling Window

将数据依据固定的窗口长度对数据进行切片,滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠

image-20220215095053803

特点:时间对齐,窗口长度固定,没有重叠,适用场景:适合做BI统计等(做每个时间段的聚合计算)

滑动窗口-SlidingWindow

滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动间隔组成。滑动窗口分配器将元素分配到固定长度的窗口中,与滚动窗口类似,窗口的大小由窗口大小参数来配置,另一个窗口滑动参数控制滑动窗口开始的频率。因此,滑动窗口如果滑动参数小于窗口大小的话,窗口是可以重叠的,在这种情况下元素会被分配到多个窗口中

image-20220215095434636

特点:时间对齐,窗口长度固定,可以有重叠,适用场景:对最近一个时间段内的统计(求某接口最近5min的失败率)

会话窗口

由一系列事件组合一个指定时间长度的timeout间隙组成,类似于web应用的session,也就是一段时间没有接收到新数据就会生成新的窗口。session窗口分配器通过session活动来对元素进行分组,session窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间的情况,相反,当它在一个固定的时间周期内不再收到元素,即非活动间隔产生,那个这个窗口就会关闭。一个session窗口通过一个session间隔来配置,这个session间隔定义了非活跃周期的长度,当这个非活跃周期产生,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去

image-20220215095703578

特点:时间无对齐

window API

TimeWindow

TimeWindow是将指定时间范围内的所有数据组成一个window,一次对一个window里面的所有数据进行计算

滚动窗口:Flink默认的时间窗口根据ProcessingTime进行窗口的划分,将Flink获取到的数据根据进入Flink的时间划分到不同的窗口中。时间间隔可以通过Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等其中的一个来指定

dataStream.map(new MapFunction<SensorReading, Tuple2<String, Double>>() {
    @Override
    public Tuple2<String, Double> map(SensorReading value) throws Exception {
        return new Tuple2<>(value.getId(), value.getTemperature());
    }
}).keyBy(data -> data.f0).timeWindow(Time.seconds(15));

滑动窗口:滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size

// 5秒钟计算一次,每一次计算15s内的所有元素
dataStream.map(new MapFunction<SensorReading, Tuple2<String, Double>>() {
    @Override
    public Tuple2<String, Double> map(SensorReading value) throws Exception {
        return new Tuple2<>(value.getId(), value.getTemperature());
    }
}).keyBy(data -> data.f0).timeWindow(Time.seconds(15),Time.seconds(5));

CountWindow

CountWindow根据窗口中相同key元素的数量来触发执行,执行时只计算元素数量达到窗口大小的key对应的结果。CountWindow的window_size指的是相同Key的元素的个数不是输入的所有元素的总数

滚动窗口:默认的CountWindow是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行

dataStream.map(new MapFunction<SensorReading, Tuple2<String, Double>>() {
    @Override
    public Tuple2<String, Double> map(SensorReading value) throws Exception {
        return new Tuple2<>(value.getId(), value.getTemperature());
    }
}).keyBy(data -> data.f0).countWindow(5).minBy("temperature");

滑动窗口:滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size

// 每收到两个相同key的数据就计算一次,每一次计算的window范围是10个元素
dataStream.map(new MapFunction<SensorReading, Tuple2<String, Double>>() {
    @Override
    public Tuple2<String, Double> map(SensorReading value) throws Exception {
        return new Tuple2<>(value.getId(), value.getTemperature());
    }
}).keyBy(data -> data.f0).countWindow(5, 2).minBy("temperature");

窗口函数

window function定义了要对窗口中收集的数据做的计算操作。主要可以分为两类:增量聚合函数,全窗口函数

input
    .keyBy(<key selector>)
    .window(SlidingProcessingTimeWindows.of(Time.hours(12), Time.hours(1), Time.hours(-8)))
    .<windowed transformation>(<window function>);

增量聚合函数

每条数据到来就进行计算,保持一个简单的状态,增量聚合函数有:ReduceFunction、AggregateFunction

ReduceFunction指定输入中的两个元素如何组合生成相同类型的输出元素。Flink使用ReduceFunction递增地聚合窗口的元素

AggregateFunction是ReduceFunction的一般化版本,它有三种类型:输入类型(IN)、累加类型(ACC)和输出类型(OUT)。输入类型是输入流中元素的类型,AggregateFunction有一个将一个输入元素添加到累加器的方法。该接口还提供了创建初始累加器、将两个累加器合并到一个累加器以及从累加器提取输出(类型为OUT)的方法。与ReduceFunction一样,Flink会在一个窗口的输入元素到达时递增地聚合它们

DataStream<Integer> resultStream = dataStream.keyBy(SensorReading::getId).window(TumblingProcessingTimeWindows.of(Time.seconds(15)))
        .aggregate(new AggregateFunction<SensorReading, Integer, Integer>() {

            // 创建累加器
            @Override
            public Integer createAccumulator() {
                return 0;
            }
java
            @Override
            public Integer add(SensorReading sensorReading, Integer integer) {

                return integer + 1;
            }

            @Override
            public Integer getResult(Integer integer) {
                return integer;
            }

            @Override
            public Integer merge(Integer integer, Integer acc1) {
                return integer + acc1;
            }
        });

全窗口函数-ProcessWindowFunction

一个ProcessWindowFunction获得一个包含窗口所有元素的Iterable,以及一个可以访问时间和状态信息的Context对象,这使得它比其他窗口函数提供了更多的灵活性。这是以性能和资源消耗为代价的,因为元素不能增量地聚合,而是需要在内部缓冲,直到认为窗口可以处理为止

先把窗口所有数据收集起来,等到计算的时候会遍历所有数据,全窗口函数有:ProcessWindowFunction、WindowFunction

DataStream<Integer> resultStream = dataStream.keyBy("id").timeWindow(Time.seconds(15)).apply(new WindowFunction<SensorReading, Integer, Tuple, TimeWindow>() {
    @Override
    public void apply(Tuple tuple, TimeWindow timeWindow, Iterable<SensorReading> iterable, Collector<Integer> collector) throws Exception {
        Integer count = IteratorUtils.toList(iterable.iterator()).size();
        collector.collect(count);
    }
});

对于简单的聚合(如count)使用ProcessWindowFunction是非常低效的

ProcessWindowFunction可以与ReduceFunction或AggregateFunction结合在一起,在元素到达窗口时进行增量聚合。当窗口关闭时,ProcessWindowFunction将提供聚合的结果。这允许它在访问ProcessWindowFunction的附加窗口元信息的同时,递增地计算窗口。

image-20220215103858548

 .reduce(new MyReduceFunction(), new MyProcessWindowFunction());

private static class AverageAggregate implements AggregateFunction<Tuple2<String, Long>, Tuple2<Long, Long>, Double> {
  @Override
  public Tuple2<Long, Long> createAccumulator() {
    return new Tuple2<>(0L, 0L);
  }

  @Override
  public Tuple2<Long, Long> add(Tuple2<String, Long> value, Tuple2<Long, Long> accumulator) {
    return new Tuple2<>(accumulator.f0 + value.f1, accumulator.f1 + 1L);
  }

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

  @Override
  public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) {
    return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
  }
}

private static class MyProcessWindowFunction extends ProcessWindowFunction<Double, Tuple2<String, Double>, String, TimeWindow> {
  public void process(String key,
                    Context context,
                    Iterable<Double> averages,
                    Collector<Tuple2<String, Double>> out) {
      Double average = averages.iterator().next();
      out.collect(new Tuple2<>(key, average));
  }
}

其他可选API

  • tigger-触发器,定义window什么时候关闭,触发计算并输出结果
  • evictor-移除器,定义移除某些数据的逻辑
  • allowedLateness-允许处理迟到的数据
  • sideOutPutLateData-将迟到的数据放入侧输出流
  • getSideOutPut-获取侧输出流

image-20220215102241372

Flink时间语义和WaterMark

image-20220215104450690

EventTime:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink通过时间戳分配器访问事件时间戳

IngestionTime:是数据进入Flink的时间

ProcessingTime:是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是ProcessingTime

WaterMark概述

流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的,虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就是指Flink接收到的事件的先后顺序不是严格按照事件的Event Time顺序排列的

image-20220215144619925

那么此时出现一个问题,一旦出现乱序,如果只根据eventTime决定window的运行,我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了,这个特别的机制,就是Watermark

WaterMark概念

WaterMark是一种衡量EventTime进展的机制

WaterMark是用于处理乱序事件的,而正确的处理乱序事件,通常用WaterMark机制结合window来实现

数据流中的WaterMark用于表示timestamp小于WaterMark的数据,都已经到达了。因此,window的执行也是由WaterMark触发的

WaterMark可以理解成一个延时触发机制,可以通过设置WaterMark的延时时长t,每次系统会校验已经到达的数据中最大的maxEventTime,然后认定eventTime小于maxEventTime-t的数据都已经到达。如果有窗口的停止时间等于maxEventTime-t,那么这个窗口被触发执行

image-20220215145310708

当Flink接收到数据时,会按照一定的规则去生成Watermark,这条Watermark就等于当前所有到达数据中的maxEventTime- 延迟时长,也就是说,Watermark是基于数据携带的时间戳生成的,一旦Watermark比当前未触发的窗口的停止时间要晚,那么就会触发相应窗口的执行。由于eventTime是由数据携带的,因此如果运行过程中无法获取新的数据,那么没有被触发的窗口将永远都不被触发

上图中,设置的允许最大延迟到达时间为 2s,所以时间戳为7s的事件对应的Watermark是5s,时间戳为12s的事件的Watermark是10s,如果我们的窗口1是1s-5s,窗口2是6s-10s,那么时间戳为7s的事件到达时的Watermarker恰好触发窗口1,时间戳为12s的事件到达时的Watermark恰好触发窗口2。

Watermark就是触发前一窗口的关窗时间,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。 只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗

WaterMark的引入

乱序数据,设置时间时间和WaterMark时间戳

// 以数据中Timestamp字段时间为事件时间,最大延迟为2s
dataStream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<SensorReading>(Time.seconds(2)) {
    @Override
    public long extractTimestamp(SensorReading element) {
        return element.getTimestamp() * 1000L;
    }
});

升序数据,设置时间时间和WaterMark时间戳

// 以数据中Timestamp字段时间为事件时间,最大延迟为2s
dataStream.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<SensorReading>() {
    @Override
    public long extractAscendingTimestamp(SensorReading element) {
        return element.getTimestamp() * 1000L;
    }
});

image-20220215150045739

Flink暴露了TimestampAssigner接口供我们实现,使我们可以自定义如何从事件数据中抽取时间戳。TimestampAssigner有两种类型AssignerWithPeriodicWatermarks和AssignerWithPunctuatedWatermarks

AssignerWithPeriodicWatermarks-具有周期性水印的赋值器

周期性的生成WaterMark:系统会周期性的将WaterMark插入到流中(水位线也是一种特殊的事件)。默认周期是200毫秒,可以进行自定义设置

// 每隔5秒产生一个watermark
env.getConfig.setAutoWatermarkInterval(5000);

产生WaterMark的逻辑:每隔5秒钟,Flink会调用AssignerWithPeriodicWatermarks的getCurrentWatermark方法。如果方法返回一个时间戳大于之前水位的时间戳,新的WaterMark会被插入到流中。这个检查保证了水位线是单调递增的,如果方法返回的时间戳小于等于之前水位的时间戳,则不会产生新的WaterMark

public class MyPeriodicAssigner implements AssignerWithPeriodicWatermarks<SensorReading> {

    // 延时一分钟
    private Long bound = 60 * 1000L;

    // 当前最大时间戳
    private Long maxTs = Long.MIN_VALUE;

    @Nullable
    @Override
    public Watermark getCurrentWatermark() {
        return new Watermark(maxTs - bound);
    }

    @Override
    public long extractTimestamp(SensorReading element, long previousElementTimestamp) {
        maxTs = Math.max(maxTs, element.getTimestamp());
        return element.getTimestamp();
    }
}

AssignerWithPunctuatedWatermarks-具有间接性水印的赋值器

间断式地生成watermark,和周期性生成的方式不同。这种方式不是固定时间的, 而是可以根据需要对每条数据进行筛选和处理

// 只给sensor_1的数据流插入watermark
public class MyPunctuatedAssigner implements AssignerWithPunctuatedWatermarks<SensorReading> {

    // 延迟一分钟
    private Long bound = 60 * 1000L;

    @Nullable
    @Override
    public Watermark checkAndGetNextWatermark(SensorReading lastElement, long extractedTimestamp) {
        if ("sensor_1".equals(lastElement.getId())) {
            return new Watermark(extractedTimestamp - bound);
        } else {
            return null;
        }
    }

    @Override
    public long extractTimestamp(SensorReading element, long previousElementTimestamp) {
        return element.getTimestamp();
    }
}

状态编程和容错机制

在Flink中有两种状态:算子状态(operatorstate)、键控状态(keyedstate)

算子状态-operatorstate

算子状态的作用范围限定为算子任务。这意味着由同一并行任务所处理的所有数据都可以访问到相同的状态,状态对于同一任务而言是共享的。算子状态不能由先相同或不同算子的另一个任务访问

image-20220215233057967

Flinik为算子状态提供三种基本数据结构:列表状态(ListState)、联合列表状态(UnionListState)、广播状态(BroadcastState)

列表状态:将状态表示为一组数据的列表

联合列表状态:也将状态表示为数据的列表。它与常规列表状态的区别在于,在故障发生时,或者从保存点启动程序时如何恢复

广播状态:如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种场景适合应用广播状态

键控状态-keyedstate

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

image-20220215233913457

Flinik为监控状态提供:ValueState、ListState、MapState、ReducingState、AggregatingState类型

ValueState:保存单个值,值的类型为泛型T

// get 操作
ValueState.value()
// set操作
ValueState.update(T value)

ListState:保存一个列表,列表里的元素的数据类型为T

// 添加值
ListState.add(T value)
// 批量添加值    
ListState.addAll(List<T> values)
// 获取值,返回Iterable<T>
ListState.get()
// 更新
ListState.update(List<T> values)

MapState:保存Key-Value对

// 获取值
MapState.get(UK key)
// 添加值
MapState.put(UK key,UV value)
// 是否包含
MapState.contains(UK key)
// 删除
MapState.remove(Uk key)
// 状态使用实例
public static class MyKeyCountMapper extends RichMapFunction<SensorReading, Integer> {
    private ValueState<Integer> keyCountState;

    // 其他类型状态的声明
    private ListState<String> myListState;
    private MapState<String, Double> myMapState;
    private ReducingState<SensorReading> myReducingState;

    @Override
    public void open(Configuration parameters) throws Exception {
        keyCountState = getRuntimeContext().getState(new ValueStateDescriptor<Integer>("key-count", Integer.class));

        myListState = getRuntimeContext().getListState(new ListStateDescriptor<String>("my-list", String.class));

        myMapState = getRuntimeContext().getMapState(new MapStateDescriptor<String, Double>("my-map", String.class, Double.class));

        myReducingState = getRuntimeContext().getReducingState(new ReducingStateDescriptor<SensorReading>("my-reducing", new ReduceFunction<SensorReading>() {
            @Override
            public SensorReading reduce(SensorReading value1, SensorReading value2) throws Exception {

            }
        }), SensorReading.class);
    }

    @Override
    public Integer map(SensorReading value) throws Exception {
        Integer count = keyCountState.value();
        count++;
        keyCountState.update(count);

        // 其他状态API调用
        myListState.add("hello");
        for (String s : myListState.get()) {
            System.out.println(s);
        }

        myMapState.get("1");
        myMapState.put("2", 12.3);

        myReducingState.add(value);

        myReducingState.clear();

        return count;
    }
}

状态一致性

在流处理过程中,一致性可以分为3个级别:at-most-once、at-least-once、exactly-once

at-most-once:没有正确性保障的委婉说法,故障发生后,技术结果可能丢失。同样的还有udp

at-least-once:技术结果可能大于正确值,但绝不会小于正确值。也就是说,技术程序在发生故障之后可能多算但是不会少算

exactly-once:保证技术结果在发生故障之后仍然和正确值一制

端到端(end-to-end)状态一致性

端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一 致性最弱的组件。

内部保证——依赖checkpoint

source端 ——需要外部源可重设数据的读取位置

sink端——需要保证从故障恢复时,数据不会重复写入外部系统而对于sink端,又有两种具体的实现方式:幂等写入和事务性写入

幂等写入:所谓幂等操作,是说一个操作可以重复执行很多次但只导致一次结果更改, 也就是说后面再重复执行就不起作用了

事务写入:需要构建事务来写入外部系统,构建的事务对应着checkpoint,等到checkpoint真正完成的时候,才把所有对应的结果写入sink系统中。事务性写入有两种实现方式:预写日志WAL和两阶段提交2PC,DataStreamAPI提供了GenericWriteAheadSink模板类和TwoPhaseCommitSinkFunction接口,可以方便地实现这两种方式的事务性写入

状态后端

MemoryStateBackend:内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在TaskManager的JVM堆上;而将checkpoint存储在JobManager的内存中

FsStateBackend:将checkpoint存到远程的持久化文件系统上。而对于本地状态,跟MemoryStateBackend一样,也会存在TaskManager的JVM堆上

RocksDBStateBackend:将所有状态序列化后,存入本地的RocksDB中存储。RocksDB的支持并不直接包含在flink中,需要引入依赖

<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-statebackend-rocksdb_2.12</artifactId>
 <version>1.14.3</version>
</dependency>
public static void main(String[] args) {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.setParallelism(1);

    // 状态后端配置
    env.setStateBackend(new FsStateBackend(""));

    // 检查点配置
    env.enableCheckpointing(1000);
    // AT-MOST-ONCE(最多一次)当任务故障时,什么都不干,既不恢复丢失的状态,也不重播丢失的数据
    // AT-LEAST-ONCE(最少一次) 不丢失事件 这种类型的保障称为 at-least-once 所有的事件都得到了处理,而一些事件还可能被处理多次
    // EXACTLY-ONCE(精确一次)恰好处理一次语义不仅仅意味着没有事件丢失,还意味着针对每一个数据,内部状态仅仅更新一次
    env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
    env.getCheckpointConfig().setCheckpointTimeout(60000);
    env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
    env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
    env.getCheckpointConfig().setPreferCheckpointForRecovery(false);
    env.getCheckpointConfig().setTolerableCheckpointFailureNumber(0);

    // 重启策略配置

    // 固定延迟重启 隔一段时间重试启动一次 (重试次数,尝试重试的时间间隔,也可 org.apache.flink.api.common.time.Time)
    env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, 100000));

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值