Hadoop 分布式文件系统 (The Hadoop Distributed Filesystem)


Hadoop 分布式文件系统 (The Hadoop Distributed Filesystem)


当一个数据集过度成长为超过一台物理机器的存储能力时,切分它以跨多台台机器分别存储就变得必要了。管理通过网络连接的多台机器存储的文件系统称为分布式文件系统(distributed filesystems)。因为它们是基于网络的,带来了网络编程所有的复杂性,因而使分布式文件系统比常规的磁盘文件系统更加复杂。例如,其中最大的挑战之一是使文件系统能够承受节点故障失败而不必担心数据丢失之苦。

Hadoop 自带了一个分布式文件系统称为 HDFS, 即 Hadoop Distributed Filesystem. HDFS 是 Hadoop 的旗舰文件系统,但 Hadoop 实际上有一个通用的文件系统抽象,因此,会看到 Hadoop 如何与其他存储系统集成(such as the local filesystem and Amazon S3).


*
*
*

1. HDFS 的设计 (The Design of HDFS)
--------------------------------------------------------------------------------------------------------------------------
HDFS 设计用于通过流式数据访问模式,存储超大文件的文件系统,运行于商用硬件的集群上。下面考察详细内容:

    
    □ 超大文件 (Very large files)
    ----------------------------------------------------------------------------------------------------------------------
    此处的超大文件是指文件具有几百 megabytes, gigabytes, 或者 terabytes 的大小(in size)。目前运行的 Hadoop 集群上有存储PB
    级的数据。

    
    □ 流式数据访问 (Streaming data access)
    ----------------------------------------------------------------------------------------------------------------------
    HDFS 是建立在最高效的数据处理模式思想之上的,即一次写入,多次读取的模式。一个数据集通常生成或从数据源复制到系统中,然
    后在这个数据集上执行各种分析操作。每次分析会涉及数据集中很大的比例,因此读取整个数据集的时间比读取第一条记录的延迟时间
    更重要。

    
    □ 商用硬件 (Commodity hardware)
    ----------------------------------------------------------------------------------------------------------------------
    Hadoop 不要求昂贵的,高可靠性的硬件。它设计运行在商用级硬件的集群上,在大型集群上,集群节点故障的机会比较高。HDFS设计
    在面对这样的故障时可以继续工作而用户感觉不到明显的中断。

    
    
那些不适合使用 HDFS 的应用也值得考察。即便将来可能会改变,但目前这些领域不适合使用 HDFS:


    □ 低延迟数据访问 (Low-latency data access)
    ----------------------------------------------------------------------------------------------------------------------
    要求低延迟访问数据的应用,几十毫秒范围内的,不适合在 HDFS 上工作。 HDFS 是为高数据吞吐量应用优化的,这可能以提高时间延
    迟为代价。目前,HBase 是对于低延迟数据访问较好的选择。


    □ 大量的小文件 (Lots of small files)
    ----------------------------------------------------------------------------------------------------------------------
    由于 namenode 将文件系统元数据存储在内存中,因此文件系统中能存储的文件数量受限于 namenode 的内存容量。根据经验法则,每
    一个文件、目录以及数据块大约占用 150 bytes。因此,例如,如果有一百万个文件,每个文件使用一个数据块,应该需要至少 300MB
    的内存。虽然存储几百万个文件是可行的,数十亿个就超出了当前硬件的能力了。
    
    
    □ 多个写入者,任意的文件修改 (Multiple writers, arbitrary file modifications)
    -----------------------------------------------------------------------------------------------------------------------
    HDFS 中的文件可能只由一个 writer 写入。写操作总是发生在文件末尾,只能以追加的方式。它不支持多个 writer 的操作,也不支持
    在文件任意位置修改。将来可能支持,但很可能相对低效。

 

*
*
*

2 HDFS 的概念 (HDFS Concepts)
--------------------------------------------------------------------------------------------------------------------------


■ 数据块 (Blocks)
--------------------------------------------------------------------------------------------------------------------------
磁盘有一个块大小(a block size)的概念, 是磁盘能够进行读写的最小数量数据。构建于单个磁盘上的文件系统通过数据块来处理数据,数
据大小是块大小的整数倍。
    
磁盘块的大小通常为 512 bytes,通常,这对文件系统用户来说是透明的,用户只关心对一个文件进行读写操作,不管它多长。然而有些执
行文件系统维护的工具,例如 df 和 fsck, 则在文件系统块级别上进行操作。
    
HDFS 也有一个块的概念,但它是一个非常大的单元——默认为 128 MB. 类似单个磁盘上的文件系统, HDFS 中的文件被切分成块大小的数据
块(block-sized chunks),作为独立的单元存储。不像单一磁盘上的文件系统,在 HDFS 中,一个比一个单一数据块小得多的文件不会占用
一整块底层存储的空间。例如,一个 1 MB 的文件存储到一个 128 MB 块大小的数据块中,占用 1 MB大小的磁盘空间,而不是128MB.
 
为一个分布式文件系统建立一个块抽象可以带来多个好处。第一个好处最明显:一个文件可以比网络上任何单一的磁盘更大。一个文件的所
有数据块并不要求存储到同一个磁盘上,因此,它们可以利用集群上任何磁盘进行存储。
    
第二,使用一个抽象数据块而不是一个文件作为存储单元简化了存储子系统。简化是所有系统的目标,对于故障繁多的分布式系统尤为重要。
存储子系统处理数据块。简化了存储管理(因为块的大小是固定的,很容易计算有多少数据可以存储到一个给定的磁盘上),这也消除了对
元数据的顾虑(因为块只存储数据,文件的元数据,例如许权限可信息等不需要存储到块中,因此,可以分开由另一个系统处理元数据)。

第三,块非常适合于复制来提供容错能力和可用性。为了确保块损坏、磁盘或机器故障数据不会丢失,每个块被复制到几个物理上分离的
机器上(通常为 3 个)。如果一个块变不可用,能以对客户端透明的方式从另一个位置读取一份数据拷贝。由于损坏或机器故障,一个数
据块不再可用时,可以从另一个位置复制一份到其他可用的机器上以恢复复制因子到正常水平(bring the replication factor back to the
normal level). 类似地,有些应用程序可能选择设置一个更高的复制因子以使一个公用文件在集群上分散读取负载。

类似磁盘文件系统, HDFS 的 fsck command 能理解数据块,例如,运行:
    
    [devalone@nutch hadoop]$ hdfs fsck / -files -blocks
    
    ...
    Status: HEALTHY
     Total size:    39936369 B
     Total dirs:    61
     Total files:   215
     Total symlinks:                0
     Total blocks (validated):      215 (avg. block size 185750 B)
     Minimally replicated blocks:   215 (100.0 %)
     Over-replicated blocks:        0 (0.0 %)
     Under-replicated blocks:       0 (0.0 %)
     Mis-replicated blocks:         0 (0.0 %)
     Default replication factor:    1
     Average block replication:     1.0
     Corrupt blocks:                0
     Missing replicas:              0 (0.0 %)
     Number of data-nodes:          1
     Number of racks:               1
    FSCK ended at Thu Jul 26 17:03:51 CST 2018 in 51 milliseconds

        
会列出文件系统中组成每一个文件的数据块信息。
    
    
    
■ 名称节点和数据节点 (Namenodes and Datanodes)
--------------------------------------------------------------------------------------------------------------------------
HDFS 集群有两种类型的节点以 "管理者 —— 工作者" 的模式(master−worker pattern) 工作: 一个 namenode (the master) 和一定数量的
datanodes (workers)。
 
namenode 管理文件系统名称空间(filesystem namespace). 它维护文件系统树(filesystem tree) 和树中所有的文件和目录的元数据信息。
这些信息以两个文件的形式持久化存储在本地磁盘:名称空间镜像(namespace image)和编辑日志(edit log)。namenode 也知道一个给定
文件所有的块所在的 datanode, 然而它并不持久化存储数据块的位置信息,因为这些信息会在系统启动时由 datanode 重新建立。
    
客户端访问文件系统代表用户与 namenode 和 datanodes 通信。客户端面向文件系统接口类似于 Portable Operating System Interface
(POSIX),因此用户代码不需要知道 namenode 和 datanodes 来操作。
    
数据节点(datanodes) 是文件系统的工作者。它们按客户端或 namenode 指令存储和获取数据块,并定期向 namenode 报告它们存储的数据
块列表。

没有 namenode, 文件系统无法工作。事实上,如果运行 namenode 的机器彻底损坏,文件系统上的所有文件都会丢失,因为没有办法知道
如何从 datanodes 上的块重建文件。正是由于这个原因,使 namenode 具有从失败中恢复的能力非常重要,并且 Hadoop 为此提供了两种
机制。

    
第一种方法是备份组成文件系统元数据持久性状态的文件。 Hadoop 可以配置成让 namenode 把它的持久化状态写入到多个文件系统中,这
种写入是同步的和原子性的。通常的配置选择是写入本地磁盘的同时也写入一个远程 NFS 挂载。

