分布式文件系统:HDFS 核心原理

目录

基础架构

HDFS Client

NameNode和DataNode

fsimage和edits

SecondaryNameNode

高可用架构

JournalNode

联邦机制

副本机制

机架感知

安全模式

平衡策略

读写原理

读原理

写原理

删除恢复机制


HDFS相关内容:

  1. HDFS超有用的知识点
  2. Hadoop3新特性之HDFS纠删码
  3. HDFS常用管理命令
  4. HDFS的副本数量配置
  5. HDFS查看namenode状态
  6. 统计HDFS目录下文件行数及文件大小

基础架构

HDFS(Hadoop Distributed File System)是 Apache Hadoop 项目的一个子项目,Hadoop 非常适于存储大型数据, 其就是使用 HDFS 作为存储系统,而HDFS 使用多台计算机存储文件,并且提供统一的访问接口,像是访问一个普通文件系统一样使用分布式文件系统。作为大数据生态最重要的组件之一,HDFS充当着大数据时代的数据管理者的角色,为各种分布式计算组件提供用于处理的数据存储的能力。

HDFS 基础架构由四个角色组成:HDFS Client、NameNode、DataNode 和 SecondaryNameNode

HDFS Client

HDFS客户端提交读写命令到HDFS,它负责:

  • 文件切分文件上传 HDFS的时候,Client 将文件切分成一个个Block(数据块),然后进行存储。
  • 与 NameNode 交互获取文件真实的位置信息。
  • 与 DataNode 交互读取或写入数据。
  • Client 提供一些命令来管理 和访问HDFS,比如启动或者关闭HDFS。

NameNode和DataNode

NameNode 是HDFS的Master节点,它负责:

  • 管理 HDFS 的名称空间;
  • 管理数据块(Block)映射信息;
  • 配置副本策略;
  • 处理客户端读写请求;
  • 周期性的接收心跳和块的状态信息报告;

 DataNode 是HDFS的Slave节点,它负责:

  • 存储实际的数据块;
  • 执行数据块的读/写操作; 
  • 周期性向NameNode汇报心跳信息;
  • 周期性向NameNode汇报数据块信息;
  • 周期性向NameNode汇报缓存数据块信息;

fsimage和edits

在Hadoop集群当中,NameNode的所有元数据信息都保存在fsimage 与 edits 文件中, 这两个文件记录了所有的数据的元数据信息,当集群重启时NameNode首先会从fsimage和edits文件中将元数据信息加载到内存中,因此NameNode机器对内存的要求相对会比DataNode高很多,元数据信息的保存目录配置在了 hdfs-site.xml 中的这两个参数:dfs.namenode.name.dir,dfs.namenode.edits.dir。

edits

  • 存放了客户端最近一段时间的操作日志;
  • 客户端对 HDFS 进行写文件时的操作会先被记录在 edits 文件中;
  • edits 修改时元数据也会更新;

fsimage

  • NameNode 中关于元数据的镜像,一般称为检查点,fsimage 存放了一份比较完整的元数据信息;
  • 因为 fsimage 是 NameNode 的完整的镜像, 如果每次都加载到内存生成树状拓扑结构,这是非常耗内存和CPU, 所以一般开始时对NameNode 的操作都放在 edits 中;
  • fsimage 内包含了 NameNode 管理下的所有 DataNode 文件和文件 block 以及 block所在的 DataNode 的元数据信息;
  • 随着 edits 内容增大,就需要在一定策略下和 fsimage 合并;

由于集群重启时NameNode会重新加载fsimage和edits文件,fsimage文件加载起来很快,但edits文件内记录的都是操作日志,因此edits文件不能无限增长,否则重放日志很慢,会影响到集群启动的速度,因此edits文件和fsimage会定期进行合并。

SecondaryNameNode

SecondaryNameNode不是NameNode 的热备。当NameNode 挂掉的时候,它并不能马上替换 NameNode 并提供服务,而是作为一个辅助者分担NameNode的工作量。在后面的内容中介绍的HDFS的高可用中会介绍真正的NameNode热备机制。

  • 定期合并 fsimage和edits,并推送给NameNode,把 edits 控制在一个范围内。
  • 在紧急情况下,可辅助恢复 NameNode。

