hadoop
目录
简介及搭建HDFS
大数据特点:
- Volume:数据量大;
- Variety:数据种类多;
- Value:数据价值密度低;
- Veracity:数据真实性;
- Velocity:数据增长速度越来越快。
- 连通性;
- 动态性;
- 可视性;
云计算给大数据的发展提供了良好的发展环境,大数据给云计算带来了挑战。大数据给人工智能提供了大量的数据训练,人工智能给大数据带来了更丰富的数据。
Hadoop是Apache的一个开源、可靠、可扩展的系统架构,可利用分布式架构来存储海量数据,以及实现分布式计算。即:
- 存储海量数据,
- 分布式计算。
原生的Hadoop权限管理不是很完善,实际一般使用CDH(商业版Hadoop),商业版集群部署也很简单。
三种安装模式:
- 单机模式:不能使用HDFS,只能使用MapReduce,主要用于调试MapReduce代码;
- 伪分布式:多个线程模拟多台真实机器(练习用);
- 完全分布式:多台机器部署;
伪分布式安装(hadoop需要先安装jdk的环境):
- 解压安装包;
- 修改主机名;
- 配置免密码登录;
- 在./etc/hadoop/hadoop-env.sh文件中配置java路径和hadoop配置文件的路径;
- 配置元数据服务器:在./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>
- 配置数据服务器:在./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>
- 配置数据服务器:在./etc/hadoop/slaves添加数据服务器ip
- 格式化hdfs:./bin/hadoop namenode -format;(会清空元数据,只在初次安装时使用)
- 启动dfs:./sbin/start-dfs.sh;(启动HDFS,也可以start-all.sh启动全部)
- 查看启动状态:jps命令,查看是否有:NameNode、QuorumPeerMain、DataNode、SecondaryNameNode这四个进程;
- 也可以通过http访问:50070验证是否其中成功;
- 需要完全重启时,要先关闭hadoop,再删除元数据目录(前面配置的目录),重新第八步;
- 配置环境变量,方便直接使用hadoop命令。
- 后续使用直接通过命令start-all.sh和stop-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》这篇论文的实现,是一个分布式、可扩展、可靠的文件系统。其具有以下特点:
- HDFS在存储数据时会进行切分,切出来的每一块称之为Block;
- 包含2类主要节点:namenode和datanode;
- 会对block进行备份,默认的副本数量为3;
- 由于元数据的存在,HDFS不适合于存储海量小数据,会占用大量namenode内存。
- 文件一旦上传,就不允许修改,适用于一写多读的场景,2.0版本允许追加数据。
- 能够快速的应对和检测故障;
- 不支持用户的并行写,其底层使用了租约锁,即带有时间限制的互斥锁。
- 分布式存储,支持海量数据存储;
- 高容错性,低成本,可以构建在廉价的服务器上;
- 不能做到低延迟访问,即不能毫秒级响应;但是吞吐率高;
Block
- 表示一个数据块,是HDFS中存储数据的基本单位;
- 每一个上传到HDFS中的文件的会被切分成一个或者多个block;
- 在1.0中每个block大小默认64MB,在2.0中默认128MB,文件小于块大小则不会切分;
namenode
- 负责管理datanode和存储元数据;
- 元数据内容及特点:
- 存储了文件路径、切块数量、副本数量、副本与datanode的关系;
- 存储在内存和磁盘中,分别是为了快速查询和崩溃恢复;
- 存储目录通过hadoop.tmp.dir配置,其中存储了edits和fsimage;
- fsimage用于记录元数据,但不是实时的,edits用于记录写操作;
- 当namenode收到写请求时会先记录到edits中,若成功再修改内存中的元数据;
- edits和fsimage会进行合并,合并的条件有:
- 距上次合并到达指定时间(fs.checkpoint.period指定,默认3600s);
- 当edits达到指定大小(fs.checkpoint.size指定,默认64MB);
- 在namenode重启时;
- 通过命令hadoop dfsadmin -rollEdits合并;
- datanode会定时(默认3s)给namenode发送RPC心跳表示需要namenode管理;
- 当前节点信息;
- 当前节点存储的block信息;
- 若namenode长时间(默认10m)没有收到datanode心跳,则认为datanode已经lost,此时会将这个datanode的信息备份到其他节点保证副本数量;
- namenode在1.0中只能存在一个,在2.0的完全分布式中最多只能存在两个。
- namenode重启时会合并edits和fsimage,加载fsimage到内存,等待datanode发送心跳(校验数据正确性和完整性),这个过程称为安全模式,此时不对外提供服务;
- 如果重启后namenode一直处于安全模式,则代表有数据彻底丢失了,此时需要强制退出安全模式:hadoop dfsadmin -safemode leave;
副本放置策略:
- 在HDFS中副本数量默认为3;
- 若为集群内部上传,则第一个副本就在上传的datanode;
- 若为集群外部上传,则namenode会选择相对空闲的datanode存储第一个副本;
- 第二个副本会优先放在和第一个副本不同机架的节点上;
- 第三个副本会优先放在和第一、二个副本不同机架的节点上;
- 更多的副本则会优先放在相对空闲的节点;
- hadoop中的机架实际上是手动编写的一个映射,是逻辑机架,不是物理机架,默认不开启机架,需要在配置中开启机架;
secondarynamenode:在1.0版本,是辅助namenode进行元数据的合并,但在2.0版本中被舍弃了;
扩展:Federation HDFS ——联邦HDFS,即对namenode进行分流;
datanode
- 用于进行数据的存储,数据以block的形式存储;
- 每隔3s向namenode发送心跳检测;
fsimage和edits
这两个文件生成的过程及作用如下:
- 执行格式化后指定的元数据目录下会生成:dfs/name(存储元数据)、dfs/data(存储数据)、dfs/name/in_user.lock(避免启动多个namenode)、fsimage初始版本;
- 在HDFS启动后,会生成edits_inprogress文件;
- 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目录
- 在namenode格式化时会自动生成,下面有name、data、namesecondary三个目录;
- 启动HDFS时,name目录下会生成in_use.lock文件,是为了防止启动多个namenode节点;
- HDFS的每一次写操作都会分配一个递增的事物编号;
- 新的操作会写到edits_inprogress_num 文件中,num为事物编号;
- HDFS第一次启动时会进行edits和fsimage文件的合并;
- 查看edits和fsimage文件:hdfs oev -i ./edits_xxxxx -o xxx.xml;
- 上传文件的拆解步骤:
- 创建同名文件名并加上_COPYING_ 后缀(OP_ADD);
- 分配blockid(OP_ALLOCATE_BLOCK_ID);
- 给时间戳分配编号(OP_SET_GENSTAMP_V2);
- 客户端传输文件到datanode(OP_ADD_BLOCK);
- 关流(OP_CLOSE);
- 重命名(OP_RENAME_OLD);
HDFS文件传输过程
文件上传过程:
- 客户端发起RPC请求;namenode收到后会检测路径合法性及权限;
- 检测通过后会生成元数据信息,并封装到输出流,返回给客户端;
- 客户端拿到输出流之后,选择最近datanode节点,采用pipeline(管道)机制做数据的上传,即客户端发送给一个datanode,该datanode会发给它的下游datanode,这样最小化推送数据的延时。
- 文件上传完毕后,datanode返回确认;
- 客户端向向namenode发送消息,namenode关闭输出流。
文件读取流程(这种方式提高了吞吐量和并发量):
- 客户端发起RPC读请求到namenode;
- namenode会查询元数据,获取这个文件对应的一个block的存储节点,然后将这些节点放入队列返回给客户端;
- 客户端按队列依次取出block的地址;
- 选择根据地址选择较近的节点读取数据;
- 读完一个block后,校验block的数据量,如果校验失败则告诉namenode该block损坏,然后换节点读取;
- 当读完一批block之后,客户端发送信息给namenode要下一批地址;
- 直到读完所有的block,客户端给namenode发送信息,然后namenode关流;
文件删除流程:
- 客户端发起RPC删除请求时,
- namenode检测发起请求的用户是否具有删除权限,没有权限则报错,有权限才继续;
- 只删除namenode的文件信息,即先记录到edits文件中,然后返回删除成功;
- 真正的删除是在datanode发送心跳检测时(默认3秒),namenode会校验block信息,返回删除指令,此时才会真正的删除。
java连接HDFS
- 引入依赖:
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.7.2</version>
</dependency>
- 连接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();
}
- 文件上传、下载、删除和重命名
@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的环境)
- 重命名mapper-site.xml.template为mapper-site.xml,并添加以下内容:
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
- 在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>
- 启动yarn进程:start-yarn.sh,然后通过jps查看状态(NodeManager和ResourceManager两个进程);
- 接着编写java代码,然后打成jar包(需要执行Main-class),拷贝到hadoop环境中;
- 运行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计算流程
- 从HDFS中获取数据;
- MapReduce将输入的数据进行逻辑切片,每一个切片是一个InputSplit对象;
- 每个InputSplit对象都会交给一个MapTask来执行;
- 每一行数据触发一次map方法;
- map方法的输入key默认是数据偏移量,输入value默认是行内容;
- 在Barrier阶段会将相同的key合并,放到迭代器中交给ReduceTask执行;
- 每个key会触发一次reduce方法;
- 将结果写入到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不应该改变最后的结果。
常见输入格式
- TextInputFormat:按行读取,key是偏移量,value是行内容;
- KeyValueTextInputFormat:整行按\t分割,第一部分为key,剩下的为value;
- 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();
}
}
- 在job对象中调用setInputFormatClass方法,指定InputFormat类;
- 到此,以上代码就可以完成将输入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));
}
}
- 在job对象调用setOutputFormatClass方法指定自定义的输出OutputFormat类。
- 到此,以上示例代码就可以修改输出格式了。
多元输入
对于需要处理在一个目录下但是格式不同的文件,可以对每种格式编写一个mapper组件,然后在运行方法中通过MultipleInput.addInputPath()静态方法绑定job、输入文件全路径、输入Format和对应mapper组件。另外,如果需要自定义输出结果文件名称和数量,可以在reduce组件中重载setup方法,初始化MultipleOutputs类,然后在reduce方法中调用MultipleOutput的write方法来指定文件名和输出的k-v,最后在运行方法中通过MultipleOutput.addNamedOutput静态方法绑定job、文件名(和reduce中写的要一致)、输出format、输出k-v类型就可以了。
Job执行流程
概念说明:
- 切片:文件的切块的描述信息(文件切片信息FileSplit在map中可以通过context获取,然后就可以获取文件相关的信息了);
- 切块:是文件块,存储的是真正的文件数据。
数据本地化策略:
- JobTracker收到MR操作请求后会访问namenode查询文件信息;
- JobTracker根据文件信息对文件进行切片,一般切片和切块大小一样;
- 根据切片数量计算MapTask数量,然后将MapTask分配到对应Block节点上。
job执行流程:
- 客户端的JobClient提交jar包并完成环境信息的收集和检测,如:组件类、KV类型是否合法、输入输出路径是否合法等,通过后向JobTracker申请JobId;
- JobTracker分配job、jobId和HDFS地址,并返回给客户端;
- 客户端JobClient将程序jar包提交到HDFS的临时文件夹,然后提交job到JobTracker;
- JobTracker进行切片,并计算MapTask和ReduceTask数量
- TaskTracker会定期向JobTracker发送心跳,若JobTracker发现有任务,则分配任务(MapTask满足数据本地化策略,ReduceTask则会分配到相对空闲的节点);
- TaskTracker领取的任务可能是MapTask,也可能是ReduceTask;
- TaskTracker去HDFS下载jar包,开启JVM子进程执行任务(每个任务都会开启JVM子进程);
- 这个过程体现了数据固定而逻辑移动;
Shuffle
hadoop的核心思想是MapReduce,而shuffle又是MapReduce的核心,shuffle的主要工作是从Map结束到Reduce开始之间的过程。
Map端的shuffle
- 提交MR程序之后会对文件进行切片,默认切片大小等于切块大小,但是若最后的剩余的数据大小/128 <1.1,则不会在切分了;(但有些文件不可切,例如压缩文件)
- 每一个切片分配一个MapTask,并且默认按行读取文件,每读取一行调用map方法;
- map方法产生的输出结果会写到缓冲区,缓冲区默认大小100MB;
- 当写入缓冲区的数据到达阈值0.8之后,则会写到硬盘,称为溢写(Spill);
- 缓冲区中数据默认是分区并且排序了的;
- 如果指定了Combiner,还会在缓冲区中进行合并;
- 缓冲区缓冲区本质上是一个环形缓冲区;
- 溢写的同时,map输出的数据会继续往缓冲区存放,要数据大于缓冲区容量才会阻塞;
- 最后 一次溢写之后如果缓冲区中仍然有数据,则写到最后一个溢写文件;
- 溢写完成后会将所有的溢写文件**合并(merge)**为一个文件,即为结果文件;
- 合并过程会对数据进行整体的分区和排序;
注意:
- 溢写过程不一定会发生,有可能会直接写入到结果文件中;
- 切片的大小不能完全确定溢写次数和溢写文件大小;
- 溢写文件的大小不一定是80MB(不仅仅是最后一个溢写文件);
Reduce端的shuffle
首先将map端产生的输出文件拷贝到reduce,接着进行合并排序,就是merge阶段,最后才是reduce过程产生结果了。
- ReduceTask的设置和分区相关,每个分区对应一个ReduceTask;
- ReduceTask启动fetch线程去MapTask抓取当前ReduceTask所对应的分区的数据;
- 将抓取的数据进行合并(merge),合并为一个结果文件;
- 合并会再次进行整体排序;
- 将相同的key的value放入一个list中,然后产生itearable对象;
- 每个key调用一次reduce方法;
注意:
- fetch线程是通过http请求抓取数据的,fetch线程数默认为5;
- merge因子默认为10,表示每10个小文件合并成一个大文件;
- ReduceTask启动阈值是0.05,表示MapTask执行完5%之后就开始抓取数据;
shuffle过程说明:
- spill过程不一定会发生(当MapTask输出的数据量小于溢写缓冲区大小×溢写阈值时,溢写阈值一般为80%,这样可以避免阻塞写入缓冲区);
- 发生了spill过程,会flush数据到spill文件中;
- Spill理论是80MB,但需要考虑序列化因素;
- 输出数据量大小取决于业务,不能只凭maptask处理的切片大小衡量;
- 每一个MapTask对应一个溢写缓冲区;
- 溢写缓冲区本质是一个字节数组;
- 溢写缓冲区也叫环形缓冲区;
- merge过程在没有发生spill或者spill只生成了一个spill结果文件时不会发生;
shuffle调优
map端的调优:
- 适当调大溢写缓冲区,减少磁盘IO,一般为250~400MB;
- 调大阈值,减少spill次数(不建议,因为可能造成线程阻塞);
- 添加Combine组件,减小输出文件大小,从而减少磁盘IO;
- 结果文件可以压缩,节省带宽,但是会耗费CPU;
reduce端的调优:
- 调整Fetch线程数,调节策略是接近或等于map任务数量;
- 若文件太多可以适当调大merge因子;
- 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;
数据倾斜
数据倾斜是指数据分布不均匀,导致部分任务执行很慢的情况,具体又分以下两种;
- map端数据倾斜,map产出数据倾斜的原因只有一个——处理不可切的文件,且文件大小不均匀;hadoop中很多压缩文件都是不可切的,这种目前无解;
- 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台。
节点说明:
- NameNode:两个NameNode节点,一个是Active,另一个是Standby,有且只有一个Active对外提供服务,NN之间通过JournalNode来同步数据,3.x版本可以配置更多NN;
- JournalNode:用于NN之间的数据通讯,一般为奇数个;
- ResourceManager:与客户端进行交互,管理NodeManager,资源管理和调度;
- DataNode:负责存储数据;
- 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的区别是前者在任意节点执行则会操作所有节点,后者则这只操作执行命令的节点:
- 启动三台zookeeper:./zkServer.sh start;
- 格式化zookeeper:hdfs zkfc -formatZK;
- 任意节点执行:hadoop-daemons.sh start journalnode,(jps检查Journalnode进程)
- 初次启动节点1的namenode格式化:hadoop namenode -format,
- 节点1启动namenode:hadoop-daemon.sh start namenode,
- 节点2namenode变为standby namenode:hdfs namenode -bootstrapStandby,
- 节点2启动namenode:hadoop-daemon.sh start namenode,(检查namenode进程)
- 启动三台datanode,任意节点执行:hadoop-daemons.sh start datanode,
- 节点1,2启动FalioverControllerActive:hadoop-daemon.sh start zkfc,(故障切换)
- 以上3到10步可以通过任意节点执行:start-dfs.sh来代替,但初次搭建不建议使用,一是为了理解步骤,二是该方式不可靠性。
- 节点1启动Resourcemananger:start-yarn.sh,(接着检查nodemanager进程)
- 节点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)是一个全局的资源管理器,负责整个系统的资源管理和分配,主要有两个组件构成:
- 调度器(Scheduler):根据应用的资源需求进行资源分配,而资源分配的单位是资源容器(Resource Container)(默认一个Container为1核1GB),Container是一个动态的资源分配单位。
- 应用程序管理器(Applications Manager),负责整个系统中所有应用程序的提交、与调度器的资源协商、启动和监控ApplicationMaster,还有失败重启等任务。
ApplicationMaster的主要功能是
- 与RM调度器协商以获取资源;
- 将任务进一步分配给DN,(资源二次分配);
- 与NN通讯以启动/停止任务;
- 监控所有任务的运行状态以及失败重启;
yarn执行流程:
- 将job任务提交到ResourceManager;
- ResourceManager会为job分配资源(默认1核1G),然后交给ApplicationMaster;
- ApplicationMaster将任务拆分成多个MapTask以及ReduceTask交给NodeManager执行;
- NodeManager会为每个MapTask和ReduceTask启动一个JVM子进程执行任务;
- 执行成功则向ApplicationMaster返回成功,向ResourceManager发送资源回收信号;
- 执行失败则向ApplicationMaster返回失败,向RM发送资源回收信号,AM重新申请资源;
小文件处理
HDFS不适合海量小文件的存储,会占用namenode内存,同样,MapReduce也不适合处理海量小文件,因为处理海量小文件会生成海量MapTask,造成资源浪费,而且每启动一个MapTask就会发生一次JVM进程的启停,既耗费性能又耗费时间;这种情况的优化方案有:
- 开启JVM重用机制(在yarn-site.xml中配置);
- 将多个切片合成一个或少量切片(job调用setInputFormatClass方法指定输入格式化类为CombineTextInputFormat);
- 将多个小文件打成har包;
hadoop生态圈
- Zookeeper:分布式协作服务;
- HDFS:分布式文件系统;
- Yarn:资源管理框架;
- MapReduce:分布式计算框架;
- Hive:数据仓库,用sql语言操作hadoop;
- Pig:数据流处理,用脚本语言操作hadoop;
- Mahout:数据挖掘库;
- Flume:日志收集工具;
- sqoop:数据导入导出工具;
- HBase:实时分布式数据库,提供低延时的数据访问;
- Phoenix:使用sql操作HBase;
- Ambari:安装、部署、配置和管理工具;
实际中一般用CDH或者Ambari使用Hadoop;