Flink-学习手册-全-

Flink 学习手册(全)

原文:zh.annas-archive.org/md5/0715B65CE6CD5C69C124166C204B4830

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

随着大型计算系统的出现,不同领域的组织以实时方式生成大量数据。作为大数据处理的最新参与者,Apache Flink 旨在以极快的速度处理连续的数据流。

这本书将成为您使用 Apache Flink 进行批处理和流数据处理的权威指南。该书首先介绍了 Apache Flink 生态系统,设置它并使用 DataSet 和 DataStream API 处理批处理和流式数据集。随后,本书将探讨如何将 SQL 的强大功能引入 Flink,并探索用于查询和操作数据的 Table API。在书的后半部分,读者将学习 Apache Flink 的其余生态系统,以实现事件处理、机器学习和图处理等复杂任务。该书的最后部分将包括诸如扩展 Flink 解决方案、性能优化和将 Flink 与 Hadoop、ElasticSearch、Cassandra 和 Kafka 等其他工具集成的主题。

无论您是想深入了解 Apache Flink,还是想探索如何更好地利用这一强大技术,您都会在本书中找到一切。本书涵盖了许多真实世界的用例,这将帮助您串联起各个方面。

本书涵盖的内容

第一章,“介绍 Apache Flink”,向您介绍了 Apache Flink 的历史、架构、特性和在单节点和多节点集群上的安装。

第二章,“使用 DataStream API 进行数据处理”,为您提供了有关 Flink 流优先概念的详细信息。您将了解有关 DataStream API 提供的数据源、转换和数据接收器的详细信息。

第三章,“使用批处理 API 进行数据处理”,为您介绍了批处理 API,即 DataSet API。您将了解有关数据源、转换和接收器的信息。您还将了解 API 提供的连接器。

第四章,“使用 Table API 进行数据处理”,帮助您了解如何将 SQL 概念与 Flink 数据处理框架相结合。您还将学习如何将这些概念应用于实际用例。

第五章,“复杂事件处理”,为您提供了如何使用 Flink CEP 库解决复杂事件处理问题的见解。您将了解有关模式定义、检测和警报生成的详细信息。

第六章,“使用 FlinkML 进行机器学习”,详细介绍了机器学习概念以及如何将各种算法应用于实际用例。

第七章,“Flink 图形 API - Gelly”,向您介绍了图形概念以及 Flink Gelly 为我们解决实际用例提供的功能。它向您介绍了 Flink 提供的迭代图处理能力。

第八章,“使用 Flink 和 Hadoop 进行分布式数据处理”,详细介绍了如何使用现有的 Hadoop-YARN 集群提交 Flink 作业。它详细介绍了 Flink 在 YARN 上的工作原理。

第九章,“在云上部署 Flink”,提供了有关如何在云上部署 Flink 的详细信息。它详细介绍了如何在 Google Cloud 和 AWS 上使用 Flink。

第十章,“最佳实践”,涵盖了开发人员应遵循的各种最佳实践,以便以高效的方式使用 Flink。它还讨论了日志记录、监控最佳实践以控制 Flink 环境。

您需要为本书准备什么

您需要一台带有 Windows、Mac 或 UNIX 等任何操作系统的笔记本电脑或台式电脑。最好有一个诸如 Eclipse 或 IntelliJ 的 IDE,当然,您需要很多热情。

这本书是为谁准备的

这本书适用于希望在分布式系统上处理批处理和实时数据的大数据开发人员,以及寻求工业化分析解决方案的数据科学家。

惯例

在本书中,您会发现一些区分不同信息类型的文本样式。以下是一些这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下: "这将在/flinkuser/.ssh文件夹中生成公钥和私钥。"

代码块设置如下:

CassandraSink.addSink(input)
  .setQuery("INSERT INTO cep.events (id, message) values (?, ?);")
  .setClusterBuilder(new ClusterBuilder() {
    @Override
    public Cluster buildCluster(Cluster.Builder builder) {
      return builder.addContactPoint("127.0.0.1").build();
    }
  })
  .build();

任何命令行输入或输出都以以下方式编写:

$sudo tar -xzf flink-1.1.4-bin-hadoop27-scala_2.11.tgz 
$cd flink-1.1.4 
$bin/start-local.sh

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中显示为: "一旦我们的所有工作都完成了,关闭集群就变得很重要。为此,我们需要再次转到 AWS 控制台,然后单击终止按钮"。

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会显示如此。

第一章:介绍 Apache Flink

随着分布式技术不断发展,工程师们试图将这些技术推向极限。以前,人们正在寻找更快、更便宜的处理数据的方法。当 Hadoop 被引入时,这种需求得到了满足。每个人都开始使用 Hadoop,开始用 Hadoop 生态系统工具替换他们的 ETL。现在,这种需求已经得到满足,Hadoop 在许多公司的生产中被使用,另一个需求出现了,即以流式方式处理数据,这催生了 Apache Spark 和 Flink 等技术。快速处理引擎、能够在短时间内扩展以及对机器学习和图技术的支持等功能,正在开发者社区中推广这些技术。

你们中的一些人可能已经在日常生活中使用 Apache Spark,并且可能一直在想,如果我有 Spark,为什么还需要使用 Flink?这个问题是可以预料的,比较是自然的。让我试着简要回答一下。我们需要在这里理解的第一件事是,Flink 基于流式优先原则,这意味着它是真正的流处理引擎,而不是将流作为小批量收集的快速处理引擎。Flink 将批处理视为流处理的特例,而在 Spark 的情况下则相反。同样,我们将在本书中发现更多这样的区别。

这本书是关于最有前途的技术之一--Apache Flink。在本章中,我们将讨论以下主题:

  • 历史

  • 架构

  • 分布式执行

  • 特性

  • 快速启动设置

  • 集群设置

  • 运行一个示例应用程序

历史

Flink 作为一个名为Stratosphere的研究项目开始,旨在在柏林地区的大学建立下一代大数据分析平台。它于 2014 年 4 月 16 日被接受为 Apache 孵化器项目。Stratosphere 的最初版本基于 Nephele 的研究论文stratosphere.eu/assets/papers/Nephele_09.pdf

以下图表显示了 Stratosphere 随时间的演变:

历史

Stratosphere 的第一个版本主要关注运行时、优化器和 Java API。随着平台的成熟,它开始支持在各种本地环境以及YARN上的执行。从 0.6 版本开始,Stratosphere 更名为 Flink。Flink 的最新版本专注于支持各种功能,如批处理、流处理、图处理、机器学习等。

Flink 0.7 引入了 Flink 最重要的功能,即 Flink 的流式 API。最初的版本只有 Java API。后来的版本开始支持 Scala API。现在让我们在下一节中看一下 Flink 的当前架构。

架构

Flink 1.X 的架构包括各种组件,如部署、核心处理和 API。我们可以轻松地将最新的架构与 Stratosphere 的架构进行比较,并看到它的演变。以下图表显示了组件、API 和库:

架构

Flink 具有分层架构,其中每个组件都是特定层的一部分。每个层都建立在其他层之上,以清晰的抽象。Flink 被设计为在本地机器、YARN 集群或云上运行。运行时是 Flink 的核心数据处理引擎,通过 API 以 JobGraph 的形式接收程序。JobGraph是一个简单的并行数据流,其中包含一组产生和消费数据流的任务。

DataStream 和 DataSet API 是程序员用于定义作业的接口。当程序编译时,这些 API 生成 JobGraphs。一旦编译完成,DataSet API 允许优化器生成最佳执行计划,而 DataStream API 使用流构建进行高效的执行计划。

然后根据部署模型将优化后的 JobGraph 提交给执行器。您可以选择本地、远程或 YARN 部署模式。如果已经运行了 Hadoop 集群,最好使用 YARN 部署模式。

分布式执行

Flink 的分布式执行由两个重要的进程组成,即主节点和工作节点。当执行 Flink 程序时,各种进程参与执行,即作业管理器、任务管理器和作业客户端。

以下图表显示了 Flink 程序的执行:

分布式执行

Flink 程序需要提交给作业客户端。然后作业客户端将作业提交给作业管理器。作业管理器负责编排资源分配和作业执行。它的第一件事是分配所需的资源。资源分配完成后,任务被提交给相应的任务管理器。收到任务后,任务管理器启动线程开始执行。在执行过程中,任务管理器不断向作业管理器报告状态的变化。可能有各种状态,如执行开始、进行中或已完成。作业执行完成后,结果被发送回客户端。

作业管理器

主进程,也称为作业管理器,协调和管理程序的执行。它们的主要职责包括调度任务、管理检查点、故障恢复等。

可以并行运行多个主节点并共享这些责任。这有助于实现高可用性。其中一个主节点需要成为领导者。如果领导节点宕机,备用主节点将被选举为领导者。

作业管理器包括以下重要组件:

  • actor 系统

  • 调度器

  • 检查点

Flink 在内部使用 Akka actor 系统在作业管理器和任务管理器之间进行通信。

actor 系统

actor 系统是具有各种角色的 actor 的容器。它提供诸如调度、配置、日志记录等服务。它还包含一个线程池,所有 actor 都是从中初始化的。所有 actor 都驻留在一个层次结构中。每个新创建的 actor 都会分配给一个父级。actor 之间使用消息系统进行通信。每个 actor 都有自己的邮箱,从中读取所有消息。如果 actor 是本地的,消息通过共享内存共享,但如果 actor 是远程的,消息则通过 RPC 调用传递。

每个父级负责监督其子级。如果子级出现任何错误,父级会收到通知。如果 actor 能够解决自己的问题,它可以重新启动其子级。如果无法解决问题,则可以将问题升级给自己的父级:

actor 系统

在 Flink 中,actor 是一个具有状态和行为的容器。actor 的线程会顺序地处理它在邮箱中接收到的消息。状态和行为由它接收到的消息确定。

调度器

在 Flink 中,执行器被定义为任务槽。每个任务管理器需要管理一个或多个任务槽。在内部,Flink 决定哪些任务需要共享槽,哪些任务必须放入特定的槽中。它通过 SlotSharingGroup 和 CoLocationGroup 来定义。

检查点

检查点是 Flink 提供一致性容错的支柱。它不断为分布式数据流和执行器状态进行一致的快照。它受 Chandy-Lamport 算法的启发,但已经修改以满足 Flink 的定制要求。有关 Chandy-Lamport 算法的详细信息可以在以下网址找到:research.microsoft.com/en-us/um/people/lamport/pubs/chandy.pdf

有关快照实现细节的详细信息可以在以下研究论文中找到:Lightweight Asynchronous Snapshots for Distributed Dataflows (arxiv.org/abs/1506.08603)。

容错机制不断为数据流创建轻量级快照。因此,它们在没有显着负担的情况下继续功能。通常,数据流的状态保存在配置的位置,如 HDFS。

在发生故障时,Flink 会停止执行器并重置它们,然后从最新可用的检查点开始执行。

流障是 Flink 快照的核心元素。它们被吸收到数据流中而不影响流程。障碍永远不会超越记录。它们将一组记录分组成一个快照。每个障碍都携带一个唯一的 ID。以下图表显示了障碍如何被注入到数据流中进行快照:

检查点

每个快照状态都报告给 Flink 的作业管理器的检查点协调器。在绘制快照时,Flink 处理记录的对齐,以避免由于任何故障而重新处理相同的记录。这种对齐通常需要一些毫秒。但对于一些强烈的应用程序,即使毫秒级的延迟也是不可接受的,我们可以选择低延迟而不是精确的单个记录处理。默认情况下,Flink 会精确处理每个记录一次。如果任何应用程序需要低延迟,并且可以接受至少一次交付,我们可以关闭该触发器。这将跳过对齐并提高延迟。

任务管理器

任务管理器是在 JVM 中以一个或多个线程执行任务的工作节点。任务管理器上的任务执行的并行性由每个任务管理器上可用的任务槽确定。每个任务代表分配给任务槽的一组资源。例如,如果一个任务管理器有四个槽,那么它将为每个槽分配 25%的内存。一个任务槽中可能运行一个或多个线程。同一槽中的线程共享相同的 JVM。同一 JVM 中的任务共享 TCP 连接和心跳消息:

任务管理器

作业客户端

作业客户端不是 Flink 程序执行的内部部分,而是执行的起点。作业客户端负责接受用户的程序,然后创建数据流,然后将数据流提交给作业管理器进行进一步执行。执行完成后,作业客户端将结果提供给用户。

数据流是执行计划。考虑一个非常简单的单词计数程序:

作业客户端

当客户端接受用户的程序时,然后将其转换为数据流。上述程序的数据流可能如下所示:

作业客户端

前面的图表显示了程序如何转换为数据流。Flink 数据流默认是并行和分布式的。对于并行数据处理,Flink 对操作符和流进行分区。操作符分区称为子任务。流可以以一对一或重新分布的方式分发数据。

数据直接从源流向映射操作符,因此无需洗牌数据。但对于 GroupBy 操作,Flink 可能需要按键重新分发数据以获得正确的结果。

作业客户端

特点

在之前的章节中,我们试图了解 Flink 的架构和其执行模型。由于其健壮的架构,Flink 充满了各种功能。

高性能

Flink 旨在实现高性能和低延迟。与 Spark 等其他流处理框架不同,您不需要进行许多手动配置以获得最佳性能。Flink 的流水线数据处理与其竞争对手相比具有更好的性能。

精确一次性有状态计算

正如我们在前一节中讨论的,Flink 的分布式检查点处理有助于确保每个记录的处理仅一次。在高吞吐量应用程序的情况下,Flink 为我们提供了一个开关,允许至少一次处理。

灵活的流式窗口

Flink 支持数据驱动的窗口。这意味着我们可以基于时间、计数或会话设计窗口。窗口也可以定制,这使我们能够在事件流中检测特定模式。

容错

Flink 的分布式、轻量级快照机制有助于实现很高程度的容错。它允许 Flink 提供高吞吐量性能和可靠的传递。

内存管理

Flink 配备了自己的内存管理,位于 JVM 内部,这使其独立于 Java 的默认垃圾收集器。它通过使用哈希、索引、缓存和排序有效地进行内存管理。

优化器

为了避免消耗大量内存的操作(如洗牌、排序等),Flink 的批处理数据处理 API 进行了优化。它还确保使用缓存以避免大量的磁盘 IO 操作。

流和批处理在一个平台上

Flink 提供了用于批处理和流处理数据的 API。因此,一旦设置了 Flink 环境,它就可以轻松托管流和批处理应用程序。事实上,Flink 首先考虑流处理,并将批处理视为流处理的特例。

Flink 拥有丰富的库,可用于机器学习、图处理、关系数据处理等。由于其架构,执行复杂事件处理和警报非常容易。我们将在后续章节中更多地了解这些库。

事件时间语义

Flink 支持事件时间语义。这有助于处理事件到达顺序混乱的流。有时事件可能会延迟到达。Flink 的架构允许我们基于时间、计数和会话定义窗口,这有助于处理这种情况。

快速开始设置

现在我们了解了 Flink 的架构和其过程模型的细节,是时候开始快速设置并自己尝试一些东西了。Flink 可以在 Windows 和 Linux 机器上运行。

我们需要做的第一件事是下载 Flink 的二进制文件。Flink 可以从 Flink 下载页面下载:flink.apache.org/downloads.html

在下载页面上,您将看到多个选项,如下面的截图所示:

快速开始设置

为了安装 Flink,您不需要安装 Hadoop。但是,如果您需要使用 Flink 连接到 Hadoop,那么您需要下载与您拥有的 Hadoop 版本兼容的确切二进制文件。

由于我已经安装了最新版本的Hadoop 2.7.0,我将下载与 Hadoop 2.7.0 兼容并基于 Scala 2.11 构建的 Flink 二进制文件。

这是直接下载链接:

www-us.apache.org/dist/flink/flink-1.1.4/flink-1.1.4-bin-hadoop27-scala_2.11.tgz

先决条件

Flink 需要首先安装 Java。因此,在开始之前,请确保已安装 Java。我在我的机器上安装了 JDK 1.8:

先决条件

在 Windows 上安装

Flink 安装非常容易。只需提取压缩文件并将其存储在所需位置。

提取后,转到文件夹并执行start-local.bat

>cd flink-1.1.4
>bin\start-local.bat

然后您会看到 Flink 的本地实例已经启动。

您还可以在http://localhost:8081/上检查 Web UI:

在 Windows 上安装

您可以通过按下Cltr + C来停止 Flink 进程。

在 Linux 上安装

与 Windows 类似,在 Linux 机器上安装 Flink 非常容易。我们需要下载二进制文件,将其放在特定文件夹中,然后进行提取和完成:

$sudo tar -xzf flink-1.1.4-bin-hadoop27-scala_2.11.tgz
$cd flink-1.1.4
$bin/start-local.sh 

与 Windows 一样,请确保 Java 已安装在机器上。

现在我们已经准备好提交一个 Flink 作业。要停止 Linux 上的本地 Flink 实例,请执行以下命令:

$bin/stop-local.sh

集群设置

设置 Flink 集群也非常简单。那些有安装 Hadoop 集群背景的人将能够非常容易地理解这些步骤。为了设置集群,让我们假设我们有四台 Linux 机器,每台机器都有适度的配置。至少两个核心和 4 GB RAM 的机器将是一个很好的选择来开始。

我们需要做的第一件事是选择集群设计。由于我们有四台机器,我们将使用一台机器作为作业管理器,另外三台机器作为任务管理器

集群设置

SSH 配置

为了设置集群,我们首先需要在作业管理器机器上进行无密码连接到任务管理器。需要在创建 SSH 密钥并将其复制到authorized_keys上执行以下步骤:

$ssh-keygen

这将在/home/flinkuser/.ssh文件夹中生成公钥和私钥。现在将公钥复制到任务管理器机器,并在任务管理器上执行以下步骤,以允许从作业管理器进行无密码连接:

sudo mkdir -p /home/flinkuser/.ssh 
sudo touch /home/flinkuser/authorized_keys 
sudo cp /home/flinkuser/.ssh 
 sudo sh -c "cat id_rsa.pub >> /home/flinkuser/.ssh/authorized_keys"

确保密钥通过执行以下命令具有受限访问权限:

sudo chmod 700 /home/flinkuser/.ssh
sudo chmod 600 /home/flinkuser/.ssh/authorized_keys 

现在您可以从作业管理器机器测试无密码 SSH 连接:

sudo ssh <task-manager-1>
sudo ssh <task-manager-2>
sudo ssh <task-manager-3>

提示

如果您正在使用任何云服务实例进行安装,请确保从 SSH 启用了 ROOT 登录。为了做到这一点,您需要登录到每台机器:打开文件/etc/ssh/sshd_config。然后将值更改为PermitRootLogin yes。保存文件后,通过执行命令重新启动 SSH 服务:sudo service sshd restart

Java 安装

接下来,我们需要在每台机器上安装 Java。以下命令将帮助您在基于 Redhat/CentOS 的 UNIX 机器上安装 Java。

wget --no-check-certificate --no-cookies --header "Cookie: 
    oraclelicense=accept-securebackup-cookie" 
    http://download.oracle.com/otn-pub/java/jdk/8u92-b14/jdk-8u92-
    linux-x64.rpm
sudo rpm -ivh jdk-8u92-linux-x64.rpm

接下来,我们需要设置JAVA_HOME环境变量,以便 Java 可以从任何地方访问。

创建一个java.sh文件:

sudo vi /etc/profile.d/java.sh

并添加以下内容并保存:

#!/bin/bash
JAVA_HOME=/usr/java/jdk1.8.0_92
PATH=$JAVA_HOME/bin:$PATH
export PATH JAVA_HOME
export CLASSPATH=.

使文件可执行并对其进行源操作:

sudo chmod +x /etc/profile.d/java.sh
source /etc/profile.d/java.sh

您现在可以检查 Java 是否已正确安装:

$ java -version
java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)

在作业管理器和任务管理器机器上重复这些安装步骤。

一旦 SSH 和 Java 安装完成,我们需要下载 Flink 二进制文件并将其提取到特定文件夹中。请注意,所有节点上的安装目录应该相同。

所以让我们开始吧:

cd /usr/local
sudo wget  http://www-eu.apache.org/dist/flink/flink-1.1.4/flink-
    1.1.4-bin-hadoop27-scala_2.11.tgz
sudo tar -xzf flink-1.1.4-bin-hadoop27-scala_2.11.tgz

现在二进制文件已经准备好,我们需要进行一些配置。

配置

Flink 的配置很简单。我们需要调整一些参数,然后就可以了。大多数配置对作业管理器节点和任务管理器节点都是相同的。所有配置都在conf/flink-conf.yaml文件中完成。

以下是作业管理器节点的配置文件:

jobmanager.rpc.address: localhost
jobmanager.rpc.port: 6123
jobmanager.heap.mb: 256
taskmanager.heap.mb: 512
taskmanager.numberOfTaskSlots: 1

您可能希望根据节点配置更改作业管理器和任务管理器的内存配置。对于任务管理器,jobmanager.rpc.address应填入正确的作业管理器主机名或 IP 地址。

因此,对于所有任务管理器,配置文件应如下所示:

jobmanager.rpc.address: <jobmanager-ip-or-host>
jobmanager.rpc.port: 6123
jobmanager.heap.mb: 256
taskmanager.heap.mb: 512
taskmanager.numberOfTaskSlots: 1

我们需要在此文件中添加JAVA_HOME详细信息,以便 Flink 确切知道从何处查找 Java 二进制文件:

export JAVA_HOME=/usr/java/jdk1.8.0_92

我们还需要在conf/slaves文件中添加从节点的详细信息,每个节点占据一个新的单独行。

示例conf/slaves文件应如下所示:

<task-manager-1>
<task-manager-2>
<task-manager-3>

启动守护程序

现在唯一剩下的就是启动 Flink 进程。 我们可以在各个节点上分别启动每个进程,也可以执行start-cluster.sh命令在每个节点上启动所需的进程:

bin/start-cluster.sh

如果所有配置都正确,那么您会看到集群正在运行。 您可以在http://<job-manager-ip>:8081/上检查 Web UI。

以下是 Flink Web UI 的一些快照:

启动守护程序

您可以单击作业管理器链接以获取以下视图:

启动守护程序

同样,您可以按以下方式查看任务管理器视图:

启动守护程序

添加额外的作业/任务管理器

Flink 为您提供了向正在运行的集群添加额外的作业和任务管理器实例的功能。

在启动守护程序之前,请确保您已按照先前给出的步骤进行操作。

要向现有集群添加额外的作业管理器,请执行以下命令:

sudo bin/jobmanager.sh start cluster

同样,我们需要执行以下命令以添加额外的任务管理器:

sudo bin/taskmanager.sh start cluster

停止守护程序和集群

作业执行完成后,您希望关闭集群。 以下命令用于此目的。

要一次停止整个集群:

sudo bin/stop-cluster.sh

要停止单个作业管理器:

sudo bin/jobmanager.sh stop cluster

要停止单个任务管理器:

sudo bin/taskmanager.sh stop cluster

运行示例应用程序

Flink 二进制文件附带了一个示例应用程序,可以直接使用。 让我们从一个非常简单的应用程序开始,单词计数。 在这里,我们将尝试一个从特定端口上的 netcat 服务器读取数据的流式应用程序。

让我们开始吧。 首先通过执行以下命令在端口9000上启动 netcat 服务器:

nc -l 9000

现在 netcat 服务器将开始监听端口 9000,所以无论您在命令提示符上输入什么都将被发送到 Flink 处理中。

接下来,我们需要启动 Flink 示例程序以侦听 netcat 服务器。 以下是命令:

bin/flink run examples/streaming/SocketTextStreamWordCount.jar --
hostname localhost --port 9000
08/06/2016 10:32:40     Job execution switched to status RUNNING.
08/06/2016 10:32:40     Source: Socket Stream -> Flat Map(1/1)   
switched to SCHEDULED
08/06/2016 10:32:40     Source: Socket Stream -> Flat Map(1/1) 
switched to DEPLOYING
08/06/2016 10:32:40     Keyed Aggregation -> Sink: Unnamed(1/1) 
switched to SCHEDULED
08/06/2016 10:32:40     Keyed Aggregation -> Sink: Unnamed(1/1) 
switched to DEPLOYING
08/06/2016 10:32:40     Source: Socket Stream -> Flat Map(1/1) 
switched to RUNNING
08/06/2016 10:32:40     Keyed Aggregation -> Sink: Unnamed(1/1) 
switched to RUNNING

这将启动 Flink 作业执行。 现在您可以在 netcat 控制台上输入一些内容,Flink 将对其进行处理。

例如,在 netcat 服务器上键入以下内容:

$nc -l 9000
hi Hello
Hello World
This distribution includes cryptographic software.  The country in
which you currently reside may have restrictions on the import,
possession, use, and/or re-export to another country, of
encryption software.  BEFORE using any encryption software, please
check your country's laws, regulations and policies concerning the
import, possession, or use, and re-export of encryption software,   
to
see if this is permitted.  See <http://www.wassenaar.org/> for    
more
information.

您可以在日志中验证输出:

$ tail -f flink-*-taskmanager-*-flink-instance-*.out
==> flink-root-taskmanager-0-flink-instance-1.out <== 
(see,2) 
(http,1) 
(www,1) 
(wassenaar,1) 
(org,1) 
(for,1) 
(more,1) 
(information,1) 
(hellow,1) 
(world,1) 

==> flink-root-taskmanager-1-flink-instance-1.out <== 
(is,1) 
(permitted,1) 
(see,2) 
(http,1)
(www,1) 
(wassenaar,1) 
(org,1) 
(for,1) 
(more,1) 
(information,1) 

==> flink-root-taskmanager-2-flink-instance-1.out <== 
(hello,1) 
(worlds,1) 
(hi,1) 
(how,1) 
(are,1) 
(you,1) 
(how,2) 
(is,1) 
(it,1) 
(going,1)

您还可以查看 Flink Web UI,以查看作业的执行情况。 以下屏幕截图显示了执行的数据流计划:

运行示例应用程序

在作业执行中,Flink 有两个运算符。 第一个是源运算符,它从 Socket 流中读取数据。 第二个运算符是转换运算符,它聚合单词的计数。

我们还可以查看作业执行的时间轴:

运行示例应用程序

摘要

在本章中,我们讨论了 Flink 如何作为大学项目开始,然后成为一款成熟的企业级数据处理平台。 我们查看了 Flink 架构的细节以及其处理模型的工作原理。 我们还学会了如何在本地和集群模式下运行 Flink。

在下一章中,我们将学习 Flink 的流式 API,并查看其细节以及如何使用该 API 来解决我们的数据流处理问题。

第二章:使用 DataStream API 进行数据处理

实时分析目前是一个重要问题。许多不同的领域需要实时处理数据。到目前为止,已经有多种技术试图提供这种能力。像 Storm 和 Spark 这样的技术已经在市场上存在很长时间了。源自物联网IoT)的应用程序需要实时或几乎实时地存储、处理和分析数据。为了满足这些需求,Flink 提供了一个名为 DataStream API 的流数据处理 API。

在本章中,我们将详细了解 DataStream API 的相关细节,涵盖以下主题:

  • 执行环境

  • 数据源

  • 转换

  • 数据汇

  • 连接器

  • 用例 - 传感器数据分析

任何 Flink 程序都遵循以下定义的解剖结构:

使用 DataStream API 进行数据处理

我们将逐步了解每个步骤以及如何使用此解剖结构的 DataStream API。

执行环境

为了开始编写 Flink 程序,我们首先需要获取现有的执行环境或创建一个。根据您要做什么,Flink 支持:

  • 获取已存在的 Flink 环境

  • 创建本地环境

  • 创建远程环境

通常情况下,您只需要使用getExecutionEnvironment()。这将根据您的上下文执行正确的操作。如果您在 IDE 中执行本地环境,则会启动本地执行环境。否则,如果您执行 JAR 文件,则 Flink 集群管理器将以分布式方式执行程序。

如果您想自己创建本地或远程环境,那么您也可以选择使用createLocalEnvironment()createRemoteEnvironmentString hostint portString.jar文件)等方法来执行。

数据源

数据源是 Flink 程序期望从中获取数据的位置。这是 Flink 程序解剖的第二步。Flink 支持多个预先实现的数据源函数。它还支持编写自定义数据源函数,因此可以轻松编程任何不受支持的内容。首先让我们尝试了解内置的源函数。

基于套接字

DataStream API 支持从套接字读取数据。您只需要指定要从中读取数据的主机和端口,它就会完成工作:

socketTextStream(hostName, port); 

您还可以选择指定分隔符:

socketTextStream(hostName,port,delimiter) 

您还可以指定 API 应尝试获取数据的最大次数:

socketTextStream(hostName,port,delimiter, maxRetry) 

基于文件

您还可以选择使用 Flink 中基于文件的源函数从文件源中流式传输数据。您可以使用readTextFile(String path)从指定路径的文件中流式传输数据。默认情况下,它将读取TextInputFormat并逐行读取字符串。

如果文件格式不是文本,您可以使用这些函数指定相同的内容:

readFile(FileInputFormat<Out> inputFormat, String path) 

Flink 还支持读取文件流,因为它们使用readFileStream()函数生成:

readFileStream(String filePath, long intervalMillis, FileMonitoringFunction.WatchType watchType) 

您只需要指定文件路径、轮询间隔(应轮询文件路径的时间间隔)和观察类型。观察类型包括三种类型:

  • 当系统应该仅处理新文件时,使用FileMonitoringFunction.WatchType.ONLY_NEW_FILES

  • 当系统应该仅处理文件的附加内容时,使用FileMonitoringFunction.WatchType.PROCESS_ONLY_APPENDED

  • 当系统应该重新处理文件的附加内容以及文件中的先前内容时,使用FileMonitoringFunction.WatchType.REPROCESS_WITH_APPENDED

如果文件不是文本文件,那么我们可以使用以下函数,它让我们定义文件输入格式:

readFile(fileInputFormat, path, watchType, interval, pathFilter, typeInfo) 

在内部,它将读取文件任务分为两个子任务。一个子任务仅基于给定的WatchType监视文件路径。第二个子任务并行进行实际的文件读取。监视文件路径的子任务是一个非并行子任务。它的工作是根据轮询间隔不断扫描文件路径,并报告要处理的文件,拆分文件,并将拆分分配给相应的下游线程:

基于文件的

转换

数据转换将数据流从一种形式转换为另一种形式。输入可以是一个或多个数据流,输出也可以是零个、一个或多个数据流。现在让我们逐个尝试理解每个转换。

映射

这是最简单的转换之一,其中输入是一个数据流,输出也是一个数据流。

在 Java 中:

inputStream.map(new MapFunction<Integer, Integer>() { 
  @Override 
  public Integer map(Integer value) throws Exception { 
        return 5 * value; 
      } 
    }); 

在 Scala 中:

inputStream.map { x => x * 5 } 

FlatMap

FlatMap 接受一个记录并输出零个、一个或多个记录。

在 Java 中:

inputStream.flatMap(new FlatMapFunction<String, String>() { 
    @Override 
    public void flatMap(String value, Collector<String> out) 
        throws Exception { 
        for(String word: value.split(" ")){ 
            out.collect(word); 
        } 
    } 
}); 

在 Scala 中:

inputStream.flatMap { str => str.split(" ") } 

过滤

过滤函数评估条件,然后,如果结果为真,则仅发出记录。过滤函数可以输出零条记录。

在 Java 中:

inputStream.filter(new FilterFunction<Integer>() { 
    @Override 
    public boolean filter(Integer value) throws Exception { 
        return value != 1; 
    } 
}); 

在 Scala 中:

inputStream.filter { _ != 1 } 

KeyBy

KeyBy 根据键逻辑地将流分区。在内部,它使用哈希函数来分区流。它返回KeyedDataStream

在 Java 中:

inputStream.keyBy("someKey"); 

在 Scala 中:

inputStream.keyBy("someKey") 

减少

Reduce 通过将上次减少的值与当前值进行减少来展开KeyedDataStream。以下代码执行了KeyedDataStream的求和减少。

在 Java 中:

keyedInputStream. reduce(new ReduceFunction<Integer>() { 
    @Override 
    public Integer reduce(Integer value1, Integer value2) 
    throws Exception { 
        return value1 + value2; 
    } 
}); 

在 Scala 中:

keyedInputStream. reduce { _ + _ } 

折叠

Fold 通过将上次的文件夹流与当前记录组合起来来展开KeyedDataStream。它发出一个数据流。

在 Java 中:

keyedInputStream keyedStream.fold("Start", new FoldFunction<Integer, String>() { 
    @Override 
    public String fold(String current, Integer value) { 
        return current + "=" + value; 
    } 
  }); 

在 Scala 中:

keyedInputStream.fold("Start")((str, i) => { str + "=" + i }) 

应用于流(1,2,3,4,5)的前面给定的函数将发出这样的流:Start=1=2=3=4=5

聚合

DataStream API 支持各种聚合,如minmaxsum等。这些函数可以应用于KeyedDataStream,以便进行滚动聚合。

在 Java 中:

keyedInputStream.sum(0) 
keyedInputStream.sum("key") 
keyedInputStream.min(0) 
keyedInputStream.min("key") 
keyedInputStream.max(0) 
keyedInputStream.max("key") 
keyedInputStream.minBy(0) 
keyedInputStream.minBy("key") 
keyedInputStream.maxBy(0) 
keyedInputStream.maxBy("key") 

在 Scala 中:

keyedInputStream.sum(0) 
keyedInputStream.sum("key") 
keyedInputStream.min(0) 
keyedInputStream.min("key") 
keyedInputStream.max(0) 
keyedInputStream.max("key") 
keyedInputStream.minBy(0) 
keyedInputStream.minBy("key") 
keyedInputStream.maxBy(0) 
keyedInputStream.maxBy("key") 

maxmaxBy之间的区别在于 max 返回流中的最大值,但maxBy返回具有最大值的键。对minminBy也适用相同的规则。

窗口

window函数允许按时间或其他条件对现有的KeyedDataStreams进行分组。以下转换通过 10 秒的时间窗口发出记录组。

在 Java 中:

inputStream.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(10))); 

在 Scala 中:

inputStream.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(10))) 

Flink 定义了数据的切片,以处理(可能是)无限的数据流。这些切片称为窗口。这种切片有助于通过应用转换来以块的方式处理数据。要对流进行窗口处理,我们需要分配一个键,以便进行分发,并且需要一个描述在窗口流上执行什么转换的函数。

要将流切片成窗口,我们可以使用预先实现的 Flink 窗口分配器。我们有选项,如滚动窗口、滑动窗口、全局和会话窗口。Flink 还允许您通过扩展WindowAssginer类来编写自定义窗口分配器。让我们尝试理解这些各种分配器是如何工作的。

全局窗口

全局窗口是永不结束的窗口,除非由触发器指定。通常在这种情况下,每个元素都分配给一个单一的按键全局窗口。如果我们不指定任何触发器,将永远不会触发任何计算。

滚动窗口

根据特定时间创建滚动窗口。它们是固定长度的窗口,不重叠。当您需要在特定时间内对元素进行计算时,滚动窗口应该是有用的。例如,10 分钟的滚动窗口可用于计算在 10 分钟内发生的一组事件。

滑动窗口

滑动窗口类似于滚动窗口,但它们是重叠的。它们是固定长度的窗口,通过用户给定的窗口滑动参数与前一个窗口重叠。当您想要计算在特定时间范围内发生的一组事件时,这种窗口处理非常有用。

会话窗口

会话窗口在需要根据输入数据决定窗口边界时非常有用。会话窗口允许窗口开始时间和窗口大小的灵活性。我们还可以提供会话间隙配置参数,指示在考虑会话关闭之前等待多长时间。

WindowAll

windowAll函数允许对常规数据流进行分组。通常这是一个非并行的数据转换,因为它在非分区数据流上运行。

在 Java 中:

inputStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(10))); 

在 Scala 中:

inputStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(10))) 

与常规数据流函数类似,我们也有窗口数据流函数。唯一的区别是它们适用于窗口化的数据流。因此,窗口缩减类似于Reduce函数,窗口折叠类似于Fold函数,还有聚合函数。

联合

Union函数执行两个或多个数据流的并集。这会并行地组合数据流。如果我们将一个流与自身组合,则每个记录都会输出两次。

在 Java 中:

inputStream. union(inputStream1, inputStream2, ...); 

在 Scala 中:

inputStream. union(inputStream1, inputStream2, ...) 

窗口连接

我们还可以通过一些键在一个公共窗口中连接两个数据流。下面的示例显示了在5秒的窗口中连接两个流的情况,其中第一个流的第一个属性的连接条件等于另一个流的第二个属性。

在 Java 中:

inputStream. join(inputStream1) 
   .where(0).equalTo(1) 
    .window(TumblingEventTimeWindows.of(Time.seconds(5))) 
    .apply (new JoinFunction () {...}); 

