3️⃣Hadoop

1. Hadoop

1.1. Hadoop是什么?

  • 分布式系统基础架构
  • 主要解决海量数据的存储和海量数据的分析计算问题。
  • Hadoop生态圈

1.2. Hadoop优势

  • 高可靠性:Hadoop底层维护多个数据副本,所以即使Hadoop某个计算元素或存储出现故障,也不会导致数据的丢失。【对数据存储的可靠性(将数据多备份几份)】
  • 高扩展性:在集群间分配任务数据,可方便的扩展数千计的节点。【如果我机器的资源不够,可以买机器加资源(同样也可以减)】
  • 高效性:在MApReduce的思想下,Hadoop是并行工作的,以加快任务处理速度。【对数据的计算(把庞大的数据分给若干个机器同时计算,执行完之后汇总)】
  • 高容错性:能够自动将失败的任务重新分配。【把庞大的数据分给若干个机器同时计算,有一台机器执行失败,Hadoop内部会把失败的计算重启重新计算,执行完之后汇总】

1.3. 大数据技术生态体系

1.4. 推荐系统框架图

1.5. 基于Hadoop3.x版本的组成

  • MApReduce(分布式计算组件):对数据进行处理,使用统计分析等手段得到需要的结果。
  • Yarn(分布式资源调度):用来调度资源给MApReduce分配和管理运行资源。
  • HDFS(分布式文件系统):可以在多台服务器上构建存储集群,存储海量的数据。

2. HDFS(分布式文件系统)架构

2.1. 是什么?

  • HDFS分布式文件系统,用于存储文件,通过目录树来定位文件位置(文件系统)。有很多台服务器联合起来实现其功能,集群中的服务器有各自的角色(分布式的)。

2.2. 使用场景

  • 适合一次写入,多次读出的场景,但不支持文件的修改,只能在文件末尾追加内容。适合用来做数据分析。

2.3. 优缺点

2.3.1. 优点

  • 高容错性
    • 数据自动保存多个副本,通过增加副本的形式,提高容错性
    • 某一个副本丢失后,他可以自动恢复。
  • 适合处理大数据(杀鸡焉用牛刀)
    • 数据规模:能处理数据规模达到GB,TB,甚至PB级别的数据;
    • 文件规模:能够处理百万规模以上的文件数量,数量相当大。
  • 可构建在廉价机器上,通过多副本机制,提高可靠性。

2.3.2. 缺点

  • 不适合低延时数据的访问,比如:毫秒级的存储数据,是做不到的。【MySQL可以做到毫秒级的存读(适合小数据)】
  • 无法高效的对大量小文件进行存储。
    • 存储大量小文件的话,它占用NameNode大量的内存来存储文件目录和块信息,这样是不可取的,因为NameNode的内存总是有限的;
    • 小文件存储的寻址时间会超过读取时间,他违反了HDFS设计目标。
  • 不支持并发写入,文件随机修改。
    • 一个文件只能有一个写,不允许多个线程同时写;
    • 仅支持数据append(追加),不支持文件的随机修改。

2.4. HDFS组成(角色)

  • NameNode(nn):名称节点(HDFS大哥)
    • 就是Master,是一个主管,管理者。
    • 配置副本策略。【传进来以后会存几个副本,存到哪里】
    • 处理客户端读写请求。【客户端先要与NameNode进行交互】
    • 管理数据块(Block)映射信息。【文件的每个块存在哪个位置,每个文件对应哪个块】
    • 存储文件的元数据(描述数据的数据),如:文件名,文件目录结构,文件属性(生成时间,副本数,文件权限),以及每个文件的块列表块所在的DataNode等。【存储的是文件的描述信息(相当于书本的目录)】
    • HDFS系统的主角色,是一个独立的进程。
    • 负责管理HDFS整个文件系统。
    • 负责管理DataNode
  • DataNode(dn):数据节点(HDFS小弟)
    • Slave,NameNode下达命令,DataNode执行实际的操作。
    • 存储实际的数据块。
    • 执行数据块的读写操作。【NameNode会告诉客户端应该去找哪一个DataNode去做读和写】
    • 主要负责数据的存储,即存入数据和取出数据
    • HDFS系统的从角色,是一个独立的进程。
    • 在本地文件系统存储文件块数据【以块为单位进行存储(默认128M)】,以及块数据的校验和【对数据做检查防止被破坏(会得到一个值)】。【存储的是真实的数据(相当于书本的内容)】
  • SecondaryNameNode(snn):主角色辅助角色(NameNode的秘书)
    • 并非NameNode的热备【热备份(随时替代)】,当NameNode挂掉的时候,它并不能马上替换NameNode并提供服务。
    • 辅助NameNode,分担其工作量,比如:定期合并Fsimage和Edits,并推送给NameNode;
    • 在紧急情况下,可辅助恢复NameNode。【在紧急情况下帮助NameNode恢复】
    • 每隔一段时间对NameNode元数据备份。
    • NameNode的辅助,是一个独立的进程。
    • 主要帮助NameNode完成元数据整理工作(打杂)
  • Client:客户端【用户操作的入口】(不重要)
    • 文件切分,文件上传HDFS的时候,Client将文件切分成一个一个Block,然后进行上传;
    • 与NameNode交互,获取文件的位置信息。
    • 与DataNode交互,读取或者写入数据。
    • Client提供一些命令来管理HDFS,比如NameNode格式化。
    • Client可以通过一些命令来访问HDFS,比如对HDFS增删改查操作

2.5. 文件块的大小(存储数据的单位)

  • HDFS中的文件在物理上是分块存储(Block),块的大小可以通过配置参数(dis.blocksize)来规定的,默认的是128MB。

思考:为什么块的大小不能设置太小,也不能设置台大?

  • HDFS的快设置太小,会增加寻址时间,程序一直在找块的开始的位置。(5个块找一个,和500个块找一个)
  • 如果块设置太大,从磁盘传输数据的时间会明显大于定位这个块开始位置所需的时间,导致程序在处理这块数据时,会非常慢。(块大传输速率慢)
  • 总结:HDFS块的大小设置主要取决于磁盘的传输速率。

