hadoop

hadoop

简介及搭建HDFS

大数据特点:

  1. Volume:数据量大;
  2. Variety:数据种类多;
  3. Value:数据价值密度低;
  4. Veracity:数据真实性;
  5. Velocity:数据增长速度越来越快。
  6. 连通性;
  7. 动态性;
  8. 可视性;

云计算给大数据的发展提供了良好的发展环境,大数据给云计算带来了挑战。大数据给人工智能提供了大量的数据训练,人工智能给大数据带来了更丰富的数据。

Hadoop是Apache的一个开源、可靠、可扩展的系统架构,可利用分布式架构来存储海量数据,以及实现分布式计算。即:

  • 存储海量数据,
  • 分布式计算。

原生的Hadoop权限管理不是很完善,实际一般使用CDH(商业版Hadoop),商业版集群部署也很简单。

三种安装模式:

  • 单机模式:不能使用HDFS,只能使用MapReduce,主要用于调试MapReduce代码;
  • 伪分布式:多个线程模拟多台真实机器(练习用);
  • 完全分布式:多台机器部署;

伪分布式安装(hadoop需要先安装jdk的环境):

  1. 解压安装包;
  2. 修改主机名;
  3. 配置免密码登录;
  4. 在./etc/hadoop/hadoop-env.sh文件中配置java路径和hadoop配置文件的路径;
  5. 配置元数据服务器:在./etc/hadoop/core-site.xml文件中添加:
<configuration>
    <!-- 指定namenode(元数据)服务器的ip和通讯端口 -->
    <property>
        <name>fs.default.name</name>
        <value>hdfs://hadoop01:9000</value>
    </property>
    <!-- 指定namenode存放元数据的目录,若不配置则默认在/tmp目录下 -->
    <property>
        <name>hadoop.tmp.dir</name>
        <value>/data/hadoop-2.9.2/tmp</value>
    </property>
    <!-- 开启回收站机制,单位分钟,默认是0不开启 -->
    <property>
        <name>fs.trash.interval</name>
        <value>1440</value>
    </property>
</configuration>
  1. 配置数据服务器:在./etc/hadoop/hdfs-site.xml文件中添加:
<configuration>
    <!-- 配置副本数量(伪分布式只能是1,因为大于1时,根据副本放置策略会放在不同节点,此过程必定失败,最终会因为备份数不够而进入安全无法退出) -->
    <property>
        <name>dfs.replication</name>
        <value>1</value>
    </property>
    <!-- 关闭权限验证(使得可以在浏览器,插件中增删改文件及目录) -->
    <property>
        <name>dfs.permissions</name>
        <value>false</value>
    </property>
    <!-- 云主机需要配置该项 -->
    <property>
        <name>dfs.client.use.datanode.hostname</name>
        <value>true</value>
    </property>
</configuration>
  1. 配置数据服务器:在./etc/hadoop/slaves添加数据服务器ip
  2. 格式化hdfs:./bin/hadoop namenode -format;(会清空元数据,只在初次安装时使用)
  3. 启动dfs:./sbin/start-dfs.sh;(启动HDFS,也可以start-all.sh启动全部)
  4. 查看启动状态:jps命令,查看是否有:NameNode、QuorumPeerMain、DataNode、SecondaryNameNode这四个进程;
  5. 也可以通过http访问:50070验证是否其中成功;
  6. 需要完全重启时,要先关闭hadoop,再删除元数据目录(前面配置的目录),重新第八步;
  7. 配置环境变量,方便直接使用hadoop命令。
  8. 后续使用直接通过命令start-all.shstop-all.sh启动和关闭。

另外为了可以直接在window的idea或者eclipse中连接hadoop,需要在windows中将hadoop的tar包解压,并配置环境变量,然后去https://github.com/steveloughran/winutils 找到最接近的插件版本下载,然后覆盖拷贝到hadoop的bin目录下。

HDFS常用命令

  • 创建目录:hadoop fs -mkdir /dir;
  • 查看目录:hadoop fs -ls /;(-lsr可以递归查看子目录)
  • 查看文件:hadoop fs -cat /dir/1.txt;
  • 查看文件末尾:hadoop fs -tail /dir/1.txt;查看末尾几行
  • 删除文件:hadoop fs -rm /dir/1.txt;
  • 删除递归目录:hadoop fs -rmr /dir/;
  • 移动文件:hadoop fs -mv /dir1/1.txt /dir2/;(也可以重名名,-cp是拷贝)
  • 创建文件:hadoop fs -touchz /dir1/1.txt;
  • 将linux中文件上传到hdfs中:hadoop fs -put ./1.txt /dir/;
  • hdfs中的文件下载到linux中:hadoop fs -get /dir/1.txt ./;
  • 下载并合并:hadoop fs -getmerge /dir /merge.txt;(下载并合并目录下的所有文件)
  • 离开安全模式:hadoop dfsadmin -safemode leave;(enter是进入安全模式)
  • 打har包:hadoop archive -archiveName xxx.har -p /src /des;(不好用)
  • 查看har包中的文件:hadoop fs -ls har:///des/xxx.har

HDFS结构及特点

HDFS是来源于《Google FIle System》这篇论文的实现,是一个分布式、可扩展、可靠的文件系统。其具有以下特点:

  1. HDFS在存储数据时会进行切分,切出来的每一块称之为Block;
  2. 包含2类主要节点:namenode和datanode;
  3. 会对block进行备份,默认的副本数量为3;
  4. 由于元数据的存在,HDFS不适合于存储海量小数据,会占用大量namenode内存。
  5. 文件一旦上传,就不允许修改,适用于一写多读的场景,2.0版本允许追加数据。
  6. 能够快速的应对和检测故障;
  7. 不支持用户的并行写,其底层使用了租约锁,即带有时间限制的互斥锁。
  8. 分布式存储,支持海量数据存储;
  9. 高容错性,低成本,可以构建在廉价的服务器上;
  10. 不能做到低延迟访问,即不能毫秒级响应;但是吞吐率高;

