剖析Hadoop核心组件,构建分布式文件处理程序

前言

大数据的起源其实可以追溯到Google 2004年前后发表的《The Google File System》《MapReduce: Simplified Data Processing on Large Clusters》《Bigtable: A Distributed Storage System for Structured Data》的三篇论文的发表。受到Goggle论文的启发,程序员Doug Cutting 根据论文原理初步实现了类似 GFSMapReduce 的功能,随后的时间里,Doug Cutting 启动了一个独立的项目专门开发维护大数据技术,这就是后来赫赫有名的 Hadoop

(注:Hadoop 2.x 相比于 Hadoop 1.x Mapreduce中的计算与调度进行了拆分)

通过上图我们可以看到,对于Hadopp而言其中HDFSMapReduceHadoop中两个核心组件。换言之,如果要学习理解Hadoop那么HDFSMapReduce 是难以绕过的难关。接下来,我们便来对Hadoop中的HDFSMapReduce的工作原理进行一次深入剖析和拆解。

HDFS分布式文件存储

如果我们将大数据计算比作烹饪,那么数据就是烹饪的食材,此时选如何选择烹饪这些食材的工具便成为困扰程序员的一个首要难题。不同于传统的数据处理,大数据时代的数据呈现出大量、高速、多样等特性,面对如此庞大的数据,采用原先的单节点手段肯定是无法处理的。而HDFS(Hadoop Distributed File System)的出现恰好可以有效缓解这一问题。

我们完全可以将 HDFS理解为烹饪"大数据"这份食材的那口厨具,食材进进出出然后在厨师的操作下被烹饪为各种菜肴层出不穷。那HDFS究竟使用了哪些黑科技才能负担起如此庞大数据的存储及处理呢?

接下来,我们就从 HDFS 的原理说起,扒一扒HDFS实现大数据高速、可靠的存储背后的秘密。

HDFS架构

在介绍HDFS之前,我们不妨思考这样一个问题,如果是我们来存储如此庞大的数据,我们又改如何处理呢?众所周知,当文件体较小时完全可以通过单机的方式来进行存储。这一点其实很像Java应用中传统的单体应用,即当应用体量较小时完全可以采用单机部署的方式来实现服务的正常访问 。但是随着数据的爆炸性增长,原先的单体方式已然捉襟见肘,此时分布式的理念应运而生。

事实上,在文件存储领域也有类似Java分布式应用的概念,不过其被称为分布式文件系统(DFS)DFS简单来看就是一种将文件存储和管理功能分布到多个物理节点上的文件系统。 其有别于传统的文件系统将所有文件和数据都存储在单一的服务器,在分布式文件系统中数据被切割成多个部分并分布存储在不同的服务器上。进而实现文件系统的高可用、扩展性和高性能的数据存储。

理解了分布式文件的系统对于文件存储的原理后,我们再来看HDFS背后的存储逻辑。如下所示为HDFS的架构图:

对于HDFS而言DataNodeNameNode是其核心组成。其中,DataNode主要负责文件数据的存储和读写操作 。当文件上传至客户端时,其会对上传的文件进行切分,进而将文件数据分割成若干数据块(Block),而这些被拆分后的(Block)则会保存于DataNode。这种分块机制使得文件能够分布在多个 DataNode 上,从而使得文件被分布性的存储在整个 HDFS服务器集群中。

NameNode 则负责整个分布式文件系统的元数据(MetaData)管理,所谓的元数据也就是文件路径名、数据块的ID以及存储位置等信息,这有点类似于操作系统中的文件分配表。

HDFS的读写过程

知晓了HDFS的机构设计后,我们接下来看其是如何来完成读写过程的。如下这张图完整的揭示了HDFS中文件写入的整体逻辑。其中文件写入逻辑大致如下:

1. 客户端请求与 NameNode 通信

当希望在 HDFS 中写入一个新文件时,客户端首先与 NameNode 进行交互,查询文件系统中该文件是否已经存在。如果文件不存在,NameNode 会允许文件创建操作,并为这个文件分配一组数据块的位置。并将分配的空间信息返回至客户端,告知其将数据写入DataNode的位置。

