参考资料:
HDFS(Hadoop Distributed File System)是Hadoop项目的核心子项目,是分布式计算中数据存储管理的基础,是基于流数据模式访问和处理超大文件的需求而开发的,可以运行于廉价的商用服务器上。它所具有的高容错、高可靠性、高可扩展性、高获得性、高吞吐率等特征为海量数据提供了不怕故障的存储,为超大数据集(Large Data Set)的应用处理带来了很多便利。
1、高容错性
2、适合批处理
3、适合大数据处理
4、流式文件访问
5、可构建在廉价机器上
1、低延时数据访问
2、小文件存储
3、并发写入、文件随机修改
HDFS 采用Master/Slave的架构来存储数据,这种架构主要由四个部分组成,分别为HDFS Client、NameNode、DataNode和Secondary NameNode。下面我们分别介绍这四个组成部分:
HDFS中的文件在物理上是分块存储(block),作为独立的存储单元,称为数据块,块的大小可以通过配置参数( dfs.blocksize)来规定,默认大小在hadoop2.x版本中是128M,老版本中是64M。
使用数据块的好处是:
HDFS的Block为什么这么大?
是为了最小化查找(seek)时间,控制定位文件与传输文件所用的时间比例。假设定位到Block所需的时间为10ms,磁盘传输速度为100M/s。如果要将定位到Block所用时间占传输时间的比例控制1%,则Block大小需要约100M。
但是如果Block设置过大,在MapReduce任务中,Map或者Reduce任务的个数如果小于集群机器数量,会使得作业运行效率很低。
HDFS的守护进程,它是一个主管、管理者。
HDFS的工作节点。NameNode 下达命令,DataNode 执行实际的操作。根据需要存储和检索数据块,并且定期向namenode发送他们所存储的块的列表。
Secondary NameNode并非 NameNode 的热备。当NameNode 挂掉的时候,它并不能马上替换 NameNode 并提供服务。
合并触发机制:1、超过3600s合并一次;2、edits文件大小超过64M
fsimage:元数据镜像文件(文件系统的目录树)
edits:元数据的操作日志(针对文件系统做的修改操作记录)
namenode内存中存储的是=fsimage+edits。
热备份:b是a的热备份,如果a坏掉。那么b马上运行代替a的工作。
冷备份:b是a的冷备份,如果a坏掉。那么b不能马上代替a工作。但是b上存储a的一些信息,减少a坏掉之后的损失。
HDFS的文件读取原理,主要包括以下几个步骤:
在读数据过程中,如果与Datanode的通信发生错误,DFSInputStream对象会尝试从下一个最佳节点读取数据,并且记住该失败节点, 后续Block的读取不会再连接该节点。
读取一个Block之后,DFSInputStram会进行检验和验证,如果Block损坏,尝试从其他节点读取数据,并且将损坏的block汇报给Namenode。
读取和写入的过程中,namenode在分配Datanode的时候,会考虑节点之间的距离。HDFS中,距离没有采用带宽来衡量,因为实际中很难准确度量两台机器之间的带宽。
Hadoop把机器之间的拓扑结构组织成树结构,并且用到达公共父节点所需跳转数之和作为距离。事实上这是一个距离矩阵的例子。
同一数据中心,同一机架,同一节点距离为0
同一数据中心,同一机架,不同节点距离为2
同一数据中心,不同机架,不同节点距离为4
不同数据中心,不同机架,不同节点距离为6
Hadoop集群的拓扑结构需要手动配置,如果没配置,Hadoop默认所有节点位于同一个数据中心的同一机架上。
HDFS的文件写入原理,主要包括以下几个步骤:
HDFS一致性:HDFS在写数据务必要保证数据的一致性与持久性,目前HDFS提供的两个保证数据一致性的方法:hsync()方法和hflush()方法。
使用hflush或hsync会导致吞吐量下降,因此设计应用时,需要在吞吐量以及数据的健壮性之间做权衡。
另外,文件写入过程中,当前正在写入的Block对其他Reader不可见。
namenode如何选择在哪个datanode 存储副本(replication)?这里需要对可靠性、写入带宽和读取带宽进行权衡。Hadoop对datanode存储副本有自己的副本策略,在其发展过程中一共有两个版本的副本策略,分别如下所示
这样选择很好地平衡了可靠性、读写性能
架构如下图
这样做的好处就是当NN内存受限时,能扩展内存,解决内存扩展问题,而且每个NN独立工作相互不受影响,比如其中一个NN挂掉啦,它不会影响其他NN提供服务,但我们需要注意的是,虽然有多个NN,分管不同的目录,但是对于特定的NN,依然存在单点故障,因为没有它没有热备,解决单点故障使用NameNode HA。
解决方案:
Active NN与Standby NN通过NFS实现共享数据,但如果Active NN与NFS之间或Standby NN与NFS之间,其中一处有网络故障的话,那就会造成数据同步问题。
架构如下图
Active NN、Standby NN有主备之分,NN Active是主的,NN Standby备用的。
集群启动之后,一个namenode是active状态,来处理client与datanode之间的请求,并把相应的日志文件写到本地中或JN(journalNode)中;
Active NN与Standby NN之间是通过一组JN共享数据(JN一般为奇数个,ZK一般也为奇数个),Active NN会把日志文件、镜像文件写到JN中去,只要JN中有一半写成功,那就表明Active NN向JN中写成功啦,Standby NN就开始从JN中读取数据,来实现与Active NN数据同步,这种方式支持容错,因为Standby NN在启动的时候,会加载镜像文件(fsimage)并周期性的从JN中获取日志文件来保持与Active NN同步。
为了实现Standby NN在Active NN挂掉之后,能迅速的再提供服务,需要DN不仅需要向Active NN汇报,同时还要向Standby NN汇报,这样就使得Standby NN能保存数据块在DN上的位置信息,因为在NameNode在启动过程中最费时工作,就是处理所有DN上的数据块的信息。
为了实现Active NN高热备,增加了FailoverController和ZK,FailoverController通过Heartbeat的方式与ZK通信,通过ZK来选举,一旦Active NN挂掉,就选取另一个FailoverController作为active状态,然后FailoverController通过rpc,让standby NN转变为Active NN。
FailoverController一方面监控NN的状态信息,一方面还向ZK定时发送心跳,使自己被选举。当自己被选为主(Active)的时候,就会通过rpc使相应NN转变Active状态。
这里有12个DN,有4个NN,NN-1与NN-2是主备关系,它们管理/share目录;NN-3与NN-4是主备关系,它们管理/user目录
启动HDFS
最初,你必须格式化配置HDFS文件系统,打开namenode(HDFS服务器), 然后执行以下命令。
$ hadoop namenode -format
格式化HDFS后,启动分布式文件系统。以下命令将启动namenode以及数据节点作为集群。
$ start-dfs.sh
你可以使用以下命令关闭HDFS。
$ stop-dfs.sh
hadoop fs -help
hadoop fs -ls /
hadoop fs -mkdir
hadoop fs -rm -r
hadoop fs -copyFromLocal
hadoop fs -put
hadoop fs -get
hadoop fs -cp
hadoop fs -mv
hadoop fs -moveFromLocal
hadoop fs -moveToLocal
hadoop fs -appendToFile
hadoop fs -getmerge /aaa/log.* ./log.sum
hadoop fs -cat
hadoop fs -tail
hadoop fs -text
hadoop fs -df -h
hadoop fs -du -s -h
hadoop fs -count
hadoop fs -setrep 3
package filesystem;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import org.apache.hadoop.fs.FsUrlStreamHandlerFactory;
import org.apache.hadoop.io.IOUtils;
public class URLCat {
static {
URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory());
}
public static void main(String[] args) throws MalformedURLException, IOException {
InputStream in = null;
String input = "hdfs://192.168.92.138:9000/user/test.txt";
try {
in = new URL(input).openStream();
IOUtils.copyBytes(in, System.out, 4096,false);
}finally {
IOUtils.closeStream(in);
}
}
}
package filesystem;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
public class FileSystemCat {
public static void main(String[] args) throws IOException {
String uri = "hdfs://192.168.92.136:9000/user/test.txt";
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri),conf);
InputStream in = null;
try {
in = fs.open(new Path(uri));
IOUtils.copyBytes(in, System.out, 1024,false);
}finally {
IOUtils.closeStream(in);
}
}
}
这里调用open()函数来获取文件的输入流,FileSystem的get()方法获取FileSystem实例。
package filesystem;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.util.Progressable;
public class FileCopyWithProgress {
public static void main(String[] args) throws Exception {
String localSrc = "E:\\share\\input\\2007_12_1.txt";
String dst = "hdfs://192.168.92.136:9000/user/logs/2008_10_2.txt";
InputStream in = new BufferedInputStream(new FileInputStream(localSrc));
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(dst),conf);
OutputStream out = fs.create(new Path(dst),new Progressable() {
public void progress() {
System.out.print("*");
}
});
IOUtils.copyBytes(in, out, 1024,true);
}
}
FileSystem的create()方法用于新建文件,返回FSDataOutputStream对象。 Progressable()用于传递回调窗口,可以用来把数据写入datanode的进度通知给应用。
package filesystem;
import java.net.URI;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
public class FileDelete {
public static void main(String[] args) throws Exception{
String uri = "hdfs://192.168.92.136:9000/user/1400.txt";
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri),conf);
fs.delete(new Path(uri));
}
}
使用delete()方法来永久性删除文件或目录。
FileSystem的其它一些方法:
public class HdfsClient {
FileSystem fs = null;
@Before
public void init() throws Exception {
// 构造一个配置参数对象,设置一个参数:我们要访问的hdfs的URI
// 从而FileSystem.get()方法就知道应该是去构造一个访问hdfs文件系统的客户端,以及hdfs的访问地址
// new Configuration();的时候,它就会去加载jar包中的hdfs-default.xml
// 然后再加载classpath下的hdfs-site.xml
Configuration conf = new Configuration();
conf.set("fs.defaultFS", "hdfs://hdp-node01:9000");
/**
* 参数优先级: 1、客户端代码中设置的值 2、classpath下的用户自定义配置文件 3、然后是服务器的默认配置
*/
conf.set("dfs.replication", "3");
// 获取一个hdfs的访问客户端,根据参数,这个实例应该是DistributedFileSystem的实例
// fs = FileSystem.get(conf);
// 如果这样去获取,那conf里面就可以不要配"fs.defaultFS"参数,而且,这个客户端的身份标识已经是hadoop用户
fs = FileSystem.get(new URI("hdfs://hdp-node01:9000"), conf, "hadoop");
}
/**
* 往hdfs上传文件
*
* @throws Exception
*/
@Test
public void testAddFileToHdfs() throws Exception {
// 要上传的文件所在的本地路径
Path src = new Path("g:/redis-recommend.zip");
// 要上传到hdfs的目标路径
Path dst = new Path("/aaa");
fs.copyFromLocalFile(src, dst);
fs.close();
}
/**
* 从hdfs中复制文件到本地文件系统
*
* @throws IOException
* @throws IllegalArgumentException
*/
@Test
public void testDownloadFileToLocal() throws IllegalArgumentException, IOException {
fs.copyToLocalFile(new Path("/jdk-7u65-linux-i586.tar.gz"), new Path("d:/"));
fs.close();
}
@Test
public void testMkdirAndDeleteAndRename() throws IllegalArgumentException, IOException {
// 创建目录
fs.mkdirs(new Path("/a1/b1/c1"));
// 删除文件夹 ,如果是非空文件夹,参数2必须给值true
fs.delete(new Path("/aaa"), true);
// 重命名文件或文件夹
fs.rename(new Path("/a1"), new Path("/a2"));
}
/**
* 查看目录信息,只显示文件
*
* @throws IOException
* @throws IllegalArgumentException
* @throws FileNotFoundException
*/
@Test
public void testListFiles() throws FileNotFoundException, IllegalArgumentException, IOException {
// 思考:为什么返回迭代器,而不是List之类的容器
RemoteIterator listFiles = fs.listFiles(new Path("/"), true);
while (listFiles.hasNext()) {
LocatedFileStatus fileStatus = listFiles.next();
System.out.println(fileStatus.getPath().getName());
System.out.println(fileStatus.getBlockSize());
System.out.println(fileStatus.getPermission());
System.out.println(fileStatus.getLen());
BlockLocation[] blockLocations = fileStatus.getBlockLocations();
for (BlockLocation bl : blockLocations) {
System.out.println("block-length:" + bl.getLength() + "--" + "block-offset:" + bl.getOffset());
String[] hosts = bl.getHosts();
for (String host : hosts) {
System.out.println(host);
}
}
System.out.println("--------------打印的分割线--------------");
}
}
/**
* 查看文件及文件夹信息
*
* @throws IOException
* @throws IllegalArgumentException
* @throws FileNotFoundException
*/
@Test
public void testListAll() throws FileNotFoundException, IllegalArgumentException, IOException {
FileStatus[] listStatus = fs.listStatus(new Path("/"));
String flag = "d-- ";
for (FileStatus fstatus : listStatus) {
if (fstatus.isFile()) flag = "f-- ";
System.out.println(flag + fstatus.getPath().getName());
}
}
}
/**
* 相对那些封装好的方法而言的更底层一些的操作方式
*
* 上层那些mapreduce spark等运算框架,去hdfs中获取数据的时候,就是调的这种底层的api
*
* @author
*/
public class StreamAccess {
FileSystem fs = null;
@Before
public void init() throws Exception {
Configuration conf = new Configuration();
fs = FileSystem.get(new URI("hdfs://hdp-node01:9000"), conf, "hadoop");
}
@Test
public void testDownLoadFileToLocal() throws IllegalArgumentException, IOException {
//先获取一个文件的输入流----针对hdfs上的
FSDataInputStream in = fs.open(new Path("/jdk-7u65-linux-i586.tar.gz"));
//再构造一个文件的输出流----针对本地的
FileOutputStream out = new FileOutputStream(new File("c:/jdk.tar.gz"));
//再将输入流中数据传输到输出流
IOUtils.copyBytes(in, out, 4096);
}
/**
* hdfs支持随机定位进行文件读取,而且可以方便地读取指定长度
*
* 用于上层分布式运算框架并发处理数据
*
* @throws IllegalArgumentException
* @throws IOException
*/
@Test
public void testRandomAccess() throws IllegalArgumentException, IOException {
//先获取一个文件的输入流----针对hdfs上的
FSDataInputStream in = fs.open(new Path("/iloveyou.txt"));
//可以将流的起始偏移量进行自定义
in.seek(22);
//再构造一个文件的输出流----针对本地的
FileOutputStream out = new FileOutputStream(new File("c:/iloveyou.line.2.txt"));
IOUtils.copyBytes(in, out, 19L, true);
}
/**
* 显示hdfs上文件的内容
*
* @throws IOException
* @throws IllegalArgumentException
*/
@Test
public void testCat() throws IllegalArgumentException, IOException {
FSDataInputStream in = fs.open(new Path("/iloveyou.txt"));
IOUtils.copyBytes(in, System.out, 1024);
}
}
Hadoop提供的distcp工具用于并行导入数据到Hadoop或者从Hadoop导出。一些例子:
hadoop distcp file1 file2 //可以作为fs -cp命令的高效替代
hadoop distcp dir1 dir2
hadoop distcp -update dir1 dir2 #update参数表示只同步被更新的文件,其他保持不变
distcp是底层使用MapReduce实现,只有map实现,没有reduce。在map中并行复制文件。 distcp尽可能在map之间平均分配文件。map的数量可以通过-m参数指定:
hadoop distcp -update -delete -p hdfs://master1:9000/foo hdfs://master2/foo
这样的操作常用于在两个集群之间复制数据,update参数表示只同步被更新过的数据,delete会删除目标目录中存在,但是源目录不存在的文件。p参数表示保留文件的全校、block大小、副本数量等属性。
如果两个集群的Hadoop版本不兼容,可以使用webhdfs协议:
hadoop distcp webhdfs://namenode1:50070/foo webhdfs://namenode2:50070/foo
在distcp工具中,如果我们指定map数量为1,不仅速度很慢,每个Block第一个副本将全部落到运行这个唯一map的节点上,直到磁盘溢出。因此使用distcp的时候,最好使用默认的map数量,即20。
HDFS在Block均匀分布在各个节点上的时候工作得最好,如果没有办法在作业中尽量保持集群平衡,例如为了限制map数量(以便其他节点可以被别的作业使用),那么可以使用balancer工具来调整集群的Block分布。