Block

  1. 表示一个数据块,是HDFS中存储数据的基本单位;
  2. 每一个上传到HDFS中的文件的会被切分成一个或者多个block;
  3. 在1.0中每个block大小默认64MB,在2.0中默认128MB,文件小于块大小则不会切分;

namenode

  1. 负责管理datanode和存储元数据;
  2. 元数据内容及特点:
    • 存储了文件路径、切块数量、副本数量、副本与datanode的关系;
    • 存储在内存和磁盘中,分别是为了快速查询和崩溃恢复;
    • 存储目录通过hadoop.tmp.dir配置,其中存储了edits和fsimage;
  3. fsimage用于记录元数据,但不是实时的,edits用于记录写操作;
  4. 当namenode收到写请求时会先记录到edits中,若成功再修改内存中的元数据;
  5. edits和fsimage会进行合并,合并的条件有:
    • 距上次合并到达指定时间(fs.checkpoint.period指定,默认3600s);
    • 当edits达到指定大小(fs.checkpoint.size指定,默认64MB);
    • 在namenode重启时;
    • 通过命令hadoop dfsadmin -rollEdits合并;
  6. datanode会定时(默认3s)给namenode发送RPC心跳表示需要namenode管理;
    • 当前节点信息;
    • 当前节点存储的block信息;
  7. 若namenode长时间(默认10m)没有收到datanode心跳,则认为datanode已经lost,此时会将这个datanode的信息备份到其他节点保证副本数量;
  8. namenode在1.0中只能存在一个,在2.0的完全分布式中最多只能存在两个。
  9. namenode重启时会合并edits和fsimage,加载fsimage到内存,等待datanode发送心跳(校验数据正确性和完整性),这个过程称为安全模式,此时不对外提供服务;
  10. 如果重启后namenode一直处于安全模式,则代表有数据彻底丢失了,此时需要强制退出安全模式:hadoop dfsadmin -safemode leave;

副本放置策略:

  1. 在HDFS中副本数量默认为3;
  2. 若为集群内部上传,则第一个副本就在上传的datanode;
  3. 若为集群外部上传,则namenode会选择相对空闲的datanode存储第一个副本;
  4. 第二个副本会优先放在和第一个副本不同机架的节点上;
  5. 第三个副本会优先放在和第一、二个副本不同机架的节点上;
  6. 更多的副本则会优先放在相对空闲的节点;
  7. hadoop中的机架实际上是手动编写的一个映射,是逻辑机架,不是物理机架,默认不开启机架,需要在配置中开启机架;

secondarynamenode:在1.0版本,是辅助namenode进行元数据的合并,但在2.0版本中被舍弃了;

扩展:Federation HDFS ——联邦HDFS,即对namenode进行分流;

datanode

  1. 用于进行数据的存储,数据以block的形式存储;
  2. 每隔3s向namenode发送心跳检测;

fsimage和edits

这两个文件生成的过程及作用如下:

  1. 执行格式化后指定的元数据目录下会生成:dfs/name(存储元数据)、dfs/data(存储数据)、dfs/name/in_user.lock(避免启动多个namenode)、fsimage初始版本;
  2. 在HDFS启动后,会生成edits_inprogress文件;
  3. 1分钟之后(后面是1小时)会将edits_inprogress会和fsimage合并为edits文件并生成新的edits_inprogress文件;

edits_inprogress文件中记录了最新的所有事物操作,HDFS收到的每个事物操作都会记录到该文件中,合并产生的每个新的edits文件都会以begin_log开头,以end_log结尾,这两者之间就是该edits文件的所有事物操作,而生成的新的edits_inprogress的begin_log则会是生成edits的end_log+1;seen_txid文件中会存储最新的edits_inprogress文件的编号

我们的一般的事物操作可能会包含多个事物操作:例如文件上传中包括文件加入HDFS并以.Copyging_结尾、分配块id、分配时间戳版本号、写入块、写完块、重命名等几个事物操作。

查看fsimage和edits文件的命令为:hdfs oev -i 文件名 -o 输出文件 -p XML(其中oev是查看edits文件,oiv是查看fsimage文件)

fsimage_n.md5文件是fsimage文件的MD5校验文件。其内容与命令md5sum faimage文件名 的输出结果相同。fsimage文件中存储的信息包括每个文件的版本号、命名空间id、整个HDFS文件数、文件创建的时间戳、文件路径、文件块数量、上传时间、访问时间、块编号、切块大小、块实际大小、块的节点信息、文件的属主/组、文件的权限等信息。

dfs目录

  1. 在namenode格式化时会自动生成,下面有name、data、namesecondary三个目录;
  2. 启动HDFS时,name目录下会生成in_use.lock文件,是为了防止启动多个namenode节点;
  3. HDFS的每一次写操作都会分配一个递增的事物编号;
  4. 新的操作会写到edits_inprogress_num 文件中,num为事物编号;
  5. HDFS第一次启动时会进行edits和fsimage文件的合并;
  6. 查看edits和fsimage文件:hdfs oev -i ./edits_xxxxx -o xxx.xml
  7. 上传文件的拆解步骤:
    • 创建同名文件名并加上_COPYING_ 后缀(OP_ADD);
    • 分配blockid(OP_ALLOCATE_BLOCK_ID);
    • 给时间戳分配编号(OP_SET_GENSTAMP_V2);
    • 客户端传输文件到datanode(OP_ADD_BLOCK);
    • 关流(OP_CLOSE);
    • 重命名(OP_RENAME_OLD);