2.6. HDFS写数据流程

  1. 客户端通过 Distributed FileSystem 模块向 NameNode 请求上传文件,NameNode 检查目标文件是否已存在,父目录是否存在。
  2. NameNode 返回是否可以上传。
  3. 客户端请求第一个 Block 上传到哪几个 DataNode 服务器上。
  4. NameNode 返回 3 个 DataNode 节点,分别为 dn1、dn2、dn3。
  5. 客户端通过 FSDataOutputStream 模块请求 dn1 上传数据,dn1 收到请求会继续调用 dn2,然后 dn2 调用 dn3,将这个通信管道建立完成。
  6. dn1、dn2、dn3 逐级应答客户端。
  7. 客户端开始往 dn1 上传第一个 Block(先从磁盘读取数据放到一个本地内存缓存), 以 Packet 为单位,dn1 收到一个 Packet 就会传给 dn2,dn2 传给 dn3;dn1 每传一个 packet 会放入一个应答队列等待应答。
  8. 当一个 Block 传输完成之后,客户端再次请求 NameNode 上传第二个 Block 的服务 器。(重复执行 3-7 步)。

2.7. 机架感知

2.8. HDFS读数据流程

  1. 客户端通过DFS( DistributedFileSystem )向NN( NameNode )请求下载文件,NN通过查询元数据,找到文件块所在的DN( DataNode )地址。
  2. 挑选一台DN(就近原则,然后随机)服务器,请求读取数据。
  3. DN开始传输数据给客户端(从磁盘里面读取数据输入流,以Packet为单位来做校验)。
  4. 客户端以Packet为单位接收,先在本地缓存,然后写入目标文件。

2.9. NameNode和SecondaryNameNode工作机制

2.9.1. 思考:NN中的元数据是存储在哪里的?内存?磁盘?

  • 如果考虑数据的可靠性,需要将元数据维护到磁盘,带来的问题是对磁盘的数据修改低。
  • 如果考虑数据访问还让修改的效率,需要将数据维护到内存,带来的问题是数据不可靠。
  • 综合考虑:磁盘+内存。

2.9.2. 如何保证解决维护到磁盘和内存带来的问题。

  • 方案:在内存中维护元数据,且在磁盘中通过Fsimage(镜像文件)来维护元数据,并且通过edits(编辑日志)文件记录对元数据的修改操作,记录的方式为文件末尾追加操作。

2.9.3. edits文件中记录的操作越来越多怎么办?

  • 定时的将Fsimage文件和edits文件进行合并。

2.9.4. 谁来做合并的事?

  • SecondaryNameNode来帮助NameNode完成合并的事。
  • 大概过程就是当满足要合并的条件后SecondaryNameNode会将NameNode中的Fsimage和当前正在使用的edits文件拉取过来。将Fsimage和edits在SecondaryNameNode的内存中进行合并,合并完成后生成一个新的Fsimage文件,再将新的Fsimage文件推送给NameNode,替换NameNode中旧的Fsimage文件。

2.9.5. NameNode工作机制

  • 第一阶段:NameNode启动
    • 第一次启动NameNode格式化后,创建Fsimage和Edits文件,如果不是第一次启动直接加载编辑日志和镜像文件到内存。
    • 客户端对元数据进行增删改的请求。
    • NameNode记录操作日志,更新滚动日志。
    • NameNode在内存中对元数据进行增删改。
  • 第二阶段: Secondary NameNode 工作
    • Secondary NameNode 询问NameNode是否需要Checkpoint,直接带回NameNode是否检查结果。
    • Secondary NameNode 请求执行Checkpoint。
    • NameNode滚动正在写的edits日志。
    • 将滚动前的编辑日志和镜像文件拷贝到 Secondary NameNode 。
    • Secondary NameNode 加载编辑日志和镜像文件到内存,并合并。
    • 生成新的镜像文件fsimage.chkpoint。
    • 拷贝到fsimage.chkpoint到NameNode。

2.10. DataNode

2.10.1. DataNode工作机制

  • 一个数据块在DataNode上以文件形式存储在磁盘上,包括另个文件,一个是数据本身,一个是元数据包括数据块的长度,块数据的校验和,以及时间戳。
  • DataNode启动后向NameNode注册,通过后,周期性(6小时)的向NameNode上报所有的块信息。
  • 心跳是每3秒一次,心跳返回结果带有NameNode给该DataNode的命令如复制块数据到另一台机器,或删除某个数据块,如果超过10分钟没有收到某个DataNode心跳,则认为该节点不可用。
  • 集群运行可以安全加入和退出一些机器。

2.10.2. DataNode掉线时

2.10.3. 数据完整性

  • 当 DataNode 读取 Block 的时候,它会计算 CheckSum。
  • 如果计算后的 CheckSum,与 Block 创建时值不一样,说明 Block 已经损坏。
  • Client 读取其他 DataNode 上的 Block。
  • 常见的校验算法 crc(32),md5(128),sha1(160)
  • DataNode 在其文件创建后周期验证 CheckSum。

3. Hadoop-HA(高可用)

3.1. 是什么?

  • 所谓的HA(High Availablity),即高可用(7*24小时不中断服务)
  • 实现高可用最关键的策略是消除单点故障,HA严格来说应该分成各个组件的HA机制;HDFS的HA和Yarn的HA
  • Hadoop2.0之前,在HDFS集群中NameNode存在单点故障
  • NameNode主要在以下两个方面影响HDFS集群:
    • NameNode机器发生意外,如宕机,集群将无法使用,知道管理员重启
    • NameNode机器需要升级,包括软件,硬件升级,此时集群也将无法使用
    • HDFS HA功能通过配置多台Active/Standby角色的NameNode实现在集群中对NameNode的热备来解决上述问题,如果出现故障,如机器崩溃或机器需要升级维护,这时可通过此种方式将NameNode很快的切换到另外一台机器。

