分布式文件系统HDFS
1. Hadoop简介
1.1 Hadoop历史
-
Hadoop作者Doug Cutting
-
Apache Lucene是一个文本搜索系统库
-
Apache Nutch作为前者的一部分,主要包括web爬虫、全文检索
-
2003年“谷歌分布式文件系统GFS”论文,2004年开源版本NDFS
-
2004年“谷歌MapReduce”论文,2005年Nutch开源版MapReduce
1.2 Hadoop架构
- Hadoop由三个模块组成:分布式存储HDFS、分布式计算MapReduce、资源调度引擎Yarn
2. HDFS是什么
-
HDFS是Hadoop中的一个子模块,如果把hadoop看做为一个操作系统,那么HDFS则是操作系统中的文件系统
- File System文件系统:操作系统中负责管理和存储文件信息的软件;具体地说,它负责为用户创建文件,存入、读出、修改、转储、删除文件等
-
HDFS (全称Hadoop Distributed File System),即hadoop的分布式文件系统。当数据集大小超出一台计算机的存储能力时,就有必要将它拆分成若干部分,然后分散到不同的计算机中存储。管理网络中跨多台计算机存储的文件系统称之为分布式文件系统(distributed filesystem)
2.1 HDFS的优缺点
2.1.1 优点:
- 适合存储大文件,能用来存储管理PB级的数据
- 处理非结构化数据
- 流式的访问数据,一次写入、多次读写
- 运行于廉价的商用机器集群上,成本低
- 高容错:故障时能继续运行且不让用户察觉到明显的中断
- 可扩展
2.1.2 缺点
- 不适合处理低延迟数据访问
- HDFS是为了处理大型数据集分析任务的,主要是为达到高的数据吞吐量而设计的
- 对于低延时的访问需求,HBase是更好的选择
- 无法高效存储大量的小文件
- 小文件会给Hadoop的扩展性和性能带来严重问题
- 利用SequenceFile、MapFile等方式归档小文件
- 不支持多用户写入及任意修改文件
- 文件有一个写入者,并且只能执行追加操作
- 不支持多个用户对同一文件的写操作,以及在文件任意位置进行修改
3. HDFS初探
3.1 HDFS命令
-
HDFS命令与linux 命令的相似性
-
参考我的另一篇博客《HDFS命令总结》
3.2 WEB UI界面
- 访问HDFS的web界面,浏览器访问
node01:50070 # node01为主机名,需要在hosts文件中添加主机名到IP的映射
3.3 HDFS编程
-
HDFS java API编程
-
如何查看官方API文档
向HDFS写数据代码:
package com.zsc.hdfs;
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 java.io.*;
import java.net.URI;
/**
* 将本地文件系统的文件通过java-API写入到HDFS文件
*/
public class CopyFileFromLocal {
public static void main(String[] args){
//本地磁盘路径
String source="D:/test.txt";
//HDFS路径 端口8020
String destination="hdfs://node01:8020/test.txt";
InputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(source));
//HDFS读写的配置文件
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(destination),conf);
//调用Filesystem的create方法返回的是FSDataOutputStream对象
//该对象不允许在文件中定位,因为HDFS只允许一个已打开的文件顺序写入或追加
OutputStream out = fs.create(new Path(destination));
IOUtils.copyBytes(in, out, 4096, true);
} catch (FileNotFoundException e) {
System.out.println("exception");
e.printStackTrace();
} catch (IOException e) {
System.out.println("exception1");
e.printStackTrace();
}
}
}
从HDFS读数据:
package com.zsc.hdfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
/**
* 从HDFS读取文件到本地
* 打包运行jar包 hadoop jar com.zsc-1.0-SNAPSHOT.jar com.zsc.hdfs.ReadFileFromHDFS
*/
public class ReadFileFromHDFS {
public static void main(String[] args) {
try {
//源文件
String srcFile = "hdfs://node01:8020/test.txt";
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(srcFile),conf);
FSDataInputStream hdfsInStream = fs.open(new Path(srcFile));
//本地文件
BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream("D:/test01.txt"));
IOUtils.copyBytes(hdfsInStream, outputStream, 4096, true);
} catch (IOException e) {
e.printStackTrace();
}
}
}
注:HDFS API编程时,要学会查看API文档
4. 核心概念block
4.1 数据块block
4.1.1 HDFS block块
-
向HDFS上传文件,是按照128M为单位,切分成一个个block,分散的存储在集群的不同数据节点datanode上
-
HDFS中一个44M大小的块会不会占据128M的空间?小于一个块大小的文件不会占据整个块的空间
4.2 block副本
如上图这样存储文件会不会存在问题?datanode只存储一个文件的一份block,block1存在在datanode1上,当datanode1因为某些原因挂掉的时候,这个文件将不再完整,所以必须引入副本机制。
-
因为HDFS是用普通的商用服务器搭建起来的,所以有节点出问题的可能性
-
那么如果每个block只有一份的话,当block所在的节点宕机后,此block将无法访问,进而导致文件无法完整读取
-
为保正数据的可用及容错,HDFS设计成每个block共有三份,即三个副本
-
如何设置副本数?
- hdfs-site.xml中的replication = 3
<property> <name>dfs.replication</name> <value>3</value> </property>
4.3 机架存储策略
-
实际机房中,会有多个机架,每个机架上存在多个服务器。
-
第一块副本:在本机器的HDFS目录下存储Block的第一个副本。
-
第二块副本:在不同Rack(机架,称为r1)的某个DataNode(称为dn2)上存储Block的第二个副本。
-
第三块:在dn2所在机架r1下,找一台其它的datanode节点,存储Block的第三个副本。
-
更多副本:随机节点
4.4 block的一些操作
-
设置文件副本数
-
作用:数据分块存储和副本的存放,是保证可靠性和高性能的关键
-
方式一:使用命令设置文件副本数;动态生效,不需要重启hadoop集群
hadoop fs -setrep -R 4 /path
-
方式二:修改配置文件hdfs-site.xml,需要重启hadoop集群才能生效
<property> <name>dfs.replication</name> <value>4</value> </property>
-
-
HDFS提供了fsck命令,用于检查HDFS上文件和目录的健康状态、获取文件的block信息和位置信息
[hadoop@node01 ~]$ hdfs fsck
- 查看文件中损坏的块
[hadoop@node01 ~]$ hdfs fsck /hadoop.txt -list-corruptfileblocks
- 查看文件的块基本信息
hdfs fsck /hadoop.txt -files -blocks -locations
- 删除损坏的文件
[hadoop@node01 ~]$ hdfs fsck /hadoop.txt -delete
5. HDFS架构
- 大多数分布式框架都是主从架
- HDFS也是主从架构Master|Slave或称为管理节点|工作节点
- NameNode为管理结点,DataNode为工作结点
5.1 NameNode
5.1.1 文件系统
-
file system文件系统:操作系统中负责管理和存储文件信息的软件;具体地说,它负责为用户提供创建文件,存入、读取、修改、转储、删除文件等功能
-
元数据:
- 关于文件或目录的描述信息,如文件所在路径、文件名称、文件类型等等,这些信息称为文件的元数据metadata
-
命名空间:
- 文件系统中,为了便于管理存储介质上的,给每个目录、目录中的文件、子目录都起了名字,这样形成的层级结构,称之为命名空间
- 同一个目录中,不能有同名的文件或目录
- 这样通过目录+文件名称的方式能够唯一的定位一个文件
5.1.2 HDFS-NameNode
-
HDFS本质上也是文件系统filesystem,所以它也有元数据metadata
-
元数据metadata保存在NameNode内存中
-
NameNode作用:
- HDFS的主节点,负责管理文件系统的命名空间,HDFS的元数据存储在NameNode节点的内存中
- 负责响应客户端对文件的读写请求
-
HDFS元数据:
- 文件目录树、所有的文件(目录)名称、文件属性(生成时间、副本、权限)、每个文件的块列表、每个block块所在的datanode列表
-
每个文件、目录、block占用大概150Byte字节的元数据,所以HDFS适合存储大文件,不适合存储小文件
-
HDFS元数据信息以两种形式保存:
- 编辑日志edits log
- 元数据镜像文件fsimage
- edits log:HDFS编辑日志文件 ,保存客户端对HDFS的所有更改记录,如增、删、重命名文件(目录),这些操作会修改HDFS目录树;NameNode会在编辑日志edit日志中记录下来;
- fsimage:HDFS元数据镜像文件 ,即将namenode内存中的数据落入磁盘生成的文件;保存了文件系统目录树信息以及文件、块、datanode的映射关系
说明:
①hdfs-site.xml中属性dfs.namenode.edits.dir的值决定;用于namenode保存edits.log文件
②hdfs-site.xml中属性dfs.namenode.name.dir的值决定;用于namenode保存fsimage文件
5.2 DataNode
- DataNode数据节点的作用:
- 存储block以及block元数据到datanode本地磁盘,此处的元数据包括数据块的长度、块数据的校验和、时间戳
5.3 SeconddaryNameNode
为什么要引入SecondaryNameNode?为什么元数据存储在NameNode在内存中?这样做有什么问题?如何解决?
-
HDFS编辑日志文件 editlog:在NameNode节点中的编辑日志editlog中,记录下来客户端对HDFS的所有更改的记录,如增、删、重命名文件(目录);一旦系统出故障,可以从editlog进行恢复,但editlog日志大小会随着时间变在越来越大,导致系统重启根据日志恢复的时候会越来越长。为了避免这种情况,引入检查点机制checkpoint,命名空间镜像fsimage就是HDFS元数据的持久性检查点,即将内存中的元数据落磁盘生成的文件;
-
此时,namenode如果重启,可以将磁盘中的fsimage文件读入内容,将元数据恢复到某一个检查点,然后再执行检查点之后记录的编辑日志,最后完全恢复元数据。
-
但是随着时间的推移,editlog记录的日志依然会变多,那么当namenode重启,恢复元数据过程中,会花越来越长的时间执行editlog中的每一个日志;而在namenode元数据恢复期间,HDFS不可用。为了解决此问题,引入secondarynamenode辅助namenode,用来合并fsimage及editlog
-
SecondaryNameNode定期做checkpoint检查点操作
- 创建检查点checkpoint的两大条件:
- SecondaryNameNode每隔1小时创建一个检查点
- Secondary NameNode每1分钟检查一次,从上一检查点开始,edits日志文件中是否已包括100万个事务,如果是,也会创建检查点
- Secondary NameNode首先请求原NameNode进行edits的滚动,这样新的编辑操作就能够进入新的文件中
- Secondary NameNode通过HTTP GET方式读取原NameNode中的fsimage及edits
- Secondary NameNode读取fsimage到内存中,然后执行edits中的每个操作,并创建一个新的统一的fsimage文件
- Secondary NameNode通过HTTP PUT方式将新的fsimage发送到原NameNode
- 原NameNode用新的fsimage替换旧的fsimage,同时系统会更新fsimage文件到记录检查点的时间。
- 这个过程结束后,NameNode就有了最新的fsimage文件和更小的edits文件
- 创建检查点checkpoint的两大条件:
-
SecondaryNameNode一般部署在另外一台节点上
- 因为它需要占用大量的CPU时间
- 并需要与namenode一样多的内存,来执行合并操作
-
如何查看edits日志文件
hdfs oev -i edits_0000000000000000256-0000000000000000363 -o /home/hadoop/edit1.xml
-
如何查看fsimage文件
hdfs oiv -p XML -i fsimage_0000000000000092691 -o fsimage.xml
-
checkpoint相关属性
属性 | 值 | 解释 |
---|---|---|
dfs.namenode.checkpoint.period | 3600秒(即1小时) | The number of seconds between two periodic checkpoints. |
dfs.namenode.checkpoint.txns | 1000000 | The Secondary NameNode or CheckpointNode will create a checkpoint of the namespace every ‘dfs.namenode.checkpoint.txns’ transactions, regardless of whether ‘dfs.namenode.checkpoint.period’ has expired. |
dfs.namenode.checkpoint.check.period | 60(1分钟) | The SecondaryNameNode and CheckpointNode will poll the NameNode every ‘dfs.namenode.checkpoint.check.period’ seconds to query the number of uncheckpointed transactions. |
5.4 心跳机制
工作原理:
- NameNode启动的时候,会开一个ipc server
- DataNode启动后向NameNode注册,每隔3秒钟向NameNode发送一个“心跳heartbeat”
- 心跳返回结果带有NameNode给该DataNode的命令,如复制块数据到另一DataNode,或删除某个数据块
- 如果超过10分钟NameNode没有收到某个DataNode 的心跳,则认为该DataNode节点不可用
- DataNode周期性(6小时)的向NameNode上报当前DataNode上的块状态报告BlockReport;块状态报告包含了一个该 Datanode上所有数据块的列表
心跳的作用:
-
通过周期心跳,NameNode可以向DataNode返回指令
-
可以判断DataNode是否在线
-
通过BlockReport,NameNode能够知道各DataNode的存储情况,如磁盘利用率、块列表;跟负载均衡有关
-
hadoop集群刚开始启动时,99.9%的block没有达到最小副本数(dfs.namenode.replication.min默认值为1),集群处于安全模式
心跳相关配置
- hdfs-default.xml
- 心跳间隔
属性 | 值 | 解释 |
---|---|---|
dfs.heartbeat.interval | 3 | Determines datanode heartbeat interval in seconds. |
- block report
More Actions属性 | 值 | 解释 |
---|---|---|
dfs.blockreport.intervalMsec | 21600000 (6小时) | Determines block reporting interval in milliseconds. |
5.5 负载均衡
-
什么原因会有可能造成不均衡?
- 机器与机器之间磁盘利用率不平衡是HDFS集群非常容易出现的情况
- 尤其是在DataNode节点出现故障或在现有的集群上增添新的DataNode的时候
-
为什么需要均衡?
- 提升集群存储资源利用率
- 从存储与计算两方面提高集群性能
-
如何手动负载均衡?
$HADOOP_HOME/sbin/start-balancer.sh -t 5% # 磁盘利用率最高的节点若比最少的节点,大于5%,触发均衡
6. HDFS读写流程
6.1 数据写流程
6.1.1 写数据详细流程
- 创建文件:
- HDFS客户端向HDFS写数据,先调用DistributedFileSystem.create()方法,在HDFS创建新的空文件
- RPC(ClientProtocol.create())远程过程调用NameNode(NameNodeRpcServer)的create(),首先在HDFS目录树指定路径添加新文件
- 然后将创建新文件的操作记录在editslog中
- NameNode.create方法执行完后,DistributedFileSystem.create()返回FSDataOutputStream,它本质是封装了一个DFSOutputStream对象
- 建立数据流管道:
- 客户端调用DFSOutputStream.write()写数据
- DFSOutputStream调用ClientProtocol.addBlock(),首先向NameNode申请一个空的数据块
- addBlock()返回LocatedBlock对象,对象包含当前数据块的所有datanode的位置信息
- 根据位置信息,建立数据流管道
- 向数据流管道pipeline中写当前块的数据:
- 客户端向流管道中写数据,先将数据写入一个检验块chunk中,大小512Byte,写满后,计算chunk的检验和checksum值(4Byte)
- 然后将chunk数据本身加上checksum,形成一个带checksum值的chunk(516Byte)
- 保存到一个更大一些的结构packet数据包中,packet为64kB大小
- packet写满后,先被写入一个dataQueue队列中
- packet被从队列中取出,向pipeline中写入,先写入datanode1,再从datanoe1传到datanode2,再从datanode2传到datanode3中
- 一个packet数据取完后,后被放入到ackQueue中等待pipeline关于该packet的ack的反馈
- 每个packet都会有ack确认包,逆pipeline(dn3 -> dn2 -> dn1)传回输出流
- 若packet的ack是SUCCESS成功的,则从ackQueue中,将packet删除;否则,将packet从ackQueue中取出,重新放入dataQueue,重新发送
- 如果当前块写完后,文件还有其它块要写,那么再调用addBlock方法(流程同上)
- 文件最后一个block块数据写完后,会再发送一个空的packet,表示当前block写完了,然后关闭pipeline
- 所有块写完,close()关闭流
- ClientProtocol.complete()通知namenode当前文件所有块写完了
6.1.2 容错
- 在写的过程中,pipeline中的datanode出现故障(如网络不通),输出流如何恢复
- 输出流中ackQueue缓存的所有packet会被重新加入dataQueue
- 输出流调用ClientProtocol.updateBlockForPipeline(),为block申请一个新的时间戳,namenode会记录新时间戳
- 确保故障datanode即使恢复,但由于其上的block时间戳与namenode记录的新的时间戳不一致,故障datanode上的block进而被删除
- 故障的datanode从pipeline中删除
- 输出流调用ClientProtocol.getAdditionalDatanode()通知namenode分配新的datanode到数据流pipeline中,并使用新的时间戳建立pipeline
- 新添加到pipeline中的datanode,目前还没有存储这个新的block,HDFS客户端通过DataTransferProtocol通知pipeline中的一个datanode复制这个block到新的datanode中
- pipeline重建后,输出流调用ClientProtocol.updatePipeline(),更新namenode中的元数据
- 故障恢复完毕,完成后续的写入流程
6.2 数据读流程
6.2.1 读文件详细流程
- 1、client端读取HDFS文件,client调用文件系统对象DistributedFileSystem的open方法
- 2、返回FSDataInputStream对象(对DFSInputStream的包装)
- 3、构造DFSInputStream对象时,调用namenode的getBlockLocations方法,获得file的开始若干block(如blk1, blk2, blk3, blk4)的存储datanode(以下简称dn)列表;针对每个block的dn列表,会根据网络拓扑做排序,离client近的排在前;
- 4、调用DFSInputStream的read方法,先读取blk1的数据,与client最近的datanode建立连接,读取数据
- 5、读取完后,关闭与dn建立的流
- 6、读取下一个block,如blk2的数据(重复步骤4、5、6)
- 7、这一批block读取完后,再读取下一批block的数据(重复3、4、5、6、7)
- 8、完成文件数据读取后,调用FSDataInputStream的close方法
6.2.2 容错
- 情况一:读取block过程中,client与datanode通信中断
- client与存储此block的第二个datandoe建立连接,读取数据
- 记录此有问题的datanode,不会再从它上读取数据
- 情况二:client读取block,发现block数据有问题
- client读取block数据时,同时会读取到block的校验和,若client针对读取过来的block数据,计算检验和,其值与读取过来的校验和不一样,说明block数据损坏
- client从存储此block副本的其它datanode上读取block数据(也会计算校验和)
- 同时,client会告知namenode此情况;
7. Hadoop HA高可用
7.1 HDFS高可用原理
- 对于HDFS ,NN存储元数据在内存中,并负责管理文件系统的命名空间和客户端对HDFS的读写请求。但是,如果只存在一个NN,一旦发生“单点故障”,会使整个系统失效。
- 虽然有个SNN,但是它并不是NN的热备份
- 因为SNN无法提供“热备份”功能,在NN故障时,无法立即切换到SNN对外提供服务,即HDFS处于停服状态。
- HDFS2.x采用了HA(High Availability高可用)架构。
- 在HA集群中,可设置两个NN,一个处于“活跃(Active)”状态,另一个处于“待命(Standby)”状态。
- 由zookeeper确保一主一备(讲zookeeper时具体展开)
- 处于Active状态的NN负责响应所有客户端的请求,处于Standby状态的NN作为热备份节点,保证与active的NN的元数据同步
- Active节点发生故障时,zookeeper集群会发现此情况,通知Standby节点立即切换到活跃状态对外提供服务
- 确保集群一直处于可用状态
- 如何热备份元数据:
- Standby NN是Active NN的“热备份”,因此Active NN的状态信息必须实时同步到StandbyNN。
- 可借助一个共享存储系统来实现状态同步,如NFS(NetworkFile System)、QJM(Quorum Journal Manager)或者Zookeeper。
- Active NN将更新数据写入到共享存储系统,Standby NN一直监听该系统,一旦发现有新的数据写入,就立即从公共存储系统中读取这些数据并加载到Standby NN自己内存中,从而保证元数据与Active NN状态一致。
- 块报告:
- NN保存了数据块到实际存储位置的映射信息,为了实现故障时的快速切换,必须保证StandbyNN中也包含最新的块映射信息
- 因此需要给所有DN配置Active和Standby两个NN的地址,把块的位置和心跳信息同时发送到两个NN上。
8. Hadoop联邦
8.1 为什么需要联邦
- 虽然HDFS HA解决了“单点故障”问题,但HDFS在扩展性、整体性能和隔离性方面仍有问题
- 系统扩展性方面,元数据存储在NN内存中,受限于内存上限(每个文件、目录、block占用约150字节)
- 整体性能方面,吞吐量受单个NN的影响
- 隔离性方面,一个程序可能会影响其他程序的运行,如果一个程序消耗过多资源会导致其他程序无法顺利运行
- HDFS HA本质上还是单名称节点
8.2 联邦
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SIqMpjD1-1586327853497)(assets/Image201909041239.png)]
- HDFS联邦可以解决以上三个问题
- HDFS联邦中,设计了多个命名空间;每个命名空间有一个NN或一主一备两个NN,使得HDFS的命名服务能够水平扩展
- 这些NN分别进行各自命名空间namespace和块的管理,相互独立,不需要彼此协调
- 每个DN要向集群中所有的NN注册,并周期性的向所有NN发送心跳信息和块信息,报告自己的状态
- HDFS联邦每个相互独立的NN对应一个独立的命名空间
- 每一个命名空间管理属于自己的一组块,这些属于同一命名空间的块对应一个“块池”的概念。
- 每个DN会为所有块池提供块的存储,块池中的各个块实际上是存储在不同DN中的
9. 文件压缩
9.1 压缩算法
-
文件压缩好处:
- 减少数据所占用的磁盘空间
- 加快数据在磁盘、网络上的IO
-
常用压缩格式
压缩格式 UNIX工具 算 法 文件扩展名 可分割 DEFLATE 无 DEFLATE .deflate No gzip gzip DEFLATE .gz No zip zip DEFLATE .zip YES bzip bzip2 bzip2 .bz2 YES LZO lzop LZO .lzo No Snappy 无 Snappy .snappy No -
Hadoop的压缩实现类;均实现CompressionCodec接口
压缩格式 对应的编码/解码器 DEFLATE org.apache.hadoop.io.compress.DefaultCodec gzip org.apache.hadoop.io.compress.GzipCodec bzip2 org.apache.hadoop.io.compress.BZip2Codec LZO com.hadoop.compression.lzo.LzopCodec Snappy org.apache.hadoop.io.compress.SnappyCodec -
查看集群是否支持本地压缩(所有节点都要确认)
[hadoop@node01 ~]$ hadoop checknative
9.2 编程实践
- 编程:上传压缩过的文件到HDFS
- 对CopyFileFromLocal代码做修改,将文件压缩后,再上传到HDFS
package com.zsc.compress;
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.io.compress.BZip2Codec;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.util.ReflectionUtils;
import java.io.*;
import java.net.URI;
/**
*
* 将本地文件系统的文件通过java-API写入到HDFS文件,并且写入时使用压缩
*/
public class CopyFileFromLocal {
/**
*
* @param args 两个参数 C:\test.txt hdfs://node01:8020/copyFromLocal/test.bz2
* @throws ClassNotFoundException
*/
public static void main(String[] args) throws ClassNotFoundException {
//HDFS读写的配置文件
Configuration conf = new Configuration();
//压缩类
BZip2Codec codec = new BZip2Codec();
codec.setConf(conf);
String source = args[0];
String destination=args[1]
InputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(source));
FileSystem fs = FileSystem.get(URI.create(destination),conf);
//调用Filesystem的create方法返回的是FSDataOutputStream对象
OutputStream out = fs.create(new Path(destination));
//对输出流的数据压缩
CompressionOutputStream compressedOut = codec.createOutputStream(out);
//流拷贝
IOUtils.copyBytes(in, compressedOut, 4096, true);
} catch (FileNotFoundException e) {
System.out.println("exception");
e.printStackTrace();
} catch (IOException e) {
System.out.println("exception1");
e.printStackTrace();
}
}
}
- 参考阅读
- 《Hadoop权威指南》 5.2章节 压缩
- HDFS文件压缩
10. 小文件治理
10.1 HDFS为何不适合存储小文件
- NameNode存储着文件系统的元数据,每个文件、目录、块大概有150字节的元数据;
- NN内存有限,因此HDFS存储文件数量的也有上限,如果小文件过多则会造成NN的压力过大
- 且HDFS能存储的数据总量也会变小
10.2 HAR文件方案
- 此方案本质需要启动mr程序,所以需要启动yarn
用法:hadoop archive -archiveName <NAME>.har -p <parent path> [-r <replication factor>]<src>* <dest>
# 创建archive文件;/testhar有两个子目录th1、th2;两个子目录中有若干文件
hadoop archive -archiveName test.har -p /testhar -r 3 th1 th2 /outhar # 原文件还存在,需手动删除
# 查看archive文件
hdfs dfs -ls -R har:///outhar/test.har
# 解压archive文件
# 方式一
hdfs dfs -cp har:///outhar/test.har/th1 hdfs:/unarchivef1 # 顺序解压
hadoop fs -ls /unarchivef1
# 方式二
hadoop distcp har:///outhar/test.har/th1 hdfs:/unarchivef2 # 并行解压,效率高,启动MR
10.3 Sequence Files方案
- SequenceFile文件,主要由一条条record记录组成;
- 具体结构(如上图):
- 一个SequenceFile首先有一个4字节的header(文件版本号)
- 接着是若干record记录
- 每个record是键值对形式的;键值类型是可序列化类型,如IntWritable、Text
- 记录间会随机的插入一些同步点sync marker,用于方便定位到记录边界
- SequenceFile文件可以作为小文件的存储容器;
- 每条record保存一个小文件的内容
- 小文件名作为当前record的键;
- 小文件的内容作为当前record的值;
- 如10000个100KB的小文件,可以编写程序将这些文件放到一个SequenceFile文件。
- 一个SequenceFile是可分割的,所以MapReduce可将文件切分成块,每一块独立操作。
- 不像HAR,SequenceFile支持压缩。记录的结构取决于是否启动压缩
- 支持两类压缩:
- 不压缩NONE,如上图
- 压缩RECORD,如上图
- 压缩BLOCK,如下图,①一次性压缩多条记录;②每一个新块Block开始处都需要插入同步点
- 在大多数情况下,以block(注意:指的是SequenceFile中的block)为单位进行压缩是最好的选择
- 因为一个block包含多条记录,利用record间的相似性进行压缩,压缩效率更高
- 把已有的数据转存为SequenceFile比较慢。比起先写小文件,再将小文件写入SequenceFile,一个更好的选择是直接将数据写入一个SequenceFile文件,省去小文件作为中间媒介.
- 支持两类压缩:
- 向SequenceFile写入数据
package com.zsc.sequencefile;
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.io.IntWritable;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.compress.BZip2Codec;
import java.io.IOException;
import java.net.URI;
public class SequenceFileWriteNewVersion {
//模拟数据源;数组中一个元素表示一个文件的内容
private static final String[] DATA = {
"The Apache Hadoop software library is a framework that allows for the distributed processing of large data sets across clusters of computers using simple programming models.",
"It is designed to scale up from single servers to thousands of machines, each offering local computation and storage.",
"Rather than rely on hardware to deliver high-availability, the library itself is designed to detect and handle failures at the application layer",
"o delivering a highly-available service on top of a cluster of computers, each of which may be prone to failures.",
"Hadoop Common: The common utilities that support the other Hadoop modules."
};
public static void main(String[] args) throws IOException {
//输出路径:要生成的SequenceFile文件名
String uri = "hdfs://node01:8020/writeSequenceFile";
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri), conf);
//向HDFS上的此SequenceFile文件写数据
Path path = new Path(uri);
//因为SequenceFile每个record是键值对的
//指定key类型
IntWritable key = new IntWritable(); //key数字 -> int -> IntWritable
//指定value类型
Text value = new Text();//value -> String -> Text
//创建向SequenceFile文件写入数据时的一些选项
//要写入的SequenceFile的路径
SequenceFile.Writer.Option pathOption = SequenceFile.Writer.file(path);
//record的key类型选项
SequenceFile.Writer.Option keyOption = SequenceFile.Writer.keyClass(IntWritable.class);
//record的value类型选项
SequenceFile.Writer.Option valueOption = SequenceFile.Writer.valueClass(Text.class);
//SequenceFile压缩方式:NONE | RECORD | BLOCK三选一
//方案一:RECORD、不指定压缩算法
// SequenceFile.Writer.Option compressOption = SequenceFile.Writer.compression(SequenceFile.CompressionType.RECORD);
// SequenceFile.Writer writer = SequenceFile.createWriter(conf, pathOption, keyOption, valueOption, compressOption);
//方案二:BLOCK、不指定压缩算法
// SequenceFile.Writer.Option compressOption = SequenceFile.Writer.compression(SequenceFile.CompressionType.BLOCK);
// SequenceFile.Writer writer = SequenceFile.createWriter(conf, pathOption, keyOption, valueOption, compressOption);
//方案三:使用BLOCK、压缩算法BZip2Codec;压缩耗时间
//再加压缩算法
BZip2Codec codec = new BZip2Codec();
codec.setConf(conf);
SequenceFile.Writer.Option compressAlgorithm = SequenceFile.Writer.compression(SequenceFile.CompressionType.RECORD, codec);
//创建写数据的Writer实例
SequenceFile.Writer writer = SequenceFile.createWriter(conf, pathOption, keyOption, valueOption, compressAlgorithm);
for (int i = 0; i < 100000; i++) {
//分别设置key、value值
key.set(100000 - i);
value.set(DATA[i % DATA.length]); //%取模 3 % 3 = 0;
System.out.printf("[%s]\t%s\t%s\n", writer.getLength(), key, value);
//在SequenceFile末尾追加内容
writer.append(key, value);
}
//关闭流
IOUtils.closeStream(writer);
}
}
- 命令查看SequenceFile内容
hadoop fs -text /writeSequenceFile
- 读取SequenceFile文件
package com.zsc.sequencefile;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.util.ReflectionUtils;
import java.io.IOException;
public class SequenceFileReadNewVersion {
public static void main(String[] args) throws IOException {
//要读的SequenceFile
String uri = "hdfs://node01:8020/writeSequenceFile";
Configuration conf = new Configuration();
Path path = new Path(uri);
//Reader对象
SequenceFile.Reader reader = null;
try {
//读取SequenceFile的Reader的路径选项
SequenceFile.Reader.Option pathOption = SequenceFile.Reader.file(path);
//实例化Reader对象
reader = new SequenceFile.Reader(conf, pathOption);
//根据反射,求出key类型对象
Writable key = (Writable)
ReflectionUtils.newInstance(reader.getKeyClass(), conf);
//根据反射,求出value类型对象
Writable value = (Writable)
ReflectionUtils.newInstance(reader.getValueClass(), conf);
long position = reader.getPosition();
System.out.println(position);
while (reader.next(key, value)) {
String syncSeen = reader.syncSeen() ? "*" : "";
System.out.printf("[%s%s]\t%s\t%s\n", position, syncSeen, key, value);
//移动到下一个record开头的位置
position = reader.getPosition(); // beginning of next record
}
} finally {
IOUtils.closeStream(reader);
}
}
}
11. 文件快照
11.1 什么是快照
- 快照比较常见的应用场景是数据备份,以防一些用户错误或灾难恢复
- 快照snapshots是HDFS文件系统的,只读的、某时间点的拷贝
- 可以针对某个目录,或者整个文件系统做快照
- 创建快照时,block块并不会被拷贝。快照文件中只是记录了block列表和文件大小,不会做任何数据拷贝
11.2 快照操作
-
允许快照
允许一个快照目录被创建。如果这个操作成功完成,这个目录就变成snapshottable
用法:hdfs dfsadmin -allowSnapshot
hdfs dfsadmin -allowSnapshot /wordcount
-
禁用快照
用法:hdfs dfsadmin -disallowSnapshot
hdfs dfsadmin -disallowSnapshot /wordcount
-
创建快照(snapshotDir必须是snapshottable)
用法:hdfs dfs -createSnapshot []
#注意:先将/wordcount目录变成允许快照的 hdfs dfs -createSnapshot /wordcount wcSnapshot
-
查看快照
hdfs dfs -ls /wordcount/.snapshot
-
重命名快照
这个操作需要拥有snapshottabl目录所有者权限
用法:hdfs dfs -renameSnapshot
hdfs dfs -renameSnapshot /wordcount wcSnapshot newWCSnapshot
-
用快照恢复误删除数据
HFDS的/wordcount目录,文件列表如下
误删除/wordcount/edit.xml文件
hadoop fs -rm /wordcount/edit.xml
恢复数据
hadoop fs -cp /wordcount/.snapshot/newWCSnapshot/edit.xml /wordcount
-
删除快照
这个操作需要拥有snapshottabl目录所有者权限
用法:hdfs dfs -deleteSnapshot
hdfs dfs -deleteSnapshot /wordcount newWCSnapshot