上面提到edits文件会根据一定策略和fsimage合并,主要由core-site.xml文件中的两个参数来管理:

<!-- 多久记录一次 HDFS 镜像, 默认 1小时 -->
<property>
  <name>fs.checkpoint.period</name>
  <value>3600</value>
</property>
<!-- 一次记录多大, 默认 64M -->
<property>
  <name>fs.checkpoint.size</name>
  <value>67108864</value>
</property>

当edits和fsimage文件合并策略触发时,合并流程如下:

  1. SNN(SecondaryNameNode)通知NN(NameNode)暂时切换将日志写到edits.new内;
  2. SNN通过GET请求将NN中的edits和fsimage文件加载到内存中;
  3. SNN将内存中的edits和fsimage合并生成新的fsimage.ckpt文件;
  4. SNN通过POST请求将生成的fsimage.ckpt文件传给NN;
  5. NN将收到的fsimage.ckpt替换旧的fsimage文件;
  6. NN将edits.new替换旧的edits文件;

高可用架构

JournalNode

上面我们介绍的都是HDFS的基础架构,从上面的内容中我们也可以看出,NameNode对于HDFS很重要,整个HDFS文件系统的元数据信息都由NameNode来管理,NameNode的可用性直接决定了Hadoop 的可用性,一旦NameNode进程不能工作了,就会影响整个集群的正常使用。因此在Hadoop2.x版本中加入了HDFS HA的特性,在典型的HA集群中,两台独立的机器被配置为NameNode。在工作集群中,NameNode机器中的一个处于Active状态,另一个处于Standby状态。Active NameNode负责群集中的所有客户端 操作,而Standby充当从服务器,Standby机器保持足够的状态以提供快速故障切换。

两个NameNode为了数据同步,会通过一组称作JournalNodes的独立进程进行相互通信。当Active 状态的NameNode的命名空间有任何修改时,会告知大部分的JournalNodes进程。Standby 状态的NameNode有能力读取JNs中的变更信息,并且一直监控edit log的变化,把变化应用于自己的命名空间。Standby 可以确保在集群出错时,命名空间状态已经完全同步了 ,以此达到快速故障切换。

HA架构下,SecondaryNameNode被JournalNode替代,实现两个NameNode之间的信息同步,由Zookeeper实现两个NameNode之间的高可用,相关的组件如下:

  • ZKFailoverController是基于Zookeeper的故障转移控制器,它负责控制NameNode的主备切换,ZKFailoverController会监测NameNode的健康状态,当发现Active NameNode出现异常时会通过Zookeeper进行一次新的选举,完成Active和Standby状态的切换;
  • HealthMonitor周期性调用NameNode的HAServiceProtocol RPC接口(monitorHealth 和 getServiceStatus),监控NameNode的健康状态并向ZKFailoverController反馈;
  • ActiveStandbyElector接收ZKFC的选举请求,通过Zookeeper自动完成主备选举,选举完成后回调ZKFailoverController的主备切换方法对NameNode进行Active和Standby状态的切换;
  • DataNodeNameNode包含了HDFS的元数据信息和数据块信息(blockmap),为了确保快速切换,Standby 状态的NameNode有必要知道集群中所有数据块的位置。为了做到这点,所有的DataNode必须配置两个NameNode的地址,同时发送数据块位置信息和心跳给他们两个。 
  • 共享存储系统(JournalNode)共享存储系统负责存储HDFS的元数据(EditsLog),Active NameNode(写入)和 Standby NameNode(读取)通过共享存储系统实现元数据同步,在主备切换过程中,新的Active NameNode必须确保元数据同步完成才能对外提供服务;对于HA集群而言,确保同一时刻只有一个NameNode处于active状态是至关重要的。否则,两个NameNode的数据状态就会产生分歧,可能丢失数据,或者产生错误的结果。为了保证这点,JNs必须确保同一时刻只有一个NameNode可以向自己写数据。

Hadoop3.x版本中的新特性允许2个以上的NameNode节点,该功能能够通过运行更多Standby NameNode来提供更高的容错性,满足一些部署的需求。比如,通过配置3个NameNode和5个JournalNode,集群能够满足允许两个节点故障的容错。

联邦机制

