# 五、Hadoop 流式

虽然 MapReduce 和 Hadoop 本身都是本机 Java 平台，但也支持用其他语言构建 MapReduce 组件。这被称为 Hadoop Streaming，这是一种简单的方法，Hadoop 调用可执行文件作为任务的一部分，而不是在 JVM 中托管一个 JAR 文件。

Hadoop Streaming 使用标准的输入和输出流与执行进程进行通信，因此它适用于任何可以构建可执行二进制文件的平台，该文件从 **stdin** 读取并写入 **stdout** 。虽然简单，但 Hadoop Streaming 是一种强大的技术，它极大地扩展了 MapReduce 的范围和灵活性。

流允许您使用 MapReduce，而不必为 mappers 和 Reduce 编写 Java 代码。这对于已经投资非 Java 平台的用户尤其有吸引力。如果您有一个用 Python 编写的自定义分析代码库，或者您想在中编写 MapReduce 代码。NET，你可以用 Hadoop Streaming 做到这一点。您甚至可以混合和匹配，使用带有 C++缩减器的 Java 映射器或带有 Python 缩减器的 R 映射器。

流式作业是一种 MapReduce 作业，但作业任务的执行模式不同。它仍然从客户端提交给纱，并且仍然遵循纱架构，应用主开始管理作业。Hadoop 附带了一个标准的 JAR 作为驱动程序，我们通过`hadoop`命令传递我们想要流式传输的可执行文件的细节。

当任务容器运行时，Java 虚拟机生成可执行进程。与应用主机的通信是在 Java 虚拟机中完成的，它充当了 Hadoop 和流可执行文件之间的桥梁。

`hadoop-streaming.jar`取四个参数(至少；可以使用更多参数配置作业)，指定输入和输出目录以及要为映射器和缩减器运行的可执行文件。代码清单 20 显示了一个有效的 Hadoop Streaming 作业提交，它使用标准的 Linux shell 命令作为映射器和缩减器。

 20:使用系统命令的 Hadoop 流

```java
  hadoop jar
  $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-2.7.2.jar \
   -input input \
   -output output4 \
   -mapper /bin/cat \
   -reducer /usr/bin/wc

```

该命令运行一个 MapReduce 程序，将`cat`命令作为映射器，将所有输入行连接成一个输出，将`wc`命令作为缩减器，对中间输出进行字数统计。

为此使用 MapReduce 意味着所有的输入文件都将由 mappers 合并，并作为单个输入呈现给 Reduce。然后，缩减器对组合文件进行计数。代码清单 21 显示了流作业的输出。

 21:流式作业的输出

```java
  # hadoop fs -cat output4/*
     2022    7600   76858

```

`wc`命令以固定的顺序写入输出，显示输入的行数、字数和总字节数，这里我们有 2022 行，包含 75KB 存储空间中的 7600 个字。这是一个微不足道的例子，但它展示了流的力量——这里我们有一些由 Hadoop 生成的真实分析，我们根本不需要编写任何代码。

Hadoop Streaming 使用一个非常简单的接口与可执行的映射器和缩减器进行通信。请记住，所有数据都是作为键值对通过 Hadoop 传输的，这也适用于流式应用。输入从 Hadoop 以一系列制表符分隔的行的形式提供给可执行文件，其中:

*   第一个制表符前的字符串是键。
*   第一个制表符后的字符串是值。
*   每个键值对作为一行发送到 **stdin** 。
*   发送到 **stdin** 的空字符串表示输入结束。

Hadoop 使用相同的协议从可执行文件中收集输出，其中 **stdout** 中的每一行都被读取为一个新的、以制表符分隔的键值对。可执行文件还可以写入错误流( **stderr** )来更新作业计数器或状态信息。

对于 mappers，流接口与 MapReduce API 相同，输入包含一个键值对。对于 Reduce，接口是不同的——在 MapReduce 中，用一个键和值的数组调用 reduce，但是在 streaming API 中，数组被展平，这意味着可执行文件在每次调用中用同一个键和数组中的单个值调用多次。

在本章的剩余部分，我们将看到如何将第 2 章中的简单字数统计作为一个 Hadoop Streaming 作业来运行。

Python 是 Hadoop 流作业的一个很好的选择。它在工程师和分析师中是一种流行的语言，它是跨平台的，并且在大多数操作系统中，它是标准安装。您可以合理地预期它将预安装在您的集群上，并且您还可以在本地运行 MapReduce 管道的简化版本来验证您的脚本。