HDFS文件传输过程

文件上传过程:

  1. 客户端发起RPC请求;namenode收到后会检测路径合法性及权限;
  2. 检测通过后会生成元数据信息,并封装到输出流,返回给客户端;
  3. 客户端拿到输出流之后,选择最近datanode节点,采用pipeline(管道)机制做数据的上传,即客户端发送给一个datanode,该datanode会发给它的下游datanode,这样最小化推送数据的延时。
  4. 文件上传完毕后,datanode返回确认;
  5. 客户端向向namenode发送消息,namenode关闭输出流。

文件读取流程(这种方式提高了吞吐量和并发量):

  1. 客户端发起RPC读请求到namenode;
  2. namenode会查询元数据,获取这个文件对应的一个block的存储节点,然后将这些节点放入队列返回给客户端;
  3. 客户端按队列依次取出block的地址;
  4. 选择根据地址选择较近的节点读取数据;
  5. 读完一个block后,校验block的数据量,如果校验失败则告诉namenode该block损坏,然后换节点读取;
  6. 当读完一批block之后,客户端发送信息给namenode要下一批地址;
  7. 直到读完所有的block,客户端给namenode发送信息,然后namenode关流;

文件删除流程:

  1. 客户端发起RPC删除请求时,
  2. namenode检测发起请求的用户是否具有删除权限,没有权限则报错,有权限才继续;
  3. 只删除namenode的文件信息,即先记录到edits文件中,然后返回删除成功;
  4. 真正的删除是在datanode发送心跳检测时(默认3秒),namenode会校验block信息,返回删除指令,此时才会真正的删除。

java连接HDFS

  1. 引入依赖:
<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-client</artifactId>
    <version>2.7.2</version>
</dependency>
  1. 连接hdfs
private FileSystem fs;
@Before
public void connect() throws URISyntaxException, IOException {
    //hadoop环境变量对象
    Configuration conf = new Configuration();
    //可以设置环境变量,但只是局部生效
    conf.set("dfs.replication", "1");
    //连接HDFS
    fs = FileSystem.get(new URI("hdfs://192.168.48.101:9000"), conf);
}
@After
public void close() throws IOException {
    fs.close();
}
  1. 文件上传、下载、删除和重命名
@Test
public void download() throws IOException {
    InputStream in = fs.open(new Path("/test1/1.txt"));
    OutputStream out = new FileOutputStream(new File("1.txt"));
    IOUtils.copyBytes(in, out, new Configuration());
}
@Test
public void upload() throws IOException {
    OutputStream out = fs.create(new Path("/test1/1.txt"));
    InputStream in = new FileInputStream(new File("1.txt"));
    IOUtils.copyBytes(in, out, new Configuration());
}
@Test
public void delete() throws IOException {
    //重命名
    //fs.rename(new Path("/test1/1.txt"), new Path("/test1/2.txt"));
    //后面的true表示递归删除
    fs.delete(new Path("/test1/1.txt"), true);
}

搭建分布式计算

搭建入门案例,完成打印传入参数的功能,步骤如下:(MapReduce需要HDFS的环境)

  1. 重命名mapper-site.xml.template为mapper-site.xml,并添加以下内容:
<property>
    <name>mapreduce.framework.name</name>
    <value>yarn</value>
</property>
  1. yarn-site.xml中添加以下内容:
<property>
    <!-- value的值来源于slaves中的值 -->
    <name>yarn.resourcemanager.hostname</name>
    <value>hadoop01</value>
</property>
<property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
</property>
  1. 启动yarn进程:start-yarn.sh,然后通过jps查看状态(NodeManager和ResourceManager两个进程);
  2. 接着编写java代码,然后打成jar包(需要执行Main-class),拷贝到hadoop环境中;
  3. 运行jar包:hadoop jar xxx.jar

mapreduce示例代码

编写map组件

编写HelloMap类,继承hadoop的Mapper类,该类有四个泛型,依次代表:输入key、value和输出key、value;输入key、value分别是输入文件的行首偏移量(行首相对文件首的偏移)和该行的内容,故输入类型一般固定为LongWriteable、Text(可以自定义),输出key和value则可以自定义;接着可以通过重载map方法来编写需要的处理逻辑,示例代码如下:

public class HelloMap extends Mapper<LongWritable, Text, LongWritable, Text> {
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //map可以多次执行write方法
        context.write(key, value);
    }
}

编写reduce组件

编写HelloReduce类,继承hadoop的Reducer类,该类的四个泛型依次代表:map组件的输出的key、value类型,reduce的输出key、value类型;接着可以通过重载reduce方法,实现对map组件的输出结果做进一步处理,示例代码如下:

public class HelloReduce extends Reducer<LongWritable, Text, LongWritable, Text> {
    @Override
    protected void reduce(LongWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        String res = "";
        for (Text value : values) {
            res += value.toString();
        }
        context.write(key, new Text(res));
    }
}

编写driver

编写main方法,通过Job对象实现调用map、reduce组件,指定输入输出文件等功能:

