使用 AWS CLI 管理 EMR

Amazon EMR (Elastic MapReduce) 是一种托管的大数据处理服务,使用户能够在云上便捷、快速地运行和管理大规模数据分析和处理任务。

我们可以在 EMR 集群上安装各种分布式程序。这里我们将介绍如何安装最基础的 Hadoop 分布式计算框架并在其上运行一个 MapReduce 程序。

创建 MapReduce 项目

MapReduce 是一种编程模型和处理大规模数据集的框架。MapReduce 的核心思想是将数据处理分为两个阶段:Map 阶段和 Reduce 阶段,通过分布式计算集群并行处理数据。

  1. Map 阶段

    • 输入:Map 阶段接收输入数据集,通常以键值对(key-value pairs)的形式存在。
    • 处理:每个输入键值对由 Map 函数处理,产生一组中间键值对。
    • 输出:输出是中间键值对,这些对通常保存在本地磁盘上并准备发送给 Reduce 阶段。
  2. Shuffle 和 Sort

    • 在 Map 阶段和 Reduce 阶段之间有一个重要的过程,称为 Shuffle 和 Sort。
    • Shuffle:将 Map 阶段输出的中间键值对按键的哈希值分配到不同的 Reduce 任务中。
    • Sort:在每个 Reduce 任务中,对接收到的中间键值对按键进行排序。
  3. Reduce 阶段

    • 输入:Reduce 阶段接收来自 Shuffle 过程的中间键值对,这些对已经按键排序。
    • 处理:每个 Reduce 函数处理一个唯一键的所有相关值,并生成最终的输出键值对。
    • 输出:Reduce 阶段输出的键值对被写入到持久化存储中,例如 HDFS(Hadoop Distributed File System)。

编辑 MapReduce 程序

我们使用 Java 作为 MapReduce 的实现语言。这里使用 Maven 来管理项目依赖。

  1. 使用 Maven 创建一个新项目:

    mvn archetype:generate \
        -DgroupId='com.example' \
        -DartifactId='my-hadoop-project' \
        -DarchetypeArtifactId=maven-archetype-quickstart \
        -DinteractiveMode=false
    
    • -DgroupId 是你的项目组名,通常是你的组织名或域名反转。
    • -DartifactId 是你的项目名。

    删去项目示例文件:

    cd my-hadoop-project
    rm -rf src/test src/main/java/com/example/App.java
    
  2. 配置 pom.xml

    进入项目目录,编辑 pom.xml 文件,添加 Hadoop 的依赖项:

    vim pom.xml
    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.example</groupId>
        <artifactId>my-hadoop-project</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
        </properties>
    
        <dependencies>
            <!-- hadoop-common -->
            <dependency>
                <groupId>org.apache.hadoop</groupId>
                <artifactId>hadoop-common</artifactId>
                <version>3.2.0</version>
            </dependency>
            <!-- hadoop-client -->
            <dependency>
                <groupId>org.apache.hadoop</groupId>
                <artifactId>hadoop-client</artifactId>
                <version>3.2.0</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <!-- 编译插件 -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                </plugin>
                <!-- JAR 打包插件 -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.1.2</version>
                    <configuration>
                        <archive>
                            <manifest>
                                <mainClass>com.example.WordCount</mainClass>
                            </manifest>
                        </archive>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    使用到的依赖:

  3. 编写 Hadoop 应用程序。

    这里我们创建一个分布式项目中经典的 Word Count 程序。它统计输入文件中各个单词的出现次数。

    vim src/main/java/com/example/WordCount.java
    
    package com.example;
    
    import java.io.IOException;
    import java.util.StringTokenizer;
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.IntWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.Mapper;
    import org.apache.hadoop.mapreduce.Reducer;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    
    public class WordCount {
        // Mapper 类
        public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
            // 定义一个常量 IntWritable 对象,值为 1,用于计数
            private final static IntWritable one = new IntWritable(1);
            // 用于存储单词的 Text 对象
            private Text word = new Text();
    
            // map 方法处理输入的每一行文本
            public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
                // 使用 StringTokenizer 将输入文本分割成单词
                StringTokenizer itr = new StringTokenizer(value.toString());
                // 遍历所有单词
                while (itr.hasMoreTokens()) {
                    // 将当前单词设置到 word 对象中
                    word.set(itr.nextToken());
                    // 输出 <单词, 1> 键值对
                    context.write(word, one);
                }
            }
        }
    
        // Reducer 类
        public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
            // 用于存储单词计数结果的 IntWritable 对象
            private IntWritable result = new IntWritable();
    
            // reduce 方法处理每个 key(单词) 对应的所有 value(出现次数)
            public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
                // 初始化单词计数总和
                int sum = 0;
                // 遍历该单词的所有出现次数并累加
                for (IntWritable val : values) {
                    sum += val.get();
                }
                // 将累加结果设置到 result 对象中
                result.set(sum);
                // 输出最终的单词和对应的计数结果
                context.write(key, result);
            }
        }
    
        // 主方法
        public static void main(String[] args) throws Exception {
            Configuration conf = new Configuration();                // 创建 Hadoop 配置对象
            Job job = Job.getInstance(conf, "word count");           // 创建并配置 Job 实例
            job.setJarByClass(WordCount.class);                      // 设置 Job 运行的类
            job.setMapperClass(TokenizerMapper.class);               // 设置 Map 任务的类
            job.setCombinerClass(IntSumReducer.class);               // 可选:在 Map 阶段后进行部分汇总,用于减少网络传输
            job.setReducerClass(IntSumReducer.class);                // 设置 Reduce 任务的类
            job.setOutputKeyClass(Text.class);                       // 设置输出 key 的类型
            job.setOutputValueClass(IntWritable.class);              // 设置输出 value 的类型
            FileInputFormat.addInputPath(job, new Path(args[1]));    // 设置输入路径,从命令行参数获取
            FileOutputFormat.setOutputPath(job, new Path(args[2]));  // 设置输出路径,从命令行参数获取
            System.exit(job.waitForCompletion(true) ? 0 : 1);        // 提交作业并等待完成,根据作业结果返回退出码
        }
    }
    
  4. 构建程序:

    mvn clean package
    

    构建产物在 target/my-hadoop-project-1.0-SNAPSHOT.jar。这就是我们要运行的 MapReduce 作业了。

  5. 准备输入数据。

    最后,我们需要为 MapReduce 程序获取输入。这里我们自己创建一个输入文件:

    vim input.txt
    
    hello world
    hello hadoop
    welcome to the world of big data
    hadoop and big data
    

