架构
Hadoop整体由HDFS、YARN、MapReduce三大部分组成,推荐架构参考:https://www.cnblogs.com/zhjh256/p/10573684.html。
注:2.x的时候引入了YARN、并调整了一系列进程,其性能较差,本文主要讲解2.0体系。1.0可以参考https://www.cnblogs.com/kubixuesheng/p/5525306.html。
官方文档(最好的参考资料):http://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html
整个HDFS集群由Namenode和Datanode构成master-worker(主从)模式。Namenode负责构建命名空间,管理文件的元数据等,而Datanode负责实际存储数据,负责读写工作。
名称节点,是HDFS的守护程序(一个核心程序),对整个分布式文件系统进行总控制,会纪录所有的元数据分布存储的状态信息,比如文件是如何分割成数据块的,以及这些数据块被存储到哪些节点上,还有对内存和I/O进行集中管理,用户首先会访问Namenode,通过该总控节点获取文件分布的状态信息,找到文件分布到了哪些数据节点,然后在和这些节点打交道,把文件拿到。故这是一个核心节点,发生故障将导致集群崩溃。
所以有备用名称节点、或称为辅助名称节点,或者检查点节点,它是监控HDFS状态的辅助后台程序,可以保存名称节点的副本,故每个集群都有一个,它与NameNode进行通讯,定期保存HDFS元数据快照。NameNode故障可以作为备用NameNode使用,CDH版本已经支持自动切换。
hdfs命令和hadoop命令的区别
可以参考https://www.cnblogs.com/lzfhope/p/6952869.html,给了较好的解释,简而言之,hdfs的相当一部分的功能可以使用hdoop来替代(目前),但hdfs有自己的一些独有的功能。hadoop主要面向更广泛复杂的功能。如果某些特性在hadoop或hdfs命令已经过时,则会给出提示,如下:
[root@quickstart ~]# hadoop fsck /user/root/hue.json -files -locations -racks #查看一个文件的详细信息,注意:此命令只能在namenode里输入,在datanode里输入会报错 DEPRECATED: Use of this script to execute hdfs command is deprecated. Instead use the hdfs command for it. Connecting to namenode via http://quickstart.cloudera:50070/fsck?ugi=root&files=1&locations=1&racks=1&path=%2Fuser%2Froot%2Fhue.json FSCK started by root (auth:SIMPLE) from /127.0.0.1 for path /user/root/hue.json at Sun Apr 07 01:55:14 PDT 2019 /user/root/hue.json 63658 bytes, 1 block(s): OK Status: HEALTHY Total size: 63658 B Total dirs: 0 Total files: 1 Total symlinks: 0 Total blocks (validated): 1 (avg. block size 63658 B) Minimally replicated blocks: 1 (100.0 %) Over-replicated blocks: 0 (0.0 %) Under-replicated blocks: 0 (0.0 %) Mis-replicated blocks: 0 (0.0 %) Default replication factor: 1 Average block replication: 1.0 Corrupt blocks: 0 Missing replicas: 0 (0.0 %) Number of data-nodes: 1 Number of racks: 1 FSCK ended at Sun Apr 07 01:55:14 PDT 2019 in 1 milliseconds The filesystem under path '/user/root/hue.json' is HEALTHY
dfs当前目录
当前目录为/user/$USER/,如下:
[root@quickstart ~]# hadoop fs -ls /user/root Found 1 items -rw-r--r-- 1 root supergroup 63658 2019-04-07 01:40 /user/root/hue.json
查看所有用户的默认目录:
[root@quickstart ~]# hadoop fs -ls /user Found 9 items drwxr-xr-x - cloudera cloudera 0 2019-04-05 22:49 /user/cloudera drwxr-xr-x - mapred hadoop 0 2019-04-01 07:14 /user/history drwxrwxrwx - hive supergroup 0 2017-10-23 10:31 /user/hive drwxrwxrwx - hue supergroup 0 2019-04-01 07:19 /user/hue drwxr-xr-x - hdfs supergroup 0 2019-04-06 06:55 /user/impala drwxrwxrwx - jenkins supergroup 0 2017-10-23 10:30 /user/jenkins drwxrwxrwx - oozie supergroup 0 2017-10-23 10:30 /user/oozie drwxrwxrwx - root supergroup 0 2019-04-07 01:40 /user/root drwxr-xr-x - hdfs supergroup 0 2017-10-23 10:31 /user/spark
写文件
在实际中,对数据文件的操作可以认为基本上都是通过java接口写到特定的目录来完成的(无论是文本文件还是Parquet文件,然后映射为Impala或Hive表)。
写文件的流程如下:
1)客户端调用DistributedFileSystem的create方法
2)DistributedFileSystem远程RPC调用Namenode在文件系统的命名空间中创建一个新文件,此时该文件没有关联到任何block。 这个过程中,Namenode会做很多校验工作,例如是否已经存在同名文件,是否有权限,如果验证通过,返回一个FSDataOutputStream对象。 如果验证不通过,抛出异常到客户端。
3)客户端写入数据的时候,DFSOutputStream分解为packets(数据包),并写入到一个数据队列中,该队列由DataStreamer消费。
4)DateStreamer负责请求Namenode分配新的block存放的数据节点。这些节点存放同一个Block的副本,构成一个管道。 DataStreamer将packet写入到管道的第一个节点,第一个节点存放好packet之后,转发给下一个节点,下一个节点存放 之后继续往下传递。
5)DFSOutputStream同时维护一个ack queue队列,等待来自datanode确认消息。当管道上的所有datanode都确认之后,packet从ack队列中移除。
6)数据写入完毕,客户端close输出流。将所有的packet刷新到管道中,然后安心等待来自datanode的确认消息。全部得到确认之后告知Namenode文件是完整的。 Namenode此时已经知道文件的所有Block信息(因为DataStreamer是请求Namenode分配block的),只需等待达到最小副本数要求,然后返回成功信息给客户端。
Namenode如何决定副本存在哪个Datanode?
HDFS的副本的存放策略是可靠性、写带宽、读带宽之间的权衡。默认策略如下:
- 第一个副本放在客户端相同的机器上,如果机器在集群之外(这也是通用的做法,独立的应用程序),随机选择一个(但是会尽可能选择容量不是太慢或者当前操作太繁忙的)
- 第二个副本随机放在不同于第一个副本的机架上。
- 第三个副本放在跟第二个副本同一机架上,但是不同的节点上,满足条件的节点中随机选择。
- 更多的副本在整个集群上随机选择,虽然会尽量避免太多副本在同一机架上。
副本的位置确定之后,在建立写入管道的时候,会考虑网络拓扑结构。下面是可能的一个存放策略:
这样选择很好滴平衡了可靠性、读写性能
- 可靠性:Block分布在两个机架上
- 写带宽:写入管道的过程只需要跨越一个交换机
- 读带宽:可以从两个机架中任选一个读取
Java写Parquet文件示例
读写文本文件的wordcount就太没意思了,来个读写parquet的示例,直接从eclipse远程运行、而不是生成jar,放到服务器上通过hadoop jar命令运行。
package hadoop; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.LongWritable; 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.input.TextInputFormat; import org.apache.parquet.example.data.Group; import org.apache.parquet.example.data.simple.SimpleGroupFactory; import org.apache.parquet.hadoop.ParquetOutputFormat; import org.apache.parquet.hadoop.example.GroupWriteSupport; import java.io.IOException; import java.util.Random; import java.util.StringTokenizer; import java.util.UUID; /** * * <p>Title: ParquetNewMR</p> * <p>Description: </p> * @author zjhua * @date 2019年4月7日 */ public class ParquetNewMR { public static class WordCountMap extends Mapper<LongWritable, Text, Text, IntWritable> { private final IntWritable one = new IntWritable(1); private Text word = new Text(); @Override public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); StringTokenizer token = new StringTokenizer(line); while (token.hasMoreTokens()) { word.set(token.nextToken()); context.write(word, one); } } } public static class WordCountReduce extends Reducer<Text, IntWritable, Void, Group> { private SimpleGroupFactory factory; @Override public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable val : values) { sum += val.get(); } Group group = factory.newGroup() .append("name", key.toString()) .append("age", sum); context.write(null,group); } @Override protected void setup(Context context) throws IOException, InterruptedException { super.setup(context); factory = new SimpleGroupFactory(GroupWriteSupport.getSchema(context.getConfiguration())); } } public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); String writeSchema = "message example { " + "required binary name; " + "required int32 age; " + "}"; conf.set("parquet.example.schema",writeSchema); Job job = new Job(conf); job.setJarByClass(ParquetNewMR.class); job.setJobName("parquet"); String in = "hdfs://192.168.223.141:8020/user/cloudera/wordcount/input"; String out = "hdfs://192.168.223.141:8020/user/cloudera/pq_out_" + UUID.randomUUID().toString(); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); job.setOutputValueClass(Group.class); job.setMapperClass(WordCountMap.class); job.setReducerClass(WordCountReduce.class); job.setInputFormatClass(TextInputFormat.class); job.setOutputFormatClass(ParquetOutputFormat.class); FileInputFormat.addInputPath(job, new Path(in)); ParquetOutputFormat.setOutputPath(job, new Path(out)); ParquetOutputFormat.setWriteSupportClass(job, GroupWriteSupport.class); job.waitForCompletion(true); } }
查看生成的文件:
读文件
读文件的流程如下:
1)客户端传递一个文件Path给FileSystem的open方法
2)DFS采用RPC远程获取文件最开始的几个block的datanode地址。Namenode会根据网络拓扑结构决定返回哪些节点(前提是节点有block副本),如果客户端本身是Datanode并且节点上刚好有block副本,直接从本地读取。
3)客户端使用open方法返回的FSDataInputStream对象读取数据(调用read方法)
4)DFSInputStream(FSDataInputStream实现了改类)连接持有第一个block的、最近的节点,反复调用read方法读取数据
5)第一个block读取完毕之后,寻找下一个block的最佳datanode,读取数据。如果有必要,DFSInputStream会联系Namenode获取下一批Block 的节点信息(存放于内存,不持久化),这些寻址过程对客户端都是不可见的。
6)数据读取完毕,客户端调用close方法关闭流对象
在读数据过程中,如果与Datanode的通信发生错误,DFSInputStream对象会尝试从下一个最佳节点读取数据,并且记住该失败节点, 后续Block的读取不会再连接该节点
读取一个Block之后,DFSInputStram会进行检验和验证,如果Block损坏,尝试从其他节点读取数据,并且将损坏的block汇报给Namenode。
客户端连接哪个datanode获取数据,是由namenode来指导的,这样可以支持大量并发的客户端请求,namenode尽可能将流量均匀分布到整个集群。
Block的位置信息是存储在namenode的内存中,因此相应位置请求非常高效,不会成为瓶颈。
Java读取Parquet文件示例
package hadoop; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.LongWritable; 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.output.FileOutputFormat; import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat; import org.apache.parquet.example.data.Group; import org.apache.parquet.hadoop.ParquetInputFormat; import org.apache.parquet.hadoop.api.DelegatingReadSupport; import org.apache.parquet.hadoop.api.InitContext; import org.apache.parquet.hadoop.api.ReadSupport; import org.apache.parquet.hadoop.example.GroupReadSupport; import java.io.IOException; import java.util.*; public class ParquetNewMRReader { public static class WordCountMap1 extends Mapper<Void, Group, LongWritable, Text> { protected void map(Void key, Group value, Mapper<Void, Group, LongWritable, Text>.Context context) throws IOException, InterruptedException { String name = value.getString("name",0); int age = value.getInteger("age",0); context.write(new LongWritable(age), new Text(name)); } } public static class WordCountReduce1 extends Reducer<LongWritable, Text, LongWritable, Text> { public void reduce(LongWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException { Iterator<Text> iterator = values.iterator(); while(iterator.hasNext()){ context.write(key,iterator.next()); } } } public static final class MyReadSupport extends DelegatingReadSupport<Group> { public MyReadSupport() { super(new GroupReadSupport()); } @Override public org.apache.parquet.hadoop.api.ReadSupport.ReadContext init(InitContext context) { return super.init(context); } } public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); String readSchema = "message example { " + "required binary name; " + "required int32 age; " + "}"; conf.set(ReadSupport.PARQUET_READ_SCHEMA, readSchema); Job job = new Job(conf); job.setJarByClass(ParquetNewMRReader.class); job.setJobName("parquet"); String in = "hdfs://192.168.223.141:8020/user/cloudera/pq_out_ae7d1402-4c53-45b7-bf10-54e05fcdeb58"; String out = "hdfs://localhost:8020/user/cloudera/wd2"; job.setMapperClass(WordCountMap1.class); job.setReducerClass(WordCountReduce1.class); job.setInputFormatClass(ParquetInputFormat.class); ParquetInputFormat.setReadSupportClass(job, MyReadSupport.class); ParquetInputFormat.addInputPath(job, new Path(in)); job.setOutputFormatClass(TextOutputFormat.class); FileOutputFormat.setOutputPath(job, new Path(out)); job.waitForCompletion(true); } }
生成的文件如下:
创建hive表进行验证。。。。
INFO : Completed executing command(queryId=hive_20190420132929_1fc94be6-c915-4897-babf-4d0cf52ecdbb); Time taken: 0.029 seconds INFO : OK +-------------------+--+ | tab_name | +-------------------+--+ | parquet_xxx | | test_parquet_new | +-------------------+--+ 2 rows selected (0.206 seconds) 0: jdbc:hive2://localhost:10000/default> select * from test_parquet_new; INFO : Compiling command(queryId=hive_20190420132929_9097c88f-f390-4fa2-891c-569b03fe106d): select * from test_parquet_new INFO : Semantic Analysis Completed INFO : Returning Hive schema: Schema(fieldSchemas:[FieldSchema(name:test_parquet_new.name, type:string, comment:null), FieldSchema(name:test_parquet_new.age, type:int, comment:null)], properties:null) INFO : Completed compiling command(queryId=hive_20190420132929_9097c88f-f390-4fa2-891c-569b03fe106d); Time taken: 0.11 seconds INFO : Executing command(queryId=hive_20190420132929_9097c88f-f390-4fa2-891c-569b03fe106d): select * from test_parquet_new INFO : Completed executing command(queryId=hive_20190420132929_9097c88f-f390-4fa2-891c-569b03fe106d); Time taken: 0.001 seconds INFO : OK +------------------------+-----------------------+--+ | test_parquet_new.name | test_parquet_new.age | +------------------------+-----------------------+--+ | Hadoop | 3 | | Oh | 1 | | a | 1 | | an | 1 | | as | 2 | | be | 1 | | can | 1 | | elephant | 1 | | fellow | 1 | | is | 3 | | what | 1 | | yellow | 2 | +------------------------+-----------------------+--+ 12 rows selected (0.788 seconds)
读写部分原理引用了:Hadoop权威指南第3章。
parquet使用:https://github.com/apache/parquet-mr/