3.2. HDFS-HA工作机制

  • 通过多NameNode消除单点故障

3.3. HDFS-HA自动故障转移

3.3.1. HA的自动故障转移依赖于Zookeeper的以下功能:

  • 故障检测:
    • 集群中的每个NameNode咋Zookeeper中维护了一个持久会话,如果机器崩溃,Zookeeper中的会话将终止,Zookeeper通知另一个NameNode需要触发故障转移
  • 现役NameNode选择:
    • Zookeeper提供了一个简单的机制用于唯一的选择一个节点为Active状态,如果目前现役NameNode崩溃,另一个节点可能从Zookeeper获得特殊的排外锁以表明他应该成为现役NameNode
  • ZKFC
    • ZKFC是自动故障转移中的另一个新组件,是Zookeeper的客户端,也监视和管理NameNode的状态,每个运行NameNode的主机也运行了一个ZKFC进程,
    • ZKFC负责:
      • 健康监测:ZKFC使用一个健康检查命令定期地ping与之在相同主机的NameNode,只要该NameNode及时地回复健康状态,ZKFC认为该节点崩溃,冻结或进入不健康状态,健康监测器标识该节点为非健康的,(询问NameNode是否还活着)
      • 基于 ZooKeeper 的选择:如果本地 NameNode 是健康的,且ZKFC 发现没有其它的节点当前持有 znode锁,它将为自己获取该锁。如果成功,则它已经赢得了选择,并负责运行故障转移进程以使它的本地 NameNode 为Active。故障转移进程与前面描述的手动故障转移相似,首先如果必要保护之前的现役 NameNode,然后本地 NameNode 转换为 Active 状态。
      • ZooKeeper 会话管理:当本地 NameNode 是健康的,ZKFC 保持一个在 ZooKeeper 中打开的会话。如果本地NameNode 处于active 状态,ZKFC 也保持一个特殊的 znode 锁,该锁使用了 ZooKeeper 对短暂节点的支持,如果会话终止,锁节点将自动删除。

3.4. HDFS-HA故障转移机制

  • 同时出现两个Active状态namenode的术语叫脑裂brain split.
  • 防止脑裂的两种方式:
    • ssh发送kill指令
    • 调用用户自定义脚本程序

3.4.1. 脑裂brain-split

3.4.1.1. 是什么?
  • 脑裂是Hadoop2.X版本后出现的全新问题,实际运行过程中很有可能出现两个namenode同时服务于整个集群的情 况,这种情况称之为脑裂。
3.4.1.2. 原因
  • 脑裂通常发生在主从namenode切换时,由于ActiveNameNode的网络延迟、设备故障等问题,另一个NameNode 会认为活跃的NameNode成为失效状态,此时StandbyNameNode会转换成活跃状态,此时集群中将会出现两个活 跃的namenode。因此,可能出现的因素有网络延迟、心跳故障、设备故障等。
3.4.1.3. 脑裂场景
  • NameNode 可能会出现这种情况,NameNode 在垃圾回收(GC)时,可能会在长时间内整个系统无响应
  • zkfc客户端也就无法向 zk 写入心跳信息,这样的话可能会导致临时节点掉线,备 NameNode 会切换到 Active 状态
  • 这种情况可能会导致整个集群会有同时有两个Active NameNode

3.5. 现有集群存在问题

3.5.1. HDFS中如果NameNode故障后,集群是否可以正常对外提供服务?如果不能,如何解决?

  • 中心化配置方式的集群都存在一个严重的问题,单点故障
  • SecondaryNameNode是否可以解决该问题?
    • 不能,SecondaryNameNode主要是帮助NameNode分担一些工作,例如合并镜像文件和编辑日志,在一些紧急情况下,例如NameNode故障后,所维护的元数据也丢失,SecondaryNameNode可以帮助NameNode恢复数据,最后还是将故障的NameNode重新启动,集群继续对外提供服务,在NameNode故障到重启的过程中,集群是不能对外提供服务的。
    • 实际上,在HA集群中,不需要再搭建SecondaryNameNode,StandbyNameNode同样拥有SecondaryNameNode的功能,
  • 高可用HA(不间断对外提供服务)
    • 高可用的思想就是搭建多NameNode架构,Hadoop2.x开始支持HA架构,但是只支持最多两个NameNode,Hadoop3.x支持两个以上的NameNode
  • 待解决的一些问题
    • 多个NameNode的状态,一个active,多个standby,如何决定?
    • 多个NameNode元数据的同步
      • 通过JournalNode做共享存储,ActiveNameNode将操作写到JournalNode中的编辑日志,StandbyNameNode从JournalNode中编辑日志读取,在内存中重构,就能保证ActiveNameNode和StandbyNameNode数据的同步

4. MApReduce(分布式计算组件)

4.1. 是什么?

  • 一个分布式运算程序的编辑框架。
  • 核心功能是将用户编写的业务逻辑代码自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。

4.2. MApReduce架构概述

  • MapReduce将计算过程分为两个阶段:MapReduce
  • Map(分):
    •  Map阶段并行处理输入数据;
    • 将数据分到多台机器进行计算;
    • Map功能接口提供了“分散”的功能,由服务器分布式对数据进行处理。
  • Reduce(合)
    • Reduce阶段对Map结果进行汇总;
    • 将多台机器中运算的结果统一汇总;
    • Reduce功能接口提供了“汇总(聚合)”的功能,将分布式的处理结果汇总统计。

4.3. 优缺点

4.3.1. 优点:

  • 易于编程
    • 简单的实现一些接口,就可以完成一个分布式程序。
  • 良好的扩展性(基于Hadoop的扩展性)
    • 计算资源不足,通过增加机器来扩展计算能力。
  • 高容错性
    • 假设其中一台机器挂掉了,他可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败。
  • 适合PB即别以上海量数据的离线处理
    • 可以实现上千台服务器集群并发工作,提供数据处理能力。

