Flink 应用示例

https://ci.apache.org/projects/flink/flink-docs-release-1.10/zh/getting-started/examples/

 

  https://www.infoworld.com/article/3293426/how-to-build-stateful-streaming-applications-with-apache-flink.html

在本文中,我将给出两个常见的状态流处理用例的示例,并讨论如何使用Flink实现它们。 第一个用例是事件驱动的应用程序,即摄取连续的事件流并将某些业务逻辑应用于这些事件的应用程序。 第二个是流分析用例,其中我将介绍两个用Flink的SQLAPI实现的分析查询,这些查询实时聚合流数据。 我们在DataArtisans中提供了公共GitHub存储库中所有示例的源代码。

在深入研究示例的细节之前,我将介绍示例应用程序接收的事件流,并解释如何运行我们提供的代码。

一连串的出租车乘车活动

我们的示例应用程序基于2013年在纽约市发生的关于出租车乘车的公共数据集。 2015年DEBS(ACM分布式事件基础系统国际会议)大挑战的组织者重新安排了原始数据集,并将其转换为一个CSV文件,我们正在从中阅读以下九个字段。

奖章-出租车的MD5和ID

Hack_license-出租车牌照的MD5和ID

Pickup_datetime-乘客被接走的时间

Dropoff_datetime-乘客下车的时间

Pickup_longitude-拾取位置的经度

Pickup_latitude-取车地点的纬度

Dropoff_longitude-降落地点的经度

Dropoff_latitude-降落地点的纬度

Total_amount-以美元支付的总额

CSV文件按其下拉时间属性的升序存储记录。 因此,该文件可以被视为旅行结束时发布的事件的有序日志。 为了运行我们在GitHub上提供的示例,您需要从GoogleDrive下载DEBS挑战的数据集。

所有示例应用程序都顺序地读取CSV文件,并将其作为出租车乘坐事件流来接收。 从那时起,应用程序就像任何其他流一样处理事件,即像从基于日志的发布订阅系统(如Apache Kafka或Kinesis)接收的流。 事实上,读取文件(或任何其他类型的持久化数据)并将其视为流是Flink统一批处理和流处理方法的基石。

运行Flink示例

如前所述,我们在GitHub存储库中发布了示例应用程序的源代码。 我们鼓励您分叉并克隆存储库。 这些示例可以很容易地从您选择的IDE中执行;您不需要设置和配置Flink集群来运行它们。 首先,导入示例的源代码作为Maven项目。 然后,执行应用程序的主类,并提供数据文件的存储位置(下载数据的链接见上文)作为程序参数。

一旦您启动了应用程序,它将在应用程序的JVM进程中启动一个本地的嵌入式Flink实例,并提交应用程序来执行它。 当Flink启动和作业的任务正在安排时,您将看到一堆日志语句。 一旦应用程序运行,其输出将被写入标准输出。

 

在Flink中构建事件驱动应用程序

现在,让我们讨论我们的第一个用例,这是一个事件驱动的应用程序。 事件驱动的应用程序接收事件流,在接收事件时执行计算,并可能发出新事件或触发外部操作。 多个事件驱动的应用程序可以通过事件日志系统将它们连接在一起,类似于大型系统可以由微服务组成。 事件驱动的应用程序、事件日志和应用程序状态快照(在Flink中称为保存点)包含一个非常强大的设计模式,因为您可以重置它们的状态并重放它们的输入以从失败中恢复、修复错误或将应用程序迁移到不同的集群。

 

在本文中,我们将检查一个事件驱动的应用程序,支持一个服务,它监视出租车司机的工作时间。 在2016年,纽约市出租车和豪华轿车委员会决定将出租车司机的工作时间限制在12小时轮班,并要求在下一班开始之前至少休息8小时。 轮班从第一次乘车开始。 从那时起,司机可以在12小时内开始新的乘车。 我们的应用程序跟踪司机的乘车情况,标记他们12小时窗口的结束时间(即他们可能开始最后一次乘车的时间),以及违反规定的标志乘车。 您可以在GitHub存储库中找到此示例的完整源代码。