public class HelloMain {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Configuration configuration = new Configuration();
        //获取job对象
        Job job = Job.getInstance(configuration);
        //设置方法入口驱动类
        job.setJarByClass(HelloMain.class);
        //设置mapper组件类和mapper输出的key、value类型
        job.setMapperClass(HelloMap.class);
        job.setMapOutputKeyClass(LongWritable.class);
        job.setMapOutputValueClass(Text.class);
        //设置reduce组件类和reduce输出的key、value类型
        job.setReducerClass(HelloReduce.class);
        job.setOutputKeyClass(LongWritable.class);
        job.setOutputValueClass(Text.class);
        //设置hdfs中的文件路径, 一般就写到目录级别,就可以处理该目录下的所有文件
        FileInputFormat.setInputPaths(job, new Path("hdfs://hadoop01:9000/test1"));
        //设置文件输出路径, 一般就设置在输入目录下,且不能事先存在
        FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop01:9000/test1/result"));
        //提交任务
        job.waitForCompletion(true);
        //嵌套job, 即在该job完成后自动立即启动一个新的job
        /**
        if(job.waitForCompletion(true)){
            //获取job2对象
            Job job = Job.getInstance(configuration);
            //...
            job.waitForCompletion(true);
        }
        **/
    }
}

打包运行

可以在本地直接运行,也可以将编写好的代码打成jar包(需要指定Main-class),然后拷贝到hadoop环境执行命令:hadoop jar xxx.jar 启动类 输入路径 输出路径

案例结果

在上面的示例代码中,只是实现了将hadoop的输入参数原样的输出到输出文件中,方便观察输入参数。运行结果如下:

//在/test1/上传1.txt的文件内容如下
hello hadoop
hello world1
hello world2
//运行程序将在/test1/下产生result目录,
//其包含_SUCCESS空文件和part-r-00000文件及内容为:
0	hello hadoop
13	hello world1
26	hello world2

根据入门案例,就可以尝试编写统计单词频次、求平均值、求最大值、去重、排序等简单案例

MapReduce计算流程

  1. 从HDFS中获取数据;
  2. MapReduce将输入的数据进行逻辑切片,每一个切片是一个InputSplit对象;
  3. 每个InputSplit对象都会交给一个MapTask来执行;
  4. 每一行数据触发一次map方法;
  5. map方法的输入key默认是数据偏移量,输入value默认是行内容;
  6. 在Barrier阶段会将相同的key合并,放到迭代器中交给ReduceTask执行;
  7. 每个key会触发一次reduce方法;
  8. 将结果写入到HDFS中;

计算组件

map组件

map组件可以单独存在,也可以和reduce一起使用,单独使用时,map的输出会直接输出到指定文件,当有reduce时,map的输出会先聚合key(将相同的key合并为一个,将key对应的value放在迭代器中),同时还要将key按字典升序排列,再将key、value交给reduce处理,reduce的输出才会最终输出到文件。而reduce组件不能单独存在,它必须接受map组件的输出。

数据传输

在MapReduce中,要求传输的数据能够被序列化,hadoop中默认的序列化机制是avro,并对avro进行了封装,具体使用方法是编写JavaBean实现Writable接口,并实现Writable接口的序列化和反序列化的方法,如果要自定义排序,则需要实现WritableComparable接口

分区

分区是将数据进行分类,默认只有一个分区,如果指定了多个分区,则每个分区都要对应一个ReduceTask,每个ReduceTask都会产生一个结果文件,默认是使用HashPartitioner进行分区,如果要自定义分区则需要编写类继承Partitioner类,两个泛型参数分别为map的输出key、value;然后重载getPartition方法,最后就可以直接通过Job对象设置reduce数量和分区方式。这样,就可以将一个map的输出交给多个不同的reduce处理。另外,分区只是将数据进行分类,不会对结果产生影响。

reduce迭代器

reduce中接收参数的迭代器只能迭代一次,多次迭代只有第一次会起作用,若一定要多次迭代,可以先将迭代器保存到集合中,然后遍历集合。迭代器底层使用了地址复用技术,即每次迭代是在同一个地址块进行的,节省了内存,只能迭代一次也是这个原因;

Combiner组件

在有多个map的情况下,想要减轻reduce的负担,可以添加Combiner组件;使用方法是在编写好map和reduce后再编写一个类继承reduce类,即实际就是一个reduce,然后在Job中指定Combiner类就可以了。该组件的具体功能是在执行reduce之前对每个map预先处理一次(一般就是预先聚合key),combiner不应该改变最后的结果。

常见输入格式

  1. TextInputFormat:按行读取,key是偏移量,value是行内容;
  2. KeyValueTextInputFormat:整行按\t分割,第一部分为key,剩下的为value;
  3. SequenceFileInputFormat、SequenceFileInputFilter、DBInputFormat等;

自定义输入k-v

编写类继承FileInputFormat类,创建类继承RecordReader类,两个类的泛型都是要自定义的k-v类型,第一个类的作用就是返回第二个类,第二类中才有真正的自定义方法。示例代码(完成将输入key改为行号)如下:

RecordReader

编写类继承RecordReader抽象类,并实现相关方法,

public class LineNumberRecordReader extends RecordReader<IntWritable, Text> {
    private LineReader reader;
    private IntWritable key = new IntWritable();
    private Text value = new Text();
    private int count = 0;