4.3.2. 缺点:

  • 不擅长实时计算
    • MapReduce 无法像 MySQL 一样,在毫秒或者秒级内返回结果。
  • 不擅长流式(动态)计算
    • 流式计算的输入数据是动态的,而 MapReduce 的输入数据集是静态的,不能动态变化。 这是因为 MapReduce 自身的设计特点决定了数据源必须是静态的。
  • 不擅长 DAG(有向无环图)计算
    • 多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下, MapReduce 并不是不能做,而是使用后,每个 MapReduce 作业的输出结果都会写入到磁盘, 会造成大量的磁盘 IO,导致性能非常的低下

4.4. 核心思想

  • 需求:统计其中每一个单词出现的总次数(查询结果:a-p一个文件,q-z一个文件)。

  • MApReduce运算程序一般需要分成2个阶段:Map阶段和Reduce阶段
  • Map阶段的并发MapTask,完全并行运行,互补相干
  • Reduce阶段的并发Reduce Task,完全互不相干。但是他们的数据完全依赖于上一个阶段的所有MapTask并发实例的输出
  • MApReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MApReduce程序,串行运行。

4.5. MapReduce 进程

  • 一个完整的 MapReduce 程序在分布式运行时有三类实例进程:
    • MrAppMaster:负责整个程序的过程调度及状态协调。
    • MapTask:负责 Map 阶段的整个数据处理流程。
    • ReduceTask:负责 Reduce 阶段的整个数据处理流程。

4.6. 常用数据序列化类型

Java类型

Hadoop Writable类型

Boolean

BooleanWritable

Byte

ByteWritable

Int

IntWritable

Float

FloatWritable

Long

LongWritable

Double

DoubleWritable

String

Text

Map

MapWritable

Array

ArrayWritable

Null

NullWritable

4.7. MApReduce编程规范

  • 用户编写的程序分成三个部分:Mapper,Reducer和Driver(驱动,写main方法的)。
  • Mapper阶段:
    • 用户自定义的Mapper要集成自己的父类。
    • Mapper的输入数据是KV对的形式(KV类型可自定义)。
    • Mapper中的业务逻辑写在map()方法中。
    • Mapper的输出数据是KV对的形式(KV类型可自定义)。
    • map()方法(MapTask进程)对每一个<K,V>调用一次。
  • Reducer阶段:
    • 用户自定义的Reducer要继承自己的父类。
    • Reducer的输入数据类型对应Mapper的输出数据类型,也是KV。
    • Reducer的业务逻辑写在reduce()方法中。
    • ReduceTask进程对每一组相同K的<k,v>组调用一次reduce()方法。
  • Driver阶段:
    • 相当于Yarn集群的客户端,用于提交我们整个程序到Yarn集群,提交的是封装了MApReduce程序相关运行参数的job对象。

4.8. Hadoop序列化

4.8.1. 什么是序列化?

  • 序列化就是把内存中的对象转换成字节序列(或其他数据传输协议)以便于存储到磁盘汇(持久化)和网络传输。(对象到磁盘)
  • 反序列化就是将收到的字节序列(或其他数据传输协议)或者是磁盘持久化数据,转换成内存中的对象。(磁盘到对象)

4.8.2. 为什么要序列化

  • 序列化可以存储“活的” 对象,可以将“活的”对象发送到远程计算机。

4.8.3. 为什么不用 Java 的序列化

  • Java 的序列化是重量级序列化框架, 一个对象被序列化后,会附带很多额外的信息, 不便于在网络中高效传输。

4.8.4. Hadoop 序列化特点:

  • 紧凑 :高效使用存储空间。
  • 快速:读写数据的额外开销小。
  • 互操作:支持多语言的交互

4.8.5. Java和Hadoop序列化

Java序列化:

  • Java类需要实现Serializable接口,并提供serialVersionUID。
  • 再通过对象流ObjectInputStream的readObject方法和ObjectOutputStream的writeObject方法。
  • 实现序列化和反序列化的过程

Hadoop序列化:

  • 自定义的类需要实现Writable接口
  • 提供无参数构造器(反序列化时会通过反射的方式调用无参数构造器来创建对象)
  • 重写write方法实现序列化的过程
  • 重写readFields方法实现反序列化的过程
  • 如果自定的类需要作为输出的key或者value来使用的话,一般建议重写toString方法,因为Hadoop会默认调用对象的toString方法进行输出。

4.9. MApReduce框架原理

4.9.1. 切片

4.9.1.1. 什么是切片?
  • 数据块:Block是HDFS物理上把数据分成一块一块。
  • 数据切片:只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。实际上每个切片就是记录了一下我要读取哪文件的哪一部分数据。
4.9.1.2. 切片与MapTask的关系
  • 有多少个切片,就需要启动多少个MapTAsk来处理
  • 一个MapTAsk处理一个切片的数据。
4.9.1.3. 切片的大小
  • 切片的大小默认情况下=块的大小。

4.9.2. InputFormat数据输入

4.9.2.1. 重要方法
  • getSplit():生成切片
  • createRecordReader():创建RecordReader对象,负责数据的读取
4.9.2.2. 子抽象类 FileInputFormat
  • getSplits():默认的切片规则的实现
  • isSplitable():判断一个文件是否可以切片
4.9.2.3. 具体的实现类
  • TextInputFormat:是默认使用的InputFormat类
    • createkecordReader():创建LineRecordReader对象
    • isSplitable():重写了该方法,对各种压缩文件进行了判断是否可切分
    • 切片的规则用的是FileInputFormat中的实现
  • combineTextInputFormat:解决小文件场景生成过多切片的问题

4.9.3. Shuffle(洗牌)机制

4.9.3.1. shuffle的位置
  • Map 方法之后,Reduce 方法之前的数据处理过程称之为 Shuffle。
4.9.3.2. shuffle的具体划分

  • map--->sort(map阶段)--->copy(Reduce阶段)--->sort(Reduce阶段)--->Reduce