第二种方法是运行一个辅助 namenode (a secondary namenode),尽管从名字上看它不是作为一个 namenode。它主要的角色是定期使用编辑
日志(edit log)合并名称空间镜像(namenode image)来防止编辑日志变得过大。secondary namenode通常运行在一个物理上分离的机器上。
因其需要与 namenode 同样多的 CPU 和内存容量来执行合并操作。它持有一个合并的名称空间镜像,可用于 namenode 发生故障时。然而,
secondary namenode 的状态滞后于主 namenode(primary namenode),因此在主 namenode 彻底失败时,数据丢失几乎是必然的。这种情况
下通常的做法是将 namenode 在 NFS 上的元数据文件复制到 secondary namenode,然后把它作为新的 primary namenode 运行。
    
    
■ 块缓存 (Block Caching)
--------------------------------------------------------------------------------------------------------------------------
通常情况下一个 datanode 从磁盘读取数据块,但对于频繁访问的文件,数据块可以显式缓存到 datanode 的内存中(以对外块缓存的方式
in an off-heap block cache)。默认的,一个块只缓存到一个 datanode 内存中,即便这个数量可以基于每文件配置(configurable on a
per-file basis)。作业调度器(Job schedulers)    ——— for MapReduce, Spark, and other frameworks, 可以利用缓存的数据块在块缓存的
datanode 上运行任务,以提升读取性能。为缓存联合使用一个小的查询表是一个不错的方法。
    
用户或应用程序通过把一个缓存指令(a cache directive)添加到一个缓存池(a cache pool)中指示 namenode 哪些文件要缓存以及缓存多长
时间。缓存池是一个为管理缓存权限和资源使用的系统管理分组。
    
    
■ HDFS 联邦 (HDFS Federation)
--------------------------------------------------------------------------------------------------------------------------
namenode 在内存中保存着文件系统中每个文件和块的引用关系,这就意味着在一个拥有大量文件的超大型集群上,内存成为集群扩展的限
制因素。 HDFS 联邦,由 2.x 发行版系列引入,允许添加 namenode 来扩展集群,每个 namenode 管理文件系统名称空间的一部分。例如,
一个 namenode 可以管理 /user 下的所有文件,而第二个 namenode 处理 /share 下的所有文件。
    
在联邦模式下,每个 namenode 管理一个名称空间卷(namespace volume), 由名称空间的元数据,和一个包含这个名称空间内文件的所有块
的块池(block pool)组成。名称空间卷是彼此独立的,意味着 namenode 互相不通信,此外,一个 namenode 发生故障不会影响由其他
namenode 管理的名称空间的可用性。然而块池(Block pool) 存储不分区,因此,datanode 要注册到集群中每一个 namenode 上,并且在
多个数据块池中存储数据块。
    
要访问一个联邦 HDFS 集群,客户端使用客户端侧的挂载表(client-side mount tables)映射文件路径到相应的 namenode 。这使用
ViewFileSystem 和 viewfs:// URIs 中进行配置来管理。

    
■ HDFS 高可用性 (HDFS High Availability)
--------------------------------------------------------------------------------------------------------------------------
联合使用复制 namenode 元数据到多个文件系统和 secondary namenode 创建检查点以防止数据丢失,但这无法提供文件系统的高可用性。
namenode 仍然是一个单点失效(a single point of failure ——- SPOF)的。如果它失效了,所有的客户端,包括 MapReduce 作业,都无法
读取,写入,或列出文件,因为 namenode 是元数据和 file-to-block 映射的唯一的存储之所。这样的事故期间,整个 Hadoop 系统无法
提供服务直到新的 namenode 上线。
    
在这样的情况下要从一个失效的 namenode 恢复,系统管理员用一个文件系统元数据的复本启动一个新的 primary namenode,并配置
datanodes 和客户端使用这个新的 namenode。这个新的 namenode 不能立刻为请求提供服务,直到它:
        
    ① 将其名称空间镜像载入内存
    ② 重放其编辑日志
    ③ 从 datanode 接收到足够的块报告以离开安全模式
        
在大型集群上,有大量的文件和数据块,一个 namenode 冷启动会花费 30分钟或更长的时间。
    
很长的恢复时间对例行维护一个问题。事实上,由于无法预料的 namenode 失效非常罕见,因此实践中,有计划的停机情况实际上更重要。

Hadoop 2 系列通过加入 HDFS 高可用性(high availability —— HA)的支持改进了上述问题。在这个实现中,一对 namenode 配置为
"活动-备用" 形式(in an active-standby configuration)。在活动的 namenode 发生故障失效时,备用 namenode 接管它的职责,在没有
明显中断的状态下继续为客户端请求服务。
    
要产生这样的效果,需要几个架构上的改变:
--------------------------------------------------------------------------------------------------------------------------
    □ namenode 必须使用高可用的共享存储来共享编辑日志。当一个备用 namenode 接管时,它读取这个共享的编辑日志直到末尾来同步
      它的状态为活动 namenode, 然后继续读取新的条目,因为这些是由活动 namenode 写入的。
    □ datanode 必须向这两个 namenode 发送块报告,因为块映射是存储在 namenode 内存上的,而不是在磁盘上。
    □ 客户端必须配置以处理 namenode 失效,使用一种对用户透明的机制
    □ secondary namenode 角色归入备用 namenode, 定期为活动 namenode 创建检查点。
--------------------------------------------------------------------------------------------------------------------------
对高可用的共享存储有两种选择:一种是 NFS filer, 或者quorum journal manager (QJM). QJM 是特定的 HDFS 实现,设计的唯一目的是
提供一个高可用的编辑日志,并且是大多数 HDFS 安装推荐的选择。QJM 运行为一组日志节点(runs as a group of journal nodes), 并且
每个编辑必须写入到这些日志节点中的大部分节点。典型地,有三个日志节点,因此系统能够容忍其中之一失效。这种安排与 ZooKeeper
的工作方式类似(QJM 实现并不使用 ZooKeeper)。
    
如果活动 namenode 失效了,备用 namenode 会很快接管工作(几十秒时间),因为它的内存中有可用的最新状态:最新的编辑日志条目和
最新的块映射。实际观察到的失效时间略长一点(大约 1 分钟左右),因为系统需要保守确定活动 namenode 确实失效了。
    
一个不太可能的情况是在活动 namenode 失效时,备用 namenode 处于停机状态,系统管理员仍然可以通过冷启动备用 namenode 。这并不
比非高可用(non-HA)更差,并且从一个操作的角度上讲这是一个进步,因为这个过程是一个构建到 Hadoop 中的标准操作过程。
    
        
    故障转移与围栏 (Failover and fencing)
    ---------------------------------------------------------------------------------------------------------------------
    从活动 namenode 到备用 namenode 的转换由系统内一个称为故障转移控制器(failover controller)的新的实体管理。有多种类型的
    故障救援控制器,但默认实现是使用 ZooKeeper 来确保只有一个 namenode 是活动的。每个 namenode 运行一个轻量的故障转移控制
    器进程,它的工作就是监视它的 namenode 失效(通过一个简单的心跳机制)并在 namenode 失效时触发一个故障转移。
    
    Failover 也可以由系统管理员手工发起,例如,在例行维护情景下。这称为平稳故障转移(graceful failover), 因为故障转移控制器
    可以安排两个 namenode 有序切换角色。
    
    然而,在非平稳故障转移情况下,无法确认失效的 namenode 是否已停止运行。例如,一个网速很慢的网络,或网络被分割能触发故障
    转移变换。即便之前的活动 namenode 仍然在运行并被认为仍然是活动 namenode。 HA 实现竭尽全力确保前一个活动 namenode 受到
    保护,防止做出任何毁坏并导致损坏的事情。这个方法被称为围栏(fencing) 。
    
    同一时刻 QJM 只允许一个 namenode 写入编辑日志,然而,前一个活动 namenode 为之前的客户端读取请求提供服务服务仍然是可能
    的,因此设置一个 SSH fencing 命令来杀掉 namenode 进程是个好主意。当使用 NFS filer 来共享编辑日志时要求使用更强大的围栏
    方法,因为它不可能在某一时刻只允许一个 namenode 写入(这也是为什么建议使用 QJM 的原因)。建立围栏机制的范围包括撤销
    namenode 访问共享存储目录的权限(通常使用供应商指定的 NFS 命令), 通过一个远程管理命令禁用它的网络端口。作为最后一种
    手段,先前的活动 namenode 可以通过技术上规避,称为 shoot the other node in the head —— STONITH, 该方法使用一种特殊的
    电力分布单元强制关闭主机。
    
    客户端的故障转移由客户端类库透明处理。最简单的实现是使用客户端侧配置(client-side configuration) 来控制故障转移。 HDFS
    URI 使用一个逻辑主机名映射到一对 namenode 地址(在配置文件中),客户端类库尝试每个 namenode 直到操作成功。

        
*
*
*

3 命令行接口 (The Command-Line Interface)
--------------------------------------------------------------------------------------------------------------------------
HDFS 上有很多接口,但命令行是其中最简单的,并且对很多开发者来说,是最熟悉的。本节通过命令行接口与 HDFS 交互。
使用的环境是 Hadoop 伪分布模式,运行在一台机器上。
    
        
■ 文件系统的基本操作 (Basic Filesystem Operations)
--------------------------------------------------------------------------------------------------------------------------
文件系统已经准备好可以用了,可以进行所有的常用文件系统操作,例如读取文件,创建目录,移动文件,删除数据,以及列出目录
内容。