Python 也是一种非常简洁的语言。完整的源代码在 GitHub 上的[六眼/Hadoop-简洁/python](https://github.com/sixeyed/hadoop-succinctly/tree/master/python) 中。代码清单 22 完整地展示了 Python 映射器。

22:Python 中的字数映射器

```java
  #!/usr/bin/env python

  import sys

  for line in sys.stdin:
    line = line.strip()
    words = line.split()
    for word in words:
      if 'dfs' in word:
        print '%s\t%s' % (word, 1)

```

当有输入到**标准输入**时，该脚本循环——该输入将从连接映射器任务和 Python 运行时的 Java 虚拟机中输入。对于每一行，我们清除任何前导或尾随空白，然后将该行拆分成单词。对于每个单词，如果它包含字符串“dfs”，我们将其打印到 **stdout** 。

将制表符分隔的输出写入 **stdout** 相当于在 Java MapReduce 中写入 Context 它写入将被馈送到 Reduce 的中间输出。

流缩减器比 Java 缩减器稍微复杂一点，因为输入是以单个键值对的形式出现的，而不是带有值集合的键。但是 Hadoop 保证进入缩减器的数据将被排序仍然成立，所以流缩减器知道当一个键改变时，前一个键的输入是完整的。

代码清单 23 显示了 Python 中的减速器代码。

代码清单 23:Python 中的字数缩减器

```java
  #!/usr/bin/env python

  import sys

  last_match = None
  total_count = 0

  for line in sys.stdin:
      line = line.strip()
      match, count = line.split('\t')
      count = int(count)

      if not last_match:
          last_match = match

      if last_match == match:
          total_count += count
      else:
          print '%s\t%s' %
  (last_match, total_count)
          total_count = 1
          last_match = match

  print '%s\t%s' % (last_match,
  total_count)

```

代码通过`stdin`循环，但是因为它是一个缩减器，它将从映射器接收中间输出。缩减器是有状态的，当它接收特定键的输入时，它将增加运行总数。当键改变时，减速器写出前一个键的总数并重置其状态。

通过将 Linux `cat`和`sort`命令与对 Python 脚本的调用链接在一起，我们可以模仿 Hadoop 的行为，并验证脚本是否正常工作。Python 安装在 *Hadoop 简洁的* Docker 容器上，本章的 Python 脚本可以在/python 目录中找到。我们可以使用代码清单 24 中的命令运行它们，它也显示了缩写输出。

 24:验证 Python 脚本

```java
  # cat $HADOOP_HOME/etc/hadoop/* |
  /python/mapper.py | sort | /python/reducer.py
  "dfs" 3
  #*.sink.ganglia.tagsForPrefix.dfs=  1
  ...

```

为了提交 Python 作业，我们使用与驱动程序相同的 Hadoop-streaming.jar 存档，但是我们还需要使用文件参数来指定我们想要运行的脚本的路径。Hadoop 会将这些文件复制到 HDFS，以便运行任务的任何节点都可以使用它们。代码清单 25 显示了完整的流作业提交。

代码清单 25:提交流 Python 作业

```java
  hadoop jar
  $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-2.7.2.jar \
   -input input \
   -output output4 \
   -mapper mapper.py \
   -reducer reducer.py \
   -file /python/mapper.py \
   -file /python/reducer.py

```

在 **jar** 命令中，我们指定了一个输出目录，减速器的结果将被写入该目录。像以前一样，我们可以使用 Hadoop**fs–cat**查看组合结果，如代码清单 26 所示。

代码清单 26:流 Python 作业的结果

```java
  # hadoop fs -cat output4/*
  "dfs" 3
  #*.sink.ganglia.tagsForPrefix.dfs=  1
  #dfs.class=org.apache.hadoop.metrics.file.FileContext 1

```

为了。NET 程序员，微软提供了一个模仿 Hadoop Java API 的库，让我们可以在其中构建 MapReduce 作业。NET，使用类似于 Java 中使用的结构。映射器和缩减器都有基类，我们可以用键和值对以类型安全的方式工作。

然而，这只是 Hadoop Streaming 的一个语法包装——最终，. NET 项目被构建为一个可执行文件，它以与普通控制台应用相同的方式通过标准输入和输出流进行读写。那个。NET API 也有一段时间没有刷新了，这意味着您必须决定依赖一个旧的 API 是否值得您从包装流接口中获得的价值。

在本章中，我们将继续讨论 Hadoop 流和。NET 源代码在 GitHub 上的[六眼/Hadoop-简洁地说/dotnet](https://github.com/sixeyed/hadoop-succinctly/tree/master/dotnet) ，它有两个控制台应用——一个用于映射器，一个用于减速器。代码与 Python 变体基本相同——将为每个任务创建一个映射器实例，并将输入文件中的行输入到 **stdin** 中。

映射器类中的`Main()`方法循环通过 **stdin** ，同时有更多的行，如代码清单 27 中的代码。

 27:读取输入。网络映射器

```java
          static void Main(string[]
  args)
          {
              string line;
              while ((line =
  Console.ReadLine()) != null)
              {
                  line = line.Trim();
                  //...                   

              }
          }

```

当收到非空行时，映射器将输入拆分为空格字符，并检查包含搜索字符串的单词的元素，如代码清单 28 所示。

 28:该。网络映射器

```java
          var words = line.Split('
  ');
          foreach (var word in words)
          {
              if
  (word.Contains("dfs"))
               {

  Console.WriteLine(string.Format("{0}\t1", word));
              }
          }                    

```

将键值对发送到上下文是用制表符分隔的键值格式化字符串并用`Console.WriteLine()`写入字符串的情况。

reducer 类以同样的方式工作，通过控制台输入流循环的`Main()`方法。缩减器的整体输入将与 Hadoop 向 Java 缩减器提供的排序和合并输入相同，但它将被展平，以便值集合中的每个项目都导致缩减器的另一行输入。

代码清单 29 显示了 reducer 的`Main()`方法，其中读取循环与映射器相同，但是我们初始化了一些变量来维护循环中的状态。

 29:读取输入。网络减速器

```java
          static void Main(string[]
  args)
          {
              string lastMatch =
  null;
              int totalCount = 0;
              string line;

              while ((line =
  Console.ReadLine()) != null)
              {
                  line = line.Trim();
                  //...
          }

```

在读循环中，功能与 Python 脚本中的相同——为每个键保留一个运行计数，当键发生变化时，发出先前的计数并重置它。代码清单 30 显示了。NET 减速器实现。

 30:该。网络减速器

```java
      var parts = line.Split('\t');
      var match = parts[0];
      var count =
  int.Parse(parts[1]);                
      if (lastMatch == null)
      {
          lastMatch = match;
      }
      if (lastMatch == match)
      {
          totalCount += count;
      }
      else
      {
          Console.WriteLine(string.Format("{0}\t{1}",
  lastMatch, totalCount));
          totalCount = 1;
          lastMatch = match;
      }
      Console.WriteLine(string.Format("{0}\t{1}",
  lastMatch, totalCount));

```

为了跑步。NET 程序，您要么需要一台带有。NET 框架安装或者可以使用跨平台。在任何运行时。在一个 Windows 集群上运行，您以通常的方式将流程序提交给 Hadoop，指定您想要运行的可执行程序的名称，并使用文件参数发送二进制文件。

代码清单 31 显示了提交命令和。网络流作业。

 31:提交流。网络作业

```java
  hadoop jar
  %HADOOP_HOME%\share\hadoop\tools\lib\hadoop-streaming-2.7.2.jar \
   -mapper "c:\dotnet\Sixeyed.HadoopSuccinctly.Streaming.Mapper.exe"
  \
   -reducer
  "c:\dotnet\Sixeyed.HadoopSuccinctly.Streaming.Reducer.exe" \
   -input input \
   -output output4

```

借助流接口，我们使用自己选择的语言构建相同类型的 MapReduce 组件。不需要显式的驱动程序，因为 Hadoop 安装提供的`hadoop-streaming.jar`充当驱动程序。我们也可以用我们选择的语言编写映射器和缩减器，并且我们可以包含我们通常使用的任何单元测试工具。

因为流可执行文件只是控制台应用，所以我们也可以使用输入数据的子集运行集成测试——将单个文件馈送到映射器并捕获输出，该输出将作为减速器的输入进行馈送。而且，因为运行连接 Hadoop 和我们的流程的 Java 虚拟机会产生成本，所以使用流的决定将包括性能考虑。

然而，要注意流媒体应用的互操作性。Hadoop 和您的可执行文件之间的协议总是以字符串的形式传递数据，因此，如果您试图将非 Java 映射器与 Java reducer 混合，您必须确保您的映射器以预期的格式序列化键和值输出，以便 reducer 可以反序列化。

流媒体应用的另一个重要考虑是确保可执行文件可以在数据节点上运行。通常，这意味着在服务器调试期间设置可执行平台——如果您知道您将使用 Python 或。NET 框架，您需要安装所需的版本作为安装的一部分。

如果在可执行程序中使用额外的库，您需要确保它们在任务运行的节点上可用。您可以使用与 Hadoop 提供的相同的依赖传递方法，用 Java MapReduce 程序分发 JAR 库。`hadoop-streaming.jar`支持`archive`参数，该参数允许您指定一个本地包与您的 MapReduce 程序一起发送。

如果您的可执行文件有库依赖项，您可以将它们打包成一个 ZIP 包，并在作业的`archive`参数中指定该 ZIP 文件，如代码清单 32 所示。

 32:提交具有相关性的流式作业

```java
  hadoop jar %HADOOP_HOME%\share\hadoop\tools\lib\hadoop-streaming-2.7.2.jar
  \
   -input input \
   -output output4 \
   -mapper /lib/mapper-with-depdencies.exe
  \
   -reducer /lib/reducer-with-dependencies.exe
  \
   -archive c:\dotnet\dependencies.zip

```

| ![](img/00010.gif) | 注意:您不需要为流式作业发送可执行文件；您可以先将它们复制到 HDFS，然后在 mapper 和 reducer 参数中指定相对 HDFS 路径，如本例所示。 |

当此作业运行时，Hadoop 将以高复制因子(默认值为 10)将归档文件复制到 HDFS，目标是它已经位于运行作业任务的节点的本地。当任务运行时，它将归档文件提取到作业的工作目录中，这样您的可执行文件将在当前目录中找到它的依赖项。

当一个 Java MapReduce 任务在 Hadoop 中的一个节点上运行时，它首先创建一个 Java 虚拟机来运行构建映射器或 Reduce 的 JAR 文件。对于流应用，没有 Java 代码，但是 JVM 仍然会被创建，除非您使用系统命令，否则可执行文件还需要引导自己的运行时。加载可执行运行时和 JVM 的成本将影响作业的整体运行时。

评估这个成本可能很困难，因为您不能使用典型的插装技术——当您的插装代码运行时，运行时已经被加载，并且成本已经发生。这种差异通常并不重要，因为您选择使用流是出于非功能性的原因(拥有想要重用的现有逻辑，或者在非 Java 平台中拥有更丰富的经验)。然而，了解做这个决定的成本总是有用的。

通过对运行在不同平台上的类似 MapReduce 程序进行基准测试，我们可以得到一个合理的成本概念。表 2 显示了通过 Java、Python 和. NET 运行字数统计 MapReduce 的结果

| 运行时间 | 平均总地图时间 | 平均总减少时间 | 平均总作业时间 | 与 Java 基线的差异 |
| --- | --- | --- | --- | --- |
| Java 语言(一种计算机语言，尤用于创建网站) | One hundred and two point three | Thirty-one point seven | One hundred and thirty-four | Zero |
| 计算机编程语言 | One hundred and twelve point nine | Thirty-four | One hundred and forty-six point nine | Twelve point nine |
| 。网 | One hundred and eight point two | Thirty-three point three | One hundred and forty-one point six | Seven point six |

表 MapReduce 程序的计算时间

对于这个基准测试，我使用了 MapReduce 程序，所有这些程序都在 GitHub 上，我对每个作业运行了五次，以获得一个合理的平均值。因为。NET 程序使用了完整的框架，它必须在 Windows 上运行，所以我在同一个单节点 Syncfusion 大数据工作室安装上运行了所有的作业(第 7 章将详细介绍)。

我们可以看到，与原生 MapReduce 相比，使用流会产生影响——流作业需要 5-10%的时间，这取决于运行时。如果您考虑在大部分 Hadoop 访问中使用流，这是需要注意的事情。这种影响不足以影响流式传输，但是时间关键的工作可能需要用 Java 来完成。

Hadoop 是一个 Java 平台，虽然 Java 是运行 MapReduce 作业的原生语言，但是 Hadoop 对任何能够产生可执行组件的平台都有很强的支持。这允许您构建 MapReduce 程序，其中映射器和 reducer 以您喜欢的任何语言编写，前提是您可以用使用平台所需的所有先决条件来配置您的 Hadoop 节点。

在本章中，我们将学习如何使用 Python 和。NET 框架。流接口使用一个简单的协议，其中数据以制表符分隔的键值对的形式逐行输入到 **stdin** 中，Hadoop 期望输出以相同的格式输入到 **stdout** 中。

尽管您应该了解类型安全和 Hadoop Streaming 的性能影响，但与利用您的首选平台和任何现有技术资产的能力相比，这两个问题通常都是微不足道的。Hadoop Streaming 支持将依赖项传送到节点，这意味着您可以为映射器和缩减器构建复杂的可执行文件，并让它们在分布式节点上毫无问题地运行。

Hadoop 流接口只是 Hadoop 跨平台支持的一个方面。在下一章中，我们将更仔细地研究 Hadoop 集群中的移动部分。在第 7 章中，我们将看到不同的 Hadoop 发行版如何在不同的平台上运行。