在 Scala 中:

inputStream. join(inputStream1) 
    .where(0).equalTo(1) 
    .window(TumblingEventTimeWindows.of(Time.seconds(5))) 
    .apply { ... }

分割

此函数根据条件将流拆分为两个或多个流。当您获得混合流并且可能希望分别处理每个数据时,可以使用此函数。

在 Java 中:

SplitStream<Integer> split = inputStream.split(new OutputSelector<Integer>() { 
    @Override 
    public Iterable<String> select(Integer value) { 
        List<String> output = new ArrayList<String>(); 
        if (value % 2 == 0) { 
            output.add("even"); 
        } 
        else { 
            output.add("odd"); 
        } 
        return output; 
    } 
}); 

在 Scala 中:

val split = inputStream.split( 
  (num: Int) => 
    (num % 2) match { 
      case 0 => List("even") 
      case 1 => List("odd") 
    } 
) 

选择

此函数允许您从拆分流中选择特定流。

在 Java 中:

SplitStream<Integer> split; 
DataStream<Integer> even = split.select("even"); 
DataStream<Integer> odd = split.select("odd"); 
DataStream<Integer> all = split.select("even","odd"); 

在 Scala 中:

val even = split select "even" 
val odd = split select "odd" 
val all = split.select("even","odd") 

项目

Project函数允许您从事件流中选择一部分属性,并仅将选定的元素发送到下一个处理流。

在 Java 中:

DataStream<Tuple4<Integer, Double, String, String>> in = // [...] 
DataStream<Tuple2<String, String>> out = in.project(3,2); 

在 Scala 中:

val in : DataStream[(Int,Double,String)] = // [...] 
val out = in.project(3,2) 

前面的函数从给定记录中选择属性编号23。以下是示例输入和输出记录:

(1,10.0, A, B )=> (B,A) 
(2,20.0, C, D )=> (D,C) 

物理分区

Flink 允许我们对流数据进行物理分区。您可以选择提供自定义分区。让我们看看不同类型的分区。

自定义分区

如前所述,您可以提供分区器的自定义实现。

在 Java 中:

inputStream.partitionCustom(partitioner, "someKey"); 
inputStream.partitionCustom(partitioner, 0); 

在 Scala 中:

inputStream.partitionCustom(partitioner, "someKey") 
inputStream.partitionCustom(partitioner, 0) 

在编写自定义分随机器时,您需要确保实现有效的哈希函数。

随机分区

随机分区以均匀的方式随机分区数据流。

在 Java 中:

inputStream.shuffle(); 

在 Scala 中:

inputStream.shuffle() 

重新平衡分区

这种类型的分区有助于均匀分布数据。它使用轮询方法进行分发。当数据发生偏斜时,这种类型的分区是很好的。

在 Java 中:

inputStream.rebalance(); 

在 Scala 中:

inputStream.rebalance() 

重新缩放

重新缩放用于在操作之间分发数据,对数据子集执行转换并将它们组合在一起。这种重新平衡仅在单个节点上进行,因此不需要在网络上进行任何数据传输。

以下图表显示了分布情况:

重新缩放

在 Java 中:

inputStream.rescale(); 

在 Scala 中:

inputStream.rescale() 

广播

广播将所有记录分发到每个分区。这会将每个元素扩展到所有分区。

在 Java 中:

inputStream.broadcast(); 

在 Scala 中:

inputStream.broadcast() 

数据接收器

数据转换完成后,我们需要将结果保存到某个地方。以下是 Flink 提供的一些保存结果的选项:

  • writeAsText(): 逐行将记录写为字符串。

  • writeAsCsV(): 将元组写为逗号分隔值文件。还可以配置行和字段分隔符。

  • print()/printErr(): 将记录写入标准输出。您也可以选择写入标准错误。

  • writeUsingOutputFormat(): 您还可以选择提供自定义输出格式。在定义自定义格式时,您需要扩展负责序列化和反序列化的OutputFormat

  • writeToSocket(): Flink 还支持将数据写入特定的套接字。需要定义SerializationSchema以进行适当的序列化和格式化。

事件时间和水印

Flink Streaming API 受到 Google Data Flow 模型的启发。它支持其流式 API 的不同时间概念。一般来说,在流式环境中有三个地方可以捕获时间。它们如下

事件时间

事件发生的时间是指其产生设备上的时间。例如,在物联网项目中,传感器捕获读数的时间。通常这些事件时间需要在记录进入 Flink 之前嵌入。在处理时,这些时间戳被提取并考虑用于窗口处理。事件时间处理可以用于无序事件。

处理时间

处理时间是机器执行数据处理流的时间。处理时间窗口只考虑事件被处理的时间戳。处理时间是流处理的最简单方式,因为它不需要处理机器和生产机器之间的任何同步。在分布式异步环境中,处理时间不提供确定性,因为它取决于记录在系统中流动的速度。

摄取时间

这是特定事件进入 Flink 的时间。所有基于时间的操作都参考这个时间戳。摄取时间比处理时间更昂贵,但它提供可预测的结果。摄取时间程序无法处理任何无序事件,因为它只在事件进入 Flink 系统后分配时间戳。

以下是一个示例,显示了如何设置事件时间和水印。在摄取时间和处理时间的情况下,我们只需要时间特征,水印生成会自动处理。以下是相同的代码片段。

在 Java 中:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime); 
//or 
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime); 

在 Scala 中:

val env = StreamExecutionEnvironment.getExecutionEnvironment 
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime) 
//or  
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime) 

在事件时间流程序中,我们需要指定分配水印和时间戳的方式。有两种分配水印和时间戳的方式:

  • 直接从数据源属性

  • 使用时间戳分配器

要使用事件时间流,我们需要按照以下方式分配时间特征

在 Java 中:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime; 

在 Scala 中:

val env = StreamExecutionEnvironment.getExecutionEnvironment 
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) 

在存储记录时,最好同时存储事件时间。Flink 还支持一些预定义的时间戳提取器和水印生成器。参考ci.apache.org/projects/flink/flink-docs-release-1.2/dev/event_timestamp_extractors.html

连接器

Apache Flink 支持允许在各种技术之间读取/写入数据的各种连接器。让我们更多地了解这一点。

Kafka 连接器

Kafka 是一个发布-订阅的分布式消息队列系统,允许用户向特定主题发布消息;然后将其分发给主题的订阅者。Flink 提供了在 Flink Streaming 中将 Kafka 消费者定义为数据源的选项。为了使用 Flink Kafka 连接器,我们需要使用特定的 JAR 文件。

以下图表显示了 Flink Kafka 连接器的工作原理:

Kafka 连接器

我们需要使用以下 Maven 依赖项来使用连接器。我一直在使用 Kafka 版本 0.9,所以我将在pom.xml中添加以下依赖项:

<dependency> 
  <groupId>org.apache.flink</groupId> 
  <artifactId>flink-connector-kafka-0.9_2.11/artifactId> 
  <version>1.1.4</version> 
</dependency> 

现在让我们试着理解如何将 Kafka 消费者作为 Kafka 源来使用:

在 Java 中:

Properties properties = new Properties(); 
  properties.setProperty("bootstrap.servers", "localhost:9092"); 
  properties.setProperty("group.id", "test"); 
DataStream<String> input  = env.addSource(new FlinkKafkaConsumer09<String>("mytopic", new SimpleStringSchema(), properties)); 

在 Scala 中:

val properties = new Properties(); 
properties.setProperty("bootstrap.servers", "localhost:9092"); 
// only required for Kafka 0.8 
properties.setProperty("zookeeper.connect", "localhost:2181"); 
properties.setProperty("group.id", "test"); 
stream = env 
    .addSource(new FlinkKafkaConsumer09String, properties)) 
    .print 

在上述代码中,我们首先设置了 Kafka 主机和 zookeeper 主机和端口的属性。接下来,我们需要指定主题名称,在本例中为mytopic。因此,如果任何消息发布到mytopic主题,它们将被 Flink 流处理。

如果您以不同的格式获取数据,那么您也可以为反序列化指定自定义模式。默认情况下,Flink 支持字符串和 JSON 反序列化器。

为了实现容错,我们需要在 Flink 中启用检查点。Flink 会定期对状态进行快照。在发生故障时,它将恢复到最后一个检查点,然后重新启动处理。

我们还可以将 Kafka 生产者定义为接收器。这将把数据写入 Kafka 主题。以下是将数据写入 Kafka 主题的方法:

在 Scala 中:

stream.addSink(new FlinkKafkaProducer09<String>("localhost:9092", "mytopic", new SimpleStringSchema())); 

在 Java 中:

stream.addSink(new FlinkKafkaProducer09String)) 

Twitter 连接器

如今,从 Twitter 获取数据并处理数据非常重要。许多公司使用 Twitter 数据来进行各种产品、服务、电影、评论等的情感分析。Flink 提供 Twitter 连接器作为一种数据源。要使用连接器,您需要拥有 Twitter 账户。一旦您拥有了 Twitter 账户,您需要创建一个 Twitter 应用程序并生成用于连接器的身份验证密钥。以下是一个链接,可以帮助您生成令牌:dev.twitter.com/oauth/overview/application-owner-access-tokens

Twitter 连接器可以通过 Java 或 Scala API 使用:

Twitter 连接器

生成令牌后,我们可以开始编写程序从 Twitter 获取数据。首先我们需要添加一个 Maven 依赖项:

<dependency> 
  <groupId>org.apache.flink</groupId> 
  <artifactId>flink-connector-twitter_2.11/artifactId> 
  <version>1.1.4</version> 
</dependency> 

接下来我们将 Twitter 作为数据源。以下是示例代码:

在 Java 中:

Properties props = new Properties(); 
props.setProperty(TwitterSource.CONSUMER_KEY, ""); 
props.setProperty(TwitterSource.CONSUMER_SECRET, ""); 
props.setProperty(TwitterSource.TOKEN, ""); 
props.setProperty(TwitterSource.TOKEN_SECRET, ""); 
DataStream<String> streamSource = env.addSource(new TwitterSource(props)); 

在 Scala 中:

val props = new Properties(); 
props.setProperty(TwitterSource.CONSUMER_KEY, ""); 
props.setProperty(TwitterSource.CONSUMER_SECRET, ""); 
props.setProperty(TwitterSource.TOKEN, ""); 
props.setProperty(TwitterSource.TOKEN_SECRET, ""); 
DataStream<String> streamSource = env.addSource(new TwitterSource(props)); 

在上述代码中,我们首先为我们得到的令牌设置属性。然后我们添加TwitterSource。如果给定的信息是正确的,那么您将开始从 Twitter 获取数据。TwitterSource以 JSON 字符串格式发出数据。示例 Twitter JSON 如下所示:

{ 
... 
"text": ""Loyalty 3.0: How to Revolutionize Customer &amp; Employee Engagement with Big Data &amp; #Gamification" can be ordered here: http://t.co/1XhqyaNjuR", 
  "geo": null, 
  "retweeted": false, 
  "in_reply_to_screen_name": null, 
  "possibly_sensitive": false, 
  "truncated": false, 
  "lang": "en", 
    "hashtags": [{ 
      "text": "Gamification", 
      "indices": [90, 
      103] 
    }], 
  }, 
  "in_reply_to_status_id_str": null, 
  "id": 330094515484508160 
... 
} 

TwitterSource提供各种端点。默认情况下,它使用StatusesSampleEndpoint,返回一组随机推文。如果您需要添加一些过滤器,并且不想使用默认端点,可以实现TwitterSource.EndpointInitializer接口。

现在我们知道如何从 Twitter 获取数据,然后可以根据我们的用例决定如何处理这些数据。我们可以处理、存储或分析数据。

RabbitMQ 连接器

RabbitMQ 是一个广泛使用的分布式、高性能的消息队列系统。它用作高吞吐量操作的消息传递系统。它允许您创建分布式消息队列,并在队列中包括发布者和订阅者。可以在以下链接进行更多关于 RabbitMQ 的阅读www.rabbitmq.com/

Flink 支持从 RabbitMQ 获取和发布数据。它提供了一个连接器,可以作为数据流的数据源。

为了使 RabbitMQ 连接器工作,我们需要提供以下信息:

  • RabbitMQ 配置,如主机、端口、用户凭据等。

  • 队列,您希望订阅的 RabbitMQ 队列的名称。

  • 关联 ID 是 RabbitMQ 的一个特性,用于在分布式世界中通过唯一 ID 相关请求和响应。Flink RabbitMQ 连接器提供了一个接口,可以根据您是否使用它来设置为 true 或 false。

  • 反序列化模式--RabbitMQ 以序列化方式存储和传输数据,以避免网络流量。因此,当接收到消息时,订阅者应该知道如何反序列化消息。Flink 连接器为我们提供了一些默认的反序列化器,如字符串反序列化器。

RabbitMQ 源为我们提供了以下关于流传递的选项:

  • 确切一次:使用 RabbitMQ 关联 ID 和 Flink 检查点机制与 RabbitMQ 事务

  • 至少一次:当启用 Flink 检查点但未设置 RabbitMQ 关联 ID 时

  • RabbitMQ 自动提交模式没有强有力的交付保证

以下是一个图表,可以帮助您更好地理解 RabbitMQ 连接器:

RabbitMQ 连接器

现在让我们看看如何编写代码来使这个连接器工作。与其他连接器一样,我们需要向代码添加一个 Maven 依赖项:

<dependency> 
  <groupId>org.apache.flink</groupId> 
  <artifactId>flink-connector-rabbitmq_2.11/artifactId> 
  <version>1.1.4</version> 
</dependency> 

以下代码段显示了如何在 Java 中使用 RabbitMQ 连接器:

//Configurations 
RMQConnectionConfig connectionConfig = new RMQConnectionConfig.Builder() 
.setHost(<host>).setPort(<port>).setUserName(..) 
.setPassword(..).setVirtualHost("/").build(); 

//Get Data Stream without correlation ids 
DataStream<String> streamWO = env.addSource(new RMQSource<String>(connectionConfig, "my-queue", new SimpleStringSchema())) 
  .print 
//Get Data Stream with correlation ids 
DataStream<String> streamW = env.addSource(new RMQSource<String>(connectionConfig, "my-queue", true, new SimpleStringSchema())) 
  .print 

同样,在 Scala 中,代码可以写成如下形式:

val connectionConfig = new RMQConnectionConfig.Builder() 
.setHost(<host>).setPort(<port>).setUserName(..) 
.setPassword(..).setVirtualHost("/").build() 
streamsWOIds = env 
    .addSource(new RMQSourceString) 
    .print 

streamsWIds = env 
    .addSource(new RMQSourceString) 
    .print 

我们还可以使用 RabbitMQ 连接器作为 Flink sink。如果要将处理过的数据发送回不同的 RabbitMQ 队列,可以按以下方式操作。我们需要提供三个重要的配置:

  • RabbitMQ 配置

  • 队列名称--要将处理过的数据发送回哪里

  • 序列化模式--RabbitMQ 的模式,将数据转换为字节

以下是 Java 中的示例代码,展示了如何将此连接器用作 Flink sink:

RMQConnectionConfig connectionConfig = new RMQConnectionConfig.Builder() 
.setHost(<host>).setPort(<port>).setUserName(..) 
.setPassword(..).setVirtualHost("/").build(); 
stream.addSink(new RMQSink<String>(connectionConfig, "target-queue", new StringToByteSerializer())); 

在 Scala 中也可以这样做:

