往期推荐:

Flink基础:入门介绍

Flink基础:DataStream API

Flink深入浅出:资源管理

Flink深入浅出:部署模式

Flink深入浅出:内存模型

Flink深入浅出:JDBC Source从理论到实战

Flink深入浅出:Sql Gateway源码分析

Flink深入浅出:JDBC Connector源码分析

Flink的经典使用场景是ETL,即Extract抽取、Transform转换、Load加载,可以从一个或多个数据源读取数据,经过处理转换后,存储到另一个地方,本篇将会介绍如何使用DataStream API来实现这种应用。注意Flink Table和SQL 
api 会很适合来做ETL,但是不妨碍从底层的DataStream API来了解其中的细节。

1 无状态的转换

无状态即不需要在操作中维护某个中间状态,典型的例子如map和flatmap。

map()

下面是一个转换操作的例子,需要根据输入数据创建一个出租车起始位置和目标位置的对象。首先定义出租车的位置对象:

public static class EnrichedRide extends TaxiRide {
    public int startCell;
    public int endCell;

    public EnrichedRide() {}

    public EnrichedRide(TaxiRide ride) {
        this.rideId = ride.rideId;
        this.isStart = ride.isStart;
        ...
        this.startCell = GeoUtils.mapToGridCell(ride.startLon, ride.startLat);
        this.endCell = GeoUtils.mapToGridCell(ride.endLon, ride.endLat);
    }

    public String toString() {
        return super.toString() + "," +
            Integer.toString(this.startCell) + "," +
            Integer.toString(this.endCell);
    }
}

使用的时候可以注册一个MapFunction,该函数接收TaxiRide对象,输出EnrichRide对象。

public static class Enrichment implements MapFunction<TaxiRide, EnrichedRide> {
    @Override
    public EnrichedRide map(TaxiRide taxiRide) throws Exception {
        return new EnrichedRide(taxiRide);
    }
}

使用时只需要创建map对象即可:

DataStream<TaxiRide> rides = env.addSource(new TaxiRideSource(...));

DataStream<EnrichedRide> enrichedNYCRides = rides
    .filter(new RideCleansingSolution.NYCFilter())
    .map(new Enrichment());

enrichedNYCRides.print();

flatmap()

MapFunction适合一对一的转换,对于输入流的每个元素都有一个元素输出。如果需要一对多的场景,可以使用flatmap:

DataStream<TaxiRide> rides = env.addSource(new TaxiRideSource(...));

DataStream<EnrichedRide> enrichedNYCRides = rides
    .flatMap(new NYCEnrichment());

enrichedNYCRides.print();

FlatMapFunction的定义:

public static class NYCEnrichment implements FlatMapFunction<TaxiRide, EnrichedRide> {
    @Override
    public void flatMap(TaxiRide taxiRide, Collector<EnrichedRide> out) throws Exception {
        FilterFunction<TaxiRide> valid = new RideCleansing.NYCFilter();
        if (valid.filter(taxiRide)) {
            out.collect(new EnrichedRide(taxiRide));
        }
    }
}

通过collector,可以在flatmap中任意添加零个或多个元素。

2 Keyed Streams

keyBy()

有时需要对数据流按照某个字段进行分组,每个事件会根据该字段相同的值汇总到一起。比如,希望查找相同出发位置的路线。如果在SQL中可能会使用GROUP BY startCell,在Flink中可以直接使用keyBy函数:

rides
    .flatMap(new NYCEnrichment())
    .keyBy(value -> value.startCell)

keyBy会引起重分区而导致网络数据shuffle,通常这种代价都很昂贵,因为每次shuffle时需要进行数据的序列化和反序列化,既浪费CPU资源,又占用网络带宽。

通过对startCell进行分组,这种方式的分组可能会由于编译器而丢失字段的类型信息,因此Flink也支持把字段包装成Tuple,基于元素位置进行分组。当然也支持使用KeySelector函数,自定义分组规则。

rides
    .flatMap(new NYCEnrichment())
    .keyBy(
        new KeySelector<EnrichedRide, int>() {

            @Override
            public int getKey(EnrichedRide enrichedRide) throws Exception {
                return enrichedRide.startCell;
            }
        })

可以直接使用lambda表达式:

