Flink-学习手册-三-

58 阅读55分钟

Flink 学习手册(三)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章: 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>]
getVertexIdsDataSet<K> getVertexIds()getVertexIds: DataSet[K]
getEdgeIdsDataSet<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)]
获取numberOfVerticeslong numberOfVertices()numberOfVertices: Long
获取numberOfEdgeslong 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机场名称
s01A
s02B
s03C
s04D
s05E

机场之间的距离信息如下表所示:

FromToDistance
s01s0210
s01s0212
s01s0322
s01s0421
s04s1122
s05s1521
s06s1721
s08s0911
s08s0912

现在让我们使用 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 YARN 会话

一旦二进制文件被提取,我们就可以启动 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 会话

将作业提交到 Flink

现在我们已经连接到 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 会话

处理完成后,您可以以两种方式停止 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 上运行单个 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 上的恢复行为

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 的安装,并准备好部署了。

启动 Flink 集群

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

一旦我们的 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

在 EMR-YARN 上执行 Flink

在 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

启动 Flink YARN 会话

或者,我们也可以通过阻止我们在上一章中已经看到的资源来启动 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 作业

现在我们可以使用这个 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 控制台,然后点击终止按钮。

EMR 5.3+上的 Flink

AWS 现在默认支持其 EMR 集群中的 Flink。为了获得这一点,我们必须遵循这些说明。

首先,我们必须转到 AWS EMR 创建集群屏幕,然后点击转到高级选项链接,如下面的屏幕截图中所示:

EMR 5.3+上的 Flink

接下来,您将看到一个屏幕,让您选择您希望拥有的其他服务。在那里,您需要勾选 Flink 1.1.4:

EMR 5.3+上的 Flink

然后点击下一步按钮,继续进行其余的设置。其余步骤与我们在前几节中看到的相同。一旦集群启动并运行,您就可以直接使用 Flink。

在 Flink 应用程序中使用 S3

亚马逊简单存储服务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)!