4.9.3.3. 分区总结
  • 如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
  • 如果1 <ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
  • 如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个 ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
  • 分区号必须从零开始,逐一累加。
4.9.3.4. 排序分类
  • 部分排序
    • MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
  • 全排序 (全部的数据做统一的排序)
    • 最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在 处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
  • 辅助排序:(GroupingComparator分组)
    • 在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部 字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
  • 二次排序
    • 在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。

4.9.4. 排序

4.9.4.1. 排序的本质是什么?
  • 比较
4.9.4.2. 比较相关的类和接口:
  • comparable:比较接口,通过comparTo方法定义比较规则
  • comparator:临时比较器,通过compare方法定义比较规则
4.9.4.3. Hadoop比较使用的类和接口
  • WritableComparable:支持序列化和比较的接口
  • WritableComparator:比较器

4.9.5. Combiner合并

4.9.5.1. Combiner介绍
  • Combiner是MR程序中Mapper和Reducer之外的一种组件。
  • Combiner组件的父类就是Reducer
  • Combiner和Reducer的区别在于运行的位置

                Combiner是在每一个MapTask所在的节点运行

                Reducer是接收全局所在Mapper的输出结果

  • Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减少网络传输量
  • Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来
4.9.5.2. 作用
  • Combiner就是在每个MapTask中提前进行数据的汇总处理,减少数据的传输量,也降低Reducer处理的数据量
  • Combiner的父类是Reducer,但是Combiner是运行在MapTask中
4.9.5.3. 使用位置
  • kv从缓冲区溢写到磁盘时使用Combiner
  • 全部数据溢写完成之后,有一个归并的过程可以使用Combiner(满足一定的条件才能使用)

4.9.6. 分组(Reduce中发生的)

4.9.6.1. 概念
  • 在ReduceTask中,相同key的多个kv对会分成一组,进入到一个reduce方法进行处理
4.9.6.2. Hadoop如何确定key是否相同?
  • 比较

4.9.7. Join多种应用

4.9.7.1. Reduce Join工作原理(两张表都大)
  • Map端的主要工作:为来自不同表或文件的key/value对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
  • Reduce端的主要工作:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在Map阶段已经打标志)分开,最后进行合并就ok了。
4.9.7.2. Map join工作原理
4.9.7.2.1. 使用场景
  • Map Join适用于一张表十分小(放入到内存中),一张表很大(直接走MapReduce程序)的场景。(或者是两张表都小)
4.9.7.2.2. 优点
  • 思考:在Reduce端处理过多的表,非常容易产生数据倾斜,怎么办?
  • 在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。

4.9.8. 数据清洗(ETL)

  • 在运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据,清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。

4.9.9. MapTask工作机制

  • Read阶段:MapTask通过InputFormat获得的RecordReader,从输入InputSplit中解析出一个个key/value。
  • Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。
  • Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般调用OutputCollector.collect()输出结果。在该函数内部,他会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中
  • Spill 阶段:即“溢写”,当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上, 生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
    • 溢写阶段详情:       
      • 利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号 Partition 进行排序,然后按照 key 进行排序。这样,经过排序后,数据以分区为单位聚集在 一起,且同一分区内所有数据按照 key 有序。
      • 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文 件 output/spillN.out(N 表示当前溢写次数)中。如果用户设置了 Combiner,则写入文件之 前,对每个分区中的数据进行一次聚集操作。
      • 将分区数据的元信息写到内存索引数据结构 SpillRecord 中,其中每个分区的元 信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大 小超过 1MB,则将内存索引写到文件 output/spillN.out.index 中。
  • Merge 阶段:当所有数据处理完成后,MapTask 对所有临时文件进行一次合并, 以确保最终只会生成一个数据文件。

        当所有数据处理完后,MapTask 会将所有临时文件合并成一个大文件,并保存到文件output/file.out 中,同时生成相应的索引文件output/file.out.index。

        在进行文件合并过程中,MapTask 以分区为单位进行合并。对于某个分区,它将采用多 轮递归合并的方式。每轮合并 mapreduce.task.io.sort.factor(默认 10)个文件,并将产生的文 件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。

        让每个 MapTask 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量 小文件产生的随机读取带来的开销。

4.9.10. ReduceTask工作机制

  • Copy 阶段:ReduceTask 从各个 MapTask 上远程拷贝一片数据,并针对某一片数 据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
  • Sort 阶段:在远程拷贝数据的同时,ReduceTask 启动了两个后台线程对内存和磁 盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照 MapReduce 语义,用 户编写 reduce()函数输入数据是按 key 进行聚集的一组数据。为了将 key 相同的数据聚在一 起,Hadoop 采用了基于排序的策略。由于各个 MapTask 已经实现对自己的处理结果进行了 局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可。
  • Reduce 阶段:reduce()函数将计算结果写到 HDFS 上。

5. YARN(分布式资源调度)架构

5.1. 是什么?

  • Yarn是一个资源调度平台,负责为运算程序提供服务器运算资源,相当于一个分布式的操作系统平台,而MapReduce等运算程序则相当于运行于操作系统之上的应用程序

5.2. YARN组成(角色)

  • ResourceManager【主角色Master(RM)】:(项目总监:管理公司中的所有开发人员)
    • 处理客户端请求。
    • 监控NodeManager。
    • 启动或监控ApplicationMaster。
    • 资源的分配与调度。
    • 整个集群的资源调度者,负责协调调度各个程序所需的资源。
  • NodeManager【从角色Slave(NM)】:(Yarn小弟)
    • 管理单个节点上的资源。
    • 处理来自ResourceManager的命令。
    • 处理来自ApplicationMaster的命令。
    • 单个服务器的资源调度者,负责调度单个服务器上的资源提供给应用程序使用。
  • ApplicationMaster(AM):(项目经理:管理某个项目的开发人员)
    • 每个Job都对应一个ApplicationMaster,主要负责Job的执行过程(资源申请,监控,容错等)
    • 任务的监控与容错。
    • 为应用程序申请资源并分配给内部的任务。
    • 负责数据的切分。
  • Container:容器
    • Container是Yarn中的资源抽象,他封装了某个节点上的多维度资源,如:内存,CPU,磁盘,网络等。
    • 容器(Container)是Yarn的NodeManager占用这部分资源