配置 MapReduce 作业

  1. 上传 MapReduce 作业到 Amazon S3。

    我们一般将 MapReduce 作业保存到 S3 方便 EMR 集群直接访问。

    mv target/my-hadoop-project-1.0-SNAPSHOT.jar job.jar
    aws s3 cp job.jar s3://my-bucket/job.jar
    
  2. 上传输入数据到 Amazon S3:

    aws s3 cp input.txt s3://my-bucket/input/
    
  3. 创建作业步骤配置。

    我们要创建一个作业,让 EMR 集群运行 WordCount 类,并以 input 目录下的文件为输入,并将结果输出到 output 目录。

    vim steps.json
    
    [
      {
        "Type": "CUSTOM_JAR",
        "Name": "WordCount",
        "ActionOnFailure": "CONTINUE",
        "Jar": "s3://my-bucket/job.jar",
        "Args": ["s3://my-bucket/input/", "s3://my-bucket/output/"],
        "MainClass": "com.example.WordCount"
      }
    ]
    

    这个作业等同于在 NameNode 上运行如下命令:

    hadoop jar \
        s3://my-bucket/job.jar \
        com.example.WordCount \
        s3://my-bucket/input/ \
        s3://my-bucket/output/
    

创建 EMR 集群

创建 EMR 集群

  1. 创建默认 IAM 角色:

    aws emr create-default-roles
    
  2. 查询 EMR 版本:

    aws emr list-release-labels
    
  3. 创建并启动 EMR 集群:

    aws emr create-cluster \
        --name 'MyEMRCluster' \
        --release-label 'emr-7.3.0' \
        --applications 'Name=Hadoop' \
        --ec2-attributes KeyName='my-ssh-key' \
        --instance-type 'm5.xlarge' \
        --log-uri 's3://my-bucket/logs/' \
        --instance-count 3 \
        --use-default-roles \
        --steps 'file://steps.json' \
        --auto-terminate
    

    这里我们通过 --steps 指定了要执行的作业,并且通过 --auto-terminate 指定了当作业执行完成时就终止 EMR 集群,以节省成本。

    启动集群需要一定时间,一般需要 5 分钟左右。

  4. 查看集群状态:

    aws emr list-clusters --query 'Clusters[0].{Name:Name,Status:Status}'
    
  5. 查看作业输出:

    aws s3 sync s3://my-bucket/output output
    

    可以看到 MapReduce 程序生成了 4 个文件:

    output
    ├── _SUCCESS
    ├── part-r-00000
    ├── part-r-00001
    └── part-r-00002
    

    其中,_SUCCESS 是空文件,标志着作业的成功完成。part-r-xxxxx 是 Reducer 的输出。每个文件对应于一个 Reducer 任务的输出结果。文件内容如下:

    # part-r-00000
    data	2
    to	1
    # part-r-00001
    big	2
    hadoop	2
    of	1
    the	1
    world	2
    # part-r-00002
    and	1
    hello	2
    welcome	1
    

    可以看到,我们的 MapReduce 程序正确完成了单词计数任务。

添加任务

如果没有配置自动终止,则可以随时为集群添加新的任务:

CLUSTER_ID=$(aws emr list-clusters --query 'Clusters[0].Id' --output text)
aws emr add-steps \
    --cluster-id $CLUSTER_ID \
    --steps 'file://steps.json'

参见:add-steps | AWS CLI Command Reference

查看作业日志

  1. 查询作业执行状态:

    CLUSTER_ID=$(aws emr list-clusters --query 'Clusters[0].Id' --output text)
    aws emr list-steps --cluster-id $CLUSTER_ID --query 'Steps[0]' --output json
    
  2. 下载作业日志:

    STEP_ID=$(aws emr list-steps --cluster-id $CLUSTER_ID --query 'Steps[0].Id' --output text)
    aws s3 sync s3://my-bucket/logs/$CLUSTER_ID/steps/$STEP_ID logs
    gunzip logs/*.gz
    

终止 EMR 集群

  1. 终止集群:
CLUSTER_ID=$(aws emr list-clusters --query 'Clusters[0].Id' --output text)
aws emr terminate-clusters --cluster-ids $CLUSTER_ID
  1. 检查集群:
aws emr list-clusters --query 'Clusters[*].{Id:Id,Name:Name,Status:Status}' --output json
posted @ 2025-01-18 19:01  Undefined443  阅读(61)  评论(0)    收藏  举报