输入:
[devalone@nutch hadoop-book]$ hadoop
Usage: hadoop [--config confdir] [COMMAND | CLASSNAME]
  CLASSNAME            run the class named CLASSNAME
 or
  where COMMAND is one of:
  fs                   run a generic filesystem user client
  version              print the version
  jar <jar>            run a jar file
                       note: please use "yarn jar" to launch
                             YARN applications, not this command.
  checknative [-a|-h]  check native hadoop and compression libraries availability
  distcp <srcurl> <desturl> copy file or directories recursively
  archive -archiveName NAME -p <parent path> <src>* <dest> create a hadoop archive
  classpath            prints the class path needed to get the
  credential           interact with credential providers
                       Hadoop jar and the required libraries
  daemonlog            get/set the log level for each daemon
  trace                view and modify Hadoop tracing settings

Most commands print help when invoked w/o parameters.

这是 hadoop shell 最基本的命令格式。

对于文件系统来说,针对的是 fs 命令。

输入:
[devalone@nutch hadoop]$ hadoop fs -help
Usage: hadoop fs [generic options]
        [-appendToFile <localsrc> ... <dst>]
        [-cat [-ignoreCrc] <src> ...]
        [-checksum <src> ...]
        [-chgrp [-R] GROUP PATH...]
        [-chmod [-R] <MODE[,MODE]... | OCTALMODE> PATH...]
        [-chown [-R] [OWNER][:[GROUP]] PATH...]
        [-copyFromLocal [-f] [-p] [-l] <localsrc> ... <dst>]
        [-copyToLocal [-p] [-ignoreCrc] [-crc] <src> ... <localdst>]
        [-count [-q] [-h] <path> ...]
        [-cp [-f] [-p | -p[topax]] <src> ... <dst>]
        [-createSnapshot <snapshotDir> [<snapshotName>]]
        [-deleteSnapshot <snapshotDir> <snapshotName>]
        [-df [-h] [<path> ...]]
        [-du [-s] [-h] <path> ...]
        [-expunge]
        [-find <path> ... <expression> ...]
        [-get [-p] [-ignoreCrc] [-crc] <src> ... <localdst>]
        [-getfacl [-R] <path>]
        [-getfattr [-R] {-n name | -d} [-e en] <path>]
        [-getmerge [-nl] <src> <localdst>]
        [-help [cmd ...]]
        [-ls [-d] [-h] [-R] [<path> ...]]
        [-mkdir [-p] <path> ...]
        [-moveFromLocal <localsrc> ... <dst>]
        [-moveToLocal <src> <localdst>]
        [-mv <src> ... <dst>]
        [-put [-f] [-p] [-l] <localsrc> ... <dst>]
        [-renameSnapshot <snapshotDir> <oldName> <newName>]
        [-rm [-f] [-r|-R] [-skipTrash] <src> ...]
        [-rmdir [--ignore-fail-on-non-empty] <dir> ...]
        [-setfacl [-R] [{-b|-k} {-m|-x <acl_spec>} <path>]|[--set <acl_spec> <path>]]
        [-setfattr {-n name [-v value] | -x name} <path>]
        [-setrep [-R] [-w] <rep> <path> ...]
        [-stat [format] <path> ...]
        [-tail [-f] <file>]
        [-test -[defsz] <path>]
        [-text [-ignoreCrc] <src> ...]
        [-touchz <path> ...]
        [-truncate [-w] <length> <path> ...]
        [-usage [cmd ...]]
...

    
获取文件系统 fs 命令的详细帮助信息。文件系统子命令以 -xxx 的形式给出,大部分命令比较熟悉,类似于 Linux 系统文件管理命令。

从本地文件系统复制一个文件到 HDFS:
    
    [devalone@nutch hadoop-book]$ hadoop fs -copyFromLocal input/docs/quangle.txt \
                                    hdfs://localhost:9000/user/devalone/quangle.txt
    
这条命令调用 Hadoop 文件系统的 shell command : fs ,fs 支持许多子命令(subcommand), 本例中使用 -copyFromLocal。本地文件
quangle.txt 被复制到运行于 localhost 的 HDFS 实例上的 /user/devlone/quangle.txt 文件。

实际上,可以忽略 URI 的 scheme 和主机名并选用默认值,hdfs://localhost 已在 core-site.xml 文件中指定了:

    [devalone@nutch hadoop-book]$ hadoop fs -copyFromLocal input/docs/quangle.txt /user/devalone/quangle.txt

也可以使用相对路径并负责文件到我们在 HDFS 中的 home 目录,本例中为 /user/devalone:
    
    [devalone@nutch hadoop-book]$ hadoop fs -copyFromLocal input/docs/quangle.txt quangle.txt
        
复制回本地文件系统并检查是否相同:
    
    [devalone@nutch hadoop-book]$ hadoop fs -copyToLocal quangle.txt quangle.copy.txt
    [devalone@nutch hadoop-book]$ md5sum input/docs/quangle.txt quangle.copy.txt
    e7891a2627cf263a079fb0f18256ffb2  input/docs/quangle.txt
    e7891a2627cf263a079fb0f18256ffb2  quangle.copy.txt
        
最后,看下文件列表,首先创建一个目录,然后显示列表:
    
    [devalone@nutch hadoop-book]$ hadoop fs -mkdir books
    [devalone@nutch hadoop-book]$ hadoop fs -ls
    Found 2 items
    drwxr-xr-x   - devalone supergroup          0 2018-07-27 17:22 books
    -rw-r--r--   1 devalone supergroup        119 2018-07-27 17:01 quangle.txt
    
返回结果第二列是这个文件的复制因子(the replication factor of the file)。伪分布模式设置了 1,因此这里显示相同的值 1 。这一
列目录的值为空,是因为复制的概念不应用到目录 —— 目录被作为元数据并存储在 namenode 上,而不是 datanode 上。第五列是文件大小,
字节单位,对于目录,值为 0。
    
第六列第七列显示的是最后修改日期和时间,最后,第八列显示的是文件或目录名称。
    
    HDFS 中的文件权限
    ---------------------------------------------------------------------------------------------------------------------
    HDFS 中的文件和目录权限模型与 POSIX 模型类似: rwx 有一个超级用户概念 (a superuser), which is the identity of the
    namenode process. 权限检查不对 superuser 执行检查。
    

*
*
*

4 Hadoop 文件系统 (Hadoop Filesystems)
--------------------------------------------------------------------------------------------------------------------------
Hadoop 有一个抽象文件系统的概念,HDFS 只是其中的一个实现。Java 抽象类 org.apache.hadoop.fs.FileSystem 定义了 Hadoop 中文件
系统的客户端接口,并且有几个具体的实现,与 Hadoop 一起提供的主要的实现列于下表:

            
                                Hadoop filesystems
                                
+===========+===============+===========================================+===================================================================+
| 文件系统    | URI scheme    | Java implementation (in org.apache.hadoop)|                            描述                                    |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| Local        | file            | fs.LocalFileSystem                        | A filesystem for a locally connected disk with client-side         |
|            |                |                                            | checksums. Use RawLocalFileSystem for a local filesystem with        |
|            |                |                                            | no checksums                                                        |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| HDFS        | hdfs            | hdfs.DistributedFileSystem                | Hadoop’s distributed filesystem. HDFS is designed to work         |
|            |                |                                            | efficiently in conjunction with MapReduce.                        |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| WebHDFS    | webhdfs        | hdfs.web.WebHdfsFileSystem                | A filesystem providing authenticated read/write access to HDFS    |
|            |                |                                            | over HTTP.                                                        |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| Secure     | swebhdfs        | hdfs.web.SWebHdfsFileSystem                | The HTTPS version of WebHDFS.                                        |
| WebHDFS    |                |                                            |                                                                    |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| HAR        | har            | fs.HarFileSystem                            | A filesystem layered on another filesystem for archiving files.     |
|            |                |                                            | Hadoop Archives are used for packing lots of files in HDFS into    |
|            |                |                                            | a single archive file to reduce the namenode’s memory usage.         |
|            |                |                                            | Use the hadoop archive command to create HAR files.                |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| View        | viewfs        | viewfs.ViewFileSystem                        | A client-side mount table for other Hadoop filesystems. Commonly    |
|            |                |                                            | used to create mount points for federated namenodes                |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| FTP        | ftp            | fs.ftp.FTPFileSystem                        | A filesystem backed by an FTP server.                                |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| S3        | s3a            | fs.s3a.S3AFileSystem                        | A filesystem backed by Amazon S3. Replaces the older s3n            |
|            |                |                                            | (S3 native) implementation.                                        |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| Azure        | wasb            | fs.azure.NativeAzureFileSystem            | A filesystem backed by Microsoft Azure.                            |
+-----------+---------------+-------------------------------------------+-------------------------------------------------------------------+
| Swift        | swift            | fs.swift.snative.SwiftNativeFileSystem    | A filesystem backed by OpenStack Swift.                            |
+-----------+---------------+-------------------------------------------|-------------------------------------------------------------------+

 