5.3. Yarn工作机制

  • MR 程序提交到客户端所在的节点。
  • YarnRunner 向 ResourceManager 申请一个 Application。
  • RM 将该应用程序的资源路径返回给 YarnRunner。
  • 该程序将运行所需资源提交到 HDFS 上。
  • 程序资源提交完毕后,申请运行 mrAppMaster。
  • RM 将用户的请求初始化成一个 Task。
  • 其中一个 NodeManager 领取到 Task 任务。
  • 该 NodeManager 创建容器 Container,并产生 MRAppmaster。
  • Container 从 HDFS 上拷贝资源到本地。
  • MRAppmaster 向 RM 申请运行 MapTask 资源。
  • RM 将运行 MapTask 任务分配给另外两个 NodeManager,另两个 NodeManager 分 别领取任务并创建容器。
  • MR 向两个接收到任务的 NodeManager 发送程序启动脚本,这两个 NodeManager 分别启动 MapTask,MapTask 对数据分区排序。
  • MrAppMaster 等待所有 MapTask 运行完毕后,向 RM 申请容器,运行 ReduceTask。
  • ReduceTask 向 MapTask 获取相应分区的数据。
  • 程序运行完毕后,MR 会向 RM 申请注销自己。

5.4. 资源调度器

5.4.1. 先进先出调度器(FIFO)

  • 优点:简单易懂;
  • 缺点:不支持多队列,生产环境很少使用

5.4.2. 容量调度器

5.4.2.1. 特点
  • 支持多个队列,每个队列可配置一定的资源量,每个队列采用FIFO调度策略
  • 为了防止同一个用户的作业独占队列中的资源,该调度器会对同一用户提交的作业所占资源量进行限定。
  • 首先,计算每个队列中正在运行的任务数与其应该分得的计算资源之间的比值,选择一个该比值最小的队列一一最闲的。
  • 其次,按照作业优先级和提交时间顺序,同时考虑用户资源量限制和内存限制对队列的任务进行排序
  • 三个队列同时按照任务的先后顺序依次执行,比如,iob11、iob21和iob31分别排在队列最前面,先运行,也是并行运行。

5.4.3. 公平调度器

5.4.3.1. 目的
  • 让所有的作业随着时间的推移,都能公平的获取等同的共享资源
5.4.3.2. 特点
  • 支持多队列多作业,每个队列可以单独配置
  • 同一队列的作业按照其优先级分享整个队列的资源,并发执行
  • 每个作业可以设置最小的资源值,调度器会保证作业获得其以上的资源
5.4.3.3. 与容器调度器相同点
  • 多队列:支持多队列多作业
  • 容量保证:管理员可为每个队列设置资源最低保证和资源使用上线
  • 灵活性:如果一个队列中的资源有剩余,可以暂时共享给那些需要资源的队列,而一旦该队列有新的应用程序提交,则其他队列借调的资源会归还给该队列
  • 多租户:支持多用户共享集群和多应用程序同时运行;为防止同一个用户的作业独占队列中的资源,该调度器会对同一用户提交的作业所占资源量进行限定
5.4.3.4. 与容器调度不同点
  • 核心调度策略不同
    • 容量调度器:优先选择资源利用率低的队列
    • 公平调度器:优先选择对资源的缺额比例大的
  • 每个队列可以单独设置资源分配方式
    • 容量调度器:FIFO,DRF
    • 公平调度器:FIFO,FAIR,DRF

5.4.4. 公平调度器—缺额

  • 公平调度器设计目标是:在时间尺度上,所有作业获得公平的资源。某一 时刻一个作业应获资源和实际获取资源的差距叫“缺额”
  • 调度器会优先为缺额大的作业分配资源

6. Hadoop优化方法

6.1. MapReduce 跑的慢的原因

MapReduce 程序效率的瓶颈在于两点:

  • 计算机性能 : CPU、内存、磁盘、网络
  • I/O 操作优化 :

    • 数据倾斜
    • Map 运行时间太长,导致 Reduce 等待过久
    • 小文件过多
    • 大量的不可分块的超大文件
    • Spill次数过多
    • Merge次数过多等

6.2. MApReduce优化方法

  • MApReduce优化方法主要从6个方面考虑:数据输入,Map阶段,Reduce阶段,IO传输,数据倾斜问题和常用的调优参数。

6.2.1. 数据传入

  • 合并小文件:在执行MR任务前将小文件进行合并,大量的小文件会产生大量的Map任务,增大Map任务装载次数,而任务的装载比较耗时,从而导致MR运行较慢。
  • 采用CombineTextInputFormat来作为输入,解决输入端大量的小文件场景

6.2.2. Map阶段

  • 减少溢写(Spill)次数:通过调整mapreduce.task.io.sort.mb及mapreduce.task.sort.spill.percent参数设置,增大触发Spill的内存上限,减少Spill次数,从而减少磁盘IO
  • 减少合并(Merge)次数:通过调整mapreduce.task.io.sort.factor参数,增大Merge的文件数目,减少Merge次数,从而缩短MR处理时间
  • 在Map之后,不影响业务逻辑前提下,先进行Combine处理,减少IO