    @Override
    public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        // 获取路径
        Path path = ((FileSplit) split).getPath();
        // 获取HDFS文件系统对象
        FileSystem fileSystem = path.getFileSystem(context.getConfiguration());
        // 获取文件输入流
        InputStream in = fileSystem.open(path);
        // 初始化行读取器
        reader = new LineReader(in);
    }
    /**
     * 此方法会一直调用直到返回false;且该方法会在map方法之前调用
     */
    @Override
    public boolean nextKeyValue() throws IOException {
        Text tmp = new Text();
        if (reader.readLine(tmp) > 0) {
            count++;
            key.set(count);
            value.set(tmp);
            return true;
        }
        return false;
    }
    /**
     * 此方法用于将输入key传给mapper组件,并且此方法调用次数同nextKeyValue方法
     */
    @Override
    public IntWritable getCurrentKey() {
        return key;
    }
    /**
     * 此方法用于将输入Value传给mapper组件,并且此方法调用次数同nextKeyValue方法
     */
    @Override
    public Text getCurrentValue() {
        return value;
    }
    @Override
    public float getProgress() {
        return 0;
    }
    @Override
    public void close() throws IOException {
        reader.close();
    }
}
FileInputFormat

编写类继承FileInputFormat抽象类,并实现相关方法,

public class LineNumberInputFormat extends FileInputFormat {
    @Override
    public RecordReader createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        return new LineNumberRecordReader();
    }
}
  1. 在job对象中调用setInputFormatClass方法,指定InputFormat类;
  2. 到此,以上代码就可以完成将输入key由行首偏移量改为行号了。

自定义输出k-v

编写类继承RecordWriter抽象类,并实现相关方法,然后编写类继承FileOutputFormat抽象类,并实现相关方法。示例代码(完场将k-v分割符改为“|”,行与行的分隔符使用“\n”)如下:

RecordWriter

编写类继承RecordWriter抽象类

public class myRecordWriter extends RecordWriter<LongWritable, Text> {
    private OutputStream out;
    public myRecordWriter(OutputStream out) {
        this.out = out;
    }
    @Override
    public void write(LongWritable key, Text value) throws IOException {
        out.write(key.toString().getBytes());
        out.write("|".getBytes());
        out.write(value.toString().trim().getBytes());
        out.write("\n".getBytes());
    }
    @Override
    public void close(TaskAttemptContext context) throws IOException {
        out.close();
    }
}
FileOutputFormat

编写类继承FileOutputFormat抽象类

public class myOutputFormat extends FileOutputFormat {
    @Override
    public RecordWriter getRecordWriter(TaskAttemptContext job) throws IOException {
        // 获取默认输出文件路径
        Path path = super.getDefaultWorkFile(job, "");
        // 根据路径获取输出流
        FileSystem fileSystem = path.getFileSystem(job.getConfiguration());
        return new myRecordWriter(fileSystem.create(path));
    }
}
  1. 在job对象调用setOutputFormatClass方法指定自定义的输出OutputFormat类。
  2. 到此,以上示例代码就可以修改输出格式了。

多元输入

对于需要处理在一个目录下但是格式不同的文件,可以对每种格式编写一个mapper组件,然后在运行方法中通过MultipleInput.addInputPath()静态方法绑定job、输入文件全路径、输入Format和对应mapper组件。另外,如果需要自定义输出结果文件名称和数量,可以在reduce组件中重载setup方法,初始化MultipleOutputs类,然后在reduce方法中调用MultipleOutput的write方法来指定文件名和输出的k-v,最后在运行方法中通过MultipleOutput.addNamedOutput静态方法绑定job、文件名(和reduce中写的要一致)、输出format、输出k-v类型就可以了。

Job执行流程

概念说明:

  1. 切片:文件的切块的描述信息(文件切片信息FileSplit在map中可以通过context获取,然后就可以获取文件相关的信息了);
  2. 切块:是文件块,存储的是真正的文件数据。

数据本地化策略:

  1. JobTracker收到MR操作请求后会访问namenode查询文件信息;
  2. JobTracker根据文件信息对文件进行切片,一般切片和切块大小一样;
  3. 根据切片数量计算MapTask数量,然后将MapTask分配到对应Block节点上。

job执行流程:

  1. 客户端的JobClient提交jar包并完成环境信息的收集和检测,如:组件类、KV类型是否合法、输入输出路径是否合法等,通过后向JobTracker申请JobId;
  2. JobTracker分配job、jobId和HDFS地址,并返回给客户端;
  3. 客户端JobClient将程序jar包提交到HDFS的临时文件夹,然后提交job到JobTracker;
  4. JobTracker进行切片,并计算MapTask和ReduceTask数量
  5. TaskTracker会定期向JobTracker发送心跳,若JobTracker发现有任务,则分配任务(MapTask满足数据本地化策略,ReduceTask则会分配到相对空闲的节点);
  6. TaskTracker领取的任务可能是MapTask,也可能是ReduceTask;
  7. TaskTracker去HDFS下载jar包,开启JVM子进程执行任务(每个任务都会开启JVM子进程);
  8. 这个过程体现了数据固定而逻辑移动;

Shuffle

hadoop的核心思想是MapReduce,而shuffle又是MapReduce的核心,shuffle的主要工作是从Map结束到Reduce开始之间的过程。

Map端的shuffle

  1. 提交MR程序之后会对文件进行切片,默认切片大小等于切块大小,但是若最后的剩余的数据大小/128 <1.1,则不会在切分了;(但有些文件不可切,例如压缩文件)
  2. 每一个切片分配一个MapTask,并且默认按行读取文件,每读取一行调用map方法;
  3. map方法产生的输出结果会写到缓冲区,缓冲区默认大小100MB
  4. 当写入缓冲区的数据到达阈值0.8之后,则会写到硬盘,称为溢写(Spill)
    • 缓冲区中数据默认是分区并且排序了的;
    • 如果指定了Combiner,还会在缓冲区中进行合并;
    • 缓冲区缓冲区本质上是一个环形缓冲区;
  5. 溢写的同时,map输出的数据会继续往缓冲区存放,要数据大于缓冲区容量才会阻塞;
  6. 最后 一次溢写之后如果缓冲区中仍然有数据,则写到最后一个溢写文件;
  7. 溢写完成后会将所有的溢写文件**合并(merge)**为一个文件,即为结果文件;
    • 合并过程会对数据进行整体的分区和排序;