2. 数据分块与分配

正如我们之前所述,文件在客户端会被分割成多个Block块。每个数据块都是 HDFS 存储的最小单元。这些数据块将被写入到不同的 DataNode 上。而HDFS为了确保容错性,其会为每个数据块创建副本,以防止单一机架的故障导致数据丢失

3. 数据块确认

当每个DataNode将客户端传来的这些数据存储到本地磁盘后,DataNode 会向客户端发送写入成功的确认信息。客户端接收到这些确认后,认为数据块写入成功。然后客户端会向 NameNode 发送文件写入完成的通知,告知 NameNode 文件的元数据已经更新,可以正式完成文件的创建。

而文件的读则可以认为是文件写入的逆过程,其逻辑如下图所示:

首先,客户端向NameNode节点发送数据访问的请求。接着,Namenode会根据文件信息收集所有数据块(Block)的位置信息,并根据数据块在文件中的先后顺序,按次序组成数据块定位集合回应给客户端。随后,客户端拿到数据块定位集合后,会根据信息定位第一个数据块所在的位置,并读取Datanode的数据流。之后根据读取偏移量依次定位下一个Datanode数据,以此类推,直至完成对文件信息读取。

总结来看,HDFS在存储数据时,数据被分割成若干数据块,并将这些块分布到集群中的多个节点,即DataNodeHHDFS内部负责实际存储数据块。然后,HDFS内部通过NameNode来负责管理文件系统的元数据(如文件名、文件大小、存储位置等),当客户端需要读取文件时,首先向NameNode请求文件的位置信息,然后直接从相应的DataNode读取数据。如此便是HDFS内部的工作原理!

明白了HDFS的工作原理后,我们接下来看Hadoop内部组件另一个核心组件MapReduce

MapReduce计算框架

MapReduce原理

继续用烹饪为例来类比,如果Hdfs是烹饪过程中的厨具,那MapReduce则更像是烹饪手法的选择。MapReduce主要由Map 阶段和 Reduce 阶段构成。

  • Map 阶段 :输入数据被分割成多个数据块,这些数据块分别由Map任务进行处理。每个Map任务将数据映射成键值对(key-value pairs),这叫做"Map操作"。

  • Reduce 阶段Reduce任务将根据Map阶段输出的键值对进行合并、汇总或聚合。最终输出结果。

其实你完全将MapReduce的思想与算法中分治思想很类似,底层逻辑基本都是拆分合并

MapReduce处理逻辑

为了更好的理解MapReduce的处理逻辑,我们通过一个简单的词频统计需求来对MapReduce的处理逻辑进行深入介绍。

事实上,如果是要统计一篇几百kb文章中对应单词出现的频率的话,其实是非常简单的。只需将文件读入内存,然后建一个 Hash 表记录每个词出现的次数就可以。此时代码大致如下:

java 复制代码
// 假设文档以读入内容,并拆分为String数组形式
pubic void wordCount(String[] content) {
   Map<String,Integer> wordCountMap = new HashMap<>();
   for(int i = 0 ; i < content.length ; i ++ ) {
       if (wordCountMap.contain(content[i])) {
          wordCountMap.put(content[i],
                          wordCountMap.get([content[i])+1);  
     }else {
          wordCountMap.put(content[i],1);
     }

}

上述逻辑在处理小数据量时是完全没问题的,但是如果想统计一个几十GB的文档中词频数时,此时将如此庞大的文件一次性读入内存显然是一件不太现实的事情。

此时,如果内将这庞大的文件拆分成多个细小的文件,然后统计各自小文件中单词出现频率,最后再将结果合并起来问题似乎就完美了。而这恰好就是MapReduce的处理逻辑。当引入MapReduce框架来统计文档单词出现频率时,此时代码如下:

Map阶段

java 复制代码
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();

    public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 将输入的行分割成单词
        String[] words = value.toString().split("\s+");

        // 对每个单词发射键值对
        for (String w : words) {
            word.set(w);
            context.write(word, one);
        }
    }
}