Hadoop 对它的文件系统提供了很多接口,并通常使用 URI 方案(URI scheme) 来选取合适的文件系统实例与之通信。举个例子,文件系统
shell 可以操作所有的 Hadoop 文件系统。列出本地文件系统根目录下的文件:
    
    [devalone@nutch hadoop]$ hadoop fs -ls file:///
    Found 22 items
    -rw-r--r--   1 root root          0 2018-05-04 10:22 file:///.autorelabel
    dr-xr-xr-x   - root root      24576 2018-07-20 13:23 file:///bin
    dr-xr-xr-x   - root root       4096 2018-07-23 10:26 file:///boot
    drwxr-xr-x   - root root         17 2018-05-07 11:45 file:///cgroups_test
    drwxr-xr-x   - root root       3060 2018-07-27 15:59 file:///dev
    drwxr-xr-x   - root root       8192 2018-07-27 15:59 file:///etc
    drwxr-xr-x   - root root         22 2018-07-20 15:59 file:///home
    dr-xr-xr-x   - root root       4096 2018-05-24 13:11 file:///lib
    dr-xr-xr-x   - root root      24576 2018-07-20 13:23 file:///lib64
    drwxr-xr-x   - root root         19 2018-04-11 12:59 file:///media
    drwxr-xr-x   - root root         18 2018-05-16 10:09 file:///mnt
    drwxr-xr-x   - root root          6 2018-05-04 13:04 file:///nm-local-dir
    drwxr-xr-x   - root root         45 2018-07-25 12:02 file:///opt
    dr-xr-xr-x   - root root          0 2018-07-27 15:58 file:///proc
    dr-xr-x---   - root root        240 2018-05-16 10:06 file:///root
    drwxr-xr-x   - root root        800 2018-07-27 17:19 file:///run
    dr-xr-xr-x   - root root      16384 2018-07-23 10:25 file:///sbin
    drwxr-xr-x   - root root          6 2018-04-11 12:59 file:///srv
    dr-xr-xr-x   - root root          0 2018-07-27 15:59 file:///sys
    drwxrwxrwt   - root root       4096 2018-07-27 17:00 file:///tmp
    drwxr-xr-x   - root root        191 2018-05-13 13:12 file:///usr
    drwxr-xr-x   - root root        267 2018-05-13 13:12 file:///var

    
虽然运行 MapReduce 程序访问任何文件系统是可行的(并且有时非常方便),但当处理大规模数据时应该选择一个具有数据本地优化的分布式
文件系统,特别是 HDFS 。
    
    
接口 (Interfaces)
--------------------------------------------------------------------------------------------------------------------------
Hadoop 是用 Java 写的,因此,大多数的 Hadoop 文件系统交互都是通过 Java API 发生的。例如,文件系统 shell 是一个 Java 应用程
序,使用 Java FileSystem class 提供文件系统操作。其他文件系统接口在本节中做简单论述。这些接口通常与 HDFS 一起使用,因为
Hadoop 中其他文件系统一般都有现有的工具来访问底层文件系统(FTP 客户端对应 FTP, S3 tools 对应 S3, 等),但它们大多数能与任何
Hadoop 文件系统工作。

    
    ① HTTP
    ----------------------------------------------------------------------------------------------------------------------
    Hadoop 通过 Java API 提供文件系统接口, 使用非 Java 应用程序访问 HDFS 不太方便。由 WebHDFS 协议提供的 HTTP REST API, 使
    其他语言与 HDFS 交互更加容易。注意 HTTP 接口比纯的 Java 客户端慢,因此应该尽量避免用在非常大的数据传输中。
    
    通过 HTTP 访问 HDFS 有两种方法:直接访问, HDFS 守护进程直接为客户端的 HTTP 请求提供服务; 第二,通过代理访问,代表客户
    端使用通常的 DistributedFileSystem API 访问。两种方法都使用 WebHDFS 协议。
    ----------------------------------------------------------------------------------------------------------------------
    在第一种场景中,在 namenode 和 datanode 中嵌入的 web 服务器作为 WebHDFS 端点(WebHDFS 默认是启用的,dfs.webhdfs.enabled
    属性设置为 true) 文件元数据的操作由 namenode 处理,而的文件读取(或写入)操作首先发送给 namenode, 然后发送 HTTP 重定向给
    客户端指明文件数据流的 datanode.
    
    ----------------------------------------------------------------------------------------------------------------------
    第二种通过 HTTP 访问 HDFS 的方法取决于一个或多个独立的代理服务器(standalone proxy servers)。代理服务器是无状态的,因此
    它们可以运行在一个标准的负载均衡器之后(behind a standard load balancer)。所有与集群的通信都经过代理服务器,因此客户端
    永远不会直接访问 namenode 或 datanode。这可以使用更严格的防火墙和带宽限制策略。一般使用代理在位于不同数据中心的 Hadoop
    集群间传输数据,或者访问一个外部网络的运行在云端上的 Hadoop 集群。
    
    HttpFS 代理提供了与 WebHDFS 相同的 HTTP(以及 HTTPS) 接口,因此客户端能通过 webhdfs(or swebhdfs) URI 访问这两者。HttpFS
    proxy 启动独立于 namenode 和 datanode 守护进程,使用 httpfs.sh 脚本,并且默认侦听在一个不同的端口号上(14000)。
    
    
    ② C 语言
    ----------------------------------------------------------------------------------------------------------------------
    Hadoop 提供了一个 Java FileSystem 接口的 C 语言库称为 libhdfs. 它被写成访问 HDFS 的 C 语言库,但不用理会它的名称,它其
    实能用于访问任何 Hadoop 文件系统。 它使用 Java 原生接口(Java Native Interface, JNI)来调用一个 Java 文件系统的客户端。也
    有一个 libwebhdfs 用于 WebHDFS 接口。
    
    C API 与 Java API 非常相似,但通常比 Java API 滞后开发,因此有些新特性可能并不支持。可以在Apache Hadoop binary tarball
    发布包的 include 目录内找到头文件,hdfs.h

    Apache Hadoop binary tarball 自带了预编译的 64-bit Linux 平台的 libhdfs 二进制文件,但对于其他平台需要根据源码包中
    BUILDING.txt的指令自己构建。
    
    
    ③ NFS
    ----------------------------------------------------------------------------------------------------------------------
    使用 Hadoop 的 NFSv3 gateway 可以将 HDFS 挂载到一个本地客户端的文件系统上。然后可以使用 Unix 实用程序(例如 ls 和 cat)
    来与文件系统交互,上载文件,并通常可以从任何编程语言使用 POSIX 类库访问文件系统。追加数据到文件可以工作,但任意修改文件
    不会工作,因为 HDFS 只能向文件末尾写入数据。
    
    参考 Hadoop 文档探究如何配置和运行 NFS gateway 以及从客户端连接到它。
    
    
    ④ FUSE
    ----------------------------------------------------------------------------------------------------------------------    
    用户空间文件系统(Filesystem in Userspace, FUSE) 允许将用户空间实现的文件系统集成为 Unix 文件系统。 Hadoop 的 Fuse-DFS
    模块允许 HDFS(或任何的    Hadoop 文件系统) 挂载为一个标准的本地文件系统。Fuse-DFS 由 C 语言实现,使用 libhdfs 作为 HDFS
    接口。目前,挂载 HDFS,Hadoop 的 NFS gateway 是更智能的解决方案,应该比 Fuse-DFS 优先使用。


*
*
*

5 Java 接口 (The Java Interface)
--------------------------------------------------------------------------------------------------------------------------
深入探索 Hadoop FileSystem class: 它是与任何一个 Hadoop 的文件系统交互的 API. 虽然主要聚焦于 HDFS 实现:

    DistributedFileSystem

但在实际写代码时应力图使用:

    FileSystem
    
抽象类,以获得跨文件系统可移植性。这对测试程序非常有用。例如,可以使用存储在本地文件系统上的数据快速运行测试。


① 从 Hadoop URL 读取数据 (Reading Data from a Hadoop URL)
--------------------------------------------------------------------------------------------------------------------------
从 Hadoop 文件系统读取文件最简单的方法之一是使用 java.net.URL 对象打开一个数据流来从中读取数据。一般用法如下:

    InputStream in = null;
    try {
    in = new URL("hdfs://host/path").openStream();
    // process in
    } finally {
    IOUtils.closeStream(in);
    }
--------------------------------------------------------------------------------------------------------------------------
让 Java 能够识别 Hadoop 的 hdfs URL scheme 还需要做一点额外的工作。通过用一个 FsUrlStreamHandlerFactory 实例来调用 URL 的
静态方法:setURLStreamHandlerFactory() 来实现。这个方法每个 JVM 只能调用一次,因此通常在一个静态块中执行(executed in a
static block)。这个限制意味着如果程序的某些其他部分(或许一个不受控制的第三方组件)已经设置了一个 URLStreamHandlerFactory,
就不能使用这种方法从 Hadoop 读取数据。

    示例:
    ----------------------------------------------------------------------------------------------------------------------
    // Displaying files from a Hadoop filesystem on standard output using a URLStreamHandler
    public class URLCat {
        static {
            URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory());
        }
        public static void main(String[] args) throws Exception {
            InputStream in = null;
            try {
                in = new URL(args[0]).openStream();
                IOUtils.copyBytes(in, System.out, 4096, false);
            } finally {
                IOUtils.closeStream(in);
            }
        }
    }

    运行:
    ----------------------------------------------------------------------------------------------------------------------
    % export HADOOP_CLASSPATH=hadoop-examples.jar
    % hadoop URLCat hdfs://localhost/user/devalone/quangle.txt

 

