数据存放存放策略
HDFS采用了多副本的冗余存储,通常把一个Block的多个副本分别存储到不同的数据结点上,默认情况下HDFS默认的副本是3,也就是冗余因子为3,每一个block被保存到三个地方,一般伪分布式的冗余因子为1,因为只有一台datanode的机器,一般的存放策略就是,两份副本放在一个rack上,另一个放在不同的rack上。
HDFS体系结构
整个HDFS的体系结构如下:
读写指定用户
hdfs和linux文件系统类似也是有用户,用户组来管理的。一般来说,在shell命令中可以创建用户,hdfs也会根据linux用户创建用户。
在代码中,无论是读或者写,都可以指定用户。Java客户端会先读取JVM系统的变量“HADOOP_USER_NAME”来作为用户名。
System.setProperty("HADOOP_USER_NAME","root");
读数据过程
首先要了解客户端,DistributedFileSystem类 ,FSDataInputStream类和FIleSystem类
客户端有shell,web,API提供调用,其底层都是java程序。
- FIleSystem类是一个抽象基类,基本上所有的HDFS的操作都在这个类中,属于HDFS的API
- DistributedFileSystem类是在客户端实现的了FIleSystem,是hdfs客户端程序中的实现,并不属于hdfs的API,属于客户端程序的具体实现。
例如:
DistributedFileSystem extends FileSystem ...{
....
}
- FSDataInputStream,站在程序的角度看待输入输出,读取数据当然是通过文件系统输入进程序了(本质是输入到缓存),这个类中还包括DFSnputStream类,这个类是FSDataInputStream的一部分,主要负责HDFS内部的通信也就是NameNode和DataNode之间的通信,虽然途中没画出来但是也要知道,在这里客户端本身就在NameNode和DataNode结点上。
有点类似于这样
public FSDataInputStream open(....) {
return new DFSClient.DFSInputStream(
....
}
}
客户端读过程:
- DistributedFileSystem.open(…), DistributedFileSystem实现了FileSystem,也是调用了FileSystem的open()。(对应图中 1)
- 之后就是创建FSDataInputStream来和NameNode进行通信(本质是DFSInputStream,只不过FSDataInputStream用这个类做成了API),获取文件开始部分元数据信息,对其Block对应的DataNode的地址进行排序,返回给客户端。(对应图中 2)
- 输入流FSDataInputStream(本质上是DFSInputStream),客户端就开始读取,根据排序的结果按序建立连接,接连读取数据。(对应图中3,4) , 对部分数据读取完毕之后,关闭与各个NameNode的连接。
- 如果没有读完,再次找到文件的下一个数据块的NameNode的位置信息,如果期间之内,已经获取了元数据在缓存中,就不用寻找。(对应图中的5,6),读取完毕之后,关闭客户端与DataNode的连接,然后一直循环,直到所有数据块读取完毕。
- 最后读取完毕之后,关闭输入流(对应7)
如果与DataNode通信错误,那么就连接block存储的下一个DataNode。
自己用java实现读过程,一般需要自己添加jar包:
(1)”/usr/local/hadoop/share/hadoop/common”目录下的hadoop-common-2.x.jar和haoop-nfs-2.x.jar;
(2)/usr/local/hadoop/share/hadoop/common/lib”目录下的所有JAR包;
(3)“/usr/local/hadoop/share/hadoop/hdfs”目录下的haoop-hdfs-2.x.ar和haoop-hdfs-nfs-2.2.x.jar;
(4)“/usr/local/hadoop/share/hadoop/hdfs/lib”目录下的所有JAR包。
具体过程:
- 通过的配置类获取FileSystem操作对象
- open获取输入流对象
- 通过本地的输入流显示
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FSDataInputStream;
public class HDFSFileExist{
public static void main(String[] args) {
try {
Configuration conf = new Configuration();
conf.set("fs.defaultFS","hdfs://localhost:9000"); // 通过此类设置NameNode的值
FileSystem fs = FileSystem.get(conf); // 通过配置类实现FileSystem
Path file = new Path("test"); // HDFS中的user路径,也可以加默认的绝对路径。
FSDataInputStream getIt = fs.open(file); // 打开输入流,创建了输入流对象
BufferedReader d = new BufferedReader(new InputStreamReader(getIt));
String content = d.readLine(); //读取文件一行
System.out.println(content);
d.close(); //关闭文件
fs.close(); //关闭hdfs
} catch (Exception e) {
e.printStackTrace();
}
}
}
写数据过程
读过程涉及到的类和写过程涉及到的类类似,不同的是读写过程。
客户端写过程:
- FileSystem.create()创建出到HDFS的输出流,具体在客户端里,是DistributedFileSystem.create()创建出FSDataOutputStream,其中包含了DFSOutputStream是实际和NameNode通信的类,都和读取类似。(对应图中1)
- DistributedFileSystem通过的一种RPC 的技术远程调用NameNode这个进程,在文件系统命名空间中创建一个新文件,NameNode执行检查,看是否符合文件已经存在,权限检查等等,全部符合之后,添加元数据信息和相关的文件信息。(对应图中2)
- 获得输出流FSDataOutputStream对象之后(实际上是DFSOutputStream中的内部队列存放了块的队列),选择合适的DataNode,客户端开始write(),采用了流水线复制策略(对应图中3,4):
(1)输出流会向NameNode提出申请,申请要保存block的若干结点,这些结点之间就会打开一个数据通道
(2)通过输出流向第一个DataNode发送Block,当写入本地达到4KB的同时,申请第二个DataNode发送连接请求并把第一个DataNode
收到的4KB数据发送给第二DataNode,以此类推直到Block写完。 - 传送完毕之后,为了确认收到的block是准确的,收到block的DataNode要向发送者发送确认包,顺着数据通道逆流发送,直到客户端收到应答的时候,将该block从队列中移除(对应图中5)。
- 不断执行3-5过程,直到文件全部写完
- 客户端关闭输出流,然后就不会写入数据了,内部队列都收到确认之后,关闭RPC远程调用NameNode,通知NameNode写入成功。
自己使用java程序写入数据如下:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.Path;
public class HDFSFileExist {
public static void main(String[] args) {
try {
Configuration conf = new Configuration();
conf.set("fs.defaultFS","hdfs://localhost:9000");
FileSystem fs = FileSystem.get(conf); // 获取配置信息,创建FileSystem对象
byte[] buff = "This is write file operating".getBytes(); // 要写入的内容
String filename = "/user/hadoop/test1.txt"; //要写入的文件名
FSDataOutputStream os = fs.create(new Path(filename)); // FileSystem创建输出流对象
os.write(buff,0,buff.length);
System.out.println("Create:"+ filename);
os.close();
fs.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
经过运行发现,自己编写的java程序,可以重复用一个文件名,并且新写入的同名文件内容会替换掉上一次同名文件的内容,类似于替换。