注意:

  1. 溢写过程不一定会发生,有可能会直接写入到结果文件中;
  2. 切片的大小不能完全确定溢写次数和溢写文件大小;
  3. 溢写文件的大小不一定是80MB(不仅仅是最后一个溢写文件);

Reduce端的shuffle

首先将map端产生的输出文件拷贝到reduce,接着进行合并排序,就是merge阶段,最后才是reduce过程产生结果了。

  1. ReduceTask的设置和分区相关,每个分区对应一个ReduceTask;
  2. ReduceTask启动fetch线程去MapTask抓取当前ReduceTask所对应的分区的数据;
  3. 将抓取的数据进行合并(merge),合并为一个结果文件;
    • 合并会再次进行整体排序;
  4. 将相同的key的value放入一个list中,然后产生itearable对象;
  5. 每个key调用一次reduce方法;

注意:

  1. fetch线程是通过http请求抓取数据的,fetch线程数默认为5;
  2. merge因子默认为10,表示每10个小文件合并成一个大文件;
  3. ReduceTask启动阈值是0.05,表示MapTask执行完5%之后就开始抓取数据;

shuffle过程说明:

  1. spill过程不一定会发生(当MapTask输出的数据量小于溢写缓冲区大小×溢写阈值时,溢写阈值一般为80%,这样可以避免阻塞写入缓冲区);
  2. 发生了spill过程,会flush数据到spill文件中;
  3. Spill理论是80MB,但需要考虑序列化因素;
  4. 输出数据量大小取决于业务,不能只凭maptask处理的切片大小衡量;
  5. 每一个MapTask对应一个溢写缓冲区;
  6. 溢写缓冲区本质是一个字节数组;
  7. 溢写缓冲区也叫环形缓冲区;
  8. merge过程在没有发生spill或者spill只生成了一个spill结果文件时不会发生;

shuffle调优

map端的调优:

  1. 适当调大溢写缓冲区,减少磁盘IO,一般为250~400MB;
  2. 调大阈值,减少spill次数(不建议,因为可能造成线程阻塞);
  3. 添加Combine组件,减小输出文件大小,从而减少磁盘IO;
  4. 结果文件可以压缩,节省带宽,但是会耗费CPU;

reduce端的调优:

  1. 调整Fetch线程数,调节策略是接近或等于map任务数量;
  2. 若文件太多可以适当调大merge因子;
  3. reduce启动比例是5%,即5%的map任务完成就开始工作,可调节该参数,一般不动;

hdfs-site.xml配置文件:

  • dfs.namenode.support.allow.format:配置是否允许格式化,默认值为true;
  • dfs.heartbeat.interval:DN心跳间隔,默认3秒;
  • dfs.blocksize:切块大小,默认128MB,一般不动,必须设置为1024的整数倍;
  • dfs.namenode.checkpoint.period:合并阈值,默认3600秒;
  • dfs.stream-buff-size:文件流缓存大小;

mapperd-site.xml文件:

  • mapreduce.task.io.sort.mb:溢写缓冲区大小,默认100MB;
  • mapreduce.map.sort.spill.percent:溢写阈值,默认0.8;
  • mapreduce.reduce.shuffle.parallelcopies:并发拷贝线程数;
  • mapreduce.job.reduce.slowstart.completedmaps:reduce启动比例,默认5%;
  • io.sort.factor:文件合并因子,默认10;
  • mapred.compress.map.output:是否压缩结果文件,默认false;
  • mapred.map.tasks.speculative.execution:是否推测执行机制,默认true;

数据倾斜

数据倾斜是指数据分布不均匀,导致部分任务执行很慢的情况,具体又分以下两种;

  1. map端数据倾斜,map产出数据倾斜的原因只有一个——处理不可切的文件,且文件大小不均匀;hadoop中很多压缩文件都是不可切的,这种目前无解;
  2. reduce端数据倾斜,reduce端的数据倾斜一般是由group by、join、distinct、reduce by、aggregate by、cogroup、repartition等操作造成,reduce端产生的数据倾斜的场景主要有:存在业务默认值,业务本身存在热点,存在恶意数据,其解决方式一般有:
    • 添加Combiner组件;
    • 添加reduce数量;
    • 在map输出key时拼接上一个随机数,也可以减轻数据倾斜的问题。
    • 将小表缓存在map中

hadoop集群搭建

集群方案

9台:01、02、03为zookeeper集群和JN节点;04为NN、RM节点,05为NN节点,06为RM节点;07、08、09为DN和NM节点。

6台:01、02、03为zookeeper集群,01、02为namenode;03启动RM;04、05、06为JN、DN、NM。

3台:将04,05,06也在01,02,03启动,hadoop集群最少3台。

节点说明:

  1. NameNode:两个NameNode节点,一个是Active,另一个是Standby,有且只有一个Active对外提供服务,NN之间通过JournalNode来同步数据,3.x版本可以配置更多NN;
  2. JournalNode:用于NN之间的数据通讯,一般为奇数个;
  3. ResourceManager:与客户端进行交互,管理NodeManager,资源管理和调度;
  4. DataNode:负责存储数据;
  5. NodeManager:负责执行任务,一般和DN在一起,使满足本地化策略;

后面介绍3台的集群搭建步骤。

