前面,分析了org.apache.hadoop.fs.Filesystem抽象类,我们已经了解到,要实现一个最基本的文件系统都需要包含哪些要点。下面具体到某个实现Filesystem的具体实现类,基于该抽象类Filesystem派生的类的继承关系:
◦org.apache.hadoop.fs.FileSystem (implements java.io.Closeable) ◦org.apache.hadoop.fs.FilterFileSystem ◦org.apache.hadoop.fs.ChecksumFileSystem ◦org.apache.hadoop.fs.InMemoryFileSystem ◦org.apache.hadoop.fs.LocalFileSystem ◦org.apache.hadoop.fs.HarFileSystem ◦org.apache.hadoop.fs.RawLocalFileSystem
? extends FileSystem
下面分析FileSystem类的两个直接子类FilterFileSystem与RawLocalFileSystem的实现:
这里,首先分析FileSystem抽象类的子类org.apache.hadoop.fs.FilterFileSystem类,它是一个文件系统的实现类,该文件系统包含了一些其他的Filesystem文件系统,并以这些Filesystem文件系统作为基本的文件系统,可能用来转换数据或者提供附加功能。
该类主要是在其内部定义了一个Filesystem属性:
protected FileSystem fs;
其实现了基类Filesystem的全部方法,是通过fs直接调用Filesystem抽象类中的方法达到的,似乎并没有增加任何其它的功能,但是可以作为一个具有最基本功能的文件系统。
RawLocalFileSystem类是实现了FileSystem的API的一个原生本地文件系统,它的属性如下所示:
static final URI NAME = URI.create("file:///"); // 用"file:///"来标识该文件系统是一个本地文件系统 private Path workingDir; // 用户的当前工作目录
workingDir在该类的构造方法中,通过获取系统的属性“user.dir”属性初始化的:
public RawLocalFileSystem() { workingDir = new Path(System.getProperty("user.dir")).makeQualified(this); }
该类中包含了3个与文件读写相关的内部类,分别为:
TrackingFileInputStream类通过继承自基类FileSystem类的Statistics statistics属性,来跟踪文件系统中数据流动。
LocalFSFileInputStream类包装了一个FileInputStream流属性,通过构造方法构造一个TrackingFileInputStream来完成输入流的相关操作。
LocalFSFileOutputStream类包装了一个FileOutputStream流属性,以来FileOutputStream定义的操作来实现对文件系统中的输出流的操作。该类还实现了org.apache.hadoop.fs.Syncable接口,使得该文件输出流类能够实现并调用该接口定义的sync()方法,实现强制对基本设备的缓冲区执行同步操作。
还有一个内部类,是封装文件信息的实体类RawLocalFileStatus,实现了getGroup、setOwner与getOwner、setPermission与getPermission、execCommand等方法,其中set前缀的方法设置对应的信息都是通过执行Shell命令来实现的;而get前缀的方法获取对应的信息也是通过执行Shell命令,并调用一个私有方法loadPermissionInfo加载必要的信息。
另外,RawLocalFileSystem继承实现了FileSystem抽象类中基本的文件操作,例如文件创建、打开、删除等等。
? extends FilterFileSystem
下面分析FilterFileSystem类的两个直接子类ChecksumFileSystem与HarFileSystem的实现:
该类是一个基于校验和的文件系统的抽象类,它继承自FilterFileSystem类,它的特点就是在客户端为每一个原生文件(raw file)创建一个校验和文件,扩展名为“.crc”,用它可以校验原生文件的完整性。
1、增加的属性
这个基于校验和文件的文件系统,增加了如下两个属性:
private int bytesPerChecksum = 512; // 校验和文件的大小 private boolean verifyChecksum = true; // 是否验证校验和
其中,bytesPerChecksum默认值为512字节,你可以通过配置Hadoop的core-default.xml文件中的I/O属性io.bytes.per.checksum:
<property> <name>io.bytes.per.checksum</name> <value>512</value> <description>The number of bytes per checksum. Must not be larger than io.file.buffer.size.</description> </property>
每个校验和文件的大小不能比一个文件所对应的的缓存大,一个文件的缓存大小通过io.file.buffer.size属性可以在core-default.xml文件中进行配置。
verifyChecksum属性默认为true,表示基于ChecksumFileSystem文件系统的文件,在读写过程中,需要检查校验和文件,验证原生文件的完整性。
2、校验和文件
ChecksumFileSystem提供了与校验和文件相关的一些基本的操作:
(1)通过setConf方法,设置文件系统的校验和文件大小:
public void setConf(Configuration conf) { super.setConf(conf); if (conf != null) { bytesPerChecksum = conf.getInt("io.bytes.per.checksum", 512); } }
从基于core-default.xml配置文件的配置类Configuration的实例来获取,默认大小512字节。
(2)判断文件系统中的文件是否是校验和文件:
public static boolean isChecksumFile(Path file) { String name = file.getName(); return name.startsWith(".") && name.endsWith(".crc"); }
一个Path file文件的名称为“.crc”时,才被认为是校验和文件。
(3)获取文件系统校验和文件大小参数:
public int getBytesPerSum() { return bytesPerChecksum; }
(4)获取指定大小的文件对应的校验和文件大小:
public long getChecksumFileLength(Path file, long fileSize) { return getChecksumLength(fileSize, getBytesPerSum()); }
可以看一下,getChecksumLength方法是如何计算一个校验和文件的大小的:
public static long getChecksumLength(long size, int bytesPerSum) { // 这里给出一计算校验和文件大小的公式: // 校验和文件大小 = ((文件大小 + bytesPerSum + 1)/bytesPerSum)*4 + CHECKSUM_VERSION长度 + 4 return ((size + bytesPerSum - 1) / bytesPerSum) * 4 + CHECKSUM_VERSION.length + 4; }
(5) 计算校验和文件所分配的缓存的大小:
private int getSumBufferSize(int bytesPerSum, int bufferSize) { int defaultBufferSize = getConf().getInt("io.file.buffer.size", 4096); // 获取文件缓存大小,默认为4096 int proportionalBufferSize = bufferSize / bytesPerSum; // 计算每一个校验和文件应该分配的文件缓存大小 return Math.max(bytesPerSum, Math.max(proportionalBufferSize, defaultBufferSize)); }
校验和文件如果全部放入到文件缓存中,至少分配的缓存的大小,等于校验和文件的大小。但是,可能文件缓存bufferSize可能会足够大,从而在为校验和文件分配缓存的时候,可能会比一个校验和文件的大小要大。检验和文件需要加入到文件缓存中,然后系统从缓存中执行读写操作操作。
3、基本文件操作
实现的基本操作包括文件创建、打开、读写、拷贝、移动。最主要的是真正实现了文件在不同文件系统之间的拷贝操作,如下所示:
@Override public void copyFromLocalFile(boolean delSrc, Path src, Path dst) throws IOException { Configuration conf = getConf(); FileUtil.copy(getLocal(conf), src, this, dst, delSrc, conf); } @Override public void copyToLocalFile(boolean delSrc, Path src, Path dst) throws IOException { Configuration conf = getConf(); FileUtil.copy(this, src, getLocal(conf), dst, delSrc, conf); }
其中,文件工具类org.apache.hadoop.fs.FileUtil的copy方法是实现拷贝操作的核心方法,实现过程比较复杂,可以从此处开始一步步跟踪代码,了解拷贝的实现细节。
4、读写校验和文件
当读校验和文件时,是为了验证数据文件的完整性,在该文件系统实现类中,与检查校验和文件相关的类的继承层次关系如下所示:
◦java.io.InputStream ◦org.apache.hadoop.fs.FSInputStream(implements org.apache.hadoop.fs.Seekable, org.apache.hadoop.fs.PositionedReadable) ◦org.apache.hadoop.fs.FSInputChecker ◦org.apache.hadoop.fs.ChecksumFileSystem.ChecksumFSInputChecker
ChecksumFSInputChecker类对一个打开的文件(以FSInputStream输入流的形式),验证该文件与其对应的校验和文件是否匹配,做一个匹配性检查。
该类封装了ChecksumFileSystem、FSDataInputStream、FSDataInputStream三个类的对象,如下所示:
private ChecksumFileSystem fs; // 文件系统 private FSDataInputStream datas; // 原生文件的输入流对象 private FSDataInputStream sums; // 校验和文件的输入流对象
当一个原生文件需要被存储到指定的主机文件系统中,同时计算该文件对应的校验和信息,并将该校验和信息流式写入到对应的校验和文件中。与校验和文件创建与写操作相关的类的继承层次关系如下所示:
◦java.io.OutputStream ◦org.apache.hadoop.fs.FSOutputSummer ◦org.apache.hadoop.fs.ChecksumFileSystem.ChecksumFSOutputSummer
ChecksumFSOutputSummer类为一个校验和文件提供了一个FSDataOutputStream输出流对象,它为原生文件数据生成一个校验和,并流式写入到FSDataOutputStream sums输出流中。
在读写校验和文件的时候,都是将校验和文件数据放到文件缓冲区,通过流对象来执行读写操作。
HarFileSystem是Hadoop归档文件系统(Hadoop Archive Filesystem)的实现,该文件系统具有索引文件及其相关内容。用来标识归档文件系统的URI的形式如下所示:
har://underlyingfsscheme-host:port/archivepath
或者
har:///archivepath
首先看该文件系统的属性:
public static final int VERSION = 1; // HarFileSystem的版本号 private URI uri; // 标识HarFileSystem的URI private int version; // 等于版本号HarFileSystem.VERSION private URI underLyingURI; // HarFileSystem的基本URI private Path archivePath; // 归档的路径Path private Path masterIndex; // masterIndex文件 private Path archiveIndex; // 索引文件 private String harAuth; // HarFileSystem文件系统的URI授权部分字符串
该文件系统类其它文件系统实现类一样,实现了归档文件系统的中对文件的基本操作。
? extends ChecksumFileSystem
下面分析ChecksumFileSystem类的直接子类LocalFileSystem类的实现。
LocalFileSystem类实现了FileSystem的API,它是一个基于校验和的本地文件系统。
通过构造方法可以看到,它是以org.apache.hadoop.fs.RawLocalFileSystem为基本文件系统的,如下所示:
public LocalFileSystem() { this(new RawLocalFileSystem()); } public LocalFileSystem(FileSystem rawLocalFileSystem) { super(rawLocalFileSystem); rfs = rawLocalFileSystem; }
该类实现了ChecksumFileSystem类中定义但未实现的,用于向文件系统报告校验和文件出错的方法,同时把出错的校验和文件重命名后,移动到指定的目录(bad_files)中,在该目录中的文件是不能够被重新使用的,如下所示:
public boolean reportChecksumFailure(Path p, FSDataInputStream in, long inPos, FSDataInputStream sums, long sumsPos) { try { File f = ((RawLocalFileSystem)fs).pathToFile(p).getCanonicalFile(); // 得到Path的规范化抽象路径名称 String device = new DF(f, getConf()).getMount(); // 根据路径名称f查询:找到同一设备上可写的最顶层目录,这里使用了Unix系统的df命令获取磁盘使用情况统计数据,确定一个有足够空间可以进行写入操作的目录 File parent = f.getParentFile(); File dir = null; while (parent!=null && parent.canWrite() && parent.toString().startsWith(device)) { dir = parent; parent = parent.getParentFile(); } if (dir==null) { throw new IOException( "not able to find the highest writable parent dir"); } File badDir = new File(dir, "bad_files"); if (!badDir.mkdirs()) { if (!badDir.isDirectory()) { throw new IOException("Mkdirs failed to create " + badDir.toString()); } } String suffix = "." + rand.nextInt(); File badFile = new File(badDir, f.getName()+suffix); LOG.warn("Moving bad file " + f + " to " + badFile); in.close(); // 关闭文件输入流 f.renameTo(badFile); // rename it // 移除校验和文件 File checkFile = ((RawLocalFileSystem)fs).pathToFile(getChecksumFile(p)); checkFile.renameTo(new File(badDir, checkFile.getName()+suffix)); } catch (IOException e) { LOG.warn("Error moving bad file " + p + ": " + e); } return false; }