我们的应用程序是用Flink的数据流API和密钥处理函数实现的。 数据流API是一种基于类型数据流概念的功能API。 数据流 DataStream<T>  是T类型的事件流的逻辑表示。流通过向它应用一个函数来处理,该函数产生另一个数据流,可能是不同类型的数据流。 Flink通过将事件分配到流分区并将不同的函数实例应用到每个分区来并行处理流。

下面的代码片段显示了我们监控应用程序的高级流..

// ingest stream of taxi rides.//获取出租车数据流
DataStream<TaxiRide> rides = TaxiRides.getRides(env, inputPath);

DataStream<Tuple2<String, String>> notifications = rides
   // partition stream by the driver’s license id //根据出租车司机的id进行分区
   .keyBy(r -> r.licenseId)
   // monitor ride events and generate notifications//监控乘车事件并生成通知
   .process(new MonitorWorkTime());

// print notifications// 打印通知
notifications.print();

该应用程序开始接收一系列出租车乘车事件。 在我们的示例中,这些事件是从文本文件中读取、解析并存储在出租车骑行POJO对象中。 实际应用程序通常会从消息队列或事件日志中摄取事件,例如Apache Kafka或Pravega。 下一步是通过司机的驾驶执照ID来锁定出租车驾驶事件。 密钥By操作在声明的字段上划分流,这样所有具有相同密钥的事件都由以下函数的相同并行实例处理。 在我们的例子中,我们在许可证ID字段上进行分区,因为我们希望监视每个驱动程序的工作时间。

 

接下来,我们将监视器工作时间函数应用于分区出租车骑行事件。 该功能跟踪每个司机的乘车情况,并监视他们的班次和休息时间。 它发出Tuple2<String, String>;类型的事件,其中每个元组表示由驱动程序的许可证ID和消息组成的通知。 最后,我们的应用程序通过将消息打印到标准输出来发出消息。 现实世界的应用程序会将通知写入外部消息或存储系统,如Apache Kafka、HDFS或数据库系统,或者触发外部调用立即将它们推送出去。

现在我们已经讨论了应用程序的总体流程,让我们看看 MonitorWorkTime函数,它包含了应用程序的大多数实际业务逻辑。 监视器工作时间函数是一个状态键处理函数,它接收出租车骑行事件并发出 Tuple2<String, String>;记录。 键处理函数接口具有处理数据的两种方法: processElement()  和onTimer()。 每个到达事件都调用 processElement() 方法。 当先前注册的计时器触发时,调用onTimer()方法。 下面的片段显示了监视器工作时间函数的骨架以及在处理方法之外声明的所有内容。

public static class MonitorWorkTime
    extends KeyedProcessFunction<String, TaxiRide, Tuple2<String, String>> {

  // time constants in milliseconds
  private static final long ALLOWED_WORK_TIME = 12 * 60 * 60 * 1000; // 12 hours
  private static final long REQ_BREAK_TIME = 8 * 60 * 60 * 1000;     // 8 hours
  private static final long CLEAN_UP_INTERVAL = 28 * 60 * 60 * 1000; // 24 hours

 private transient DateTimeFormatter formatter;

  // state handle to store the starting time of a shift
  ValueState<Long> shiftStart;

  @Override
  public void open(Configuration conf) {
    // register state handle
    shiftStart = getRuntimeContext().getState(
      new ValueStateDescriptor<>(“shiftStart”, Types.LONG));
    // initialize time formatter
    this.formatter = DateTimeFormat.forPattern(“yyyy-MM-dd HH:mm:ss”);
  }

  // processElement() and onTimer() are discussed in detail below.
}