准备

  • 搭建伪分布式环境

  • 配置免密登录:3台相互都配置免密登录,修改/etc/hosts文件;

  • 备份:重命名./etc/hadoop为./etc/hadoop-standalone/,即保留原来的伪分布式的配置文件;

core-site.xml

<configuration>
    <!-- 指定namenode存放元数据的目录,若不配置则默认在/tmp目录下 -->
    <property>
        <name>hadoop.tmp.dir</name>
        <value>/data/hadoop-2.9.2/tmp</value>
    </property>
    <!--用来指定hdfs的老大,ns为自定义的集群中namenode的别名-->
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://ns</value>
    </property>
    <!--执行zookeeper地址-->
    <property>
        <name>ha.zookeeper.quorum</name>
        <value>hadoop01:2181,hadoop02:2181,hadoop03:2181</value>
    </property>
</configuration>

hdfs-site.xml

<configuration>
    <!--执行hdfs的nameservice,和core-site.xml保持一致,后面的相关配置也要一致-->
    <property>
        <name>dfs.nameservices</name>
        <value>ns</value>
    </property>
    <!--ns下有两个namenode,分别是nn1,nn2-->
    <property>
        <name>dfs.ha.namenodes.ns</name>
        <value>nn1,nn2</value>
    </property>
    <!--nn1的RPC通信地址-->
    <property>
        <name>dfs.namenode.rpc-address.ns.nn1</name>
        <value>hadoop01:9000</value>
    </property>
    <!--nn1的http通信地址-->
    <property>
        <name>dfs.namenode.http-address.ns.nn1</name>
        <value>hadoop01:50070</value>
    </property>
    <!--nn2的RPC通信地址-->
    <property>
        <name>dfs.namenode.rpc-address.ns.nn2</name>
        <value>hadoop02:9000</value>
    </property>
    <!--nn2的http通信地址-->
    <property>
        <name>dfs.namenode.http-address.ns.nn2</name>
        <value>hadoop02:50070</value>
    </property>
    <!--指定namenode的元数据在JournalNode上的存放位置,这样,namenode2可以从jn集群里获取最新的namenode的信息,达到热备的效果-->
    <property>
        <name>dfs.namenode.shared.edits.dir</name>
        <value>qjournal://hadoop01:8485;hadoop02:8485;hadoop03:8485/ns</value>
    </property>
    <!--指定JournalNode存放数据的位置-->
    <property>
        <name>dfs.journalnode.edits.dir</name>
        <value>/data/hadoop-2.9.2/tmp/journal</value>
    </property>
    <!--开启namenode故障时自动切换-->
    <property>
        <name>dfs.ha.automatic-failover.enabled</name>
        <value>true</value>
    </property>
    <!--配置切换的实现方式-->
    <property>
        <name>dfs.client.failover.proxy.provider.ns</name>
        <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
    </property>
    <!--配置隔离机制-->
    <property>
        <name>dfs.ha.fencing.methods</name>
        <value>sshfence</value>
    </property>
    <!--配置隔离机制的ssh登录秘钥所在的位置-->
    <property>
        <name>dfs.ha.fencing.ssh.private-key-files</name>
        <value>/root/.ssh/id_rsa</value>
    </property>
    <!--配置namenode数据存放的位置,可以不配置,如果不配置,默认用的是core-site.xml里配置的hadoop.tmp.dir的路径-->
    <!-- <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:///data/hadoop-2.9.2/tmp/namenode</value>
    </property> -->
    <!--配置datanode数据存放的位置,可以不配置,如果不配置,默认用的是core-site.xml里配置的hadoop.tmp.dir的路径-->
    <!-- <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:///data/hadoop-2.9.2/tmp/datanode</value> -->
    </property>
    <!--配置block副本数量-->
    <property>
        <name>dfs.replication</name>
        <value>3</value>
    </property>
    <!--设置hdfs的操作权限,false表示任何用户都可以在hdfs上操作文件-->
    <property>
        <name>dfs.permissions</name>
        <value>false</value>
    </property>
</configuration>

mapred-site.xml

<configuration>
    <!--指定mapreduce运行在yarn上-->
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
</configuration>

yarn-site.xml

<configuration>
    <!-- 开启YARN HA, 开启高可用 --> 
    <property>
        <name>yarn.resourcemanager.ha.enabled</name>
        <value>true</value>
    </property>
    <!-- 指定两个resourcemanager的名称 --> 
    <property>
        <name>yarn.resourcemanager.ha.rm-ids</name>
        <value>rm1,rm2</value>
    </property>
    <!-- 配置rm1,rm2的主机 --> 
    <property>
        <name>yarn.resourcemanager.hostname.rm1</name>
        <value>hadoop01</value>
    </property>
    <property>
        <name>yarn.resourcemanager.hostname.rm2</name>
        <value>hadoop03</value>
    </property>
    <!--开启yarn恢复机制-->
    <property>
        <name>yarn.resourcemanager.recovery.enabled</name>
        <value>true</value>
    </property>
    <!--执行rm恢复机制实现类-->
    <property>
        <name>yarn.resourcemanager.store.class</name>
        <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value>
    </property>
    <!-- 配置zookeeper的地址 -->  
    <property>
        <name>yarn.resourcemanager.zk-address</name>
        <value>hadoop01:2181,hadoop02:2181,hadoop03:2181</value>
        <description>描述信息</description>
    </property>
    <!-- 指定YARN HA的名称 -->
    <property>
        <name>yarn.resourcemanager.cluster-id</name>
        <value>yarn-ha</value>
    </property>
    <!--指定yarn的老大 resoucemanager的地址-->
    <property>
        <name>yarn.resourcemanager.hostname</name>
        <value>hadoop01</value>
    </property>
    <!--NodeManager获取数据的方式-->
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
</configuration>