rides
    .flatMap(new NYCEnrichment())
    .keyBy(enrichedRide -> enrichedRide.startCell)

key可以自定义计算规则

keyselector不限制从必须从事件中抽取key,也可以自定义任何计算key的方法。但需要保证输出的key是一致的,并且实现了对应的hashCode和equals方法。生成key的规则一定要稳定,因为生成key可能在应用运行的任何时间,因此一定要保证key生成规则的持续稳定。

key可以通过某个字段选择:

keyBy(enrichedRide -> enrichedRide.startCell)

也可以直接替换成某个方法:

keyBy(ride -> GeoUtils.mapToGridCell(ride.startLon, ride.startLat))

Keyed Stream的聚合

下面的例子中,创建了一个包含startCell和花费时间的二元组:

import org.joda.time.Interval;

DataStream<Tuple2<Integer, Minutes>> minutesByStartCell = enrichedNYCRides
    .flatMap(new FlatMapFunction<EnrichedRide, Tuple2<Integer, Minutes>>() {

        @Override
        public void flatMap(EnrichedRide ride,
                            Collector<Tuple2<Integer, Minutes>> out) throws Exception {
            if (!ride.isStart) {
                Interval rideInterval = new Interval(ride.startTime, ride.endTime);
                Minutes duration = rideInterval.toDuration().toStandardMinutes();
                out.collect(new Tuple2<>(ride.startCell, duration));
            }
        }
    });

现在需要输出每个起始位置最长距离的路线,有很多种方式可以实现。以上面的数据为例,可以通过startcell进行聚合,然后选择时间最大的元素输出:

minutesByStartCell
  .keyBy(value -> value.f0) // .keyBy(value -> value.startCell)
  .maxBy(1) // duration
  .print();

可以得到输出结果:

4> (64549,5M)
4> (46298,18M)
1> (51549,14M)
1> (53043,13M)
1> (56031,22M)
1> (50797,6M)
...
1> (50797,8M)
...
1> (50797,11M)
...
1> (50797,12M)

状态

上面是一个有状态的例子,Flink需要记录每个key的最大值。无论何时在应用中涉及到状态,都需要考虑这个状态有多大。如果key的空间是无限大的,那么flink可能需要维护大量的状态信息。当使用流时,一定要对无限窗口的聚合十分敏感,因为它是对整个流进行操作,很有可能因为维护的状态信息不断膨胀,而导致内存溢出。在上面使用的maxBy就是经典的的聚合操作,也可以使用更通用的reduce来自定义聚合方法。

3 有状态的操作

Flink针对状态的管理有很多易用的特性,比如:

  • 支持本地保存:基于进程内存来保存状态

  • 状态的持久化:定期保存到检查点,保证容错

  • 垂直扩展:Flink状态可以把状态保存到RocksDB中,也支持扩展到本地磁盘

  • 水平扩展:状态支持在集群中扩缩容,通过调整并行度,自动拆分状态

  • 可查询:Flink的状态可以在外部直接查询

Rich函数

Flink有几种函数接口,包括FilterFunction, MapFunction,FlatMapFunction等。对于每个接口,Flink都提供了对应的Rich方法。比如RichFlatMapFunction,提供了额外的一些方法:

  • open(Configuration c) 在初始化的时候调用一次,用于加载静态数据,开启外部服务的连接等

  • close() 流关闭时调用

  • getRuntimeContext() 提供进入全局状态的方法,需要了解如何创建和查询状态

使用Keyed State的例子

下面是一个针对事件的key进行去重的例子:

private static class Event {
    public final String key;
    public final long timestamp;
    ...
}

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

    env.addSource(new EventSource())
        .keyBy(e -> e.key)
        .flatMap(new Deduplicator())
        .print();

    env.execute();
}

为了实现这个功能,deduplicator需要记住一些信息,对于每个key,都需要记录是否已经存在。Flink支持几种不同类型的状态,最简单的一种是valueState。对于每个key,flink都为它保存一个对象,在上面的例子中对象是Boolean。Deduplicator有两个方法:open()和flatMap()。open方法通过descriptor为状态起了一个标识名称,并声明类型为Boolean。

public static class Deduplicator extends RichFlatMapFunction<Event, Event> {
    ValueState<Boolean> keyHasBeenSeen;

