Hadoop2 学习手册(三)
八、数据生命周期管理
我们前面的章节非常关注技术,描述了特定的工具或技术以及如何使用它们。 在本章和下一章中,我们将采取更自上而下的方法,描述您可能遇到的问题空间,然后探索如何解决它。 我们将特别介绍以下主题:
- 我们所说的术语数据生命周期管理的含义是什么
- 为什么需要考虑数据生命周期管理
- 可用于解决问题的工具类别
- 如何使用这些工具构建 Twitter 情绪分析管道的前半部分
什么是数据生命周期管理
数据不只存在于某个时间点。 特别是对于长期运行的生产工作流,您可能会在 Hadoop 集群中获取大量数据。 需求很少会长时间保持不变,因此除了新逻辑之外,您可能还会看到该数据的格式发生了变化,或者需要使用多个数据源来提供在应用中处理的数据集。 我们使用术语数据生命周期管理来描述一种处理数据收集、存储和转换的方法,该方法可确保数据处于需要的位置,采用其需要的格式,并允许数据和系统随时间演变。
数据生命周期管理的重要性
如果构建数据处理应用,则根据定义,您依赖于处理的数据。 正如我们考虑应用和系统的可靠性一样,有必要确保数据也是生产就绪的。
在某些情况下,需要将数据吸收到 Hadoop 中。 它是企业的一部分,通常与外部系统有多个集成点。 如果来自这些系统的数据获取不可靠,则对处理该数据的作业的影响通常与重大系统故障一样具有破坏性。 数据接收成为本身的一个关键组件。 当我们说摄取需要可靠时,我们不仅仅是指数据正在到达;它还必须以一种可用的格式到达,并通过一种能够处理随时间演变的机制。
其中许多问题的问题在于,除非流量很大,系统很关键,而且任何问题的业务影响都不是微不足道的,否则它们不会以显著的方式出现。 对于不太关键的数据流有效的临时方法通常不会进行扩展,但在活动系统上进行替换会非常痛苦。
帮助工具
但是不要惊慌! 有许多类别的工具可以帮助解决数据生命周期管理问题。 在本章中,我们将提供以下三大类别的示例:
- 编排服务:构建接收管道通常有多个独立的阶段,我们将使用编排工具来描述、执行和管理这些阶段
- 连接器:鉴于与外部系统集成的重要性,我们将了解如何使用连接器来简化 Hadoop 存储提供的抽象
- 文件格式:我们存储数据的方式会影响我们管理格式随时间演变的方式,有几种丰富的存储格式可以支持这一点
构建推文分析能力
在前面的章节中,我们使用了 Twitter 数据分析的各种实现来描述几个概念。 我们将把这一能力推向更深层次,并将其作为一个主要案例进行研究。
在本章中,我们将构建一条数据接收管道,构建一个在设计时考虑到可靠性和未来发展的生产就绪数据流。
我们将在本章中逐步构建管道。 在每个阶段,我们将强调哪些内容发生了变化,但不能在没有将章的大小增加两倍的情况下包含每个阶段的完整清单。 然而,本章的源代码包含了每一次迭代的全部内容。
获取推文数据
我们需要做的第一件事是获取实际的 tweet 数据。 与前面的示例一样,我们可以将-j和-n参数传递给stream.py,以将 JSON tweet 转储到 stdout:
$ stream.py -j -n 10000 > tweets.json
因为我们有这个工具可以按需创建一批示例 tweet,所以我们可以通过定期运行此作业来开始我们的接收管道。 但如何做到呢?
介绍 Oozie
当然,我们可以将块放在一起,并使用类似 cron 的东西来进行简单的作业调度,但请记住,我们需要一个考虑到可靠性的接收管道。 因此,我们非常需要一种可以用来检测故障并以其他方式响应异常情况的调度工具。
我们将在这里使用的工具是 Oozie(Hadoop),这是一个关注 oozie.apache.org 生态系统的工作流引擎和调度器。
Oozie 提供了一种将工作流定义为一系列节点的方法,这些节点具有可配置的参数和从一个节点到下一个节点的受控转换。 它是作为 Cloudera QuickStart VM 的一部分安装的,主命令行客户机名为oozie,这并不奇怪。
备注
我们已经针对 Cloudera QuickStart VM 的 5.0 版本测试了本章中的工作流,在撰写 Oozie 的最新版本 5.1 时,它存在一些问题。 然而,我们的工作流中没有特定于版本的东西,因此它们应该与任何正确工作的 Ooziev4 实现兼容。
虽然 Oozie 功能强大且灵活,但它可能需要一点时间才能适应,所以我们将举几个例子,并描述我们在此过程中正在做些什么。
Oozie 工作流中最常见的节点是操作。 在动作节点中实际执行工作流的步骤;其他节点类型在决策、并行性和故障检测方面处理工作流的管理。 Oozie 可以执行多种类型的操作。 其中之一是 shell 操作,它可用于在系统上执行任何命令,例如本机二进制文件、shell 脚本或任何其他命令行实用程序。 让我们创建一个脚本来生成 tweet 文件,并将其复制到 HDFS:
set -e
source twitter.keys
python stream.py -j -n 500 > /tmp/tweets.out
hdfs dfs -put /tmp/tweets.out /tmp/tweets/tweets.out
rm -f /tmp/tweets.out
请注意,如果包含的任何命令失败,第一行将导致整个脚本失败。 我们使用环境文件为twitter.keys中的脚本提供 Twitter 密钥,其格式如下:
export TWITTER_CONSUMER_KEY=<value>
export TWITTER_CONSUMER_SECRET=<value>
export TWITTER_ACCESS_KEY=<value>
export TWITTER_ACCESS_SECRET=<value>
Oozie 使用 XML 描述其工作流,通常存储在名为workflow.xml的文件中。 让我们来看看调用 shell 命令的 Oozie 工作流的定义。
Oozie 工作流的模式称为Workflow-app,我们可以为该工作流指定一个特定的名称。 在 CLI 或 Oozie Web 用户界面中查看作业历史记录时,这很有用。 在本书的示例中,我们将使用递增的版本号,以便更容易地分离源库中的迭代。 下面是我们为工作流应用指定特定名称的方式:
<workflow-app name="v1">
Oozie 工作流由一系列相连的节点组成,每个节点代表流程中的一个步骤,并由工作流定义中的 XML 节点表示。 Oozie 有许多节点处理工作流从一个步骤到下一个步骤的过渡。 其中第一个节点是 Start 节点,它只说明要作为工作流一部分执行的第一个节点的名称,如下所示:
<start to="fs-node"/>
然后,我们就有了命名开始节点的定义。 在本例中,它是一个动作节点,它是实际执行某些处理的大多数 Oozie 节点的泛型节点类型,如下所示:
<action name="fs-node">
Action 是一个广泛的节点类别,然后我们通常会对它进行专门化,并针对此给定节点进行特定处理。 在本例中,我们使用 fs 节点类型,它允许我们执行文件系统操作:
<fs>
我们希望确保要将 tweet 数据文件复制到的 HDFS 上的目录存在、为空,并且具有适当的权限。 为此,我们尝试删除目录(如果存在),然后创建它,最后应用所需的权限,如下所示:
<delete path="${nameNode}/tmp/tweets"/>
<mkdir path="${nameNode}/tmp/tweets"/>
<chmod path="${nameNode}/tmp/tweets" permissions="777"/>
</fs>
稍后我们将看到另一种设置目录的方法。 执行完节点的功能后,Oozie 需要知道如何继续工作流。 在大多数情况下,如果此节点成功,这将包括移动到另一个操作节点,否则将中止工作流。 这是由下面的元素指定的。 Ok 节点给出执行成功时要转换到的节点的名称;错误节点为失败情况命名目标节点。 下面是 OK 和 FAIL 节点的使用方式:
<ok to="shell-node"/>
<error to="fail"/>
</action>
<action name="shell-node">
第二个操作节点再次使用其特定的处理类型进行专门化;在本例中,我们有一个 shell 节点:
<shell >
然后,shell 操作将指定 Hadoop JobTracker 和 NameNode 位置。 请注意,实际值是由变量给出的;我们稍后将解释它们的来源。 JobTracker 和 NameNode 指定如下:
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
正如在第 3 章,Processing-MapReduce 和 Beyond中提到的,MapReduce 使用多个队列为不同的资源调度方法提供支持。 下一个元素指定工作流应该提交到的 MapReduce 队列:
<configuration>
<property>
<name>mapred.job.queue.name</name>
<value>${queueName}</value>
</property>
</configuration>
现在 shell 节点已完全配置,我们可以再次通过变量指定要调用的命令,如下所示:
<exec>${EXEC}</exec>
Oozie 工作流的各个步骤作为 MapReduce 作业执行。 因此,此 shell 操作将作为特定 TaskTracker 上的特定任务实例执行。 因此,在执行操作之前,我们需要指定需要将哪些文件复制到 TaskTracker 计算机上的本地工作目录。 在本例中,我们需要复制主 shell 脚本、Python tweet 生成器和 Twitter 配置文件,如下所示:
<file>${workflowRoot}/${EXEC}</file>
<file>${workflowRoot}/twitter.keys</file>
<file>${workflowRoot}/stream.py</file>
关闭 shell 元素后,根据操作是否成功完成,我们再次指定要执行的操作。 由于 MapReduce 用于作业执行,因此根据定义,大多数节点类型都有内置的重试和恢复逻辑,尽管 shell 节点并非如此:
</shell>
<ok to="end"/>
<error to="fail"/>
</action>
如果工作流失败,我们就在这种情况下终止它。 kill节点类型就是这样做的-终止工作流,使其无法继续执行任何进一步的步骤,通常会在此过程中记录错误消息。 下面是kill节点类型的使用方法:
<kill name="fail">
<message>Shell action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]</message>
</kill>
另一方面,end节点只是暂停工作流,并将其记录为在 Oozie 中成功完成:
<end name="end"/>
</workflow-app>
显而易见的问题是,前面的变量代表什么,它们从哪里获得具体的值。 前面的变量是通常称为 EL 的 Oozie 表达式语言的示例。
除了描述流中步骤的工作流定义文件(workflow.xml)之外,我们还需要创建一个配置文件,该文件为给定的工作流执行提供特定值。 这种功能和配置的分离使我们可以编写可在不同集群、不同文件位置或不同变量值上使用的工作流,而不必重新创建工作流本身。 按照惯例,此文件通常命名为job.properties。 对于前面的工作流,这里有一个示例job.properties文件。
首先,我们指定要向其提交工作流的 JobTracker、NameNode 和 MapReduce 队列的位置。 以下操作应该可以在 Cloudera 5.0 QuickStart VM 上运行,尽管在 5.1 版中,主机名已更改为quickstart.cloudera。 重要的是,指定的 NameNode 和 JobTracker 地址需要在 Oozie 白名单中-VM 上的本地服务是自动添加的:
jobTracker=localhost.localdomain:8032
nameNode=hdfs://localhost.localdomain:8020
queueName=default
接下来,我们为工作流定义和相关文件在 HDFS 文件系统上的位置设置一些值。 请注意,使用了表示运行作业的用户名的变量。 这允许将单个工作流应用于不同的路径,具体取决于提交用户,如下所示:
tasksRoot=book
workflowRoot=${nameNode}/user/${user.name}/${tasksRoot}/v1
oozie.wf.application.path=${nameNode}/user/${user.name}/${tasksRoot}/v1
接下来,我们将工作流中要执行的命令命名为${EXEC}:
EXEC=gettweets.sh
更复杂的工作流将需要job.properties文件中的其他条目;前面的工作流非常简单。
oozie命令行工具需要知道 Oozie 服务器在哪里运行。 这可以作为参数添加到每个 Oozie shell 命令中,但很快就会变得很笨拙。 相反,您可以设置 shell 环境变量,如下所示:
$ export OOZIE_URL='http://localhost:11000/oozie'
完成所有这些工作之后,我们现在可以实际运行 Oozie 工作流了。 按照job.properties文件中的值中指定的方式在 HDFS 上创建一个目录。 在前面的命令中,我们将在 HDFS 上的主目录下将其创建为book/v1。 将stream.py、gettweets.sh和twitter.properties文件复制到该目录;这些文件是执行 shell 命令实际执行所需的文件。 然后,将workflow.xml文件添加到同一目录。
然后,要运行工作流,我们需要执行以下操作:
$ oozie job -run -config <path-to-job.properties>
如果提交成功,Oozie 会将作业名称打印到屏幕上。 您可以使用以下命令查看此工作流的当前状态:
$ oozie job -info <job-id>
您还可以检查作业的日志:
$ oozie job -log <job-id>
此外,可以使用以下命令查看所有当前和最近的职务:
$ oozie jobs
关于 HDFS 文件权限的说明
Shell 命令中有一个微妙的方面,可以捕捉粗心大意的人。 作为拥有fs节点的替代方案,我们可以在 shell 节点中包含一个准备元素,以便在文件系统上创建所需的目录。 它将如下所示:
<prepare>
<mkdir path="${nameNode}/tmp/tweets"/>
</prepare>
准备阶段由提交工作流的用户执行,但由于实际脚本执行是在 Yarn 上执行的,因此通常以 Yarn 用户的身份执行。 您可能会遇到这样的问题:脚本生成 tweet,在 HDFS 上创建/tmp/tweets目录,但脚本随后无法拥有写入该目录的权限。 您可以通过更精确地分配权限来解决这个问题,或者,如前所述,您可以添加一个文件系统节点来封装所需的操作。 在本章中,我们将混合使用这两种技术;对于非 shell 节点,我们将使用 Prepare 元素,特别是当所需的目录仅由该节点操作时。 对于涉及 shell 节点的情况,或者创建的目录将跨多个节点使用的情况,我们将安全地使用更显式的fs节点。
让开发变得更容易
在开发期间管理 Oozie 作业的文件和资源有时会变得很笨拙。 有些文件需要在 HDFS 上,而有些文件需要在本地,对某些文件的更改需要对其他文件进行更改。 最简单的方法通常是开发或更改本地文件系统上的工作流目录的完整克隆,并将更改从那里推送到 HDFS 中名称相似的目录,当然,不要忘记要确保所有更改都在修订控制之下! 对于工作流的操作执行,job.properties文件是唯一需要位于本地文件系统上的文件,反之,所有其他文件都需要位于 HDFS 上。 请始终记住这一点:对工作流的本地副本进行更改、忘记将更改推送到 HDFS、然后弄不清工作流为什么没有反映这些更改太容易了。
提取数据并摄取到 Hive 中
有了个 HDFS 上的数据,我们现在可以为条 tweet 和用户提取单独的数据集,并像前几章一样放置数据。 我们可以重用extract_for_hive.pig将原始 tweet JSON 解析成单独的文件,再次将它们存储在 HDFS 上,然后执行一个配置单元步骤,该步骤将这些新文件摄取到配置单元表中,以获取 tweet、用户和地点。
要在 Oozie 中做到这一点,我们需要在工作流中添加两个新节点,第一步的 Pig 操作和第二步的 Have 操作。
对于我们的配置单元操作,我们只创建三个指向 Pig 生成的文件的外部表。 然后,这将允许我们遵循前面描述的摄取临时表或外部表的模型,并使用 HiveQLINSERT语句从临时表或外部表中插入可操作的(通常是分区的)表。 此create.hql脚本可以在github.com/learninghad…中找到,但其形式如下所示:
CREATE DATABASE IF NOT EXISTS twttr ;
USE twttr;
DROP TABLE IF EXISTS tweets;
CREATE EXTERNAL TABLE tweets (
...
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE
LOCATION '${ingestDir}/tweets';
DROP TABLE IF EXISTS user;
CREATE EXTERNAL TABLE user (
...
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE
LOCATION '${ingestDir}/users';
DROP TABLE IF EXISTS place;
CREATE EXTERNAL TABLE place (
...
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE
LOCATION '${ingestDir}/places';
请注意,每个表上的文件分隔符也被显式设置为与我们从 Pig 输出的内容相匹配。 除了之外,两个脚本中的位置都由变量指定,我们将在job.properties文件中为这些变量提供具体的值。
使用前面的语句,我们可以为在源代码中找到的工作流创建 Pig 节点,作为管道的 v2。 节点定义的大部分看起来与前面使用的 shell 节点类似,因为我们设置了相同的配置元素;还要注意我们使用prepare元素创建所需的输出目录。 我们可以为工作流创建 Pig 节点,如以下action所示:
<action name="pig-node">
<pig>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<prepare>
<delete path="${nameNode}/${outputDir}"/>
<mkdir path="${nameNode}/${outputDir}"/>
</prepare>
<configuration>
<property>
<name>mapred.job.queue.name</name>
<value>${queueName}</value>
</property>
</configuration>
与 shell 命令类似,我们需要告诉 Pig 操作实际 Pig 脚本的位置。 这在以下script元素中指定:
<script>${workflowRoot}/pig/extract_for_hive.pig</script>
我们还需要修改用于调用 Pig 脚本的命令行,以添加个参数。 以下元素执行此操作;请注意构造模式,其中一个元素添加实际参数名称,下一个元素添加其值(我们将在下一节中看到另一种传递参数的机制):
<argument>-param</argument>
<argument>inputDir=${inputDir}</argument>
<argument>-param</argument>
<argument>outputDir=${outputDir}</argument>
</pig>
由于我们要从此步骤移至配置单元节点,因此需要适当设置以下元素:
<ok to="hive-node"/>
<error to="fail"/>
</action>
配置单元操作本身与前面的节点略有不同;尽管它以类似的方式启动,但它指定了特定于配置单元操作的命名空间,如下所示:
<action name="hive-node">
<hive >
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
配置单元操作需要配置单元本身使用的许多配置元素,并且在大多数情况下,我们将hive-site.xml文件复制到工作流目录并指定其位置,如以下 XML 所示;请注意,此机制不是配置单元特定的,也可用于自定义操作:
<job-xml>${workflowRoot}/hive-site.xml</job-xml>
此外,我们可能需要覆盖某些 MapReduce 默认配置属性,如以下 XML 所示,其中我们指定作业应使用中间压缩:
<configuration>
<property>
<name>mapred.compress.map.output</name>
<value>true</value>
</property>
</configuration>
配置配置单元环境后,我们现在指定配置单元脚本的位置:
<script>${workflowRoot}/hive/create.hql</script>
我们还让到提供将参数传递给配置单元脚本的机制。 但是,我们不是一次构建一个组件的命令行,而是添加将job.properties文件中的配置元素名称映射到配置单元脚本中指定的变量的param元素;Pig 操作也支持此机制:
<param>dbName=${dbName}</param>
<param>ingestDir=${ingestDir}</param>
</hive>
然后,Hive 节点与其他节点一样关闭,如下所示:
<ok to="end"/>
<error to="fail"/>
</action>
我们现在需要将所有这些放在一起,以便在 Oozie 中运行多阶段工作流。 完整的workflow.xml文件可在github.com/learninghad…中找到,工作流程如下图所示:
数据接收工作流 v2
此工作流执行前面讨论的所有步骤;它生成 tweet 数据,通过 Pig 提取数据子集,然后将这些数据摄取到配置单元中。
关于工作流目录结构的说明
我们现在的工作流目录中有相当多的文件,最好采用一些结构和命名约定。 对于当前工作流,我们在 HDFS 上的目录如下所示:
/hive/
/hive/create.hql
/lib/
/pig/
/pig/extract_for_hive.pig
/scripts/
/scripts/gettweets.sh
/scripts/stream-json-batch.py
/scripts/twitter-keys
/hive-site.xml
/job.properties
/workflow.xml
我们遵循的模型是将配置文件保存在顶级目录中,但将与给定操作类型相关的文件保存在专用子目录中。 请注意,拥有一个lib目录是很有用的,即使它是空的,因为有些节点类型会查找它。
使用前面的结构,我们组合作业的job.properties文件现在如下所示:
jobTracker=localhost.localdomain:8032
nameNode=hdfs://localhost.localdomain:8020
queueName=default
tasksRoot=book
workflowRoot=${nameNode}/user/${user.name}/${tasksRoot}/v2
oozie.wf.application.path=${nameNode}/user/${user.name}/${tasksRoot}/v2
oozie.use.system.libpath=true
EXEC=gettweets.sh
inputDir=/tmp/tweets
outputDir=/tmp/tweetdata
ingestDir=/tmp/tweetdata
dbName=twttr
在前面的代码中,我们已经完全更新了workflow.xml定义,以包含到目前为止描述的所有步骤-包括创建所需目录的初始fs节点,而无需担心用户权限。
HCatalog 简介
如果我们看看我们当前的工作流程,我们如何使用 HDFS 作为 Pig 和 Have 之间的接口是低效的。 我们需要将 Pig 脚本的结果输出到 HDFS 上,然后配置单元脚本可以将其用作一些新表的位置。 这突出表明,将数据存储在配置单元中通常非常有用,但这是有限的,因为很少有工具(主要是配置单元)可以访问配置单元元存储,从而读取和写入此类数据。 仔细想想,Hive 有两个主要层:用于访问和操作其数据的工具,以及对该数据运行查询的执行框架。
Hive 的 HCatalog 子项目有效地提供了这些层中第一层的独立实现-访问和操作 Hive 转储中的数据的方法。 HCatalog 为其他工具(如 Pig 和 MapReduce)提供了本机读写存储在 HDFS 上的表结构数据的机制。
当然,请记住,数据以一种或另一种格式存储在 HDFS 上。 配置单元元存储提供了将这些文件抽象到配置单元熟悉的关系表结构中的模型。 因此,当我们说我们在 HCatalog 中存储数据时,我们真正的意思是我们在 HDFS 上存储数据,这样这些数据就可以通过配置单元元存储中指定的表结构公开。 相反,当我们提到配置单元数据时,我们真正指的是其元数据存储在配置单元元数据中,并且可以由任何元数据感知工具(如 HCatalog)访问的数据。
使用 HCatalog
HCatalog 命令行工具称为HCAT,它将预装在 Cloudera QuickStart VM 上-实际上,它与任何 0.11 以后的配置单元版本一起安装。
hcat实用程序没有交互模式,因此通常将其与显式命令行参数一起使用,或者将其指向命令文件,如下所示:
$ hcat –e "use default; show tables"
$ hcat –f commands.hql
尽管 HCAT 工具很有用并且可以合并到脚本中,但是对于我们这里的目的来说,HCatalog 更有趣的元素是它与 Pig 的集成。 HCatalog 定义了一个名为HCatLoader的新 Pig 加载器和一个名为HCatStorer的存储器。 顾名思义,它们允许 Pig 脚本直接读取或写入配置单元表。 我们可以使用此机制将 Oozie 工作流中以前的 Pig 和 Have 操作替换为一个基于 HCatalog 的 Pig 操作,该操作将 Pig 作业的输出直接写入到 Have 中的表中。
为清楚起见,我们将创建名为tweets_hcat、places_hcat和users_hcat的新表,并将此数据插入其中;请注意,这些表不再是外部表:
CREATE TABLE tweets_hcat…
CREATE TABLE places_hcat …
CREATE TABLE users_hcat …
请注意,如果脚本文件中包含这些命令,则可以使用 HCAT CLI 工具执行它们,如下所示:
$ hcat –f create.hql
然而,HCAT CLI 工具没有提供类似于配置单元 CLI 的交互式 shell。 我们现在可以使用前面的 Pig 脚本,只需要更改存储命令,用HCatStorer替换PigStorage。 因此,我们更新的 Pig 脚本extract_to_hcat.pig包括如下store命令:
store tweets_tsv into 'twttr.tweets_hcat' using org.apache.hive.hcatalog.pig.HCatStorer();
注意,HCatStorer类的包名有org.apache.hive.hcatalog前缀;当 HCatalog 在 Apache 孵化器中时,它使用org.apache.hcatalog作为它的包前缀。 这个旧的表单现在已经过时了,应该使用显式地将 HCatalog 显示为配置单元的子项目的新表单。
有了这个新的 Pig 脚本,我们现在可以使用 HCatalog 将以前的 Pig 和 Have 操作替换为更新后的 Pig 操作。 这还需要首次使用 oozie sharelib,我们将在下一节讨论这一点。 在我们的工作流定义中,此操作的pig元素的定义如以下 XML 所示,并且可以在源包中找到作为管道的 v3;在 v3 中,我们还添加了一个实用工具配置单元节点,以在 Pig 节点之前运行,以确保在执行需要它们的 Pig 脚本之前,所有必需的表都已存在。
<pig>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<job-xml>${workflowRoot}/hive-site.xml</job-xml>
<configuration>
<property>
<name>mapred.job.queue.name</name>
<value>${queueName}</value>
</property>
<property>
<name>oozie.action.sharelib.for.pig</name>
<value>pig,hcatalog</value>
</property>
</configuration>
<script>${workflowRoot}/pig/extract_to_hcat.pig
</script>
<argument>-param</argument>
<argument>inputDir=${inputDir}</argument>
</pig>
值得注意的两个更改是添加了对hive-site.xml文件的显式引用;这是 HCatalog 所必需的,以及新的配置元素,它告诉 Oozie 包含所需的HCatalogjar。
Oozie Sharelib
上一次添加的涉及到我们到目前为止还没有提到的 Oozie 的一个重要方面:Ooziesharelib。 当 Oozie 运行其所有不同的操作类型时,它需要多个 JAR 来访问 Hadoop 并调用各种工具,如 Have 和 Pig。 作为 Oozie 安装的一部分,已经在 HDFS 上放置了大量依赖 JAR,供 Oozie 及其各种操作类型使用:这就是 Ooziesharelib。
对于 Oozie 的大多数用法,只要知道sharelib存在就足够了,通常在/user/oozie/share/lib on HDFS下,当需要添加一些显式配置值时(如前面的示例所示)。 当使用 Pig 操作时,Pig Jars 将被自动拾取,但是当 Pig 脚本使用诸如 HCatalog 之类的东西时,Oozie 将不会显式知道此依赖项。
Oozie CLI 允许操作sharelib,尽管需要这样做的场景超出了本书的范围。 不过,要查看 ooziesharelib中包含哪些组件,以下命令可能很有用:
$ oozie admin -shareliblist
以下命令可用于查看包含sharelib中特定组件的各个 JAR,在本例中为 HCatalog:
$ oozie admin -shareliblist hcat
这些命令可用于验证是否包含了所需的 JAR,以及查看正在使用的特定版本。
HCatalog 和分区表
如果您第二次重新运行上一个工作流,它将失败;深入查看日志,您将看到 HCatalog 抱怨它无法写入已包含数据的表。 这是 HCatalog 的当前限制;默认情况下,它将表和表中的分区视为不可变的。 另一方面,HIVE 将向表或分区添加新数据;它的默认表视图是可变的。
对配置单元和 HCatalog 即将进行的更改将看到一个新表属性的支持,该属性将在这两个工具中控制此行为;例如,添加到表定义中的以下内容将允许像现在的配置单元中支持的那样追加表格:
TBLPROPERTIES("immutable"="false")
不过,目前在配置单元和 HCatalog 的发货版本中不可用。 对于向表中添加越来越多数据的工作流来说,我们因此需要为每次新运行的工作流创建一个新分区。 我们在管道的 v4 中进行了这些更改,首先使用整数分区键重新创建表,如下所示:
CREATE TABLE tweets_hcat (
…)
PARTITIONED BY (partition_key int)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS SEQUENCEFILE;
CREATE TABLE `places_hcat`(
… )
partitioned by(partition_key int)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS SEQUENCEFILE
TBLPROPERTIES("immutable"="false") ;
CREATE TABLE `users_hcat`(
…)
partitioned by(partition_key int)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS SEQUENCEFILE
TBLPROPERTIES("immutable"="false") ;
PigHCatStorer采用可选的分区定义,我们相应地修改了 Pig 脚本中的store语句;例如:
store tweets_tsv into 'twttr.tweets_hcat'
using org.apache.hive.hcatalog.pig.HCatStorer(
'partition_key=$partitionKey');
然后,我们修改workflow.xml文件中的 Pig 操作,以包括此附加参数:
<script>${workflowRoot}/pig/extract_to_hcat.pig</script>
<param>inputDir=${inputDir}</param>
<param>partitionKey=${partitionKey}</param>
那么问题就是我们如何将此分区键传递给工作流。 我们可以在job.properties文件中指定它,但这样做会遇到在下一次重新运行时尝试写入现有分区的相同问题。
摄取工作流 v4
目前,我们将把它作为显式参数传递给 Oozie CLI 的调用,稍后再探索更好的方法:
$ oozie job –run –config v4/job.properties –DpartitionKey=12345
备注
请注意,此行为的后果是使用相同参数重新运行 HCAT 工作流将失败。 在测试工作流或使用本书中的示例代码时,请注意这一点。
生成派生数据
既然我们已经建立了主数据管道,那么在添加每个新的个附加数据集之后,我们很可能希望采取一系列操作。 作为一个简单的例子,请注意,使用我们前面的机制将每组用户数据添加到单独的分区,users_hcat表将多次包含用户。 让我们为唯一用户创建一个新表,并在每次添加新用户数据时重新生成该表。
请注意,考虑到前面提到的 HCatalog 的限制,我们将使用一个配置单元操作来实现此目的,因为我们需要替换表中的数据。
首先,我们将为唯一用户信息创建一个新表,如下所示:
CREATE TABLE IF NOT EXISTS `unique_users`(
`user_id` string ,
`name` string ,
`description` string ,
`screen_name` string )
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\t'
STORED AS sequencefile ;
在该表中,我们将只存储从不更改(ID)或很少更改(屏幕名称等)的用户属性。 然后,我们可以编写一条简单的配置单元语句,从完整的users_hcat表填充此表:
USE twttr;
INSERT OVERWRITE TABLE unique_users
SELECT DISTINCT user_id, name, description, screen_name
FROM users_hcat;
然后,我们可以在工作流中的上一个 Pig 节点之后添加一个附加的配置单元操作节点。 在执行此操作时,我们发现简单地给节点命名(如 hive-node)的模式不是一个好主意,因为我们现在有两个基于配置单元的节点。 在工作流的 v5 中,我们添加了此新节点,并将节点更改为更具描述性的名称:
摄取工作流 v5
并行执行多个动作
我们的工作流有两种类型的活动:初始化文件系统和配置单元表的节点的初始设置,以及执行实际处理的功能节点。 如果我们看一下我们一直在使用的两个设置节点,很明显它们是完全不同的,并且不是相互依赖的。 因此,我们可以利用称为fork和join节点的 Oozie 特性来并行执行这些操作。 现在,我们的workflow.xml文件的开头变为:
<start to="setup-fork-node"/>
Ooziefork节点包含许多path元素,每个元素指定一个起始节点。 其中每一项都将并行推出:
<fork name="setup-fork-node">
<path start="setup-filesystem-node" />
<path start="create-tables-node" />
</fork>
指定的每个操作节点与我们以前使用的任何操作节点都没有什么不同。 动作节点可以链接到一系列其他节点;唯一的要求是每个并行的动作系列必须转换到与fork节点关联的join节点,如下所示:
<action name="setup-filesystem-node">
…
<ok to="setup-join-node"/>
<error to="fail"/>
</action>
<action name="create-tables-node">
…
<ok to="setup-join-node"/>
<error to="fail"/>
</action>
join节点本身充当协调点;任何已完成的工作流都将等待,直到fork节点中指定的所有路径都到达该点。 此时,工作流在join节点内指定的节点处继续。 下面是join节点的使用方法:
<join name="create-join-node" to="gettweets-node"/>
在前面的代码中,出于空间目的我们省略了操作定义,但完整的工作流定义在 V6 中:
摄取工作流 v6
调用子工作流
尽管fork/join机制使并行操作的过程更加高效,但如果我们在主workflow.xml定义中包含它,它仍然会增加大量的冗长。 从概念上讲,我们有一系列操作来执行工作流所需的相关任务,但不一定是工作流的一部分。 对于这种情况和类似情况,Oozie 提供了调用子工作流的能力。 父工作流将执行子工作流并等待其完成,并能够将配置元素从一个工作流传递到另一个工作流。
子工作流本身将是一个完整的工作流,通常存储在 HDFS 上的一个目录中,该目录具有我们期望的工作流、主workflow.xml文件以及任何所需的配置单元、PIG 或类似文件的所有常见结构。
我们可以在 HDFS 上创建一个名为 Setup-Workflow 的新目录,并在其中创建文件系统和配置单元创建操作所需的文件。 子工作流配置文件如下所示:
<workflow-app name="create-workflow">
<start to="setup-fork-node"/>
<fork name="setup-fork-node">
<path start="setup-filesystem-node" />
<path start="create-tables-node" />
</fork>
<action name="setup-filesystem-node">
…
</action>
<action name="create-tables-node">
…
</action>
<join name="create-join-node" to="end"/>
<kill name="fail">
<message>Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]</message>
</kill>
<end name="end"/>
</workflow-app>
定义了此子工作流后,我们将修改主工作流的第一个节点以使用子工作流节点,如下所示:
<start to="create-subworkflow-node"/>
<action name="create-subworkflow-node">
<sub-workflow>
<app-path>${subWorkflowRoot}</app-path>
<propagate-configuration/>
</sub-workflow>
<ok to="gettweets-node"/>
<error to="fail"/>
</action>
我们将在父工作流的job.properties中指定subWorkflowPath,propagate-configuration元素将父工作流的配置传递给子工作流。
添加全局设置
通过将实用程序节点提取到子工作流中,我们可以显著减少主工作流定义中的混乱和复杂性。 在我们的接收管道的 v7 中,我们将进行一个额外的简化,并添加一个全局配置部分,如下所示:
<workflow-app name="v7">
<global>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<job-xml>${workflowRoot}/hive-site.xml</job-xml>
<configuration>
<property>
<name>mapred.job.queue.name</name>
<value>${queueName}</value>
</property>
</configuration>
</global>
<start to="create-subworkflow-node"/>
通过添加此全局配置节,我们无需在剩余工作流中的配置单元和 PIG 节点中指定任何这些值(请注意,当前外壳节点不支持全局配置机制)。 这可以极大地简化我们的一些节点;例如,我们的 Pig 节点现在如下所示:
<action name="hcat-ingest-node">
<pig>
<configuration>
<property>
<name>oozie.action.sharelib.for.pig</name>
<value>pig,hcatalog</value>
</property>
</configuration>
<script>${workflowRoot}/pig/extract_to_hcat.pig</script>
<param>inputDir=${inputDir}</param>
<param>dbName=${dbName}</param>
<param>partitionKey=${partitionKey}</param>
</pig>
<ok to="derived-data-node"/>
<error to="fail"/>
</action>
可以看到,我们可以添加额外的配置元素,或者实际上覆盖全局部分中指定的配置元素,从而产生更加清晰的操作定义,只关注特定于相关操作的信息。 我们的 Workflow v7 既添加了全局部分,又添加子工作流,这显著提高了工作流的可读性:
摄取工作流 v7
外部数据的挑战
当我们依赖外部数据来驱动我们的应用时,我们隐含地依赖于该数据的质量和稳定性。 当然,对于任何数据都是如此,但是当数据是由我们无法控制的外部源生成时,风险很可能更高。 无论如何,当我们在这些数据馈送的基础上构建我们期望的可靠应用时,特别是当我们的数据量增长时,我们需要考虑如何降低这些风险。
数据验证
我们使用通用术语数据验证来指代确保传入数据符合我们的期望的行为,并潜在地应用标准化来相应地修改它,甚至删除格式错误或损坏的输入。 这实际上涉及的将是非常特定的应用。 在某些情况下,重要的是确保系统只接收符合给定的准确或干净定义的数据。 对于我们的 tweet 数据,我们不关心每一条记录,可以很容易地采用一种策略,比如删除在我们关心的特定字段中没有值的记录。 但是,对于其他应用,必须捕获每条输入记录,这可能会驱动逻辑的实现,以重新格式化每条记录,以确保其符合要求。 在其他情况下,只有正确的记录会被摄取,但其余的记录可能不会被丢弃,而是会存储在其他地方供以后分析。
底线是,试图定义一种通用的数据验证方法远远超出了本章的范围。
但是,我们可以提供一些想法,说明在管道中的什么位置合并各种类型的验证逻辑。
验证操作
执行任何必要的验证或清理的逻辑可以直接合并到其他操作中。 运行脚本以收集数据的外壳节点可以添加命令,以不同的方式处理格式错误的记录。 将数据加载到表中的 PIG 和 HIVE 操作可以对摄取执行筛选(在 Pig 中更容易完成),或者在将数据从摄取表复制到操作存储区时添加警告。
但是,有一种观点认为应该在工作流中添加一个验证节点,即使它最初并不执行实际的逻辑。 例如,这可能是一个 Pig 操作,它读取数据,应用验证,并将验证后的数据写入新位置以供后续节点读取。 这样做的好处是,我们可以在以后更新验证逻辑,而无需更改其他操作,这应该会降低意外破坏管道其余部分的风险,并使节点在职责方面的定义更加清晰。 这一思路的自然延伸是,新的验证子工作流很可能也是一个很好的模型,因为它不仅提供职责分离,而且使验证逻辑更易于测试和更新。
这种方法的明显缺点是,它增加了额外的处理和另一个读取数据并重新写入数据的周期。 当然,这直接违背了我们在考虑使用来自 Pig 的 HCatalog 时强调的优势之一。
最后,这将归结为在性能与工作流复杂性和可维护性之间的权衡。 在考虑如何执行验证以及这对您的工作流意味着什么时,请在决定实现之前考虑所有这些要素。
处理格式更改
我们不能仅仅因为我们有数据流入我们的系统并确信数据得到了充分的验证就宣布的胜利。 特别是当数据来自外部来源时,我们必须考虑数据的结构可能会随着时间的推移而发生变化。
请记住,像配置单元这样的系统仅在读取数据时才应用表架构。 这对于实现灵活的数据存储和获取是一个巨大的好处,但当获取的数据与针对其执行的查询不再匹配时,可能会导致面向用户的查询或工作负载突然失败。 在写入时应用模式的关系数据库甚至不允许将此类数据摄取到系统中。
处理对数据格式所做更改的明显方法是将现有数据重新处理为新格式。 虽然这在较小的数据集上是容易处理的,但在大型 Hadoop 集群中看到的那种卷上,它很快就变得不可行了。
使用 avro 处理模式演变
Avro 在与 Hive 的集成方面有一些功能,可以帮助我们解决这个问题。 如果我们将表作为 tweet 数据,我们可以用以下 avro 模式表示 tweet 记录的结构:
{
"namespace": "com.learninghadoop2.avrotables",
"type":"record",
"name":"tweets_avro",
"fields":[
{"name": "created_at", "type": ["null" ,"string"]},
{"name": "tweet_id_str", "type": ["null","string"]},
{"name": "text","type":["null","string"]},
{"name": "in_reply_to", "type": ["null","string"]},
{"name": "is_retweeted", "type": ["null","string"]},
{"name": "user_id", "type": ["null","string"]},
{"name": "place_id", "type": ["null","string"]}
]
}
在名为tweets_avro.avsc的文件中创建前面的模式-这是 Avro 模式的标准文件扩展名。 然后,将其放在 HDFS 上;我们希望有一个公共位置来存放模式文件,比如/schema/avro。
有了这个定义,我们现在可以创建一个配置单元表格,该表格使用此模式作为其表格规范,如下所示:
CREATE TABLE tweets_avro
PARTITIONED BY ( `partition_key` int)
ROW FORMAT SERDE
'org.apache.hadoop.hive.serde2.avro.AvroSerDe'
WITH SERDEPROPERTIES (
'avro.schema.url'='hdfs://localhost.localdomain:8020/schema/avro/tweets_avro.avsc'
)
STORED AS INPUTFORMAT
'org.apache.hadoop.hive.ql.io.avro.AvroContainerInputFormat'
OUTPUTFORMAT
'org.apache.hadoop.hive.ql.io.avro.AvroContainerOutputFormat';
然后,从配置单元(或也支持此类定义的 HCatalog)中查看表定义:
describe tweets_avro
OK
created_at string from deserializer
tweet_id_str string from deserializer
text string from deserializer
in_reply_to string from deserializer
is_retweeted string from deserializer
user_id string from deserializer
place_id string from deserializer
partition_key int None
我们还可以像使用其他表一样使用该表,例如,将分区 3 中的数据从非 avro 表复制到 avro 表,如下所示:
SET hive.exec.dynamic.partition.mode=nonstrict
INSERT INTO TABLE tweets_avro
PARTITION (partition_key)
SELECT FROM tweets_hcat
备注
与前面的示例一样,如果类路径中不存在 avro 依赖项,我们需要将 avro MapReduce JAR 添加到我们的环境中,然后才能从表中进行选择。
我们现在有了一个由 avro 模式指定的新 twets 表;到目前为止,它看起来和其他表一样。 但是,对于我们在本章中的目的而言,真正的好处在于我们可以如何使用 Avro 机制来处理模式演变。 让我们向表架构添加一个新字段,如下所示:
{
"namespace": "com.learninghadoop2.avrotables",
"type":"record",
"name":"tweets_avro",
"fields":[
{"name": "created_at", "type": ["null" ,"string"]},
{"name": "tweet_id_str", "type": ["null","string"]},
{"name": "text","type":["null","string"]},
{"name": "in_reply_to", "type": ["null","string"]},
{"name": "is_retweeted", "type": ["null","string"]},
{"name": "user_id", "type": ["null","string"]},
{"name": "place_id", "type": ["null","string"]},
{"name": "new_feature", "type": "string", "default": "wow!"}
]
}
有了这个新的模式,我们就可以验证表定义是否也已更新,如下所示:
describe tweets_avro;
OK
created_at string from deserializer
tweet_id_str string from deserializer
text string from deserializer
in_reply_to string from deserializer
is_retweeted string from deserializer
user_id string from deserializer
place_id string from deserializer
new_feature string from deserializer
partition_key int None
在不添加任何新数据的情况下,我们可以在将返回现有数据默认值的新字段上运行查询,如下所示:
SELECT new_feature FROM tweets_avro LIMIT 5;
...
OK
wow!
wow!
wow!
wow!
wow!
更令人印象深刻的是,新列不需要添加到末尾;它可以在记录中的任何位置。 使用此机制,我们现在可以更新 Avro 模式以表示新的数据结构,并看到这些更改自动反映在配置单元表定义中。 任何引用新列的查询都将检索不存在该字段的所有现有数据的默认值。
请注意,我们在这里使用的默认机制是 Avro 的核心,并不特定于配置单元。 Avro 是一种非常强大和灵活的格式,在许多领域都有应用,绝对值得我们在这里进行更深入的研究。
从技术上讲,这为我们提供了向前兼容性。 我们可以对表模式进行更改,并使所有现有数据自动保持与新结构的兼容。但是,我们不能继续将旧格式的数据吸收到更新表中,因为该机制不提供向后兼容性:
INSERT INTO TABLE tweets_avro
PARTITION (partition_key)
SELECT * FROM tweets_hcat;
FAILED: SemanticException [Error 10044]: Line 1:18 Cannot insert into target table because column number/types are different 'tweets_avro': Table insclause-0 has 8 columns, but query has 7 columns.
通过 Avro 支持模式演变,可以将数据更改作为正常业务的一部分来处理,而不是经常变成消防紧急情况。 但很明显,这不是免费的;仍然需要在管道中做出改变,并将这些改变投入生产。 但是,拥有提供前向兼容性的配置单元表确实允许以更易于管理的步骤执行该过程;否则,您将需要在管道的每个阶段同步更改。 如果更改是从接收到插入到 Avro 支持的配置单元表中,那么这些表的所有用户都可以保持不变(只要他们不做像select *这样的事情,这无论如何都是一个糟糕的主意),并继续对新数据运行现有查询。 然后,可以根据摄取机制的不同时间表更改这些应用。 在摄取管道的 V8 中,我们展示了如何充分使用 avro 表来实现所有现有功能。
备注
请注意,撰写本文时尚未发布的配置单元 0.14 可能会包含更多对 Avro 的内置支持,这可能会进一步简化模式演变的过程。 如果阅读本文时配置单元 0.14 可用,请务必查看最终实现。
关于使用 Avro 模式进化的最后思考
通过对 Avro 的讨论,我们已经触及了更广泛主题的某些方面,特别是更大范围的数据管理以及围绕数据版本控制和保留的策略。 这一领域的大部分内容都变得非常特定于一个组织,但这里有一些我们认为更广泛适用的临别想法。
仅进行附加更改
我们在前面的示例中讨论了添加列。 有时(尽管更少见),源数据会删除列,或者您发现不再需要新列。 Avro 并没有真正提供工具来帮助实现这一点,我们觉得这通常是不受欢迎的。 我们倾向于维护旧数据,而不是删除旧列,而不是在所有新数据中使用空列。 如果您控制数据格式,这将更容易管理;如果您正在接收外部源,则要遵循此方法,您将需要重新处理数据以删除旧列,或者更改接收机制以为所有新数据添加默认值。
显式管理架构版本
在前面的示例中,我们有一个模式文件,我们直接对其进行了更改。 这可能是一个非常糟糕的想法,因为它使我们无法跟踪随时间推移的模式更改。 除了将模式视为受版本控制的构件(您的模式也在 Git 中,不是吗?)。 使用显式版本标记每个模式通常很有用。 当传入数据也显式版本化时,这特别有用。 然后,您可以添加新文件并使用ALTER TABLE语句将配置单元表定义指向新架构,而不是覆盖现有架构文件。 当然,我们在这里假设您不能选择对具有不同格式的旧数据使用不同的查询。 尽管配置单元没有选择模式的自动机制,但在某些情况下,您可能可以手动进行控制,从而避开进化问题。
考虑模式分发
当使用模式文件时,请考虑如何将其分发给客户端。 如果像前面的示例一样,文件位于 HDFS 上,那么给它一个较高的复制因子可能是有意义的。 该文件将由查询表的每个 MapReduce 作业中的每个映射器检索。
还可以将 avro URL 指定为本地文件系统位置(file://),这对开发很有用,也可以作为 Web 资源(http://)。 尽管后者非常有用,因为它是一种将模式分发到非 Hadoop 客户端的便捷机制,但请记住,Web 服务器上的负载可能很高。 使用现代硬件和高效的 Web 服务器,这很可能不是一个大问题,但如果您有一个由数千台机器组成的集群,运行许多并行作业,其中每个映射器都需要访问 Web 服务器,那么请小心。
收集其他数据
许多数据处理系统没有单一的数据接收来源;通常,一个主要来源由其他次要来源丰富。 现在,我们将了解如何将此类参考数据的检索合并到我们的数据仓库中。
从高层次上讲,这个问题与我们检索原始 tweet 数据没有太大区别,因为我们希望从外部来源提取数据,可能对其进行一些处理,并将其存储在以后可以使用的地方。 但这确实突出了我们需要考虑的一个方面:我们真的想在每次接收新 tweet 时检索这些数据吗? 答案当然是否定的。 参考数据很少更改,我们可以轻松地获取它,而不是像新的 tweet 数据那样频繁。 这提出了一个我们到目前为止一直回避的问题:我们如何安排 Oozie 工作流?
安排工作流
到目前为止,我们已经从 CLI 按需运行所有 Oozie 工作流。 Oozie 还有一个调度程序,它可以定时启动作业,也可以在满足外部标准(如 HDFS 中出现的数据)时启动作业。 我们的工作流程很适合让我们的主推文管道运行,比如说,每 10 分钟运行一次,但参考数据每天只刷新一次。
提示
无论何时检索数据,都要仔细考虑如何处理执行删除/替换操作的数据集。 特别是,在检索和验证新数据之前不要执行删除操作;否则,在下一次检索成功之前,任何需要引用数据的作业都将失败。 将破坏性操作包括在仅在成功完成检索步骤后才触发的子工作流中可能是一个很好的选择。
Oozie 实际上定义了它可以运行的两种类型的应用:我们到目前为止已经使用的工作流和协调器,它们根据各种标准调度要执行的工作流。 协调器作业在概念上类似于我们的其他工作流;我们将 XML 配置文件推送到 HDFS 上,并在运行时使用参数化的属性文件对其进行配置。 此外,协调器作业具有从触发其执行的事件接收附加参数化的功能。
用一个例子来描述这可能是最好的。 比方说,我们希望像前面提到的那样创建一个协调器,该协调器每 10 分钟执行我们摄取工作流的 v7。 下面是coordinator.xml文件(协调器 XML 定义的标准名称):
<coordinator-app name="tweets-10min-coordinator" frequency="${freq}" start="${startTime}" end="${endTime}" timezone="UTC" >
协调器中的主要操作节点是工作流,我们需要为其指定其在 HDFS 上的根位置和所有必需的属性,如下所示:
<action>
<workflow>
<app-path>${workflowPath}</app-path>
<configuration>
<property>
<name>workflowRoot</name>
<value>${workflowRoot}</value>
</property>
…
我们还需要包括工作流中的任何操作或其触发的任何子工作流所需的任何属性;实际上,这意味着需要在此处包括要触发的任何工作流中存在的任何用户定义变量,如下所示:
<property>
<name>dbName</name>
<value>${dbName}</value>
</property>
<property>
<name>partitionKey</name><value>${coord:formatTime(coord:nominalTime(), 'yyyyMMddhhmm')}
</value>
</property>
<property>
<name>exec</name>
<value>gettweets.sh</value>
</property>
<property>
<name>inputDir</name>
<value>/tmp/tweets</value>
</property>
<property>
<name>subWorkflowRoot</name>
<value>${subWorkflowRoot}</value>
</property>
</configuration>
</workflow>
</action>
</coordinator-app>
我们在前面的 XML 中使用了一些特定于协调器的特性。 请注意协调器开始和结束时间的详细说明以及频率(以分钟为单位)。 我们在这里使用最简单的形式;Oozie 也有一组函数来允许相当丰富的频率规格。
我们在定义partitionKey变量时使用了协调器 EL 函数。 早些时候,在从 CLI 运行工作流时,我们明确指定了这些,但提到了还有一种更好的方法-就是这样。 以下表达式生成包含年、月、日、小时和分钟的格式化输出:
${coord:formatTime(coord:nominalTime(), 'yyyyMMddhhmm')}
如果我们随后使用它作为分区键的值,我们可以确保每次工作流调用都能在我们的HCatalog表中正确地创建一个唯一的分区。
协调器作业的相应job.properties看起来与我们前面的配置文件非常相似,其中包含 NameNode 和类似变量的常用条目,以及特定于应用的变量的值,如dbName。 此外,我们还需要指定 HDFS 上协调器位置的根目录,如下所示:
oozie.coord.application.path=${nameNode}/user/${user.name}/${tasksRoot}/tweets_10min
请注意oozie.coord名称空间前缀,而不是之前使用的oozie.wf。 有了 HDFS 上的协调器定义,我们就可以将文件提交给 Oozie,就像之前的作业一样。 但在本例中,作业将仅在给定的时间段内运行。 具体地说,当系统时钟在startTime和endTime之间时,它将每五分钟运行一次(频率是可变的)。
我们已经在本章的源代码中包含了tweets_10min目录中的完整配置。
其他 Oozie 触发器
前面的协调器有一个非常简单的触发器;它在指定的时间范围内定期启动。 Oozie 还有一个称为 DataSets 的附加功能,它可以由新数据的可用性触发。
这并不是非常适合我们到目前为止定义我们的管道的方式,但是想象一下,我们的工作流不是将收集 tweet 作为第一步,而是一个外部系统在不断地将新的 tweet 文件推送到 HDFS 上。 可以将 Oozie 配置为根据目录模式查找新数据的存在,或者专门在 HDFS 上出现就绪文件时触发。 后一种配置提供了一种非常方便的机制来集成 MapReduce 作业的输出,默认情况下,MapReduce 作业会将_SUCCESS文件写入其输出目录。
Oozie 数据集可以说是整个系统中最强大的部分之一,由于空间原因,我们在这里不能公正地对待它们。 但我们强烈建议您参考 Oozie 主页以获取更多信息。
齐心协力
让我们回顾一下我们到目前为止已经讨论过的内容,以及我们如何使用 Oozie 构建一系列复杂的工作流,这些工作流通过组合所有讨论的技术来实现数据生命周期管理的方法。
首先,重要的是定义明确的职责,并使用良好的设计和关注点分离原则实现系统的各个部分。 通过应用这一点,我们最终得到了几个不同的工作流:
- 确保环境(主要是 HDFS 和配置单元元数据)正确配置的子工作流
- 用于执行数据验证的子工作流
- 触发前两个子工作流,然后通过多步骤接收管道提取新数据的主工作流
- 每 10 分钟执行上述工作流的协调员
- 第二协调器,其摄取将对应用流水线有用的参考数据
我们还使用 Avro 模式定义所有表,并在任何可能的情况下使用它们来帮助管理模式演变和随时间变化的数据格式。
在本章的源代码中,我们将在工作流的最终版本中提供这些组件的完整源代码。
提供帮助的其他工具
尽管 Oozie 是一个非常强大的工具,但有时要正确编写工作流定义文件可能有些困难。 随着管道变得越来越庞大,管理复杂性成为一项挑战,即使将其良好的功能划分为多个工作流也是如此。 在更简单的层面上,XML 对于人类来说从来就不是一件有趣的事情! 有一些工具可以提供帮助。 自称为 Hadoop UI(gethue.com/)的工具 Hue 提供了一些图形工具来帮助编写、执行和管理 Oozie 工作流。 虽然功能强大,但 Hue 不是一个初学者工具;我们将在第 11 章,下一步去哪里中更多地提到它。
一个名为 Falcon(falcon.incubator.apache.org)的新 apache 项目可能也会令人感兴趣。 Falcon 使用 Oozie 构建一系列更高级别的数据流和操作。 例如,Falcon 提供了实现和确保跨多个 Hadoop 集群进行跨站点复制的配方。 猎鹰团队正在开发更好的界面来构建他们的工作流程,所以这个项目可能很值得一看。
摘要
希望本章将数据生命周期管理的主题作为一个枯燥的抽象概念进行介绍。 我们讲了很多内容,特别是:
- 数据生命周期管理的定义,以及它如何涵盖通常在大数据量中变得重要的许多问题和技术
- 按照良好的数据生命周期管理原则构建数据接收管道的概念,然后可供更高级别的分析工具使用
- Oozie 作为一个专注于 Hadoop 的工作流管理器,以及我们如何使用它将一系列操作组合到一个统一的工作流中
- 各种 Oozie 工具,如子工作流、并行操作执行和全局变量,使我们能够将真正的设计原则应用到我们的工作流中
- HCatalog 以及它如何为配置单元以外的工具提供读写表结构数据的方法;我们展示了它的巨大前景以及与 Pig 等工具的集成,但也强调了当前的一些弱点
- Avro 是我们选择的处理模式随时间演变的工具
- 使用 Oozie 协调器基于时间间隔或数据可用性构建计划的工作流,以推动多个摄取管道的执行
- 其他一些工具可以使这些任务变得更容易,即色调和猎鹰
在下一章中,我们将介绍几种高级分析工具和框架,它们可以在接收管道中收集的数据基础上构建复杂的应用逻辑。
九、让开发变得更容易
在本章中,我们将介绍如何根据用例和最终目标,使用构建在 JavaAPI 之上的大量抽象和框架来简化 Hadoop 中的应用开发。 我们将特别了解以下主题:
- 流 API 如何允许我们使用 Python 和 Ruby 等动态语言编写 MapReduce 作业
- Apache Crunch 和 Kite Morphline 等框架如何允许我们使用更高级别的抽象来表示数据转换管道
- Kite Data 是 Cloudera 开发的一个很有前途的框架,它如何为我们提供了应用设计模式和样板来简化 Hadoop 生态系统中不同组件的集成和互操作性的能力
选择框架
在前面的章中,我们了解了用于编写分布式应用的 MapReduce 和 Spark 编程 API。 虽然这些 API 非常强大和灵活,但它们具有一定程度的复杂性,可能需要大量的开发时间。
为了减少冗长,我们引入了 Pig 和 Have 框架,它们将特定于领域的语言 Pig 拉丁语和 Have QL 编译到许多 MapReduce 作业或 Spark DAG 中,有效地将 API 抽象出来。 这两种语言都可以使用 UDF 进行扩展,UDF 是将复杂逻辑映射到 Pig 和 Have 数据模型的一种方式。
当我们需要一定程度的灵活性和模块性时,事情可能会变得棘手。 根据用例和开发人员需求,Hadoop 生态系统提供了大量的 API、框架和库选择。 在本章中,我们将识别四类用户,并将其与以下相关工具进行匹配:
- 希望避免使用 Java 而倾向于使用动态语言编写 MapReduce 作业脚本的开发人员,或者使用未在 JVM 上实现的语言的开发人员。 典型的用例是前期分析和快速原型制作:Hadoop Streaming
- Java 开发人员,需要集成 Hadoop 生态系统的组件,并且可以受益于代码化的设计模式和样板:Kite Data
- 希望使用熟悉的 API 编写模块化数据管道的 Java 开发人员:Apache Crunch
- 更愿意配置数据转换链的开发人员。 例如,一个想要在 ETL 管道中嵌入现有代码的数据工程师:Kite Morphines
Hadoop 流
我们在前面已经提到过,MapReduce 程序不必用 Java 编写。 您可能想要或需要用另一种语言编写地图和减少任务,原因有几个。 也许您有现有的代码可以利用,或者需要使用第三方二进制文件-原因是多种多样且合理的。
Hadoop 提供了许多机制来帮助非 Java 开发,其中最主要的是 Hadoop 管道(提供本地 C++接口)和 Hadoop 流(允许任何使用标准输入和输出的程序用于映射和缩减任务)。 使用 MapReduce Java API,map 和 Reduce 任务都为包含任务功能的方法提供实现。 这些方法将输入作为方法参数接收到任务,然后通过Context对象输出结果。 这是一个清晰且类型安全的接口,但根据定义它是特定于 Java 的。
Hadoop 流媒体采用了一种不同的方法。 使用流,您可以编写一个映射任务,该任务从标准输入读取其输入,一次一行,并将其结果的输出提供给标准输出。 然后,Reduce 任务也执行同样的操作,同样只对其数据流使用标准输入和输出。
从标准输入和输出读取和写入的任何程序都可以在流中使用,比如编译的二进制文件、Unix shell 脚本或用动态语言(如 Python 或 Ruby)编写的程序。 流媒体的最大优势是,它可以让你尝试想法,并比使用 Java 更快地迭代它们。 您只需编写脚本并将它们作为参数传递到流 JAR 文件,而不是编译/JAR/提交循环。 特别是在对新数据集进行初步分析或尝试新想法时,这可以显著加快开发速度。
关于动态语言和静态语言的经典争论平衡了快速开发与运行时性能和类型检查的好处。 使用流式传输时,这些动态缺点也适用于。 因此,我们倾向于使用流式处理进行前期分析,使用 Java 实现将在生产集群上执行的作业。
Python 中的流式字数统计
我们将通过使用 Python 重新实现我们熟悉的字数统计示例来演示 Hadoop 流。 首先,我们创建一个脚本作为我们的映射器。 它使用for循环使用标准输入中的 UTF-8 编码文本行,将其拆分成单词,并使用print函数将每个单词写入标准输出,如下所示:
#!/bin/env python
import sys
for line in sys.stdin:
# skip empty lines
if line == '\n':
continue
# preserve utf-8 encoding
try:
line = line.encode('utf-8')
except UnicodeDecodeError:
continue
# newline characters can appear within the text
line = line.replace('\n', '')
# lowercase and tokenize
line = line.lower().split()
for term in line:
if not term:
continue
try:
print(
u"%s" % (
term.decode('utf-8')))
except UnicodeEncodeError:
continue
减法器统计标准输入中每个单词的出现次数,并将输出作为最终值提供给标准输出,如下所示:
#!/bin/env python
import sys
count = 1
current = None
for word in sys.stdin:
word = word.strip()
if word == current:
count += 1
else:
if current:
print "%s\t%s" % (current.decode('utf-8'), count)
current = word
count = 1
if current == word:
print "%s\t%s" % (current.decode('utf-8'), count)
备注
在这两种情况下,我们都隐式使用前面章节中讨论的 Hadoop 输入和输出格式。 它是处理源文件的TextInputFormat,并将每一行一次提供给映射脚本。 相反,TextOutputFormat将确保 Reduce 任务的输出也被正确地写为文本。
将map.py和reduce.py复制到 HDFS,然后使用前几章中的样本数据将脚本作为流作业执行,如下所示:
$ hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-file map.py \
-mapper "python map.py" \
-file reduce.py \
-reducer "python reduce.py" \
-input sample.txt \
-output output.txt
备注
推文采用UTF-8编码。 确保相应地设置了PYTHONIOENCODING,以便在 UNIX 终端中通过管道传输数据:
$ export PYTHONIOENCODING='UTF-8'
可以从命令行提示符执行相同的代码:
$ cat sample.txt | python map.py| python reduce.py > out.txt
映射器和减速器代码可在github.com/learninghad…中找到。
使用流式处理时作业的差异
在 Java 中,我们知道我们的map()方法将为每个输入键/值对调用一次,而我们的reduce()方法将为每个键及其值集调用。
对于流,我们不再有 map 或 Reduce 方法的概念;相反,我们编写了处理接收数据流的脚本。 这改变了我们编写减速器的方式。 在 Java 中,每个键的值分组是由 Hadoop 执行的;每次调用 Reduce 方法都会收到一个用制表符分隔的键及其所有值。 在流式处理中,Reduce 任务的每个实例每次都被赋予一个单独的未收集的值。
Hadoop 流确实会对键进行排序,例如,如果映射器发出以下数据:
First 1
Word 1
Word 1
A 1
First 1
流减少器将按以下顺序接收它:
A 1
First 1
First 1
Word 1
Word 1
Hadoop 仍然收集每个键的值,并确保每个键只传递给一个减法器。 换句话说,Reducer 获取多个键的所有值,并将它们分组在一起;但是,它们不会打包到 Reducer 的单独执行中,也就是每个键一个值,这与 Java API 不同。 由于 Hadoop 流使用stdin和stdout通道在任务之间交换数据,因此调试和错误消息不应打印到标准输出。 在下面的示例中,我们将使用 Pythonlogging(docs.python.org/2/library/l…)包将警告语句记录到一个文件中。
查找文本中的重要单词
我们现在将实现一个度量Term Frequency-Inverse Document Frequency(TF-IDF),它将帮助我们根据单词在一组文档(在我们的例子中是 tweet)中出现的频率来确定它们的重要性。
直观地说,如果一个词经常出现在文档中,它就很重要,应该给它一个高分。 然而,如果一个词出现在许多文档中,我们应该用较低的分数来惩罚它,因为它是一个常见的词,而且它的出现频率并不是本文所独有的。
因此,许多文档中出现的常见单词(如The和表示的*)将被缩小。 在一条推文中频繁出现的词语将被放大。 TF-IDF 的使用通常与其他度量和技术结合使用,包括停用词删除和文本分类。 请注意,此技术在处理较短的文档(如 tweet)时会有缺点。 在这种情况下,术语频率分量将趋向于变为 1。 相反,人们可以利用这一特性来检测离群值。*
我们将在示例中使用的 TF-IDF 的定义如下:
tf = # of times term appears in a document (raw frequency)
idf = 1+log(# of documents / # documents with term in it)
tf-idf = tf * idf
我们将使用三个 MapReduce 作业在 Python 中实现该算法:
- 第一个计算词频
- 第二个计算文档频率(IDF 的分母)
- 第三个计算每条推文的 TF-IDF
计算词频
词频部分与字数统计示例非常相似。 主要区别在于,我们将使用多字段、制表符分隔的键来跟踪术语和文档 ID 的同时出现情况。 对于 JSON 格式的每个 tweet,映射器提取id_str和text字段,标记化text,并发出term、doc_id元组:
for tweet in sys.stdin:
# skip empty lines
if tweet == '\n':
continue
try:
tweet = json.loads(tweet)
except:
logger.warn("Invalid input %s " % tweet)
continue
# In our example one tweet corresponds to one document.
doc_id = tweet['id_str']
if not doc_id:
continue
# preserve utf-8 encoding
text = tweet['text'].encode('utf-8')
# newline characters can appear within the text
text = text.replace('\n', '')
# lowercase and tokenize
text = text.lower().split()
for term in text:
try:
print(
u"%s\t%s" % (
term.decode('utf-8'), doc_id.decode('utf-8'))
)
except UnicodeEncodeError:
logger.warn("Invalid term %s " % term)
在减法器中,我们以制表符分隔的字符串形式发出文档中每个术语的频率:
freq = 1
cur_term, cur_doc_id = sys.stdin.readline().split()
for line in sys.stdin:
line = line.strip()
try:
term, doc_id = line.split('\t')
except:
logger.warn("Invalid record %s " % line)
# the key is a (doc_id, term) pair
if (doc_id == cur_doc_id) and (term == cur_term):
freq += 1
else:
print(
u"%s\t%s\t%s" % (
cur_term.decode('utf-8'), cur_doc_id.decode('utf-8'), freq))
cur_doc_id = doc_id
cur_term = term
freq = 1
print(
u"%s\t%s\t%s" % (
cur_term.decode('utf-8'), cur_doc_id.decode('utf-8'), freq))
要使此实现正常工作,减速器输入按术语排序是至关重要的。 我们可以使用以下管道从命令行测试这两个脚本:
$ cat tweets.json | python map-tf.py | sort -k1,2 | \
python reduce-tf.py
在命令行中,我们使用sort实用程序,而在 MapReduce 中,我们将使用org.apache.hadoop.mapreduce.lib.KeyFieldBasedComparator。 该比较器实现sort命令提供的功能子集。 特别地,可以使用–k<position>选项指定按字段排序。 要按术语(键的第一个字段)过滤,我们设置-D mapreduce.text.key.comparator.options=-k1:
/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-D map.output.key.field.separator=\t \
-D stream.num.map.output.key.fields=2 \
-Dmapreduce.output.key.comparator.class=\
org.apache.hadoop.mapreduce.lib.KeyFieldBasedComparator \
-D mapreduce.text.key.comparator.options=-k1,2 \
-input tweets.json \
-output /tmp/tf-out.tsv \
-file map-tf.py \
-mapper "python map-tf.py" \
-file reduce-tf.py \
-reducer "python reduce-tf.py"
备注
我们在比较器选项中指定哪些字段属于键(用于混洗)。
映射器和减速器代码可在github.com/learninghad…中找到。
计算单据频次
计算文档频率的主逻辑在减法器中,而映射器只是一个标识函数,它加载并输送 TF 作业的输出(按术语排序)。 在缩减器中,对于每个术语,我们计算它在所有文档中出现的次数。 对于每个术语,我们保留(term,doc_id,tf)元组的缓冲区key_cache,当找到新的术语时,我们将缓冲区刷新为标准输出,以及累积的文档频率df:
# Cache the (term,doc_id, tf) tuple.
key_cache = []
line = sys.stdin.readline().strip()
cur_term, cur_doc_id, cur_tf = line.split('\t')
cur_tf = int(cur_tf)
cur_df = 1
for line in sys.stdin:
line = line.strip()
try:
term, doc_id, tf = line.strip().split('\t')
tf = int(tf)
except:
logger.warn("Invalid record: %s " % line)
continue
# term is the only key for this input
if (term == cur_term):
# increment document frequency
cur_df += 1
key_cache.append(
u"%s\t%s\t%s" % (term.decode('utf-8'), doc_id.decode('utf-8'), tf))
else:
for key in key_cache:
print("%s\t%s" % (key, cur_df))
print (
u"%s\t%s\t%s\t%s" % (
cur_term.decode('utf-8'),
cur_doc_id.decode('utf-8'),
cur_tf, cur_df)
)
# flush the cache
key_cache = []
cur_doc_id = doc_id
cur_term = term
cur_tf = tf
cur_df = 1
for key in key_cache:
print(u"%s\t%s" % (key.decode('utf-8'), cur_df))
print(
u"%s\t%s\t%s\t%s\n" % (
cur_term.decode('utf-8'),
cur_doc_id.decode('utf-8'),
cur_tf, cur_df))
我们可以通过以下命令行测试脚本:
$ cat /tmp/tf-out.tsv | python map-df.py | python reduce-df.py > /tmp/df-out.tsv
我们可以通过以下方式测试 Hadoop 流上的脚本:
/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-D map.output.key.field.separator=\t \
-D stream.num.map.output.key.fields=3 \
-D mapreduce.output.key.comparator.class=\
org.apache.hadoop.mapreduce.lib.KeyFieldBasedComparator \
-D mapreduce.text.key.comparator.options=-k1 \
-input /tmp/tf-out.tsv/part-00000 \
-output /tmp/df-out.tsv \
-mapper org.apache.hadoop.mapred.lib.IdentityMapper \
-file reduce-df.py \
-reducer "python reduce-df.py"
在 Hadoop 上,我们使用org.apache.hadoop.mapred.lib.IdentityMapper,它提供与map-df.py脚本相同的逻辑。
映射器和减速器代码可在github.com/learninghad…中找到。
把它们放在一起-TF-IDF
要计算 TF-IDF,我们只需要一个使用上一步输出的映射器:
num_doc = sys.argv[1]
for line in sys.stdin:
line = line.strip()
try:
term, doc_id, tf, df = line.split('\t')
tf = float(tf)
df = float(df)
num_doc = float(num_doc)
except:
logger.warn("Invalid record %s" % line)
# idf = num_doc / df
tf_idf = tf * (1+math.log(num_doc / df))
print("%s\t%s\t%s" % (term, doc_id, tf_idf))
集合中的文档数作为参数传递给tf-idf.py:
/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-D mapreduce.reduce.tasks=0 \
-input /tmp/df-out.tsv/part-00000 \
-output /tmp/tf-idf.out \
-file tf-idf.py \
-mapper "python tf-idf.py 15578"
要计算 tweet 总数,我们可以将cat和wcUnix 实用程序与 Hadoop 流结合使用:
/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-input tweets.json \
-output tweets.cnt \
-mapper /bin/cat \
-reducer /usr/bin/wc
映射器源代码可以在github.com/learninghad…找到。
套件数据
Kite SDK(Hadoop)是类、命令行工具和示例的集合,旨在简化在 www.kitesdk.org 之上构建应用的过程。
在本节中,我们将了解 Kite 的子项目 Kite Data 如何简化与 Hadoop 数据仓库的几个组件的集成。 风筝示例可以在github.com/kite-sdk/ki…找到。
在 Cloudera 的 QuickStart VM 上,可以在/opt/cloudera/parcels/CDH/lib/kite/找到 Kite Jars。
Kite 数据被组织在许多子项目中,我们将在下面的部分中描述其中一些子项目。
_
顾名思义,核心是数据模块中提供的所有功能的构建块。 它的主要抽象是数据集和存储库。
org.kitesdk.data.Dataset接口用于表示一组不可变的数据:
@Immutable
public interface Dataset<E> extends RefinableView<E> {
String getName();
DatasetDescriptor getDescriptor();
Dataset<E> getPartition(PartitionKey key, boolean autoCreate);
void dropPartition(PartitionKey key);
Iterable<Dataset<E>> getPartitions();
URI getUri();
}
每个数据集由org.kitesdk.data.DatasetDescriptor接口的名称和实例标识,即数据集的结构描述,并提供其模式(org.apache.avro.Schema)和分区策略。
Reader<E>接口的实现用于从底层存储系统读取数据并产生类型为E的反序列化实体。 newReader()方法可用于获取给定数据集的适当实现:
public interface DatasetReader<E> extends Iterator<E>, Iterable<E>, Closeable {
void open();
boolean hasNext();
E next();
void remove();
void close();
boolean isOpen();
}
DatasetReader的实例将提供读取和迭代数据流的方法。 类似地,org.kitesdk.data.DatasetWriter提供了将数据流写入Dataset对象的接口:
public interface DatasetWriter<E> extends Flushable, Closeable {
void open();
void write(E entity);
void flush();
void close();
boolean isOpen();
}
与阅读器一样,编写器也是一次性使用的对象。 它们序列化类型为E的实体的实例,并将它们写入底层存储系统。 编写器通常不会直接实例化;相反,可以通过newWriter()工厂方法创建适当的实现。 DatasetWriter的实现将持有资源,直到调用close(),并期望调用方在写入器不再使用时调用finally块中的close()。 最后,请注意DatasetWriter的实现通常不是线程安全的。 从多个线程访问编写器的行为未定义。
数据集的一个特殊情况是View接口,如下所示:
public interface View<E> {
Dataset<E> getDataset();
DatasetReader<E> newReader();
DatasetWriter<E> newWriter();
boolean includes(E entity);
public boolean deleteAll();
}
视图携带现有数据集的键和分区的子集;它们在概念上类似于关系模型中的“视图”概念。
View界面可以从数据范围或键范围创建,也可以作为其他视图之间的联合创建。
_Data HCatalog
Data HCatalog 是一个模块,可用于访问 HCatalog 存储库。 该模块的核心抽象是org.kitesdk.data.hcatalog.HCatalogAbstractDatasetRepository及其具体实现org.kitesdk.data.hcatalog.HCatalogDatasetRepository。
它们描述了使用 HCatalog 管理存储的元数据和 HDFS 的DatasetRepository,如下所示:
public class HCatalogDatasetRepository extends HCatalogAbstractDatasetRepository {
HCatalogDatasetRepository(Configuration conf) {
super(conf, new HCatalogManagedMetadataProvider(conf));
}
HCatalogDatasetRepository(Configuration conf, MetadataProvider provider) {
super(conf, provider);
}
public <E> Dataset<E> create(String name, DatasetDescriptor descriptor) {
getMetadataProvider().create(name, descriptor);
return load(name);
}
public boolean delete(String name) {
return getMetadataProvider().delete(name);
}
public static class Builder {
…
}
}
备注
从 Kite 0.17 开始,Data HCatalog 已弃用,取而代之的是新的 Data Have 模块。
数据目录的位置由Hive/HCatalog选择(所谓的“托管表”),或者在创建该类的实例时通过在构造函数中提供文件系统和根目录(外部表)来指定。
_
kite-data-模块通过Dataset接口公开配置单元模式。 从 Kite 0.17 开始,此包将取代 Data HCatalog。
Колибрипрограмма数据映射还原
org.kitesdk.data.mapreduce包提供了接口,用于使用 MapReduce 对数据集进行读写操作。
==同步,由长者更正==
org.kitesdk.data.spark包提供了接口,用于使用 Apache Spark 对数据集进行读写操作。
_
org.kitesdk.data.crunch.CrunchDatasets包是一个帮助器类,用于将数据集和视图公开为 CrunchReadableSource或Target类:
public class CrunchDatasets {
public static <E> ReadableSource<E> asSource(View<E> view, Class<E> type) {
return new DatasetSourceTarget<E>(view, type);
}
public static <E> ReadableSource<E> asSource(URI uri, Class<E> type) {
return new DatasetSourceTarget<E>(uri, type);
}
public static <E> ReadableSource<E> asSource(String uri, Class<E> type) {
return asSource(URI.create(uri), type);
}
public static <E> Target asTarget(View<E> view) {
return new DatasetTarget<E>(view);
}
public static Target asTarget(String uri) {
return asTarget(URI.create(uri));
}
public static Target asTarget(URI uri) {
return new DatasetTarget<Object>(uri);
}
}
♫T0\ApacheCrunch
Apache Crunch(crunch.apache.org)是一个 Java 和 Scala 库,用于创建 MapReduce 作业的管道。 它基于谷歌的 FlumeJava(dl.acm.org/citation.cf…)论文和库。 该项目的目标是通过公开许多实现聚合、联接、过滤和排序记录等操作的模式,使熟悉 Java 编程语言的任何人都能尽可能简单地编写 MapReduce 作业。
与 Pig 等工具类似,Crunch 管道是通过组合不可变的分布式数据结构并在这些结构上运行所有处理操作来创建的;它们被表示和实现为用户定义的函数。 管道被编译成 MapReduce 作业的 DAG,其执行由库的规划者管理。 Crunch 允许我们编写迭代代码,并从映射和归约操作的角度抽象出思考的复杂性,同时避免了对诸如 Pig 拉丁语之类的特殊编程语言的需要。 此外,Crunch 提供了一个高度可定制的类型系统,允许我们处理和混合 Hadoop Writables、HBase 和 Avro 序列化对象。
FlumeJava 的主要假设是,MapReduce 对于几类问题来说是错误的抽象级别,这些问题的计算通常由多个链式作业组成。 出于性能原因,我们经常需要将逻辑上独立的操作(例如,过滤、投影、分组和其他转换)组合到单个物理 MapReduce 作业中。 这一方面对代码的可测试性也有影响。 虽然我们不会在本章中讨论这一方面,但我们鼓励读者通过参考 Crunch 的文档来深入了解它。
入门
QuickStart 虚拟机上已经安装了 Crash Jars。 默认情况下,JAR 位于/opt/cloudera/parcels/CDH/lib/crunch中。
或者,最近的 Crunch 库可以从crunch.apache.org/download.ht…、从 Maven Central 或特定于 Cloudera 的存储库下载。
概念
压缩管道由两个抽象组成:PCollection和PTable。
PCollection<T>接口是类型为T的对象的分布式不可变集合。 PTable<Key, Value>接口是由Key类型的键和Value类型的值组成的分布式、不可变的哈希表(PCollection 的子接口),它公开了使用键-值对的方法。
这两个抽象支持以下四个基元操作:
parallelDo:将用户定义函数DoFn应用于给定的PCollection,并返回新的PCollectionunion:将两个或多个PCollections合并为单个虚拟PCollectiongroupByKey:按键对PTable的元素进行排序和分组combineValues:聚合来自groupByKey操作的值
github.com/learninghad…实现了一个 Crunch MapReduce 管道,该管道对出现的散列标签进行计数:
Pipeline pipeline = new MRPipeline(HashtagCount.class, getConf());
pipeline.enableDebug();
PCollection<String> lines = pipeline.readTextFile(args[0]);
PCollection<String> words = lines.parallelDo(new DoFn<String, String>() {
public void process(String line, Emitter<String> emitter) {
for (String word : line.split("\\s+")) {
if (word.matches("(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)")) {
emitter.emit(word);
}
}
}
}, Writables.strings());
PTable<String, Long> counts = words.count();
pipeline.writeTextFile(counts, args[1]);
// Execute the pipeline as a MapReduce.
pipeline.done();
在本例中,我们首先创建一个MRPipeline管道,并使用它首先将用stream.py -t创建的sample.txt的内容读取到一个字符串集合中,其中该集合的每个元素代表一条 tweet。 我们使用tweet.split("\\s+")将每条 tweet 标记为单词,并发出与标签正则表达式匹配的每个单词,序列化为 Writable。 请注意,标记化和过滤操作由parallelDo调用创建的 MapReduce 作业并行执行。 我们创建了一个PTable,它将每个表示为字符串的 hashtag 与它在数据集中出现的次数关联起来。 最后,我们将PTable计数作为文本文件写入 HDFS。 使用pipeline.done()执行流水线。
要编译和执行管道,我们可以使用 Gradle 来管理所需的依赖项,如下所示:
$ ./gradlew jar
$ ./gradlew copyJars
将使用copyJars下载的 Crunch 和 Avro 依赖项添加到LIBJARS环境变量:
$ export CRUNCH_DEPS=build/libjars/crunch-example/lib
$ export LIBJARS=${LIBJARS},${CRUNCH_DEPS}/crunch-core-0.9.0-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-1.7.5-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-mapred-1.7.5-cdh5.0.3-hadoop2.jar
然后,在 Hadoop 上运行示例:
$ hadoop jar build/libs/crunch-example.jar \
com.learninghadoop2.crunch.HashtagCount \
tweets.json count-out \
-libjars $LIBJARS
数据序列化
框架的目标之一是使处理包含嵌套和重复数据结构(如协议缓冲区和 Thrift 记录)的复杂记录变得容易。
org.apache.crunch.types.PType接口定义了在 Crunch 管道中使用的数据类型与用于从 HDFS 读取数据/向 HDFS 写入数据的序列化和存储格式之间的映射。 每个PCollection都有一个关联的PType,它告诉 Crunch 如何读/写数据。
org.apache.crunch.types.PTypeFamily接口提供抽象工厂来实现共享相同序列化格式的PType实例。 目前,Crunch 支持两种类型的系列:一种基于 Writable 接口,另一种基于 Apache Avro。
备注
尽管 Crunch 允许在同一管道中混合和匹配使用PType的不同实例的PCollection接口,但每个PCollection接口的PType必须属于唯一的系列。 例如,不可能将键序列化为 Writable 并使用 avro 序列化其值的PTable。
这两个类型族都支持一组通用的原语类型(字符串、长整型、整型、浮点型、双精度型、布尔型和字节),以及可以由其他PTypes构造的更复杂的PType接口。 其中包括其他PType的元组和集合。 一个特别重要、复杂的PType是tableOf,它确定paralleDo的返回类型是PCollection还是PTable。
可以通过继承和扩展 Avro 和 Writable 族的内置内容来创建新的PTypes。 这需要实现 InputMapFn<S, T>和 Output MapFn<T, S>类。 我们为S是原始类型而T是新类型的实例实现PType。
派生的PTypes可以在PTypes类中找到。 其中包括对协议缓冲区、Thrift 记录、Java Enums、BigInteger 和 UUID 的序列化支持。 我们在章,使用 Apache Pig 进行数据分析中讨论的 Elephant Bird 库包含其他示例。
数据处理模式
org.apache.crunch.lib为常见的数据操作操作实现了许多设计模式。
聚合和排序
org.apache.crunch.lib提供的大多数数据处理模式依赖于PTable的groupByKey方法。该方法有三种不同的重载形式:
groupByKey():让规划人员确定分区的数量groupByKey(int numPartitions):用于设置开发者指定的分区数量groupByKey(GroupingOptions options):允许我们指定用于混洗的自定义分区和比较器
org.apache.crunch.GroupingOptions类采用 Hadoop 的Partitioner和RawComparator类的实例来实现自定义分区和排序操作。
groupByKey方法返回PGroupedTable的实例,PGroupedTable是 Crunch 对分组表格的表示。 它对应于 MapReduce 作业的混洗阶段的输出,并允许将值与combineValue方法组合。
org.apache.crunch.lib.Aggregate包公开了对PCollection实例执行简单聚合(count、max、top 和 length)的方法。
Sort 提供了一个 API 来对其内容实现Comparable接口的PCollection和PTable实例进行排序。
默认情况下,Crunch 使用一个缩减器对数据进行排序。 可以通过将所需的分区数传递给sort方法来修改此行为。 Sort.Order方法用信号表示应该进行排序的顺序。
下面是如何为集合指定不同的排序选项:
public static <T> PCollection<T> sort(PCollection<T> collection)
public static <T> PCollection<T> sort(PCollection<T> collection, Sort.Order order)
public static <T> PCollection<T> sort(PCollection<T> collection, int numReducers, Sort.Order order)
下面是如何为表指定不同的排序选项:
public static <K,V> PTable<K,V> sort(PTable<K,V> table)
public static <K,V> PTable<K,V> sort(PTable<K,V> table, Sort.Order key)
public static <K,V> PTable<K,V> sort(PTable<K,V> table, int numReducers, Sort.Order key)
最后,sortPairs使用Sort.ColumnOrder中指定的列顺序对PCollection对进行排序:
sortPairs(PCollection<Pair<U,V>> collection, Sort.ColumnOrder... columnOrders)
连接数据
org.apache.crunch.lib.Join包是基于公共密钥加入PTables的 API。 支持以下四种联接操作:
fullJoinjoin(默认为innerJoin)leftJoinrightJoin
这些方法具有共同的返回类型和签名。 作为参考,我们将描述实现内部联接的常用join方法:
public static <K,U,V> PTable<K,Pair<U,V>> join(PTable<K,U> left, PTable<K,V> right)
org.apache.crunch.lib.Join.JoinStrategy包提供了定义自定义联接策略的接口。 Crunch 的默认策略(defaultStrategy)是连接数据减少端。
管道实施和执行
Crunch 伴随着管道接口的三个实现而来。 本章隐含使用的最旧的是org.apache.crunch.impl.mr.MRPipeline,它使用 Hadoop 的 MapReduce 作为其执行引擎。 org.apache.crunch.impl.mem.MemPipeline允许在内存中执行所有操作,而不执行到磁盘的序列化。 Crunch 0.10 引入了org.apache.crunch.impl.spark.SparkPipeline,它编译并运行 Apache Spark 的 DAGPCollections。
SparkPippeline
使用 SparkPipeline,Crunch 将的大部分执行任务委托给 Spark,并执行相对较少的计划任务,以下例外情况除外:
- 多路输入
- 多路输出
- 数据序列化
- 检查点设置
在撰写本文时,SparkPipeline 仍在大力开发中,可能无法处理标准 MRPipeline 的所有用例。 Crunch 社区正在积极工作,以确保两种实现之间的完全兼容性。
Pippeline
MemPipeline 在客户机上执行内存中的。 与 MRPipeline 不同,MemPipeline 不是显式创建的,而是通过调用静态方法MemPipeline.getInstance()引用的。 所有操作都在内存中,PTypes 的使用非常少。
压缩示例
现在我们将使用 Apache Crunch 以更模块化的方式重新实现到目前为止编写的一些 MapReduce 代码。
词语共现
在第 3 章,Processing-MapReduce and Beyond中,我们展示了一个 MapReduce 作业 BiGramCount,用于计算 tweet 中单词的共现次数。 同样的逻辑可以实现为DoFn。 使用 Crunch,我们可以使用复杂类型Pair<String, String>,而不是发出多字段键并在稍后阶段对其进行解析,如下所示:
class BiGram extends DoFn<String, Pair<String, String>> {
@Override
public void process(String tweet,
Emitter<Pair<String, String>> emitter) {
String[] words = tweet.split(" ") ;
Text bigram = new Text();
String prev = null;
for (String s : words) {
if (prev != null) {
emitter.emit(Pair.of(prev, s));
}
prev = s;
}
}
}
请注意,与 MapReduce 相比,BiGramCrunch 实现是一个独立的类,可以在任何其他代码库中轻松重用。 此示例的代码包含在github.com/learninghad…中。
TF-IDF
我们可以使用MRPipeline实现 TF-IDF 作业链,如下所示:
public class CrunchTermFrequencyInvertedDocumentFrequency
extends Configured implements Tool, Serializable {
private Long numDocs;
@SuppressWarnings("deprecation")
public static class TF {
String term;
String docId;
int frequency;
public TF() {}
public TF(String term,
String docId, Integer frequency) {
this.term = term;
this.docId = docId;
this.frequency = (int) frequency;
}
}
public int run(String[] args) throws Exception {
if(args.length != 2) {
System.err.println();
System.err.println("Usage: " + this.getClass().getName() + " [generic options] input output");
return 1;
}
// Create an object to coordinate pipeline creation and execution.
Pipeline pipeline =
new MRPipeline(TermFrequencyInvertedDocumentFrequency.class, getConf());
// enable debug options
pipeline.enableDebug();
// Reference a given text file as a collection of Strings.
PCollection<String> tweets = pipeline.readTextFile(args[0]);
numDocs = tweets.length().getValue();
// We use Avro reflections to map the TF POJO to avsc
PTable<String, TF> tf = tweets.parallelDo(new TermFrequencyAvro(), Avros.tableOf(Avros.strings(), Avros.reflects(TF.class)));
// Calculate DF
PTable<String, Long> df = Aggregate.count(tf.parallelDo( new DocumentFrequencyString(), Avros.strings()));
// Finally we calculate TF-IDF
PTable<String, Pair<TF, Long>> tfDf = Join.join(tf, df);
PCollection<Tuple3<String, String, Double>> tfIdf = tfDf.parallelDo(new TermFrequencyInvertedDocumentFrequency(),
Avros.triples(
Avros.strings(),
Avros.strings(),
Avros.doubles()));
// Serialize as avro
tfIdf.write(To.avroFile(args[1]));
// Execute the pipeline as a MapReduce.
PipelineResult result = pipeline.done();
return result.succeeded() ? 0 : 1;
}
…
}
与流式传输相比,我们在这里遵循的方法具有许多优势。 首先,我们不需要使用单独的脚本手动链接 MapReduce 作业。 这项任务是 Crunch 的主要目的。 其次,我们可以将度量的每个组件表示为不同的类,使其更容易在未来的应用中重用。
为了实现词频,我们创建了一个DoFn类,它接受 tweet 作为输入并发出Pair<String, TF>。 第一个元素是一个术语,第二个元素是将使用 avro 序列化的 POJO 类的实例。 TF部分包含三个变量: term、documentId和frequency。 在引用实现中,我们希望输入数据是我们反序列化和解析的 JSON 字符串。 我们还将标记化作为 Process 方法的一个子任务。
根据用例的不同,我们可以分别在和DoFns中抽象这两个操作,如下所示:
class TermFrequencyAvro extends DoFn<String,Pair<String, TF>> {
public void process(String JSONTweet,
Emitter<Pair <String, TF>> emitter) {
Map<String, Integer> termCount = new HashMap<>();
String tweet;
String docId;
JSONParser parser = new JSONParser();
try {
Object obj = parser.parse(JSONTweet);
JSONObject jsonObject = (JSONObject) obj;
tweet = (String) jsonObject.get("text");
docId = (String) jsonObject.get("id_str");
for (String term : tweet.split("\\s+")) {
if (termCount.containsKey(term.toLowerCase())) {
termCount.put(term,
termCount.get(term.toLowerCase()) + 1);
} else {
termCount.put(term.toLowerCase(), 1);
}
}
for (Entry<String, Integer> entry : termCount.entrySet()) {
emitter.emit(Pair.of(entry.getKey(), new TF(entry.getKey(), docId, entry.getValue())));
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}
文档频率很简单。 对于在项频率步骤中生成的每个Pair<String, TF>,我们发出项-该对的第一个元素。 我们汇总并计算得到的术语的PCollection,以获得文档频率,如下所示:
class DocumentFrequencyString extends DoFn<Pair<String, TF>, String> {
@Override
public void process(Pair<String, TF> tfAvro,
Emitter<String> emitter) {
emitter.emit(tfAvro.first());
}
}
最后,我们将共享密钥(Term)上的PTableTF 与PTableDF 连接起来,并将得到的Pair<String, Pair<TF, Long>>对象提供给TermFrequencyInvertedDocumentFrequency。
对于每个术语和文档,我们计算 TF-IDF 并返回term、docIf和tfIdf三元组:
class TermFrequencyInvertedDocumentFrequency extends MapFn<Pair<String, Pair<TF, Long>>, Tuple3<String, String, Double> > {
@Override
public Tuple3<String, String, Double> map(
Pair<String, Pair<TF, Long>> input) {
Pair<TF, Long> tfDf = input.second();
Long df = tfDf.second();
TF tf = tfDf.first();
double idf = 1.0+Math.log(numDocs / df);
double tfIdf = idf * tf.frequency;
return Tuple3.of(tf.term, tf.docId, tfIdf);
}
}
我们使用MapFn,因为我们将为每个输入输出一条记录。 本例的源代码可以在github.com/learninghad…找到。
可以使用以下命令编译和执行该示例:
$ ./gradlew jar
$ ./gradlew copyJars
如果尚未完成,请将使用copyJars下载的 Crunch 和 Avro 依存关系添加到LIBJARS环境变量,如下所示:
$ export CRUNCH_DEPS=build/libjars/crunch-example/lib
$ export LIBJARS=${LIBJARS},${CRUNCH_DEPS}/crunch-core-0.9.0-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-1.7.5-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-mapred-1.7.5-cdh5.0.3-hadoop2.jar
此外,将json-simpleJAR 添加到LIBJARS:
$ export LIBJARS=${LIBJARS},${CRUNCH_DEPS}/json-simple-1.1.1.jar
最后,将CrunchTermFrequencyInvertedDocumentFrequency作为 MapReduce 作业运行,如下所示:
$ hadoop jar build/libs/crunch-example.jar \
com.learninghadoop2.crunch.CrunchTermFrequencyInvertedDocumentFrequency \
-libjars ${LIBJARS} \
tweets.json tweets.avro-out
风筝睡眠线
Kite Morphines 是一个数据转换库,灵感来自 Unix 管道,最初是作为 Cloudera 搜索的一部分开发的。 变形线是内存中的转换命令链,它依赖插件结构来利用异构数据源。 它使用声明性命令对记录执行 ETL 操作。 命令在配置文件中定义,该文件稍后将提供给驱动程序类。
其目标是通过提供一个允许开发人员用一系列配置设置替换编程的库,使将 ETL 逻辑嵌入到任何 Java 代码库中成为一项微不足道的任务。
概念
形态线是围绕两个抽象构建的:Command和Record。
记录是org.kitesdk.morphline.api.Record接口的实现:
public final class Record {
private ArrayListMultimap<String, Object> fields;
…
private Record(ArrayListMultimap<String, Object> fields) {…}
public ListMultimap<String, Object> getFields() {…}
public List get(String key) {…}
public void put(String key, Object value) {…}
…
}
记录是一组个命名字段,其中每个字段都有一个包含一个或多个值的列表。 Record是在 Google Guava 的ListMultimap和ArrayListMultimap类之上实现的。 请注意,值可以是任何 Java 对象,字段可以是多值的,并且两条记录不需要使用公共字段名。 记录可以包含_attachment_body字段,该字段可以是java.io.InputStream或字节数组。
命令实现org.kitesdk.morphline.api.Command接口:
public interface Command {
void notify(Record notification);
boolean process(Record record);
Command getParent();
}
命令将记录转换为零个或多个记录。 命令可以调用为读写操作以及添加或删除字段提供的Record实例上的方法。
命令被链接在一起,在变形线的每一步,父命令将记录发送给子命令,子命令继而处理这些记录。 父母和孩子之间使用两个通信通道(平面)交换信息;通知通过控制平面发送,记录通过数据平面发送。 记录由process()方法处理,该方法返回一个布尔值以指示是否应该继续进行变形线。
命令不是直接实例化的,而是通过实现org.kitesdk.morphline.api.CommandBuilder接口来实例化的:
public interface CommandBuilder {
Collection<String> getNames();
Command build(Config config,
Command parent,
Command child,
MorphlineContext context);
}
getNames方法返回可用于调用命令的名称。 支持多个名称以允许向后兼容名称更改。 build()方法创建并返回一个以给定的变形线配置为根的命令。
org.kitesdk.morphline.api.MorphlineContext接口允许将附加参数传递给所有 Morphline 命令。
条形态线的数据模型是按照源-管-宿模式构建的,在这种模式下,从源捕获数据,通过多个处理步骤通过管道传输数据,然后将其输出传送到宿中。
Morphline 命令
Kite Morphines 附带了许多默认命令,这些命令实现了常见序列化格式(纯文本、Avro、JSON)的数据转换。 当前可用的命令组织为变形线的子项目,包括:
kite-morphlines-core-stdio:将从二进制大型对象(BLOB)和文本读取数据kite-morphlines-core-stdlib:包装用于数据操作和表示的 Java 数据类型kite-morphlines-avro:是,用于序列化和反序列化 avro 格式的数据kite-morphlines-json:将序列化和反序列化 JSON 格式的数据kite-morphlines-hadoop-core:是否用于访问 HDFSkite-morphlines-hadoop-parquet-avro:是,用于序列化和反序列化 Parquet 格式的数据kite-morphlines-hadoop-sequencefile:用于序列化和反序列化 Sequencefile 格式的数据kite-morphlines-hadoop-rcfile:使用序列化和反序列化 RC 文件格式的数据
所有可用命令的列表可在kitesdk.org/docs/0.17.0…中找到。
命令是通过在配置文件morphline.conf中声明一系列转换来定义的,然后由驱动程序编译并执行该配置文件。 例如,我们可以指定一个read_tweets变形行,它将加载存储为 JSON 数据的 tweet,使用 Jackson 序列化和反序列化它们,并通过组合org.kitesdk.morphline包中包含的默认readJson和head命令打印前 10 个,如下所示:
morphlines : [{
id : read_tweets
importCommands : ["org.kitesdk.morphline.**"]
commands : [{
readJson {
outputClass : com.fasterxml.jackson.databind.JsonNode
}}
{
head {
limit : 10
}}
]
}]
现在,我们将展示如何从独立的 Java 程序和 MapReduce 执行此变形线。
MorphlineDriver.java显示了如何使用嵌入到主机系统中的库。 我们在 main方法中执行的第一步是加载 Morphline 的 JSON 配置,构建一个MorphlineContext对象,并将其编译成Command的实例,该实例充当 Morphline 的起始节点。 请注意,Compiler.compile()接受一个finalChild参数;在本例中,它是RecordEmitter。 我们使用RecordEmitter作为形态线的接收器,要么将记录打印到 stdout,要么将其存储到 HDFS 中。 在MorphlineDriver示例中,我们使用org.kitesdk.morphline.base.Notifications以事务的方式管理和监视 Morphline 生命周期。
调用Notifications.notifyStartSession(morphline)将在通过调用Notifications.notifyBeginTransaction定义的事务内启动转换链。 成功后,我们使用Notifications.notifyShutdown(morphline)终止管道。 在失败的情况下,我们回滚事务Notifications.notifyRollbackTransaction(morphline),并将异常处理程序从变形行上下文传递到调用 Java 代码:
public class MorphlineDriver {
private static final class RecordEmitter implements Command {
private final Text line = new Text();
@Override
public Command getParent() {
return null;
}
@Override
public void notify(Record record) {
}
@Override
public boolean process(Record record) {
line.set(record.get("_attachment_body").toString());
System.out.println(line);
return true;
}
}
public static void main(String[] args) throws IOException {
/* load a morphline conf and set it up */
File morphlineFile = new File(args[0]);
String morphlineId = args[1];
MorphlineContext morphlineContext = new MorphlineContext.Builder().build();
Command morphline = new Compiler().compile(morphlineFile, morphlineId, morphlineContext, new RecordEmitter());
/* Prepare the morphline for execution
*
* Notifications are sent through the communication channel
* */
Notifications.notifyBeginTransaction(morphline);
/* Note that we are using the local filesystem, not hdfs*/
InputStream in = new BufferedInputStream(new FileInputStream(args[2]));
/* fill in a record and pass it over */
Record record = new Record();
record.put(Fields.ATTACHMENT_BODY, in);
try {
Notifications.notifyStartSession(morphline);
boolean success = morphline.process(record);
if (!success) {
System.out.println("Morphline failed to process record: " + record);
}
/* Commit the morphline */
} catch (RuntimeException e) {
Notifications.notifyRollbackTransaction(morphline);
morphlineContext.getExceptionHandler().handleException(e, null);
}
finally {
in.close();
}
/* shut it down */
Notifications.notifyShutdown(morphline);
}
}
在本例中,我们将 JSON 格式的数据从本地文件系统加载到一个InputStream对象中,并使用它来初始化一个新的Record实例。 RecordEmitter类包含链的最后一个处理的记录实例,我们在该实例上提取_attachment_body并将其打印到标准输出。 MorphlineDriver的源代码可以在github.com/learninghad…中找到。
使用 MapReduce 作业中相同的变形线非常简单。 在 Mapper 的设置阶段,我们构建一个包含实例化逻辑的上下文,而 map 方法设置Record对象并触发处理逻辑,如下所示:
public static class ReadTweets
extends Mapper<Object, Text, Text, NullWritable> {
private final Record record = new Record();
private Command morphline;
@Override
protected void setup(Context context)
throws IOException, InterruptedException {
File morphlineConf = new File(context.getConfiguration()
.get(MORPHLINE_CONF));
String morphlineId = context.getConfiguration()
.get(MORPHLINE_ID);
MorphlineContext morphlineContext =
new MorphlineContext.Builder()
.build();
morphline = new org.kitesdk.morphline.base.Compiler()
.compile(morphlineConf,
morphlineId,
morphlineContext,
new RecordEmitter(context));
}
public void map(Object key, Text value, Context context)
throws IOException, InterruptedException {
record.put(Fields.ATTACHMENT_BODY,
new ByteArrayInputStream(
value.toString().getBytes("UTF8")));
if (!morphline.process(record)) {
System.out.println(
"Morphline failed to process record: " + record);
}
record.removeAll(Fields.ATTACHMENT_BODY);
}
}
在 MapReduce 代码中,我们修改了RecordEmitter以从后处理的记录中提取Fields有效负载,并将其存储到上下文中。 这允许我们通过在 MapReduce 配置样板中指定FileOutputFormat将数据写入 HDFS:
private static final class RecordEmitter implements Command {
private final Text line = new Text();
private final Mapper.Context context;
private RecordEmitter(Mapper.Context context) {
this.context = context;
}
@Override
public void notify(Record notification) {
}
@Override
public Command getParent() {
return null;
}
@Override
public boolean process(Record record) {
line.set(record.get(Fields.ATTACHMENT_BODY).toString());
try {
context.write(line, null);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
请注意,我们现在可以通过修改morphline.conf来更改处理管道行为并添加进一步的数据转换,而无需明确更改实例化和处理逻辑。 MapReduce 驱动程序源代码可以在github.com/learninghad…中找到。
这两个示例都可以使用以下命令从ch9/kite/编译:
$ ./gradlew jar
$ ./gradlew copyJar
我们将runtime依赖项添加到LIBJARS,如下所示
$ export KITE_DEPS=/home/cloudera/review/hadoop2book-private-reviews-gabriele-ch8/src/ch8/kite/build/libjars/kite-example/lib
export LIBJARS=${LIBJARS},${KITE_DEPS}/kite-morphlines-core-0.17.0.jar,${KITE_DEPS}/kite-morphlines-json-0.17.0.jar,${KITE_DEPS}/metrics-core-3.0.2.jar,${KITE_DEPS}/metrics-healthchecks-3.0.2.jar,${KITE_DEPS}/config-1.0.2.jar,${KITE_DEPS}/jackson-databind-2.3.1.jar,${KITE_DEPS}/jackson-core-2.3.1.jar,${KITE_DEPS}/jackson-annotations-2.3.0.jar
我们可以使用以下内容运行 MapReduce 驱动程序:
$ hadoop jar build/libs/kite-example.jar \
com.learninghadoop2.kite.morphlines.MorphlineDriverMapReduce \
-libjars ${LIBJARS} \
morphline.conf \
read_tweets \
tweets.json \
morphlines-out
可以使用以下命令执行 Java 独立驱动程序:
$ export CLASSPATH=${CLASSPATH}:${KITE_DEPS}/kite-morphlines-core-0.17.0.jar:${KITE_DEPS}/kite-morphlines-json-0.17.0.jar:${KITE_DEPS}/metrics-core-3.0.2.jar:${KITE_DEPS}/metrics-healthchecks-3.0.2.jar:${KITE_DEPS}/config-1.0.2.jar:${KITE_DEPS}/jackson-databind-2.3.1.jar:${KITE_DEPS}/jackson-core-2.3.1.jar:${KITE_DEPS}/jackson-annotations-2.3.0.jar:${KITE_DEPS}/slf4j-api-1.7.5.jar:${KITE_DEPS}/guava-11.0.2.jar:${KITE_DEPS}/hadoop-common-2.3.0-cdh5.0.3.jar
$ java -cp $CLASSPATH:./build/libs/kite-example.jar \
com.learninghadoop2.kite.morphlines.MorphlineDriver \
morphline.conf \
read_tweets tweets.json \
morphlines-out
摘要
在本章中,我们介绍了四个简化 Hadoop 开发的工具。 我们特别介绍了以下内容:
- Hadoop Streaming 如何允许使用动态语言编写 MapReduce 作业
- Kite Data 如何简化与异构数据源的接口
- Apache Crunch 如何提供高级抽象来编写实现通用设计模式的 Spark 和 MapReduce 作业的管道
- Morphline 如何允许我们声明命令和数据转换链,然后这些命令和数据转换可以嵌入到任何 Java 代码库中
在第 10 章,运行 Hadoop2 集群中,我们将把重点从软件开发领域转移到系统管理上。 我们将讨论如何设置、管理和扩展 Hadoop 集群,同时考虑到监控和安全性等方面。
十、运行 Hadoop 集群
在本章中,我们将稍微改变我们的关注点,看看在运行可操作的 Hadoop 集群时您将面临的一些注意事项。 我们将特别介绍以下主题:
- 为什么开发人员应该关心操作,为什么 Hadoop 操作不同
- 有关 Cloudera Manager 及其功能和限制的更多详细信息
- 设计既可在物理硬件上使用又可在 EMR 上使用的集群
- 保护 Hadoop 集群的安全
- Hadoop 监控
- 对在 Hadoop 上运行的应用问题进行故障排除
我是一名开发人员-我不关心操作!
在进一步讨论之前,我们需要解释一下为什么我们要在一本直接面向开发人员的书中加入一个关于系统操作的章节。 对于任何为更传统的平台(例如,Web 应用、数据库编程等)进行开发的人来说,规范很可能是在开发和运营之间进行非常清晰的划分。 第一组构建代码并将其打包,第二组控制和操作代码运行的环境。
近年来,DevOps 运动获得了发展势头,他们相信,如果这些孤岛被移除,团队之间的合作更加紧密,对每个人都是最好的。 当涉及到运行基于 Hadoop 的系统和服务时,我们相信这是绝对必要的。
Hadoop 和 DevOps 实践
尽管开发人员可以从概念上构建一个随时可以被遗忘的应用,但实际情况往往更加微妙。 在运行时分配给应用的资源数量很可能是开发人员希望影响的。 一旦应用开始运行,操作人员在尝试优化集群时可能需要深入了解应用。 这确实不像传统企业 IT 中看到的那样明确划分职责。 这可能是一件非常好的事情。
换句话说,开发人员需要更多地了解操作方面,操作人员也需要更多地了解开发人员在做什么。 因此,请将本章视为我们帮助您与运营人员进行这些讨论的贡献。 我们不打算在本章结束时让您成为 Hadoop 专家管理员;这本身就是一个专门的角色和技能集。 取而代之的是,我们将对您确实需要了解的问题进行一站式巡视,一旦您的应用在实时集群上运行,这将使您的工作变得更轻松。
根据这篇报道的性质,我们将涉及很多主题,而且只会略微深入;如果有更深层次的兴趣,我们会提供进一步调查的链接。 只要确保你的操作人员参与进来就行了!
Cloudera 管理器
在本书中,我们使用了Cloudera Hadoop Distribution(CDH)作为最常用的平台,它具有便捷的 QuickStart 虚拟机和强大的 Cloudera Manager 应用。 使用基于 Cloudera 的集群,Cloudera Manager 将成为(至少最初)您进入系统的主要界面,用于管理和监视集群,所以让我们来探索一下。
请注意,Cloudera Manager 拥有大量高质量的在线文档。 我们不会在这里重复此文档;相反,我们将尝试强调 Cloudera Manager 在您的开发和运营工作流中的位置,以及您可能想要接受它还是不想接受它。 Cloudera Manager 的最新版本和以前版本的文档可以通过主 Cloudera 文档页面www.cloudera.com/content/sup…访问。
付款或不付款
在对 Cloudera Manager 感到兴奋之前,有一点很重要,那就是查阅当前的文档,了解免费版本中有哪些功能可用,哪些功能需要订阅付费 Cloudera 产品。 如果你绝对想要付费版本提供的一些功能,但又不能或不想为订阅服务付费,那么 Cloudera Manager,甚至整个 Cloudera 发行版,可能都不太适合你。 我们将在第 11 章、*、*中回到这个主题。
使用 Cloudera Manager 进行集群管理
使用 QuickStart VM 不会很明显,但 Cloudera Manager 是用于管理集群中所有服务的主要工具。 如果您想启用一项新服务,您将使用 Cloudera Manager。 要更改配置,您需要 Cloudera Manager。 要升级到最新版本,您将再次需要 Cloudera Manager。
即使集群的主要管理是由操作人员处理的,作为开发人员,您可能仍然希望熟悉 Cloudera Manager 界面,以便查看集群到底是如何配置的。 如果您的作业运行缓慢,那么查看 Cloudera Manager 以了解当前的配置情况很可能是您的第一步。 Cloudera Manager Web 界面的默认端口为7180,因此主页通常通过 URL(如http://<hostname>:7180/cmf/home)连接,如以下屏幕截图所示:
Cloudera Manager 主页
查看界面是值得的;但是,如果您要连接具有管理员权限的用户帐户,请小心!
单击Clusters链接,这将展开,给出当前由 Cloudera Manager 实例管理的集群的列表。 这应该告诉您单个 Cloudera Manager 实例可以管理多个集群。 这非常有用,特别是在开发和生产中有许多集群的情况下。
对于每个扩展的集群,将有当前在集群上运行的服务的列表。 单击一项服务,然后您将看到其他选项列表。 选择配置,您可以开始浏览该特定服务的详细配置。 单击Actions,您将获得一些特定于服务的选项;这通常包括停止、启动、重新启动和管理服务。
单击Hosts选项而不是Clusters,您可以开始深入查看 Cloudera Manager 管理的服务器,并从那里查看在每个服务器上部署了哪些服务组件。
Cloudera 管理器和其他管理工具
最后一条评论可能会提出一个问题:Cloudera Manager 如何与其他系统管理工具集成? 鉴于我们早先关于 DevOps 理念的重要性的评论,它与 DevOps 环境中受欢迎的工具的集成情况如何?
诚实的回答是:并不总是很好。 尽管主 Cloudera Manager 服务器本身可以由自动化工具(如 Pupet 或 Chef)管理,但有一个明确的假设是,Cloudera Manager 将控制 Cloudera Manager 在其集群中包含的所有主机上安装和配置 Cloudera Manager 所需的所有软件。 对于一些管理员来说,这使得 Cloudera Manager 背后的硬件看起来像一个大的黑匣子;他们可能控制基本操作系统的安装,但未来配置基线的管理完全由 Cloudera Manager 管理。 这里没有什么可做的;它就是这样-为了获得 Cloudera Manager 的好处,它会将自身作为一个新的管理系统添加到您的基础架构中,而这与您更广泛的环境的契合度将视具体情况而定。
使用 Cloudera Manager 进行监控
在系统监控方面也可以提出类似的观点,因为 Cloudera Manager 在概念上也是一个复制点。 但是开始在界面上单击,很快就会发现 Cloudera Manager 提供了一套极其丰富的工具来评估托管集群的运行状况和性能。
从绘制 Impala 查询的相对性能图,到显示 Yarn 应用的作业状态,再到提供存储在 HDFS 上的块的低级数据,所有这些都可以在一个界面中完成。 我们将在本章后面讨论 Hadoop 上的故障排除是如何具有挑战性的,但 Cloudera Manager 提供的单点可见性是评估集群运行状况或性能的一个很好的工具。 我们将在本章后面更详细地讨论监控。
查找配置文件
当运行由 Cloudera Manager 管理的集群时,首先面临的困惑之一就是试图查找该集群使用的配置文件。 在产品的普通 Apache 发行版中,比如核心 Hadoop,通常会有文件存储在/etc/hadoop中,类似地,/etc/hive存储在 hive 中,/etc/oozie存储在 oozie 中,依此类推。
然而,在 Cloudera Manager 管理的集群中,每次重新启动服务时都会重新生成配置文件,并且配置文件不是位于文件系统的/etc位置,而是位于/var/run/cloudera-scm-agent-process/<pid>-<task name>/,其中最后一个目录的名称可能是7007-yarn-NODEMANAGER。 对于任何习惯于使用早期 Hadoop 集群或其他不做此类操作的发行版的人来说,这可能看起来很奇怪。 但在 Cloudera Manager 控制的集群中,使用 Web 界面浏览配置通常比查找底层配置文件更容易。 哪种方法最好? 这有点哲理,每个团队都需要决定哪一个最适合他们。
Cloudera Manager API
我们只给出了 Cloudera Manager 的最高级别概述,在这样做的过程中,完全忽略了一个可能对某些组织非常有用的领域:Cloudera Manager 提供了一个允许将其功能集成到其他系统和工具中的 API。 如果您可能对此感兴趣,请参考文档。
Cloudera Manager 锁定
这就把我们带到了围绕 Cloudera Manager 的整个讨论中隐含的观点:它确实在一定程度上锁定了 Cloudera 及其发行版。 这种锁定可能只以某些方式存在;例如,代码应该可以跨集群移植,模数是关于不同底层版本的常见警告-但是集群本身可能不容易重新配置为使用不同的发行版。 假设切换发行版将是一个完全的删除/重新格式化/重新安装活动。
我们并不是说不要使用它,而是您需要注意 Cloudera Manager 的使用带来的锁定。 对于几乎没有专门的运营支持或现有基础设施的小型团队来说,Cloudera Manager 为您提供的重要功能可能会盖过这种锁定的影响。
对于较大的团队或在与现有工具和流程集成更重要的环境中工作的团队来说,决策可能不那么明确。 查看 Cloudera Manager,与您的运营人员讨论,确定最适合您的产品。
请注意,可以手动下载并安装 Cloudera 发行版的各种组件,而无需使用 Cloudera Manager 来管理集群及其主机。 对于一些用户来说,这可能是一个有吸引力的中间选择,因为可以使用 Cloudera 软件,但部署和管理可以内置到现有的部署和管理工具中。 这也可能是避免前面提到的付费 Cloudera 支持级别的额外费用的一种方式。
Ambari-开源替代方案
Ambari 是一个 Apache 项目(ambari.apache.org),从理论上讲,它提供了 Cloudera Manager 的开源替代方案。 它是 Hortonworks 发行版的管理控制台。 在撰写本文时,Hortonworks 的员工也是项目的绝大多数贡献者。
考虑到 Ambari 的开源特性,人们可以预料到,它依赖于其他开源产品,如 Pupet 和 Nagios,来提供对其托管集群的管理和监控。 它还具有类似于 Cloudera Manager 的高级功能,即安装、配置、管理和监视 Hadoop 集群以及其中的组件服务。
了解 Ambari 项目是件好事,因为选择不仅仅是完全锁定 Cloudera 和 Cloudera Manager,还是手动管理集群。 Ambari 提供了一个图形化的工具,随着它的成熟,它可能值得考虑,甚至可以参与进来。 在 HDP 集群上,可通过http://<hostname>:8080/#/main/dashboard访问与前面所示的 Cloudera Manager 主页相当的 Ambari UI,其屏幕截图如下所示:
安巴里
Hadoop 2 世界中的操作
正如在第 2 章,Storage中提到的,Hadoop2 中对 HDFS 所做的一些最重要的更改涉及其容错性和与外部系统的更好集成。 这不仅仅是出于好奇,尤其是 NameNode 的高可用性特性,从 Hadoop 1 开始就对集群的管理产生了巨大的影响。在 2012 年左右糟糕的过去,Hadoop 集群的运营准备的很大一部分都是围绕 NameNode 故障的缓解和恢复过程来构建的。 如果在 Hadoop1 中 NameNode 死了,并且您没有 HDFSfsimage元数据文件的备份,那么您基本上就失去了对所有数据的访问权限。 如果元数据永久丢失,那么数据也会永久丢失。
Hadoop2 增加了内置的 NameNode HA 和使其工作的机制。 此外,还有一些组件,如进入 HDFS 的 NFS 网关,这使得它成为一个更加灵活的系统。 但这种额外的能力确实是以牺牲更多的活动部件为代价的。 要启用 NameNode HA,JournalManager 和 FailoverController 中还有其他组件,并且 NFS 网关需要特定于 Hadoop 的 portmap 和 nfsd 服务实现。
Hadoop2 现在还拥有个与外部服务的广泛的其他集成点,以及在其上运行的更广泛的应用和服务选择。 因此,从操作的角度看 Hadoop2 可能会很有用,因为它牺牲了 Hadoop1 的简单性,换取了额外的复杂性,从而提供了更强大的平台。
资源共享
在 Hadoop1 中,人们必须考虑资源共享的唯一时刻是考虑将哪个调度器用于 MapReduce JobTracker。 由于所有作业最终都被转换为 MapReduce 代码,因此在 MapReduce 级别拥有资源共享策略通常足以管理大量集群工作负载。
Hadoop 2 和 Yarn 改变了这一局面。 除了运行许多 MapReduce 作业之外,一个集群还可能在其他 YAR ApplicationMaster 之上运行许多其他应用。 TEZ 和 Spark 本身就是框架,它们在其提供的接口上运行额外的应用。
如果所有东西都在 Yarn 上运行,那么它提供了配置分配给应用的每个容器消耗的最大资源分配(在 CPU、内存和即将到来的 I/O 方面)的方法。 这里的主要目标是确保分配足够的资源以保持硬件的充分利用,而不会有未使用的容量或使其过载。
当非 Yarn 应用(如 Impala)在集群上运行并希望获取分配的容量片段(尤其是在 Impala 中的内存)时,事情会变得更加有趣。 比方说,如果您在相同的主机上以非 Yarn 模式运行 Spark,或者实际上任何其他分布式应用可能受益于 Hadoop 机器上的托管,也可能会发生这种情况。
基本上,在 Hadoop2 中,您需要更多地将集群看作一个多租户环境,需要更多地关注向各个租户分配资源。
这里确实没有什么灵丹妙药的建议;正确的配置将完全取决于托管的服务及其运行的工作负载。 这是另一个示例,您希望与运营团队密切合作,使用阈值执行一系列负载测试,以确定各种客户端的资源要求是什么,以及哪种方法可以提供最高的利用率和性能。 Cloudera 工程师的以下博客文章很好地概述了他们如何让 Impala 和 MapReduce 有效共存来解决这个问题:blog.cloudera.com/blog/2013/0…。
构建物理集群
在考虑硬件资源分配之前,有一个较小的要求:定义和选择用于集群的硬件。 在本节中,我们将讨论物理集群,并在下一节中继续讨论 Amazon EMR。
任何特定的硬件建议一经撰写就会过期。 我们建议仔细阅读各个 Hadoop 发行版供应商的网站,因为他们经常就当前推荐的配置撰写新文章。
我们不会告诉您需要多少内核或 GB 内存,而是从稍微高一点的级别来看硬件选择。 首先要意识到的是,运行 Hadoop 集群的主机很可能与企业的其他主机看起来非常不同。 Hadoop 针对低(ER)成本的硬件进行了优化,因此不会看到少量非常大的服务器,而应该看到更多具有更少企业可靠性功能的机器。 但不要认为 Hadoop 会在你身边的任何垃圾上运行得很好。 有可能,但最近典型 Hadoop 服务器的配置已经远离了低端市场,相反,最适合的似乎是中端服务器,在那里可以以较低的价格实现最大的核心/磁盘/内存。
与存储数据和执行应用逻辑的工作节点不同,对于运行 HDFS NameNode 或 Yar ResourceManager 等服务的主机,您还应该有不同的资源需求。 对于前者,通常对大量存储的需求要小得多,但通常需要更大的内存和可能更快的磁盘。
对于 Hadoop 工作节点,核心、内存和 I/O 这三个主要硬件类别之间的比率通常是最重要的。 这将直接为您做出有关工作负荷和资源分配的决策提供信息。
例如,许多工作负载往往会受到 I/O 限制,在主机上分配的容器数量是物理磁盘数量的许多倍,实际上可能会因为争用旋转磁盘而导致整体速度减慢。 在撰写本文时,当前的建议是 Yarn 容器的数量不超过磁盘数量的 1.8 倍。 如果您的工作负载是 I/O 受限的,那么您很可能会通过向集群添加更多主机来获得更好的性能,而不是尝试在当前主机上运行更多容器、更快的处理器或更多内存。
相反,如果您希望运行大量并发的 Impala、Spark 和其他需要大量内存的作业,那么内存可能很快就会成为压力最大的资源。 这就是为什么即使您可以从发行商那里获得通用集群的最新硬件建议,您仍然需要针对您的预期工作负载进行验证并进行相应的定制。 在小型测试集群上或在 EMR 上进行基准测试确实是不可替代的,它可以成为探索多个应用的资源需求的一个很好的平台,这些应用可以为硬件采购决策提供信息。 也许 EMR 可能是您的主要环境;如果是这样,我们将在后面的部分讨论这一点。
物理布局
如果您确实使用物理集群,您将需要考虑一些在 EMR 上基本上是透明的事情。
机架感知
对于集群来说,这些方面的第一个方面是构建机架感知,这些集群足够大,可以占用一个以上的数据中心空间。 如第 2 章、存储中所述,当 HDFS 放置新文件的副本时,它会尝试将第二个副本放置在与第一个副本不同的主机上,并将第三个副本放置在多机架系统中不同的设备机架中。 此启发式方法旨在最大限度地提高恢复能力;即使整个设备机架发生故障,也至少有一个副本可用。 MapReduce 使用类似的逻辑来尝试获得更均衡的任务分布。
如果您不执行任何操作,则每台主机都将被指定为位于单个默认机架中。 但是,如果集群增长超过这一点,您将需要更新机架名称。
在幕后,Hadoop 通过执行用户提供的将节点主机名映射到机架名称的脚本来发现节点的机架。 Cloudera Manager 允许在给定主机上设置机架名称,然后在 Hadoop 调用其机架识别脚本时检索该名称。 要为主机设置机架,请单击Hosts->->Assign Rack,然后从 Cloudera Manager 主页分配机架。
发文:2013 年 2 月 10 日星期日晚上 11:00
如前所述,您的集群中可能有两种类型的硬件:运行工作器的机器和运行服务器的机器。 在部署物理集群时,您需要确定哪些服务以及这些服务的哪些子组件在哪些物理机上运行。
对于工作者来说,这通常非常简单;大多数(尽管不是全部)服务在所有工作者主机上都有一个工作者代理模型。 但是,对于主/服务器组件,需要稍微考虑一下。 如果您有三个主节点,那么如何扩展您的主 NameNode 和备用 NameNode:Yarn 资源管理器、可能的色调、几个配置单元服务器和一个 Oozie 管理器? 其中一些功能高度可用,而另一些则不是。 随着您向集群中添加越来越多的服务,您还将看到这个主服务列表大幅增长。
在理想情况下,每个服务主机可能有一台主机,但这只适用于非常大的集群;在较小的安装中,它的成本高得令人望而却步。 另外,它可能总是有点浪费。 这里也没有一成不变的规则,但一定要查看可用的硬件,并尝试将服务尽可能地分布在节点上。 例如,不要让两个 NameNode 有两个节点,然后将其他所有内容放在第三个节点上。 考虑单个主机故障的影响,并管理布局以将其降至最低。 随着集群跨多个设备机架扩展,还需要考虑如何在单机架故障中幸存下来。 Hadoop 本身对此很有帮助,因为 HDFS 将尝试确保每个数据块在至少两个机架上都有副本。 但是,例如,如果所有主节点都驻留在单个机架中,则会削弱这种类型的弹性。
升级服务
升级 Hadoop 历来是一项耗时且有一定风险的任务。 在手动部署的集群(即不受 Cloudera Manager 等工具管理的集群)上仍然是这种情况。
如果您使用的是 Cloudera Manager,那么它会将耗时的部分从活动中去掉,但不一定会带来风险。 任何升级都应始终被视为发生意外问题的可能性很高的活动,您应该安排足够的集群停机时间来应对这种意外的兴奋。 在测试集群上进行测试升级确实是无可替代的,这强调了将 Hadoop 视为环境的一个组件的重要性,该组件需要像其他组件一样被视为部署生命周期。
有时升级需要修改 HDFS 元数据,或者可能会影响文件系统。 当然,这才是真正的风险所在。 除了运行测试升级外,还要注意将 HDFS 设置为升级模式的功能,这将有效地创建升级前文件系统状态的快照,并将一直保留到升级完成。 此非常有用,因为即使是出现严重错误并损坏数据的升级也有可能完全回滚。
在电子病历上构建集群
Elastic MapReduce 是一种灵活的解决方案,根据需求和工作负载,可以与物理 Hadoop 集群相邻,也可以替换物理 Hadoop 集群。 正如我们到目前为止已经看到的,EMR 提供了预加载和配置了配置单元、流和 Pig 的集群,以及允许执行 MapReduce 应用的自定义 JAR 集群。
第二个要区分的是短暂生命周期和长期生命周期。 按需生成临时 EMR 集群;将数据加载到 S3 或 HDFS 中,执行一些处理工作流,存储输出结果,然后自动关闭集群。 工作流终止后,长期运行的集群将保持活动状态,并且集群仍可用于复制新数据和执行新工作流。 长时间运行的集群通常非常适合数据仓库或处理足够大的数据集,因此与临时实例相比,加载和处理数据的效率会很低。
在一份面向潜在用户的必读白皮书(可在media.amazonwebservices.com/AWS_Amazon_…找到)中,亚马逊提供了一个启发式方法来估计哪种集群类型更适合使用,如下所示:
如果每天的作业数(设置集群的时间包括 Amazon S3 数据加载时间,如果使用 Amazon S3+数据处理时间)<24 小时,请考虑临时 Amazon EMR 集群或物理实例。 通过将-live 参数传递给 ElasticMapduce 命令来实例化长时间运行的实例,该命令启用了 Keep Alive 选项并禁用了自动终止。*
请注意,临时集群和长期运行的集群共享相同的属性和限制;尤其是,一旦集群关闭,HDFS 上的数据就不会持久化。
关于文件系统的注意事项
到目前为止,在我们的示例中,我们假设数据在 S3 中可用。 在本例中,存储桶作为s3n文件系统挂载在 EMR 中,并用作输入源和临时文件系统来存储计算中的中间数据。 在 S3 中,我们引入了潜在的 I/O 开销,读写等操作会触发GET和PUT HTTP请求。
备注
请注意,EMR 不支持 S3 数据块存储。 S3 URI 映射到 S3n。
另一种选择是将数据加载到集群 HDFS 中,并从那里运行处理。 在这种情况下,我们确实有更快的 I/O 和数据局部性,但我们会失去持久性。 当集群关闭时,我们的数据就会消失。 根据经验,如果您正在运行临时集群,那么使用 S3 作为后端是有意义的。 在实践中,人们应该根据工作流特性进行监控和决策。 迭代的多遍 MapReduce 作业将极大地受益于 HDFS;有人可能会争辩说,对于这些类型的工作流,像 TEZ 或 Spark 这样的执行引擎会更合适。
将数据导入电子病历
将数据从 HDFS 复制到 S3 时,建议使用 s3Distcp(docs.aws.amazon.com/ElasticMapR…),而不是 Apache Distcp 或 Hadoop Distcp。 此方法也适用于在 EMR 内以及从 S3 到 HDFS 传输数据。 要将大量数据从本地磁盘移动到 S3,Amazon 建议使用 Jets3t 或 GNU 并行来并行化工作负载。 通常,重要的是要知道,对 S3 的 PUT 请求的上限是每个文件 5 GB。 要上传较大的文件,需要依赖分块上传(API),这是一种允许将大文件拆分成较小部分并在上传时重新组装的 aws.amazon.com/about-aws/w… 也可以使用 AWS CLI 或流行的 S3CMD 实用程序等工具复制文件,但这些工具没有 AS s3Distcp 的并行优势。
EC2 实例和调整
EMR 集群的大小取决于数据集大小、文件和块的数量(确定拆分数量)和工作负载类型(尽量避免在任务内存耗尽时溢出到磁盘)。 根据经验,好的大小应该最大限度地提高并行度。 每个实例的映射器和减少器的数量以及每个 JVM 守护进程的堆大小通常由 EMR 在可用资源发生变化的情况下提供和调优集群时配置。
←T0 抯集群调整
除了前面针对在 EMR 上运行的集群的注释之外,在任何类型的集群上运行工作负载时,还需要记住一些一般想法。 当然,当在 EMR 之外运行时,这将更加明确,因为它通常抽象出一些细节。
JVM 注意事项
您应该运行 64 位版本的 JVM 并使用服务器模式。 这个可能需要更长的时间来生成优化的代码,但它也使用了更积极的策略,并将随着时间的推移重新优化代码。 这使得它更适合长期运行的服务,比如 Hadoop 进程。
确保为 JVM 分配足够的内存,以防止过度频繁的垃圾收集(GC)暂停。 并发标记和清除收集器是目前针对 Hadoop 测试和推荐最多的收集器。 自从 JDK7 引入以来,垃圾优先(G1)收集器已经成为许多其他工作负载的 GC 选项,因此值得关注推荐的最佳实践的发展。 这些选项可以在 Cloudera Manager 的每个服务的配置部分中配置为自定义 Java 参数。
小文件问题
在考虑服务协同定位时,您将考虑将堆分配给工作节点上的个 Java 进程。 但是关于 NameNode 有一个特殊的情况,您应该知道:小文件问题。
Hadoop 针对具有大块大小的超大型文件进行了优化。 但有时特定的工作负载或数据源会将许多小文件推送到 HDFS 上。 这很可能是次优的,因为它表明每次处理一个块的每个任务在完成之前只会读取少量数据,从而导致效率低下。
拥有许多小文件也会消耗更多的 NameNode 内存;它在内存中保存从文件到块的映射,因此保存每个文件和块的元数据。 如果文件数量和数据块数量快速增加,那么 NameNode 内存使用量也会增加。 这可能只影响系统的一个子集,因为在撰写本文时,1 GB 内存可以支持 200 万个文件或块,但是使用 2 或 4 GB 的默认堆大小,很容易达到这个限制。 如果 NameNode 需要开始非常积极地运行垃圾收集,或者最终耗尽内存,那么您的集群将非常不健康。 缓解方法是将更多堆分配给 JVM;较长期的方法是将许多小文件合并为数量较少的较大文件。 理想情况下,使用可拆分的压缩编解码器进行压缩。
映射和减少优化
映射器和减法器都提供了优化性能的区域;这里有几点需要考虑:
- 映射器的数量取决于分割的数量。 当文件小于默认块大小或使用不可拆分格式压缩时,映射器的数量将等于文件的数量。 否则,映射器的数量由每个文件的总大小除以块大小得出。
- 压缩映射器输出以减少对磁盘的写入并增加 I/O。LZO 是执行此任务的好格式。
- 避免溢出到磁盘:映射器应该有足够的内存来保留尽可能多的数据。
- 减速器数量:建议您使用的减速器数量少于减速器总容量(这样可以避免执行等待)。
安全性
一旦你构建了一个集群,你首先想到的就是如何保护它,对吗? 别担心,大多数人都不担心。但是,随着 Hadoop 从研究部门的内部分析转变为直接驱动关键系统,它不能被忽视太久。
保护 Hadoop 不是心血来潮或没有经过重大测试就能完成的事情。 我们不能就这一问题给出详细的建议,也不能强烈强调认真对待和妥善处理这一问题的必要性。 这可能会耗费时间,可能会花费金钱,但要权衡一下集群受损的成本。
安全性也是一个比 Hadoop 集群大得多的话题。 我们将探索 Hadoop 中提供的一些安全特性,但是您确实需要一个连贯的安全策略,这些离散的组件都适合这些安全策略。
Hadoop 安全模型的演变
在 Hadoop1 中,实际上没有安全保护,因为提供的安全模型有明显的攻击向量。 您用来连接到集群的 Unix 用户 ID 被认为是有效的,并且您拥有该用户的所有权限。 显然,这意味着在可以访问集群的主机上拥有管理访问权限的任何人都可以有效地模拟任何其他用户。
这导致了所谓的“头节点”访问模型的发展,根据该模型,Hadoop 集群与除头节点之外的所有主机隔离,所有对集群的访问都通过这个集中控制的节点进行中介。 对于缺乏真正的安全模型来说,这是一种有效的缓解措施,即使在使用更丰富的安全方案的情况下,这仍然是有用的。
超越基本授权
核心 Hadoop 增加了额外的安全功能,解决了之前的问题。 具体而言,它们涉及以下内容:
- 集群可能要求用户通过 Kerberos 进行身份验证,并证明他们是他们所说的那个人。
- 在安全模式下,集群还可以使用 Kerberos 进行所有节点到节点的通信,从而确保所有通信节点都经过身份验证,并防止恶意节点尝试加入集群。
- 为了简化管理,可以将用户收集到组中,可以针对这些组定义数据访问权限。 这称为基于角色的访问控制(RBAC),它是拥有多个用户的安全集群的先决条件。 可以从公司系统(如 LDAP 或 Active Directory)检索用户-组映射。
- HDFS 可以应用 ACL 来取代当前受 Unix 启发的所有者/组/世界模型。
这些功能为 Hadoop 提供了比过去更强大的安全态势,但是社区正在快速发展,并且出现了更多专门的 Apache 项目来解决特定的安全领域。
ApacheSentryHadoop是一个为 sentry.incubator.apache.org 数据和服务提供更细粒度授权的系统。 其他服务构建哨兵映射,例如,这不仅允许对特定的 HDFS 目录施加特定限制,而且还允许对实体(如配置单元表)施加特定限制。
Sentry 专注于为 Hadoop 安全性的内部细粒度方面提供更丰富的工具,而 Apache Knox(knox.apache.org)提供了到 Hadoop 的安全网关,该网关与外部身份管理系统集成,并提供访问控制机制来允许或禁止访问特定的 Hadoop 服务和操作。 它通过向 Hadoop 提供一个仅支持 REST 的接口并保护对此 API 的所有调用来实现这一点。
Hadoop 安全的未来
在 Hadoop 世界中还有许多其他的发展。 核心 Hadoop2.5 向 HDFS 添加了扩展的文件属性,可用作附加访问控制机制的基础。 未来的版本将包含更好地支持传输中和静态数据加密的功能,由英特尔(github.com/intel-hadoo…)领导的犀牛计划(Project Rhino)正在构建对文件系统加密模块、安全文件系统以及在某种程度上更全面的密钥管理基础设施的更丰富支持。
Hadoop 发行版供应商正在迅速采取行动,将这些功能添加到他们的发行版中,因此,如果您关心安全性(您关心的,不是吗!),那么请参考文档以了解您的发行版的最新版本。 新的安全功能正在添加,甚至是即时更新,而且在重大升级之前不会推迟。
使用安全集群的后果
在用现在可用的和即将到来的所有安全好处来取笑你之后,给你一些警告才是公平的。 安全性通常很难正确实现,错误地使用缺陷部署带来的安全感通常比知道自己没有安全性更糟糕。
然而,即使您操作正确,运行安全集群也会产生后果。 这无疑增加了管理员(通常也是用户)的工作难度,因此肯定会有开销。 特定的 Hadoop 工具和服务的工作方式也会有所不同,具体取决于集群上采用的安全性。
我们在章,数据生命周期管理中讨论了 Oozie,它在幕后使用自己的委派令牌。 这允许 Oozie 用户提交作业,然后代表最初提交的用户执行这些作业。 在只使用基本授权机制的集群中,这很容易配置,但在安全集群中使用 Oozie 需要向工作流定义和常规 Oozie 配置添加额外的逻辑。 对于 Hadoop 或 Oozie 来说,这不是问题;然而,与 Hadoop2 中 HDFS 更好的 HA 特性带来的额外复杂性类似,更好的安全机制只会带来您需要考虑的成本和后果。
监控
在本章的早些时候,我们讨论了 Cloudera Manager 作为可视化的监控工具,并暗示它也可以通过编程方式与其他监控系统集成。 但是,在将 Hadoop 插入任何监控框架之前,有必要考虑一下对 Hadoop 集群进行操作监控意味着什么。
Hadoop-故障无关紧要
传统的系统监控往往是一个相当二进制的工具;一般来说,要么某些东西在工作,要么不工作。主机是活的还是死的,Web 服务器是否响应。但在 Hadoop 世界里,事情有点不同;重要的是服务的可用性,即使特定的硬件或软件发生故障,这仍然可以被视为实时的。 如果单个工作节点出现故障,任何 Hadoop 集群都不会出现问题。 从 Hadoop2 开始,如果配置了 HA,甚至服务器进程(如 NameNode)的故障也不应该成为问题。 因此,对 Hadoop 的任何监视都需要考虑服务运行状况,而不是特定主机的运行状况,这一点应该不重要。 全天候寻呼机的操作人员不会高兴在凌晨 3 点被寻呼时发现 10,000 个集群中的一个工作节点出现故障。 的确,一旦集群的规模超过了某一点,单个硬件出现故障几乎是家常便饭。
监控集成
您将不会构建自己的监控工具;相反,您可能希望与现有工具和框架集成。 对于流行的开源监控工具,如 Nagios 和 Zabbix,有多个示例模板可以集成 Hadoop 的服务范围和特定于节点的指标。
这可以实现前面所暗示的那种分离;Yarn 资源管理器的故障将是一个高危急事件,很可能会导致向操作人员发送警报,但应该只捕获特定主机上的高负载,而不会导致警报被触发。? 这就提供了在发生不好的事情时触发警报的双重功能,此外,它还可以捕获和提供随时间推移深入研究系统数据以进行趋势分析所需的信息。
Cloudera Manager 提供了 REST 接口,这是另一个集成点,Nagios 等工具可以根据该接口集成和提取 Cloudera Manager 定义的服务级别指标,而不必定义自己的指标。
对于构建在 IBM Tivoli 或 HP OpenView 等框架之上的重量级企业监控基础设施,Cloudera Manager 还可以通过这些系统收集的 SNMP 陷阱传递事件。
应用级指标
有时,您可能还希望您的应用收集可以在系统内集中捕获的指标。 不同的计算模型实现这一点的机制会有所不同,但最著名的是 MapReduce 中可用的应用计数器。
当 MapReduce 作业完成时,它会输出许多计数器,这些计数器由系统在整个作业执行过程中收集,这些计数器处理映射任务的数量、写入的字节数、失败的任务等指标。 您还可以编写特定于应用的指标,这些指标将与系统计数器一起使用,并在整个 map/Reduce 执行过程中自动聚合。 首先定义一个 Java 枚举,并在其中命名您需要的指标,如下所示:
public enum AppMetrics{
MAX_SEEN,
MIN_SEEN,
BAD_RECORDS
};
然后,在 Map 或 Reduce 实现的 Map、Reduce、Setup 和 Cleanup 方法中,您可以执行类似以下操作来将计数器递增 1:
Context.getCounter(AppMetrics.BAD_RECORDS).increment(1);
有关该机制的更多详细信息,请参考org.apache.hadoop.mapreduce.Counter接口的 JavaDoc。
故障排除
监视和记录计数器或附加信息固然不错,但知道如何在排除应用故障时真正找到所需的信息可能会让人望而生畏。 在本节中,我们将了解 Hadoop 如何存储日志和系统信息。 我们可以区分三种类型的原木,如下所示:
- Yarn 应用,包括 MapReduce 作业
- 守护程序日志(NameNode 和 ResourceManager)
- 记录非分布式工作负载的服务,例如,HiveServer2 记录到
/var/log
除了这些日志类型之外,Hadoop 还在文件系统(存储可用性、复制系数和块数量)和系统级别公开了许多指标。 如前所述,Apache Ambari 和 Cloudera Manager 作为前端都做得很好,它们集中了对调试信息的访问。 但是,在幕后,每个服务要么记录到 HDFS,要么记录到单节点文件系统。 此外,YAR、MapReduce 和 HDFS 通过 Web 接口和编程 API 公开它们的日志文件和指标。
日志记录级别
默认情况下,Hadoop 将消息记录到 Log4j。 Log4j 是通过类路径中的log4j.properties配置的。 此文件定义记录的内容和使用的布局:
log4j.rootLogger=${root.logger}
root.logger=INFO,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n
缺省的根记录器是INFO,console,它将级别为INFO及以上的所有消息记录到控制台的stderr。 部署在 Hadoop 上的单个应用可以发布它们自己的log4j.properties,并根据需要设置它们发出的日志的级别和其他属性。
Hadoop 守护进程有一个网页可以获取和设置任何 Log4j 属性的日志级别。 此接口由每个服务 Web UI 中的/LogLevel端点公开。 要启用ResourceManager类的调试日志记录,我们将访问http://resourcemanagerhost:8088/LogLevel,屏幕截图如下所示:
获取并设置 ResourceManager 上的日志级别
或者,Yarndaemonlog <host:port>命令与service /LogLevel端点连接。 我们可以使用–getlevel <property>参数检查ResourceManager类的与mapreduce.map.log.level相关联的级别,如下所示:
$ hadoop daemonlog -getlevel localhost.localdomain:8088 mapreduce.map.log.level
Connecting to http://localhost.localdomain:8088/logLevel?log=mapreduce.map.log.level Submitted Log Name: mapreduce.map.log.level Log Class: org.apache.commons.logging.impl.Log4JLogger Effective level: INFO
可以使用-setlevel <property> <level>选项修改有效级别:
$ hadoop daemonlog -setlevel localhost.localdomain:8088 mapreduce.map.log.level DEBUG
Connecting to http://localhost.localdomain:8088/logLevel?log=mapreduce.map.log.level&level=DEBUG
Submitted Log Name: mapreduce.map.log.level
Log Class: org.apache.commons.logging.impl.Log4JLogger
Submitted Level: DEBUG
Setting Level to DEBUG ...
Effective level: DEBUG
请注意,此设置将影响ResourceManager类生成的所有日志。 这包括系统生成的条目以及在 Yarn 上运行的应用生成的条目。
访问日志文件
根据分布情况,日志文件位置和命名约定可能会有所不同。 Apache Ambari 和 Cloudera Manager 集中访问服务和单个应用的日志文件。 在 Cloudera 的 QuickStart VM 上,可以在http://localhost.localdomain:7180/cmf/hardware/hosts/1/processes处找到当前运行的进程及其日志文件的链接、stderr和stdout通道的概览,屏幕截图如下所示:
访问 Cloudera Manager 中的日志资源
Ambari 通过 HDP 沙盒上http://127.0.0.1:8080/#/main/services处的Services仪表板提供了类似的概览,屏幕截图如下所示:
访问 Apache Ambari 上的日志资源
非分布式日志通常位于每个集群节点的/var/log/<service>下。 Yarn 容器和 MRv2 原木的位置也取决于分布。 在 CDH5 上,这些资源在 HDFS 中的/tmp/logs/<user>下可用。
访问分布式日志的标准方式是通过命令行工具或使用服务 Web UI。
例如,该命令如下所示:
$ yarn application -list -appStates ALL
前面的命令将列出所有正在运行和重试的 Yarn 应用。 任务列中的 URL 指向显示任务日志的 Web 界面,如下所示:
14/08/03 14:44:38 INFO client.RMProxy: Connecting to ResourceManager at localhost.localdomain/127.0.0.1:8032 Total number of applications (application-types: [] and states: [NEW, NEW_SAVING, SUBMITTED, ACCEPTED, RUNNING, FINISHED, FAILED, KILLED]):4 Application-Id Application-Name Application-Type User Queue State Final-State Progress Tracking-URL application_1405630696162_0002 PigLatin:DefaultJobName MAPREDUCE cloudera root.cloudera FINISHED SUCCEEDED 100% http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0002 application_1405630696162_0004 PigLatin:DefaultJobName MAPREDUCE cloudera root.cloudera FINISHED SUCCEEDED 100% http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0004 application_1405630696162_0003 PigLatin:DefaultJobName MAPREDUCE cloudera root.cloudera FINISHED SUCCEEDED 100% http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0003 application_1405630696162_0005 PigLatin:DefaultJobName MAPREDUCE cloudera root.cloudera FINISHED SUCCEEDED 100% http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0005
例如,指向属于用户 Cloudera 的任务的链接http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0002是存储在hdfs:///tmp/logs/cloudera/logs/application_1405630696162_0002/下的内容的前端。
在以下部分中,我们将概述可用于不同服务的 UI。
备注
使用–log-uri s3://<bucket>选项配置 EMR 集群将确保 Hadoop 日志复制到s3://<bucket>位置。
ResourceManager、NodeManager 和 Application Manager
在 YAINE 上,ResourceManager web UI 提供 Hadoop 集群的信息和常规作业统计数据、正在运行/完成/失败的作业,以及作业历史日志文件。 默认情况下,UI 显示在http://<resourcemanagerhost>:8088/,可以在下面的屏幕截图中看到:
资源管理器
应用
在左侧栏上,可以查看感兴趣的应用状态:NEW、SUBMITTED、ACCEPTED、RUNNING、FINISHING、FINISHED、FAILED或KILLED。 根据应用状态,以下信息可用:
- 应用 ID
- 提交用户
- 应用名称
- 应用所在的调度程序队列
- 开始/结束时间和状态
- 链接到应用历史记录的跟踪 UI
此外,Cluster Metrics视图还提供以下信息:
- 整体应用状态
- 运行中的集装箱数量
- 内存使用情况
- 节点状态
个节点
Nodes视图是 NodeManager 服务菜单的前端,它显示有关节点正在运行的应用的运行状况和位置信息,如下所示:
节点状态
集群的每个单独节点通过其自己的 UI 在主机级别公开更多信息和统计信息。 这些信息包括节点上运行的 Hadoop 版本、节点上有多少可用内存、节点状态以及正在运行的应用和容器列表,如以下屏幕截图所示:
单节点信息
调度器
以下屏幕截图显示了 Scheduler 窗口:
计划安排者 / 调度机 / 调度程序 / 制表人
MapReduce
虽然 MapReducev1 和 MapReducev2 中提供了相同的信息和日志记录详细信息,但访问方式略有不同。
MapReduce v1
以下屏幕截图显示了 MapReduce JobTracker UI:
作业跟踪器 UI
作业跟踪器 UI 默认在http://<jobtracker>:50070中提供,它显示有关当前正在运行的所有作业以及停用的 MapReduce 作业的信息、集群资源和运行状况的摘要以及调度信息和完成百分比,如以下屏幕截图所示:
作业详细信息
对于每个正在运行和停用的作业,都有详细信息可用,包括其 ID、所有者、优先级、任务分配和映射器的任务启动。 单击jobid链接将进入作业详细信息页面-与mapred job –list命令显示的 URL 相同。 此资源提供有关 map 和 Reduce 任务的详细信息,以及作业、文件系统和 MapReduce 级别的常规计数器统计信息;其中包括使用的内存、读/写操作数以及读写字节数。
对于每个映射和减少操作,JobTracker 会显示总任务、挂起任务、正在运行任务、已完成任务和失败任务,如以下屏幕截图所示:
作业任务概述
单击工单表格中的链接将进入任务和任务尝试级别的进一步概述,如以下屏幕截图所示:
任务尝试次数
从最后一页开始,我们可以访问每个任务尝试的日志,包括每个单独 TaskTracker 主机上成功任务和失败/终止任务的日志。 此日志包含有关 MapReduce 作业状态的最精细信息,包括 Log4j 附加器的输出以及通过管道传输到stdout和stderr通道以及syslog的输出,如以下屏幕截图所示:
TaskTracker 日志
MapReduce v2(Yarn)
正如我们在第 3 章,Processing-MapReduce 以及之后的中看到的,对于 YAIN,MapReduce 只是众多可以部署的处理框架之一。 回想一下前面的章节,JobTracker 和 TaskTracker 服务分别被 ResourceManager 和 NodeManager 取代。 因此,来自 YAR 的服务 UI 和日志文件都比 MapReducev1 更通用。
资源管理器中显示的application_1405630696162_0002名称对应于具有job_1405630696162_0002ID 的 MapReduce 作业。该应用 ID 属于在容器内运行的任务,单击它将显示 MapReduce 作业的概览,并允许从任一阶段向下钻取各个任务,直至到达单任务日志,如以下屏幕截图所示:
包含 MapReduce 作业的 Yarn 应用
作业历史服务器
Year 附带了一个 JobHistoryREST 服务,该服务公开有关已完成应用的详细信息。 目前,它只支持 MapReduce,并提供有关已完成作业的信息。 这包括提交作业的作业最终状态SUCCESSFUL或FAILED、MAP 和 Reduce 任务总数以及时间信息。
在http://<jobhistoryhost>:19888/jobhistory提供了一个 UI,如以下截图所示:
作业历史记录界面
单击每个作业 ID 将转到 Yarn 应用屏幕截图中显示的 MapReduce 作业 UI。
NameNode 和 DataNode
Hadoop 分布式文件系统(HDFS)的 Web 界面通常显示有关 NameNode 本身以及文件系统的信息。
默认情况下位于http://<namenodehost>:50070/,如下图所示:
NameNode UI
概述菜单显示有关 DFS 容量和使用情况以及数据块池状态的 NameNode 信息,并提供 DataNode 运行状况和可用性状态的摘要。 此页中包含的信息在很大程度上等同于命令行提示符中显示的信息:
$ hdfs dfsadmin –report
DataNodes 菜单提供有关每个节点状态的更详细信息,并提供单个主机级别的深入查看,包括可用节点和已停用的节点,如以下屏幕截图所示:
数据节点 UI
摘要
这是围绕运行可操作 Hadoop 集群的考虑因素进行的短暂停留。 我们并没有试图将开发人员转变为管理员,但希望更广阔的视角能帮助您帮助您的运营人员。 我们特别讨论了以下主题:
- Hadoop 如何天然地适合 DevOps 方法,因为它的多层复杂性意味着开发人员和运营人员之间不可能也不希望有实质性的知识差距
- Cloudera Manager,以及它如何成为一款出色的管理和监控工具;不过,如果您有其他企业工具,并且存在供应商锁定风险,那么它可能会导致集成问题
- Ambari,Cloudera Manager 的 Apache 开源替代品,以及如何在 Hortonworks 发行版中使用它
- 如何考虑为物理 Hadoop 集群选择硬件,以及这如何自然地符合 Hadoop 2 世界中可能的多个工作负载如何在共享资源上和平共处的考虑因素
- 启动和使用 EMR 集群的不同注意事项,以及这如何既是物理集群的附件,又是物理集群的替代方案
- Hadoop 安全生态系统,它是一个发展非常迅速的领域,今天可用的功能比几年前要好得多,而且仍然有很多东西即将出现
- 监控 Hadoop 集群,考虑在拥抱故障的 Hadoop 模型中哪些事件很重要,以及如何将这些警报和指标集成到其他企业监控框架中
- 如何对 Hadoop 集群的问题进行故障排除,包括可能发生的情况以及如何找到信息为您的分析提供信息
- 快速浏览 Hadoop 提供的各种 Web 用户界面,这些界面可以很好地概述系统中各个组件内发生的情况
这就是我们对 Hadoop 的深入讨论。 在最后一章中,我们将对更广泛的 Hadoop 生态系统表达一些想法,为书中没有机会介绍的有用和有趣的工具和产品提供一些指导,并建议如何参与社区。