6.2.3. Reduce阶段

  • 合并设置Map和Reduce数:两个都不能设置太小,也不能设置太多。太少,会导致Task等待,延长处理时间;太多,会导致Map,Reduce任务间竞争资源,造成处理超时等错误
  • 设置Map,Reduce共存:调整mapreduce.job.reduce.slowstart.completedmaps参数,使Map运行到一定程度后,Reduce也开始运行,减少Reduce的等待时间
  • 规范使用Reduce:因为Reduce在用于 连接数据集的时候将会产生大量的网络消耗
  • 合理设置Reduce端的Buffer:默认情况下,数据达到一个阈值的时候Buffer中的数据就会写入磁盘,然后Reduce会从磁盘中获得所有的数据。也就是说,Buffer和Reduce是没有直接关联的,中间多次写磁盘-->读磁盘的过程,既然有这个弊端,那么就可以通过参数来配置,使得Buffer中的一部分数据可以直接输送到Reduce,从而减少IO开销: mapreduce.reduce,input.buffer.percent,默认为0.0。当值大于0的时候,会保留指定比例的内存读Buffer中的数据直接拿给Reduce使用。这样一来,设置Buffer需要内存,读取数据需要内存,Reduce计算也要内存,所以要根据作业的运行情兄进行调整

6.2.4. IO传输

  • 采用数据压缩的方式:减少网络IO的时间,安装Snappy和LZO压缩编辑器
  • 使用SequenceFile二进制文件

6.2.5. 数据倾斜

  • 数据倾斜现象
    • 数据倾斜频率——某一个区域的数据量要远远大于其他区域
    • 数据大小倾斜——部分记录的大小远远大于平均值
  • 减少数据倾斜的方法:
    • 方法1:抽样或范围分区
      • 可以通过对原始数据进行抽样得到结果集来预设分区边界值
    • 方法2:自定义分区
      • 基于输出键的背景知识进行自定义分区。例如,如果Map输出键的单词来源于一本书。且其中某几个专业词汇较多。那么就可以自定义分区将这些专业词汇发送给固定的一部分Reduce实例。而将其他的都发送给剩余的Reduce实例。

    • 方法3:Combie
      • 使用Combine可以大量地减小数据倾料。在可能的情况下,Combine的目的就是聚合并精简数据。
    • 方法4:采用Map Join,尽量避免Redmce Join。

7. Hadoop小文件的优化

7.1. 小文件的弊端

  • HDFS 上每个文件都要在 NameNode 上创建对应的元数据,这个元数据的大小约为 150byte,这样当小文件比较多的时候,就会产生很多的元数据文件,一方面会大量占用 NameNode 的内存空间,另一方面就是元数据文件过多,使得寻址索引速度变慢。
  • 小文件过多,在进行 MR 计算时,会生成过多切片,需要启动过多的 MapTask。每个 MapTask 处理的数据量小,导致 MapTask 的处理时间比启动时间还小,白白消耗资源。

7.2. 小文件解决方案

  • 小文件优化方向:
    • 在数据采集的时候,就将小文件或小批数据合成大文件再上传 HDFS(数据源头)
    • 在业务处理之前,在HDFS上使用MapReduce程序对小文件进行合并
    • 在MapReduce处理时,可采用 CombineTextInputFormat 提高效率
    • 开启uber模式,实现jvm重用
  • Hadoop Archive(存储方向)
    • 是一个高效的将小文件放入 HDFS 块中的文件存档工具,能够将多个小文件打包成一 个 HAR 文件,从而达到减少 NameNode 的内存使用
  • SequenceFile
    • SequenceFile 是由一系列的二进制 k/v 组成,如果为 key 为文件名,value 为文件内容可将大批小文件合并成一个大文件
  • CombineTextInputFormat
    • CombineTextInputFormat 用于将多个小文件在切片过程中生成一个单独的切片或者少量的切片。
  • 开启uber 模式,实现jvm重用。默认情况下,每个 Task 任务都需要启动一个jvm来运行,如果 Task 任务计算的数据量很小,我们可以让同一个Job 的多个 Task 运行在一个Jvm中,不必为每个 Task 都开启一个Jvm.

8. MapReduce、HDFS、Yarn配合工作(作业提交全过程)

  1. 提交jar包程序到节点,想RM申请资源.
  2. 返回一个路径和一个task文件夹,task文件夹再这个路径中
  3. 转向hdfs文件存储系统找对应的文件,并对文件进行切片操作,然后会在task中创建jar(运行的java代码),job.split(文件的切片信息,默认情况下切片大小等于hdfs文件存储block大小,128M),job.xml(里面包含job运行所需的配置)。
  4. 向RM(资源老大)申请运行ApplicationMaster(mapTask管理)程序
  5. 初始化一个task,并放入队列中,等待被领取
  6. 空闲的Nodemanager领取task任务
  7. 创建Container容器,并且配置cpu+ram等资源。
  8. 把上面的资源文件copy到NodeManager节点Container容器中,并根据jar开启ApplicationMaster(mapTask管理),并根据切片(切片有多种方式,可以查看前几篇文档)信息分配需要几个MapTask(一个切片对应一个MapTask)
  9. 向RM申请需要多加几个MapTask容器
  10. 空闲的NameNode领取任务并创建容器(一个或多个)
  11. ApplicationMaster(mapTask管理)向MapTask所在容器发送启动脚本
  12. MapTask容器向HDFS中NameNode请求下载文件。
  13. NameNode返回给MapTask容器文件的元数据信息。
  14. MapTask容器向HDFS发送元数据信息。
  15. 对多个DateNode请求数据串行执行的(有的文件很大,可能分割成多个block存在不同的DateNode上),这里设计网络拓扑-节点距离计算会找寻最近的节点数据下载。
  16. 传输数据,读取文本,并转化为k,v对形式。
  17. 这里可以继承Mapper接口
  18. Map方法中对(k,v)数据做自己的的业务逻辑,然后ConTest.write(k,v)输出。
  19. 通过OutPutController输出到环形缓冲区。
  20. 环形缓冲区默认大小100M,阈值是80%,根据分区,快排(详细可以查看大数据第六篇shuffle)
  21. 溢写到磁盘文件
  22. 多个溢写文件进行归并排序(分区的话,每个分区归并排序)如果设置了Combainer合并的话这里也可以合并(减少网络传输)
  23. 通过网络传输把数据copy拉取到ReduceTask容器中(可能在同一节点,可能不在),先放在内存中(速度快),满了的话再写到磁盘中。
  24. 因为存在多个MapTask的输出,所以这里要进行归并排序
  25. 每次读一组(k,v)这里涉及hadoop为什么要默认对key排序,因为26步会获取相同key的values值,如果不排序每次都需要遍历查询,效率低。
  26. Reduce方法中对(k,v)数据做自己的的业务逻辑,然后ConTest.write(k,v)输出
  27. 向HDFS中的NameNode请求写入数据。(涉及网络拓扑-节点距离计算)找最近的。
  28. NameNode响应可以上传文件。
  29. Reduce容器请求上传第一个Block(0~128M),NameNode返回DateNode节点1,2,3可以上传。
  30. Reduce容器与DateNode请求建立传输通道,串联方式请求1到2到3
  31. 3到2到1分别作出应答。
  32. 串联方式开始传输数据1到2到3,这样就形成了3个副本。
  33. 完成后向ResourceManager注销自己的资源。