val connectionConfig = new RMQConnectionConfig.Builder() 
.setHost(<host>).setPort(<port>).setUserName(..) 
.setPassword(..).setVirtualHost("/").build() 
stream.addSink(new RMQSinkString。

在许多用例中,您可能希望使用 Flink 处理数据,然后将其存储在 ElasticSearch 中。为此,Flink 支持 ElasticSearch 连接器。到目前为止,ElasticSearch 已经发布了两个主要版本。Flink 支持它们两个。

对于 ElasticSearch 1.X,需要添加以下 Maven 依赖项:

```java
<dependency> 
  <groupId>org.apache.flink</groupId> 
  <artifactId>flink-connector-elasticsearch_2.11</artifactId> 
  <version>1.1.4</version> 
</dependency> 

Flink 连接器提供了一个 sink,用于将数据写入 ElasticSearch。它使用两种方法连接到 ElasticSearch:

  • 嵌入式节点

  • 传输客户端

以下图表说明了这一点:

ElasticSearch 连接器

嵌入式节点模式

在嵌入式节点模式中,sink 使用 BulkProcessor 将文档发送到 ElasticSearch。我们可以配置在将文档发送到 ElasticSearch 之前缓冲多少个请求。

以下是代码片段:

DataStream<String> input = ...; 

Map<String, String> config = Maps.newHashMap(); 
config.put("bulk.flush.max.actions", "1"); 
config.put("cluster.name", "cluster-name"); 

input.addSink(new ElasticsearchSink<>(config, new IndexRequestBuilder<String>() { 
    @Override 
    public IndexRequest createIndexRequest(String element, RuntimeContext ctx) { 
        Map<String, Object> json = new HashMap<>(); 
        json.put("data", element); 

        return Requests.indexRequest() 
                .index("my-index") 
                .type("my-type") 
                .source(json); 
    } 
})); 

在上述代码片段中,我们创建了一个哈希映射,其中包含集群名称以及在发送请求之前要缓冲多少个文档的配置。然后我们将 sink 添加到流中,指定要存储的索引、类型和文档。在 Scala 中也有类似的代码:

val input: DataStream[String] = ... 

val config = new util.HashMap[String, String] 
config.put("bulk.flush.max.actions", "1") 
config.put("cluster.name", "cluster-name") 

text.addSink(new ElasticsearchSink(config, new IndexRequestBuilder[String] { 
  override def createIndexRequest(element: String, ctx: RuntimeContext): IndexRequest = { 
    val json = new util.HashMap[String, AnyRef] 
    json.put("data", element) 
    Requests.indexRequest.index("my-index").`type`("my-type").source(json) 
  } 
})) 

传输客户端模式

ElasticSearch 允许通过端口 9300 的传输客户端进行连接。Flink 支持通过其连接器使用这些连接。这里唯一需要提到的是配置中存在的所有 ElasticSearch 节点。

以下是 Java 中的片段:

DataStream<String> input = ...; 

Map<String, String> config = Maps.newHashMap(); 
config.put("bulk.flush.max.actions", "1"); 
config.put("cluster.name", "cluster-name"); 

List<TransportAddress> transports = new ArrayList<String>(); 
transports.add(new InetSocketTransportAddress("es-node-1", 9300)); 
transports.add(new InetSocketTransportAddress("es-node-2", 9300)); 
transports.add(new InetSocketTransportAddress("es-node-3", 9300)); 

input.addSink(new ElasticsearchSink<>(config, transports, new IndexRequestBuilder<String>() { 
    @Override 
    public IndexRequest createIndexRequest(String element, RuntimeContext ctx) { 
        Map<String, Object> json = new HashMap<>(); 
        json.put("data", element); 

        return Requests.indexRequest() 
                .index("my-index") 
                .type("my-type") 
                .source(json); 
    } 
})); 

在这里,我们还提供了有关集群名称、节点、端口、发送的最大请求数等的详细信息。在 Scala 中,类似的代码可以编写如下:

val input: DataStream[String] = ... 

val config = new util.HashMap[String, String] 
config.put("bulk.flush.max.actions", "1") 
config.put("cluster.name", "cluster-name") 

val transports = new ArrayList[String] 
transports.add(new InetSocketTransportAddress("es-node-1", 9300)) 
transports.add(new InetSocketTransportAddress("es-node-2", 9300)) 
transports.add(new InetSocketTransportAddress("es-node-3", 9300)) 

text.addSink(new ElasticsearchSink(config, transports, new IndexRequestBuilder[String] { 
  override def createIndexRequest(element: String, ctx: RuntimeContext): IndexRequest = { 
    val json = new util.HashMap[String, AnyRef] 
    json.put("data", element) 
    Requests.indexRequest.index("my-index").`type`("my-type").source(json) 
  } 
})) 

Cassandra 连接器

Cassandra 是一个分布式、低延迟的 NoSQL 数据库。它是一个基于键值的数据库。许多高吞吐量应用程序将 Cassandra 用作其主要数据库。Cassandra 使用分布式集群模式,其中没有主从架构。任何节点都可以进行读取和写入。有关 Cassandra 的更多信息可以在此处找到:cassandra.apache.org/

Apache Flink 提供了一个连接器,可以将数据写入 Cassandra。在许多应用程序中,人们可能希望将来自 Flink 的流数据存储到 Cassandra 中。以下图表显示了 Cassandra sink 的简单设计:

Cassandra 连接器

与其他连接器一样,要获得此连接器,我们需要将其添加为 Maven 依赖项:

<dependency> 
  <groupId>org.apache.flink</groupId> 
  <artifactId>flink-connector-cassandra_2.11</artifactId> 
  <version>1.1.4</version> 
</dependency>

一旦添加了依赖项,我们只需要添加 Cassandra sink 及其配置,如下所示:

在 Java 中:

CassandraSink.addSink(input) 
  .setQuery("INSERT INTO cep.events (id, message) values (?, ?);") 
  .setClusterBuilder(new ClusterBuilder() { 
    @Override 
    public Cluster buildCluster(Cluster.Builder builder) { 
      return builder.addContactPoint("127.0.0.1").build(); 
    } 
  }) 
  .build() 

上述代码将数据流写入名为events的表中。该表期望事件 ID 和消息。在 Scala 中也是如此:

CassandraSink.addSink(input) 
  .setQuery("INSERT INTO cep.events (id, message) values (?, ?);") 
  .setClusterBuilder(new ClusterBuilder() { 
    @Override 
    public Cluster buildCluster(Cluster.Builder builder) { 
      return builder.addContactPoint("127.0.0.1").build(); 
    } 
  }) 
  .build(); 

用例 - 传感器数据分析

既然我们已经看过了 DataStream API 的各个方面,让我们尝试使用这些概念来解决一个真实的用例。考虑一个安装了传感器的机器,我们希望从这些传感器收集数据,并计算每五分钟每个传感器的平均温度。

以下是架构:

用例 - 传感器数据分析

在这种情况下,我们假设传感器正在向名为temp的 Kafka 主题发送信息,信息格式为(时间戳,温度,传感器 ID)。现在我们需要编写代码从 Kafka 主题中读取数据,并使用 Flink 转换进行处理。

在这里需要考虑的重要事情是,由于我们已经从传感器那里得到了时间戳数值,我们可以使用事件时间计算来处理时间因素。这意味着即使事件到达时是无序的,我们也能够处理这些事件。

我们从简单的流执行环境开始,它将从 Kafka 中读取数据。由于事件中有时间戳,我们将编写自定义的时间戳和水印提取器来读取时间戳数值,并根据此进行窗口处理。以下是相同的代码片段。

// set up the streaming execution environment 
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 
// env.enableCheckpointing(5000); 
nv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime); 
Properties properties = new Properties(); 
properties.setProperty("bootstrap.servers", "localhost:9092"); 

properties.setProperty("zookeeper.connect", "localhost:2181"); 
properties.setProperty("group.id", "test"); 

FlinkKafkaConsumer09<String> myConsumer = new FlinkKafkaConsumer09<>("temp", new SimpleStringSchema(), 
                      properties); 
myConsumer.assignTimestampsAndWatermarks(new CustomWatermarkEmitter()); 

在这里,我们假设我们从 Kafka 主题中以字符串格式接收事件,并且格式为:

Timestamp,Temperature,Sensor-Id

以下是从记录中提取时间戳的示例代码:

public class CustomWatermarkEmitter implements AssignerWithPunctuatedWatermarks<String> {
    private static final long serialVersionUID = 1L;

    @Override
    public long extractTimestamp(String arg0, long arg1) {
        if (null != arg0 && arg0.contains(",")) {
           String parts[] = arg0.split(",");
           return Long.parseLong(parts[0]);
           }

          return 0;
    }
    @Override
    public Watermark checkAndGetNextWatermark(String arg0, long arg1) {
        if (null != arg0 && arg0.contains(",")) {
            String parts[] = arg0.split(",");
            return new Watermark(Long.parseLong(parts[0]));
        }
        return null;
    }
}

现在我们简单地创建了分区数据流,并对温度数值进行了平均计算,如下面的代码片段所示:

DataStream<Tuple2<String, Double>> keyedStream = env.addSource(myConsumer).flatMap(new Splitter()).keyBy(0)
.timeWindow(Time.seconds(300))
.apply(new WindowFunction<Tuple2<String, Double>, Tuple2<String, Double>, Tuple, TimeWindow>() {
    @Override
    public void apply(Tuple key, TimeWindow window, 
    Iterable<Tuple2<String, Double>> input,
    Collector<Tuple2<String, Double>> out) throws Exception {
        double sum = 0L;
            int count = 0;
            for (Tuple2<String, Double> record : input) {
                sum += record.f1;
                count++;
            }
     Tuple2<String, Double> result = input.iterator().next();
     result.f1 = (sum/count);
     out.collect(result);
   }
});

当执行上述给定的代码时,如果在 Kafka 主题上发布了适当的传感器事件,那么我们将每五分钟得到每个传感器的平均温度。

完整的代码可以在 GitHub 上找到:github.com/deshpandetanmay/mastering-flink/tree/master/chapter02/flink-streaming

总结

在本章中,我们从 Flink 最强大的 API 开始:DataStream API。我们看了数据源、转换和接收器是如何一起工作的。然后我们看了各种技术连接器,比如 ElasticSearch、Cassandra、Kafka、RabbitMQ 等等。

最后,我们还尝试将我们的学习应用于解决真实世界的传感器数据分析用例。

在下一章中,我们将学习 Flink 生态系统中另一个非常重要的 API,即 DataSet API。

第三章:使用批处理 API 进行数据处理

尽管许多人欣赏流数据处理在大多数行业中的潜在价值,但也有许多用例,人们认为不需要以流式方式处理数据。在所有这些情况下,批处理是前进的方式。到目前为止,Hadoop 一直是数据处理的默认选择。但是,Flink 也通过 DataSet API 支持批处理数据处理。

对于 Flink,批处理是流处理的一种特殊情况。在data-artisans.com/batch-is-a-special-case-of-streaming/上有一篇非常有趣的文章详细解释了这个想法。

在本章中,我们将详细了解 DataSet API 的详细信息。这包括以下主题:

  • 数据源

  • 转换

  • 数据接收器

  • 连接器

正如我们在上一章中学到的,任何 Flink 程序都遵循以下定义的解剖结构:

使用批处理 API 进行数据处理

DataSet API 也不例外。我们将详细了解每个步骤。我们已经在上一章中讨论了如何获取执行环境。因此,我们将直接转向 DataSet API 支持的数据源的详细信息。

数据源

源是 DataSet API 期望从中获取数据的地方。它可以是文件形式,也可以是来自 Java 集合。这是 Flink 程序解剖的第二步。DataSet API 支持许多预先实现的数据源函数。它还支持编写自定义数据源函数,因此可以轻松地编程任何不受支持的内容。首先让我们尝试理解内置的源函数。

基于文件的

Flink 支持从文件中读取数据。它逐行读取数据并将其作为字符串返回。以下是您可以使用的内置函数来读取数据:

  • readTextFile(Stringpath): 从指定路径读取文件中的数据。默认情况下,它将读取TextInputFormat并逐行读取字符串。

  • readTextFileWithValue(Stringpath): 从指定路径读取文件中的数据。它返回StringValuesStringValues是可变字符串。

  • readCsvFile(Stringpath): 从逗号分隔的文件中读取数据。它返回 Java POJOs 或元组。

  • readFileofPremitives(path, delimiter, class): 将新行解析为原始数据类型,如字符串或整数。

  • readHadoopFile(FileInputFormat, Key, Value, path): 从指定路径使用给定的FileInputFormatKey类和Value类读取文件。它将解析后的值返回为元组Tuple2<Key,Value>

  • readSequenceFile(Key, Value, path): 从指定路径使用给定的SequenceFileInputFormatKey类和Value类读取文件。它将解析后的值返回为元组Tuple2<Key,Value>

注意

对于基于文件的输入,Flink 支持递归遍历指定路径中的文件夹。为了使用这个功能,我们需要设置一个环境变量,并在读取数据时将其作为参数传递。要设置的变量是recursive.file.enumeration。我们需要将此变量设置为true以启用递归遍历。

基于集合的

使用 Flink DataSet API,我们还可以从基于 Java 的集合中读取数据。以下是一些我们可以使用的函数来读取数据:

  • fromCollection(Collection): 从基于 Java 的集合创建数据集。

  • fromCollection(Iterator, Class): 从迭代器创建数据集。迭代器的元素由类参数给定的类型。

  • fromElements(T): 创建一个包含一系列对象的数据集。对象类型在函数本身中指定。

  • fromParallelCollection(SplittableIterator, Class): 这将并行从迭代器创建数据集。Class 代表对象类型。

  • generateSequence(from, to): 生成给定范围内的数字序列。

通用源

DataSet API 支持一些通用函数来读取数据:

  • readFile(inputFormat, path): 这将从给定路径创建一个FileInputFormat类型的数据集

  • createInput(inputFormat): 这将创建一个通用输入格式的数据集

压缩文件

Flink 支持在读取文件时解压缩文件,如果它们标有适当的扩展名。我们不需要对读取压缩文件进行任何不同的配置。如果检测到具有适当扩展名的文件,则 Flink 会自动解压缩并发送到进一步处理。

这里需要注意的一点是,文件的解压缩不能并行进行,因此在实际数据处理开始之前可能需要一些时间。

在这个阶段,建议避免使用压缩文件,因为在 Flink 中解压缩不是可扩展的活动。

支持以下算法:

压缩算法 扩展名 是否并行?
Gzip .gz, .gzip
Deflate .deflate

转换

数据转换将数据集从一种形式转换为另一种形式。输入可以是一个或多个数据集,输出也可以是零个、一个或多个数据流。现在让我们逐个了解每个转换。

映射

这是最简单的转换之一,输入是一个数据集,输出也是一个数据集。

在 Java 中:

inputSet.map(new MapFunction<Integer, Integer>() { 
  @Override 
  public Integer map(Integer value) throws Exception { 
        return 5 * value; 
      } 
    }); 

在 Scala 中:

inputSet.map { x => x * 5 } 

在 Python 中:

inputSet.map { lambda x : x * 5 } 

Flat map

flat map 接受一个记录并输出零个、一个或多个记录。

在 Java 中:

inputSet.flatMap(new FlatMapFunction<String, String>() { 
    @Override 
    public void flatMap(String value, Collector<String> out) 
        throws Exception { 
        for(String word: value.split(" ")){ 
            out.collect(word); 
        } 
    } 
}); 

在 Scala 中:

inputSet.flatMap { str => str.split(" ") } 

在 Python 中:

inputSet.flat_map {lambda str, c:[str.split() for line in str } 

过滤

过滤函数评估条件,然后如果返回true则只发出记录。过滤函数可以输出零个记录。

在 Java 中:

inputSet.filter(new FilterFunction<Integer>() { 
    @Override 
    public boolean filter(Integer value) throws Exception { 
        return value != 1; 
    } 
}); 
In Scala: 
inputSet.filter { _ != 1 } 

在 Python 中:

inputSet.filter {lambda x: x != 1 } 

项目

项目转换删除或移动元组的元素到另一个元组。这可以用来对特定元素进行选择性处理。

在 Java 中:

DataSet<Tuple3<Integer, String, Double>> in = // [...] 
DataSet<Tuple2<String, Integer>> out = in.project(1,0); 

在 Scala 中,不支持这种转换。

在 Python 中:

inputSet.project(1,0) 

对分组数据集进行减少

减少转换根据用户定义的减少函数将每个组减少为单个元素。

在 Java 中:

public class WC { 
  public String word; 
  public int count; 
} 

//Reduce function 
public class WordCounter implements ReduceFunction<WC> { 
  @Override 
  public WC reduce(WC in1, WC in2) { 
    return new WC(in1.word, in1.count + in2.count); 
  } 
} 

// [...] 
DataSet<WC> words = // [...] 
DataSet<WC> wordCounts = words 
                         // grouping on field "word" 
                         .groupBy("word") 
                         // apply ReduceFunction on grouped DataSet 
                         .reduce(new WordCounter()); 

在 Scala 中:

class WC(val word: String, val count: Int) { 
  def this() { 
    this(null, -1) 
  } 
} 

val words: DataSet[WC] = // [...] 
val wordCounts = words.groupBy("word").reduce { 
  (w1, w2) => new WC(w1.word, w1.count + w2.count) 
} 

在 Python 中,代码不受支持。

按字段位置键对分组数据集进行减少

对于元组数据集,我们也可以按字段位置进行分组。以下是一个例子。

在 Java 中:

DataSet<Tuple3<String, Integer, Double>> reducedTuples = tuples 
                           // group by on second and third field  
                            .groupBy(1, 2) 
                            // apply ReduceFunction 
                            .reduce(new MyTupleReducer()); 

在 Scala 中:

val reducedTuples = tuples.groupBy(1, 2).reduce { ... } 

在 Python 中:

reducedTuples = tuples.group_by(1, 2).reduce( ... ) 

组合组

在一些应用中,在进行一些更多的转换之前进行中间操作非常重要。在这种情况下,组合操作非常方便。中间转换可以减小大小等。

这是使用贪婪策略在内存中执行的,需要进行多个步骤。

在 Java 中:

DataSet<String> input = [..]  

  DataSet<Tuple2<String, Integer>> combinedWords = input 
  .groupBy(0); // group similar words 
  .combineGroup(new GroupCombineFunction<String, Tuple2<String,  
   Integer>() { 

    public void combine(Iterable<String> words,   
    Collector<Tuple2<String, Integer>>) { // combine 
        String key = null; 
        int count = 0; 

        for (String word : words) { 
            key = word; 
            count++; 
        } 
        // emit tuple with word and count 
        out.collect(new Tuple2(key, count)); 
    } 
}); 

在 Scala 中:

val input: DataSet[String] = [..]  

val combinedWords: DataSet[(String, Int)] = input 
  .groupBy(0) 
  .combineGroup { 
    (words, out: Collector[(String, Int)]) => 
        var key: String = null 
        var count = 0 

        for (word <- words) { 
            key = word 
            count += 1 
        } 
        out.collect((key, count)) 
} 

在 Python 中,不支持这段代码。

对分组元组数据集进行聚合

聚合转换非常常见。我们可以很容易地对元组数据集执行常见的聚合,如summinmax。以下是我们执行的方式。

在 Java 中:

DataSet<Tuple3<Integer, String, Double>> input = // [...] 
DataSet<Tuple3<Integer, String, Double>> output = input 
             .groupBy(1)        // group DataSet on second field 
             .aggregate(SUM, 0) // compute sum of the first field 
             .and(MIN, 2);      // compute minimum of the third field 

在 Scala 中:

val input: DataSet[(Int, String, Double)] = // [...] 
val output = input.groupBy(1).aggregate(SUM, 0).and(MIN, 2) 

在 Python 中:

input = # [...] 
output = input.group_by(1).aggregate(Sum, 0).and_agg(Min, 2) 

请注意,在 DataSet API 中,如果我们需要应用多个聚合,我们需要使用and关键字。

对分组元组数据集进行 MinBy

minBy函数从元组数据集的每个组中选择一个元组,其值为最小值。用于比较的字段必须是可比较的。

在 Java 中:

DataSet<Tuple3<Integer, String, Double>> input = // [...] 
DataSet<Tuple3<Integer, String, Double>> output = input 
                  .groupBy(1)   // group by on second field 
                  .minBy(0, 2); // select tuple with minimum values for first and third field. 

在 Scala 中:

val input: DataSet[(Int, String, Double)] = // [...] 
val output: DataSet[(Int, String, Double)] = input 
           .groupBy(1)                                     
           .minBy(0, 2)

在 Python 中,不支持这段代码。

对分组元组数据集进行 MaxBy

MaxBy函数从元组数据集的每个组中选择一个元组,其值为最大值。用于比较的字段必须是可比较的。

在 Java 中:

DataSet<Tuple3<Integer, String, Double>> input = // [...] 
DataSet<Tuple3<Integer, String, Double>> output = input 
                  .groupBy(1)   // group by on second field 
                  .maxBy(0, 2); // select tuple with maximum values for         
                                /*first and third field. */

在 Scala 中:

val input: DataSet[(Int, String, Double)] = // [...] 
val output: DataSet[(Int, String, Double)] = input 
.groupBy(1)                                    
.maxBy(0, 2)  

在 Python 中,不支持这段代码。

对完整数据集进行减少

减少转换允许在整个数据集上应用用户定义的函数。以下是一个例子。

在 Java 中:

public class IntSumReducer implements ReduceFunction<Integer> { 
  @Override 
  public Integer reduce(Integer num1, Integer num2) { 
    return num1 + num2; 
  } 
} 

DataSet<Integer> intNumbers = // [...] 
DataSet<Integer> sum = intNumbers.reduce(new IntSumReducer()); 

在 Scala 中:

val sum = intNumbers.reduce (_ + _) 

在 Python 中:

sum = intNumbers.reduce(lambda x,y: x + y) 

对完整数据集进行组减少

组减少转换允许在整个数据集上应用用户定义的函数。以下是一个例子。

在 Java 中:

DataSet<Integer> input = // [...] 
DataSet<Integer> output = input.reduceGroup(new MyGroupReducer()); 

在 Scala 中:

val input: DataSet[Int] = // [...] 
val output = input.reduceGroup(new MyGroupReducer())  

在 Python 中:

output = data.reduce_group(MyGroupReducer()) 

对完整元组数据集进行聚合

我们可以对完整数据集运行常见的聚合函数。到目前为止,Flink 支持MAXMINSUM

在 Java 中:

DataSet<Tuple2<Integer, Double>> output = input 
.aggregate(SUM, 0) // SUM of first field                   
.and(MIN, 1); // Minimum of second  

在 Scala 中:

val input: DataSet[(Int, String, Double)] = // [...] 
val output = input.aggregate(SUM, 0).and(MIN, 2)  

在 Python 中:

output = input.aggregate(Sum, 0).and_agg(Min, 2) 

在完整元组数据集上的 MinBy

MinBy函数从完整数据集中选择一个数值最小的元组。用于比较的字段必须是可比较的。

在 Java 中:

DataSet<Tuple3<Integer, String, Double>> input = // [...] 
DataSet<Tuple3<Integer, String, Double>> output = input 
                  .minBy(0, 2); // select tuple with minimum values for 
                                first and third field. 

在 Scala 中:

val input: DataSet[(Int, String, Double)] = // [...] 
val output: DataSet[(Int, String, Double)] = input 
.minBy(0, 2)  

在 Python 中,此代码不受支持。

在完整元组数据集上的 MaxBy

MaxBy选择数值最大的单个元组完整数据集。用于比较的字段必须是可比较的。

在 Java 中:

DataSet<Tuple3<Integer, String, Double>> input = // [...] 
DataSet<Tuple3<Integer, String, Double>> output = input 
                 .maxBy(0, 2); // select tuple with maximum values for first and third field. 

在 Scala 中:

val input: DataSet[(Int, String, Double)] = // [...] 
val output: DataSet[(Int, String, Double)] = input 
                                  .maxBy(0, 2)  

在 Python 中,此代码不受支持。

不同

distinct 转换从源数据集中发出不同的值。这用于从源中删除重复的值。

在 Java 中:

DataSet<Tuple2<Integer, Double>> output = input.distinct(); 

在 Scala 中:

val output = input.distinct() 

在 Python 中,此代码不受支持。

连接

join 转换将两个数据集连接成一个数据集。连接条件可以定义为每个数据集的一个键。

在 Java 中:

public static class Student { public String name; public int deptId; } 
public static class Dept { public String name; public int id; } 
DataSet<Student> input1 = // [...] 
DataSet<Dept> input2 = // [...] 
DataSet<Tuple2<Student, Dept>> 
            result = input1.join(input2) 
.where("deptId")                                  
.equalTo("id"); 

在 Scala 中:

val input1: DataSet[(String, Int)] = // [...] 
val input2: DataSet[(String, Int)] = // [...] 
val result = input1.join(input2).where(1).equalTo(1) 

在 Python 中

result = input1.join(input2).where(1).equal_to(1)  

注意

有各种其他方式可以连接两个数据集。在这里有一个链接,您可以阅读更多关于所有这些连接选项的信息:ci.apache.org/projects/flink/flink-docs-master/dev/batch/dataset_transformations.html#join

交叉

交叉转换通过应用用户定义的函数对两个数据集进行交叉乘积。

在 Java 中:

DataSet<Class> input1 = // [...] 
DataSet<class> input2 = // [...] 
DataSet<Tuple3<Integer, Integer, Double>> 
            result = 
            input1.cross(input2) 
                   // applying CrossFunction 
                   .with(new MyCrossFunction()); 

在 Scala 中:

val result = input1.cross(input2) { 
//custom function 
} 

在 Python 中:

result = input1.cross(input2).using(MyCrossFunction()) 

联合

union 转换结合了两个相似的数据集。我们也可以一次联合多个数据集。

在 Java 中:

DataSet<Tuple2<String, Integer>> input1 = // [...] 
DataSet<Tuple2<String, Integer>> input2 = // [...] 
DataSet<Tuple2<String, Integer>> input3 = // [...] 
DataSet<Tuple2<String, Integer>> unioned = input1.union(input2).union(input3); 

在 Scala 中:

val input1: DataSet[(String, Int)] = // [...] 
val input2: DataSet[(String, Int)] = // [...] 
val input3: DataSet[(String, Int)] = // [...] 
val unioned = input1.union(input2).union(input3)  

在 Python 中:

unioned = input1.union(input2).union(input3) 

重新平衡

这个转换均匀地重新平衡并行分区。这有助于提高性能,因为它有助于消除数据倾斜。

在 Java 中:

DataSet<String> in = // [...] 
DataSet<Tuple2<String, String>> out = in.rebalance(); 

在 Scala 中:

val in: DataSet[String] = // [...] 
val out = in.rebalance() 

在 Python 中,此代码不受支持。

哈希分区

这个转换在给定的键上对数据集进行分区。

在 Java 中:

DataSet<Tuple2<String, Integer>> in = // [...] 
DataSet<Tuple2<String, String>> out = in.partitionByHash(1); 

在 Scala 中:

val in: DataSet[(String, Int)] = // [...] 
val out = in.partitionByHash(1) 

在 Python 中,此代码不受支持。

范围分区

这个转换在给定的键上对数据集进行范围分区。

在 Java 中:

DataSet<Tuple2<String, Integer>> in = // [...] 
DataSet<Tuple2<String, String>> out = in.partitionByRange(1); 

在 Scala 中:

val in: DataSet[(String, Int)] = // [...] 
val out = in.partitionByRange(1) 

在 Python 中,此代码不受支持。

排序分区

这个转换在给定的键和给定的顺序上本地对分区数据集进行排序。

在 Java 中:

DataSet<Tuple2<String, Integer>> in = // [...] 
DataSet<Tuple2<String, String>> out = in.sortPartition(1,Order.ASCENDING); 

在 Scala 中:

val in: DataSet[(String, Int)] = // [...] 
val out = in.sortPartition(1, Order.ASCENDING) 

在 Python 中,此代码不受支持。

首 n

这个转换任意返回数据集的前 n 个元素。

在 Java 中:

DataSet<Tuple2<String, Integer>> in = // [...] 
// Returns first 10 elements of the data set.  
DataSet<Tuple2<String, String>> out = in.first(10); 

在 Scala 中:

val in: DataSet[(String, Int)] = // [...] 
val out = in.first(10) 

在 Python 中,此代码不受支持。

广播变量

广播变量允许用户将某些数据集作为集合访问到所有操作符。通常,当您希望在某个操作中频繁引用少量数据时,可以使用广播变量。熟悉 Spark 广播变量的人也可以在 Flink 中使用相同的功能。

我们只需要广播一个具有特定名称的数据集,它将在每个执行器上都可用。广播变量保存在内存中,因此在使用它们时必须谨慎。以下代码片段显示了如何广播数据集并根据需要使用它。

// Get a data set to be broadcasted 
DataSet<Integer> toBroadcast = env.fromElements(1, 2, 3); 
DataSet<String> data = env.fromElements("India", "USA", "UK").map(new RichMapFunction<String, String>() { 
    private List<Integer> toBroadcast; 
    // We have to use open method to get broadcast set from the context 
    @Override 
    public void open(Configuration parameters) throws Exception { 
    // Get the broadcast set, available as collection 
    this.toBroadcast = 
    getRuntimeContext().getBroadcastVariable("country"); 
    } 

    @Override 
    public String map(String input) throws Exception { 
          int sum = 0; 
          for (int a : toBroadcast) { 
                sum = a + sum; 
          } 
          return input.toUpperCase() + sum; 
    } 
}).withBroadcastSet(toBroadcast, "country"); // Broadcast the set with name 
data.print(); 

当我们有查找条件要用于转换时,广播变量非常有用,查找数据集相对较小。

数据接收器

数据转换完成后,我们需要将结果保存在某个地方。以下是 Flink DataSet API 提供的一些选项,用于保存结果:

  • writeAsText(): 这将记录一行一行地写入字符串。

  • writeAsCsV(): 这将元组写为逗号分隔值文件。还可以配置行和字段分隔符。

  • print()/printErr(): 这将记录写入标准输出。您也可以选择写入标准错误。

  • write(): 这支持在自定义FileOutputFormat中写入数据。

  • output(): 这用于不基于文件的数据集。这可以用于我们想要将数据写入某个数据库的地方。

连接器

Apache Flink 的 DataSet API 支持各种连接器,允许在各种系统之间读取/写入数据。让我们尝试更多地探索这一点。

文件系统

Flink 允许默认连接到各种分布式文件系统,如 HDFS、S3、Google Cloud Storage、Alluxio 等。在本节中,我们将看到如何连接到这些文件系统。

为了连接到这些系统,我们需要在pom.xml中添加以下依赖项:

<dependency> 
  <groupId>org.apache.flink</groupId> 
  <artifactId>flink-hadoop-compatibility_2.11</artifactId> 
  <version>1.1.4</version> 
</dependency> 

这使我们能够使用 Hadoop 数据类型、输入格式和输出格式。Flink 支持开箱即用的可写和可比较可写,因此我们不需要兼容性依赖项。

HDFS

要从 HDFS 文件中读取数据,我们使用readHadoopFile()createHadoopInput()方法创建数据源。为了使用此连接器,我们首先需要配置flink-conf.yaml并将fs.hdfs.hadoopconf设置为正确的 Hadoop 配置目录。

生成的数据集将是与 HDFS 数据类型匹配的元组类型。以下代码片段显示了如何做到这一点。

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 
DataSet<Tuple2<LongWritable, Text>> input = 
    env.readHadoopFile(new TextInputFormat(), LongWritable.class, Text.class, textPath);  

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 
val input: DataSet[(LongWritable, Text)] = 
  env.readHadoopFile(new TextInputFormat, classOf[LongWritable], classOf[Text], textPath) 

我们还可以使用此连接器将处理后的数据写回 HDFS。OutputFormat包装器期望数据集以Tuple2格式。以下代码片段显示了如何将处理后的数据写回 HDFS。

在 Java 中:

// Get the processed data set 
DataSet<Tuple2<Text, IntWritable>> results = [...] 

// Set up the Hadoop Output Format. 
HadoopOutputFormat<Text, IntWritable> hadoopOF = 
  // create the Flink wrapper. 
  new HadoopOutputFormat<Text, IntWritable>( 
    // set the Hadoop OutputFormat and specify the job. 
    new TextOutputFormat<Text, IntWritable>(), job 
  ); 
hadoopOF.getConfiguration().set("mapreduce.output.textoutputformat.separator", " "); 
TextOutputFormat.setOutputPath(job, new Path(outputPath)); 

// Emit data  
result.output(hadoopOF); 

在 Scala 中:

// Get the processed data set 
val result: DataSet[(Text, IntWritable)] = [...] 

val hadoopOF = new HadoopOutputFormatText,IntWritable 

hadoopOF.getJobConf.set("mapred.textoutputformat.separator", " ") 
FileOutputFormat.setOutputPath(hadoopOF.getJobConf, new Path(resultPath)) 

result.output(hadoopOF) 

Amazon S3

如前所述,Flink 默认支持从 Amazon S3 读取数据。但是,我们需要在 Hadoop 的core-site.xml中进行一些配置。我们需要设置以下属性:

<!-- configure the file system implementation --> 
<property> 
  <name>fs.s3.impl</name> 
  <value>org.apache.hadoop.fs.s3native.NativeS3FileSystem</value> 
</property> 
<!-- set your AWS ID --> 
<property> 
  <name>fs.s3.awsAccessKeyId</name> 
  <value>putKeyHere</value> 
</property> 
<!-- set your AWS access key --> 
<property> 
  <name>fs.s3.awsSecretAccessKey</name> 
  <value>putSecretHere</value> 
</property> 

完成后,我们可以像这样访问 S3 文件系统:

// Read from S3 bucket 
env.readTextFile("s3://<bucket>/<endpoint>"); 
// Write to S3 bucket 
stream.writeAsText("s3://<bucket>/<endpoint>"); 

Alluxio

Alluxio 是一个开源的、内存速度的虚拟分布式存储。许多公司都在使用 Alluxio 进行高速数据存储和处理。您可以在www.alluxio.org/上了解更多关于 Alluxio 的信息。

Flink 默认支持从 Alluxio 读取数据。但是,我们需要在 Hadoop 的core-site.xml中进行一些配置。我们需要设置以下属性:

<property> 
  <name>fs.alluxio.impl</name> 
  <value>alluxio.hadoop.FileSystem</value> 
</property> 

完成后,我们可以像这样访问 Alluxio 文件系统:

// Read from Alluxio path 
env.readTextFile("alluxio://<path>"); 

// Write to Alluxio path 
stream.writeAsText("alluxio://<path>"); 

Avro

Flink 内置支持 Avro 文件。它允许轻松读写 Avro 文件。为了读取 Avro 文件,我们需要使用AvroInputFormat。以下代码片段显示了如何读取 Avro 文件:

AvroInputFormat<User> users = new AvroInputFormat<User>(in, User.class); 
DataSet<User> userSet = env.createInput(users); 

数据集准备好后,我们可以轻松执行各种转换,例如:

userSet.groupBy("city") 

Microsoft Azure 存储

Microsoft Azure Storage 是一种基于云的存储,允许以持久且可扩展的方式存储数据。Flink 支持管理存储在 Microsoft Azure 表存储上的数据。以下解释了我们如何做到这一点。

首先,我们需要从git下载azure-tables-hadoop项目,然后编译它:

git clone https://github.com/mooso/azure-tables-hadoop.git 
cd azure-tables-hadoop 
mvn clean install 

接下来,在pom.xml中添加以下依赖项:

<dependency> 
    <groupId>org.apache.flink</groupId> 
    <artifactId>flink-hadoop-compatibility_2.11</artifactId> 
    <version>1.1.4</version> 
</dependency> 
<dependency> 
  <groupId>com.microsoft.hadoop</groupId> 
  <artifactId>microsoft-hadoop-azure</artifactId> 
  <version>0.0.4</version> 
</dependency> 

接下来,我们编写以下代码来访问 Azure 存储:

final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

    // create a  AzureTableInputFormat, using a Hadoop input format wrapper 
    HadoopInputFormat<Text, WritableEntity> hdIf = new HadoopInputFormat<Text, WritableEntity>(new AzureTableInputFormat(), Text.class, WritableEntity.class, new Job()); 

// set account URI     
hdIf.getConfiguration().set(AzureTableConfiguration.Keys.ACCOUNT_URI.getKey(), "XXXX"); 
    // set the secret storage key 
    hdIf.getConfiguration().set(AzureTableConfiguration.Keys.STORAGE_KEY.getKey(), "XXXX"); 
    // set the table name  
    hdIf.getConfiguration().set(AzureTableConfiguration.Keys.TABLE_NAME.getKey(), "XXXX"); 

 DataSet<Tuple2<Text, WritableEntity>> input = env.createInput(hdIf); 

现在我们已经准备好处理数据集了。

MongoDB

通过开源贡献,开发人员已经能够将 Flink 连接到 MongoDB。在本节中,我们将讨论这样一个项目。

该项目是开源的,可以从 GitHub 下载:

git clone https://github.com/okkam-it/flink-mongodb-test.git 
cd flink-mongodb-test 
mvn clean install 

接下来,我们在 Java 程序中使用前面的连接器连接到 MongoDB:

// set up the execution environment 
    final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

// create a MongodbInputFormat, using a Hadoop input format wrapper 
HadoopInputFormat<BSONWritable, BSONWritable> hdIf =  
        new HadoopInputFormat<BSONWritable, BSONWritable>(new MongoInputFormat(), 
       BSONWritable.class, BSONWritable.class, new JobConf()); 

// specify connection parameters 
hdIf.getJobConf().set("mongo.input.uri",  
                "mongodb://localhost:27017/dbname.collectioname"); 

DataSet<Tuple2<BSONWritable, BSONWritable>> input = env.createInput(hdIf); 

一旦数据作为数据集可用,我们可以轻松进行所需的转换。我们还可以像这样将数据写回 MongoDB 集合:

MongoConfigUtil.setOutputURI( hdIf.getJobConf(),  
                "mongodb://localhost:27017/dbname.collectionname "); 
 // emit result (this works only locally) 
 result.output(new HadoopOutputFormat<Text,BSONWritable>( 
                new MongoOutputFormat<Text,BSONWritable>(), hdIf.getJobConf())); 

迭代

Flink 支持的一个独特功能是迭代。如今,许多开发人员希望使用大数据技术运行迭代的机器学习和图处理算法。为了满足这些需求,Flink 支持通过定义步骤函数来运行迭代数据处理。

迭代器操作符

迭代器操作符由以下组件组成:

迭代器操作符

  • 迭代输入:这是接收到的初始数据集或上一次迭代的输出

  • 步骤函数:这是需要应用于输入数据集的函数

  • 下一个部分解:这是需要反馈到下一次迭代的步骤函数的输出

  • 迭代结果:在完成所有迭代后,我们得到迭代的结果

迭代次数可以通过各种方式进行控制。一种方式可以是设置要执行的迭代次数,或者我们也可以进行条件终止。

增量迭代器

增量运算符对一组元素进行增量迭代操作。增量迭代器和常规迭代器之间的主要区别在于,增量迭代器在更新解决方案集而不是在每次迭代中完全重新计算解决方案集上工作。

这导致了更高效的操作,因为它使我们能够在更短的时间内专注于解决方案的重要部分。下图显示了 Flink 中增量迭代器的流程。

增量迭代器

  • 迭代输入:我们必须从某些文件中读取增量迭代器的工作集和解决方案集

  • 步骤函数:步骤函数是需要应用于输入数据集的函数

  • 下一个工作集/更新解决方案:在每次迭代解决方案集之后,它会根据最新结果进行更新,并将下一个工作集提供给下一个迭代

  • 迭代结果:在完成所有迭代后,我们以解决方案集的形式获得迭代的结果

由于增量迭代器在热数据集本身上运行,因此性能和效率非常好。以下是一篇详细的文章,讨论了使用 Flink 迭代器进行 PageRank 算法。data-artisans.com/data-analysis-with-flink-a-case-study-and-tutorial/

用例 - 使用 Flink 批处理 API 进行运动员数据洞察

现在我们已经了解了 DataSet API 的细节,让我们尝试将这些知识应用到一个真实的用例中。假设我们手头有一个数据集,其中包含有关奥运会运动员及其在各种比赛中表现的信息。示例数据如下表所示:

球员 国家 年份 比赛 金牌 银牌 铜牌 总计
杨伊琳 中国 2008 体操 1 0 2 3
利塞尔·琼斯 澳大利亚 2000 游泳 0 2 0 2
高基贤 韩国 2002 短道速滑 1 1 0 2
陈若琳 中国 2008 跳水 2 0 0 2
凯蒂·莱德基 美国 2012 游泳 1 0 0 1
鲁塔·梅卢蒂特 立陶宛 2012 游泳 1 0 0 1
达尼尔·吉尔塔 匈牙利 2004 游泳 0 1 0 1
阿里安娜·方塔纳 意大利 2006 短道速滑 0 0 1 1
奥尔加·格拉茨基赫 俄罗斯 2004 韵律体操 1 0 0 1
卡里克莱亚·潘塔齐 希腊 2000 韵律体操 0 0 1 1
金·马丁 瑞典 2002 冰球 0 0 1 1
凯拉·罗斯 美国 2012 体操 1 0 0 1
加布里埃拉·德拉戈伊 罗马尼亚 2008 体操 0 0 1 1
塔莎·施维克特-沃伦 美国 2000 体操 0 0 1 1

现在我们想要得到答案,比如,每个国家有多少运动员参加了比赛?或者每个比赛有多少运动员参加了?由于数据处于静止状态,我们将使用 Flink 批处理 API 进行分析。

可用的数据以 CSV 格式存在。因此,我们将使用 Flink API 提供的 CSV 读取器,如下面的代码片段所示。

final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 
DataSet<Record> csvInput = env.readCsvFile("olympic-athletes.csv") 
                     .pojoType(Record.class, "playerName", "country", "year", "game", "gold", "silver", "bronze", "total"); 

一旦数据被正确解析,就很容易继续使用它。以下代码片段显示了如何获取每个国家的球员数量的信息:

DataSet<Tuple2<String, Integer>> groupedByCountry = csvInput
.flatMap(new FlatMapFunction<Record, Tuple2<String, Integer>>() {
private static final long serialVersionUID = 1L;
@Override
public void flatMap(Record record, Collector<Tuple2<String, Integer>> out) throws Exception {
out.collect(new Tuple2<String, Integer>(record.getCountry(), 1));
}
}).groupBy(0).sum(1);
groupedByCountry.print();

在前面的代码片段中,我们首先创建了以球员国家为键,值为1的数据集,然后对其进行分组并求和以获得总数。一旦我们执行了代码,输出如下所示:

(Australia,11)
(Belarus,7)
(China,25)
(France,3)
(Germany,2)
(Italy,4)
(Turkey,1)
(United States,22)
(Cameroon,2)
(Hungary,1)
(Kenya,1)
(Lithuania,1)
(Russia,23)
(Spain,2)
(Ukraine,1)
(Chinese Taipei,2)
(Great Britain,1)
(Romania,14)
(Switzerland,1)
(Bulgaria,3)
(Finland,1)
(Greece,7)
(Japan,1)
(Mexico,1)
(Netherlands,2)
(Poland,1)
(South Korea,6)
(Sweden,6)
(Thailand,1)

同样,我们可以应用相同的逻辑来查找每场比赛的球员数量,如下面的代码片段所示:

DataSet<Tuple2<String, Integer>> groupedByGame = csvInput
.flatMap(new FlatMapFunction<Record, Tuple2<String, Integer>>() { private static final long serialVersionUID = 1L;
@Override
public void flatMap(Record record, Collector<Tuple2<String, Integer>> out) throws Exception {
out.collect(new Tuple2<String, Integer>(record.getGame(), 1));
}
}).groupBy(0).sum(1);
groupedByGame.print();

前面代码片段的输出如下:

(Basketball,1)
(Gymnastics,42)
(Ice Hockey,7)
(Judo,1)
(Swimming,33)
(Athletics,2)
(Fencing,2)
(Nordic Combined,1)
(Rhythmic Gymnastics,27)
(Short-Track Speed Skating,5)
(Table Tennis,1)
(Weightlifting,4)
(Boxing,3)
(Taekwondo,3)
(Archery,3)
(Diving,14)
(Figure Skating,1)
(Football,2)
(Shooting,1)

这样,您可以运行各种其他转换以获得所需的输出。此用例的完整代码可在github.com/deshpandetanmay/mastering-flink/tree/master/chapter03/flink-batch上找到。

摘要

在本章中,我们学习了 DataSet API。它使我们能够进行批处理。我们学习了各种转换以进行数据处理。后来,我们还探索了各种基于文件的连接器,以从 HDFS、Amazon S3、MS Azure、Alluxio 等读取/写入数据。

在最后一节中,我们看了一个用例,在这个用例中,我们应用了在前几节中学到的知识。

在下一章中,我们将学习另一个非常重要的 API,即 Table API,从 Flink 的生态系统角度来看。

第四章:使用表 API 进行数据处理

在前几章中,我们谈到了 Apache Flink 提供的批处理和流处理数据处理 API。在本章中,我们将讨论 Table API,它是 Flink 中用于数据处理的 SQL 接口。Table API 操作的是可以从数据集和数据流中创建的表接口。一旦数据集/数据流被注册为表,我们就可以自由地应用关系操作,如聚合、连接和选择。

表也可以像常规 SQL 查询一样进行查询。一旦操作完成,我们需要将表转换回数据集或数据流。Apache Flink 在内部使用另一个名为 Apache Calcite 的开源项目来优化这些查询转换。

在本章中,我们将涵盖以下主题:

  • 注册表

  • 访问已注册的表

  • 操作员

  • 数据类型

  • SQL

现在让我们开始吧。

为了使用 Table API,我们需要做的第一件事是创建一个 Java Maven 项目,并在其中添加以下依赖项:

  <dependency> 
      <groupId>org.apache.flink</groupId> 
      <artifactId>flink-table_2.11</artifactId> 
      <version>1.1.4</version> 
    </dependency> 

这个依赖项将在你的类路径中下载所有必需的 JAR 包。下载完成后,我们就可以使用 Table API 了。

注册表

为了对数据集/数据流进行操作,首先我们需要在TableEnvironment中注册一个表。一旦表以唯一名称注册,就可以轻松地从TableEnvironment中访问。

TableEnvironment维护一个内部表目录用于表注册。以下图表显示了细节:

注册表

拥有唯一的表名非常重要,否则你会得到一个异常。

注册数据集

为了在数据集上执行 SQL 操作,我们需要在BatchTableEnvironment中将其注册为表。在注册表时,我们需要定义一个 Java POJO 类。

例如,假设我们需要注册一个名为 Word Count 的数据集。这个表中的每条记录都有单词和频率属性。相同的 Java POJO 如下所示:

public static class WC { 
    public String word; 
    public long frequency; 
    public WC(){ 
    } 

    public WC(String word, long frequency) { 
      this.word = word; 
      this.frequency = frequency; 
    } 

    @Override 
    public String toString() { 
      return "WC " + word + " " + frequency; 
    } 
  } 

在 Scala 中,相同的类可以定义如下:

case class WordCount(word: String, frequency: Long) 

现在我们可以注册这个表了。

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

BatchTableEnvironment tEnv = TableEnvironment.getTableEnvironment(env); 

DataSet<WC> input = env.fromElements(new WC("Hello", 1), new WC("World", 1), new WC("Hello", 1)); 

// register the DataSet as table "WordCount" 
tEnv.registerDataSet("WordCount", input, "word, frequency"); 

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 

val tEnv = TableEnvironment.getTableEnvironment(env) 

val input = env.fromElements(WordCount("hello", 1), WordCount("hello", 1), WordCount("world", 1), WordCount("hello", 1)) 

//register the dataset  
tEnv.registerDataSet("WordCount", input, 'word, 'frequency) 

注意

请注意,数据集表的名称不能匹配^_DataSetTable_[0-9]+模式,因为它保留用于内部内存使用。

注册数据流

与数据集类似,我们也可以在StreamTableEnvironment中注册数据流。在注册表时,我们需要定义一个 Java POJO 类。

例如,假设我们需要注册一个名为 Word Count 的数据流。这个表中的每条记录都有一个单词和频率属性。相同的 Java POJO 如下所示:

public static class WC { 
    public String word; 
    public long frequency; 

    public WC() { 
    }s 
    public WC(String word, long frequency) { 
      this.word = word; 
      this.frequency = frequency; 
    } 

    @Override 
    public String toString() { 
      return "WC " + word + " " + frequency; 
    } 
  } 

在 Scala 中,相同的类可以定义如下:

case class WordCount(word: String, frequency: Long) 

现在我们可以注册这个表了。

在 Java 中:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 
    StreamTableEnvironment tEnv = TableEnvironment.getTableEnvironment(env); 

    DataStream<WC> input = env.fromElements(new WC("Hello", 1), new WC("World", 1), new WC("Hello", 1)); 

    // register the DataStream as table "WordCount" 
    tEnv.registerDataStream("WordCount", input, "word, frequency"); 

在 Scala 中:

val env = StreamExecutionEnvironment.getExecutionEnvironment 

val tEnv = TableEnvironment.getTableEnvironment(env) 

val input = env.fromElements(WordCount("hello", 1), WordCount("hello", 1), WordCount("world", 1), WordCount("hello", 1)) 

//register the dataset  
tEnv.registerDataStream("WordCount", input, 'word, 'frequency) 

注意

请注意,数据流表的名称不能匹配^_DataStreamTable_[0-9]+模式,因为它保留用于内部内存使用。

注册表

与数据集和数据流类似,我们也可以注册来自 Table API 的表。

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 
BatchTableEnvironment tEnv = TableEnvironment.getTableEnvironment(env); 

DataSet<WC> input = env.fromElements(new WC("Hello", 1), new WC("World", 1), new WC("Hello", 1)); 

tEnv.registerDataSet("WordCount", input, "word, frequency"); 

Table selectedTable = tEnv 
        .sql("SELECT word, SUM(frequency) as frequency FROM WordCount GROUP BY word having word = 'Hello'"); 

tEnv.registerTable("selected", selectedTable); 

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 

val tEnv = TableEnvironment.getTableEnvironment(env) 

val input = env.fromElements(WordCount("hello", 1), WordCount("hello", 1), WordCount("world", 1), WordCount("hello", 1)) 

tEnv.registerDataSet("WordCount", input, 'word, 'frequency) 

val table = tEnv.sql("SELECT word, SUM(frequency) FROM WordCount GROUP BY word") 

val selected = tEnv.sql("SELECT word, SUM(frequency) FROM WordCount GROUP BY word where word = 'hello'") 
    tEnv.registerTable("selected", selected) 

注册外部表源

Flink 允许我们使用TableSource从源中注册外部表。表源可以让我们访问存储在数据库中的数据,如 MySQL 和 Hbase,在文件系统中的数据,如 CSV、Parquet 和 ORC,或者还可以读取消息系统,如 RabbitMQ 和 Kafka。

目前,Flink 允许使用 CSV 源从 CSV 文件中读取数据,并使用 Kafka 源从 Kafka 主题中读取 JSON 数据。

CSV 表源

现在让我们看看如何直接使用 CSV 源读取数据,然后在表环境中注册源。

CSV 源默认在flink-tableAPI JAR 中可用,因此不需要添加任何其他额外的 Maven 依赖项。以下依赖项就足够了:

    <dependency> 
      <groupId>org.apache.flink</groupId> 
      <artifactId>flink-table_2.11</artifactId> 
      <version>1.1.4</version> 
    </dependency> 

以下代码片段显示了如何读取 CSV 文件并注册表源。

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 
BatchTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env); 

TableSource orders = new CsvTableSource("/path/to/file", ...) 

// register a TableSource as external table "orders" 
tableEnv.registerTableSource("orders", orders) 

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 
val tableEnv = TableEnvironment.getTableEnvironment(env) 

val orders: TableSource = new CsvTableSource("/path/to/file", ...) 

// register a TableSource as external table "orders" 
tableEnv.registerTableSource("orders", orders) 

Kafka JSON 表源

我们还可以在表环境中注册 Kafka JSON 表源。为了使用此 API,我们需要添加以下两个依赖项:

第一个是 Table API:

<dependency> 
      <groupId>org.apache.flink</groupId> 
      <artifactId>flink-table_2.11</artifactId> 
      <version>1.1.4</version> 
    </dependency> 

第二个依赖项将是 Kafka Flink 连接器:

  • 如果您使用 Kafka 0.8,请应用:
        <dependency> 
            <groupId>org.apache.flink</groupId> 
            <artifactId>flink-connector-kafka-0.8_2.11</artifactId> 
            <version>1.1.4</version> 
        </dependency> 

  • 如果您使用 Kafka 0.9,请应用:
        <dependency> 
            <groupId>org.apache.flink</groupId> 
            <artifactId>flink-connector-kafka-0.9_2.11</artifactId> 
            <version>1.1.4</version> 
        </dependency> 

现在我们需要按照以下代码片段中所示编写代码:

String[] fields =  new String[] { "id", "name", "price"}; 
Class<?>[] types = new Class<?>[] { Integer.class, String.class, Double.class }; 

KafkaJsonTableSource kafkaTableSource = new Kafka08JsonTableSource( 
    kafkaTopic, 
    kafkaProperties, 
    fields, 
    types); 

tableEnvironment.registerTableSource("kafka-source", kafkaTableSource); 
Table result = tableEnvironment.ingest("kafka-source"); 

在前面的代码中,我们为 Kafka 0.8 定义了 Kafka 源,然后在表环境中注册了该源。

访问注册的表

一旦表被注册,我们可以从TableEnvironment中很容易地访问它,如下所示:

tableEnvironment.scan("tableName") 

前面的语句扫描了以名称"tableName"注册的表在BatchTableEnvironment中:

tableEnvironment.ingest("tableName") 

前面的语句摄取了以名称"tableName"注册的表在StreamTableEnvironment中:

操作符

Flink 的 Table API 提供了各种操作符作为其特定领域语言的一部分。大多数操作符都在 Java 和 Scala API 中可用。让我们逐个查看这些操作符。

select 操作符

select操作符类似于 SQL select 操作符,允许您选择表中的各种属性/列。

在 Java 中:

Table result = in.select("id, name"); 
Table result = in.select("*"); 

在 Scala 中:

val result = in.select('id, 'name); 
val result = in.select('*); 

where 操作符

where操作符用于过滤结果。

在 Java 中:

Table result = in.where("id = '101'"); 

在 Scala 中:

val result = in.where('id == "101"); 

过滤器操作符

filter操作符可以用作where操作符的替代。

在 Java 中:

Table result = in.filter("id = '101'"); 

在 Scala 中:

val result = in.filter('id == "101"); 

as 操作符

as操作符用于重命名字段:

在 Java 中:

Table in = tableEnv.fromDataSet(ds, "id, name"); 
Table result = in.as("order_id, order_name"); 

在 Scala 中:

val in = ds.toTable(tableEnv).as('order_id, 'order_name ) 

groupBy 操作符

这类似于 SQL groupBy操作,根据给定的属性对结果进行聚合。

在 Java 中:

Table result = in.groupBy("company"); 

在 Scala 中:

val in = in.groupBy('company) 

join 操作符

join操作符用于连接表。我们必须至少指定一个相等的连接条件。

在 Java 中:

Table employee = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table dept = tableEnv.fromDataSet(dept, "d_id, d_name"); 

Table result = employee.join(dept).where("deptId = d_id").select("e_id, e_name, d_name"); 

在 Scala 中:

val employee = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId); 

val dept = deptDS.toTable(tableEnv, 'd_id, 'd_name); 

Table result = employee.join(dept).where('deptId == 'd_id).select('e_id, 'e_name, 'd_name); 

leftOuterJoin 操作符

leftOuterJoin操作符通过从左侧指定的表中获取所有值并仅从右侧表中选择匹配的值来连接两个表。我们必须至少指定一个相等的连接条件。

在 Java 中:

Table employee = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table dept = tableEnv.fromDataSet(dept, "d_id, d_name"); 

Table result = employee.leftOuterJoin(dept).where("deptId = d_id").select("e_id, e_name, d_name"); 

在 Scala 中:

val employee = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId); 

val dept = deptDS.toTable(tableEnv, 'd_id, 'd_name); 

Table result = employee.leftOuterJoin(dept).where('deptId == 'd_id).select('e_id, 'e_name, 'd_name); 

rightOuterJoin 操作符

rightOuterJoin操作符通过从右侧指定的表中获取所有值并仅从左侧表中选择匹配的值来连接两个表。我们必须至少指定一个相等的连接条件。

在 Java 中:

Table employee = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table dept = tableEnv.fromDataSet(dept, "d_id, d_name"); 

Table result = employee.rightOuterJoin(dept).where("deptId = d_id").select("e_id, e_name, d_name"); 

在 Scala 中:

val employee = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId); 

val dept = deptDS.toTable(tableEnv, 'd_id, 'd_name); 

Table result = employee.rightOuterJoin(dept).where('deptId == 'd_id).select('e_id, 'e_name, 'd_name); 

fullOuterJoin 操作符

fullOuterJoin操作符通过从两个表中获取所有值来连接两个表。我们必须至少指定一个相等的连接条件。

在 Java 中:

Table employee = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table dept = tableEnv.fromDataSet(dept, "d_id, d_name"); 

Table result = employee.fullOuterJoin(dept).where("deptId = d_id").select("e_id, e_name, d_name"); 

在 Scala 中:

val employee = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId); 

val dept = deptDS.toTable(tableEnv, 'd_id, 'd_name); 

Table result = employee.fullOuterJoin(dept).where('deptId == 'd_id).select('e_id, 'e_name, 'd_name); 

union 操作符

union操作符合并两个相似的表。它删除结果表中的重复值。

在 Java 中:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table result = employee1.union(employee2); 

在 Scala 中:

val employee1 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

val employee2 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

Table result = employee1.union(employee2) 

unionAll 操作符

unionAll操作符合并两个相似的表。

在 Java 中:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table result = employee1.unionAll(employee2); 

在 Scala 中:

val employee1 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

val employee2 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

Table result = employee1.unionAll(employee2) 

intersect 操作符

intersect操作符返回两个表中匹配的值。它确保结果表没有任何重复项。

在 Java 中:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table result = employee1.intersect(employee2); 

在 Scala 中:

val employee1 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

val employee2 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

Table result = employee1.intersect(employee2) 

intersectAll 操作符

intersectAll操作符返回两个表中匹配的值。结果表可能有重复记录。

在 Java 中:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table result = employee1.intersectAll(employee2); 

在 Scala 中:

val employee1 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

val employee2 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

Table result = employee1.intersectAll(employee2) 

minus 操作符

minus操作符返回左表中不存在于右表中的记录。它确保结果表没有任何重复项。

在 Java 中:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table result = employee1.minus(employee2); 

在 Scala 中:

val employee1 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

val employee2 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

Table result = employee1.minus(employee2) 

minusAll 操作符

minusAll操作符返回左表中不存在于右表中的记录。结果表可能有重复记录。

在 Java 中:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table employee2 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table result = employee1.minusAll(employee2); 

在 Scala 中:

val employee1 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

val employee2 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

Table result = employee1.minusAll(employee2) 

distinct 操作符

distinct操作符仅从表中返回唯一值记录。

在 Java 中:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table result = employee1.distinct(); 

在 Scala 中:

val employee1 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

Table result = employee1.distinct() 

orderBy 操作符

orderBy操作符返回在全局并行分区中排序的记录。您可以选择升序或降序的顺序。

在 Java 中:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

Table result = employee1.orderBy("e_id.asc"); 

在 Scala 中:

val employee1 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 

Table result = employee1.orderBy('e_id.asc) 

limit 操作符

limit操作符限制了从给定偏移量排序的记录在全局并行分区中。

在 Java 中:

Table employee1 = tableEnv.fromDataSet(emp, "e_id, e_name, deptId"); 

//returns records from 6th record 
Table result = employee1.orderBy("e_id.asc").limit(5); 

//returns 5 records from 4th record 
Table result1 = employee1.orderBy("e_id.asc").limit(3,5); 

在 Scala 中:

val employee1 = empDS.toTable(tableEnv, 'e_id, 'e_name, 'deptId) 
//returns records from 6th record 
Table result = employee1.orderBy('e_id.asc).limit(5) 
//returns 5 records from 4th record 
Table result = employee1.orderBy('e_id.asc).limit(3,5) 

数据类型

表 API 支持常见的 SQL 数据类型,可以轻松使用。在内部,它使用TypeInformation来识别各种数据类型。目前它不支持所有 Flink 数据类型:

表 API SQL Java 类型
Types.STRING VARCHAR java.lang.String
Types.BOOLEAN BOOLEAN java.lang.Boolean
Types.BYTE TINYINT java.lang.Byte
Types.SHORT SMALLINT java.lang.Short
Types.INT INTEGERINT java.lang.Integer
Types.LONG BIGINT java.lang.Long
Types.FLOAT REALFLOAT java.lang.Float
Types.DOUBLE DOUBLE java.lang.Double
Types.DECIMAL DECIMAL java.math.BigDecimal
Types.DATE DATE java.sql.Date
Types.TIME TIME java.sql.Time
Types.TIMESTAMP TIMESTAMP(3) java.sql.Timestamp
Types.INTERVAL_MONTHS INTERVAL YEAR TO MONTH java.lang.Integer
Types.INTERVAL_MILLIS INTERVAL DAY TO SECOND(3) java.lang.Long

随着社区的持续发展和支持,将很快支持更多的数据类型。

SQL

表 API 还允许我们使用sql()方法编写自由形式的 SQL 查询。该方法在内部还使用 Apache Calcite 进行 SQL 语法验证和优化。它执行查询并以表格格式返回结果。稍后,表格可以再次转换为数据集或数据流或TableSink以进行进一步处理。

这里需要注意的一点是,为了让 SQL 方法访问表,它们必须在TableEnvironment中注册。

SQL 方法不断添加更多支持,因此如果不支持任何语法,将出现TableException错误。

现在让我们看看如何在数据集和数据流上使用 SQL 方法。

数据流上的 SQL

可以使用SELECT STREAM关键字在使用TableEnvironment注册的数据流上执行 SQL 查询。数据集和数据流之间的大部分 SQL 语法是通用的。要了解更多关于流语法的信息,Apache Calcite 的 Streams 文档会很有帮助。可以在以下网址找到:calcite.apache.org/docs/stream.html

假设我们想要分析定义为(idnamestock)的产品模式。需要使用sql()方法编写以下代码。

在 Java 中:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 
StreamTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env); 

DataStream<Tuple3<Long, String, Integer>> ds = env.addSource(...); 
// register the DataStream as table "Products" 
tableEnv.registerDataStream("Products", ds, "id, name, stock"); 
// run a SQL query on the Table and retrieve the result as a new Table 
Table result = tableEnv.sql( 
  "SELECT STREAM * FROM Products WHERE name LIKE '%Apple%'"); 

在 Scala 中:

val env = StreamExecutionEnvironment.getExecutionEnvironment 
val tEnv = TableEnvironment.getTableEnvironment(env) 

val ds: DataStream[(Long, String, Integer)] = env.addSource(...) 
// register the DataStream under the name "Products" 
tableEnv.registerDataStream("Products", ds, 'id, 'name, 'stock) 
// run a SQL query on the Table and retrieve the result as a new Table 
val result = tableEnv.sql( 
  "SELECT STREAM * FROM Products WHERE name LIKE '%Apple%'") 

表 API 使用类似于 Java 的词法策略来正确定义查询。这意味着标识符的大小写保留,并且它们是区分大小写匹配的。如果您的任何标识符包含非字母数字字符,则可以使用反引号引用它们。

例如,如果要定义一个名为'my col'的列,则需要使用如下所示的反引号:

"SELECT col as `my col` FROM table " 

支持的 SQL 语法

正如前面所述,Flink 使用 Apache Calcite 来验证和优化 SQL 查询。在当前版本中,支持以下巴科斯-瑙尔范式BNF):

query: 
  values 
  | { 
      select 
      | selectWithoutFrom 
      | query UNION [ ALL ] query 
      | query EXCEPT query 
      | query INTERSECT query 
    } 
    [ ORDER BY orderItem [, orderItem ]* ] 
    [ LIMIT { count | ALL } ] 
    [ OFFSET start { ROW | ROWS } ] 
    [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY] 

orderItem: 
  expression [ ASC | DESC ] 

select: 
  SELECT [ STREAM ] [ ALL | DISTINCT ] 
  { * | projectItem [, projectItem ]* } 
  FROM tableExpression 
  [ WHERE booleanExpression ] 
  [ GROUP BY { groupItem [, groupItem ]* } ] 
  [ HAVING booleanExpression ] 

selectWithoutFrom: 
  SELECT [ ALL | DISTINCT ] 
  { * | projectItem [, projectItem ]* } 

projectItem: 
  expression [ [ AS ] columnAlias ] 
  | tableAlias . * 

tableExpression: 
  tableReference [, tableReference ]* 
  | tableExpression [ NATURAL ] [ LEFT | RIGHT | FULL ] JOIN tableExpression [ joinCondition ] 

joinCondition: 
  ON booleanExpression 
  | USING '(' column [, column ]* ')' 

tableReference: 
  tablePrimary 
  [ [ AS ] alias [ '(' columnAlias [, columnAlias ]* ')' ] ] 

tablePrimary: 
  [ TABLE ] [ [ catalogName . ] schemaName . ] tableName 

values: 
  VALUES expression [, expression ]* 

groupItem: 
  expression 
  | '(' ')' 
  | '(' expression [, expression ]* ')' 

标量函数

表 API 和 SQL 支持各种内置的标量函数。让我们逐一了解这些。

表 API 中的标量函数

以下是表 API 中支持的标量函数列表:

Java 函数 Scala 函数 描述
ANY.isNull ANY.isNull 如果给定的表达式为空,则返回true
ANY.isNotNull ANY.isNotNull 如果给定的表达式不为空,则返回true
BOOLEAN.isTrue BOOLEAN.isTrue 如果给定的布尔表达式为true,则返回true。否则返回False
BOOLEAN.isFalse BOOLEAN.isFalse 如果给定的布尔表达式为 false,则返回true。否则返回False
NUMERIC.log10() NUMERIC.log10() 计算给定值的以 10 为底的对数。
NUMERIC.ln() NUMERIC.ln() 计算给定值的自然对数。
NUMERIC.power(NUMERIC) NUMERIC.power(NUMERIC) 计算给定数字的另一个值的幂。
NUMERIC.abs() NUMERIC.abs() 计算给定值的绝对值。
NUMERIC.floor() NUMERIC.floor() 计算小于或等于给定数字的最大整数。
NUMERIC.ceil() NUMERIC.ceil() 计算大于或等于给定数字的最小整数。
STRING.substring(INT, INT) STRING.substring(INT, INT) 在给定索引处创建给定长度的字符串子串
STRING.substring(INT) STRING.substring(INT) 创建给定字符串的子串,从给定索引开始到末尾。起始索引从 1 开始,包括在内。
STRING.trim(LEADING, STRING) STRING.trim(TRAILING, STRING) STRING.trim(BOTH, STRING) STRING.trim(BOTH) STRING.trim() STRING.trim(leading = true, trailing = true, character = " ") 从给定字符串中移除前导和/或尾随字符。默认情况下,两侧的空格将被移除。
STRING.charLength() STRING.charLength() 返回字符串的长度。
STRING.upperCase() STRING.upperCase() 使用默认区域设置的规则将字符串中的所有字符转换为大写。
STRING.lowerCase() STRING.lowerCase() 使用默认区域设置的规则将字符串中的所有字符转换为小写。
STRING.initCap() STRING.initCap() 将字符串中每个单词的初始字母转换为大写。假设字符串只包含[A-Za-z0-9],其他所有内容都视为空格。
STRING.like(STRING) STRING.like(STRING) 如果字符串与指定的 LIKE 模式匹配,则返回 true。例如,"Jo_n%"匹配以"Jo(任意字母)n"开头的所有字符串。
STRING.similar(STRING) STRING.similar(STRING) 如果字符串与指定的 SQL 正则表达式模式匹配,则返回true。例如,"A+"匹配至少包含一个"A"的所有字符串。
STRING.toDate() STRING.toDate 将形式为"yy-mm-dd"的日期字符串解析为 SQL 日期。
STRING.toTime() STRING.toTime 将形式为"hh:mm:ss"的时间字符串解析为 SQL 时间。
STRING.toTimestamp() STRING.toTimestamp 将形式为"yy-mm-dd hh:mm:ss.fff"的时间戳字符串解析为 SQL 时间戳。
TEMPORAL.extract(TIMEINTERVALUNIT) NA 提取时间点或时间间隔的部分。将该部分作为长整型值返回。例如,2006-06-05 .toDate.extract(DAY) 导致 5
TIMEPOINT.floor(TIMEINTERVALUNIT) TIMEPOINT.floor(TimeIntervalUnit) 将时间点向下舍入到给定的单位。例如,"12:44:31".toDate.floor(MINUTE) 导致 12:44:00
TIMEPOINT.ceil(TIMEINTERVALUNIT) TIMEPOINT.ceil(TimeIntervalUnit) 将时间点四舍五入到给定的单位。例如,"12:44:31".toTime.floor(MINUTE) 导致 12:45:00
currentDate() currentDate() 返回 UTC 时区的当前 SQL 日期。
currentTime() currentTime() 返回 UTC 时区的当前 SQL 时间。
currentTimestamp() currentTimestamp() 返回 UTC 时区的当前 SQL 时间戳。
localTime() localTime() 返回本地时区的当前 SQL 时间。
localTimestamp() localTimestamp() 返回本地时区的当前 SQL 时间戳。

Scala functions in SQL

以下是sql()方法中支持的标量函数列表:

函数 描述
EXP(NUMERIC) 计算给定幂的自然对数。
LOG10(NUMERIC) 计算给定值的以 10 为底的对数。
LN(NUMERIC) 计算给定值的自然对数。
POWER(NUMERIC, NUMERIC) 计算给定数字的另一个值的幂。
ABS(NUMERIC) 计算给定值的绝对值。
FLOOR(NUMERIC) 计算小于或等于给定数字的最大整数。
CEIL(NUMERIC) 计算大于或等于给定数字的最小整数。
SUBSTRING(VARCHAR, INT, INT) SUBSTRING(VARCHAR FROM INT FOR INT) 从给定索引开始创建给定长度的字符串的子字符串。索引从 1 开始,是包含的,即包括索引处的字符。子字符串具有指定的长度或更少。
SUBSTRING(VARCHAR, INT)``SUBSTRING(VARCHAR FROM INT) 从给定索引开始创建给定字符串的子字符串直到末尾。起始索引从 1 开始,是包含的。
TRIM(LEADING VARCHAR FROM VARCHAR) TRIM(TRAILING VARCHAR FROM VARCHAR) TRIM(BOTH VARCHAR FROM VARCHAR) TRIM(VARCHAR) 从给定的字符串中移除前导和/或尾随字符。默认情况下,两侧的空格将被移除。
CHAR_LENGTH(VARCHAR) 返回字符串的长度。
UPPER(VARCHAR) 使用默认区域设置的规则将字符串中的所有字符转换为大写。
LOWER(VARCHAR) 使用默认区域设置的规则将字符串中的所有字符转换为小写。
INITCAP(VARCHAR) 将字符串中每个单词的首字母转换为大写。假定字符串仅包含[A-Za-z0-9],其他所有内容都视为空格。
VARCHAR LIKE VARCHAR 如果字符串与指定的 LIKE 模式匹配,则返回 true。例如,"Jo_n%"匹配所有以"Jo(任意字母)n"开头的字符串。
VARCHAR SIMILAR TO VARCHAR 如果字符串与指定的 SQL 正则表达式模式匹配,则返回 true。例如,"A+"匹配至少包含一个"A"的所有字符串。
DATE VARCHAR 将形式为"yy-mm-dd"的日期字符串解析为 SQL 日期。
TIME VARCHAR 将形式为"hh:mm:ss"的时间字符串解析为 SQL 时间。
TIMESTAMP VARCHAR 将形式为"yy-mm-dd hh:mm:ss.fff"的时间戳字符串解析为 SQL 时间戳。
EXTRACT(TIMEINTERVALUNIT FROM TEMPORAL) 提取时间点或时间间隔的部分。将该部分作为长值返回。例如,EXTRACT(DAY FROM DATE '2006-06-05')得到5
FLOOR(TIMEPOINT TO TIMEINTERVALUNIT) 将时间点向下舍入到给定的单位。例如,FLOOR(TIME '12:44:31' TO MINUTE)得到12:44:00
CEIL(TIMEPOINT TO TIMEINTERVALUNIT) 将时间点向上舍入到给定的单位。例如,CEIL(TIME '12:44:31' TO MINUTE)得到12:45:00
CURRENT_DATE 返回 UTC 时区中的当前 SQL 日期。
CURRENT_TIME 返回 UTC 时区中的当前 SQL 时间。
CURRENT_TIMESTAMP 返回 UTC 时区中的当前 SQL 时间戳。
LOCALTIME 返回本地时区中的当前 SQL 时间。
LOCALTIMESTAMP 返回本地时区中的当前 SQL 时间戳。

使用案例 - 使用 Flink Table API 进行运动员数据洞察

现在我们已经了解了 Table API 的细节,让我们尝试将这些知识应用到一个真实的用例中。假设我们手头有一个数据集,其中包含有关奥运运动员及其在各种比赛中的表现的信息。

样本数据如下表所示:

运动员 国家 年份 比赛 金牌 银牌 铜牌 总计
杨伊琳 中国 2008 体操 1 0 2 3
利塞尔·琼斯 澳大利亚 2000 游泳 0 2 0 2
高基贤 韩国 2002 短道速滑 1 1 0 2
陈若琳 中国 2008 跳水 2 0 0 2
凯蒂·莱德基 美国 2012 游泳 1 0 0 1
鲁塔·梅卢蒂特 立陶宛 2012 游泳 1 0 0 1
丹尼尔·吉尔塔 匈牙利 2004 游泳 0 1 0 1
阿里安娜·方塔纳 意大利 2006 短道速滑 0 0 1 1
奥尔加·格拉茨基赫 俄罗斯 2004 韵律体操 1 0 0 1
Kharikleia Pantazi 希腊 2000 韵律体操 0 0 1 1
Kim Martin 瑞典 2002 冰球 0 0 1 1
Kyla Ross 美国 2012 体操 1 0 0 1
Gabriela Dragoi 罗马尼亚 2008 体操 0 0 1 1
Tasha Schwikert-Warren 美国 2000 体操 0 0 1 1

现在我们想要得到答案,比如,每个国家或每个比赛赢得了多少枚奖牌。由于我们的数据是结构化数据,我们可以使用 Table API 以 SQL 方式查询数据。所以让我们开始吧。

可用的数据是以 CSV 格式提供的。因此,我们将使用 Flink API 提供的 CSV 阅读器,如下面的代码片段所示:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 
BatchTableEnvironment tableEnv = TableEnvironment.getTableEnvironment(env); 
DataSet<Record> csvInput = env 
          .readCsvFile("olympic-athletes.csv") 
          .pojoType(Record.class, "playerName", "country", "year",   
                    "game", "gold", "silver", "bronze", "total"); 

接下来,我们需要使用这个数据集创建一个表,并在 Table Environment 中注册它以进行进一步处理:

Table atheltes = tableEnv.fromDataSet(csvInput); 
tableEnv.registerTable("athletes", atheltes); 

接下来,我们可以编写常规的 SQL 查询,以从数据中获取更多见解。或者我们可以使用 Table API 操作符来操作数据,如下面的代码片段所示:

Table groupedByCountry = tableEnv.sql("SELECT country, SUM(total) as frequency FROM athletes group by country"); 
DataSet<Result> result = tableEnv.toDataSet(groupedByCountry, Result.class); 
result.print(); 
Table groupedByGame = atheltes.groupBy("game").select("game, total.sum as frequency"); 
DataSet<GameResult> gameResult = tableEnv.toDataSet(groupedByGame, GameResult.class); 
gameResult.print(); 

通过 Table API,我们可以以更简单的方式分析这样的数据。这个用例的完整代码可以在 GitHub 上找到:github.com/deshpandetanmay/mastering-flink/tree/master/chapter04/flink-table

总结

在本章中,我们了解了 Flink 支持的基于 SQL 的 API,称为 Table API。我们还学习了如何将数据集/流转换为表,使用TableEnvironment注册表、数据集和数据流,然后使用注册的表执行各种操作。对于来自 SQL 数据库背景的人来说,这个 API 是一种福音。

在下一章中,我们将讨论一个非常有趣的库,叫做复杂事件处理,以及如何将其用于解决各种业务用例。

第五章:复杂事件处理

在上一章中,我们谈到了 Apache Flink 提供的 Table API 以及我们如何使用它来处理关系数据结构。从本章开始,我们将开始学习有关 Apache Flink 提供的库以及如何将它们用于特定用例的更多信息。首先,让我们尝试了解一个名为复杂事件处理CEP)的库。CEP 是一个非常有趣但复杂的主题,在各行业都有其价值。无论在哪里都有预期的事件流,人们自然希望在所有这些用例中执行复杂事件处理。让我们尝试了解 CEP 的全部内容。

什么是复杂事件处理?

CEP 分析高频率和低延迟发生的不同事件流。如今,各行业都可以找到流事件,例如:

  • 在石油和天然气领域,传感器数据来自各种钻井工具或上游油管设备

  • 在安全领域,活动数据、恶意软件信息和使用模式数据来自各种终端

  • 在可穿戴设备领域,数据来自各种手腕带,包含有关您的心率、活动等信息

  • 在银行领域,数据来自信用卡使用、银行活动等

分析变化模式以实时通知常规装配中的任何变化非常重要。CEP 可以理解事件流、子事件及其序列中的模式。CEP 有助于识别有意义的模式和无关事件之间的复杂关系,并实时或准实时发送通知以防止损害:

什么是复杂事件处理?

上图显示了 CEP 流程的工作原理。尽管流程看起来很简单,但 CEP 具有各种能力,例如:

  • 在输入事件流可用时立即生成结果的能力

  • 提供诸如时间内聚合和两个感兴趣事件之间的超时等计算能力

  • 提供在检测到复杂事件模式时实时/准实时警报和通知的能力

  • 能够连接和关联异构源并分析其中的模式

  • 实现高吞吐量、低延迟处理的能力

市场上有各种解决方案。随着大数据技术的进步,我们有多个选项,如 Apache Spark、Apache Samza、Apache Beam 等,但没有一个专门的库适用于所有解决方案。现在让我们尝试了解 Flink 的 CEP 库可以实现什么。

Flink CEP

Apache Flink 提供了 Flink CEP 库,提供了执行复杂事件处理的 API。该库包括以下核心组件:

  • 事件流

  • 模式定义

  • 模式检测

  • 警报生成

Flink CEP

Flink CEP 使用 Flink 的数据流 API 称为 DataStream。程序员需要定义要从事件流中检测到的模式,然后 Flink 的 CEP 引擎检测到模式并采取适当的操作,例如生成警报。

为了开始,我们需要添加以下 Maven 依赖项:

<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-cep-scala_2.10 --> 
<dependency> 
    <groupId>org.apache.flink</groupId> 
    <artifactId>flink-cep-scala_2.11</artifactId> 
    <version>1.1.4</version> 
</dependency> 

事件流

CEP 的一个非常重要的组件是其输入事件流。在早期的章节中,我们已经看到了 DataStream API 的详细信息。现在让我们利用这些知识来实现 CEP。我们需要做的第一件事就是为事件定义一个 Java POJO。假设我们需要监视温度传感器事件流。

首先,我们定义一个抽象类,然后扩展这个类。

注意

在定义事件 POJO 时,我们需要确保实现hashCode()equals()方法,因为在比较事件时,编译将使用它们。

以下代码片段演示了这一点。

首先,我们编写一个抽象类,如下所示:

package com.demo.chapter05; 

public abstract class MonitoringEvent { 

  private String machineName; 

  public String getMachineName() { 
    return machineName; 
  } 

  public void setMachineName(String machineName) { 
    this.machineName = machineName; 
  } 

  @Override 
  public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + ((machineName == null) ? 0 : machineName.hashCode()); 
    return result; 
  } 

  @Override 
  public boolean equals(Object obj) { 
    if (this == obj) 
      return true; 
    if (obj == null) 
      return false; 
    if (getClass() != obj.getClass()) 
      return false; 
    MonitoringEvent other = (MonitoringEvent) obj; 
    if (machineName == null) { 
      if (other.machineName != null) 
        return false; 
    } else if (!machineName.equals(other.machineName)) 
      return false; 
    return true; 
  } 

  public MonitoringEvent(String machineName) { 
    super(); 
    this.machineName = machineName; 
  } 

} 

然后我们为实际温度事件创建一个 POJO:

package com.demo.chapter05; 

public class TemperatureEvent extends MonitoringEvent { 

  public TemperatureEvent(String machineName) { 
    super(machineName); 
  } 

  private double temperature; 

  public double getTemperature() { 
    return temperature; 
  } 

  public void setTemperature(double temperature) { 
    this.temperature = temperature; 
  } 

  @Override 
  public int hashCode() { 
    final int prime = 31; 
    int result = super.hashCode(); 
    long temp; 
    temp = Double.doubleToLongBits(temperature); 
    result = prime * result + (int) (temp ^ (temp >>> 32)); 
    return result; 
  } 

  @Override 
  public boolean equals(Object obj) { 
    if (this == obj) 
      return true; 
    if (!super.equals(obj)) 
      return false; 
    if (getClass() != obj.getClass()) 
      return false; 
    TemperatureEvent other = (TemperatureEvent) obj; 
    if (Double.doubleToLongBits(temperature) != Double.doubleToLongBits(other.temperature)) 
      return false; 
    return true; 
  } 

  public TemperatureEvent(String machineName, double temperature) { 
    super(machineName); 
    this.temperature = temperature; 
  } 

  @Override 
  public String toString() { 
    return "TemperatureEvent [getTemperature()=" + getTemperature() + ", getMachineName()=" + getMachineName() 
        + "]"; 
  } 

} 

现在我们可以定义事件源如下:

在 Java 中:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 
    DataStream<TemperatureEvent> inputEventStream = env.fromElements(new TemperatureEvent("xyz", 22.0), 
        new TemperatureEvent("xyz", 20.1), new TemperatureEvent("xyz", 21.1), new TemperatureEvent("xyz", 22.2), 
        new TemperatureEvent("xyz", 22.1), new TemperatureEvent("xyz", 22.3), new TemperatureEvent("xyz", 22.1), 
        new TemperatureEvent("xyz", 22.4), new TemperatureEvent("xyz", 22.7), 
        new TemperatureEvent("xyz", 27.0)); 

在 Scala 中:

val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment 
    val input: DataStream[TemperatureEvent] = env.fromElements(new TemperatureEvent("xyz", 22.0), 
      new TemperatureEvent("xyz", 20.1), new TemperatureEvent("xyz", 21.1), new TemperatureEvent("xyz", 22.2), 
      new TemperatureEvent("xyz", 22.1), new TemperatureEvent("xyz", 22.3), new TemperatureEvent("xyz", 22.1), 
      new TemperatureEvent("xyz", 22.4), new TemperatureEvent("xyz", 22.7), 
      new TemperatureEvent("xyz", 27.0)) 

模式 API

模式 API 允许您非常轻松地定义复杂的事件模式。每个模式由多个状态组成。要从一个状态转换到另一个状态,通常需要定义条件。条件可以是连续性或过滤掉的事件。

Pattern API

让我们尝试详细了解每个模式操作。

开始

初始状态可以定义如下:

在 Java 中:

Pattern<Event, ?> start = Pattern.<Event>begin("start"); 

在 Scala 中:

val start : Pattern[Event, _] = Pattern.begin("start") 

过滤器

我们还可以为初始状态指定过滤条件:

在 Java 中:

start.where(new FilterFunction<Event>() { 
    @Override 
    public boolean filter(Event value) { 
        return ... // condition 
    } 
}); 

在 Scala 中:

start.where(event => ... /* condition */) 

子类型

我们还可以根据它们的子类型过滤事件,使用subtype()方法:

在 Java 中:

start.subtype(SubEvent.class).where(new FilterFunction<SubEvent>() { 
    @Override 
    public boolean filter(SubEvent value) { 
        return ... // condition 
    } 
}); 

在 Scala 中:

start.subtype(classOf[SubEvent]).where(subEvent => ... /* condition */) 

模式 API 还允许我们一起定义多个条件。我们可以使用ORAND运算符。

在 Java 中:

pattern.where(new FilterFunction<Event>() { 
    @Override 
    public boolean filter(Event value) { 
        return ... // condition 
    } 
}).or(new FilterFunction<Event>() { 
    @Override 
    public boolean filter(Event value) { 
        return ... // or condition 
    } 
}); 

在 Scala 中:

pattern.where(event => ... /* condition */).or(event => ... /* or condition */) 

连续性

如前所述,我们并不总是需要过滤事件。总是可能有一些我们需要连续而不是过滤的模式。

连续性可以有两种类型 - 严格连续性和非严格连续性。

严格连续性

严格连续性需要直接成功的两个事件,这意味着两者之间不应该有其他事件。这个模式可以通过next()定义。

在 Java 中:

Pattern<Event, ?> strictNext = start.next("middle"); 

在 Scala 中:

val strictNext: Pattern[Event, _] = start.next("middle") 

非严格连续

非严格连续性可以被定义为其他事件允许在特定两个事件之间。这个模式可以通过followedBy()定义。

在 Java 中:

Pattern<Event, ?> nonStrictNext = start.followedBy("middle"); 

在 Scala 中:

val nonStrictNext : Pattern[Event, _] = start.followedBy("middle") 

模式 API 还允许我们根据时间间隔进行模式匹配。我们可以定义基于时间的时间约束如下。

在 Java 中:

next.within(Time.seconds(30)); 

在 Scala 中:

next.within(Time.seconds(10)) 

检测模式

要检测事件流中的模式,我们需要通过模式运行流。CEP.pattern()返回PatternStream

以下代码片段显示了我们如何检测模式。首先定义模式,以检查温度值是否在10秒内大于26.0度。

在 Java 中:

Pattern<TemperatureEvent, ?> warningPattern = Pattern.<TemperatureEvent> begin("first") 
        .subtype(TemperatureEvent.class).where(new FilterFunction<TemperatureEvent>() { 
          public boolean filter(TemperatureEvent value) { 
            if (value.getTemperature() >= 26.0) { 
              return true; 
            } 
            return false; 
          } 
        }).within(Time.seconds(10)); 

    PatternStream<TemperatureEvent> patternStream = CEP.pattern(inputEventStream, warningPattern); 

在 Scala 中:

val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment 

val input = // data 

val pattern: Pattern[TempEvent, _] = Pattern.begin("start").where(event => event.temp >= 26.0) 

val patternStream: PatternStream[TempEvent] = CEP.pattern(input, pattern) 

从模式中选择

一旦模式流可用,我们需要从中选择模式,然后根据需要采取适当的操作。我们可以使用selectflatSelect方法从模式中选择数据。

选择

select 方法需要PatternSelectionFunction实现。它有一个 select 方法,该方法将为每个事件序列调用。select方法接收匹配事件的字符串/事件对的映射。字符串由状态的名称定义。select方法返回确切的一个结果。

要收集结果,我们需要定义输出 POJO。在我们的案例中,假设我们需要生成警报作为输出。然后我们需要定义 POJO 如下:

package com.demo.chapter05; 

public class Alert { 

  private String message; 

  public String getMessage() { 
    return message; 
  } 

  public void setMessage(String message) { 
    this.message = message; 
  } 

  public Alert(String message) { 
    super(); 
    this.message = message; 
  } 

  @Override 
  public String toString() { 
    return "Alert [message=" + message + "]"; 
  } 

  @Override 
  public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + ((message == null) ? 0 :  
    message.hashCode()); 
    return result; 
  } 

  @Override 
  public boolean equals(Object obj) { 
    if (this == obj) 
      return true; 
    if (obj == null) 
      return false; 
    if (getClass() != obj.getClass()) 
      return false; 
    Alert other = (Alert) obj; 
    if (message == null) { 
      if (other.message != null) 
        return false; 
    } else if (!message.equals(other.message)) 
      return false; 
    return true; 
  } 

} 

接下来我们定义选择函数。

在 Java 中:

class MyPatternSelectFunction<IN, OUT> implements PatternSelectFunction<IN, OUT> { 
    @Override 
    public OUT select(Map<String, IN> pattern) { 
        IN startEvent = pattern.get("start"); 
        IN endEvent = pattern.get("end"); 
        return new OUT(startEvent, endEvent); 
    } 
} 

在 Scala 中:

def selectFn(pattern : mutable.Map[String, IN]): OUT = { 
    val startEvent = pattern.get("start").get 
    val endEvent = pattern.get("end").get 
    OUT(startEvent, endEvent) 
} 

flatSelect

flatSelect方法类似于select方法。两者之间的唯一区别是flatSelect可以返回任意数量的结果。flatSelect方法有一个额外的Collector参数,用于输出元素。

以下示例显示了如何使用flatSelect方法。

在 Java 中:

class MyPatternFlatSelectFunction<IN, OUT> implements PatternFlatSelectFunction<IN, OUT> { 
    @Override 
    public void select(Map<String, IN> pattern, Collector<OUT> collector) { 
        IN startEvent = pattern.get("start"); 
        IN endEvent = pattern.get("end"); 

        for (int i = 0; i < startEvent.getValue(); i++ ) { 
            collector.collect(new OUT(startEvent, endEvent)); 
        } 
    } 
} 

在 Scala 中:

def flatSelectFn(pattern : mutable.Map[String, IN], collector : Collector[OUT]) = { 
    val startEvent = pattern.get("start").get 
    val endEvent = pattern.get("end").get 
    for (i <- 0 to startEvent.getValue) { 
        collector.collect(OUT(startEvent, endEvent)) 
    } 
} 

处理超时的部分模式

有时,如果我们将模式限制在时间边界内,可能会错过某些事件。可能会丢弃事件,因为它们超出了长度。为了对超时事件采取行动,selectflatSelect方法允许超时处理程序。对于每个超时事件模式,都会调用此处理程序。

在这种情况下,select 方法包含两个参数:PatternSelectFunctionPatternTimeoutFunction。超时函数的返回类型可以与选择模式函数不同。超时事件和选择事件被包装在Either.RightEither.Left类中。

以下代码片段显示了我们在实践中如何做事情。

在 Java 中:

PatternStream<Event> patternStream = CEP.pattern(input, pattern); 

DataStream<Either<TimeoutEvent, ComplexEvent>> result = patternStream.select( 
    new PatternTimeoutFunction<Event, TimeoutEvent>() {...}, 
    new PatternSelectFunction<Event, ComplexEvent>() {...} 
); 

DataStream<Either<TimeoutEvent, ComplexEvent>> flatResult = patternStream.flatSelect( 
    new PatternFlatTimeoutFunction<Event, TimeoutEvent>() {...}, 
    new PatternFlatSelectFunction<Event, ComplexEvent>() {...} 
);  

在 Scala 中,选择 API:

val patternStream: PatternStream[Event] = CEP.pattern(input, pattern) 

DataStream[Either[TimeoutEvent, ComplexEvent]] result = patternStream.select{ 
    (pattern: mutable.Map[String, Event], timestamp: Long) => TimeoutEvent() 
} { 
    pattern: mutable.Map[String, Event] => ComplexEvent() 
} 

flatSelect API 与Collector一起调用,因为它可以发出任意数量的事件:

val patternStream: PatternStream[Event] = CEP.pattern(input, pattern) 

DataStream[Either[TimeoutEvent, ComplexEvent]] result = patternStream.flatSelect{ 
    (pattern: mutable.Map[String, Event], timestamp: Long, out: Collector[TimeoutEvent]) => 
        out.collect(TimeoutEvent()) 
} { 
    (pattern: mutable.Map[String, Event], out: Collector[ComplexEvent]) => 
        out.collect(ComplexEvent()) 
} 

用例 - 在温度传感器上进行复杂事件处理

在早期的章节中,我们学习了 Flink CEP 引擎提供的各种功能。现在是时候了解我们如何在现实世界的解决方案中使用它了。为此,让我们假设我们在一个生产某些产品的机械公司工作。在产品工厂中,有必要不断监视某些机器。工厂已经设置了传感器,这些传感器不断发送机器的温度。

现在我们将建立一个系统,不断监视温度值,并在温度超过一定值时生成警报。

我们可以使用以下架构:

温度传感器上的复杂事件处理用例

在这里,我们将使用 Kafka 从传感器收集事件。为了编写一个 Java 应用程序,我们首先需要创建一个 Maven 项目并添加以下依赖项:

  <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-cep-scala_2.11 --> 
    <dependency> 
      <groupId>org.apache.flink</groupId> 
      <artifactId>flink-cep-scala_2.11</artifactId> 
      <version>1.1.4</version> 
    </dependency> 
    <!-- https://mvnrepository.com/artifact/org.apache.flink/flink- streaming-java_2.11 --> 
    <dependency> 
      <groupId>org.apache.flink</groupId> 
      <artifactId>flink-streaming-java_2.11</artifactId> 
      <version>1.1.4</version> 
    </dependency> 
    <!-- https://mvnrepository.com/artifact/org.apache.flink/flink- streaming-scala_2.11 --> 
    <dependency> 
      <groupId>org.apache.flink</groupId> 
      <artifactId>flink-streaming-scala_2.11</artifactId> 
      <version>1.1.4</version> 
    </dependency> 
    <dependency> 
      <groupId>org.apache.flink</groupId> 
      <artifactId>flink-connector-kafka-0.9_2.11</artifactId> 
      <version>1.1.4</version> 
    </dependency> 

接下来,我们需要做以下事情来使用 Kafka。

首先,我们需要定义一个自定义的 Kafka 反序列化器。这将从 Kafka 主题中读取字节并将其转换为TemperatureEvent。以下是执行此操作的代码。

EventDeserializationSchema.java

package com.demo.chapter05; 

import java.io.IOException; 
import java.nio.charset.StandardCharsets; 

import org.apache.flink.api.common.typeinfo.TypeInformation; 
import org.apache.flink.api.java.typeutils.TypeExtractor; 
import org.apache.flink.streaming.util.serialization.DeserializationSchema; 

public class EventDeserializationSchema implements DeserializationSchema<TemperatureEvent> { 

  public TypeInformation<TemperatureEvent> getProducedType() { 
    return TypeExtractor.getForClass(TemperatureEvent.class); 
  } 

  public TemperatureEvent deserialize(byte[] arg0) throws IOException { 
    String str = new String(arg0, StandardCharsets.UTF_8); 

    String[] parts = str.split("="); 
    return new TemperatureEvent(parts[0], Double.parseDouble(parts[1])); 
  } 

  public boolean isEndOfStream(TemperatureEvent arg0) { 
    return false; 
  } 

} 

接下来,在 Kafka 中创建名为temperature的主题:

bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic temperature 

现在我们转到 Java 代码,该代码将监听 Flink 流中的这些事件:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 

    Properties properties = new Properties(); 
    properties.setProperty("bootstrap.servers", "localhost:9092"); 
    properties.setProperty("group.id", "test"); 

DataStream<TemperatureEvent> inputEventStream = env.addSource( 
        new FlinkKafkaConsumer09<TemperatureEvent>("temperature", new EventDeserializationSchema(), properties)); 

接下来,我们将定义模式,以检查温度是否在10秒内是否大于26.0摄氏度:

Pattern<TemperatureEvent, ?> warningPattern = Pattern.<TemperatureEvent> begin("first").subtype(TemperatureEvent.class).where(new FilterFunction<TemperatureEvent>() { 
          private static final long serialVersionUID = 1L; 

          public boolean filter(TemperatureEvent value) { 
            if (value.getTemperature() >= 26.0) { 
              return true; 
            } 
            return false; 
          } 
        }).within(Time.seconds(10)); 

接下来将此模式与事件流匹配并选择事件。我们还将将警报消息添加到结果流中,如下所示:

DataStream<Alert> patternStream = CEP.pattern(inputEventStream, warningPattern) 
        .select(new PatternSelectFunction<TemperatureEvent, Alert>() { 
          private static final long serialVersionUID = 1L; 

          public Alert select(Map<String, TemperatureEvent> event) throws Exception { 

            return new Alert("Temperature Rise Detected:" + event.get("first").getTemperature() 
                + " on machine name:" + event.get("first").getMachineName()); 
          } 

}); 

为了知道警报是如何生成的,我们将打印结果:

patternStream.print(); 

然后我们执行流:

env.execute("CEP on Temperature Sensor"); 

现在我们已经准备好执行应用程序了。当我们在 Kafka 主题中收到消息时,CEP 将继续执行。

实际执行将如下所示。以下是我们如何提供样本输入:

xyz=21.0 
xyz=30.0 
LogShaft=29.3 
Boiler=23.1 
Boiler=24.2 
Boiler=27.0 
Boiler=29.0 

以下是样本输出的样子:

Connected to JobManager at Actor[akka://flink/user/jobmanager_1#1010488393] 
10/09/2016 18:15:55  Job execution switched to status RUNNING. 
10/09/2016 18:15:55  Source: Custom Source(1/4) switched to SCHEDULED  
10/09/2016 18:15:55  Source: Custom Source(1/4) switched to DEPLOYING  
10/09/2016 18:15:55  Source: Custom Source(2/4) switched to SCHEDULED  
10/09/2016 18:15:55  Source: Custom Source(2/4) switched to DEPLOYING  
10/09/2016 18:15:55  Source: Custom Source(3/4) switched to SCHEDULED  
10/09/2016 18:15:55  Source: Custom Source(3/4) switched to DEPLOYING  
10/09/2016 18:15:55  Source: Custom Source(4/4) switched to SCHEDULED  
10/09/2016 18:15:55  Source: Custom Source(4/4) switched to DEPLOYING  
10/09/2016 18:15:55  CEPPatternOperator(1/1) switched to SCHEDULED  
10/09/2016 18:15:55  CEPPatternOperator(1/1) switched to DEPLOYING  
10/09/2016 18:15:55  Map -> Sink: Unnamed(1/4) switched to SCHEDULED  
10/09/2016 18:15:55  Map -> Sink: Unnamed(1/4) switched to DEPLOYING  
10/09/2016 18:15:55  Map -> Sink: Unnamed(2/4) switched to SCHEDULED  
10/09/2016 18:15:55  Map -> Sink: Unnamed(2/4) switched to DEPLOYING  
10/09/2016 18:15:55  Map -> Sink: Unnamed(3/4) switched to SCHEDULED  
10/09/2016 18:15:55  Map -> Sink: Unnamed(3/4) switched to DEPLOYING  
10/09/2016 18:15:55  Map -> Sink: Unnamed(4/4) switched to SCHEDULED  
10/09/2016 18:15:55  Map -> Sink: Unnamed(4/4) switched to DEPLOYING  
10/09/2016 18:15:55  Source: Custom Source(2/4) switched to RUNNING  
10/09/2016 18:15:55  Source: Custom Source(3/4) switched to RUNNING  
10/09/2016 18:15:55  Map -> Sink: Unnamed(1/4) switched to RUNNING  
10/09/2016 18:15:55  Map -> Sink: Unnamed(2/4) switched to RUNNING  
10/09/2016 18:15:55  Map -> Sink: Unnamed(3/4) switched to RUNNING  
10/09/2016 18:15:55  Source: Custom Source(4/4) switched to RUNNING  
10/09/2016 18:15:55  Source: Custom Source(1/4) switched to RUNNING  
10/09/2016 18:15:55  CEPPatternOperator(1/1) switched to RUNNING  
10/09/2016 18:15:55  Map -> Sink: Unnamed(4/4) switched to RUNNING  
1> Alert [message=Temperature Rise Detected:30.0 on machine name:xyz] 
2> Alert [message=Temperature Rise Detected:29.3 on machine name:LogShaft] 
3> Alert [message=Temperature Rise Detected:27.0 on machine name:Boiler] 
4> Alert [message=Temperature Rise Detected:29.0 on machine name:Boiler] 

我们还可以配置邮件客户端并使用一些外部网络钩子来发送电子邮件或即时通讯通知。

注意

应用程序的代码可以在 GitHub 上找到:github.com/deshpandetanmay/mastering-flink

摘要

在本章中,我们学习了 CEP。我们讨论了涉及的挑战以及我们如何使用 Flink CEP 库来解决 CEP 问题。我们还学习了 Pattern API 以及我们可以使用的各种运算符来定义模式。在最后一节中,我们试图连接各个点,看到一个完整的用例。通过一些改变,这个设置也可以在其他领域中使用。

在下一章中,我们将看到如何使用 Flink 的内置机器学习库来解决复杂的问题。

第六章:使用 FlinkML 进行机器学习

在上一章中,我们讨论了如何使用 Flink CEP 库解决复杂的事件处理问题。在本章中,我们将看到如何使用 Flink 的机器学习库 FlinkML 进行机器学习。FlinkML 包括一组支持的算法,可用于解决现实生活中的用例。在本章中,我们将看看 FlinkML 中有哪些算法以及如何应用它们。

在深入研究 FlinkML 之前,让我们首先尝试理解基本的机器学习原理。

什么是机器学习?

机器学习是一种利用数学让机器根据提供给它们的数据进行分类、预测、推荐等的工程流。这个领域非常广阔,我们可以花费数年来讨论它。但为了保持我们的讨论集中,我们只讨论本书范围内所需的内容。

非常广泛地,机器学习可以分为三大类:

  • 监督学习

  • 无监督学习

  • 半监督学习!什么是机器学习?

前面的图表显示了机器学习算法的广泛分类。现在让我们详细讨论这些。

监督学习

在监督学习中,我们通常会得到一个输入数据集,这是实际事件的历史记录。我们还知道预期的输出应该是什么样子。使用历史数据,我们选择了哪些因素导致了结果。这些属性被称为特征。使用历史数据,我们了解了以前的结果是如何计算的,并将相同的理解应用于我们想要进行预测的数据。

监督学习可以再次细分为:

  • 回归

  • 分类

回归

在回归问题中,我们试图使用连续函数的输入来预测结果。回归意味着基于另一个变量的分数来预测一个变量的分数。我们将要预测的变量称为标准变量,我们将进行预测的变量称为预测变量。可能会有多个预测变量;在这种情况下,我们需要找到最佳拟合线,称为回归线。

注意

您可以在en.wikipedia.org/wiki/Regression_analysis上了解更多关于回归的信息。

用于解决回归问题的一些常见算法如下:

  • 逻辑回归

  • 决策树

  • 支持向量机(SVM)

  • 朴素贝叶斯

  • 随机森林

  • 线性回归

  • 多项式回归

分类

在分类中,我们预测离散结果的输出。分类作为监督学习的一部分,也需要提供输入数据和样本输出。在这里,基于特征,我们试图将结果分类为一组定义好的类别。例如,根据给定的特征,将人员记录分类为男性或女性。或者,根据客户行为,预测他/她是否会购买产品。或者根据电子邮件内容和发件人,预测电子邮件是否是垃圾邮件。参考en.wikipedia.org/wiki/Statistical_classification

为了理解回归和分类之间的区别,考虑股票数据的例子。回归算法可以帮助预测未来几天股票的价值,而分类算法可以帮助决定是否购买股票。

无监督学习

无监督学习并不给我们任何关于结果应该如何的想法。相反,它允许我们根据属性的特征对数据进行分组。我们根据记录之间的关系推导出聚类。

与监督学习不同,我们无法验证结果,这意味着没有反馈方法来告诉我们是否做对了还是错了。无监督学习主要基于聚类算法。

聚类

为了更容易理解聚类,让我们考虑一个例子;假设我们有 2 万篇关于各种主题的新闻文章,我们需要根据它们的内容对它们进行分组。在这种情况下,我们可以使用聚类算法,将一组文章分成小组。

我们还可以考虑水果的基本例子。假设我们有苹果、香蕉、柠檬和樱桃在一个水果篮子里,我们需要将它们分类成组。如果我们看它们的颜色,我们可以将它们分成两组:

  • 红色组:苹果和樱桃

  • 黄色组:香蕉和柠檬

现在我们可以根据另一个特征,它的大小,进行更多的分组:

  • 红色和大尺寸:苹果

  • 红色和小尺寸:樱桃

  • 黄色和大尺寸:香蕉

  • 黄色和小尺寸:柠檬

以下图表显示了聚类的表示:

聚类

通过查看更多特征,我们也可以进行更多的聚类。在这里,我们没有任何训练数据和要预测的变量,不像在监督学习中。我们的唯一任务是学习更多关于特征,并根据输入对记录进行聚类。

以下是一些常用于聚类的算法:

  • K 均值聚类

  • 层次聚类

  • 隐马尔可夫模型

关联

关联问题更多是关于学习和通过定义关联规则进行推荐。例如,关联规则可能指的是购买 iPhone 的人更有可能购买 iPhone 手机壳的假设。

如今,许多零售公司使用这些算法进行个性化推荐。例如,在www.amazon.com,如果我倾向于购买产品X,然后亚马逊也向我推荐产品Y,那么这两者之间一定存在一些关联。

基于这些原则的一些算法如下:

  • Apriori 算法

  • Eclat 算法

  • FDP 增长算法

半监督学习

半监督学习是监督学习的一个子类,它考虑了用于训练的未标记数据。通常,在训练过程中,有大量未标记数据,只有很少量的标记数据。许多研究人员和机器学习实践者发现,当标记数据与未标记数据一起使用时,结果很可能更准确。

注意

有关半监督学习的更多细节,请参阅en.wikipedia.org/wiki/Semi-supervised_learning

FlinkML

FlinkML 是由 Flink 支持的一组算法库,可用于解决现实生活中的用例。这些算法被构建成可以利用 Flink 的分布式计算能力,并且可以轻松进行预测、聚类等。目前,只支持了少量算法集,但列表正在增长。

FlinkML 的重点是 ML 开发人员需要编写最少的粘合代码。粘合代码是帮助将各种组件绑定在一起的代码。FlinkML 的另一个目标是保持算法的使用简单。

Flink 利用内存数据流和本地执行迭代数据处理。FlinkML 允许数据科学家在本地测试他们的模型,使用数据子集,然后在更大的数据上以集群模式执行它们。

FlinkML 受 scikit-learn 和 Spark 的 MLlib 启发,允许您清晰地定义数据管道,并以分布式方式解决机器学习问题。

Flink 开发团队的路线图如下:

  • 转换器和学习者的管道

  • 数据预处理:

  • 特征缩放

  • 多项式特征基映射

  • 特征哈希

  • 文本特征提取

  • 降维

  • 模型选择和性能评估:

  • 使用各种评分函数进行模型评估

  • 用于模型选择和评估的交叉验证

  • 超参数优化

  • 监督学习:

  • 优化框架

  • 随机梯度下降

  • L-BFGS

  • 广义线性模型

  • 多元线性回归

  • LASSO,岭回归

  • 多类逻辑回归

  • 随机森林

  • 支持向量机

  • 决策树

  • 无监督学习:

  • 聚类

  • K 均值聚类

  • 主成分分析

  • 推荐:

  • ALS

  • 文本分析:

  • LDA

  • 统计估计工具

  • 分布式线性代数

  • 流式机器学习

突出显示的算法已经是现有的 Flink 源代码的一部分。在接下来的部分中,我们将看看如何在实践中使用它们。

支持的算法

要开始使用 FlinkML,我们首先需要添加以下 Maven 依赖项:

<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-ml_2.11 --> 
<dependency> 
    <groupId>org.apache.flink</groupId> 
    <artifactId>flink-ml_2.11</artifactId> 
    <version>1.1.4</version> 
</dependency> 

现在让我们试着了解支持的算法以及如何使用它们。

监督学习

Flink 支持监督学习类别中的三种算法。它们如下:

  • 支持向量机(SVM)

  • 多元线性回归

  • 优化框架

让我们一次学习一个开始。

支持向量机

支持向量机SVMs)是监督学习模型,用于解决分类和回归问题。它有助于将对象分类到一个类别或另一个类别。它是非概率线性分类。SVM 可以用在各种例子中,例如以下情况:

  • 常规数据分类问题

  • 文本和超文本分类问题

  • 图像分类问题

  • 生物学和其他科学问题

Flink 支持基于软间隔的 SVM,使用高效的通信分布式双坐标上升算法。

有关该算法的详细信息可在ci.apache.org/projects/flink/flink-docs-release-1.2/dev/libs/ml/svm.html#description找到。

Flink 使用随机双坐标上升SDCA)来解决最小化问题。为了使该算法在分布式环境中高效,Flink 使用 CoCoA 算法,该算法在本地数据块上计算 SDCA,然后将其合并到全局状态中。

注意

该算法的实现基于以下论文:arxiv.org/pdf/1409.1458v2.pdf

现在让我们看看如何使用该算法解决实际问题。我们将以鸢尾花数据集(en.wikipedia.org/wiki/Iris_flower_data_set)为例,该数据集由四个属性组成,决定了鸢尾花的种类。以下是一些示例数据:

萼片长度 萼片宽度 花瓣长度 花瓣宽度 种类
5.1 3.5 1.4 0.2 1
5.6 2.9 3.6 1.3 2
5.8 2.7 5.1 1.9 3

在这里,使用数字格式的类别作为 SVM 的输入非常重要:

种类代码 种类名称
1 鸢尾花山鸢尾
2 鸢尾花变色鸢尾
3 鸢尾花维吉尼亚

在使用 Flink 的 SVM 算法之前,我们需要做的另一件事是将 CSV 数据转换为 LibSVM 数据。

注意

LibSVM 数据是一种用于指定 SVM 数据集的特殊格式。有关 LibSVM 的更多信息,请访问www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/

要将 CSV 数据转换为 LibSVM 数据,我们将使用github.com/zygmuntz/phraug/blob/master/csv2libsvm.py上提供的一些开源 Python 代码。

要将 CSV 转换为 LibSVM,我们需要执行以下命令:

    csv2libsvm.py <input file> <output file> [<label index = 0>] [<skip 
    headers = 0>]

现在让我们开始编写程序:

package com.demo.chapter06 

import org.apache.flink.api.scala._ 
import org.apache.flink.ml.math.Vector 
import org.apache.flink.ml.common.LabeledVector 
import org.apache.flink.ml.classification.SVM 
import org.apache.flink.ml.RichExecutionEnvironment 

object MySVMApp { 
  def main(args: Array[String]) { 
    // set up the execution environment 
    val pathToTrainingFile: String = "iris-train.txt" 
    val pathToTestingFile: String = "iris-train.txt" 
    val env = ExecutionEnvironment.getExecutionEnvironment 

    // Read the training dataset, from a LibSVM formatted file 
    val trainingDS: DataSet[LabeledVector] = 
    env.readLibSVM(pathToTrainingFile) 

    // Create the SVM learner 
    val svm = SVM() 
      .setBlocks(10) 

    // Learn the SVM model 
    svm.fit(trainingDS) 

    // Read the testing dataset 
    val testingDS: DataSet[Vector] = 
    env.readLibSVM(pathToTestingFile).map(_.vector) 

    // Calculate the predictions for the testing dataset 
    val predictionDS: DataSet[(Vector, Double)] = 
    svm.predict(testingDS) 
    predictionDS.writeAsText("out") 

    env.execute("Flink SVM App") 
  } 
} 

所以,现在我们已经准备好运行程序了,您将在输出文件夹中看到预测的输出。

以下是代码:

(SparseVector((0,5.1), (1,3.5), (2,1.4), (3,0.2)),1.0) 
(SparseVector((0,4.9), (1,3.0), (2,1.4), (3,0.2)),1.0) 
(SparseVector((0,4.7), (1,3.2), (2,1.3), (3,0.2)),1.0) 
(SparseVector((0,4.6), (1,3.1), (2,1.5), (3,0.2)),1.0) 
(SparseVector((0,5.0), (1,3.6), (2,1.4), (3,0.2)),1.0) 
(SparseVector((0,5.4), (1,3.9), (2,1.7), (3,0.4)),1.0) 
(SparseVector((0,4.6), (1,3.4), (2,1.4), (3,0.3)),1.0) 
(SparseVector((0,5.0), (1,3.4), (2,1.5), (3,0.2)),1.0) 
(SparseVector((0,4.4), (1,2.9), (2,1.4), (3,0.2)),1.0) 
(SparseVector((0,4.9), (1,3.1), (2,1.5), (3,0.1)),1.0) 
(SparseVector((0,5.4), (1,3.7), (2,1.5), (3,0.2)),1.0) 
(SparseVector((0,4.8), (1,3.4), (2,1.6), (3,0.2)),1.0) 
(SparseVector((0,4.8), (1,3.0), (2,1.4), (3,0.1)),1.0) 

我们还可以通过设置各种参数来微调结果:

参数 描述
Blocks 设置输入数据将被分成的块数。最好将这个数字设置为你想要实现的并行度。在每个块上,执行本地随机对偶坐标上升。默认值为None
Iterations 设置外部循环方法的迭代次数,例如,SDCA 方法在分块数据上应用的次数。默认值为10
LocalIterations 定义需要在本地执行的 SDCA 迭代的最大次数。默认值为10
Regularization 设置算法的正则化常数。您设置的值越高,加权向量的 2 范数就越小。默认值为1
StepSize 定义了权重向量更新的初始步长。在算法变得不稳定的情况下,需要设置这个值。默认值为1.0
ThresholdValue 定义决策函数的限制值。默认值为0.0
OutputDecisionFunction 将其设置为 true 将返回每个示例的超平面距离。将其设置为 false 将返回二进制标签。
Seed 设置随机长整数。这将用于初始化随机数生成器。

多元线性回归

多元线性回归MLR)是简单线性回归的扩展,其中使用多个自变量(X)来确定单个自变量(Y)。预测值是输入变量的线性变换,使得观察值和预测值的平方偏差之和最小。

MLR 试图通过拟合线性方程来建模多个解释变量和响应变量之间的关系。

注意

关于 MLR 的更详细的解释可以在此链接找到www.stat.yale.edu/Courses/1997-98/101/linmult.htm

现在让我们尝试使用 MLR 解决鸢尾花数据集的相同分类问题。首先,我们需要训练数据集来训练我们的模型。

在这里,我们将使用在 SVM 上一节中使用的相同的数据文件。现在我们有iris-train.txtiris-test.txt,它们已经转换成了 LibSVM 格式。

以下代码片段显示了如何使用 MLR:

package com.demo.flink.ml 

import org.apache.flink.api.scala._ 
import org.apache.flink.ml._ 
import org.apache.flink.ml.common.LabeledVector 
import org.apache.flink.ml.math.DenseVector 
import org.apache.flink.ml.math.Vector 
import org.apache.flink.ml.preprocessing.Splitter 
import org.apache.flink.ml.regression.MultipleLinearRegression 

object MLRJob { 
  def main(args: Array[String]) { 
    // set up the execution environment 
    val env = ExecutionEnvironment.getExecutionEnvironment 
    val trainingDataset = MLUtils.readLibSVM(env, "iris-train.txt") 
    val testingDataset = MLUtils.readLibSVM(env, "iris-test.txt").map { 
    lv => lv.vector } 
    val mlr = MultipleLinearRegression() 
      .setStepsize(1.0) 
      .setIterations(5) 
      .setConvergenceThreshold(0.001) 

    mlr.fit(trainingDataset) 

    // The fitted model can now be used to make predictions 
    val predictions = mlr.predict(testingDataset) 

    predictions.print() 

  } 
} 

完整的代码和数据文件可以在github.com/deshpandetanmay/mastering-flink/tree/master/chapter06上下载。我们还可以通过设置各种参数来微调结果:

参数 描述
Iterations 设置最大迭代次数。默认值为10
Stepsize 梯度下降方法的步长。这个值控制了梯度下降方法在相反方向上可以移动多远。调整这个参数非常重要,以获得更好的结果。默认值为0.1
Convergencethreshold 直到迭代停止的平方残差的相对变化的阈值。默认值为None
Learningratemethod Learningratemethod 用于计算每次迭代的学习率。

优化框架

Flink 中的优化框架是一个开发者友好的包,可以用来解决优化问题。这不是一个解决确切问题的特定算法,而是每个机器学习问题的基础。

一般来说,它是关于找到一个模型,带有一组参数,通过最小化函数。FlinkML 支持随机梯度下降SGD),并具有以下类型的正则化:

正则化函数 类名
L1 正则化 GradientDescentL1
L2 正则化 GradientDescentL2
无正则化 SimpleGradient

以下代码片段显示了如何使用 FlinkML 使用 SGD:

// Create SGD solver 
val sgd = GradientDescentL1() 
  .setLossFunction(SquaredLoss()) 
  .setRegularizationConstant(0.2) 
  .setIterations(100) 
  .setLearningRate(0.01) 
  .setLearningRateMethod(LearningRateMethod.Xu(-0.75)) 

// Obtain data 
val trainingDS: DataSet[LabeledVector] = ... 

// Optimize the weights, according to the provided data 
val weightDS = sgd.optimize(trainingDS) 

我们还可以使用参数来微调算法:

参数 描述

| LossFunction | Flink 支持以下损失函数:

  • 平方损失

  • 铰链损失

  • 逻辑损失

  • 默认值为None

|

RegularizationConstant 要应用的正则化权重。默认值为0.1
Iterations 要执行的最大迭代次数。默认值为10
ConvergenceThreshold 直到迭代停止的残差平方和的相对变化阈值。默认值为None
LearningRateMethod 用于计算每次迭代的学习率的方法。
LearningRate 这是梯度下降方法的初始学习率。
Decay 默认值为0.0

推荐

推荐引擎是最有趣和最常用的机器学习技术之一,用于提供基于用户和基于项目的推荐。亚马逊等电子商务公司使用推荐引擎根据客户的购买模式和评论评分来个性化推荐。

Flink 还支持基于 ALS 的推荐。让我们更详细地了解 ALS。

交替最小二乘法

交替最小二乘法ALS)算法将给定的矩阵R分解为两个因子UV,使得 交替最小二乘法

为了更好地理解该算法的应用,让我们假设我们有一个包含用户u对书籍b提供的评分r的数据集。

这是一个样本数据格式(user_idbook_idrating)

1  10 1 
1  11 2 
1  12 5 
1  13 5 
1  14 5 
1  15 4 
1  16 5 
1  17 1 
1  18 5 
2  10 1 
2  11 2 
2  15 5 
2  16 4.5 
2  17 1 
2  18 5 
3  11 2.5 
3  12 4.5 
3  13 4 
3  14 3 
3  15 3.5 
3  16 4.5 
3  17 4 
3  18 5 
4  10 5 
4  11 5 
4  12 5 
4  13 0 
4  14 2 
4  15 3 
4  16 1 
4  17 4 
4  18 1 

现在我们可以将这些信息提供给 ALS 算法,并开始从中获得推荐。以下是使用 ALS 的代码片段:

package com.demo.chapter06 

import org.apache.flink.api.scala._ 
import org.apache.flink.ml.recommendation._ 
import org.apache.flink.ml.common.ParameterMap 

object MyALSApp { 
  def main(args: Array[String]): Unit = { 

    val env = ExecutionEnvironment.getExecutionEnvironment 
    val inputDS: DataSet[(Int, Int, Double)] = env.readCsvFile(Int,  
    Int, Double) 

    // Setup the ALS learner 
    val als = ALS() 
      .setIterations(10) 
      .setNumFactors(10) 
      .setBlocks(100) 
      .setTemporaryPath("tmp") 

    // Set the other parameters via a parameter map 
    val parameters = ParameterMap() 
      .add(ALS.Lambda, 0.9) 
      .add(ALS.Seed, 42L) 

    // Calculate the factorization 
    als.fit(inputDS, parameters) 

    // Read the testing dataset from a csv file 
    val testingDS: DataSet[(Int, Int)] = env.readCsvFile[(Int, Int)]   
    ("test-data.csv") 

    // Calculate the ratings according to the matrix factorization 
    val predictedRatings = als.predict(testingDS) 

    predictedRatings.writeAsCsv("output") 

    env.execute("Flink Recommendation App") 
  } 
} 

一旦您执行应用程序,您将获得推荐结果。与其他算法一样,您可以微调参数以获得更好的结果:

参数 描述
NumFactors 用于基础模型的潜在因子的数量。默认值为10
Lambda 这是一个正则化因子;我们可以调整此参数以获得更好的结果。默认值为1
Iterations 要执行的最大迭代次数。默认值为10
Blocks 用户和项目矩阵分组的块数。块越少,发送的冗余数据就越少。默认值为None
Seed 用于初始化项目矩阵生成器的种子值。默认值为0
TemporaryPath 这是用于存储中间结果的路径。

无监督学习

现在让我们试着了解 FinkML 为无监督学习提供了什么。目前,它只支持一种算法,称为 k 最近邻接算法。

k 最近邻接

k 最近邻接kNN)算法旨在为另一个数据集中的每个对象找到 k 个最近邻居。它是许多数据挖掘算法中最常用的解决方案之一。kNN 是一项昂贵的操作,因为它是找到 k 个最近邻居并执行连接的组合。考虑到数据的量,很难在集中的单台机器上执行此操作,因此总是很好地拥有可以在分布式架构上工作的解决方案。FlinkML 算法提供了分布式环境下的 kNN。

注意

可以在这里找到一篇描述在分布式环境中实现 kNN 的研究论文:arxiv.org/pdf/1207.0141v1.pdf

在这里,想法是计算每个训练和测试点之间的距离,然后找到给定点的最近点。计算每个点之间的距离是一项耗时的活动,在 Flink 中通过实现四叉树来简化。

使用四叉树通过对数据集进行分区来减少计算。这将计算减少到仅对数据子集进行。以下图表显示了使用四叉树和不使用四叉树的计算:

k 最近邻连接

您可以在这里找到有关使用四叉树计算最近邻居的详细讨论:danielblazevski.github.io/assets/player/KeynoteDHTMLPlayer.html

四叉树并不总是表现得更好。如果数据是空间的,四叉树可能是最糟糕的选择。但作为开发人员,我们不需要担心这一点,因为 FlinkML 会根据可用的数据来决定是否使用四叉树。

以下代码片段显示了如何在 FlinkML 中使用 kNN 连接:

import org.apache.flink.api.common.operators.base.CrossOperatorBase.CrossHint 
import org.apache.flink.api.scala._ 
import org.apache.flink.ml.nn.KNN 
import org.apache.flink.ml.math.Vector 
import org.apache.flink.ml.metrics.distances.SquaredEuclideanDistanceMetric 

val env = ExecutionEnvironment.getExecutionEnvironment 

// prepare data 
val trainingSet: DataSet[Vector] = ... 
val testingSet: DataSet[Vector] = ... 

val knn = KNN() 
  .setK(3) 
  .setBlocks(10) 
  .setDistanceMetric(SquaredEuclideanDistanceMetric()) 
  .setUseQuadTree(false) 
  .setSizeHint(CrossHint.SECOND_IS_SMALL) 

// run knn join 
knn.fit(trainingSet) 
val result = knn.predict(testingSet).collect() 

以下是一些我们可以用来微调结果的参数:

参数 描述
K 要搜索的最近邻居的数量。默认值为5
DistanceMetric 设置用于计算两点之间距离的距离度量。默认情况下,使用欧几里德距离度量。
Blocks 输入数据应该分成的块数。将此数字设置为并行度的理想值。
UseQuadTree 设置是否使用四叉树进行处理。默认值为None。如果未指定任何内容,算法会自行决定。

实用程序

FlinkML 支持各种可扩展的实用程序,在进行数据分析和预测时非常方便。其中一个实用程序是距离度量。Flink 支持一组可以使用的距离度量。以下链接显示了 Flink 支持的距离度量:ci.apache.org/projects/flink/flink-docs-release-1.2/dev/libs/ml/distance_metrics.html

如果前面提到的算法都不能满足您的需求,您可以考虑编写自己的自定义距离算法。以下代码片段显示了如何实现:

class MyDistance extends DistanceMetric { 
  override def distance(a: Vector, b: Vector) = ... // your implementation  
} 

object MyDistance { 
  def apply() = new MyDistance() 
} 

val myMetric = MyDistance() 

使用距离度量的一个很好的应用是 kNN 连接算法,您可以设置要使用的距离度量。

另一个重要的实用程序是Splitter,它可以用于交叉验证。在某些情况下,我们可能没有测试数据集来验证我们的结果。在这种情况下,我们可以使用Splitter来拆分训练数据集。

以下是一个示例:

// A Simple Train-Test-Split 
val dataTrainTest: TrainTestDataSet = Splitter.trainTestSplit(data, 0.6, true) 

在前面的示例中,我们将训练数据集分成了实际数据的 60%和 40%的部分。

还有另一种获取更好结果的方法,称为TrainTestHoldout拆分。在这里,我们使用一部分数据进行训练,一部分进行测试,另一部分用于最终结果验证。以下代码片段显示了如何实现:

// Create a train test holdout DataSet 
val dataTrainTestHO: trainTestHoldoutDataSet = Splitter.trainTestHoldoutSplit(data, Array(6.0, 3.0, 1.0)) 

我们可以使用另一种策略,称为 K 折拆分。在这种方法中,训练集被分成k个相等大小的折叠。在这里,为每个折叠创建一个算法,然后针对其测试集进行验证。以下代码显示了如何进行 K 折拆分:

// Create an Array of K TrainTestDataSets 
val dataKFolded: Array[TrainTestDataSet] =  Splitter.kFoldSplit(data, 10) 

我们还可以使用多随机拆分;在这里,我们可以指定要创建多少个数据集以及原始数据的什么部分:

// create an array of 5 datasets of 1 of 50%, and 5 of 10% each  
val dataMultiRandom: Array[DataSet[T]] = Splitter.multiRandomSplit(data, Array(0.5, 0.1, 0.1, 0.1, 0.1)) 

数据预处理和管道

Flink 支持 Python scikit-learn 风格的管道。FlinkML 中的管道是将多个转换器和预测器链接在一起的特性。一般来说,许多数据科学家希望轻松地查看和构建机器学习应用的流程。Flink 允许他们使用管道的概念来实现这一点。

一般来说,ML 管道有三个构建块:

  • 估计器: 估计器使用fit方法对模型进行实际训练。例如,在线性回归模型中找到正确的权重。

  • 转换器: 转换器正如其名称所示,具有一个transform方法,可以帮助进行输入缩放。

  • 预测器: 预测器具有predict方法,该方法应用算法生成预测,例如,SVM 或 MLR。

管道是一系列估计器、转换器和预测器。预测器是管道的末端,在此之后不能再链接任何内容。

Flink 支持各种数据预处理工具,这将有助于我们提高结果。让我们开始了解细节。

多项式特征

多项式特征是一种将向量映射到* d *次多项式特征空间的转换器。多项式特征有助于通过改变函数的图形来解决分类问题。让我们通过一个例子来理解这一点:

  • 考虑一个线性公式:F(x,y) = 1x + 2y;

  • 想象我们有两个观察结果:

  • x=12y=2

  • x=5y =5.5

在这两种情况下,我们得到 f() = 16。如果这些观察结果属于两个不同的类别,那么我们无法区分这两个类别。现在,如果我们添加一个称为z的新特征,该特征是前两个特征的组合z = x+y

现在 f(x,y,z) = 1x + 2y + 3z*

现在相同的观察结果将是

  • (112)+ (22) + (324) = 88*

  • (15)+ (25.5) + (327.5) = 98.5*

通过使用现有特征添加新特征的方式可以帮助我们获得更好的结果。Flink 多项式特征允许我们使用预构建函数做同样的事情。

为了在 Flink 中使用多项式特征,我们有以下代码:

val polyFeatures = PolynomialFeatures() 
      .setDegree(3) 

标准缩放器

标准缩放器通过使用用户指定的均值和方差来缩放输入数据。如果用户没有指定任何值,则默认均值为0,标准差为1。标准缩放器是一个具有fittransform方法的转换器。

首先,我们需要像下面的代码片段中所示定义均值和标准差的值:

  val scaler = StandardScaler() 
      .setMean(10.0) 
      .setStd(2.0) 

接下来,我们需要让它了解训练数据集的均值和标准差,如下面的代码片段所示:

scaler.fit(trainingDataset)

最后,我们使用用户定义的均值和标准差来缩放提供的数据,如下面的代码片段所示:

val scaledDS = scaler.transform(trainingDataset)

现在我们可以使用这些缩放后的输入数据进行进一步的转换和分析。

最小-最大缩放器

最小-最大缩放器类似于标准缩放器,但唯一的区别是它确保每个特征的缩放位于用户定义的minmax值之间。

以下代码片段显示了如何使用它:

val minMaxscaler = MinMaxScaler()
.setMin(1.0)
.setMax(3.0)
minMaxscaler.fit(trainingDataset)
val scaledDS = minMaxscaler.transform(trainingDataset)

因此,我们可以使用这些数据预处理操作来增强结果。这些还可以组合在管道中创建工作流程。

以下代码片段显示了如何在管道中使用这些数据预处理操作:

// Create pipeline PolynomialFeatures -> MultipleLinearRegression
val pipeline = polyFeatures.chainPredictor(mlr)
// train the model
pipeline.fit(scaledDS)
// The fitted model can now be used to make predictions
val predictions = pipeline.predict(testingDataset)
predictions.print()

完整的代码可在 GitHub 上找到github.com/deshpandetanmay/mastering-flink/tree/master/chapter06

摘要

在本章中,我们了解了不同类型的机器学习算法。我们看了各种监督和无监督算法,以及它们各自的示例。我们还看了 FlinkML 提供的各种实用工具,在数据分析过程中非常方便。后来我们看了数据预处理操作以及如何在管道中使用它们。

在接下来的章节中,我们将看一下 Flink 的图处理能力。

第七章: Flink 图 API - Gelly

我们生活在社交媒体时代,每个人都以某种方式与他人联系。每个单独的对象都与另一个对象有关系。Facebook 和 Twitter 是社交图的绝佳例子,其中xy是朋友,p正在关注q,等等。这些图如此庞大,以至于我们需要一个能够高效处理它们的引擎。如果我们被这样的图所包围,分析它们以获取更多关于它们关系和下一级关系的见解非常重要。

市场上有各种技术可以帮助我们分析这样的图,例如 Titan 和 Neo4J 等图数据库,Spark GraphX 和 Flink Gelly 等图处理库等。在本章中,我们将了解图的细节以及如何使用 Flink Gelly 来分析图数据。

那么让我们开始吧。

什么是图?

在计算机科学领域,图是表示对象之间关系的一种方式。它由一组通过边连接的顶点组成。顶点是平面上的对象,由坐标或某个唯一的 id/name 标识,而是连接顶点的链接,具有一定的权重或关系。图可以是有向的或无向的。在有向图中,边从一个顶点指向另一个顶点,而在无向图中,边没有方向。

以下图表显示了有向图的基本表示:

什么是图?

图结构可以用于各种目的,例如找到到达某个目的地的最短路径,或者用于查找某些顶点之间关系的程度,或者用于查找最近的邻居。

现在让我们深入了解 Flink 的图 API - Gelly。

Flink 图 API - Gelly

Flink 提供了一个名为 Gelly 的图处理库,以简化图分析的开发。它提供了用于存储和表示图数据的数据结构,并提供了分析图的方法。在 Gelly 中,我们可以使用 Flink 的高级函数将图从一种状态转换为另一种状态。它还提供了一组用于详细图分析的算法。

Gelly 目前作为 Flink 库的一部分可用,因此我们需要在程序中添加 Maven 依赖项才能使用它。

Java 依赖:

<dependency> 
    <groupId>org.apache.flink</groupId> 
    <artifactId>flink-gelly_2.11</artifactId> 
    <version>1.1.4</version> 
</dependency> 

Scala 依赖:

<dependency> 
    <groupId>org.apache.flink</groupId> 
    <artifactId>flink-gelly-scala_2.11</artifactId> 
    <version>1.1.4</version> 
</dependency> 

现在让我们看看我们有哪些选项可以有效地使用 Gelly。

图表示

在 Gelly 中,图被表示为节点数据集和边数据集。

图节点

图节点由Vertex数据类型表示。Vertex数据类型包括唯一 ID 和可选值。唯一 ID 应实现可比较接口,因为在进行图处理时,我们通过它们的 ID 进行比较。一个Vertex可以有一个值,也可以有一个空值。空值顶点由类型NullValue定义。

以下代码片段显示了如何创建节点:

在 Java 中:

// A vertex with a Long ID and a String value 
Vertex<Long, String> v = new Vertex<Long, String>(1L, "foo"); 

// A vertex with a Long ID and no value 
Vertex<Long, NullValue> v = new Vertex<Long, NullValue>(1L, NullValue.getInstance()); 

在 Scala 中:

// A vertex with a Long ID and a String value 
val v = new Vertex(1L, "foo") 

// A vertex with a Long ID and no value 
val v = new Vertex(1L, NullValue.getInstance()) 

图边

同样,边可以由类型Edge定义。Edge具有源节点 ID、目标节点 ID 和可选值。该值表示关系的程度或权重。源和目标 ID 需要是相同类型的。没有值的边可以使用NullValue定义。

以下代码片段显示了 Java 和 Scala 中的Edge定义:

在 Java 中:

// Edge connecting Vertices with Ids 1 and 2 having weight 0.5 

Edge<Long, Double> e = new Edge<Long, Double>(1L, 2L, 0.5); 

Double weight = e.getValue(); // weight = 0.5 

在 Scala 中:

// Edge connecting Vertices with Ids 1 and 2 having weight 0.5 

val e = new Edge(1L, 2L, 0.5) 

val weight = e.getValue // weight = 0.5 

在 Gelly 中,图始终是从源顶点到目标顶点的有向的。为了显示无向图,我们应该添加另一条边,表示从目标到源的连接和返回。

以下代码片段表示了 Gelly 中的有向图:

在 Java 中:

// A vertex with a Long ID and a String value 
Vertex<Long, String> v1 = new Vertex<Long, String>(1L, "foo"); 

// A vertex with a Long ID and a String value 
Vertex<Long, String> v2 = new Vertex<Long, String>(2L, "bar"); 

// Edge connecting Vertices with Ids 1 and 2 having weight 0.5 

Edge<Long, Double> e = new Edge<Long, Double>(1L, 2L, 0.5); 

在 Scala 中:

// A vertex with a Long ID and a String value 
val v1 = new Vertex(1L, "foo") 

// A vertex with a Long ID and a String value 
val v2 = new Vertex(1L, "bar") 

// Edge connecting Vertices with Ids 1 and 2 having weight 0.5 

val e = new Edge(1L, 2L, 0.5) 

以下是它的可视化表示:

图边

以下代码片段表示了 Gelly 中无向图的顶点和边的定义:

在 Java 中:

// A vertex with a Long ID and a String value 
Vertex<Long, String> v1 = new Vertex<Long, String>(1L, "foo"); 

// A vertex with a Long ID and a String value 
Vertex<Long, String> v2 = new Vertex<Long, String>(2L, "bar"); 

// Edges connecting Vertices with Ids 1 and 2 having weight 0.5 

Edge<Long, Double> e1 = new Edge<Long, Double>(1L, 2L, 0.5); 

Edge<Long, Double> e2 = new Edge<Long, Double>(2L, 1L, 0.5); 

在 Scala 中:

// A vertex with a Long ID and a String value 
val v1 = new Vertex(1L, "foo") 

// A vertex with a Long ID and a String value 
val v2 = new Vertex(1L, "bar") 

// Edges connecting Vertices with Ids 1 and 2 having weight 0.5 

val e1 = new Edge(1L, 2L, 0.5) 

val e2 = new Edge(2L, 1L, 0.5) 

以下是其相同的可视表示:

Graph edges

图创建

在 Flink Gelly 中,可以以多种方式创建图。以下是一些示例。

来自边和顶点数据集

以下代码片段表示我们如何使用边数据集和可选顶点创建图:

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

DataSet<Vertex<String, Long>> vertices = ... 

DataSet<Edge<String, Double>> edges = ... 

Graph<String, Long, Double> graph = Graph.fromDataSet(vertices, edges, env); 

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 

val vertices: DataSet[Vertex[String, Long]] = ... 

val edges: DataSet[Edge[String, Double]] = ... 

val graph = Graph.fromDataSet(vertices, edges, env) 

来自表示边的元组数据集

以下代码片段表示我们如何使用表示边的 Tuple2 数据集创建图。在这里,Gelly 会自动将 Tuple2 转换为具有源和目标顶点 ID 以及空值的边。

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

DataSet<Tuple2<String, String>> edges = ... 

Graph<String, NullValue, NullValue> graph = Graph.fromTuple2DataSet(edges, env); 

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 

val edges: DataSet[(String, String)] = ... 

val graph = Graph.fromTuple2DataSet(edges, env) 

以下代码片段表示我们如何使用表示边的 Tuple3 数据集创建图。这里,顶点使用 Tuple2 表示,而边使用 Tuple3 表示,包含有关源顶点、目标顶点和权重的信息。我们还可以从 CSV 文件中读取一组值:

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

DataSet<Tuple2<String, Long>> vertexTuples = env.readCsvFile("path/to/vertex/input.csv").types(String.class, Long.class); 

DataSet<Tuple3<String, String, Double>> edgeTuples = env.readCsvFile("path/to/edge/input.csv").types(String.class, String.class, Double.class); 

Graph<String, Long, Double> graph = Graph.fromTupleDataSet(vertexTuples, edgeTuples, env); 

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 

val vertexTuples = env.readCsvFileString, Long 

val edgeTuples = env.readCsvFileString, String, Double 

val graph = Graph.fromTupleDataSet(vertexTuples, edgeTuples, env) 

来自 CSV 文件

以下代码片段表示我们如何使用 CSV 文件读取器创建图。CSV 文件应以顶点和边的形式表示数据。

以下代码片段创建了一个图,该图来自 CSV 文件,格式为边的源、目标、权重,以及顶点的 ID、名称:

val env = ExecutionEnvironment.getExecutionEnvironment 

// create a Graph with String Vertex IDs, Long Vertex values and Double Edge values 
val graph = Graph.fromCsvReaderString, Long, Double 

我们还可以通过在创建图时定义map函数来使用顶点值初始化程序:

val simpleGraph = Graph.fromCsvReaderLong, Double, NullValue { 
            def map(id: Long): Double = { 
                id.toDouble 
            } 
        }, 
        env = env) 

来自集合列表

我们还可以从列表集合创建图。以下代码片段显示了我们如何从边和顶点列表创建图:

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

List<Vertex<Long, Long>> vertexList = new ArrayList... 

List<Edge<Long, String>> edgeList = new ArrayList... 

Graph<Long, Long, String> graph = Graph.fromCollection(vertexList, edgeList, env); 

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 

val vertexList = List(...) 

val edgeList = List(...) 

val graph = Graph.fromCollection(vertexList, edgeList, env) 

如果没有提供顶点输入,则可以考虑提供一个map初始化函数,如下所示:

val env = ExecutionEnvironment.getExecutionEnvironment 

// initialize the vertex value to be equal to the vertex ID 
val graph = Graph.fromCollection(edgeList, 
    new MapFunction[Long, Long] { 
       def map(id: Long): Long = id 
    }, env)

图属性

以下表格显示了用于检索图属性的一组可用方法:

属性 在 Java 中 在 Scala 中
getVertices数据集 DataSet<Vertex<K, VV>> getVertices() getVertices: DataSet[Vertex<K, VV>]
getEdges数据集 DataSet<Edge<K, EV>> getEdges() getEdges: DataSet[Edge<K, EV>]
getVertexIds DataSet<K> getVertexIds() getVertexIds: DataSet[K]
getEdgeIds DataSet<Tuple2<K, K>> getEdgeIds() getEdgeIds: DataSet[(K, K)]
获取顶点 ID 和所有顶点的inDegrees数据集 DataSet<Tuple2<K, LongValue>> inDegrees() inDegrees: DataSet[(K, LongValue)]
获取顶点 ID 和所有顶点的outDegrees数据集 DataSet<Tuple2<K, LongValue>> outDegrees() outDegrees: DataSet[(K, LongValue)]
获取顶点 ID 和所有顶点的 in、getDegree数据集 DataSet<Tuple2<K, LongValue>> getDegrees() getDegrees: DataSet[(K, LongValue)]
获取numberOfVertices long numberOfVertices() numberOfVertices: Long
获取numberOfEdges long numberOfEdges() numberOfEdges: Long
getTriplets提供了由源顶点、目标顶点和边组成的三元组 DataSet<Triplet<K, VV, EV>> getTriplets() getTriplets: DataSet[Triplet<K, VV, EV>]

图转换

Gelly 提供了各种转换操作,可帮助将图从一种形式转换为另一种形式。以下是我们可以使用 Gelly 进行的一些转换。

映射

Gelly 提供了保持顶点和边 ID 不变并根据函数中给定的值转换值的映射转换。此操作始终返回一个新图。以下代码片段显示了如何使用它。

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 
Graph<Long, Long, Long> graph = Graph.fromDataSet(vertices, edges, env); 

// increment each vertex value by 5 
Graph<Long, Long, Long> updatedGraph = graph.mapVertices( 
        new MapFunction<Vertex<Long, Long>, Long>() { 
          public Long map(Vertex<Long, Long> value) { 
            return value.getValue() + 5; 
          } 
        }); 

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 
val graph = Graph.fromDataSet(vertices, edges, env) 

// increment each vertex value by 5 
val updatedGraph = graph.mapVertices(v => v.getValue + 5) 

翻译

Translate 是一种特殊函数,允许翻译顶点 ID、顶点值、边 ID 等。翻译是使用用户提供的自定义映射函数执行的。以下代码片段显示了我们如何使用 translate 函数。

在 Java 中:

// translate each vertex and edge ID to a String 
Graph<String, Long, Long> updatedGraph = graph.translateGraphIds( 
        new MapFunction<Long, String>() { 
          public String map(Long id) { 
            return id.toString(); 
          } 
        }); 

// translate vertex IDs, edge IDs, vertex values, and edge values to LongValue 
Graph<LongValue, LongValue, LongValue> updatedGraph = graph 
                .translateGraphIds(new LongToLongValue()) 
                .translateVertexValues(new LongToLongValue()) 
                .translateEdgeValues(new LongToLongValue()) 

在 Scala 中:

// translate each vertex and edge ID to a String 
val updatedGraph = graph.translateGraphIds(id => id.toString) 

过滤

FilterFunction可用于根据某些条件过滤顶点和边。filterOnEdges将创建原始图的子图。在此操作中,顶点数据集保持不变。同样,filterOnVertices对顶点值应用过滤器。在这种情况下,找不到目标节点的边将被移除。以下代码片段显示了我们如何在 Gelly 中使用FilterFunction

在 Java 中:

Graph<Long, Long, Long> graph = ... 

graph.subgraph( 
    new FilterFunction<Vertex<Long, Long>>() { 
           public boolean filter(Vertex<Long, Long> vertex) { 
          // keep only vertices with positive values 
          return (vertex.getValue() > 2); 
         } 
       }, 
    new FilterFunction<Edge<Long, Long>>() { 
        public boolean filter(Edge<Long, Long> edge) { 
          // keep only edges with negative values 
          return (edge.getTarget() == 3); 
        } 
    }) 

在 Scala 中:

val graph: Graph[Long, Long, Long] = ... 
graph.subgraph((vertex => vertex.getValue > 2), (edge => edge.getTarget == 3)) 

以下是前述代码的图形表示:

Filter

同样,以下图表显示了filterOnEdges

Filter

连接

join操作有助于将顶点和边数据集与其他数据集进行连接。joinWithVertices方法与顶点 ID 和 Tuple2 的第一个字段进行连接。join方法返回一个新的图。同样,输入数据集可以与边进行连接。我们可以通过三种方式连接边:

  • joinWithEdges:在源和目标顶点 ID 的复合键上与 Tuple3 数据集进行连接

  • joinWithEdgeOnSource:与 Tuple2 数据集在源键和 Tuple2 数据集的第一个属性上进行连接

  • joinWithEdgeOnTarget:与目标键和 Tuple2 数据集的第一个属性进行连接

以下代码片段显示了如何在 Gelly 中使用连接:

在 Java 中:

Graph<Long, Double, Double> network = ... 

DataSet<Tuple2<Long, LongValue>> vertexOutDegrees = network.outDegrees(); 

// assign the transition probabilities as the edge weights 
Graph<Long, Double, Double> networkWithWeights = network.joinWithEdgesOnSource(vertexOutDegrees, 
        new VertexJoinFunction<Double, LongValue>() { 
          public Double vertexJoin(Double vertexValue, LongValue inputValue) { 
            return vertexValue / inputValue.getValue(); 
          } 
        }); 

在 Scala 中:

val network: Graph[Long, Double, Double] = ... 

val vertexOutDegrees: DataSet[(Long, LongValue)] = network.outDegrees 
// assign the transition probabilities as the edge weights 

val networkWithWeights = network.joinWithEdgesOnSource(vertexOutDegrees, (v1: Double, v2: LongValue) => v1 / v2.getValue) 

反向

reverse方法返回一个边方向被颠倒的图。

以下代码片段显示了如何使用相同的方法:

在 Java 中:

Graph<Long, Double, Double> network = ...; 
Graph<Long, Double, Double> networkReverse  = network.reverse(); 

在 Scala 中:

val network: Graph[Long, Double, Double] = ... 
val networkReversed: Graph[Long, Double, Double] = network.reverse  

无向的

undirected方法返回一个具有与原始边相反的额外边的新图。

以下代码片段显示了如何使用相同的方法:

在 Java 中:

Graph<Long, Double, Double> network = ...; 
Graph<Long, Double, Double> networkUD  = network.undirected(); 

在 Scala 中:

val network: Graph[Long, Double, Double] = ... 
val networkUD: Graph[Long, Double, Double] = network.undirected 

联合

union操作返回一个组合了两个图的顶点和边的图。它在顶点 ID 上进行连接。重复的顶点将被移除,而边将被保留。

以下是union操作的图形表示:

Union

相交

intersect方法执行给定图数据集的边的交集。如果两条边具有相同的源和目标顶点,则它们被视为相等。该方法还包含 distinct 参数;如果设置为true,它只返回不同的图。以下是一些代码片段,展示了intersect方法的用法。

在 Java 中:

ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

// create first graph from edges {(1, 2, 10) (1, 2, 11), (1, 2, 10)} 
List<Edge<Long, Long>> edges1 = ... 
Graph<Long, NullValue, Long> graph1 = Graph.fromCollection(edges1, env); 

// create second graph from edges {(1, 2, 10)} 
List<Edge<Long, Long>> edges2 = ... 
Graph<Long, NullValue, Long> graph2 = Graph.fromCollection(edges2, env); 

// Using distinct = true results in {(1,2,10)} 
Graph<Long, NullValue, Long> intersect1 = graph1.intersect(graph2, true); 

// Using distinct = false results in {(1,2,10),(1,2,10)} as there is one edge pair 
Graph<Long, NullValue, Long> intersect2 = graph1.intersect(graph2, false); 

在 Scala 中:

val env = ExecutionEnvironment.getExecutionEnvironment 

// create first graph from edges {(1, 2, 10) (1, 2, 11), (1, 2, 10)} 
val edges1: List[Edge[Long, Long]] = ... 
val graph1 = Graph.fromCollection(edges1, env) 

// create second graph from edges {(1, 2, 10)} 
val edges2: List[Edge[Long, Long]] = ... 
val graph2 = Graph.fromCollection(edges2, env) 

// Using distinct = true results in {(1,2,10)} 
val intersect1 = graph1.intersect(graph2, true) 

// Using distinct = false results in {(1,2,10),(1,2,10)} as there is one edge pair 
val intersect2 = graph1.intersect(graph2, false) 

图变异

Gelly 提供了向现有图添加/移除边和顶点的方法。让我们逐一了解这些变异。

变异 在 Java 中 在 Scala 中
添加顶点。Graph<K, VV, EV> addVertex(final Vertex<K, VV> vertex) addVertex(vertex: Vertex[K, VV])
添加顶点列表。Graph<K, VV, EV> addVertices(List<Vertex<K, VV>> verticesToAdd) addVertices(verticesToAdd: List[Vertex[K, VV]])
向图中添加边。如果边和顶点不存在,则添加新的边和顶点。Graph<K, VV, EV> addEdge(Vertex<K, VV> source, Vertex<K, VV> target, EV edgeValue) addEdge(source: Vertex[K, VV], target: Vertex[K, VV], edgeValue: EV)
添加边,如果顶点不存在,则该边被视为无效。Graph<K, VV, EV> addEdges(List<Edge<K, EV>> newEdges) addEdges(edges: List[Edge[K, EV]])
从给定的图中移除顶点,移除边和顶点。Graph<K, VV, EV> removeVertex(Vertex<K, VV> vertex) removeVertex(vertex: Vertex[K, VV])
从给定的图中移除多个顶点。Graph<K, VV, EV> removeVertices(List<Vertex<K, VV>> verticesToBeRemoved) removeVertices(verticesToBeRemoved: List[Vertex[K, VV]])
移除与给定边匹配的所有边。Graph<K, VV, EV> removeEdge(Edge<K, EV> edge) removeEdge(edge: Edge[K, EV])
移除与给定边列表匹配的边。Graph<K, VV, EV> removeEdges(List<Edge<K, EV>> edgesToBeRemoved) removeEdges(edgesToBeRemoved: List[Edge[K, EV]])

邻域方法

邻域方法有助于执行与其第一跳邻域相关的操作。诸如reduceOnEdges()reduceOnNeighbours()之类的方法可用于执行聚合操作。第一个用于计算顶点相邻边的聚合,而后者用于计算相邻顶点的聚合。邻居范围可以通过提供边方向来定义,我们有选项,如INOUTALL

考虑一个例子,我们需要获取OUT方向边的所有顶点的最大权重:

邻域方法

现在我们想要找出每个顶点的最大加权OUT边。Gelly 为我们提供了邻域方法,我们可以用它找到所需的结果。以下是相同的代码片段:

在 Java 中:

Graph<Long, Long, Double> graph = ... 

DataSet<Tuple2<Long, Double>> maxWeights = graph.reduceOnEdges(new SelectMaxWeight(), EdgeDirection.OUT); 

// user-defined function to select the max weight 
static final class SelectMaxWeight implements ReduceEdgesFunction<Double> { 

    @Override 
    public Double reduceEdges(Double firstEdgeValue, Double secondEdgeValue) { 
      return Math.max(firstEdgeValue, secondEdgeValue); 
    } 
} 

在 Scala 中:

val graph: Graph[Long, Long, Double] = ... 

val minWeights = graph.reduceOnEdges(new SelectMaxWeight, EdgeDirection.OUT) 

// user-defined function to select the max weight 
final class SelectMaxWeight extends ReduceEdgesFunction[Double] { 
  override def reduceEdges(firstEdgeValue: Double, secondEdgeValue: Double): Double = { 
    Math.max(firstEdgeValue, secondEdgeValue) 
  } 
 } 

Gelly 通过首先分离每个顶点并找出每个顶点的最大加权边来解决这个问题。以下是相同的图形表示:

邻域方法

同样,我们也可以编写一个函数来计算所有邻域中传入边的值的总和。

在 Java 中:

Graph<Long, Long, Double> graph = ... 

DataSet<Tuple2<Long, Long>> verticesWithSum = graph.reduceOnNeighbors(new SumValues(), EdgeDirection.IN); 

static final class SumValues implements ReduceNeighborsFunction<Long> { 

        @Override 
        public Long reduceNeighbors(Long firstNeighbor, Long secondNeighbor) { 
          return firstNeighbor + secondNeighbor; 
      } 
} 

在 Scala 中:

val graph: Graph[Long, Long, Double] = ... 

val verticesWithSum = graph.reduceOnNeighbors(new SumValues, EdgeDirection.IN) 

final class SumValues extends ReduceNeighborsFunction[Long] { 
     override def reduceNeighbors(firstNeighbor: Long, secondNeighbor: Long): Long = { 
      firstNeighbor + secondNeighbor 
    } 
} 

图验证

Gelly 为我们提供了一个实用程序,在将其发送进行处理之前验证输入图。在各种情况下,我们首先需要验证图是否符合某些条件,然后才能将其发送进行进一步处理。验证可能是检查图是否包含重复边或检查图结构是否为二部图。

注意

二部图或双图是一个图,其顶点可以分为两个不同的集合,以便每个集合中的每个顶点都与另一个集合中的顶点相连。二部图的一个简单例子是篮球运动员和他们所效力的球队的图。在这里,我们将有两个集合,分别是球员和球队,每个球员集合中的顶点都将与球队集合中的顶点相连。有关二部图的更多细节,请阅读这里en.wikipedia.org/wiki/Bipartite_graph

我们也可以定义自定义验证方法来获得所需的输出。Gelly 还提供了一个名为InvalidVertexValidator的内置验证器。这将检查边集是否包含验证顶点 ID。以下是一些展示其用法的代码片段。

在 Java 中:

Graph<Long, Long, Long> graph = Graph.fromCollection(vertices, edges, env); 

// Returns false for invalid vertex id.  
graph.validate(new InvalidVertexIdsValidator<Long, Long, Long>()); 

在 Scala 中:

val graph = Graph.fromCollection(vertices, edges, env) 

// Returns false for invalid vertex id.  
graph.validate(new InvalidVertexIdsValidator[Long, Long, Long]) 

迭代图处理

Gelly 增强了 Flink 的迭代处理能力,以支持大规模图处理。目前它支持以下模型的实现:

  • 顶点中心

  • 分散-聚集

  • 聚集-求和-应用

让我们首先在 Gelly 的背景下理解这些模型。

顶点中心迭代

正如名称所示,这些迭代是建立在顶点处于中心的思想上。在这里,每个顶点并行处理相同的用户定义函数。执行的每一步被称为超集。只要顶点知道其唯一 ID,它就可以向另一个顶点发送消息。这个消息将被用作下一个超集的输入。

要使用顶点中心迭代,用户需要提供一个ComputeFunction。我们还可以定义一个可选的MessageCombiner来减少通信成本。我们可以解决问题,比如单源最短路径,在这种情况下,我们需要找到从源顶点到所有其他顶点的最短路径。

注意

单源最短路径是我们试图最小化连接两个不同顶点的权重之和。一个非常简单的例子可能是城市和航班路线的图。在这种情况下,SSSP 算法将尝试找到连接两个城市的最短距离,考虑到可用的航班路线。有关 SSSP 的更多细节,请参阅en.wikipedia.org/wiki/Shortest_path_problem

以下代码片段展示了我们如何使用 Gelly 解决单源最短路径问题。

在 Java 中:

// maximum number of iterations 
int maxIterations = 5; 

// Run vertex-centric iteration 
Graph<Long, Double, Double> result = graph.runVertexCentricIteration( 
            new SSSPComputeFunction(), new SSSPCombiner(), maxIterations); 

// Extract the vertices as the result 
DataSet<Vertex<Long, Double>> singleSourceShortestPaths = result.getVertices(); 

//User defined compute function to minimize the distance between //the vertices 

public static final class SSSPComputeFunction extends ComputeFunction<Long, Double, Double, Double> { 

public void compute(Vertex<Long, Double> vertex, MessageIterator<Double> messages) { 

    double minDistance = (vertex.getId().equals(srcId)) ? 0d : Double.POSITIVE_INFINITY; 

    for (Double msg : messages) { 
        minDistance = Math.min(minDistance, msg); 
    } 

    if (minDistance < vertex.getValue()) { 
        setNewVertexValue(minDistance); 
        for (Edge<Long, Double> e: getEdges()) { 
            sendMessageTo(e.getTarget(), minDistance + e.getValue()); 
        } 
    } 
} 

// message combiner helps in optimizing the communications 
public static final class SSSPCombiner extends MessageCombiner<Long, Double> { 

    public void combineMessages(MessageIterator<Double> messages) { 

        double minMessage = Double.POSITIVE_INFINITY; 
        for (Double msg: messages) { 
           minMessage = Math.min(minMessage, msg); 
        } 
        sendCombinedMessage(minMessage); 
    } 
} 

在 Scala 中:

// maximum number of iterations 
val maxIterations = 5 

// Run the vertex-centric iteration 
val result = graph.runVertexCentricIteration(new SSSPComputeFunction, new SSSPCombiner, maxIterations) 

// Extract the vertices as the result 
val singleSourceShortestPaths = result.getVertices 

//User defined compute function to minimize the distance between //the vertices 

final class SSSPComputeFunction extends ComputeFunction[Long, Double, Double, Double] { 

    override def compute(vertex: Vertex[Long, Double], messages:   
    MessageIterator[Double]) = { 

    var minDistance = if (vertex.getId.equals(srcId)) 0 else  
    Double.MaxValue 

    while (messages.hasNext) { 
        val msg = messages.next 
        if (msg < minDistance) { 
            minDistance = msg 
        } 
    } 

    if (vertex.getValue > minDistance) { 
        setNewVertexValue(minDistance) 
        for (edge: Edge[Long, Double] <- getEdges) { 
            sendMessageTo(edge.getTarget, vertex.getValue +  
            edge.getValue) 
        } 
    } 
} 

// message combiner helps in optimizing the communications 
final class SSSPCombiner extends MessageCombiner[Long, Double] { 

    override def combineMessages(messages: MessageIterator[Double]) { 

        var minDistance = Double.MaxValue 

        while (messages.hasNext) { 
          val msg = inMessages.next 
          if (msg < minDistance) { 
            minDistance = msg 
          } 
        } 
        sendCombinedMessage(minMessage) 
    } 
} 

我们可以在顶点中心迭代中使用以下配置。

参数 描述
名称:setName() 设置顶点中心迭代的名称。可以在日志中看到。
并行度:setParallelism() 设置并行执行的并行度。
广播变量:addBroadcastSet() 将广播变量添加到计算函数中。
聚合器:registerAggregator() 注册自定义定义的聚合器函数,供计算函数使用。
未管理内存中的解集:setSolutionSetUnmanagedMemory() 定义解集是否保存在受控内存中。

Scatter-Gather 迭代

Scatter-Gather 迭代也适用于超集迭代,并且在其中心也有一个顶点,我们还定义了一个并行执行的函数。在这里,每个顶点有两件重要的事情要做:

  • Scatter:Scatter 生成需要发送到其他顶点的消息

  • Gather:Gather 从收到的消息中更新顶点值

Gelly 提供了 scatter 和 gather 的方法。用户只需实现这两个函数即可利用这些迭代。ScatterFunction为其余顶点生成消息,而GatherFunction根据收到的消息计算顶点的更新值。

以下代码片段显示了如何使用 Gelly-Scatter-Gather 迭代解决单源最短路径问题:

在 Java 中:

// maximum number of iterations 
int maxIterations = 5; 

// Run the scatter-gather iteration 
Graph<Long, Double, Double> result = graph.runScatterGatherIteration( 
      new MinDistanceMessenger(), new VertexDistanceUpdater(), maxIterations); 

// Extract the vertices as the result 
DataSet<Vertex<Long, Double>> singleSourceShortestPaths = result.getVertices(); 

// Scatter Gather function definition  

// Through scatter function, we send distances from each vertex 
public static final class MinDistanceMessenger extends ScatterFunction<Long, Double, Double, Double> { 

  public void sendMessages(Vertex<Long, Double> vertex) { 
    for (Edge<Long, Double> edge : getEdges()) { 
      sendMessageTo(edge.getTarget(), vertex.getValue() + edge.getValue()); 
    } 
  } 
} 

// In gather function, we gather messages sent in previous //superstep to find out the minimum distance.  
public static final class VertexDistanceUpdater extends GatherFunction<Long, Double, Double> { 

  public void updateVertex(Vertex<Long, Double> vertex, MessageIterator<Double> inMessages) { 
    Double minDistance = Double.MAX_VALUE; 

    for (double msg : inMessages) { 
      if (msg < minDistance) { 
        minDistance = msg; 
      } 
    } 

    if (vertex.getValue() > minDistance) { 
      setNewVertexValue(minDistance); 
    } 
  } 
} 

在 Scala 中:

// maximum number of iterations 
val maxIterations = 5 

// Run the scatter-gather iteration 
val result = graph.runScatterGatherIteration(new MinDistanceMessenger, new VertexDistanceUpdater, maxIterations) 

// Extract the vertices as the result 
val singleSourceShortestPaths = result.getVertices 

// Scatter Gather definitions 

// Through scatter function, we send distances from each vertex 
final class MinDistanceMessenger extends ScatterFunction[Long, Double, Double, Double] { 

  override def sendMessages(vertex: Vertex[Long, Double]) = { 
    for (edge: Edge[Long, Double] <- getEdges) { 
      sendMessageTo(edge.getTarget, vertex.getValue + edge.getValue) 
    } 
  } 
} 

// In gather function, we gather messages sent in previous //superstep to find out the minimum distance.  
final class VertexDistanceUpdater extends GatherFunction[Long, Double, Double] { 

  override def updateVertex(vertex: Vertex[Long, Double], inMessages: MessageIterator[Double]) = { 
    var minDistance = Double.MaxValue 

    while (inMessages.hasNext) { 
      val msg = inMessages.next 
      if (msg < minDistance) { 
      minDistance = msg 
      } 
    } 

    if (vertex.getValue > minDistance) { 
      setNewVertexValue(minDistance) 
    } 
  } 
} 

我们可以使用以下参数配置 Scatter-Gather 迭代:

参数 描述
名称:setName() 设置 scatter-gather 迭代的名称。可以在日志中看到。
并行度:setParallelism() 设置并行执行的并行度。
广播变量:addBroadcastSet() 将广播变量添加到计算函数中。
聚合器:registerAggregator() 注册自定义定义的聚合器函数,供计算函数使用。
未管理内存中的解集:setSolutionSetUnmanagedMemory() 定义解集是否保存在受控内存中。
顶点数量:setOptNumVertices() 访问迭代中顶点的总数。
度数:setOptDegrees() 设置在迭代中要达到的入/出度数。
消息方向:setDirection() 默认情况下,我们只考虑出度进行处理,但我们可以通过设置此属性来更改。选项有inoutall

Gather-Sum-Apply 迭代

与前两个模型一样,Gather-Sum-ApplyGSA)迭代也在迭代步骤中同步。每个超集包括以下步骤:

  1. Gather:在边和每个邻居上执行用户定义的函数,生成部分值。

  2. Sum:在早期步骤中计算的部分值将在此步骤中聚合。

  3. Apply:通过将上一步的聚合值和当前值应用于顶点值来更新每个顶点值。

我们将尝试使用 GSA 迭代来解决单源最短路径问题。要使用此功能,我们需要为 gather、sum 和 apply 定义自定义函数。

在 Java 中:

// maximum number of iterations 
int maxIterations = 5; 

// Run the GSA iteration 
Graph<Long, Double, Double> result = graph.runGatherSumApplyIteration( 
        new CalculateDistances(), new ChooseMinDistance(), new UpdateDistance(), maxIterations); 

// Extract the vertices as the result 
DataSet<Vertex<Long, Double>> singleSourceShortestPaths = result.getVertices(); 

// Functions for GSA 

// Gather 
private static final class CalculateDistances extends GatherFunction<Double, Double, Double> { 

  public Double gather(Neighbor<Double, Double> neighbor) { 
    return neighbor.getNeighborValue() + neighbor.getEdgeValue(); 
  } 
} 

// Sum 
private static final class ChooseMinDistance extends SumFunction<Double, Double, Double> { 

  public Double sum(Double newValue, Double currentValue) { 
    return Math.min(newValue, currentValue); 
  } 
} 

// Apply 
private static final class UpdateDistance extends ApplyFunction<Long, Double, Double> { 

  public void apply(Double newDistance, Double oldDistance) { 
    if (newDistance < oldDistance) { 
      setResult(newDistance); 
    } 
  } 
} 

在 Scala 中:

// maximum number of iterations 
val maxIterations = 10 

// Run the GSA iteration 
val result = graph.runGatherSumApplyIteration(new CalculateDistances, new ChooseMinDistance, new UpdateDistance, maxIterations) 

// Extract the vertices as the result 
val singleSourceShortestPaths = result.getVertices 

// Custom function for GSA 

// Gather 
final class CalculateDistances extends GatherFunction[Double, Double, Double] { 

  override def gather(neighbor: Neighbor[Double, Double]): Double = { 
    neighbor.getNeighborValue + neighbor.getEdgeValue 
  } 
} 

// Sum 
final class ChooseMinDistance extends SumFunction[Double, Double, Double] { 

  override def sum(newValue: Double, currentValue: Double): Double = { 
    Math.min(newValue, currentValue) 
  } 
} 

// Apply 
final class UpdateDistance extends ApplyFunction[Long, Double, Double] { 

  override def apply(newDistance: Double, oldDistance: Double) = { 
    if (newDistance < oldDistance) { 
      setResult(newDistance) 
    } 
  } 
} 

我们可以使用以下参数配置 GSA 迭代:

参数 描述
名称:setName() 设置 GSA 迭代的名称。可以在日志中看到。
并行度:setParallelism() 设置并行执行的并行度。
广播变量:addBroadcastSet() 将广播变量添加到计算函数中。
聚合器:registerAggregator() 注册自定义定义的聚合器函数,供计算函数使用。
未管理内存中的解集:setSolutionSetUnmanagedMemory() 定义解集是否保存在受控内存中。
顶点数量:setOptNumVertices() 访问迭代中顶点的总数。
邻居方向:setDirection() 默认情况下,我们只考虑处理的OUT度,但我们可以通过设置此属性来更改。选项有INOUTALL

用例 - 机场旅行优化

让我们考虑一个使用案例,其中我们有关于机场和它们之间距离的数据。为了从特定机场前往某个目的地,我们必须找到两者之间的最短路径。我们的机场数据如下表所示:

Id 机场名称
s01 A
s02 B
s03 C
s04 D
s05 E

机场之间的距离信息如下表所示:

From To Distance
s01 s02 10
s01 s02 12
s01 s03 22
s01 s04 21
s04 s11 22
s05 s15 21
s06 s17 21
s08 s09 11
s08 s09 12

现在让我们使用 Gelly 来找到单源最短路径。

在这里,我们可以在前一节学到的三种算法中选择其中一种。在这个例子中,我们将使用顶点中心迭代方法。

为了解决单源最短路径问题,我们必须首先从 CSV 文件中加载数据,如下面的代码所示:

// set up the batch execution environment 
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

// Create graph by reading from CSV files 
DataSet<Tuple2<String, Double>> airportVertices = env 
            .readCsvFile("nodes.csv").types(String.class, Double.class); 

DataSet<Tuple3<String, String, Double>> airportEdges = env 
            .readCsvFile("edges.csv") 
            .types(String.class, String.class, Double.class); 

Graph<String, Double, Double> graph = Graph.fromTupleDataSet(airportVertices, airportEdges, env); 

接下来,我们在创建的图上运行前一节中讨论的顶点中心迭代:

// define the maximum number of iterations
int maxIterations = 10;

// Execute the vertex-centric iteration
Graph<String, Double, Double> result = graph.runVertexCentricIteration(new SSSPComputeFunction(), new SSSPCombiner(), maxIterations);

// Extract the vertices as the result
DataSet<Vertex<String, Double>> singleSourceShortestPaths = result.getVertices();
singleSourceShortestPaths.print();

计算函数和组合器的实现与我们在前一节中所看到的类似。当我们运行这段代码时,我们将得到从给定源顶点到 SSSP 的答案。

此用例的完整代码和样本数据可在 github.com/deshpandetanmay/mastering-flink/tree/master/chapter07/flink-gelly 上找到

总的来说,所有三种迭代方式看起来都很相似,但它们有微小的差异。根据用例,人们需要真正思考使用哪种算法。这里有一些关于这个想法的好文章 ci.apache.org/projects/flink/flink-docs-release-1.1/apis/batch/libs/gelly.html#iteration-abstractions-comparison

总结

在本章中,我们探讨了 Flink Gelly 库提供的图处理 API 的各个方面。我们学习了如何定义图,加载数据并对其进行处理。我们还研究了可以对图进行的各种转换。最后,我们学习了 Gelly 提供的迭代图处理选项的详细信息。

在下一章中,我们将看到如何在 Hadoop 和 YARN 上执行 Flink 应用程序。

第八章:使用 Flink 和 Hadoop 进行分布式数据处理

在过去的几年中,Apache Hadoop 已成为数据处理和分析基础设施的核心和必要部分。通过 Hadoop 1.X,社区学习了使用 MapReduce 框架进行分布式数据处理,而 Hadoop 的下一个版本,2.X 则教会了我们使用 YARN 框架进行资源的高效利用和调度。YARN 框架是 Hadoop 数据处理的核心部分,它处理诸如作业执行、分发、资源分配、调度等复杂任务。它允许多租户、可伸缩性和高可用性。

YARN 最好的部分在于它不仅仅是一个框架,更像是一个完整的操作系统,开发人员可以自由开发和执行他们选择的应用程序。它通过让开发人员只专注于应用程序开发,忘记并行数据和执行分发的痛苦来提供抽象。YARN 位于 Hadoop 分布式文件系统之上,还可以从 AWS S3 等文件系统中读取数据。

YARN 应用程序框架建得非常好,可以托管任何分布式处理引擎。最近,新的分布式数据处理引擎如 Spark、Flink 等出现了显著增长。由于它们是为在 YARN 集群上执行而构建的,因此人们可以很容易地在同一个 YARN 集群上并行尝试新的东西。这意味着我们可以在同一个集群上使用 YARN 运行 Spark 和 Flink 作业。在本章中,我们将看到如何利用现有的 Hadoop/YARN 集群并行执行我们的 Flink 作业。

所以让我们开始吧。

Hadoop 的快速概述

你们大多数人可能已经了解 Hadoop 及其功能,但对于那些对分布式计算世界还不熟悉的人,让我试着简要介绍一下 Hadoop。

Hadoop 是一个分布式的开源数据处理框架。它由两个重要部分组成:一个数据存储单元,Hadoop 分布式文件系统(HDFS)和资源管理单元,另一个资源协商器(YARN)。以下图表显示了 Hadoop 生态系统的高级概述:

Hadoop 的快速概述

HDFS

HDFS,顾名思义,是一个用于数据存储的高可用性分布式文件系统。如今,这是大多数公司的核心框架之一。HDFS 由主从架构组成,具有 NameNode、辅助 NameNode 和 DataNode 等守护程序。

在 HDFS 中,NameNode 存储有关要存储的文件的元数据,而 DataNode 存储组成文件的实际块。数据块默认情况下是三倍复制的,以实现高可用性。辅助 NameNode 用于备份存储在 NameNode 上的文件系统元数据。

注意

这是一个链接,您可以在hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html上阅读有关 HDFS 的更多信息。

YARN

在 YARN 之前,MapReduce 是运行在 HDFS 之上的数据处理框架。但人们开始意识到它在处理作业跟踪器数量方面的限制。这催生了 YARN。YARN 背后的基本思想是分离资源管理和调度任务。YARN 具有全局资源管理器和每个应用程序的应用程序主管。资源管理器在主节点上工作,而它有一个每个工作节点代理——节点管理器,负责管理容器,监视它们的使用情况(CPU、磁盘、内存)并向资源管理器报告。

资源管理器有两个重要组件--调度程序应用程序管理器。调度程序负责在队列中调度应用程序,而应用程序管理器负责接受作业提交,协商应用程序特定应用程序主节点的第一个容器。它还负责在应用程序主节点发生故障时重新启动应用程序主节点

由于像 YARN 这样的操作系统提供了可以扩展构建应用程序的 API。SparkFlink就是很好的例子。

注意

您可以在hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-site/YARN.html阅读更多关于 YARN 的信息。

现在让我们看看如何在 YARN 上使用 Flink。

Flink 在 YARN 上

Flink 已经内置支持在 YARN 上准备执行。使用 Flink API 构建的任何应用程序都可以在 YARN 上执行,而无需太多努力。如果用户已经有一个 YARN 集群,则无需设置或安装任何内容。Flink 希望满足以下要求:

  • Hadoop 版本应该是 2.2 或更高

  • HDFS 应该已经启动

配置

为了在 YARN 上运行 Flink,需要进行以下配置。首先,我们需要下载与 Hadoop 兼容的 Flink 发行版。

注意

二进制文件可在flink.apache.org/downloads.html下载。您必须从以下选项中进行选择。

配置

假设我们正在运行 Hadoop 2.7 和 Scala 2.11。我们将下载特定的二进制文件并将其存储在安装和运行 Hadoop 的节点上。

下载后,我们需要按照这里所示的方式提取tar文件:

$tar -xzf flink-1.1.4-bin-hadoop27-scala_2.11.tgz
$cd flink-1.1.4

一旦二进制文件被提取,我们就可以启动 Flink 会话。Flink 会话是一个会话,它在各自的节点上启动所有所需的 Flink 服务(作业管理器和任务管理器),以便我们可以开始执行 Flink 作业。要启动 Flink 会话,我们有以下可执行文件和给定选项:

# bin/yarn-session.sh
Usage:
 Required
 -n,--container <arg>            Number of YARN container to     
                                         allocate (=Number of Task  
                                         Managers)
 Optional
 -D <arg>                        Dynamic properties
 -d,--detached                   Start detached
 -id,--applicationId <arg>       Attach to running YARN session
 -j,--jar <arg>                  Path to Flink jar file
 -jm,--jobManagerMemory <arg>    Memory for JobManager 
                                         Container [in MB]
 -n,--container <arg>            Number of YARN container to 
                                         allocate (=Number of Task 
                                         Managers)
 -nm,--name <arg>                Set a custom name for the 
                                         application on YARN
 -q,--query                      Display available YARN 
                                         resources (memory, cores)
 -qu,--queue <arg>               Specify YARN queue.
 -s,--slots <arg>                Number of slots per 
                                         TaskManager
 -st,--streaming                 Start Flink in streaming mode
 -t,--ship <arg>                 Ship files in the specified 
                                         directory (t for transfer)
 -tm,--taskManagerMemory <arg>   Memory per TaskManager  
                                         Container [in MB]
 -z,--zookeeperNamespace <arg>   Namespace to create the 
                                         Zookeeper sub-paths for high 
                                         availability mode

我们必须确保YARN_CONF_DIRHADOOP_CONF_DIR环境变量已设置,以便 Flink 可以找到所需的配置。现在让我们通过提供信息来启动 Flink 会话。

以下是我们如何通过提供有关任务管理器数量、每个任务管理器的内存和要使用的插槽的详细信息来启动 Flink 会话:

# bin/yarn-session.sh -n 2 -tm 1024 -s 10
2016-11-14 10:46:00,126 WARN    
    org.apache.hadoop.util.NativeCodeLoader                                   
    - Unable to load native-hadoop library for your platform... using 
    builtin-java classes where applicable
2016-11-14 10:46:00,184 INFO  
    org.apache.flink.yarn.YarnClusterDescriptor                            
    - The configuration directory ('/usr/local/flink/flink-1.1.3/conf') 
    contains both LOG4J and Logback configuration files. Please delete 
    or rename one of them.
2016-11-14 10:46:01,263 INFO  org.apache.flink.yarn.Utils                                   
    - Copying from file:/usr/local/flink/flink-
    1.1.3/conf/log4j.properties to 
    hdfs://hdpcluster/user/root/.flink/application_1478079131011_0107/
    log4j.properties
2016-11-14 10:46:01,463 INFO  org.apache.flink.yarn.Utils                                      
    - Copying from file:/usr/local/flink/flink-1.1.3/lib to   
    hdfs://hdp/user/root/.flink/application_1478079131011_0107/lib
2016-11-14 10:46:02,337 INFO  org.apache.flink.yarn.Utils                                     
    - Copying from file:/usr/local/flink/flink-1.1.3/conf/logback.xml    
    to hdfs://hdpcluster/user/root/.flink/
    application_1478079131011_0107/logback.xml
2016-11-14 10:46:02,350 INFO  org.apache.flink.yarn.Utils                                      
    - Copying from file:/usr/local/flink/flink-1.1.3/lib/flink-  
    dist_2.11-1.1.3.jar to hdfs://hdpcluster/user/root/.flink/
    application_1478079131011_0107/flink-dist_2.11-1.1.3.jar
2016-11-14 10:46:03,157 INFO  org.apache.flink.yarn.Utils                                      
    - Copying from /usr/local/flink/flink-1.1.3/conf/flink-conf.yaml to    
    hdfs://hdpcluster/user/root/.flink/application_1478079131011_0107/
    flink-conf.yaml
org.apache.flink.yarn.YarnClusterDescriptor                           
    - Deploying cluster, current state ACCEPTED
2016-11-14 10:46:11,976 INFO  
    org.apache.flink.yarn.YarnClusterDescriptor                               
    - YARN application has been deployed successfully.
Flink JobManager is now running on 10.22.3.44:43810
JobManager Web Interface: 
    http://myhost.com:8088/proxy/application_1478079131011_0107/
2016-11-14 10:46:12,387 INFO  Remoting                                                      
    - Starting remoting
2016-11-14 10:46:12,483 INFO  Remoting                                                      
    - Remoting started; listening on addresses :
    [akka.tcp://flink@10.22.3.44:58538]
2016-11-14 10:46:12,627 INFO     
    org.apache.flink.yarn.YarnClusterClient                                
    - Start application client.
2016-11-14 10:46:12,634 INFO  
    org.apache.flink.yarn.ApplicationClient                                
    - Notification about new leader address 
    akka.tcp://flink@10.22.3.44:43810/user/jobmanager with session ID 
    null.
2016-11-14 10:46:12,637 INFO    
    org.apache.flink.yarn.ApplicationClient                                
    - Received address of new leader   
    akka.tcp://flink@10.22.3.44:43810/user/jobmanager 
    with session ID null.
2016-11-14 10:46:12,638 INFO  
    org.apache.flink.yarn.ApplicationClient                                
    - Disconnect from JobManager null.
2016-11-14 10:46:12,640 INFO  
    org.apache.flink.yarn.ApplicationClient                                
    - Trying to register at JobManager 
    akka.tcp://flink@10.22.3.44:43810/user/jobmanager.
2016-11-14 10:46:12,649 INFO  
    org.apache.flink.yarn.ApplicationClient                                
    - Successfully registered at the ResourceManager using JobManager 
    Actor[akka.tcp://flink@10.22.3.44:43810/user/jobmanager#-862361447]

如果配置目录未正确设置,您将收到错误消息。在这种情况下,首先可以设置配置目录,然后启动 Flink YARN 会话。

以下命令设置了配置目录:

export HADOOP_CONF_DIR=/etc/hadoop/conf
export YARN_CONF_DIR=/etc/hadoop/conf

注意

我们还可以通过访问以下 URL 来检查 Flink Web UI:http://host:8088/proxy/application_<id>/#/overview.

这是同样的屏幕截图:

启动 Flink YARN 会话

同样,我们也可以在http://myhost:8088/cluster/app/application_1478079131011_0107上检查 YARN 应用程序 UI。

启动 Flink YARN 会话

现在我们已经连接到 YARN 的 Flink 会话,我们已经准备好将 Flink 作业提交到 YARN。

我们可以使用以下命令和选项提交 Flink 作业:

#./bin/flink
./flink <ACTION> [OPTIONS] [ARGUMENTS]

我们可以使用运行操作来执行 Flink 作业。在运行中,我们有以下选项:

选项 描述
-c, --class <classname> 具有程序入口点(main()方法或getPlan()方法)的类。只有在 JAR 文件没有在其清单中指定类时才需要。
-C,--classpath 在集群中的所有节点的每个用户代码类加载器中添加 URL。路径必须指定协议(例如file://)并且在所有节点上都可以访问(例如通过 NFS 共享)。您可以多次使用此选项来指定多个 URL。协议必须受到{@link java.net.URLClassLoader}支持。如果您希望在 Flink YARN 会话中使用某些第三方库,可以使用此选项。
-d,--detached 如果存在,以分离模式运行作业。分离模式在您不想一直运行 Flink YARN 会话时很有用。在这种情况下,Flink 客户端只会提交作业并分离自己。我们无法使用 Flink 命令停止分离的 Flink YARN 会话。为此,我们必须使用 YARN 命令杀死应用程序 yarn application -kill
-m,--jobmanager host:port 要连接的作业管理器(主节点)的地址。使用此标志连接到与配置中指定的不同作业管理器。
-p,--parallelism 运行程序的并行度。可选标志,用于覆盖配置中指定的默认值。
-q,--sysoutLogging 如果存在,抑制标准OUT的日志输出。
-s,--fromSavepoint 重置作业的保存点路径,例如 file:///flink/savepoint-1537。保存点是 Flink 程序的外部存储状态。它们是存储在某个位置的快照。如果 Flink 程序失败,我们可以从其上次存储的保存点恢复它。有关保存点的更多详细信息 ci.apache.org/projects/flink/flink-docs-release-1.2/setup/savepoints.html
-z,--zookeeperNamespace 用于创建高可用模式的 Zookeeper 子路径的命名空间

yarn-cluster模式提供以下选项:

选项 描述
-yD 动态属性
yd,--yarndetached 启动分离
-yid,--yarnapplicationId 连接到正在运行的 YARN 会话
-yj,--yarnjar Flink jar 文件的路径
-yjm,--yarnjobManagerMemory 作业管理器容器的内存(以 MB 为单位)
-yn,--yarncontainer 分配的 YARN 容器数(=任务管理器数)
-ynm,--yarnname 为 YARN 上的应用设置自定义名称
-yq,--yarnquery 显示可用的 YARN 资源(内存,核心)
-yqu,--yarnqueue 指定 YARN 队列
-ys,--yarnslots 每个任务管理器的插槽数
-yst,--yarnstreaming 以流模式启动 Flink
-yt,--yarnship 在指定目录中传输文件(t 表示传输)
-ytm,--yarntaskManagerMemory 每个 TaskManager 容器的内存(以 MB 为单位)
-yz,--yarnzookeeperNamespace 用于创建高可用模式的 Zookeeper 子路径的命名空间

现在让我们尝试在 YARN 上运行一个示例单词计数示例。以下是如何执行的步骤。

首先,让我们将输入文件存储在 HDFS 上,作为单词计数程序的输入。在这里,我们将在 Apache 许可证文本上运行单词计数。以下是我们下载并将其存储在 HDFS 上的方式:

wget -O LICENSE-2.0.txt http://www.apache.org/licenses/LICENSE-
    2.0.txt
hadoop fs -mkdir in
hadoop fs -put LICENSE-2.0.txt in

现在我们将提交示例单词计数作业:

./bin/flink run ./examples/batch/WordCount.jar 
    hdfs://myhost/user/root/in  hdfs://myhost/user/root/out

这将调用在 YARN 集群上执行的 Flink 作业。您应该在控制台上看到:

 **# ./bin/flink run ./examples/batch/WordCount.jar** 
2016-11-14 11:26:32,603 INFO  
    org.apache.flink.yarn.cli.FlinkYarnSessionCli               
    - YARN properties set default parallelism to 20
2016-11-14 11:26:32,603 INFO   
    org.apache.flink.yarn.cli.FlinkYarnSessionCli                 
    - YARN properties set default parallelism to 20
YARN properties set default parallelism to 20
2016-11-14 11:26:32,603 INFO    
    org.apache.flink.yarn.cli.FlinkYarnSessionCli               
    - Found YARN properties file /tmp/.yarn-properties-root
2016-11-14 11:26:32,603 INFO  
    org.apache.flink.yarn.cli.FlinkYarnSessionCli              
    - Found YARN properties file /tmp/.yarn-properties-root
Found YARN properties file /tmp/.yarn-properties-root
2016-11-14 11:26:32,603 INFO  
    org.apache.flink.yarn.cli.FlinkYarnSessionCli             
    - Using Yarn application id from YARN properties  
    application_1478079131011_0107
2016-11-14 11:26:32,603 INFO  
    org.apache.flink.yarn.cli.FlinkYarnSessionCli                        
    - Using Yarn application id from YARN properties  
    application_1478079131011_0107
Using Yarn application id from YARN properties   
    application_1478079131011_0107
2016-11-14 11:26:32,604 INFO  
    org.apache.flink.yarn.cli.FlinkYarnSessionCli               
    - YARN properties set default parallelism to 20
2016-11-14 11:26:32,604 INFO  
    org.apache.flink.yarn.cli.FlinkYarnSessionCli                
    - YARN properties set default parallelism to 20
YARN properties set default parallelism to 20
2016-11-14 11:26:32,823 INFO  
    org.apache.hadoop.yarn.client.api.impl.TimelineClientImpl     
    - Timeline service address: http://hdpdev002.pune-
    in0145.slb.com:8188/ws/v1/timeline/
2016-11-14 11:26:33,089 INFO  
    org.apache.flink.yarn.YarnClusterDescriptor               
    - Found application JobManager host name myhost.com' and port  
    '43810' from supplied application id 
    'application_1478079131011_0107'
Cluster configuration: Yarn cluster with application id 
    application_1478079131011_0107
Using address 163.183.206.249:43810 to connect to JobManager.
Starting execution of program
2016-11-14 11:26:33,711 INFO  
    org.apache.flink.yarn.YarnClusterClient                  
    - TaskManager status (2/1)
TaskManager status (2/1)
2016-11-14 11:26:33,712 INFO  
    org.apache.flink.yarn.YarnClusterClient                
    - All TaskManagers are connected
All TaskManagers are connected
2016-11-14 11:26:33,712 INFO  
    org.apache.flink.yarn.YarnClusterClient                       
    - Submitting job with JobID: b57d682dd09f570ea336b0d56da16c73\. 
    Waiting for job completion.
Submitting job with JobID: b57d682dd09f570ea336b0d56da16c73\. 
    Waiting for job completion.
Connected to JobManager at 
    Actor[akka.tcp://flink@163.183.206.249:43810/user/
    jobmanager#-862361447]
11/14/2016 11:26:33     Job execution switched to status RUNNING.
11/14/2016 11:26:33     CHAIN DataSource (at   
    getDefaultTextLineDataSet(WordCountData.java:70) 
    (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap 
    (FlatMap at main(WordCount.java:80)) -> Combine(SUM(1), at 
    main(WordCount.java:83)(1/1) switched to RUNNING
11/14/2016 11:26:34     DataSink (collect())(20/20) switched to 
    FINISHED
...
11/14/2016 11:26:34     Job execution switched to status FINISHED.
(after,1)
(coil,1)
(country,1)
(great,1)
(long,1)
(merit,1)
(oppressor,1)
(pangs,1)
(scorns,1)
(what,1)
(a,5)
(death,2)
(die,2)
(rather,1)
(be,4)
(bourn,1)
(d,4)
(say,1)
(takes,1)
(thy,1)
(himself,1)
(sins,1)
(there,2)
(whips,1)
(would,2)
(wrong,1)
...
 **Program execution finished** 
 **Job with JobID b57d682dd09f570ea336b0d56da16c73 has finished.** 
 **Job Runtime: 575 ms** 
Accumulator Results:
- 4950e35c195be901e0ad6a8ed25790de (java.util.ArrayList) [170 
      elements]
2016-11-14 11:26:34,378 INFO    
      org.apache.flink.yarn.YarnClusterClient             
      - Disconnecting YarnClusterClient from ApplicationMaster

以下是来自 Flink 应用程序主 UI 的作业执行的屏幕截图。这是 Flink 执行计划的屏幕截图:

提交作业到 Flink

接下来我们可以看到执行此作业的步骤的屏幕截图:

提交作业到 Flink

最后,我们有 Flink 作业执行时间轴的截图。时间轴显示了所有可以并行执行的步骤以及需要按顺序执行的步骤:

提交作业到 Flink

处理完成后,您可以以两种方式停止 Flink YARN 会话。首先,您可以在启动 YARN 会话的控制台上简单地执行Cltr+C。这将发送终止信号并停止 YARN 会话。

第二种方法是执行以下命令来停止会话:

./bin/yarn-session.sh -id application_1478079131011_0107 stop

我们可以立即看到 Flink YARN 应用程序被终止:

2016-11-14 11:56:59,455 INFO  
    org.apache.flink.yarn.YarnClusterClient  
    Sending shutdown request to the Application Master
2016-11-14 11:56:59,456 INFO    
    org.apache.flink.yarn.ApplicationClient  
    Sending StopCluster request to JobManager.
2016-11-14 11:56:59,464 INFO  
    org.apache.flink.yarn.YarnClusterClient  
    - Deleted Yarn properties file at /tmp/.yarn-properties-root
2016-11-14 11:56:59,464 WARN  
    org.apache.flink.yarn.YarnClusterClient  
    Session file directory not set. Not deleting session files
2016-11-14 11:56:59,565 INFO  
    org.apache.flink.yarn.YarnClusterClient  
    - Application application_1478079131011_0107 finished with state   
    FINISHED and final state SUCCEEDED at 1479104819469
 **2016-11-14 11:56:59,565 INFO  
    org.apache.flink.yarn.YarnClusterClient  
    - YARN Client is shutting down** 

我们还可以在 YARN 上运行单个 Flink 作业,而不会阻塞 YARN 会话的资源。如果您只希望在 YARN 上运行单个 Flink 作业,这是一个很好的选择。在之前的情况下,当我们在 YARN 上启动 Flink 会话时,它会阻塞资源和核心,直到我们停止会话,而在这种情况下,资源会在作业执行时被阻塞,并且一旦作业完成,它们就会被释放。以下命令显示了如何在 YARN 上执行单个 Flink 作业而不需要会话:

./bin/flink run -m yarn-cluster -yn 2  
    ./examples/batch/WordCount.jar

我们可以看到与之前情况下相似的结果。我们还可以使用 YARN 应用程序 UI 跟踪其进度和调试。以下是同一样本的截图:

在 YARN 上运行单个 Flink 作业

Flink 在 YARN 上提供以下配置参数来调整恢复行为:

参数 描述
yarn.reallocate-failed 设置 Flink 是否应重新分配失败的任务管理器容器。默认值为true
yarn.maximum-failed-containers 设置应用程序主在 YARN 会话失败之前接受的最大失败容器数。默认值为启动时请求的任务管理器数量。
yarn.application-attempts 设置应用程序主尝试的次数。默认值为1,这意味着如果应用程序主失败,YARN 会话将失败。

这些配置需要在conf/flink-conf.yaml中,或者可以在会话启动时使用-D参数进行设置。

工作细节

在前面的章节中,我们看到了如何在 YARN 上使用 Flink。现在让我们试着了解它的内部工作原理:

工作细节

上图显示了 Flink 在 YARN 上的内部工作原理。它经历了以下步骤:

  1. 检查 Hadoop 和 YARN 配置目录是否已设置。

  2. 如果是,则联系 HDFS 并将 JAR 和配置存储在 HDFS 上。

  3. 联系节点管理器以分配应用程序主。

  4. 一旦分配了应用程序主,就会启动 Flink 作业管理器。

  5. 稍后,根据给定的配置参数启动 Flink 任务管理器。

现在我们已经准备好在 YARN 上提交 Flink 作业了。

摘要

在本章中,我们讨论了如何使用现有的 YARN 集群以分布式模式执行 Flink 作业。我们详细了解了一些实际示例。

在下一章中,我们将看到如何在云环境中执行 Flink 作业。

第九章:在云上部署 Flink

近年来,越来越多的公司投资于基于云的解决方案,这是有道理的,考虑到我们通过云实现的成本和效率。亚马逊网络服务AWS)、Google Cloud 平台GCP)和微软 Azure 目前在这一业务中是明显的领导者。几乎所有这些公司都提供了相当方便使用的大数据解决方案。云提供了及时高效的解决方案,人们不需要担心硬件购买、网络等问题。

在本章中,我们将看到如何在云上部署 Flink。我们将详细介绍在 AWS 和 Google Cloud 上安装和部署应用程序的方法。所以让我们开始吧。

在 Google Cloud 上的 Flink

Flink 可以使用一个名为 BDUtil 的实用程序在 Google Cloud 上部署。这是一个开源实用程序,供所有人使用 cloud.google.com/hadoop/bdutil。我们需要做的第一步是安装Google Cloud SDK

安装 Google Cloud SDK

Google Cloud SDK 是一个可执行实用程序,可以安装在 Windows、Mac 或 UNIX 操作系统上。您可以根据您的操作系统选择安装模式。以下是一个链接,指导用户了解详细的安装过程 cloud.google.com/sdk/downloads

在这里,我假设您已经熟悉 Google Cloud 的概念和术语;如果没有,我建议阅读 cloud.google.com/docs/

在我的情况下,我将使用 UNIX 机器启动一个 Flink-Hadoop 集群。所以让我们开始安装。

首先,我们需要下载 Cloud SDK 的安装程序。

wget 
    https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-
    cloud-sdk-135.0.0-linux-x86_64.tar.gz

接下来,我们通过以下命令解压文件:

tar -xzf google-cloud-sdk-135.0.0-linux-x86_64.tar.gz

完成后,我们需要初始化 SDK:

cd google-cloud-sdk
bin/gcloud init

这将启动一个交互式安装过程,并需要您根据需要提供输入。下面的截图显示了这个过程:

安装 Google Cloud SDK

还建议通过执行以下命令进行身份验证:

gcloud auth login

这将为您提供一个 URL,可以在您的机器浏览器中打开。点击该 URL,您将获得一个用于身份验证的代码。

身份验证完成后,我们就可以开始 BDUtil 安装了。

安装 BDUtil

正如我们之前所说,BDUtil 是 Google 开发的一个实用程序,旨在在 Google Cloud 上实现无故障的大数据安装。您可以安装以下服务:

  • Hadoop - HDP 和 CDH

  • Flink

  • Hama

  • Hbase

  • Spark

  • Storm

  • Tajo

安装 BDUtil 需要以下步骤。首先,我们需要下载源代码:

wget 
    https://github.com/GoogleCloudPlatform/bdutil/archive/master.zip

通过以下命令解压代码:

unzip master.zip
cd bdutil-master

注意

如果您在 Google Compute 机器上使用 BDUtil 操作,建议使用非 root 帐户。通常情况下,所有计算引擎机器默认禁用 root 登录。

现在我们已经完成了 BDUtil 的安装,并准备好部署了。

BDUtil 至少需要一个项目,我们将在其中进行安装,并且需要一个存放临时文件的存储桶。要创建一个存储桶,您可以转到Cloud Storage部分,并选择创建一个存储桶,如下截图所示:

启动 Flink 集群

我们已经将这个存储桶命名为bdutil-flink-bucket。接下来,我们需要编辑bdutil_env.sh文件,配置有关项目名称、存储桶名称和要使用的 Google Cloud 区域的信息。我们还可以设置其他内容,如机器类型和操作系统。bdutil_env.sh如下所示:

 # A GCS bucket used for sharing generated SSH keys and GHFS configuration. 
CONFIGBUCKET="bdutil-flink-bucket" 

# The Google Cloud Platform text-based project-id which owns the GCE resources. 
PROJECT="bdutil-flink-project" 

###################### Cluster/Hardware Configuration ######### 
# These settings describe the name, location, shape and size of your cluster, 
# though these settings may also be used in deployment-configuration--for 
# example, to whitelist intra-cluster SSH using the cluster prefix. 

# GCE settings. 
GCE_IMAGE='https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/backports-debian-7-wheezy-v20160531' 
GCE_MACHINE_TYPE='n1-standard-4' 
GCE_ZONE="europe-west1-d" 
# When setting a network it's important for all nodes be able to communicate 
# with eachother and for SSH connections to be allowed inbound to complete 
# cluster setup and configuration. 

默认情况下,配置启动三个节点,Hadoop/Flink 集群,一个主节点和两个工作节点。

注意

如果您正在使用 GCP 的试用版,则建议使用机器类型为n1-standard-2。这将限制节点类型的 CPU 和存储。

现在我们已经准备好启动集群,使用以下命令:

./bdutil -e extensions/flink/flink_env.sh deploy

这将开始创建机器并在其上部署所需的软件。如果一切顺利,通常需要 10-20 分钟的时间来启动和运行集群。在开始执行之前,您应该查看屏幕截图告诉我们什么。

启动 Flink 集群

完成后,您将看到以下消息:

gcloud --project=bdutil ssh --zone=europe-west1-c hadoop-m 
Sat Nov 19 06:12:27 UTC 2016: Staging files successfully deleted. 
Sat Nov 19 06:12:27 UTC 2016: Invoking on master: ./deploy-ssh-master-setup.sh 
.Sat Nov 19 06:12:27 UTC 2016: Waiting on async 'ssh' jobs to finish. Might take a while... 
. 
Sat Nov 19 06:12:29 UTC 2016: Step 'deploy-ssh-master-setup,*' done... 
Sat Nov 19 06:12:29 UTC 2016: Invoking on workers: ./deploy-core-setup.sh 
..Sat Nov 19 06:12:29 UTC 2016: Invoking on master: ./deploy-core-setup.sh 
.Sat Nov 19 06:12:30 UTC 2016: Waiting on async 'ssh' jobs to finish. Might take a while... 
... 
Sat Nov 19 06:13:14 UTC 2016: Step 'deploy-core-setup,deploy-core-setup' done... 
Sat Nov 19 06:13:14 UTC 2016: Invoking on workers: ./deploy-ssh-worker-setup.sh 
..Sat Nov 19 06:13:15 UTC 2016: Waiting on async 'ssh' jobs to finish. Might take a while... 
.. 
Sat Nov 19 06:13:17 UTC 2016: Step '*,deploy-ssh-worker-setup' done... 
Sat Nov 19 06:13:17 UTC 2016: Invoking on master: ./deploy-master-nfs-setup.sh 
.Sat Nov 19 06:13:17 UTC 2016: Waiting on async 'ssh' jobs to finish. Might take a while... 
. 
Sat Nov 19 06:13:23 UTC 2016: Step 'deploy-master-nfs-setup,*' done... 
Sat Nov 19 06:13:23 UTC 2016: Invoking on workers: ./deploy-client-nfs-setup.sh 
..Sat Nov 19 06:13:23 UTC 2016: Invoking on master: ./deploy-client-nfs-setup.sh 
.Sat Nov 19 06:13:24 UTC 2016: Waiting on async 'ssh' jobs to finish. Might take a while... 
... 
Sat Nov 19 06:13:33 UTC 2016: Step 'deploy-client-nfs-setup,deploy-client-nfs-setup' done... 
Sat Nov 19 06:13:33 UTC 2016: Invoking on master: ./deploy-start.sh 
.Sat Nov 19 06:13:34 UTC 2016: Waiting on async 'ssh' jobs to finish. Might take a while... 
. 
Sat Nov 19 06:13:49 UTC 2016: Step 'deploy-start,*' done... 
Sat Nov 19 06:13:49 UTC 2016: Invoking on workers: ./install_flink.sh 
..Sat Nov 19 06:13:49 UTC 2016: Invoking on master: ./install_flink.sh 
.Sat Nov 19 06:13:49 UTC 2016: Waiting on async 'ssh' jobs to finish. Might take a while... 
... 
Sat Nov 19 06:13:53 UTC 2016: Step 'install_flink,install_flink' done... 
Sat Nov 19 06:13:53 UTC 2016: Invoking on master: ./start_flink.sh 
.Sat Nov 19 06:13:54 UTC 2016: Waiting on async 'ssh' jobs to finish. Might take a while... 
. 
Sat Nov 19 06:13:55 UTC 2016: Step 'start_flink,*' done... 
Sat Nov 19 06:13:55 UTC 2016: Command steps complete. 
Sat Nov 19 06:13:55 UTC 2016: Execution complete. Cleaning up temporary files... 
Sat Nov 19 06:13:55 UTC 2016: Cleanup complete. 

如果中途出现任何故障,请查看日志。您可以访问 Google 云计算引擎控制台以获取主机和从机的确切 IP 地址。

现在,如果您检查作业管理器 UI,您应该有两个任务管理器和四个任务插槽可供使用。您可以访问 URL http://<master-node-ip>:8081。以下是相同的示例屏幕截图:

启动 Flink 集群

执行示例作业

您可以通过启动一个示例词频统计程序来检查一切是否正常运行。为此,我们首先需要登录到 Flink 主节点。以下命令启动了 Flink 安装提供的一个示例词频统计程序。

/home/hadoop/flink-install/bin$ ./flink run   
    ../examples/WordCount.jar

11/19/2016 06:56:05     Job execution switched to status RUNNING. 
11/19/2016 06:56:05     CHAIN DataSource (at getDefaultTextLineDataSet(WordCountData.java:70) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (FlatMap at main(WordCount.java:69)) -> Combine(SUM(1), at main(WordCount.java:72)(1/1) switched to SCHEDULED 
11/19/2016 06:56:05     CHAIN DataSource (at getDefaultTextLineDataSet(WordCountData.java:70) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (FlatMap at main(WordCount.java:69)) -> Combine(SUM(1), at main(WordCount.java:72)(1/1) switched to DEPLOYING 
11/19/2016 06:56:05     CHAIN DataSource (at getDefaultTextLineDataSet(WordCountData.java:70) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (FlatMap at main(WordCount.java:69)) -> Combine(SUM(1), at main(WordCount.java:72)(1/1) switched to RUNNING 
11/19/2016 06:56:05     CHAIN Reduce (SUM(1), at main(WordCount.java:72) -> FlatMap (collect())(1/4) switched to SCHEDULED 
11/19/2016 06:56:05     CHAIN DataSource (at getDefaultTextLineDataSet(WordCountData.java:70) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (FlatMap at main(WordCount.java:69)) -> Combine(SUM(1), at main(WordCount.java:72)(1/1) switched to FINISHED 
... 
RUNNING 
11/19/2016 06:56:06     DataSink (collect() sink)(3/4) switched to SCHEDULED 
11/19/2016 06:56:06     DataSink (collect() sink)(3/4) switched to DEPLOYING 
11/19/2016 06:56:06     DataSink (collect() sink)(1/4) switched to SCHEDULED 
11/19/2016 06:56:06     DataSink (collect() sink)(1/4) switched to DEPLOYING 
11/19/2016 06:56:06     CHAIN Reduce (SUM(1), at main(WordCount.java:72) -> FlatMap (collect())(1/4) switched to FINISHED 
11/19/2016 06:56:06     CHAIN Reduce (SUM(1), at main(WordCount.java:72) -> FlatMap (collect())(3/4) switched to FINISHED 
11/19/2016 06:56:06     DataSink (collect() sink)(3/4) switched to  
11/19/2016 06:56:06     CHAIN Reduce (SUM(1), at  
11/19/2016 06:56:06     DataSink (collect() sink)(2/4) switched to FINISHED 
11/19/2016 06:56:06     Job execution switched to status FINISHED. 
(after,1) 
(arms,1) 
(arrows,1) 
(awry,1) 
(bare,1) 
(be,4) 
(coil,1) 
(consummation,1) 
(contumely,1) 
(d,4) 
(delay,1) 
(despis,1) 
... 

以下屏幕截图显示了作业的执行地图:

执行示例作业

以下是一个时间轴的屏幕截图,显示了所有任务的执行情况:

执行示例作业

关闭集群

一旦我们完成了所有的执行,如果我们不再希望进一步使用集群,最好关闭它。

以下是一个命令,我们需要执行以关闭我们启动的集群:

./bdutil -e extensions/flink/flink_env.sh delete

在删除集群之前,请务必确认配置。以下是一个屏幕截图,显示了将要删除的内容和完整的过程:

关闭集群

在 AWS 上使用 Flink

现在让我们看看如何在亚马逊网络服务(AWS)上使用 Flink。亚马逊提供了一个托管的 Hadoop 服务,称为弹性 Map Reduce(EMR)。我们可以结合使用 Flink。我们可以在 EMR 上进行阅读aws.amazon.com/documentation/elastic-mapreduce/

在这里,我假设您已经有 AWS 帐户并了解 AWS 的基础知识。

启动 EMR 集群

我们需要做的第一件事就是启动 EMR 集群。我们首先需要登录到 AWS 帐户,并从控制台中选择 EMR 服务,如下图所示:

启动 EMR 集群

接下来,我们转到 EMR 控制台,并启动一个包含一个主节点和两个从节点的三节点集群。在这里,我们选择最小的集群大小以避免意外计费。以下屏幕截图显示了 EMR 集群创建屏幕:

启动 EMR 集群

通常需要 10-15 分钟才能启动和运行集群。一旦集群准备就绪,我们可以通过 SSH 连接到集群。为此,我们首先需要单击“创建安全组”部分,并添加规则以添加 SSH 端口 22 规则。以下屏幕显示了安全组部分,在其中我们需要编辑 SSH 的“入站”流量规则:

启动 EMR 集群

现在我们已经准备好使用 SSH 和私钥登录到主节点。一旦使用 Hadoop 用户名登录,您将看到以下屏幕:

启动 EMR 集群

一旦我们的 EMR 集群准备就绪,安装 Flink 就非常容易。我们需要执行以下步骤:

  1. 从链接flink.apache.org/downloads.html下载与正确的 Hadoop 版本兼容的 Flink。我正在下载与 Hadoop 2.7 版本兼容的 Flink:
wget http://www-eu.apache.org/dist/flink/flink-1.1.4/flink-
        1.1.4-bin-hadoop27-scala_2.11.tgz

  1. 接下来,我们需要解压安装程序:
tar -xzf flink-1.1.4-bin-hadoop27-scala_2.11.tgz

  1. 就是这样,只需进入解压后的文件夹并设置以下环境变量,我们就准备好了:
cd flink-1.1.4
export HADOOP_CONF_DIR=/etc/hadoop/conf
export YARN_CONF_DIR=/etc/hadoop/conf

在 YARN 上执行 Flink 非常容易。我们已经在上一章中学习了有关 YARN 上的 Flink 的详细信息。以下步骤显示了一个示例作业执行。这将向 YARN 提交一个单个的 Flink 作业:

./bin/flink run -m yarn-cluster -yn 2 
    ./examples/batch/WordCount.jar

您将立即看到 Flink 的执行开始,并在完成后,您将看到词频统计结果:

2016-11-20 06:41:45,760 INFO  org.apache.flink.yarn.YarnClusterClient                       - Submitting job with JobID: 0004040e04879e432365825f50acc80c. Waiting for job completion. 
Submitting job with JobID: 0004040e04879e432365825f50acc80c. Waiting for job completion. 
Connected to JobManager at Actor[akka.tcp://flink@172.31.0.221:46603/user/jobmanager#478604577] 
11/20/2016 06:41:45     Job execution switched to status RUNNING. 
11/20/2016 06:41:46     CHAIN DataSource (at getDefaultTextLineDataSet(WordCountData.java:70) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (FlatMap at main(WordCount.java:80)) -> Combine(SUM(1), at main(WordCount.java:83)(1/1) switched to RUNNING 
11/20/2016 06:41:46     Reduce (SUM(1), at  
getDefaultTextLineDataSet(WordCountData.java:70) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (FlatMap at main(WordCount.java:80)) -> Combine(SUM(1), at main(WordCount.java:83)(1/1) switched to FINISHED 
11/20/2016 06:41:46     Reduce (SUM(1), at main(WordCount.java:83)(1/2) switched to DEPLOYING 
11/20/2016 06:41:46     Reduce (SUM(1), at main(WordCount.java:83)(1/2) switched to RUNNING 
11/20/2016 06:41:46     Reduce (SUM(1), at main(WordCount.java:83)(2/2) switched to RUNNING 
1/20/2016 06:41:46     Reduce (SUM(1), at main(WordCount.java:83)(1/2) switched to FINISHED 
11/20/2016 06:41:46     DataSink (collect())(2/2) switched to DEPLOYING 
11/20/2016 06:41:46     Reduce (SUM(1), at main(WordCount.java:83)(2/2) switched to FINISHED 
11/20/2016 06:41:46     DataSink (collect())(2/2) switched to RUNNING 
11/20/2016 06:41:46     DataSink (collect())(2/2) switched to FINISHED 
11/20/2016 06:41:46     Job execution switched to status FINISHED. 
(action,1) 
(after,1) 
(against,1) 
(and,12) 
(arms,1) 
(arrows,1) 
(awry,1) 
(ay,1) 
(bare,1) 
(be,4) 
(bodkin,1) 
(bourn,1) 
(calamity,1) 
(cast,1) 
(coil,1) 
(come,1) 

我们还可以查看 YARN 集群 UI,如下面的屏幕截图所示:

在 EMR-YARN 上执行 Flink

或者,我们也可以通过阻止我们在上一章中已经看到的资源来启动 YARN 会话。Flink YARN 会话将创建一个持续运行的 YARN 会话,可用于执行多个 Flink 作业。此会话将持续运行,直到我们停止它。

要启动 Flink YARN 会话,我们需要执行以下命令:

$ bin/yarn-session.sh -n 2 -tm 768 -s 4

在这里,我们启动了两个具有每个 768 MB 内存和 4 个插槽的任务管理器。您将在控制台日志中看到 YARN 会话已准备就绪的情况:

2016-11-20 06:49:09,021 INFO  org.apache.flink.yarn.YarnClusterDescriptor                 
- Using values: 
2016-11-20 06:49:09,023 INFO  org.apache.flink.yarn.YarnClusterDescriptor                   
-   TaskManager count = 2
2016-11-20 06:49:09,023 INFO  org.apache.flink.yarn.YarnClusterDescriptor                   
-   JobManager memory = 1024
2016-11-20 06:49:09,023 INFO  org.apache.flink.yarn.YarnClusterDescriptor                   
-   TaskManager memory = 768 
2016-11-20 06:49:09,488 INFO  org.apache.hadoop.yarn.client.api.impl.TimelineClientImpl     
- Timeline service address: http://ip-172-31-2-68.ap-south-1.compute.internal:8188/ws/v1/timeline/ 
2016-11-20 06:49:09,613 INFO  org.apache.hadoop.yarn.client.RMProxy                         - Connecting to ResourceManager at ip-172-31-2-68.ap-south-1.compute.internal/172.31.2.68:8032 
2016-11-20 06:49:10,309 WARN  org.apache.flink.yarn.YarnClusterDescriptor                   
- The configuration directory ('/home/hadoop/flink-1.1.3/conf') contains both LOG4J and Logback configuration files. Please delete or rename one of them. 
2016-11-20 06:49:10,325 INFO  org.apache.flink.yarn.Utils                                   - Copying from file:/home/hadoop/flink-1.1.3/conf/log4j.properties to hdfs://ip-172-31-2-68.ap-south-1.compute.internal:8020/user/hadoop/.flink/application_1479621657204_0004/log4j.properties 
2016-11-20 06:49:10,558 INFO  org.apache.flink.yarn.Utils                                   - Copying from file:/home/hadoop/flink-1.1.3/lib to hdfs://ip-172-31-2-68.ap-south-1.compute.internal:8020/user/hadoop/.flink/application_1479621657204_0004/lib 
2016-11-20 06:49:12,392 INFO  org.apache.flink.yarn.Utils                                   - Copying from /home/hadoop/flink-1.1.3/conf/flink-conf.yaml to hdfs://ip-172-31-2-68.ap-south-1.compute.internal:8020/user/hadoop/.flink/application_1479621657204_0004/flink-conf.yaml 
2016-11-20 06:49:12,825 INFO  org.apache.flink.yarn.YarnClusterDescriptor                   
- Submitting application master application_1479621657204_0004 
2016-11-20 06:49:12,893 INFO  org.apache.hadoop.yarn.client.api.impl.YarnClientImpl         
- Submitted application application_1479621657204_0004 
2016-11-20 06:49:12,893 INFO  org.apache.flink.yarn.YarnClusterDescriptor                   
- Waiting for the cluster to be allocated 
2016-11-20 06:49:17,929 INFO  org.apache.flink.yarn.YarnClusterDescriptor                   
- YARN application has been deployed successfully. 
Flink JobManager is now running on 172.31.0.220:45056 
JobManager Web Interface: http://ip-172-31-2-68.ap-south-1.compute.internal:20888/proxy/application_1479621657204_0004/ 
2016-11-20 06:49:18,117 INFO  org.apache.flink.yarn.YarnClusterClient                       - Starting client actor system. 
2016-11-20 06:49:18,591 INFO  akka.event.slf4j.Slf4jLogger                                  - Slf4jLogger started 
2016-11-20 06:49:18,671 INFO  Remoting                                                       
akka.tcp://flink@172.31.0.220:45056/user/jobmanager. 
2016-11-20 06:49:19,343 INFO  org.apache.flink.yarn.ApplicationClient                       - Successfully registered at the ResourceManager using JobManager Actor[akka.tcp://flink@172.31.0.220:45056/user/jobmanager#1383364724] 
Number of connected TaskManagers changed to 2\. Slots available: 8 

这是 Flink 作业管理器 UI 的屏幕截图,我们可以看到两个任务管理器和八个任务插槽:

启动 Flink YARN 会话

现在我们可以使用这个 YARN 会话来提交 Flink 作业,执行以下命令:

$./bin/flink run ./examples/batch/WordCount.jar

您将看到如下代码所示的词频统计作业的执行:

2016-11-20 06:53:06,439 INFO  org.apache.flink.yarn.cli.FlinkYarnSessionCli                 
- Found YARN properties file /tmp/.yarn-properties-hadoop 
2016-11-20 06:53:06,439 INFO  org.apache.flink.yarn.cli.FlinkYarnSessionCli                 
- Found YARN properties file /tmp/.yarn-properties-hadoop 
Found YARN properties file /tmp/.yarn-properties-hadoop 
2016-11-20 06:53:06,508 INFO  org.apache.flink.yarn.cli.FlinkYarnSessionCli                 
-  
org.apache.flink.yarn.cli.FlinkYarnSessionCli                 
- YARN properties set default parallelism to 8 
YARN properties set default parallelism to 8 
2016-11-20 06:53:06,510 INFO  org.apache.flink.yarn.cli.FlinkYarnSessionCli                 
- Found YARN properties file /tmp/.yarn-properties-hadoop 
2016-11-20 06:53:07,069 INFO  org.apache.hadoop.yarn.client.api.impl.TimelineClientImpl     
- Timeline service address: http://ip-172-31-2-68.ap-south-1.compute.internal:8188/ws/v1/timeline/ 
Executing WordCount example with default input data set. 
Use --input to specify file input. 
Printing result to stdout. Use --output to specify output path. 
2016-11-20 06:53:07,728 INFO  org.apache.flink.yarn.YarnClusterClient                       - Waiting until all TaskManagers have connected 
Waiting until all TaskManagers have connected 
2016-11-20 06:53:07,729 INFO  org.apache.flink.yarn.YarnClusterClient                        
Submitting job with JobID: a0557f5751fa599b3eec30eb50d0a9ed. Waiting for job completion. 
Connected to JobManager at Actor[akka.tcp://flink@172.31.0.220:45056/user/jobmanager#1383364724] 
11/20/2016 06:53:09     Job execution switched to status RUNNING. 
11/20/2016 06:53:09     CHAIN DataSource (at getDefaultTextLineDataSet(WordCountData.java:70) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (FlatMap at main(WordCount.java:80)) -> Combine(SUM(1), at main(WordCount.java:83)(1/1) switched to SCHEDULED 
11/20/2016 06:53:09     CHAIN DataSource (at getDefaultTextLineDataSet(WordCountData.java:70) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (FlatMap at main(WordCount.java:80)) -> Combine(SUM(1), at main(WordCount.java:83)(1/1) switched to DEPLOYING 
11/20/2016 06:53:09     CHAIN DataSource (at getDefaultTextLineDataSet(WordCountData.java:70) (org.apache.flink.api.java.io.CollectionInputFormat)) -> FlatMap (FlatMap at main(WordCount.java:80)) -> Combine(SUM(1), at  
11/20/2016 06:53:10     DataSink (collect())(7/8) switched to FINISHED 
11/20/2016 06:53:10     DataSink (collect())(8/8) switched to FINISHED 
11/20/2016 06:53:10     Job execution switched to status FINISHED. 
(bourn,1) 
(coil,1) 
(come,1) 
(d,4) 
(dread,1) 
(is,3) 
(long,1) 
(make,2) 
(more,1) 
(must,1) 
(no,2) 
(oppressor,1) 
(pangs,1) 
(perchance,1) 
(sicklied,1) 
(something,1) 
(takes,1) 
(these,1) 
(us,3) 
(what,1) 
Program execution finished 
Job with JobID a0557f5751fa599b3eec30eb50d0a9ed has finished. 
Job Runtime: 903 ms 
Accumulator Results: 
- f895985ab9d76c97aba23bc6689c7936 (java.util.ArrayList) [170 elements] 

这是作业执行详细信息和任务分解的屏幕截图:

在 YARN 会话上执行 Flink 作业

我们还可以看到时间轴详细信息,显示了所有并行执行的任务以及按顺序执行的任务。以下是同样的屏幕截图:

在 YARN 会话上执行 Flink 作业

关闭集群

完成所有工作后,关闭集群非常重要。为此,我们需要再次转到 AWS 控制台,然后点击终止按钮。

AWS 现在默认支持其 EMR 集群中的 Flink。为了获得这一点,我们必须遵循这些说明。

首先,我们必须转到 AWS EMR 创建集群屏幕,然后点击转到高级选项链接,如下面的屏幕截图中所示:

EMR 5.3+上的 Flink

接下来,您将看到一个屏幕,让您选择您希望拥有的其他服务。在那里,您需要勾选 Flink 1.1.4:

EMR 5.3+上的 Flink

然后点击下一步按钮,继续进行其余的设置。其余步骤与我们在前几节中看到的相同。一旦集群启动并运行,您就可以直接使用 Flink。

亚马逊简单存储服务S3)是 AWS 提供的一种软件即服务,用于在 AWS 云中存储数据。许多公司使用 S3 进行廉价的数据存储。它是作为服务的托管文件系统。S3 可以用作 HDFS 的替代方案。如果某人不想投资于完整的 Hadoop 集群,可以考虑使用 S3 而不是 HDFS。Flink 为您提供 API,允许读取存储在 S3 上的数据。

我们可以像简单文件一样使用 S3 对象。以下代码片段显示了如何在 Flink 中使用 S3 对象:

// Read data from S3 bucket 
env.readTextFile("s3://<bucket>/<endpoint>"); 

// Write data to S3 bucket 
stream.writeAsText("s3://<bucket>/<endpoint>"); 

// Use S3 as FsStatebackend 
env.setStateBackend(new FsStateBackend("s3://<your-bucket>/<endpoint>"));

Flink 将 S3 视为任何其他文件系统。它使用 Hadoop 的 S3 客户端。

要访问 S3 对象,Flink 需要进行身份验证。这可以通过使用 AWS IAM 服务来提供。这种方法有助于保持安全性,因为我们不需要分发访问密钥和秘密密钥。

总结

在本章中,我们学习了如何在 AWS 和 GCP 上部署 Flink。这对于更快的部署和安装非常方便。我们可以用最少的工作量生成和删除 Flink 集群。

在下一章中,我们将学习如何有效地使用 Flink 的最佳实践。

第十章:最佳实践

到目前为止,在本书中,我们已经学习了关于 Flink 的各种知识。我们从 Flink 的架构和它支持的各种 API 开始。我们还学习了如何使用 Flink 提供的图形和机器学习 API。现在在这个总结性的章节中,我们将讨论一些最佳实践,您应该遵循以创建高质量可维护的 Flink 应用程序。

我们将讨论以下主题:

  • 日志最佳实践

  • 使用自定义序列化器

  • 使用和监控 REST API

  • 背压监控

所以让我们开始吧。

日志最佳实践

在任何软件应用程序中配置日志非常重要。日志有助于调试问题。如果我们不遵循这些日志记录实践,将很难理解作业的进度或是否存在任何问题。我们可以使用一些库来获得更好的日志记录体验。

配置 Log4j

正如我们所知,Log4j 是最广泛使用的日志记录库之一。我们可以在任何 Flink 应用程序中配置它,只需很少的工作。我们只需要包含一个log4j.properties文件。我们可以通过将其作为Dlog4j.configuration=/path/to/log4j.properties参数传递来传递log4j.properties文件。

Flink 支持以下默认属性文件:

配置 Logback

如今,很多人更喜欢 Logback 而不是 Log4j,因为它具有更多的功能。Logback 提供更快的 I/O、经过彻底测试的库、广泛的文档等。Flink 也支持为应用程序配置 Logback。

我们需要使用相同的属性来配置logback.xmlDlogback.configurationFile=<file>,或者我们也可以将logback.xml文件放在类路径中。示例logback.xml如下所示:

<configuration> 
    <appender name="file" class="ch.qos.logback.core.FileAppender"> 
        <file>${log.file}</file> 
        <append>false</append> 
        <encoder> 
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level   
            %logger{60} %X{sourceThread} - %msg%n</pattern> 
        </encoder> 
    </appender> 

    <!-- This affects logging for both user code and Flink --> 
    <root level="INFO"> 
        <appender-ref ref="file"/> 
    </root> 

    <!-- Uncomment this if you want to only change Flink's logging --> 
    <!--<logger name="org.apache.flink" level="INFO">--> 
        <!--<appender-ref ref="file"/>--> 
    <!--</logger>--> 

    <!-- The following lines keep the log level of common  
    libraries/connectors on 
         log level INFO. The root logger does not override this. You 
         have to manually 
         change the log levels here. --> 
    <logger name="akka" level="INFO"> 
        <appender-ref ref="file"/> 
    </logger> 
    <logger name="org.apache.kafka" level="INFO"> 
        <appender-ref ref="file"/> 
    </logger> 
    <logger name="org.apache.hadoop" level="INFO"> 
        <appender-ref ref="file"/> 
    </logger> 
    <logger name="org.apache.zookeeper" level="INFO"> 
        <appender-ref ref="file"/> 
    </logger> 

    <!-- Suppress the irrelevant (wrong) warnings from the Netty 
     channel handler --> 
    <logger name="org.jboss.netty.channel.DefaultChannelPipeline" 
    level="ERROR"> 
        <appender-ref ref="file"/> 
    </logger> 
</configuration> 

我们可以随时更改logback.xml文件,并根据我们的偏好设置日志级别。

应用程序中的日志记录

在任何 Flink 应用程序中使用 SLF4J 时,我们需要导入以下包和类,并使用类名初始化记录器:

import org.slf4j.LoggerFactory 
import org.slf4j.Logger 

Logger LOG = LoggerFactory.getLogger(MyClass.class) 

使用占位符机制而不是使用字符串格式化也是最佳实践。占位符机制有助于避免不必要的字符串形成,而只进行字符串连接。以下代码片段显示了如何使用占位符:

LOG.info("Value of a = {}, value of b= {}", myobject.a, myobject.b); 

我们还可以在异常处理中使用占位符日志记录:

catch(Exception e){ 
  LOG.error("Error occurred {}",  e); 
} 

使用 ParameterTool

自 Flink 0.9 以来,我们在 Flink 中有一个内置的ParameterTool,它有助于从外部源(如参数、系统属性或属性文件)获取参数。在内部,它是一个字符串映射,它将键保留为参数名称,将值保留为参数值。

例如,我们可以考虑在我们的 DataStream API 示例中使用 ParameterTool,其中我们需要设置 Kafka 属性:

String kafkaproperties = "/path/to/kafka.properties";
ParameterTool parameter = ParameterTool.fromPropertiesFile(propertiesFile);

从系统属性

我们可以读取系统变量中定义的属性。我们需要在初始化之前通过设置Dinput=hdfs://myfile来传递系统属性文件。

现在我们可以按以下方式在ParameterTool中读取所有这些属性:

ParameterTool parameters = ParameterTool.fromSystemProperties(); 

从命令行参数

我们还可以从命令行参数中读取参数。在调用应用程序之前,我们必须设置--elements

以下代码显示了如何从命令行参数中读取参数:

ParameterTool parameters = ParameterTool.fromArgs(args); 

来自.properties 文件

我们还可以从.properties文件中读取参数。以下是此代码:

String propertiesFile = /my.properties"; 
ParameterTool parameters = ParameterTool.fromPropertiesFile(propertiesFile); 

我们可以在 Flink 程序中读取参数。以下显示了我们如何获取参数:

parameter.getRequired("key"); 
parameter.get("paramterName", "myDefaultValue"); 
parameter.getLong("expectedCount", -1L); 
parameter.getNumberOfParameters() 

命名大型 TupleX 类型

正如我们所知,元组是用于表示复杂数据结构的复杂数据类型。它是各种原始数据类型的组合。通常建议不要使用大型元组;而是建议使用 Java POJOs。如果要使用元组,建议使用一些自定义 POJO 类型来命名它。

为大型元组创建自定义类型非常容易。例如,如果我们想要使用Tuple8,则可以定义如下:

//Initiate Record Tuple
RecordTuple rc = new RecordTuple(value0, value1, value2, value3, value4, value5, value6, value7);

// Define RecordTuple instead of using Tuple8
public static class RecordTuple extends Tuple8<String, String, Integer, String, Integer, Integer, Integer, Integer> {

         public RecordTuple() {
               super();
         }

         public RecordTuple(String value0, String value1, Integer value2, String value3, Integer value4, Integer value5,
                     Integer value6, Integer value7) {
               super(value0, value1, value2, value3, value4, value5, value6, value7);
         }
      } 

注册自定义序列化程序

在分布式计算世界中,非常重要的是要注意每一个小细节。序列化就是其中之一。默认情况下,Flink 使用 Kryo 序列化程序。Flink 还允许我们编写自定义序列化程序,以防您认为默认的序列化程序不够好。我们需要注册自定义序列化程序,以便 Flink 能够理解它。注册自定义序列化程序非常简单;我们只需要在 Flink 执行环境中注册其类类型。以下代码片段显示了我们如何做到这一点:

final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); 

// register the class of the serializer as serializer for a type 
env.getConfig().registerTypeWithKryoSerializer(MyCustomType.class, MyCustomSerializer.class); 

// register an instance as serializer for a type 
MySerializer mySerializer = new MySerializer(); 
env.getConfig().registerTypeWithKryoSerializer(MyCustomType.class, mySerializer); 

这是一个完整的自定义序列化程序示例类,网址为github.com/deshpandetanmay/mastering-flink/blob/master/chapter10/flink-batch-adv/src/main/java/com/demo/flink/batch/RecordSerializer.java

以及自定义类型在github.com/deshpandetanmay/mastering-flink/blob/master/chapter10/flink-batch-adv/src/main/java/com/demo/flink/batch/Record.java

我们需要确保自定义序列化程序必须扩展 Kryo 的序列化程序类。使用 Google Protobuf 和 Apache Thrift,这已经完成了。

注意

您可以在github.com/google/protobuf了解更多关于 Google Protobuf 的信息。有关 Apache Thrift 的详细信息,请访问thrift.apache.org/

为了使用 Google Protobuf,您可以添加以下 Maven 依赖项:

<dependency> 
  <groupId>com.twitter</groupId> 
  <artifactId>chill-protobuf</artifactId> 
  <version>0.5.2</version> 
</dependency> 
<dependency> 
  <groupId>com.google.protobuf</groupId> 
  <artifactId>protobuf-java</artifactId> 
  <version>2.5.0</version> 
</dependency> 

度量

Flink 支持一个度量系统,允许用户了解有关 Flink 设置和在其上运行的应用程序的更多信息。如果您在一个非常庞大的生产系统中使用 Flink,那将非常有用,其中运行了大量作业,我们需要获取每个作业的详细信息。我们还可以使用这些来提供给外部监控系统。因此,让我们尝试了解可用的内容以及如何使用它们。

注册度量

度量函数可以从任何扩展RichFunction的用户函数中使用,方法是调用getRuntimeContext().getMetricGroup()。这些方法返回一个MetricGroup对象,可用于创建和注册新的度量。

Flink 支持各种度量类型,例如:

  • 计数器

  • 计量表

  • 直方图

计数器

计数器可以用于在处理过程中计算某些事物。计数器的一个简单用途可以是计算数据中的无效记录。您可以选择根据条件增加或减少计数器。以下代码片段显示了这一点:

public class TestMapper extends RichMapFunction<String, Integer> { 
  private Counter errorCounter; 

  @Override 
  public void open(Configuration config) { 
    this.errorCounter = getRuntimeContext() 
      .getMetricGroup() 
      .counter("errorCounter"); 
  } 

  @public Integer map(String value) throws Exception { 
    this.errorCounter.inc(); 
  } 
} 

计量表

计量表可以在需要时提供任何值。为了使用计量表,首先我们需要创建一个实现org.apache.flink.metrics.Gauge的类。稍后,您可以将其注册到MetricGroup中。

以下代码片段显示了在 Flink 应用程序中使用计量表:

public class TestMapper extends RichMapFunction<String, Integer> { 
  private int valueToExpose; 

  @Override 
  public void open(Configuration config) { 
    getRuntimeContext() 
      .getMetricGroup() 
      .gauge("MyGauge", new Gauge<Integer>() { 
        @Override 
        public Integer getValue() { 
          return valueToReturn; 
        } 
      }); 
  } 
} 

直方图

直方图提供了长值在度量上的分布。这可用于随时间监视某些度量。以下代码片段显示了如何使用它:

public class TestMapper extends RichMapFunction<Long, Integer> { 
  private Histogram histogram; 

  @Override 
  public void open(Configuration config) { 
    this.histogram = getRuntimeContext() 
      .getMetricGroup() 
      .histogram("myHistogram", new MyHistogram()); 
  } 

  @public Integer map(Long value) throws Exception { 
    this.histogram.update(value); 
  } 
} 

米用于监视特定参数的平均吞吐量。使用markEvent()方法注册事件的发生。我们可以使用MeterGroup上的meter(String name, Meter meter)方法注册米:

public class MyMapper extends RichMapFunction<Long, Integer> { 
  private Meter meter; 

  @Override 
  public void open(Configuration config) { 
    this.meter = getRuntimeContext() 
      .getMetricGroup() 
      .meter("myMeter", new MyMeter()); 
  } 

  @public Integer map(Long value) throws Exception { 
    this.meter.markEvent(); 
  } 
} 

报告者

通过在conf/flink-conf.yaml文件中配置一个或多个报告者,可以将指标显示到外部系统中。大多数人可能知道诸如 JMX 之类的系统,这些系统有助于监视许多系统。我们可以考虑在 Flink 中配置 JMX 报告。报告者应具有以下表中列出的某些属性:

配置 描述
metrics.reporters 命名报告者的列表
metrics.reporter.<name>.<config> 用于名为<name>的报告者的配置
metrics.reporter.<name>.class 用于名为<name>的报告者的报告者类
metrics.reporter.<name>.interval 名为<name>的报告者的间隔时间
metrics.reporter.<name>.scope.delimiter 名为<name>的报告者的范围

以下是 JMX 报告者的报告配置示例:

metrics.reporters: my_jmx_reporter 

metrics.reporter.my_jmx_reporter.class: org.apache.flink.metrics.jmx.JMXReporter 
metrics.reporter.my_jmx_reporter.port: 9020-9040 

一旦我们在config/flink-conf.yaml中添加了上述给定的配置,我们需要启动 Flink 作业管理器进程。现在,Flink 将开始将这些变量暴露给 JMX 端口8789。我们可以使用 JConsole 来监视 Flink 发布的报告。JConsole 默认随 JDK 安装。我们只需要转到 JDK 安装目录并启动JConsole.exe。一旦 JConsole 运行,我们需要选择 Flink 作业管理器进程进行监视,我们可以看到可以监视的各种值。以下是监视 Flink 的 JConsole 屏幕的示例截图。

报告者

注意

除了 JMX,Flink 还支持 Ganglia、Graphite 和 StasD 等报告者。有关这些报告者的更多信息,请访问ci.apache.org/projects/flink/flink-docs-release-1.2/monitoring/metrics.html#reporter

监控 REST API

Flink 支持监视正在运行和已完成应用程序的状态。这些 API 也被 Flink 自己的作业仪表板使用。状态 API 支持get方法,该方法返回给定作业的信息的 JSON 对象。目前,默认情况下在 Flink 作业管理器仪表板中启动监控 API。这些信息也可以通过作业管理器仪表板访问。

Flink 中有许多可用的 API。让我们开始了解其中一些。

配置 API

这提供了 API 的配置详细信息:http://localhost:8081/config

以下是响应:

{ 
    "refresh-interval": 3000, 
    "timezone-offset": 19800000, 
    "timezone-name": "India Standard Time", 
    "flink-version": "1.0.3", 
    "flink-revision": "f3a6b5f @ 06.05.2016 @ 12:58:02 UTC" 
} 

概述 API

这提供了 Flink 集群的概述:http://localhost:8081/overview

以下是响应:

{ 
    "taskmanagers": 1, 
    "slots-total": 1, 
    "slots-available": 1, 
    "jobs-running": 0, 
    "jobs-finished": 1, 
    "jobs-cancelled": 0, 
    "jobs-failed": 0, 
    "flink-version": "1.0.3", 
    "flink-commit": "f3a6b5f" 
} 

作业概述

这提供了最近运行并当前正在运行的作业的概述:http://localhost:8081/jobs

以下是响应:

{ 
    "jobs-running": [], 
    "jobs-finished": [ 
        "cd978489f5e76e5988fa0e5a7c76c09b" 
    ], 
    "jobs-cancelled": [], 
    "jobs-failed": [] 
} 

http://localhost:8081/joboverview API 提供了 Flink 作业的完整概述。它包含作业 ID、开始和结束时间、运行持续时间、任务数量及其状态。状态可以是已启动、运行中、已终止或已完成。

以下是响应:

{ 
    "running": [], 
    "finished": [ 
        { 
            "jid": "cd978489f5e76e5988fa0e5a7c76c09b", 
            "name": "Flink Java Job at Sun Dec 04 16:13:16 IST 2016", 
            "state": "FINISHED", 
            "start-time": 1480848197679, 
            "end-time": 1480848198310, 
            "duration": 631, 
            "last-modification": 1480848198310, 
            "tasks": { 
                "total": 3, 
                "pending": 0, 
                "running": 0, 
                "finished": 3, 
                "canceling": 0, 
                "canceled": 0, 
                "failed": 0 
            } 
        } 
    ] 
} 

特定作业的详细信息

这提供了特定作业的详细信息。我们需要提供上一个 API 返回的作业 ID。当提交作业时,Flink 为该作业创建一个有向无环作业(DAG)。该图包含作业的任务和执行计划的顶点。以下输出显示了相同的细节。 http://localhost:8081/jobs/<jobid>

以下是响应:

{ 
    "jid": "cd978489f5e76e5988fa0e5a7c76c09b", 
    "name": "Flink Java Job at Sun Dec 04 16:13:16 IST 2016", 
    "isStoppable": false, 
    "state": "FINISHED", 
    "start-time": 1480848197679, 
    "end-time": 1480848198310, 
    "duration": 631, 
    "now": 1480849319207, 
    "timestamps": { 
        "CREATED": 1480848197679, 
        "RUNNING": 1480848197733, 
        "FAILING": 0, 
        "FAILED": 0, 
        "CANCELLING": 0, 
        "CANCELED": 0, 
        "FINISHED": 1480848198310, 
        "RESTARTING": 0 
    }, 
    "vertices": [ 
        { 
            "id": "f590afd023018e19e30ce3cd7a16f4b1", 
            "name": "CHAIN DataSource (at  
             getDefaultTextLineDataSet(WordCountData.java:70) 
             (org.apache.flink.api.java.io.CollectionInputFormat)) -> 
             FlatMap (FlatMap at main(WordCount.java:81)) ->   
             Combine(SUM(1), at main(WordCount.java:84)", 
            "parallelism": 1, 
            "status": "FINISHED", 
            "start-time": 1480848197744, 
            "end-time": 1480848198061, 
            "duration": 317, 
            "tasks": { 
                "CREATED": 0, 
                "SCHEDULED": 0, 
                "DEPLOYING": 0, 
                "RUNNING": 0, 
                "FINISHED": 1, 
                "CANCELING": 0, 
                "CANCELED": 0, 
                "FAILED": 0 
            }, 
            "metrics": { 
                "read-bytes": 0, 
                "write-bytes": 1696, 
                "read-records": 0, 
                "write-records": 170 
            } 
        }, 
        { 
            "id": "c48c21be9c7bf6b5701cfa4534346f2f", 
            "name": "Reduce (SUM(1), at main(WordCount.java:84)", 
            "parallelism": 1, 
            "status": "FINISHED", 
            "start-time": 1480848198034, 
            "end-time": 1480848198190, 
            "duration": 156, 
            "tasks": { 
                "CREATED": 0, 
                "SCHEDULED": 0, 
                "DEPLOYING": 0, 
                "RUNNING": 0, 
                "FINISHED": 1, 
                "CANCELING": 0, 
                "CANCELED": 0, 
                "FAILED": 0 
            }, 
            "metrics": { 
                "read-bytes": 1696, 
                "write-bytes": 1696, 
                "read-records": 170, 
                "write-records": 170 
            } 
        }, 
        { 
            "id": "ff4625cfad1f2540bd08b99fb447e6c2", 
            "name": "DataSink (collect())", 
            "parallelism": 1, 
            "status": "FINISHED", 
            "start-time": 1480848198184, 
            "end-time": 1480848198269, 
            "duration": 85, 
            "tasks": { 
                "CREATED": 0, 
                "SCHEDULED": 0, 
                "DEPLOYING": 0, 
                "RUNNING": 0, 
                "FINISHED": 1, 
                "CANCELING": 0, 
                "CANCELED": 0, 
                "FAILED": 0 
            }, 
            "metrics": { 
                "read-bytes": 1696, 
                "write-bytes": 0, 
                "read-records": 170, 
                "write-records": 0 
            } 
        } 
    ], 
    "status-counts": { 
        "CREATED": 0, 
        "SCHEDULED": 0, 
        "DEPLOYING": 0, 
        "RUNNING": 0, 
        "FINISHED": 3, 
        "CANCELING": 0, 
        "CANCELED": 0, 
        "FAILED": 0 
    }, 
    "plan": { 
//plan details 

    } 
} 

用户定义的作业配置

这提供了特定作业使用的用户定义作业配置的概述:

http://localhost:8081/jobs/<jobid>/config

以下是响应:

{ 
    "jid": "cd978489f5e76e5988fa0e5a7c76c09b", 
    "name": "Flink Java Job at Sun Dec 04 16:13:16 IST 2016", 
    "execution-config": { 
        "execution-mode": "PIPELINED", 
        "restart-strategy": "default", 
        "job-parallelism": -1, 
        "object-reuse-mode": false, 
        "user-config": {} 
    } 
} 

同样,您可以在自己的设置中探索以下列出的所有 API:

/config 
/overview 
/jobs 
/joboverview/running 
/joboverview/completed 
/jobs/<jobid> 
/jobs/<jobid>/vertices 
/jobs/<jobid>/config 
/jobs/<jobid>/exceptions 
/jobs/<jobid>/accumulators 
/jobs/<jobid>/vertices/<vertexid> 
/jobs/<jobid>/vertices/<vertexid>/subtasktimes 
/jobs/<jobid>/vertices/<vertexid>/taskmanagers 
/jobs/<jobid>/vertices/<vertexid>/accumulators 
/jobs/<jobid>/vertices/<vertexid>/subtasks/accumulators 
/jobs/<jobid>/vertices/<vertexid>/subtasks/<subtasknum> 
/jobs/<jobid>/vertices/<vertexid>/subtasks/<subtasknum>/attempts/<attempt> 
/jobs/<jobid>/vertices/<vertexid>/subtasks/<subtasknum>/attempts/<attempt>/accumulators 
/jobs/<jobid>/plan 

背压监控

背压是 Flink 应用程序中的一种特殊情况,其中下游运算符无法以与推送数据的上游运算符相同的速度消耗数据。这开始在管道上施加压力,并且数据流开始朝相反方向流动。一般来说,如果发生这种情况,Flink 会在日志中警告我们。

在源汇场景中,如果我们看到对源的警告,那么这意味着汇正在以比源产生数据更慢的速度消耗数据。

监控所有流作业的背压非常重要,因为高背压的作业可能会失败或产生错误的结果。可以从 Flink 仪表板监控背压。

Flink 不断处理背压监控,对运行任务进行采样堆栈跟踪。如果采样显示任务卡在内部方法中,这表明存在背压。

平均而言,作业管理器每 50 毫秒触发 100 个堆栈跟踪。根据卡在内部过程中的任务数量,决定背压警告级别,如下表所示:

比率 背压级别
0 到 0.10 正常
0.10 到 0.5
0.5 到 1

您还可以通过设置以下参数来配置样本的数量和间隔:

参数 描述
jobmanager.web.backpressure.refresh-interval 重置可用统计信息的刷新间隔。默认为 60,000,1 分钟。
jobmanager.web.backpressure.delay-between-samples 样本之间的延迟间隔。默认为 50 毫秒。
jobmanager.web.backpressure.num-samples 用于确定背压的样本数量。默认为 100

总结

在这最后一章中,我们看了一些应该遵循的最佳实践,以实现 Flink 的最佳性能。我们还研究了各种监控 API 和指标,这些可以用于详细监控 Flink 应用程序。

对于 Flink,我想说旅程刚刚开始,我相信多年来,社区和支持会变得更加强大和更好。毕竟,Flink 被称为大数据的第四代4G)!

posted @ 2024-05-21 12:53  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报