Reduce阶段

java 复制代码
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        // 对每个单词的计数求和
        for (IntWritable val : values) {
            sum += val.get();
        }
        context.write(key, new IntWritable(sum));
    }
}

可以看到,在上述代码中map函数中的value 是要统计的所有文本中的一行数据,其处理过大是map 函数将这行文本中的单词提取出来,针对每个单词输出一个 <word,1> 这样的 <Key, Value> 对。而MapReduce计算框架会将这些 <word , 1> 收集起来,将相同的 word 放在一起,形成类似<word , <1,1,1,1,1,1,1...>> 这样的<Key, Value 集合 >数据,然后将其输入给 reduce进行进一步处理。而我们的reduce的输入参数 values 就是由很多个 1组成的集合,而 Key 就是具体的单词。

具体来看,reduce 在执行时,会将这个集合里的1求和,再将单词的和组成一个 <Key, Value>,也就是<word, sum>输出。此时,每一个输出就是一个单词和它的词频统计总和。

以上就是 MapReduce 编程模型的主要计算过程和原理。总的来看,在Hadoop架构中 HDFS提供了一个分布式文件系统,负责将数据存储在集群中的多个节点上。进而它将大文件分割成多个小块,并在多个DataNode上分布存储。而 MapReduce任务会从HDFS中读取输入数据,并将处理结果写回HDFS

在介绍完Hadoop中核心组件的原理,回到我们的今天的主题,我们将基于Hadoop进行编程,并编写一款简单的分布式文件处理系统,并将处理结果存储回HDFS

代码实战

在开始之前我们先对此次用到的maven依赖进行一个介绍。对于一个hadoop应用受到的hadoop-common依赖当然是不能少的,而为了能编写程序的MapReduce逻辑,还需引入hadoop-mapreduce-client-core;除了上述两个构建hadoop应用的核心依赖外,剩下就是数据处理工具了。由于我们输入文件主要为jsoncsv文件,所以笔者选择使用jacksoncommons-csv来完成对数据的处理。

(注:此处数据处理的依赖其实并无太多限制,完全可以采用自己喜欢和擅长的。)

文件我们完全可以通过手动上传至HDFS,到时只需为程序提供文件地址路径即可,所以我们没必要手写文件读入的代码。我们只需将主要精力集中于MapReduce的任务构建即可。我们首先构建MapReduce中的Map阶段,此时map处理逻辑如下:

构建Map

java 复制代码
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.Iterator;

public class FileProcessingMapper extends Mapper<Object, Text, Text, Text> {
    private static final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
        String line = value.toString().trim();
        // 检查是否为空行
        if (line.isEmpty()) {
            return;
        }
        // 判断CSV还是JSON格式
        if (line.startsWith("{")) {
            // 处理JSON数据
            JsonNode jsonNode = objectMapper.readTree(line);
            String processedData = cleanJsonData(jsonNode);
            context.write(new Text("json"), new Text(processedData));
        } else {
            // 处理CSV数据
            Iterable<CSVRecord> records = CSVFormat.DEFAULT.parse(line);
            for (CSVRecord record : records) {
                String processedData = cleanCsvData(record);
                context.write(new Text("name"), new Text(processedData));
            }
        }
    }

    /***
     * 对json数据格式进行处理
     * @param jsonNode
     * @return
     */
    private String cleanJsonData(JsonNode jsonNode) {
        // 这里可以添加自定义的JSON数据清理逻辑,如果是json数据则仅读取其中的name属性
        return jsonNode.get("name").asText();
    }
    /**
     * 对csv文件数据进行处理
     * @param record
     * @return
     */
    private String cleanCsvData(CSVRecord record) {
        // 这里可以添加自定义的CSV数据清理逻辑,此处默认仅获取CSV的第一列
        return record.get(0); 
    }
}