1-11、26为Yarn;12-17为HDFS写数据流程;18-25、27-31为MapReduce;19-25为Shuffle;32-41为HDFS写数据流程。

(0)MR程序提交到客户端所在的节点,在集群模式中运行MR程序,当运行到主函数的waitForCompletion()函数时创建YarnRunner(本地模式是LocalRunner)。

(1)YarnRunner向ResourceManager申请一个Application运行任务。

(2)RM将该应用程序的资源路径返回给YarnRunner,让YarnRunner把提交的job放到集群路径上。

(3)该程序将运行所需资源提交到HDFS集群路径上,包括split切片信息(控制开启MapTask的数量)、配置参数文件xml(控制任务按照xml里的参数运行)以及jar包(程序代码)。

(4)程序资源提交完毕后,YarnRunner申请运行MrAppMaster。

(5)RM将用户的请求初始化成一个Task任务,然后放到任务队列中排队(任务队列默认是容器)。

(6)空闲NodeManager领取Task任务。

(7)该NodeManager创建容器Container(任务的执行只能发生在容器中,容器中封装了资源),并在容器中启动MRAppmaster进程。

(8)Container从HDFS集群路径上拷贝资源(即切片信息等)到本地。

(9)容器拿到切片信息后,由MRAppmaster向RM 再次申请运行MapTask(切片个数=MapTask个数,有多少切片就申请开启多少MapTask)。

(10)RM将运行MapTask任务分配给NodeManager,NodeManager领取任务并创建容器(MapTask对应的容器有可能在同一个NodeManager节点上),并拷贝cpu、ram和jar包资源。

(11)MRAppMaster向接收到任务的NodeManager发送程序启动命令,NodeManager分别启动MapTask,然后开启YarnChild进程。

(12)MapTask向客户端读数据。

(13)客户端的InputFormat默认使用TextInputFormat,通过DistributedFileSystem分布式文件系统向NameNode请求下载文件,NameNode通过查询权限和元数据,找到文件块所在的DataNode地址。

(14)NameNode返回目标文件的元数据。

(15)客户端创建流,通过流对象挑选一台DataNode服务器,请求读取数据(串行读取)。(就近原则,然后随机;同时需要考虑当前节点的负载均衡,判断数据流量,当达到一定的量级时可用先访问其他节点)

(16)DataNode开始传输数据给客户端(从磁盘里面读取数据输入流,以Packet为单位来做校验),客户端以Packet为单位接收,先在本地缓存。

(17)数据以<k,v>的形式传入到MapTask。

(18)进入Mapper阶段,将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。

(19)当数据处理完成后,一般会调用OutputCollector.collect()输出结果,将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中。

(20)缓冲区内部对数据分区存储,当数据进入到环形缓冲区时就进行分区标记(会根据分区进入到不同的reduce),缓冲区一侧存数据,一侧存索引,当数据达到80%时进行反向溢写。溢写之前需要对分区中的数据进行排序(对索引使用快速排序)。

(21)当环形缓冲区满后,产生大量的溢写文件,先要对数据进行一次本地排序,MapReduce会将数据写到本地磁盘上,生成临时文件。

(22)当所有数据处理完成后(即溢写完成后),MapTask对所有临时文件(溢写文件)归并排序。

(23)对数据进行合并操作。

(24)对分区的数据进行压缩。

(25)将压缩好的数据安装分区写入磁盘。

(26)MrAppMaster向RM再次申请容器运行ReduceTask程序。

(27)ReduceTask拉取相应分区数据先存储到内存。

(28)内存不够时溢出到磁盘。

(29)针对内存和C盘中的每个map输出的数据进行归并排序。

(30)按照相同的key分组。

(31)对于相同的key的数据进入到同一个reduce()处理函数。

(32)将计算结果通过OutputFormat(输出)的RecordWriter调用write()写出数据。

(33)客户端通过分布式文件系统向NameNode请求上传文件,NameNode检查权限,以及目标文件是否已存在,父目录是否存在。

(34)NameNode响应是否可以上传。

(35)客户端请求第一个Block返回上传块的DataNode服务器。

(36)NameNode根据节点是否可用、负载均衡以及哪个节点距离最近等因素返回DataNode节点。

(37)客户端创建流,通过FSDataOutputStream流请求第一个DataNode节点上传数据)。

(38)dn1收到请求会继续调用dn2,将这个通信管道建立完成。(以Packet为单位建立管道)。

(39-40)FSDataOutputStream流建立一个ACK队列用于接收应答,由dn逐层应答,只有都应答成功才会上传一个块。

(41)客户端开始往dn1上传第一个块Block(先从磁盘读取数据放到一个本地内存缓存),以Packet为单位,dn1收到一个Packet就会传给dn2;dn1每传一个packet会放入一个应答队列等待应答。当一个Block传输完成之后,客户端再次请求NameNode上传第二个Block的服务器。

(42)ReduceTask程序运行完毕后,MrAppMaster会向RM申请注销自己,释放容器等资源。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值