虽然HA模式保证了NameNode的高可用,但HDFS中其实最严重的问题就是小文件过多导致的NameNode维护的元数据信息过多,由此引起的性能下降的问题,因此无论在MapReduce还是Spark程序中都应当尽量避免产生过多小文件。同时HDFS也推出了NameNode的水平扩展方案:联邦机制(Federation)

HDFS联邦机制表示有多个NameNode,但和HA模式下的多个NameNode的意思不同,在HA模式下的NameNode是主备的概念,而联邦机制中的多NameNode类似分管,表示某个NameNode管理某块命名空间(namespace)内的元数据信息,将本来大量的元数据信息分散在多个NameNode上进行管理,它们之间相互独立不需要互相协助,各自分工,管理自己的区域。

 

同一个namespace下的block集合被称为Block Pool(存储池),因此在联邦机制下,每个NameNode对应一个Block Pool,对应到DataNode中就是一个个的Block Pool,分别有对应的ID,这时候在DataNode上就不仅仅存储一个Block Pool下的数据了,而是多个,且存储在DataNode的datadir路径里的名为BP-xx.xx的目录。

联邦机制的好处是,多个NameNode共有一个集群里的存储资源,且每个NameNode都可以单独对外提供服务,解决了单个Active NameNode的内存瓶颈问题。但联邦还是没有解决单点故障的问题,假如维护namespace为BP-003的NameNode宕机,则客户端也无法读取DataNode中的/BP-003的数据,因此当集群规模较大的时候,应采用HA+Federation的部署方案,即上图中每个Active NameNode都对应了一个Standby NameNode提升可用性。

副本机制

HDFS中的文件以Block块的形式存储在HDFS文件系统中,目的为:

  • 一个文件有可能大于集群中任意一个磁盘,引入块机制,可以很好的解决这个问题;
  • 使用块作为文件存储的逻辑单位可以简化存储子系统;
  • 块非常适合用于数据备份,提供数据容错能力;

在 Hadoop1.x 当中, 文件的 block 块默认大小是 64M,在Hadoop2.x中, 文件的 block 块大小默认是128M, block 块的大小和副本数量可以通过 hdfs-site.xml 当中的配置文件进行指定:

<!-- 块大小,单位为字节 -->
<property>
    <name>dfs.block.size</name>
    <value>134217728</value>
</property>

<!-- 副本数量,默认3个副本 -->
<property>
    <name>dfs.replication</name>
    <value>3</value>
</property>

注意

  • 当文件大于配置的块大小时会被拆分,如130M的文件会被拆分为两个块,一个128M另一个2M。
  • 当文件小于配置的块大小时不会拆分,如100M的文件不会拆分,只有一个100M的块。 

机架感知

分布式的集群通常包含非常多的机器,由于受到机架槽位和交换机网口的限制,通常大型的分布式集群都会跨好几个机架,由多个机架上的机器共同组成一个分布式集群。机架内的机器之间的网络速度通常都会高于跨机架机器之间的网络速度,并且机架之间机器的网络通信通常受到上层交换机间网络带宽的限制。

Hadoop在设计时考虑到数据的安全与高效,数据文件默认在HDFS上存放三份,存储策略为:

  1. 第一个block副本放在客户端所在的数据节点里(如果客户端不在集群范围内,则从整个集群中随机选择一个合适的数据节点来存放);
  2. 第二个副本放置在与第一个副本所在节点不同的机架内的数据节点上(随机选择);
  3. 第三个副本放置在不同机架的节点上;
  4. 如果还有其他副本,则随机放在其他节点上;

这样设计的好处是:

  • 当本地数据损坏时,节点可以从同一机架内的相邻节点拿到数据,速度肯定比从跨机架节点上拿数据要快;
  • 当整个机架的网络出现异常,也能保证在其它机架的节点上找到数据;

为了降低整体的带宽消耗和读取延时,HDFS会尽量让程序读取离它最近的节点上的副本,以节约带宽和提升性能。HDFS通过机架感知这一特性实现此功能。

在默认情况下,机架感知没有被启用,所有的机器hadoop都默认在同一个默认的机架下,名为 “/default-rack”,这种情况下,任何一台datanode机器,不管物理上是否属于同一个机架,都会被认为是在同一个机架下,此时,就很容易出现增添机架间网络负载的情况。因为此时hadoop集群的HDFS在选机器的时候,是随机选择的,也就是说,很有可能由于副本的随机分配导致大量的网络传输从而影响性能和集群的服务。

