flink学习日记2-DatastreamAPI(一)

31 阅读7分钟

DataStreamAPI

了解

DataStream类用于表示flink程序中的数据集合,集合中的数据可重复、不可变、不可直接查看。只能通过API的转换操作来处理数据如比如 map、filter、keyBy、window 等操作,由于集合数据的不可变,每次操作都会生成一个新的流而不是直接改变原来的流数据。

执行环境 StreamExecutionEnvironment

  • StreamExecutionEnvironment 是所有 Flink 程序的基础,是处理程序的入口。

createLocalEnvironment

手动创建一个本地环境,程序运行时启动一个mini集群。

createRemoteEnvironment

手动提交代码到远程 JobManager,一般需要指定主机名、端口、提交的jar包

getExecutionEnvironment

  • 最常用

  • 自动根据上下文选择执行模式:

    • 在 IDE 本地运行 —— 创建 Local 环境
    • 提交到 Flink 集群(如 Yarn、K8s)—— 创建 集群环境
  • 可以传入configuration一键配置

触发执行execute

  • flink的代码是声明式的,需要调用StreamExecutionEnvironmentexecute() 方法来触发程序执行

  • execute方法之前的所有代码,只是定义了每个作业要执行的操作,由此构建一个数据处理流程图(DAG)

  • execute方法将一直等待作业完成,然后返回一个执行结果JobExecutionResult

  • 为什么flink程序采用这种模式?

    • 全局优化:延迟执行允许flink知道整个作业结构并进行算子链、并行度、网络传输优化。
    • 分布式系统的任务调度:为了能在多台机器上协同工作,flink必须能提前了解整个作业结构并进行调度。
    • 实时流处理的容错机制:flink必须先构建完整的任务拓扑结构才能正确构建容错机制
  • 异步executeAsync

    • 不会阻塞主线程
    • 会返回一个 JobClient,你可以通过它与刚刚提交的作业进行通信。

源算子Source

  • Source 是flink程序从数据源读取数据的算子,是数据流的起点。
  • 决定输入数据的格式、时间戳和水位线等

flink对pojo类的识别

  • public
  • 有public的无参构造函数
  • 字段必须是public或有getter/setter
  • 字段必须是可序列化的
  • 不使用final修饰字段

官方内置源

  • 从java集合读取
DataStreamSource<Integer> ds = env.fromCollection(Arrays.asList(1, 2, 3, 4));
  • fromElement可以不创建数组直接填写元素
DataStreamSource<Integer> ds = env.fromElements(1,2,3,4);
  • 从socket读
DataStreamSource<String> ds = env.socketTextStream("hostname", port);

从文件读

  • 过时用法,在flink较新版本中不能使用
DataStreamSource<String> ds = env.readTextFile("path//to//file");
  • 使用FileSource

    • 导入flink-connector-files
    • env.fromSource(Source类,水位线,数据源名称)
    • FileSource<String> fileSource = FileSource.forRecordStreamFormat(new TextLineInputFormat(), new Path("inputs//words.txt")).build();
      DataStreamSource<String> ds = env.fromSource(fileSource, WatermarkStrategy.noWatermarks(), "file_source");
      

从kafka读

  • 导入flink-connector-kafka
  • 使用KafkaSource设置源信息并创建数据源
  • 注:flink的消费策略earliest/latest是一定从最早/最晚进行消费
KafkaSource<String> test01 = KafkaSource.<String>builder()
        .setBootstrapServers("192.168.64.128:9092,192.168.64.128:9192,192.168.64.128:9292")
        .setGroupId("test01")
        .setTopics("quickstart-events")
        .setValueOnlyDeserializer(new SimpleStringSchema())
        .setStartingOffsets(OffsetsInitializer.earliest())
        .build();
  • 使用env.fromSource指定数据源
DataStreamSource<String> test_01 = env.fromSource(test01, WatermarkStrategy.noWatermarks(), "test_01");

基本转换算子

map

读入一条数据,转换成一个新的数据流。

一个输入——》一个输出

SingleOutputStreamOperator<String> mapped = ds.map(new RichMapFunction<WaterSensor, String>() {
    @Override
    public String map(WaterSensor waterSensor) throws Exception {
        return waterSensor.getId();
    }
});
//SingleOutputStreamOperator<String> mapped = ds.map(waterSensor -> waterSensor.getId());
mapped.print();

filter

保留满足布尔条件表达式的数据

SingleOutputStreamOperator<WaterSensor> filtered = ds.filter(new FilterFunction<WaterSensor>() {
    @Override
    public boolean filter(WaterSensor waterSensor) throws Exception {
        return waterSensor.getId().equals("1");
    }
});
filtered.print();

flatmap

  • 又称为扁平映射,主要是将数据流中的整体展开成一个个的个体使用。
  • collect方法将处理好的数据形成一个新的流并传递到下游
  • 一个输入——》多个输出