② 使用 FileSystem API 读取数据 (Reading Data Using the FileSystem API)
--------------------------------------------------------------------------------------------------------------------------
正如上一节解释的,有时候在应用程序中设置 URLStreamHandlerFactory 是不可能的。这种情况下,需要使用 FileSystem API 来打开一个
文件输入流。

一个文件在 Hadoop 文件系统上由一个 Hadoop Path 对象表示(不是一个 java.io.File 对象,因为它的语意与本地文件系统联系太紧密)。
可以把 Path 对象理解为 Hadoop 文件系统 URI, 例如:hdfs://localhost/user/devalone/quangle.txt

FileSystem 是一个通用文件系统 API, 因此首先是获取一个要使用的文件系统实例,本例中为 HDFS。有几个静态工厂方法来获取 FileSystem
实例:

    public static FileSystem get(Configuration conf) throws IOException
    public static FileSystem get(URI uri, Configuration conf) throws IOException
    public static FileSystem get(URI uri, Configuration conf, String user) throws IOException

Configuration 对象封装了客户端或服务器的配置信息,可以使用从类路径读取的配置文件设置,例如,etc/hadoop/core-site.xml.
第一个方法返回默认的文件系统(在 core-site.xml 文件指定的,如果没有指定则放回默认的本地文件系统)。
第二个方法使用给定的 URI scheme 和 authority来确定使用的文件系统,如果在给定的 URI中没有指定 scheme,则返回默认的文件系统。
第三个方法为给定的用户返回文件系统,在安全环境下很重要。

有些情况下,想要获取一个本地文件系统实例,为此,可以使用一个方便的方法 getLocal():

    public static LocalFileSystem getLocal(Configuration conf) throws IOException

有了一个 FileSystem 实例在手,就可以调用一个 open() 方法来得到一个文件的输入流:

    public FSDataInputStream open(Path f) throws IOException
    public abstract FSDataInputStream open(Path f, int bufferSize) throws IOException
    
第一个方法使用默认的缓冲区大小为 4 KB

    示例:
    // Displaying files from a Hadoop filesystem on standard output by using the FileSystem directly
    public class FileSystemCat {
        public static void main(String[] args) throws Exception {
            String uri = args[0];
            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, 4096, false);
            } finally {
                IOUtils.closeStream(in);
            }
        }
    }
    
    运行:
    ----------------------------------------------------------------------------------------------------------------------
    % hadoop FileSystemCat hdfs://localhost/user/devalone/quangle.txt

 

    FSDataInputStream
    ----------------------------------------------------------------------------------------------------------------------
    FileSystem 的 open() method 实际上返回的是 FSDataInputStream 而不是标准的 java.io 类。这个类是 java.io.DataInputStream
    的一个特殊子类,支持随机访问,因此可以从流的任何部分读取:
    
    package org.apache.hadoop.fs;
    public class FSDataInputStream extends DataInputStream
        implements Seekable, PositionedReadable {
        // implementation elided
    }
    
    Seekable 接口允许定位到文件中的一个位置,并提供了一个查询方法来得到当前位置与文件开始的偏移量:
    
    public interface Seekable {
        void seek(long pos) throws IOException;
        long getPos() throws IOException;
    }
    
    使用一个大于文件长度的位置调用 seek() 会引发一个 IOException 异常。seek() 方法可以移动到文件中任意的,绝对的位置。
    
    示例:
    ----------------------------------------------------------------------------------------------------------------------
    // Displaying files from a Hadoop filesystem on standard output twice, by using seek()
    public class FileSystemDoubleCat {
        public static void main(String[] args) throws Exception {
            String uri = args[0];
            Configuration conf = new Configuration();
            FileSystem fs = FileSystem.get(URI.create(uri), conf);
            FSDataInputStream in = null;
            try {
                in = fs.open(new Path(uri));
                IOUtils.copyBytes(in, System.out, 4096, false);
                in.seek(0); // go back to the start of the file
                IOUtils.copyBytes(in, System.out, 4096, false);
            } finally {
                IOUtils.closeStream(in);
            }
        }
    }
    
    运行:
    ----------------------------------------------------------------------------------------------------------------------
    % hadoop FileSystemDoubleCat hdfs://localhost/user/devalone/quangle.txt
    On the top of the Crumpetty Tree
    The Quangle Wangle sat,
    But his face you could not see,
    On account of his Beaver Hat.
    On the top of the Crumpetty Tree
    The Quangle Wangle sat,
    But his face you could not see,
    On account of his Beaver Hat.        
    
    
    FSDataInputStream 也实现了 PositionedReadable 接口来从一个给定的偏移量开始读取文件的部分:
        
        public interface PositionedReadable {
            public int read(long position, byte[] buffer, int offset, int length) throws IOException;
            public void readFully(long position, byte[] buffer, int offset, int length) throws IOException;
            public void readFully(long position, byte[] buffer) throws IOException;
        }
    
    read() method 从文件中给定的 position 位置开始读取 length 个字节到 buffer 中给定的 offset 偏移位置。返回值是实际读取的
    字节数,调用者应该检查这个值,因为它可能比 length 小。
    
    readFully() method 读取 length 个字节(或对于只提供一个字节数组 buffer 参数的版本 buffer.length 个字节)到 buffer 中,除
    非到达文件末尾,这种情况抛出 EOFException 异常。
    
    所有的方法会保留文件当前的偏移量并且是线程安全的(FSDataInputStream 不是设计用于并发访问的,因此最好创建多个实例),因此
    它们提供了访问文件其他部分的便利方法。
    ----------------------------------------------------------------------------------------------------------------------
    最后,记住调用 seek() 方法是相对高开销的操作应该少使用。应建立流式数据来构建应用程序访问模式(例如,使用 MapReduce),
    而不是执行大量的 seek 操作。
        
        
③ 写入数据 (Writing Data)
--------------------------------------------------------------------------------------------------------------------------
FileSystem 类有一系列创建文件的方法。最简单的方法是接受一个待创建文件的 Path 对象并返回一个用于写入的输出流:

    public FSDataOutputStream create(Path f) throws IOException
    
这个方法的多个重载版本允许指定是否强制覆盖现有文件,文件的复制因子,写入文件时使用的缓冲区大小,文件的块大小,以及文件的许
可权限。


    WARNING:
    ----------------------------------------------------------------------------------------------------------------------
    create() method 创建写入文件的任何父目录如果不存在的话。尽管这样很方便,但有时不希望这种行为。如果想要在父目录不存在的
    情况下写入失败,应首先通过调用 exists() method 来检查父目录是否存在。另一种方法,使用 FileContext,它允许控制父目录是否
    创建。
    

也有一个重载方法用于传递一个回调接口,Progressable, 这样程序就可以接收数据写入 datanode 进度的通知:

    package org.apache.hadoop.util;
    
    public interface Progressable {
        public void progress();
    }

作为创建文件的另一种途径,可以使用 append() method 向已存在的文件追加数据(也有很多重载版本):

    public FSDataOutputStream append(Path f) throws IOException
    

追加操作允许一个单一的 writer 修改一个已写入的文件,通过打开它并从文件最后的偏移量处写入数据。通过这个 API, 产生无边界文件
的应用程序,例如日志文件,可以向一个已存在的文件在其关闭之后写入数据。

追加操作是可选的而且并非所有的 Hadoop 文件系统都实现。例如,HDFS 支持追加,但 S3 文件系统不支持。

下面示例显示了如何复制一个本地文件到 Hadoop 文件系统。通过 progress() method 每次被 Hadoop 调用时打印一个句点来演示进度,也
就是每 64 KB 数据包被写入 datanode 管线之后。

    示例:
    ----------------------------------------------------------------------------------------------------------------------
    // Copying a local file to a Hadoop filesystem
    public class FileCopyWithProgress {
        public static void main(String[] args) throws Exception {
            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);
            OutputStream out = fs.create(new Path(dst), new Progressable() {
                    public void progress() {
                        System.out.print(".");
                    }
                    });
            IOUtils.copyBytes(in, out, 4096, true);
        }
    }

    运行:
    ----------------------------------------------------------------------------------------------------------------------
    % hadoop FileCopyWithProgress input/docs/1400-8.txt
    hdfs://localhost/user/devalone/1400-8.txt
    .................
        

目前,没有其他 Hadoop 文件系统在写入期间调用 process(). 进度在 MapReduce 应用程序中很重要。


    FSDataOutputStream
    ----------------------------------------------------------------------------------------------------------------------
    FileSystem 的 create() method 返回一个 FSDataOutputStream, 与 FSDataInputStream 类似,有一个查询文件中当前位置的方法:
    
        package org.apache.hadoop.fs;
        
        public class FSDataOutputStream extends DataOutputStream implements Syncable {
            public long getPos() throws IOException {
            // implementation elided
            }
        // implementation elided
        }
    
    但是,不像 FSDataInputStream, FSDataOutputStream 不允许定位。这是因为 HDFS 只允许顺序写入一个打开的文件或追加到一个已
    写入数据的文件。换句话说,除了向文件尾部添加,不支持向文件任意位置写入数据,所以在写入时定位的没什么价值。


    
④ 目录 (Directories)
--------------------------------------------------------------------------------------------------------------------------
FileSystem 提供了一个创建目录的方法:

    public boolean mkdirs(Path f) throws IOException
    
这个方法创建所有必要的父目录如果不存在的话,就像 java.io.File’s mkdirs() method。如果创建成功(包括所有父目录)返回 true。