通过修改配置文件core-site.xml 中的参数 topology.script.file.name 开启机架感知,value指定为一个可执行程序,通常为一个脚本(根据入参IP返回该IP地址对应的datanode所在的rack)。

:网络拓扑如下图:

开启机架感知后,NameNode就可以画出上图所示的datanode网络拓扑图。D1,R1都是交换机,最底层是datanode。则H1的rackid=/D1/R1/H1,H1的parent是R1,R1的parent是D1。有了这些rackid信息就可以计算出任意两台datanode之间的距离。

distance(/D1/R1/H1,/D1/R1/H1)=0  相同的datanode
distance(/D1/R1/H1,/D1/R1/H2)=2  同一rack下的不同datanode
distance(/D1/R1/H1,/D1/R1/H4)=4  同一IDC下的不同datanode
distance(/D1/R1/H1,/D2/R3/H7)=6  不同IDC下的datanode

安全模式

安全模式是hadoop的一种保护机制,用于保证集群中的数据块的安全性。当集群启动的时候,会首先进入安全模式,当系统处于安全模式时会检查数据块的完整性。

假设我们设置的副本数(即参数dfs.replication)是5,那么在datanode上就应该有5个副本存在,假设只存在3个副本,那么比例就是3/5=0.6。在配置文件hdfs-default.xml中定义了一个最小的副本的副本率0.999,我们的副本率0.6明显小于0.99,因此系统会自动的复制副本到其他的dataNode,使得副本率不小于0.999.如果系统中有8个副本,超过我们设定的5个副本,那么系统也会删除多余的3个副本。

在安全模式下,系统会处于只读状态,NameNode不会处理任何数据块的复制和删除命令。DataNode会向NameNode上传他们数据块的列表,让NameNode得到数据块的位置信息,并对每个文件对应的数据块副本进行统计:

  • 当最小副本条件满足时,即:一定比例的数据块都到达最小副本数,系统会在30s后退出安全模式。
  • 当最小的副本条件未达到要求时,就会对副本数不足的数据块安排DataNode进行复制,直到达到最小的副本数。

注意:在启动一个刚刚格式化的HDFS时由于没有数据块,所以系统不会进入安全模式。

HDFS安全模式操作命令:

hdfs dfsadmin  -safemode  get #查看安全模式状态
hdfs dfsadmin  -safemode enter #进入安全模式
hdfs dfsadmin  -safemode leave #离开安全模式

安全模式相关参数在hdfs-site.xml 文件中配置:

<!-- 指定退出条件,需要达到最小副本数的数据块比例,默认是 0.999 -->
<property>
    <name>dfs.namenode.safemode.threshold-pct</name>
    <value>0.999f</value>
</property>

<!-- 指定系统退出安全模式时需要的延迟时间,单位为毫秒,默认为 30s -->
<property>
    <name>dfs.namenode.safemode.extension</name>
    <value>30000</value>
</property>

如果 NameNode 长时间处于安全模式,可能是因为 hdfs 的数据损坏过多。使用命令hadoop fsck / 检查 hdfs 文件分布的情况。

平衡策略

在HDFS的DN节点间的数据不平衡情况下,尤其在新增和下架节点、或者人为干预副本数量的时候,会大大的影响数据读取的性能,降低任务的执行速度甚至崩溃,因此HDFS有一个组件叫做Balancer,使用该组件可以保证每个节点的数据均衡分布

#开始数据平衡:
#用默认的10%的阈值启动balancer
start-balancer.sh    
#或指定3%的阈值启动balancer
hdfs dfs balancer -threshold 3
start-balancer.sh -threshold 3    

#停止数据平衡:
stop-balancer.sh

hdfs balancer
      [-threshold <threshold>]
      [-policy <policy>]
      [-exclude [-f <hosts-file> | <comma-separated list of hosts>]]
      [-include [-f <hosts-file> | <comma-separated list of hosts>]]
      [-idleiterations <idleiterations>]