SingleOutputStreamOperator<String> flattedMap = ds.flatMap(new FlatMapFunction<WaterSensor, String>() {
    @Override
    public void flatMap(WaterSensor waterSensor, Collector<String> collector) throws Exception {
        if(waterSensor.getId().equals("1")){
            collector.collect(waterSensor.getVal().toString());
        }else if(waterSensor.getId().equals("2")){
            collector.collect(waterSensor.getTs().toString());
            collector.collect(waterSensor.getVal().toString());
        }
    }
});
flattedMap.print();

聚合算子

keyby

按key对数据进行分组。通过指定的键,将一条流在逻辑上划分成不同的分区(并行处理的子任务)

后续的窗口、聚合操作必须先进行 keyBy

KeyedStream<WaterSensor, String> keyed = ds.keyBy(new KeySelector<WaterSensor, String>() {
    @Override
    public String getKey(WaterSensor waterSensor) throws Exception {
        return waterSensor.getId();
    }
});

观察输出的分区编号,可以发现相同key的数据都在一个分区上

sum/min/max

KeyedStream<WaterSensor, String> keyed = ds.keyBy(new KeySelector<WaterSensor, String>() {
    @Override
    public String getKey(WaterSensor waterSensor) throws Exception {
        return waterSensor.getId();
    }
});
keyed.sum("val").print("sum_of_val");
//keyed.max("ts").print("max_of_ts");
//keyed.min("ts").print("min_of_ts");

minby/maxby

  • 与min/max的区别:min/max只会取比较字段的最大值,非比较字段只会保留第一次输入的值;而minby/maxby非比较字段保留比较字段取到最大值这一条数据所对应的值

reduce

  • 按key聚合
  • 第一条到达数据不会触发reduce方法
  • reduce(value1,value2)其中value1是上一次计算的结果
keyed.reduce(new ReduceFunction<WaterSensor>() {
    @Override
    public WaterSensor reduce(WaterSensor w1, WaterSensor w2) throws Exception {
        return new WaterSensor(w1.getId(),w2.getTs(),w1.getVal()+w2.getVal());
    }
}).print();

物理分区

决定上游算子输出的每条数据应该发送到哪个下游并行子任务的机制

随机分区shuffle

将元素随机地均匀划分到分区

ds.shuffle();

轮询rebalance

把数据平均分配到下游所有并行任务,可以避免热点,会跨 TaskManager

ds.rebalance();

本地轮询rescale

仅在本地将元素以轮询的方式分发到下游算子,不会跨机器发送(更高效)

广播broadcast

上游的每条数据发送给下游所有并行任务

ds.broadcast();

全局发送global

所有数据都只发送到下游的第 0 号子任务

ds.global();

自定义分区

通过编写方法实现Partitioner接口来编写自定义的分区方法

public class MyPartitioner implements Partitioner<String> {
    @Override
    public int partition(String key, int numPartitions) {
        return key.hashCode() % numPartitions;
    }
}
​
ds.partitionCustom( new MyPartitioner(),value -> value.key);

分流

filter

对于一条流编写多个filter可以简单实现分流的效果,但对于复杂的需求实现起来会相当麻烦,而且一条流需要经过多次判断会导致性能不佳。

ds.filter(s->Integer.parseInt(s)%2==0).print("even");
ds.filter(s->Integer.parseInt(s)%2>0).print("odd");