该函数声明了几个以毫秒为单位的时间间隔常量、一个时间格式化程序和一个由Flink管理的键状态的状态句柄。 管理状态是定期检查指定和自动恢复,以防失败。 键状态是按每个键组织的,这意味着函数将保持每个句柄和键的一个值。 在我们的例子中,监视器工作时间函数为每个密钥保持一个长值,即每个许可证ID。 移位启动状态存储驱动程序移位的起始时间。 状态句柄在open()方法中初始化,在处理第一个事件之前调用一次。

现在,让我们看看processElement() 方法。

@Override
public void processElement(
    TaxiRide ride,
    Context ctx,
    Collector<Tuple2<String, String>> out) throws Exception {

  // look up start time of the last shift
  Long startTs = shiftStart.value();

  if (startTs == null ||
    startTs < ride.pickUpTime - (ALLOWED_WORK_TIME + REQ_BREAK_TIME)) {

    // this is the first ride of a new shift.
    startTs = ride.pickUpTime;
    shiftStart.update(startTs);
    long endTs = startTs + ALLOWED_WORK_TIME;
    out.collect(Tuple2.of(ride.licenseId,
      “You are allowed to accept new passengers until “ + formatter.print(endTs)));

    // register timer to clean up the state in 24h
    ctx.timerService().registerEventTimeTimer(startTs + CLEAN_UP_INTERVAL);
  } else if (startTs < ride.pickUpTime - ALLOWED_WORK_TIME) {
    // this ride started after the allowed work time ended.
    // it is a violation of the regulations!
    out.collect(Tuple2.of(ride.licenseId,
      “This ride violated the working time regulations.”));
  }
}

 

对每个出租车乘车事件调用进程元素()方法。 首先,该方法从状态句柄中获取驱动程序移位的开始时间。 如果状态不包含开始时间(开始TS==NULL),或者如果最后一次移位启动超过20小时(ALLOWED_WORK_TIME) REQ_BREAK_TIME)早于当前乘坐,当前乘坐是新班次的第一次乘坐.. 在任何一种情况下,该函数通过更新班次的开始时间到当前乘坐的开始时间来启动新班次,以新班次的结束时间向驾驶员发出消息,并在24小时内注册一个计时器来清理状态。

如果当前乘坐不是新班次的第一次乘坐,则功能检查是否违反工作时间规定,即是否比司机当前班次的开始晚超过12小时。 如果是这样的话,该函数会发出一条消息来通知驱动程序违规的情况。

 

The processElement() method of the MonitorWorkTime function registers a timer to clean up the state 24 hours after the start of a shift.

监视器工作时间函数的过程元素()方法在轮班开始后24小时注册一个计时器来清理状态。

Removing state that is no longer needed is important to prevent growing state sizes due to leaking state.

移除不再需要的状态对于防止由于泄漏状态而增加状态大小非常重要。

A timer fires when the time of the application passes the timer’s timestamp.

当应用程序的时间通过计时器的时间戳时,计时器就会触发。

At that point, the onTimer() method is called.

此时调用onTimer()方法..

Similar to state, timers are maintained per key, and the function is put into the context of the associated key before the onTimer() method is called.

类似于状态,每个键维护定时器,在调用onTimer()方法之前,将函数放入关联密钥的上下文中。

Hence, all state access is directed to the key that was active when the timer was registered.

因此,所有状态访问都指向计时器注册时处于活动状态的密钥。

@Override
public void onTimer(
    long timerTs,
    OnTimerContext ctx,
    Collector<Tuple2<String, String>> out) throws Exception {

  // remove the shift state if no new shift was started already.
  Long startTs = shiftStart.value();
  if (startTs == timerTs - CLEAN_UP_INTERVAL) {
    shiftStart.clear();
  }
}

The processElement() method registers timers for 24 hours after a shift started to clean up state that is no longer needed.

进程元素()方法在一次轮班开始清理不再需要的状态后24小时注册计时器。

Cleaning up the state is the only logic that the onTimer() method implements.

清理状态是onTimer()方法实现的唯一逻辑。

