本篇文章翻译了Hadoop系列下的 HDFS Architecture ,原文最初经过笔者翻译后大概有6000字,之后笔者对内容进行了精简化压缩,从而使笔者自己和其他读者们阅读本文时能够更加高效快速的完成对Hadoop的学习或复习。本文主要介绍了Hadoop的整体架构,包括但不限于节点概念、命名空间、数据容错机制、数据管理方式、简单的脚本命令和垃圾回收概念。
PS:笔者新手一枚,如果看出哪里存在问题,欢迎下方留言!
Hadoop Distributed File System(HDFS)是高容错、高吞吐量、用于处理海量数据的分布式文件系统。
HDFS一般由成百上千的机器组成,每个机器存储整个数据集的一部分数据,机器故障的快速发现与恢复是HDFS的核心目标。
HDFS对接口的核心目标是高吞吐量而非低延迟。
HDFS支持海量数据集合,一个集群一般能够支持千万以上数量级的文件。
HDFS应用需要对文件写一次读多次的接口模型,文件变更只支持尾部添加和截断。
HDFS的海量数据与一致性接口特点,使得迁移计算以适应文件内容要比迁移数据从而支持计算更加高效。
HDFS支持跨平台使用。
HDFS使用主从架构。一个HDFS集群由一个NameNode、一个主服务器(用于管理系统命名空间和控制客户端文件接口)、大量的DataNode(一般一个节点一个,用于管理该节点数据存储)。HDFS对外暴露了文件系统命名空间并允许在文件中存储用户数据。一个文件被分成一个或多个块,这些块存储在一组DataNode中。NameNode执行文件系统命名空间的打开关闭重命名等命令并记录着块和DataNode之间的映射。DataNode用于处理客户端的读写请求和块的相关操作。NameNode和DataNode一般运行在GNU/Linux操作系统上,HDFS使用Java语言开发的,因此NameNode和DataNode可以运行在任何支持Java的机器上,再加上Java语言的高度可移植性,使得HDFS可以发布在各种各样的机器上。一个HDFS集群中运行一个NameNode,其他机器每个运行一个(也可以多个,非常少见)DataNode。NameNode简化了系统的架构,只用于存储所有HDFS元数据,用户数据不会进入该节点。下图为HDFS架构图:
HDFS支持传统的分层文件管理,用户或者应用能够在目录下创建目录或者文件。文件系统命名空间和其他文件系统是相似的,支持创建、删除、移动和重命名文件。HDFS支持用户数量限制和访问权限控制,不支持软硬链接,用户可以自己实现软硬链接。NameNode控制该命名空间,命名空间任何变动几乎都要记录到NameNode中。应用可以在HDFS中对文件声明复制次数,这个次数叫做复制系数,会被记录到NameNode中。
HDFS将每个文件存储为一个或多个块,并为文件设置了块的大小和复制系数从而支持文件容错。一个文件所有的块(除了最后一个块)大小相同,后来支持了可变长度的块。复制系数在创建文件时赋值,后续可以更改。文件在任何时候只能有一个writer。NameNode负责块复制,它周期性收到每个数据节点的心跳和块报告,心跳表示数据节点的正常运作,块报告包含了这个DataNode的所有块。
副本存储方案对于HDFS的稳定性和性能至关重要。为了提升数据可靠性、灵活性和充分利用网络带宽,HDFS引入了机架感知的副本存储策略,该策略只是副本存储策略的第一步,为后续优化打下基础。大型HDFS集群一般运行于横跨许多支架的计算机集群中,一般情况下同一支架中两个节点数据传输快于不同支架。一种简单的方法是将副本存放在单独的机架上,从而防止丢失数据并提高带宽,但是增加了数据写入的负担。一般情况下,复制系数是3,HDFS存储策略是将第一份副本存储到本地机器或者同一机架下一个随机DataNode,另外两份副本存储到同一个远程机架的不同DataNode。NameNode不允许同一DataNode存储相同副本多次。在机架感知的策略基础上,后续支持了 存储类型和机架感知相结合的策略 ,简单来说就是在机架感知基础上判断DataNode是否支持该类型的文件,不支持则寻找下一个。
HDFS读取数据使用就近原则,首先寻找相同机架上是否存在副本,其次本地数据中心,最后远程数据中心。
启动时,NameNode进入安全模式,该模式下不会发生数据块复制,NameNode接收来自DataNode的心跳和块报告,每个块都有一个最小副本数量n,数据块在NameNode接受到该块n次后,认为这个数据块完成安全复制。当完成安全复制的数据块比例达到一个可配的百分比值并再过30s后,NameNode退出安全模式,最后判断是否仍然存在未达到最小复制次数的数据块,并对这些块进行复制操作。
NameNode使用名为EditLog的事务日志持续记录文件系统元数据的每一次改动(如创建文件、改变复制系数),使用名为FsImage的文件存储全部的文件系统命名空间(包括块到文件的映射关系和文件系统的相关属性),EditLog和FsImage都存储在NameNode本地文件系统中。NameNode在内存中保存着元数据和块映射的快照,当NameNode启动后或者某个配置项达到阈值时,会从磁盘中读取EditLog和FsImage,通过EditLog新的记录更新内存中的FsImage,再讲新版本的FsImage刷新到磁盘中,然后截断EditLog中已经处理的记录,这个过程就是一个检查点。检查点的目的是确保文件系统通过在内存中使用元数据的快照从而持续的观察元数据的变更并将快照信息存储到磁盘FsImage中。检查点通过下面两个配置参数出发,时间周期(dfs.namenode.checkpoint.period)和文件系统事务数量(dfs.namenode.checkpoint.txns),二者同时配置时,满足任意一个条件就会触发检查点。
所有的HDFS网络协议都是基于TCP/IP的,客户端建立一个到NameNode机器的可配置的TCP端口,用于二者之间的交互。DataNode使用DataNode协议和NameNode交互,RPC包装了客户端协议和DataNode协议,通过设计,NameNode不会发起RPC,只负责响应来自客户端或者DataNode的RPC请求。
HDFS的核心目标是即使在失败或者错误情况下依然能够保证数据可靠性,三种常见失败情况包括NameNode故障、DataNode故障和network partitions。
网络分区可能会导致部分DataNode市区和NameNode的连接,NameNode通过心跳包判断并将失去连接的DataNode标记为挂掉状态,于是所有注册到挂掉DataNode的数据都不可用了,可能会导致部分数据块的复制数量低于了原本配置的复制系数。NameNode不断地追踪哪些需要复制的块并在必要时候进行复制,触发条件包含多种情况:DataNode不可用、复制乱码、硬件磁盘故障或者认为增大负值系数。为了避免DataNode的状态不稳定导致的复制风暴,标记DataNode挂掉的超时时间设置比较长(默认10min),用户可以设置更短的时间间隔来标记DataNode为陈旧状态从而避免在对读写性能要求高的请求上使用这些陈旧节点。
HDFS架构兼容数据各种重新平衡方案,一种方案可以在某个DataNode的空闲空间小于某个阈值时将数据移动到另一个DataNode上;在某个特殊文件突然有高的读取需求时,一种方式是积极创建额外副本并且平衡集群中的其他数据。这些类型的平衡方案暂时还未实现(不太清楚现有方案是什么...)。
存储设备、网络或者软件的问题都可能导致从DataNode获取的数据发生乱码,HDFS客户端实现了对文件内容的校验,客户端在创建文件时,会计算文件中每个块的校验值并存储到命名空间,当客户端取回数据后会使用校验值对每个块进行校验,如果存在问题,客户端就会去另一个DataNode获取这个块的副本。
FsImage和EditLog是HDFS的核心数据结构,他们的错误会导致整个HDFS挂掉,因此,NameNode应该支持时刻维持FsImage和EditLog的多分复制文件,它们的任何改变所有文件应该同步更新。另一个选择是使用 shared storage on NFS 或者 distributed edit log 支持多个NameNode,官方推荐 distributed edit log 。
快照能够存储某一特殊时刻的数据副本,从而支持HDFS在发生错误时会滚到上一个稳定版本。
HDFS的应用场景是大的数据集下,且数据只需要写一次但是要读取一到多次并且支持流速读取数据。一般情况下一个块大小为128MB,因此一个文件被切割成128MB的大块,且每个快可能分布在不同的DataNode。
当客户端在复制系数是3的条件下写数据时,NameNode通过目标选择算法收到副本要写入的DataNode的集合,第1个DataNode开始一部分一部分的获取数据,把每个部分存储到本地并转发给第2个DataNode,第2个DataNode同样的把每个部分存储到本地并转发给第3个DataNode,第3个DataNode将数据存储到本地,这就是管道复制。
HDFS提供了多种访问方式,比如 FileSystem Java API 、 C language wrapper for this Java API 和 REST API ,而且还支持浏览器直接浏览。通过使用 NFS gateway ,客户端可以在本地文件系统上安装HDFS。
HDFS使用目录和文件的方式管理数据,并提供了叫做 FS shell 的命令行接口,下面有一些简单的命令:
DFSAdmin命令集合用于管理HDFS集群,这些命令只有集群管理员可以使用,下面有一些简单的命令:
正常的HDFS安装都会配置一个web服务,通过可配的TCP端口对外暴露命名空间,从而使得用户可以通过web浏览器查看文件内容。
如果垃圾回收配置打开,通过FS shell移除的文件不会立刻删除,而是会移动到一个垃圾文件专用的目录(/user/username/.Trash),类似回收站,只要文件还存在于那个目录下,则随时可以被回复。绝大多数最近删除的文件都被移动到了垃圾目录(/user/username/.Trash/Current),并且HDFS每个一段时间在这个目录下创建一个检查点用于删除已经过期的旧的检查点,详情见 expunge command of FS shell 。在垃圾目录中的文件过期后,NameNode会删除这个文件,文件删除会引起这个文件的所有块的空间空闲,需要注意的是在文件被删除之后和HDFS的可用空间变多之间会有一些时间延迟(个人认为是垃圾回收机制占用的时间)。下面是一些简单的理解删除文件的例子:
当文件复制系数减小时,NameNode会选择多余的需要删除的副本,在收到心跳包时将删除信息发送给DataNode。和上面一样,这个删除操作也是需要一些时间后,才能在集群上展现空闲空间的增加。
HDFS Architecture
最差的情况可以在远端写个socketserver, 然后你写个client连, 让server调hadoop命令
Java API读写HDFS
public class FSOptr {
/**
* @param args
*/
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Configuration cOnf= new Configuration();
makeDir(conf);
rename(conf);
delete(conf);
}
// 创建文件目录
private static void makeDir(Configuration conf) throws Exception {
FileSystem fs = FileSystem.get(conf);
Path dir = new Path("/user/hadoop/data/20140318");
boolean result = fs.mkdirs(dir);// 创建文件夹
System.out.println("make dir :" + result);
// 创建文件,并写入内容
Path dst = new Path("/user/hadoop/data/20140318/tmp");
byte[] buff = "hello,hadoop!".getBytes();
FSDataOutputStream outputStream = fs.create(dst);
outputStream.write(buff, 0, buff.length);
outputStream.close();
FileStatus files[] = fs.listStatus(dst);
for (FileStatus file : files) {
System.out.println(file.getPath());
}
fs.close();
}
// 重命名文件
private static void rename(Configuration conf) throws Exception {
FileSystem fs = FileSystem.get(conf);
Path oldName = new Path("/user/hadoop/data/20140318/1.txt");
Path newName = new Path("/user/hadoop/data/20140318/2.txt");
fs.rename(oldName, newName);
FileStatus files[] = fs.listStatus(new Path(
"/user/hadoop/data/20140318"));
for (FileStatus file : files) {
System.out.println(file.getPath());
}
fs.close();
}
// 删除文件
@SuppressWarnings("deprecation")
private static void delete(Configuration conf) throws Exception {
FileSystem fs = FileSystem.get(conf);
Path path = new Path("/user/hadoop/data/20140318");
if (fs.isDirectory(path)) {
FileStatus files[] = fs.listStatus(path);
for (FileStatus file : files) {
fs.delete(file.getPath());
}
} else {
fs.delete(path);
}
// 或者
fs.delete(path, true);
fs.close();
}
/**
* 下载,将hdfs文件下载到本地磁盘
*
* @param localSrc1
* 本地的文件地址,即文件的路径
* @param hdfsSrc1
* 存放在hdfs的文件地址
*/
public boolean sendFromHdfs(String hdfsSrc1, String localSrc1) {
Configuration cOnf= new Configuration();
FileSystem fs = null;
try {
fs = FileSystem.get(URI.create(hdfsSrc1), conf);
Path hdfs_path = new Path(hdfsSrc1);
Path local_path = new Path(localSrc1);
fs.copyToLocalFile(hdfs_path, local_path);
return true;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
* 上传,将本地文件copy到hdfs系统中
*
* @param localSrc
* 本地的文件地址,即文件的路径
* @param hdfsSrc
* 存放在hdfs的文件地址
*/
public boolean sendToHdfs1(String localSrc, String hdfsSrc) {
InputStream in;
try {
in = new BufferedInputStream(new FileInputStream(localSrc));
Configuration cOnf= new Configuration();// 得到配置对象
FileSystem fs; // 文件系统
try {
fs = FileSystem.get(URI.create(hdfsSrc), conf);
// 输出流,创建一个输出流
OutputStream out = fs.create(new Path(hdfsSrc),
new Progressable() {
// 重写progress方法
public void progress() {
// System.out.println("上传完一个设定缓存区大小容量的文件!");
}
});
// 连接两个流,形成通道,使输入流向输出流传输数据,
IOUtils.copyBytes(in, out, 10240, true); // in为输入流对象,out为输出流对象,4096为缓冲区大小,true为上传后关闭流
return true;
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return false;
}
/**
* 移动
*
* @param old_st原来存放的路径
* @param new_st移动到的路径
*/
public boolean moveFileName(String old_st, String new_st) {
try {
// 下载到服务器本地
boolean down_flag = sendFromHdfs(old_st, "/home/hadoop/文档/temp");
Configuration cOnf= new Configuration();
FileSystem fs = null;
// 删除源文件
try {
fs = FileSystem.get(URI.create(old_st), conf);
Path hdfs_path = new Path(old_st);
fs.delete(hdfs_path);
} catch (IOException e) {
e.printStackTrace();
}
// 从服务器本地传到新路径
new_st = new_st + old_st.substring(old_st.lastIndexOf("/"));
boolean uplod_flag = sendToHdfs1("/home/hadoop/文档/temp", new_st);
if (down_flag uplod_flag) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
// copy本地文件到hdfs
private static void CopyFromLocalFile(Configuration conf) throws Exception {
FileSystem fs = FileSystem.get(conf);
Path src = new Path("/home/hadoop/word.txt");
Path dst = new Path("/user/hadoop/data/");
fs.copyFromLocalFile(src, dst);
fs.close();
}
// 获取给定目录下的所有子目录以及子文件
private static void getAllChildFile(Configuration conf) throws Exception {
FileSystem fs = FileSystem.get(conf);
Path path = new Path("/user/hadoop");
getFile(path, fs);
}
private static void getFile(Path path, FileSystem fs)throws Exception {
FileStatus[] fileStatus = fs.listStatus(path);
for (int i = 0; i fileStatus.length; i++) {
if (fileStatus[i].isDir()) {
Path p = new Path(fileStatus[i].getPath().toString());
getFile(p, fs);
} else {
System.out.println(fileStatus[i].getPath().toString());
}
}
}
//判断文件是否存在
private static boolean isExist(Configuration conf,String path)throws Exception{
FileSystem fileSystem = FileSystem.get(conf);
return fileSystem.exists(new Path(path));
}
//获取hdfs集群所有主机结点数据
private static void getAllClusterNodeInfo(Configuration conf)throws Exception{
FileSystem fs = FileSystem.get(conf);
DistributedFileSystem hdfs = (DistributedFileSystem)fs;
DatanodeInfo[] dataNodeStats = hdfs.getDataNodeStats();
String[] names = new String[dataNodeStats.length];
System.out.println("list of all the nodes in HDFS cluster:"); //print info
for(int i=0; i dataNodeStats.length; i++){
names[i] = dataNodeStats[i].getHostName();
System.out.println(names[i]); //print info
}
}
//get the locations of a file in HDFS
private static void getFileLocation(Configuration conf)throws Exception{
FileSystem fs = FileSystem.get(conf);
Path f = new Path("/user/cluster/dfs.txt");
FileStatus filestatus = fs.getFileStatus(f);
BlockLocation[] blkLocatiOns= fs.getFileBlockLocations(filestatus,0,filestatus.getLen());
int blkCount = blkLocations.length;
for(int i=0; i blkCount; i++){
String[] hosts = blkLocations[i].getHosts();
//Do sth with the block hosts
System.out.println(hosts);
}
}
//get HDFS file last modification time
private static void getModificationTime(Configuration conf)throws Exception{
FileSystem fs = FileSystem.get(conf);
Path f = new Path("/user/cluster/dfs.txt");
FileStatus filestatus = fs.getFileStatus(f);
long modificatiOnTime= filestatus.getModificationTime(); // measured in milliseconds since the epoch
Date d = new Date(modificationTime);
System.out.println(d);
}
}
把程序打成jar包放到Linux上
转到目录下执行命令 hadoop jar mapreducer.jar /home/clq/export/java/count.jar hdfs://ubuntu:9000/out06/count/
上面一个是本地文件,一个是上传hdfs位置
成功后出现:打印出来,你所要打印的字符。
package com.clq.hdfs;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
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 {
//********************************
//把本地的一个文件拷贝到hdfs上
//********************************
public static void main(String[] args) throws IOException {
String localSrc = args[0];
String dst = args[1];
InputStream in = new BufferedInputStream(new FileInputStream(localSrc));
Configuration cOnf= new Configuration();
FileSystem fs = FileSystem.get(URI.create(dst), conf);
FSDataOutputStream out = fs.create(new Path(dst), new Progressable() {
@Override
public void progress() {
System.out.print(".");
}
});
IOUtils.copyBytes(in, out, conf, true);
}
}
可能出现异常:
Exception in thread "main" org.apache.hadoop.ipc.RemoteException: java.io.IOException: Cannot create /out06; already exists as a directory
at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFileInternal(FSNamesystem.java:1569)
at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.startFile(FSNamesystem.java:1527)
at org.apache.hadoop.hdfs.server.namenode.NameNode.create(NameNode.java:710)
at org.apache.hadoop.hdfs.server.namenode.NameNode.create(NameNode.java:689)
at sun.reflect.GeneratedMethodAccessor7.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:587)
at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:1432)
at org.apache.hadoop.ipc.Server$Handler$1.run(Server.java:1428)
at java.security.AccessController.doPrivileged(Native Method)
at javax.security.auth.Subject.doAs(Subject.java:415)
说明你这个路径在hdfs上已经存在,换一个即可。