-threshold  10      #集群平衡的条件,datanode间磁盘使用率相差阈值,区间选择:0~100。Threshold参数为集群是否处于均衡状态设置了一个目标
-policy datanode    #默认为datanode,datanode级别的平衡策略
-exclude  -f  /tmp/ip1.txt  #默认为空,指定该部分ip不参与balance, -f:指定输入为文件
-include  -f  /tmp/ip2.txt  #默认为空,只允许该部分ip参与balance,-f:指定输入为文件
-idleiterations  5          #最大迭代次数,默认为 5

可选的配置参数如下:

#并行移动的block数量,默认5
dfs.datanode.balance.max.concurrent.moves

#Balance工具所占用的带宽,默认1048576(1MB)
dfs.datanode.balance.bandwidthPerSec

#用于执行block移动的线程池大小,默认1000
dfs.balancer.moverThreads

#每次balance进行迭代的过程最大移动数据量,默认10737418240(10GB)
dfs.balancer.max-size-to-move

#获取block的数量,默认2147483648(2GB)
dfs.balancer.getBlocks.size

#用来平衡的最小block大小,默认10485760(10MB)
dfs.balancer.getBlocks.minblock-size

平衡算法根据各Datanode的使用情况,将集群中的节点分为四类:过度闲置、平均值以下、平均值以上、过度使用

然后根据划分的角色进行配对:

  • 过度使用--> 过度闲置
  • 过度使用-->平均值下
  • 平均值上-->过度闲置

为了保证HDFS数据安全性,数据块移动策略如下:

  1. 源DataNode的存储类型和目的DataNode的存储类型一致;
  2. 该block的副本未被安排;
  3. 目的DataNode不包含同样的副本;
  4. 移动之后该机架上的block不会减少;

根据角色配对以及移动策略,Balancer数据均衡流程为:

  1. 与NameNode交互,获取DataNode磁盘使用情况;
  2. 根据数据分布情况对DataNode进行角色划分并配对;
  3. 根据移动策略从源DataNode移动block到目标DataNode,并删除源DataNode上的block;
  4. 获取移动结果,并继续移动其他数据块,直到没有数据可以移动或者HDFS集群以及达到了平衡的标准为止,然后向NameNode提交更新后的DataNode信息;

当触发下面的条件时,Balancer会自动退出:

  1. 集群已达到均衡状态;
  2. 没有block能被移动;
  3. 连续5次(参数:idleiterations) 迭代移动没有任何一个block被移动;
  4. 当与NameNode交互时出现了IOException;
  5. 另一个Balancer进程在运行中。

读写原理

读原理

客户端向HDFS发送读取文件的请求完整流程如下:

  1. 客户端向NN提交读请求;
  2. NN进行权限检查、获取文件块列表:{blk_a_1: dn1,dn2,dn4 ;blk_a_2: dn2,dn3,dn4}
  3. NN根据机架感知将距离客户端最近的文件块所在的DN列表返回给客户端:{blk_a_1:dn1, blk_a_2:dn2}
  4. 客户端和每个block所在的DN建立管道;
  5. 客户端读取数据,以数据包packet(64k)进行传输;
  6. 客户端将接收到的block合并为一个完整的文件;

NameNode会视情况返回文件的部分或者全部block列表,对于每个block,NameNode 都会返回含有该 block 副本的 DataNode 地址; 这些返回的 DN 地址,会按照集群拓扑结构得出 DataNode 与客户端的距离,然后进行排序,排序两个规则:网络拓扑结构内距离Client 近的排靠前;心跳机制中超时汇报的 DN 状态为 STALE,这样的排靠后;以此来提升网络传输的效率。

客户端选取排序靠前的 DataNode 来读取 block,如果客户端本身就是DataNode,那么将从本地直接获取数据(短路读取)。

每读取完一个 block 都会进行 checksum 验证,如果读取 DataNode 时出现错误,客户端会通知 NameNode,然后再从下一个拥有该block 副本的DataNode 继续读。

写原理