MapReduce中的Map阶段中,我们主要定义了输入数据的处理逻辑,当前我们的程序主要支持json格式和csv文件的输入。其中所定义的cleanJsonDatacleanCsvData主要用于完成对数据的预处理及操纵。笔者在此处所定义的逻辑较为简单,对于json数据格式则获取其中的name属性的内容,而对于csv格式数据则仅读取其中的首列数据。

(注:此处cleanJsonDatacleanCsvData完全可以进行更深次的扩展,出于简单考虑笔者此处选择最简单的逻辑,感兴趣的读者完全可以对其进行替换~)

构建完Map阶段后,接下来我们来构建其中的Reduce逻辑。

Reduce处理逻辑

java 复制代码
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class FileProcessingReducer extends Reducer<Text, Text, Text, Text> {

    @Override
    public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        for (Text val : values) {
            // 
            context.write(key, val);
        }
    }
}

我们的reduce逻辑其实很简单,就是将map阶段读取到的内容进行记录回写出即可。

(注:此处的逻辑完全可以扩展,笔者为了追求简单,所以才进行简化~)
Driver驱动类

java 复制代码
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

@Sl4j
public class FileProcessingDriver {

    public static void main(String[] args) throws Exception {

        log.info("Driver启动类执行,输入参数格式  <input path> <output path> .........");
        if (args.length != 2) {
            log.error("参数输入不合法,请重试!");
            System.exit(-1);
        }
        // <1> 设定job信息
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf, "File Processing Job");
        // <2> 设定处理任务处理信息
        job.setJarByClass(FileProcessingDriver.class);
        job.setMapperClass(FileProcessingMapper.class);
        job.setReducerClass(FileProcessingReducer.class);

        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(Text.class);
        // <3> 加载文件路径信息
        FileInputFormat.addInputPath(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        // <4> 当任务执行成功,程序成功退出
        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }
}

以上的代码编写完后,通过mvn clean package命令将上述程序打打成jar包,然后执行下面的命令

java 复制代码
hadoop jar FileProcessingJob.jar FileProcessingDriver input/demo.json output/

进而就可以在yarn调度平台看到上述任务执行情况。任务执行完毕后,我们可通过命令进行查看结果。如果我们是CSV文件输入的输入,假设我们输入内容为:

csv 复制代码
John,30,Engineer 
Jane,25,Designer
.........

则我们的Mapper会读取CSV文件内容在进行cleanCsvData方法进行清洗数据时,仅会个字段)。假设方法仅提取第一个字段即(name)属性。此时预期输入结果如下:

csv 复制代码
name John 
name Jane

至此,我们就对hadoop中的核心组件HDFSMapReduce进行了深入的分析和介绍,在此基础上我们利用相关知识构建出一个简易版的分布式文件处理程序,希望上述的讲解对你入门理解大数据有所帮助!

总结

在未接触Hadoop之前你可能觉得其是一个非常复杂的框架,但如果你有静下心来看一看Hadoop的源码,你会发现这其实就是纯 Java 编写的软件。在实现过程中其并没有什么高深的技术难点,都是些基础的编程手法应用。阅读起来整体不算太难,对此感兴趣的同学,可下来进行研读一番~

相关推荐
喝醉酒的小白11 分钟前
ES 集群 A 和 ES 集群 B 数据流通
大数据·elasticsearch·搜索引擎
炭烤玛卡巴卡15 分钟前
初学elasticsearch
大数据·学习·elasticsearch·搜索引擎
it噩梦17 分钟前
es 中使用update 、create 、index的区别
大数据·elasticsearch
小灰灰要减肥31 分钟前
装饰者模式
java
张铁铁是个小胖子42 分钟前
MyBatis学习
java·学习·mybatis
0zxm1 小时前
06 - Django 视图view
网络·后端·python·django
m0_748257181 小时前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
天冬忘忧1 小时前
Flink优化----数据倾斜
大数据·flink
李昊哲小课1 小时前
deepin 安装 zookeeper
大数据·运维·zookeeper·debian·hbase
筒栗子1 小时前
复习打卡大数据篇——Hadoop MapReduce
大数据·hadoop·mapreduce