侧输出流output

  • 使用 OutputTag 来标识不同的侧输出流。
  • OutputTag tag = new OutputTag<>("tag"){}。object为侧输出流中的数据类型。即侧输出流可以支持不同类型。
    utputTag<WaterSensor> s1 = new OutputTag<>("s1"){};
    OutputTag<WaterSensor> s2 = new OutputTag<>("s2"){};
    
    • 使用ProcessFunction发送数据到侧输出流
    • 只有 ProcessFunction(及其子类)支持侧输出流
    SingleOutputStreamOperator<WaterSensor> processed = mapped.process(new ProcessFunction<WaterSensor, WaterSensor>() {
        @Override
        public void processElement(WaterSensor waterSensor, ProcessFunction<WaterSensor, WaterSensor>.Context context, Collector<WaterSensor> collector) throws Exception {
            if (waterSensor.getId().equals("s1")) context.output(s1, waterSensor);
            else if (waterSensor.getId().equals("s2")) context.output(s2, waterSensor);
            else collector.collect(waterSensor);
        }
    });
    
    • processed结果是主流的数据
    • 在processed结果上使用getSideOutput(OutputTag) 方法获取侧输出流
    processed.getSideOutput(s1).print("s1");
    processed.getSideOutput(s2).print("s2");
    

    合流

    union

    • 可以合并多条数据类型相同的流,对于不同数据类型的流需要转换成相同类型才能合并
    • 本质上是把多个upstream operator的输出拼接成一个输入序列
    • 不会改变数据分区策略
    • 下游算子类型必须支持多输入,下游算子决定最终并行关系
    DataStreamSource<Integer> odd = env.fromElements(1, 3, 5,7);
    DataStreamSource<Integer> even = env.fromElements(2, 4, 6, 8);
    DataStreamSource<String> text = env.fromElements("111", "222", "333");
    ​
    DataStream<Integer> nums = odd.union(even).union(text.map(s->Integer.valueOf(s)));
    nums.print();
    

    connect

    • 连接操作允许连接两条不同数据类型的流
    • 连接后不会把两条流混合,而是保持各自的数据形式不变,彼此独立(这在处理方法中可以体现)
    • 要想得到新的数据流,连接后还需要进行进一步处理
    ConnectedStreams<Integer, String> odd_text = odd.connect(text);
    SingleOutputStreamOperator<Integer> mapped = odd_text.map(new CoMapFunction<Integer, String, Integer>() {
        @Override
        public Integer map1(Integer integer) throws Exception {
            return integer;
        }
    ​
        @Override
        public Integer map2(String s) throws Exception {
            return Integer.valueOf(s);
        }
    });
    mapped.print();
    
    • 连接的两条流处理时如果操作涉及到对方(例如进行匹配),由于可能在不同的并行子任务中,需要先进行keyby,否则输出会出错
    DataStreamSource<Tuple2<Integer, String>> ds1 = env.fromElements(Tuple2.of(1, "001"), Tuple2.of(2, "002"));
    DataStreamSource<Tuple3<Integer, String, Integer>> ds2 = env.fromElements(Tuple3.of(1, "001", 1), Tuple3.of(2, "002", 2),Tuple3.of(1,"121",21));
    ​
    HashMap<Integer, List<String>> ds1_cache = new HashMap<>();
    HashMap<Integer, List<Tuple2<String, Integer>>> ds2_cache = new HashMap<>();
    ​
    ​
    ConnectedStreams<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>> connected = ds1.connect(ds2);
    ConnectedStreams<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>> keyed = connected.keyBy(new KeySelector<Tuple2<Integer, String>, Integer>() {
    ​
        @Override
        public Integer getKey(Tuple2<Integer, String> integerStringTuple2) throws Exception {
            return integerStringTuple2.f0;
        }
    }, new KeySelector<Tuple3<Integer, String, Integer>, Integer>() {
        @Override
        public Integer getKey(Tuple3<Integer, String, Integer> integerStringIntegerTuple3) throws Exception {
            return integerStringIntegerTuple3.f0;
        }
    });
    ​
    keyed.process(new CoProcessFunction<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>, Tuple3<Integer,String,Integer>>() {
        @Override
        public void processElement1(Tuple2<Integer, String> integerStringTuple2, CoProcessFunction<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>, Tuple3<Integer, String, Integer>>.Context context, Collector<Tuple3<Integer, String, Integer>> collector) throws Exception {
            if(!ds1_cache.containsKey(integerStringTuple2.f0))ds1_cache.put(integerStringTuple2.f0, new ArrayList<>());
            ds1_cache.get(integerStringTuple2.f0).add(integerStringTuple2.f1);
    ​
            if(ds2_cache.containsKey(integerStringTuple2.f0)){
                for (Tuple2<String, Integer> value : ds2_cache.get(integerStringTuple2.f0)) {
                    collector.collect(Tuple3.of(integerStringTuple2.f0,integerStringTuple2.f1+"<-->"+value.f0,value.f1));
                }
            }
        }
    ​
        @Override
        public void processElement2(Tuple3<Integer, String, Integer> integerStringIntegerTuple3, CoProcessFunction<Tuple2<Integer, String>, Tuple3<Integer, String, Integer>, Tuple3<Integer, String, Integer>>.Context context, Collector<Tuple3<Integer, String, Integer>> collector) throws Exception {
            if(!ds2_cache.containsKey(integerStringIntegerTuple3.f0))ds2_cache.put(integerStringIntegerTuple3.f0, new ArrayList<>());
            ds2_cache.get(integerStringIntegerTuple3.f0).add(Tuple2.of(integerStringIntegerTuple3.f1,integerStringIntegerTuple3.f2));
    ​
            if(ds1_cache.containsKey(integerStringIntegerTuple3.f0)){
                for (String s : ds1_cache.get(integerStringIntegerTuple3.f0)) {
                    collector.collect(Tuple3.of(integerStringIntegerTuple3.f0,s+"<-->"+integerStringIntegerTuple3.f1,integerStringIntegerTuple3.f2));
                }
            }
        }
    }).print();
    

    例如以上键值匹配的代码,如果移除keyby操作,最终输出的结果会少于正确匹配的结果