一般地,不需要显式创建目录,因为调用 create() method 写入一个文件会创建所有父目录。
    
    
⑤ 查询文件系统 (Querying the Filesystem)
--------------------------------------------------------------------------------------------------------------------------
    
    
    文件元数据:FileStatus (File metadata: FileStatus)
    ----------------------------------------------------------------------------------------------------------------------
    任何文件系统的重要特征是导航其目录结构和获取有关它存储的文件和目录的信息。 FileStatus class 封装了文件和目录的文件系统
    元数据,包括文件长度,块大小,复本数量,修改时间,所有者,以及权限信息。

    FileSystem 的 getFileStatus() method 提供了一个获取文件或目录的 FileStatus 对象的方法。
    
    
    示例:
    ----------------------------------------------------------------------------------------------------------------------
    // Demonstrating file status information
    public class ShowFileStatusTest {
        private MiniDFSCluster cluster; // use an in-process HDFS cluster for testing
        private FileSystem fs;
        
        @Before
        public void setUp() throws IOException {
            Configuration conf = new Configuration();
            if (System.getProperty("test.build.data") == null) {
                System.setProperty("test.build.data", "/tmp");
            }
            cluster = new MiniDFSCluster.Builder(conf).build();
            fs = cluster.getFileSystem();
            OutputStream out = fs.create(new Path("/dir/file"));
            out.write("content".getBytes("UTF-8"));
            out.close();
        }
        
        @After
        public void tearDown() throws IOException {
            if (fs != null) { fs.close(); }
            if (cluster != null) { cluster.shutdown(); }
        }
        
        @Test(expected = FileNotFoundException.class)
        public void throwsFileNotFoundForNonExistentFile() throws IOException {
            fs.getFileStatus(new Path("no-such-file"));
        }
        
        @Test
        public void fileStatusForFile() throws IOException {
            Path file = new Path("/dir/file");
            FileStatus stat = fs.getFileStatus(file);
            assertThat(stat.getPath().toUri().getPath(), is("/dir/file"));
            
            assertThat(stat.isDirectory(), is(false));
            assertThat(stat.getLen(), is(7L));
            assertThat(stat.getModificationTime(),
            is(lessThanOrEqualTo(System.currentTimeMillis())));
            assertThat(stat.getReplication(), is((short) 1));
            assertThat(stat.getBlockSize(), is(128 * 1024 * 1024L));
            assertThat(stat.getOwner(), is(System.getProperty("user.name")));
            assertThat(stat.getGroup(), is("supergroup"));
            assertThat(stat.getPermission().toString(), is("rw-r--r--"));
        }
        
        @Test
        public void fileStatusForDirectory() throws IOException {
            Path dir = new Path("/dir");
            FileStatus stat = fs.getFileStatus(dir);
            assertThat(stat.getPath().toUri().getPath(), is("/dir"));
            assertThat(stat.isDirectory(), is(true));
            assertThat(stat.getLen(), is(0L));
            assertThat(stat.getModificationTime(),
            is(lessThanOrEqualTo(System.currentTimeMillis())));
            assertThat(stat.getReplication(), is((short) 0));
            assertThat(stat.getBlockSize(), is(0L));
            assertThat(stat.getOwner(), is(System.getProperty("user.name")));
            assertThat(stat.getGroup(), is("supergroup"));
            assertThat(stat.getPermission().toString(), is("rwxr-xr-x"));
        }
    }
    
    
    列出文件 (Listing files)
    ----------------------------------------------------------------------------------------------------------------------
    列出一个目录的内容, FileSystem 的 listStatus() 方法:
    
        public FileStatus[] listStatus(Path f) throws IOException
        public FileStatus[] listStatus(Path f, PathFilter filter) throws IOException
        public FileStatus[] listStatus(Path[] files) throws IOException
        public FileStatus[] listStatus(Path[] files, PathFilter filter)    throws IOException
    
    当参数为一个文件时,最简单的重载形式返回一个长度为 1 的 FileStatus 数组。如果参数是一个目录,它返回 0 或多个FileStatus
    对象表示目录中包含的文件和目录。
    
    重载形式允许提供一个 PathFilter 参数来限制匹配的文件和目录。最后,如果指定一个路径数组,其执行结果相当于依次轮流传递每
    条路径并对其调用 listStatus() method, 再将 FileStatus 对象数组累积存入同一数组中。这对从文件系统树的不同部分构建输入文
    件列表进行处理时非常有用。
    
    示例:
    ----------------------------------------------------------------------------------------------------------------------
    // Showing the file statuses for a collection of paths in a Hadoop filesystem
    public class ListStatus {
        public static void main(String[] args) throws Exception {
            String uri = args[0];

            Configuration conf = new Configuration();
            FileSystem fs = FileSystem.get(URI.create(uri), conf);
            Path[] paths = new Path[args.length];
            for (int i = 0; i < paths.length; i++) {
                paths[i] = new Path(args[i]);
            }
            FileStatus[] status = fs.listStatus(paths);
            Path[] listedPaths = FileUtil.stat2Paths(status);
            for (Path p : listedPaths) {
                System.out.println(p);
            }
        }
    }
    
    
    运行:
    ----------------------------------------------------------------------------------------------------------------------
    % hadoop ListStatus hdfs://localhost/ hdfs://localhost/user/devalone
    hdfs://localhost/user
    hdfs://localhost/user/devalone/books
    hdfs://localhost/user/devalone/quangle.txt
    

    文件模式 (File patterns)
    ----------------------------------------------------------------------------------------------------------------------
    在一个单一的操作中处理一批的文件(sets of files) 是很常见的要求。例如,一个日志处理的 MapReduce 作业可能分析一个月的包含
    在几个目录内的文件。不用累积每个文件和目录最为输入,更便利方法是利用一个简单的表达式使用通配符来匹配多个文件,该操作被
    称为通配符(globbing)。Hadoop 提供两个 Filesystem 方法用于处理通配符:
    
        public FileStatus[] globStatus(Path pathPattern) throws IOException
        public FileStatus[] globStatus(Path pathPattern, PathFilter filter) throws IOException
    
    globStatus() 方法返回一个 FileStatus 对象数组,它们的路径匹配提供的模式(pattern),由路径排序。一个可选的 PathFilter
    指定更进一步的匹配限制。Hadoop 支持和 Unix bash shell 相同的通配符,如下表所示。
    
    
    
                            Glob characters and their meanings
                            
    +===========+===========================+=======================================================================+
    | 通配符    |              名称            |                    匹配                                                |
    +-----------+---------------------------+-----------------------------------------------------------------------+
    | *            | asterisk                    | Matches zero or more characters                                        |
    +-----------+---------------------------+-----------------------------------------------------------------------+
    | ?        | question mark                | Matches a single character                                            |
    +-----------+---------------------------+-----------------------------------------------------------------------+
    | [ab]        | character class            | Matches a single character in the set {a, b}                            |
    +-----------+---------------------------+-----------------------------------------------------------------------+
    | [^ab]        | negated character class    | Matches a single character that is not in the set {a, b}                |
    +-----------+---------------------------+-----------------------------------------------------------------------+
    | [a-b]        | character range            | Matches a single character in the (closed) range [a, b], where a is    |
    |            |                            | lexicographically less than or equal to b                                |
    +-----------+---------------------------+-----------------------------------------------------------------------+
    | [^ab]        | negated character range    | Matches a single character that is not in the (closed) range [a, b],    |
    |            |                            | where a is lexicographically less than or equal to b                    |
    +-----------+---------------------------+-----------------------------------------------------------------------+
    | {a,b}        | alternation                | Matches either expression a or b                                        |
    +-----------+---------------------------+-----------------------------------------------------------------------+
    | \c        | escaped character            | Matches character c when it is a metacharacter                        |
    +-----------+---------------------------+-----------------------------------------------------------------------+

设想日志文件存储在按日期组织的目录结构中,则2007年最后一天的日志文件会存储在名为 /2007/12/31 目录中。假设文件列表如下所示:

    /
    ├──2007/
    │ └─── 12/
    │         ├── 30/
    │         └── 31/
    └── 2008/
        └── 01/
            ├── 01/
            └── 02/
    