客户端向HDFS发送上传文件的请求完整流程如下:

  1. 客户端向NameNode提交写请求;
  2. NN进行权限检查、判断是否满足写条件;
  3. NN返回信息:可以上传;并将写操作记录在edits日志内。
  4. 客户端根据HDFS的块策略将文件切分为n个block文件块;
  5. 请求上传第一个文件块blk_a_1;
  6. 根据DN上的block信息和机架感知,选出可以上传的DN列表:(dn1,dn2,dn4);
  7. NN返回可上传的DN列表(dn1,dn2,dn4);
  8. 客户端和DN建立数据传输管道,上传的DN之间也建立管道;
  9. 客户端向DN以数据包packet(64k)传递数据,dn1收到一个packet会传给dn2,dn2收到会传给dn4,每传一个packet会放入一个应答队列等待应答;
  10. DN将收到的packet数据包进行缓存;
  11. 在管道的反方向上, DN逐个发送 ack(命令正确应答), 最终由管道中第一个DN节点dn1将ack发送给客户端;
  12. 当文件块block的packet传输完成则将缓存的临时文件放置在指定的HDFS上传路径;
  13. 继续上传其余的block文件块;
  14. 所有block上传完毕后返回完成信号给NN;

删除恢复机制

当从HDFS中删除某个文件时,这个文件并不会立刻从HDFS中删除,而是将这个文件重命名并转移到回收站 /trash目录,只要这个文件还在/trash目录下,该文件就可以迅速被恢复。回收站的位置在HDFS上的/user/$USER/.Trash/Current/ 

文件在/trash目录中存放的时间默认为6个小时,当超过这个时间时,NN就会将文件从名称空间中删除;

删除文件会使得该文件相关的数据块被释放;注意:从用户删除文件到HDFS空闲空间的增加之间会有一定时间的延迟;

通过修改hdfs-site.xml文件中下面的配置可以修改回收站过期的时间:

#时间单位是秒
<property>
    <name>fs.trash.interval</name>
    <value>1440</value>
</property>

希望本文对你有帮助,请点个赞鼓励一下作者吧~ 谢谢!

HDFS(Hadoop分布式文件系统)是Hadoop生态系统的关键组件之一,它是一个分布式文件系统,专门用于存储和处理大规模数据集。下面是HDFS核心概念和工作原理的概述: 1. 核心概念: - 文件:HDFS将数据组织成文件的形式进行存储,文件被分割成一个或多个数据块(Block)进行存储。 - 数据块(Block):HDFS将文件分割为固定大小的数据块进行存储,默认大小为128MB,每个数据块都被多个副本复制到不同的节点上以实现数据冗余和容错性。 - 命名空间(Namespace):HDFS使用类似UNIX文件系统的目录结构组织文件,并使用唯一的路径名来标识文件。 - NameNode:NameNode是HDFS的主节点,负责管理文件系统的命名空间和元数据信息,包括文件和目录的创建、删除、重命名等操作。 - DataNode:DataNode是HDFS的工作节点,负责存储和管理实际的数据块,包括数据块的读取、写入、复制等操作。 2. 工作原理: - 数据分布和冗余:当客户端向HDFS写入数据时,数据被分割成多个数据块,并按照一定的策略将数据块分配到不同的DataNode上进行存储。每个数据块有多个副本,这些副本分布在不同的机架和节点上以实现数据的冗余和容错性。 - 元数据管理:NameNode负责管理文件系统的命名空间和元数据信息,包括文件和目录的创建、删除、重命名等操作。元数据信息存储在内存,通过写入持久化的操作日志(Edit Log)和文件系统镜像(FsImage)来实现持久化和恢复。 - 数据读取和写入:当客户端读取数据时,它首先向NameNode获取所需数据块的位置信息,然后直接与DataNode进行交互进行数据读取。对于写入操作,客户端先将数据写入到一个DataNode,然后DataNode之间通过管道(Pipeline)将数据复制到其他副本所在的DataNode。 - 容错和故障恢复:HDFS通过周期性地向NameNode报告心跳来维护DataNode的存活状态。当一个DataNode失效时,NameNode将该DataNode上的数据块重新复制到其他存活的DataNode上以保证数据的冗余和可靠性。 HDFS的设计目标是适用于大规模数据集的存储和处理,具有高容错性、高吞吐量和可扩展性等特点。通过以上的核心概念和工作原理HDFS能够提供高效的数据存储和可靠的数据访问服务。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王义凯_Rick

遇见即是缘,路过就给个评论吧~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值