1 HDFS基本概念的思维导图
2 使用Java API操作HDFS
2.1 maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dhhy</groupId>
<artifactId>hadoopapp</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<encoding>UTF-8</encoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.7.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin </artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.dhhy.mr.wordcount.WordcountDriver</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>aliyunmaven</id>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
</repositories>
</project>
2.2 源码
package com.dhhy.hdfs;
/**
* Created by JayLai on 2019-06-14 21:41:09
*/
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.apache.hadoop.io.IOUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.net.URI;
/**
* 使用Java API 操作HDFS
* Created by JayLai on 2019/04/2019
*/
public class HDFSApp {
public static final String HDFS_PATH = "hdfs://localhost:8020";
FileSystem fileSystem = null;
Configuration configuration = null;
/**
* 新增文件夹
*/
@Test
public void mkdir() throws Exception{
fileSystem.mkdirs(new Path("/javaapi"));
}
/**
* 上传文件到HDFS
*
* @throws Exception
*/
@Test
public void copyFromLocalFile() throws Exception {
Path localPath = new Path("/opt/bigdata/data/hello.txt");
Path hdfsPath = new Path("/javaapi");
fileSystem.copyFromLocalFile(localPath, hdfsPath);
}
/**
* 创建文件
*/
@Test
public void create() throws Exception {
FSDataOutputStream output = fileSystem.create(new Path("/javaapi/test.txt"));
output.write("hello hadoop".getBytes());
output.flush();
output.close();
}
/**
* 查看某个目录下的所有文件
*/
@Test
public void listFiles() throws Exception {
FileStatus[] fileStatuses = fileSystem.listStatus(new Path("/"));
for(FileStatus fileStatus : fileStatuses) {
String isDir = fileStatus.isDirectory() ? "文件夹" : "文件";
short replication = fileStatus.getReplication();
long len = fileStatus.getLen();
String path = fileStatus.getPath().toString();
System.out.println(isDir + "\t" + replication + "\t" + len + "\t" + path);
}
}
/**
* 查看文件内容
*/
@Test
public void cat() throws Exception{
FSDataInputStream in = fileSystem.open(new Path("/javaapi/hello.txt"));
IOUtils.copyBytes(in, System.out, 1024);
in.close();
}
/**
* 下载HDFS文件
*/
@Test
public void copyToLocalFile() throws Exception {
Path localPath = new Path("/opt/bigdata/data/hello2.txt");
Path hdfsPath = new Path("/javaapi/hello.txt");
fileSystem.copyToLocalFile(hdfsPath, localPath);
}
/**
* 删除
*/
@Test
public void delete() throws Exception{
fileSystem.delete(new Path("/javaapi"), true);
}
/**
* 建立链接
* @throws Exception
*/
@Before
public void setUp() throws Exception {
System.out.println("HDFSApp.setUp");
configuration = new Configuration();
#DistributedFileSystem类的实例
fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration, "hadoop");
}
/**
* 关闭链接
* @throws Exception
*/
@After
public void tearDown() throws Exception {
configuration = null;
fileSystem = null;
System.out.println("HDFSApp.tearDown");
}
}
3 HDFS的读数据流程
1)客户端获取DistributedFileSystem类的一个实例
2)DistributedFileSystem通过RPC向NameNode请求下载文件,NameNode通过查询元数据,找到文件块所在的DataNode地址。
3)DistributedFileSystem类返回一个FSDataOutputStream对象给客户端以便读取数据。
4)客户端调用read()方法,与网络拓扑中距离最近的文件中第一个block所在DataNode建立连接。DataNode开始传输数据给客户端(从磁盘里面读取数据输入流,以Packet为单位来做校验),客户端以Packet为单位接收,先在本地缓存,然后写入目标文件。
5)读取结束后关闭越datanode的连接,寻找下一块最佳datanode
6)全部读取完毕,FSDataOutputStream调用close()方法。
4 HDFS的写数据流程
1)客户端获取DistributedFileSystem类的一个实例
2)DistributedFileSystem对NameNode创建一个RPC调用请求上传文件,NameNode检查目标文件是否已存在,父目录是否存在。检查通过后namenode就会为创建新文件记录一条记录。
3)DistributedFileSystem类返回一个FSDataOutputStream对象给客户端以便写数据。
4)客户端请求第一个 Block上传到哪几个DataNode服务器上。NameNode返回3个DataNode节点,分别为dn1、dn2、dn3。
5)客户端通过FSDataOutputStream模块请求dn1上传数据,dn1收到请求会继续调用dn2,然后dn2调用dn3,将这个通信管道建立完成。
6)dn1、dn2、dn3逐级应答客户端。
7)客户端开始往dn1上传第一个Block(先从磁盘读取数据放到一个本地内存缓存),以Packet为单位,dn1收到一个Packet就会传给dn2,dn2传给dn3;dn1每传一个packet会放入一个应答队列等待应答。
8)当一个Block传输完成之后,客户端再次请求NameNode上传第二个Block的服务器。(重复执行4-7步)。
5 HDFS主节点的工作机制
5.1 NameNode
namenode以内存形式存储集群的元数据信息。
5.2 Fsimage和Edits
为了保证元数据信息不丢失,NameNode将元数据备份到磁盘的fsimage(映像文件)。后续客户端发起事务操作时,把这个动作追加到Edits(编辑日志),然后才更新内存中的信息。这样做的好处是速度快和安全。如果只写在内存中修改元素据信息,发生断电则会造成数据丢失。如果所有元数据信息都写入到Fsimage,则速度下降。
5.3 辅助NameNode
如果发生断电,NameNode重启时先加载fsimage到内存中,再执行Edits中的操作来恢复元数据信息。系统运行一段时间后,Edits文件越来越庞大,重启NameNode后执行Edits中的操作时间越会越来越久。因此需要辅助NameNode 定时定量的把集群中的Edits转为 Fsimage 文件。
5.4 检查点触发条件
触发辅助NameNode合并Edits到Fsimages主要有2个条件,可以通过hdfs-default.xml进行修改。
5.4.1 时间
辅助namenode默认每隔一小时执行一次合并操作。
<property>
<name>dfs.namenode.checkpoint.period</name>
<value>3600</value>
</property>
5.4.2 edits的大小
edits中记载的事务达到100万个时,也会触发合并,检查edits中事务大小的频率是1分钟。
<property>
<name>dfs.namenode.checkpoint.txns</name>
<value>1000000</value>
<description>操作动作次数</description>
</property>
<property>
<name>dfs.namenode.checkpoint.check.period</nam>
<value>60</value>
<description> 1分钟检查一次操作次数</description>
</property >
5.5 安全模式
nanenode启动时会进入安全模式,此时客户端只能进行非事务性操作,如读取数据。只有整个文件系统中99.9%的快满足最小复本级别(默认为1)时,namenode才会在30后退出安全模式。
<property>
<name>dfs.namenode.replication.min</name>
<value>1</value>
</property>
5.6 创建检查点的流程
备注:红色的操作为辅助NameNode工作流程
黑色的操作为NameNode的工作流程
6 HDFS数据节点的工作机制
6.1 DataNode的工作流程
6.2 新增数据节点
HDFS集群运行运行时添加新的数据节点。直接启动DataNode,即可关联到集群
hadoop@ubuntu18:/opt/bigdata/hadoop-2.6.0-cdh5.9.3$ sbin/hadoop-daemon.sh start
datanode
hadoop@ubuntu18:/opt/bigdata/hadoop-2.6.0-cdh5.9.3$ sbin/yarn-daemon.sh start
6.3 掉线时间
DataNode超过10分钟+30秒(默认值)没有向NameNode汇报心跳信息,则会被NameNode判定为掉线。
掉线时间 = 2 * recheck-interval + 10 * interval,配置中dfs.namenode.heartbeat.recheck-interval单位是毫秒,dfs.heartbeat.interval单位是秒
<property>
<name>dfs.namenode.heartbeat.recheck-interval</name>
<value>300000</value>
</property>
<property>
<name>dfs.heartbeat.interval</name>
<value>3</value>
</property>
7 HDFS的高可用
7.1 单点故障(SPOF)
单一NameNode存在单点故障问题,因此需要通过两个NameNode消除单点故障。其中一个namenode处于活跃状态,负责集群中所有客户端操作;另外一个处于备用状态。当Aacive namenode发生故障,备份NameNode迅速切换为活跃状态,为客户端提供服务,保证HDFS高可用。
7.2 JournalNode共享存储
主备NameNode直接如何确保数据一致性?答案是通过JournalNode(简称JN)。
Active NameNode将编辑日志写入JN,而Standby NameNode只能读取JN中的数据,从而确保数据同步。
7.3 主备NameNode的切换
楼上提到主备节点的数据共享,但是还是没有解决主备NameNode的切换问题。
主流解决方案式通过Zookeeper实现主备切换,流程如下所示:
7.4 组件功能
1)ZKFailoverController
基于Zookeeper的故障转移控制器,负责控制主备NameNode的切换。ZKFailoverController会监测namenode的监控状态,当发现Active NameNode出现异常时会通过Zookeeper进行一次新的选举,完成Active和Standby的切换。
2)HealthMonitor
周期性调用NameNode的HAService RPC接口(monitorHealth和getServiceStatus),监控NameNode的健康状态并向ZKFailoverController反馈。
3)ActiveStandbyElector
接受ZKFC的选举请求,通过ZooKeeper自动完成主备选举,选举完成后回调ZKFailoverController的主备切换方法对NameNode进行Active和Standby状态的切换
4)JournalNode
存储元数据,实现主备NameNode数据同步,Active NameNode写入和Standby NameNode读取通过JN实现元数据同步。当主备切换过程中,从节点如果没有完全同步元数据信息,是不能对外提供服务的。
7.5 脑裂问题(split-brain)
正常情况下,一个Hadoop集群只能有一个Active NameNode,假如系统发生故障(比如网络问题等),导致Standby NameNode的状态也发生改变,成为Active NameNode。此时,两个主节点开始争抢共享资源,导致系统混乱,数据质量无法保证。
7.6 围栏机制(fencing)
Hadoop集群通过JN的 fencing机制,确保只有一个NameNode能写成功。具体流程如下:
1)每个NameNode改变状态的时候,向DataName发送自身的状态和一个序列号。
2) DataNode在运行过程中维护此序列号,当主备切换时,新的NameNode在返回DataNode心跳时会返回自己的Active状态和一个更大的序列号。DataName接收到这个返回是认为该NameNode为新的Active节点。
3) 如果旧的Aactive NameNode(比如发生Full GC导致长时间卡顿)恢复,返回给DataNode的心跳信息包含Active状态和原来的序列号,这时DataNode就会拒绝这个NameNode的命令。
8 HDFS小文件合并
8.1 小文件合并成一个大文件下载到本地
HDFS上的javaapi目录下面有n个小文件,合并后下载到本地,命名为local.txt
hadoop@ubuntu18:/opt/bigdata$ hadoop fs -ls /javaapi
Found 2 items
-rwxrwxrwx 3 hadoop supergroup 13 2020-06-15 00:55 /javaapi/hello.txt
-rw-r--r-- 3 hadoop supergroup 12 2020-07-15 21:30 /javaapi/test.txt
hadoop@ubuntu18:/opt/bigdata$ hadoop fs -getmerge /javaapi/*.txt ./local.txt
8.2 小文件合并成一个大文件后上传到HDFS
/**
* 合并小文件
* @throws Exception
*/
@Test
public void mergeFIle() throws Exception{
FSDataOutputStream outputStream = fileSystem.create(new Path("/javaapi/bigfile.txt"));
LocalFileSystem localFileSystem = FileSystem.getLocal(new Configuration());
FileStatus[] fileStatuses = localFileSystem.listStatus(new Path("/opt/bigdata/data/hdfs"));
//便历每个小文件
for (FileStatus fileStatus : fileStatuses) {
//获取每个小文件的输入流
FSDataInputStream inputStream = localFileSystem.open(fileStatus.getPath());
//将小文件的数据复制到大文件的输出流
IOUtils.copyBytes(inputStream, outputStream, 1024);
inputStream.close();
}
localFileSystem.close();
outputStream.close();
}
9 HDFS的联邦
HDFS的HA解决节点的高可用问题,但是没解决单个NameNode内存容量问题。
每个block会占用NameName约150b内存,随着文件数量的提高,NameNode迟早面临内存耗尽问题。
目前解决方案式通过联邦机制,横向拓展NameNode的个数。
优点是优点:
1)解决单个NameNode的内存不足问题
2)共享存储集群(共享DataNode),提高DataNode的利用率,
缺点:每个NameNode的元数据是隔离的,客户端不知道数据存放在哪个NameNode上,需要在上层做一层封装。
10 HDFS的故障处理
10.1 NameNode发生故障
SecondaryNameNode上也备份着元数据信息,可以将SecondaryNameNode中数据拷贝到NameNode存储数据的目录。具体流程如下:
1)拷贝SecondaryNameNode中数据到原NameNode存储数据目录
2)重新启动NameNode
hadoop@ubuntu18:/opt/bigdata/hadoop-2.6.0-cdh5.9.3$ pwd
/opt/bigdata/hadoop-2.6.0-cdh5.9.3
hadoop@ubuntu18:/opt/bigdata/hadoop-2.6.0-cdh5.9.3$ sbin/hadoop-daemon.sh start
11 参考文献
1)TomWhite,Hadoop权威指南 第4版. 2017, 清华大学出版
2)社尚硅谷频 http://www.atguigu.com/bigdata_video.shtml