下面是一些文件通配符及其扩展(expansions):
    
    +-------------------+---------------------------+
    |     通配符            |        扩展                |
    +-------------------+---------------------------+
    | /*                | /2007 /2008                |
    +-------------------+---------------------------+
    | /*/*                | /2007/12 /2008/01            |
    +-------------------+---------------------------+
    | /*/12/*            | /2007/12/30 /2007/12/31    |
    +-------------------+---------------------------+
    | /200?                | /2007 /2008                |
    +-------------------+---------------------------+
    | /200[78]            | /2007 /2008                |
    +-------------------+---------------------------+
    | /200[7-8]            | /2007 /2008                |
    +-------------------+---------------------------+
    | /200[^01234569]    | /2007 /2008                |
    +-------------------+---------------------------+
    | /*/*/{31,01}        | /2007/12/31 /2008/01/01    |
    +-------------------+---------------------------+
    | /*/*/3{0,1}        | /2007/12/30 /2007/12/31    |
    +-------------------+---------------------------+
    | /*/{12/31,01/01}    | /2007/12/31 /2008/01/01    |
    +-------------------+---------------------------+
    
    
    PathFilter 对象 (PathFilter)
    ----------------------------------------------------------------------------------------------------------------------
    通配符模式(Glob patterns) 不总是足够强大以精确描述我们想要访问的一个文件集。例如,一般不太可能使用一个通配符模式来排除
    一个特定的文件。Filesystem 的 listStatus() 和 globStatus() 方法接受一个可选的 PathFilter 参数, 它可以通过编程方式控制
    文件匹配:
    
        package org.apache.hadoop.fs;
        public interface PathFilter {
            boolean accept(Path path);
        }
                
    
    示例:
    ----------------------------------------------------------------------------------------------------------------------
    // A PathFilter for excluding paths that match a regular expression
    public class RegexExcludePathFilter implements PathFilter {
        private final String regex;
        
        public RegexExcludePathFilter(String regex) {
            this.regex = regex;
        }
        
        public boolean accept(Path path) {
            return !path.toString().matches(regex);
        }
    }

这个过滤器只通过那些不匹配正则表达式(regular expression)的文件。在匹配器拣选出一个初始的包含文件集之后,使用过滤器再次提炼
输出结果。例如:

    fs.globStatus(new Path("/2007/*/*"), new RegexExcludeFilter("^.*/2007/12/31$"))

将扩展为 /2007/12/30

过滤器只能作用于文件名,由一个 Path 对象表示。它们不能用于文件属性,例如创建时间。尽管如此,它们能执行通配符模式不能完成的,
正则表达式也不能实现的工作。例如,如果在一个由日期布局的目录结构中存储文件(如上一节中的例子),则可以写一个 PathFilter 按一
个给定的日期范围来拣选文件。
        
        
⑥ 删除文件 (Deleting Data)
--------------------------------------------------------------------------------------------------------------------------
使用 FileSystem 的 delete() method 可以永久删除文件和目录:

    public boolean delete(Path f, boolean recursive) throws IOException
    
如果 f  是一个文件或一个空目录,recursive 的值被忽略。一个非空目录及其内容被删除,只有当 recursive 值为 true 时有效(否则抛出
IOException 异常)。
    
    
*
*
*

6 数据流 (Data Flow)
--------------------------------------------------------------------------------------------------------------------------
        
        
6.1 剖析文件读取 (Anatomy of a File Read)
--------------------------------------------------------------------------------------------------------------------------
读取数据时客户端与 HDFS, namenode, 以及 datanode 之间交互过程:

    ① 第一步,客户端通过调用 Filesystem 的 open() 方法打开要读取的文件,对于 HDFS 来说是一个 DistributedFileSystem 实例。
    
    ② 第二步,DistributedFileSystem 使用 remote procedure calls (RPCs) 呼叫 namenode 以确定文件开始的几个数据块的位置。对
    于每个数据块, namenode 返回含有一个数据块复本的 datanode 地址。此外, datanode 根据它们与客户端的距离排序(根据集群网络
    的拓扑结构)。如果客户端本身是一个 datanode (例如,一个 MapReduce 任务的情景),如果这个 datanode 驻留了一个块复本,客户
    端将从本地 datanode 读取数据。

    DistributedFileSystem 返回一个 FSDataInputStream 给客户端用于从中读取数据。 FSDataInputStream 内部封装了一个
    DFSInputStream, 它管理 datanode 和 namenode I/O 。
    
    ③ 第三步,客户端在流上调用 read() method. DFSInputStream, 存储着文件前几个块的数据节点地址,连接到文件的第一个数据块的
    第一个 datanode (最近的)。
    
    ④ 第四步,数据从 datanode 流回反复调用 read()方法的客户端。
    
    ⑤ 当读取到达数据块的尾部时,DFSInputStream 关闭与 datanode 的连接,然后查找下一个数据块最佳 datanode. 这些对客户端都是
    透明的,从客户端的角度,它只是读取连续的数据流。
    
    ⑥ 数据块是按顺序读取的,客户端读取整个数据流过程中,DFSInputStream 不断地打开新的连接到 datanode。 它也呼叫 namenode
    以获取下一批数据块的 datanode 位置。当客户端结束读取时,它调用 FSDataInputStream 的 close() 方法关闭数据流。


在读取过程中,如果 DFSInputStream 与一个 datanode 通信遇到错误,它会为该数据块尝试下一个最近的 datanode. 它会记住那个失败的
datanode, 以便为后面的数据块不会做不必要的尝试。DFSInputStream 也为从 datanode 传输的数据验证校验和。如果发现一个损坏的数据
块,DFSInputStream 尝试从另外一个 datanode 读取块复本,它也会向 namenode 报告损坏的数据块。

这个设计的一个重要方面是客户端连接 datanode 直接获取数据并由 namenode 指引每个数据块最佳的 datanode。因为数据流量分散到集群
中所有的 datanodes,因此这个设计允许 HDFS 扩展到大规模的并发客户端。其间,namenode 仅仅服务于块位置请求(存储在内存中,使其
非常高效)并不做别的事,例如服务于数据,那将很快就成为客户端增长的瓶颈。


    网络拓扑和 Hadoop (NETWORK TOPOLOGY AND HADOOP)
    ----------------------------------------------------------------------------------------------------------------------
    在本地网络中,两个节点被称为“彼此邻近(close)” 是什么意思?在海量数据(high-volume)处理环境下,限制因素是节点间数据传输
    速率,带宽是稀缺的资源。这里的思想是使用两个节点间的带宽作为距离(distance)衡量。
    
    不是测量两个节点间的带宽,那在实践中是很难做到的, Hadoop 采用一种简单的方法,网络被表示为一棵树,两个节点间的距离是指
    它们距离它们最近的共同祖先距离的总和。树的层次没有预定义,但对应于数据中心,机架和正在运行的节点,通常有层次关系。其思
    想是以下每个场景的可用带宽逐步减少:
    
        □ 一个节点上的进程
        □ 同一个机架上不同的节点
        □ 同一个数据中心不同机架上的节点间
        □ 不同数据中心的节点间
    
    例如,假设一个节点 n1 在机架 r1 上,在数据中心 d1 中。这可以表示为 /d1/r1/n1. 使用这种表示法,下面是四个场景的距离
    (distance):
            
        □ distance(/d1/r1/n1, /d1/r1/n1) = 0 (processes on the same node)
        □ distance(/d1/r1/n1, /d1/r1/n2) = 2 (different nodes on the same rack)
        □ distance(/d1/r1/n1, /d1/r2/n3) = 4 (nodes on different racks in the same data center)
        □ distance(/d1/r1/n1, /d2/r3/n4) = 6 (nodes in different data centers)
    
    认识到这一点很重要,Hadoop 不能智能地为用户探索出网络拓扑,这需要一些帮助。默认地,它假设网络是扁平的,即单一的一层结构,
    换句话说,所有的节点都在一个数据中心的一个机架上。对于小型集群,可能就是实际场景,不需要进一步配置。

        
6.2 剖析文件写入 (Anatomy of a File Write)
--------------------------------------------------------------------------------------------------------------------------
考虑新建一个文件,写入数据,然后关闭文件的过程。

    ① 第一步,客户端通过调用 DistributedFileSystem 的 create() 方法创建文件
    
    ② 第二步,DistributedFileSystem 通过 RPC 调用 namenode 在文件系统名称空间创建一个新文件,此时,文件没有任何数据块与之
    关联。 namenode 执行各种检查以确保该文件尚不存在,以及客户端有新建该文件的权限。如果这些检查都通过,namenode 创建一条新
    文件的记录,否则,文件创建失败并且客户端抛出 IOException 异常。DistributedFileSystem 为客户端返回一个 FSDataOutputStream
    用于写入数据。FSDataOutputStream 封装了一个    DFSOutputStream, 用于处理与 datanode 和 namenode 通信。
    
    ③ 第三步,客户端写入数据,DFSOutputStream 数据切分成一个个数据包并写入到内部队列,称为数据队列(data queue)。数据队列由
    DataStreamer 负责处理,负责请求 namenode 分配新的数据块,通过选取一个恰当的 datanode 列表来存储数据复本。datanode 列表
    构成了一个管道(pipeline),这里假设复制等级(replication level)为 3, 因此管道中有 3 个 节点。
    
    ④ 第四步,DataStreamer 将数据包流向管道中第一个 datanode, 第一个 datanode 存储数据包并把数据包转发给管道中第二个
    datanode, 第二个 datanode 存储数据包并转发给管道中第三个(也是最后一个) datanode.
    
    ⑤ 第五步,DFSOutputStream 也维护一个内部的数据包队列来等待 datanode 确认,称为确认队列(ack queue)。一个确认数据包只有
    在管道中所有的 datanode 都已确认时才从队列中移除。
    
    
    如果任何 datanode 在数据写入时失败,则执行以下操作(对写入数据的客户端是透明的):首先,管道关闭,确认队列里的任何包都
    添加到数据队列的前端,这样失败节点下游的节点就不会丢失任何数据包。为在正常 datanode 上的当前数据块给定一个新的标识,并
    传递给 namenode,这样失败 datanode 上的部分数据块会在该 datanode 之后恢复时被删除。这个失败的 datanode 会从管道上移除,
    并且从两个正常的 datanode 建立一个新的管道。剩余的块的数据被写入到管道的两个正常的 datanode。namenode 注意到数据块是
    复本不足(under-replicated),并安排在另一节点创建更多的复本。后续的数据块按正常处理。
    
    
    在一个数据块写入时同时多个 datanode 失败也是可能的,但不太容易发生。只要有 dfs.namenode.replication.min个复本(默认值1)
    写入,写入就会成功。并且数据块会被异步复制到集群中直到达到目标复制因子(dfs.replication, 默认值为 3 )。
    
    ⑥ 第六步,当客户端结束数据写入,它在数据流上调用 close() 方法。这个操作刷新所有剩余的数据包到 datanode 管道并等待确认。
    
    
    ⑦ 第七步,联系 namenode 发送信号标识文件写入完成。 namenode 已经知道文件由哪些块组成(因 DataStreamer 向其请求块分配),
    因此它在返回成功之前只需要等待数据块进行最小数量的复制。
        
        
    复本放置 (REPLICA PLACEMENT):
    ----------------------------------------------------------------------------------------------------------------------
    namenode 如何选择 datanode 来存储块复本?这里在可靠性和写入带宽及读取带宽之间有个权衡。例如,把所有的复本放到一个节点上
    导致损失最低的写入带宽(因为复制管道运行于单一节点),但没有提供实际的冗余(如果节点失效了,块的数据丢失)。另外,跨机
    架读取带宽很高。另一种极端情况,跨数据中心放置复本可以提供最大限度的冗余,但带宽成本非常高。即便在同一数据中心,也有各
    种不同的放置策略。
    
    Hadoop 的默认策略是把第一个复本放置在与客户端相同的节点上(对于不在集群上运行的客户端,一个节点的选择是随机的,然而系统
    尽量不选取太满或太忙的节点)。第二个复本放置在与第一个不同的机架上(off-rack),随机选择。第三个复本放置在与第二个同一机架,
    但随机选择的不同节点上。更多的复本放置在集群上随机选取的节点上,然而系统会尽量避免在同一机架内放置过多的复本。
    
    一旦选择了复本位置,就根据网络拓扑建立一个 datanode 管道。
    ----------------------------------------------------------------------------------------------------------------------
    总体来说,这一策略给出了一个很好的平衡,可靠性(数据块存储在两个机架上),写入带宽(写入只经由了一个网络交换机),读取
    性能(可以从两个机架选择读取),以及数据块跨集群分布(客户端只需在本地机架写入一个数据块)。

        
        