slaves

hadoop01
hadoop02
hadoop03

启动集群

启动之前先将上面改好的配置文件复制到其他的hadoop主机;

说明:hadoop-daemons.sh与hadoop-daemon.sh的区别是前者在任意节点执行则会操作所有节点,后者则这只操作执行命令的节点:

  1. 启动三台zookeeper:./zkServer.sh start
  2. 格式化zookeeper:hdfs zkfc -formatZK
  3. 任意节点执行:hadoop-daemons.sh start journalnode,(jps检查Journalnode进程)
  4. 初次启动节点1的namenode格式化:hadoop namenode -format
  5. 节点1启动namenode:hadoop-daemon.sh start namenode
  6. 节点2namenode变为standby namenode:hdfs namenode -bootstrapStandby
  7. 节点2启动namenode:hadoop-daemon.sh start namenode,(检查namenode进程)
  8. 启动三台datanode,任意节点执行:hadoop-daemons.sh start datanode
  9. 节点1,2启动FalioverControllerActive:hadoop-daemon.sh start zkfc,(故障切换)
  10. 以上3到10步可以通过任意节点执行:start-dfs.sh来代替,但初次搭建不建议使用,一是为了理解步骤,二是该方式不可靠性。
  11. 节点1启动Resourcemananger:start-yarn.sh,(接着检查nodemanager进程)
  12. 节点3启动副Resourcemanager:yarn-daemon.sh start resourcemanager

验证集群

浏览器输入:http://ip:50070 检查两台的namenode信息、状态等;(yarn管理地址:http://节点1ip:8088 )。
到此三台hadoop的集群搭建完成。
只要搭建成功了,后面就可以通过命令:start-all.sh,stop-all.sh来启动和停止了。

热添加hadoop数据节点方式:修改所有的/etc/host文件,修改所有的slaves文件,然后将任意节点的所有配置文件复制给新节点,新节点执行命令start-启动

注意:hadoop故障没有自动切换,可以查看hadoop-root-zkfc-hadoop02.log日志文件,若报fuser: command not found的警告,则需要安装相关的包,通过yum search fuser发现需要yum install psmisc

yarn介绍

在hadoop1.0中,没有yarn,所有任务调度和资源管理都是MapReduce自己管理,最核心的节点就是JobTracker(负责资源管理和任务调度,TaskTracker负责任务执行),JobTracker决定了整个集群的性能,经过试验,JobTracker最多只能管理4000个节点;

yarn是hadoop2.0的一种新的资源管理器,可为上层应用提供统一的资源管理和调度,它的引入为集群在资源利用率、资源统一管理和数据共享等方面带来了巨大的好处;而MapReduce成为了纯粹的计算框架。

yarn的基本思想是将原本的JobTracker(资源管理和作业调度)分离,主要方法是创建一个全局的ResourceManager(RM,资源管理)和若干个针对程序(MapReduce)的ApplicationMaster(AM,任务执行)。

ResourceManager(RM)是一个全局的资源管理器,负责整个系统的资源管理和分配,主要有两个组件构成:

  1. 调度器(Scheduler):根据应用的资源需求进行资源分配,而资源分配的单位是资源容器(Resource Container)(默认一个Container为1核1GB),Container是一个动态的资源分配单位。
  2. 应用程序管理器(Applications Manager),负责整个系统中所有应用程序的提交、与调度器的资源协商、启动和监控ApplicationMaster,还有失败重启等任务。

ApplicationMaster的主要功能是

  1. 与RM调度器协商以获取资源;
  2. 将任务进一步分配给DN,(资源二次分配);
  3. 与NN通讯以启动/停止任务;
  4. 监控所有任务的运行状态以及失败重启;

yarn执行流程:

  1. 将job任务提交到ResourceManager;
  2. ResourceManager会为job分配资源(默认1核1G),然后交给ApplicationMaster;
  3. ApplicationMaster将任务拆分成多个MapTask以及ReduceTask交给NodeManager执行;
  4. NodeManager会为每个MapTask和ReduceTask启动一个JVM子进程执行任务;
  5. 执行成功则向ApplicationMaster返回成功,向ResourceManager发送资源回收信号;
  6. 执行失败则向ApplicationMaster返回失败,向RM发送资源回收信号,AM重新申请资源;

小文件处理

HDFS不适合海量小文件的存储,会占用namenode内存,同样,MapReduce也不适合处理海量小文件,因为处理海量小文件会生成海量MapTask,造成资源浪费,而且每启动一个MapTask就会发生一次JVM进程的启停,既耗费性能又耗费时间;这种情况的优化方案有:

  1. 开启JVM重用机制(在yarn-site.xml中配置);
  2. 将多个切片合成一个或少量切片(job调用setInputFormatClass方法指定输入格式化类为CombineTextInputFormat);
  3. 将多个小文件打成har包;

hadoop生态圈

  1. Zookeeper:分布式协作服务;
  2. HDFS:分布式文件系统;
  3. Yarn:资源管理框架;
  4. MapReduce:分布式计算框架;
  5. Hive:数据仓库,用sql语言操作hadoop;
  6. Pig:数据流处理,用脚本语言操作hadoop;
  7. Mahout:数据挖掘库;
  8. Flume:日志收集工具;
  9. sqoop:数据导入导出工具;
  10. HBase:实时分布式数据库,提供低延时的数据访问;
  11. Phoenix:使用sql操作HBase;
  12. Ambari:安装、部署、配置和管理工具;

实际中一般用CDH或者Ambari使用Hadoop;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值