    @Override
    public void open(Configuration conf) {
        ValueStateDescriptor<Boolean> desc = new ValueStateDescriptor<>("keyHasBeenSeen", Types.BOOLEAN);
        keyHasBeenSeen = getRuntimeContext().getState(desc);
    }

    @Override
    public void flatMap(Event event, Collector<Event> out) throws Exception {
        if (keyHasBeenSeen.value() == null) {
            out.collect(event);
            keyHasBeenSeen.update(true);
        }
    }
}

flatMap中调用state.value()获取状态。flink在上下文中为每个key保存了一个状态值,只有当值为null时,说明这个key之前没有出现过,然后将其更新为true。当flink调用open时,状态是空的。但是当调用flatMap时,key可以通过context进行访问。当在集群模式中运行时,会有很多个Deduplicator实例,每个负责维护一部分key的事件。因此,当使用单个事件的valuestate时,要理解它背后其实不是一个值,而是每个key都对应一个状态值,并且分布式的存储在集群中的各个节点进程上。

清除状态

有时候key的空间可能是无限制的,flink会为每个key存储一个boolean对象。如果key的数量是有限的还好,但是应用往往是持续不间断的运行,那么key可能会无限增长,因此需要清理不再使用的key。可以通过state.clear()进行清理。比如针对某个key按照某一时间频率进行清理,在processFunction中可以了解到如何在事件驱动的应用中执行定时器操作。也可以在状态描述符中为状态设置TTL生存时间,这样状态可以自动进行清理。

非keyed状态

状态也支持在非key类型的上下文中使用,这种叫做操作符状态,operator state。典型的场景是Flink读取Kafka时记录的offset信息。

4 连接流

大部分场景中Flink都是接收一个数据流输出一个数据流,类似管道式的处理数据:

也有的场景需要动态的修改函数中的信息,比如阈值、规则或者其他的参数,这种设计叫做connected streams,流会拥有两个输入,类似:

在下面的例子中,通过控制流用来指定必须过滤的单词:

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

    DataStream<String> control = env.fromElements("DROP", "IGNORE").keyBy(x -> x);
    DataStream<String> streamOfWords = env.fromElements("Apache", "DROP", "Flink", "IGNORE").keyBy(x -> x);

    control
        .connect(datastreamOfWords)
        .flatMap(new ControlFunction())
        .print();

    env.execute();
}

两个流可以通过key的方式连接,keyby用来分组数据,这样保证相同类型的数据可以进入到相同的实例中。上面的例子两个流都是字符串,

public static class ControlFunction extends RichCoFlatMapFunction<String, String, String> {
    private ValueState<Boolean> blocked;

    @Override
    public void open(Configuration config) {
        blocked = getRuntimeContext().getState(new ValueStateDescriptor<>("blocked", Boolean.class));
    }

    @Override
    public void flatMap1(String control_value, Collector<String> out) throws Exception {
        blocked.update(Boolean.TRUE);
    }

    @Override
    public void flatMap2(String data_value, Collector<String> out) throws Exception {
        if (blocked.value() == null) {
            out.collect(data_value);
        }
    }
}

blocked用于记录key的控制逻辑,key的state会在两个流间共享。flatMap1和flatMap2会被两个流调用,分别用来更新和获取状态,从而实现通过一个流控制另一个流的目的。

总结:本片从状态上讲述了有状态的操作和无状态的操作,还介绍了状态的使用以及连接流的适用场景。后面会介绍DataStream的操作和状态的管理。