6.3 一致性模型 (Coherency Model)
--------------------------------------------------------------------------------------------------------------------------
文件系统的一致性模型描述了读取或写入一个文件的数据可见性。HDFS 为性能牺牲了一些 POSIX 要求,因此一些操作在行为上可能与期望
的有所不同。

创建一个文件之后,它在文件系统名称空间是可见的,正如所期望的:

    Path p = new Path("p");
    fs.create(p);
    assertThat(fs.exists(p), is(true));

然而,任何写入文件的内容并不保证是可见的,即便流被刷新。因此,文件显示的长度为 0 :

    Path p = new Path("p");
    OutputStream out = fs.create(p);
    out.write("content".getBytes("UTF-8"));
    out.flush();
    assertThat(fs.getFileStatus(p).getLen(), is(0L));

一旦更多块数据写入之后,第一个块对新 reader 才可见。这对后续的块也是如此:总是当前写入的块对其他 reader 不可见。

HDFS 提供了一个强制所有的缓存都刷新到 datanode 的方法,通过 FSDataOutputStream 的 hflush() method。一个成功的 hflush()方法
返回之后,HDFS 确保文件中所有写入的数据到达写入管道的所有的 datanode, 并对所有的新 reader 可见:

    Path p = new Path("p");
    FSDataOutputStream out = fs.create(p);
    out.write("content".getBytes("UTF-8"));
    out.hflush();
    assertThat(fs.getFileStatus(p).getLen(), is(((long) "content".length())));
    

注意,hflush() 不确保 datanode 已将数据写入磁盘,只保证数据在 datanode 的内存中(例如,如果数据中心断电,数据会丢失)。为了
更好地保证数据写入磁盘,使用 hsync() 替代 hflush()。


hsync() 的行为很像 POSIX 中的 fsync() 系统调用,为一个文件描述符提交缓存的数据。例如,使用标准的 Java API 写入一个本地文件,
确保在刷新流并同步之后看到文件内容:

    FileOutputStream out = new FileOutputStream(localFile);
    out.write("content".getBytes("UTF-8"));
    out.flush(); // flush to operating system
    out.getFD().sync(); // sync to disk
    assertThat(localFile.length(), is(((long) "content".length())));

在 HDFS 中关闭一个文件也隐式执行 hflush():

    Path p = new Path("p");
    OutputStream out = fs.create(p);
    out.write("content".getBytes("UTF-8"));
    out.close();
    assertThat(fs.getFileStatus(p).getLen(), is(((long) "content".length())));
    
    对应用程序设计的重要性 (Consequences for application design):
    --------------------------------------------------------------------------------------------------------------------------
    这个一致模型和设计应用程序的具体方法息息相关。不调用 hflush() 或 hsync(),应该准备在客户端或系统失效时丢失一个块的数据。对
    很多应用来说,这是不可接受的。所以应该在合适的地方调用 hflush(),例如写入一定数量的记录或字节之后。尽管 hflush() 操作设计
    不会过多增加 HDFS 负担,它还是有些开销的(hsync() 开销更大),因此在数据的鲁棒性和吞吐量之间有所取舍。如何权衡与具体应用相关,
    通过选择不同的调用 hflush() 或 hsync() 方法的频率来衡量应用程序的性能,最终找到一个合适的值。


*
*
*

7 通过 distcp 并行复制 (Parallel Copying with distcp)
--------------------------------------------------------------------------------------------------------------------------    
前面着重介绍单线程访问的 HDFS 访问模型。可以对一个文件集合进行处理,通过指定一个文件通配符,但要高效的并发处理这些文件,就
需要自己写程序实现。

Hadoop 自带了一个有用的程序 distcp, 能并发地将数据复制到 Hadoop 文件系统,或从 Hadoop 中将数据复制出来。

distcp 用法之一是作为 hadoop fs -cp 的一个高效替代。例如,能将一个文件复制为另一个:

    % hadoop distcp file1 file2
    
也可以用它复制目录:

    % hadoop distcp dir1 dir2


如果 dir2 不存在,会被创建,并且 dir1 目录的内容会被复制到那里。如果指定多个源路径,所有的内容都会复制到目的地。


如果 dir2 已经存在,则 dir1 会被复制到它的下面,创建目录结构 dir2/dir1. 如果这不是想要的,可以提供 -overwrite 选项保持相同
的目录结构并强制文件覆盖。

也可以使用 -update 选项只更新改变了的文件。最好用一个例子说明,如果改变了一个在 dir1 子树下的文件,可以同步这种变化到 dir2 中:

    % hadoop distcp -update dir1 dir2


distcp 是作为一个 MapReduce 作业实现的,复制工作由并发运行在集群上的多个 map 执行的。没有 reducer。每个文件由一个单一的map
复制,并且 distcp 尝试给每个 map 大约相同数量的数据,通过将文件归并到大致相等的配额。默认,最多使用 20 个 map, 但可以通过给
distcp 指定 -m 参数改变。

--------------------------------------------------------------------------------------------------------------------------
一个非常典型的应用 distcp 的场景是在两个 HDFS 集群之间传输数据。例如,下面的命令在第二个集群上创建了一个第一个集群的 /foo
目录的备份:

    % hadoop distcp -update -delete -p hdfs://namenode1/foo hdfs://namenode2/foo


-delete 标记使 distcp 从目标上(destination)删除在源中(source)没有的任何文件或目录, -p 的意思是保留文件的状态属性,如许可权
限,块大小,复制数量。

可以不带任何参数运行 distcp 来查看精确的用法指南。

如果两个集群运行不兼容的 HDFS 版本,可以使用 webhdfs 协议在它们之间运行 distcp:

    % hadoop distcp webhdfs://namenode1:50070/foo webhdfs://namenode2:50070/foo
    
另外一个变体是使用 HttpFs 代理来作为 distcp 的源或目的地,好处是能够设置防火墙和带宽控制。


    保持 HDFS 集群的均衡 (Keeping an HDFS Cluster Balanced)
    ----------------------------------------------------------------------------------------------------------------------
    当向 HDFS 复制数据时,考虑集群的均衡是很重要的。 文件的数据块均匀地分布在集群上时 HDFS 工作得最好,因此,要确保 distcp
    不会破坏这种状态。例如如果指定了 -m 1, 一个单一的 map 来执行复制作业,它的意思是,不考虑速度变慢和未能有效利用集群资源,
    每个数据块的第一个复本都会保存到运行这个 map 的节点上(直到磁盘填满)。第二和第三个复本会分散到集群中,但这一节点是不均衡
    的。将 map 的数量设置为多于集群中的节点数量会避免这个问题。由于这个原因,最好使用默认的 20 个 map 开始运行 discp.
    
    然而,这并不能总是阻止集群变得不均衡。或许想限制 map 的数量以便另外一些节点用于其他作业。这种情况下,可以使用 balancer
    工具来在其后使集群上的块分布均匀。
      
    
    
    
    
参考:

    《Hadoop - The Definitive Guide - 4th Edition》 —— 2015. Tom White
       

    
    
    

       

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值