When a timer fires, we check if the driver started a new shift in the meantime, i.

当计时器启动时,我们检查司机是否在此期间开始了新的轮班。

e.

[医]屈光正常; 正视眼; 眼; 实验者; 能量

, whether the shift starting time changed.

班次开始时间是否改变。

If that is not the case, we clear the shift state for the driver.

如果不是这样,我们为司机清除轮班状态。

The implementation of the working hour monitoring example demonstrates how Flink applications operate with state and time, the core ingredients of any slightly advanced stream processing application.

工作时间监控示例的实现演示了Flink应用程序如何以状态和时间运行,这是任何稍微先进的流处理应用程序的核心组成部分。

Flink provides many features for working with of state and time, such as support for processing and event time.

Flink为处理状态和时间提供了许多功能,例如支持处理和事件时间。

In addition, Flink provides several strategies for dealing with late records, different types of state, an efficient checkpointing mechanism to guarantee exactly-once state consistency, and many unique features centered around the concept of “savepoints,” just to name a few.

此外,Flink提供了几种策略来处理延迟记录、不同类型的状态、一种有效的检查点机制来保证状态的一致性,以及许多围绕“保存点”概念的独特特性,仅举几个例子。

The combination of all of these features results in a stream processor that provides the flexibility that is required to build very sophisticated event-driven applications and run them at scale and with operational ease.

所有这些特性的结合导致流处理器提供了构建非常复杂的事件驱动应用程序所需的灵活性,并以规模和操作方便的方式运行它们。

 

 

Flink中使用SQL分析数据流

前面的示例显示了在使用Flink的数据流API和过程函数实现应用程序时,您手头的表现力和低级控制。 然而,很大一部分应用程序(和更高级应用程序的子任务)具有非常相似的要求,不需要这种表达能力。 它们可以更简洁和方便地使用SQL来定义,SQL是数据处理和分析的标准语言。

Flink具有对批处理和流数据的统一SQL支持。 对于给定的查询,Flink计算相同的结果,而不管它是在连续摄取的记录流上执行的,还是在有界记录集上执行的,因为流和数据集提供相同的数据。 Flink支持ANSI SQL的语法和语义;它没有定义一种看起来类似于SQL的语言,并且带有专有语法和语义。

 

SELECT
  toCellId(dropOffLon, dropOffLat) AS area,
  TUMBLE_START(dropOffTime, INTERVAL ‘1’ HOUR) AS t,
  AVG(total) AS avgTotal
FROM Rides
GROUP BY
  toCellId(dropOffLon, dropOffLat),
  TUMBLE(dropOffTime, INTERVAL ‘1’ HOUR)

The query looks and behaves just like a regular SELECT-FROM-GROUP-BY query.

However, there are three functions that we need to explain:

  1. TUMBLE is a function to assign records to windows of a fixed length, here one hour.
  2. TUMBLE_START is a function that returns the start timestamp of a window, e.g., TUMBLE_START will return “2018-06-01 13:00:00.000” for the window that starts at 1 p.m. and ends at 2 p.m. on June 1, 2018.
  3. toCellId is a user-defined function that computes a cell ID, representing an area of 250 by 250 meters, from a pair of longitude-latitude values.

he query will return a result similar to this:

8> 29126,2013-01-04 11:00:00.0,44.0
4> 46551,2013-01-04 11:00:00.0,14.4625
1> 44565,2013-01-04 12:00:00.0,8.188571
3> 57031,2013-01-04 12:00:00.0,13.066667
8> 46847,2013-01-04 12:00:00.0,20.5
7> 56781,2013-01-04 12:00:00.0,7.5

https://www.elastic.co/cn/blog/building-real-time-dashboard-applications-with-apache-flink-elasticsearch-and-kibana

 

 

 

  • 来自 Ververica 的 Flink 学习网站也有许多示例。你可以从中选取能够亲自实践的部分,并加以练习。

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值