Flink基础:实时处理管道与ETL的更多相关文章

  1. Flink基础:时间和水印

    ​ 往期推荐: Flink基础:入门介绍 Flink基础:DataStream API Flink基础:实时处理管道与ETL Flink深入浅出:资源管理 Flink深入浅出:部署模式 Flink深入 ...

  2. Flink资料(1)-- Flink基础概念(Basic Concept)

    Flink基础概念 本文描述Flink的基础概念,翻译自https://ci.apache.org/projects/flink/flink-docs-release-1.0/concepts/con ...

  3. Flink入门-第一篇:Flink基础概念以及竞品对比

    Flink入门-第一篇:Flink基础概念以及竞品对比 Flink介绍 截止2021年10月Flink最新的稳定版本已经发展到1.14.0 Flink起源于一个名为Stratosphere的研究项目主 ...

  4. Flink基础概念入门

    Flink 概述 什么是 Flink Apache Apache Flink 是一个开源的流处理框架,应用于分布式.高性能.高可用的数据流应用程序.可以处理有限数据流和无限数据,即能够处理有边界和无边 ...

  5. flink基础篇

    Flink面试--核心概念和基础考察 1.简单介绍一下 Flink 2.Flink 相比传统的 Spark Streaming 有什么区别? 3.Flink 的组件栈有哪些?         面试知识 ...

  6. flink基础教程读书笔记

    数据架构设计领域发生了重大的变化,基于流的处理是变化的核心. 分布式文件系统用来存储不经常更新的数据,他们也是大规模批量计算所以来的数据存储方式. 批处理架构(lambda架构)实现计数的方式:持续摄 ...

  7. nodejs基础(管道、流)实现:复制、压缩、加密、解压,解密,写入文件

    stream流 都是events.EventEmitter的一个实例,都可以来创建自定义事件(也就是说,流是一个事件的实例) 在nodejs中 对http的请求与响应都是用流来实现的,请求就是一个输入 ...

  8. 1. flink 基础

    flink word count  程序 1. 数据集模式 pom.xml 文件 <?xml version="1.0" encoding="UTF-8" ...

  9. 数据仓库基础(二)ETL

    本文转载自:http://www.cnblogs.com/evencao/archive/2013/06/14/3135529.html ETL在数据仓库中具有以下的几个特点: 数据流动具有周期性: ...

随机推荐

  1. Redis哨兵知识点总结

    1.Redis哨兵介绍 sentinal,中文名是哨兵 A.哨兵是redis集群架构中非常重要的一个组件,主要功能如下 集群监控,负责监控redis master和slave进程是否正常工作 消息通知 ...

  2. C&C++代码单元集成测试培训

    课程简介 本课程为期3天,结合实例讲解如何使用Cantata开展C和C++代码,通过培训,可以明显提高工程师操作Cantata的效率,并加速单元测试和集成测试. [日期]2020年11月3日-5日(共 ...

  3. 2020年在项目中使用MVVM正确姿势,你用对了吗?

    最近看到了几篇与 Jetpack MVVM 有关到文章,使我不禁也想淌一下这场混水.我是在 2017 年下半年接触的 Jetpack 的那套开发工具,并且后来一直将其作为开发的主要框架.在这段时间的使 ...

  4. day32 Pyhton hashlib模块 总结异常处理

    一.当用明文密码进行信息存储的时候,会导致密码的泄露,如何解决问题 通过导入hashlib模块,利用里面存在的算法对字符串进行加密计算得到一串密文的结果 1.这个过程不可逆 2.对于同一个字符串,同一 ...

  5. Pythonic【15个代码示例】

    Python由于语言的简洁性,让我们以人类思考的方式来写代码,新手更容易上手,老鸟更爱不释手. 要写出 Pythonic(优雅的.地道的.整洁的)代码,还要平时多观察那些大牛代码,Github 上有很 ...

  6. fastdfs之同一台storage server下包含多个store path

    一,查看本地centos的版本 [root@localhost lib]# cat /etc/redhat-release CentOS Linux release 8.1.1911 (Core) 说 ...

  7. centos8平台安装zookeeper3.6集群

    一,规划三台zk服务器构成集群 ip:172.18.1.1 机器名:zk1 对应myid: 1 ip:172.18.1.2 机器名:zk2 对应myid: 2 ip:172.18.1.3 机器名:zk ...

  8. matplotlib直方图

    import matplotlib.pyplot as plt import matplotlib as mpl from matplotlib.font_manager import FontPro ...

  9. Linux运维学习第六周记

    四月上夏渐热 善疗也须调摄 文殊眼裹抽筋 金刚脑后拔楔 网络的世界让人变得不那么真实! 第六周学记 用了一周的时间学习了计算机网络基础知识,说是基础,更应该说是必备的常识! 网络的协议和管理 TCP/ ...

  10. 从APT攻击中学习

    0x01. 什么是APT? 可以看出APT攻击,叫高级可持续威胁攻击,也称为定向威胁攻击:什么是定向,也就是指定目标行业而发起进攻 这边又提到供应链和社会工程学,那是什么? 社会工程学,也就是社工,通 ...