Spark AQE 导致的 Driver OOM问题

Spark 自适应查询执行(AQE) AQE 是 Spark SQL 的一种动态优化机制,它的诞生解决了 RBO、CBO,这些启发式、静态优化机制的局限性。想要用好 AQE,我们就要掌握它的特点,以及它支持的三种优化特性的工作原理和使用方法。如果用一句话来概括 AQE 的定义,就是每当 Shuffle Map 阶段执行完毕,它都会结合这个阶段的统计信息,根据既定的规则和策略动态地调整、修正尚未执行的逻辑计划和物理计划,从而完成对原始查询语句的运行时优化。 阅读详情

背景

最近在做Spark 3.1 升级 Spark 3.5的过程中,遇到了一批SQL在运行的过程中 Driver OOM的情况,排查到是AQE开启导致的问题,再次分析记录一下,顺便了解一下Spark中指标的事件处理情况

结论

SQLAppStatusListener 类在内存中存放着 一个整个SQL查询链的所有stage以及stage的指标信息,在AQE中 一个job会被拆分成很多job,甚至几百上千的job,这个时候 stageMetrics的数据就会成百上倍的被存储在内存中,从而导致Driver OOM。
解决方法:

  1. 关闭AQE spark.sql.adaptive.enabled false
  2. 合并对应的PR-SPARK-45439

分析

背景知识:对于一个完整链接的sql语句来说(比如说从 读取数据源,到 数据处理操作,再到插入hive表),这可以称其为一个最小的SQL执行单元,这最小的数据执行单元在Spark内部是可以跟踪的,也就是用executionId来进行跟踪的。
对于一个sql,举例来说 :

insert into  TableA select * from TableB;

在生成 物理计划的过程中会调用 QueryExecution.assertOptimized 方法,该方法会触发eagerlyExecuteCommands调用,最终会到SQLExecution.withNewExecutionId方法:

  def assertOptimized(): Unit = optimizedPlan

  ...
  lazy val commandExecuted: LogicalPlan = mode match {
    case CommandExecutionMode.NON_ROOT => analyzed.mapChildren(eagerlyExecuteCommands)
    case CommandExecutionMode.ALL => eagerlyExecuteCommands(analyzed)
    case CommandExecutionMode.SKIP => analyzed
  }
  ...
  lazy val optimizedPlan: LogicalPlan = {
  // We need to materialize the commandExecuted here because optimizedPlan is also tracked under
  // the optimizing phase
  assertCommandExecuted()
  executePhase(QueryPlanningTracker.OPTIMIZATION) {
    // clone the plan to avoid sharing the plan instance between different stages like analyzing,
    // optimizing and planning.
    val plan =
      sparkSession.sessionState.optimizer.executeAndTrack(withCachedData.clone(), tracker)
    // We do not want optimized plans to be re-analyzed as literals that have been constant
    // folded and such can cause issues during analysis. While `clone` should maintain the
    // `analyzed` state of the LogicalPlan, we set the plan as analyzed here as well out of
    // paranoia.
    plan.setAnalyzed()
    plan
  }
  
  def assertCommandExecuted(): Unit = commandExecuted
  ...
  private def eagerlyExecuteCommands(p: LogicalPlan) = p transformDown {
    case c: Command =>
      // Since Command execution will eagerly take place here,
      // and in most cases be the bulk of time and effort,
      // with the rest of processing of the root plan being just outputting command results,
      // for eagerly executed commands we mark this place as beginning of execution.
      tracker.setReadyForExecution()
      val qe = sparkSession.sessionState.executePlan(c, CommandExecutionMode.NON_ROOT)
      val name = commandExecutionName(c)
      val result = QueryExecution.withInternalError(s"Eagerly executed $name failed.") {
        SQLExecution.withNewExecutionId(qe, Some(name)) {
          qe.executedPlan.executeCollect()
        }
      }  

而SQLExecution.withNewExecutionId主要的作用是设置当前计划的所属的executionId:

    val executionId = SQLExecution.nextExecutionId
    sc.setLocalProperty(EXECUTION_ID_KEY, executionId.toString)

该EXECUTION_ID_KEY的值会在JobStart的时候传递给Event,以便记录跟踪整个执行过程中的指标信息。
同时我们在方法中eagerlyExecuteCommands看到qe.executedPlan.executeCollect()这是具体的执行方法,针对于insert into 操作来说,物理计划就是
InsertIntoHadoopFsRelationCommand,这里的run方法最终会流转到DAGScheduler.submitJob方法:

    eventProcessLoop.post(JobSubmitted(
      jobId, rdd, func2, partitions.toArray, callSite, waiter,
      JobArtifactSet.getActiveOrDefault(sc),
      Utils.cloneProperties(properties)))

最终会被DAGScheduler.handleJobSubmitted处理,其中会发送SparkListenerJobStart事件:

    listenerBus.post(
      SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos,
        Utils.cloneProperties(properties)))

该事件会被SQLAppStatusListener捕获,从而转到onJobStart处理,这里有会涉及到指标信息的存储,这里我们截图出dump的内存占用情况:
在这里插入图片描述

可以看到 SQLAppStatusListener 的 LiveStageMetrics 占用很大,也就是 accumIdsToMetricType占用很大

那在AQE中是怎么回事呢?
我们知道再AQE中,任务会从source节点按照shuffle进行分割,从而形成单独的job,从而生成对应的shuffle指标,具体的分割以及执行代码在AdaptiveSparkPlanExec.getFinalPhysicalPlan中,如下:

      var result = createQueryStages(currentPhysicalPlan)
      val events = new LinkedBlockingQueue[StageMaterializationEvent]()
      val errors = new mutable.ArrayBuffer[Throwable]()
      var stagesToReplace = Seq.empty[QueryStageExec]
      while (!result.allChildStagesMaterialized) {
        currentPhysicalPlan = result.newPlan
        if (result.newStages.nonEmpty) {
          stagesToReplace = result.newStages ++ stagesToReplace
          executionId.foreach(onUpdatePlan(_, result.newStages.map(_.plan)))

          // SPARK-33933: we should submit tasks of broadcast stages first, to avoid waiting
          // for tasks to be scheduled and leading to broadcast timeout.
          // This partial fix only guarantees the start of materialization for BroadcastQueryStage
          // is prior to others, but because the submission of collect job for broadcasting is
          // running in another thread, the issue is not completely resolved.
          val reorderedNewStages = result.newStages
            .sortWith {
              case (_: BroadcastQueryStageExec, _: BroadcastQueryStageExec) => false
              case (_: BroadcastQueryStageExec, _) => true
              case _ => false
            }

          // Start materialization of all new stages and fail fast if any stages failed eagerly
          reorderedNewStages.foreach { stage =>
            try {
              stage.materialize().onComplete { res =>
                if (res.isSuccess) {
                  events.offer(StageSuccess(stage, res.get))
                } else {
                  events.offer(StageFailure(stage, res.failed.get))
                }
                // explicitly clean up the resources in this stage
                stage.cleanupResources()
              }(AdaptiveSparkPlanExec.executionContext)

这里就是得看stage.materialize()这个方法,这两个stage只有两类:BroadcastQueryStageExec 和 ShuffleQueryStageExec,
这两个物理计划稍微分析一下如下:

  • BroadcastQueryStageExec
    数据流如下:
    broadcast.submitBroadcastJob
          ||
          \/
    promise.future
          ||
          \/
    relationFuture
          ||
          \/
    child.executeCollectIterator()
    
    
    其中 promise的设置在relationFuture方法中,而relationFuture 会被doPrepare调用,而submitBroadcastJob会调用executeQuery,从而调用doPrepare,executeCollectIterator()最终也会发送JobSubmitted事件,分析和上面的一样
  • ShuffleQueryStageExec
     shuffle.submitShuffleJob
          ||
          \/
     sparkContext.submitMapStage(shuffleDependency)
          ||
          \/
     dagScheduler.submitMapStage
    
    

该submitMapStage会发送MapStageSubmitted事件:

    eventProcessLoop.post(MapStageSubmitted(
      jobId, dependency, callSite, waiter, JobArtifactSet.getActiveOrDefault(sc),
      Utils.cloneProperties(properties)))

最终会被DAGScheduler.handleMapStageSubmitted处理,其中会发送SparkListenerJobStart事件:

    listenerBus.post(
      SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos,
        Utils.cloneProperties(properties)))

该事件会被SQLAppStatusListener捕获,从而转到onJobStart处理:

  private val liveExecutions = new ConcurrentHashMap[Long, LiveExecutionData]()
  private val stageMetrics = new ConcurrentHashMap[Int, LiveStageMetrics]()
   ...
 
  override def onJobStart(event: SparkListenerJobStart): Unit = {
    val executionIdString = event.properties.getProperty(SQLExecution.EXECUTION_ID_KEY)
    if (executionIdString == null) {
      // This is not a job created by SQL
      return
    }

    val executionId = executionIdString.toLong
    val jobId = event.jobId
    val exec = Option(liveExecutions.get(executionId))

该方法会获取事件中的executionId,在AQE中,同一个执行单元的executionId是一样的,所以stageMetrics内存占用会越来越大。
而这里指标的更新是在AdaptiveSparkPlanExec.onUpdatePlan等方法中。

这样整个事件的数据流以及问题的产生原因就应该很清楚了。

其他

为啥AQE以后多个Job还是共享一个executionId呢?因为原则上来说,如果没有开启AQE之前,一个SQL执行单元的是属于同一个Job的,开启了AQE之后,因为AQE的原因,一个Job被拆成了了多个Job,但是从逻辑上来说,还是属于同一个SQL处理单元的所以还是得归属到一次执行中。

spark-AQE/Tungsten介绍及其触发条件 定义:Spark 3.0引入的自适应查询优化技术运行时动态调整执行计划。核心能力动态分区合并:自动合并小分区以减少Shuffle数据量。自动Broadcast Join:根据数据量动态切换Join策略。Skew Join优化:识别数据倾斜并自动调整分区策略。配置参数Tungsten(钨丝计划)定义:Spark 1.6推出的底层性能优化框架,通过内存管理和代码生成提升效率。关键技术内存列式存储:减少内存占用并提升缓存利用率。代码生成(Codegen):动态生成JVM字节码替代解释执行。 阅读详情

相关推荐

Spark 3.0自适应查询执行框架(AQE)

1. AQE设计原理 AQE 可以理解成是 Spark Catalyst 之上的一层,它可以在运行时修改 Spark plan。 AQE 完全基于精确的运行时统计信息进行优化,引入了 Query Stages 的概念 ,并且以 Query Stage 为粒度,进行运行时的优化,其工作原理如下所示: Query Stage 是由 Shuffle 或 broadcast exchange 划分的,在运行下一个 Query Stage 之前,上一个 ...

L13763338360的博客 2950

Spark3 新特性之AQE

Spark3 新特性之AQE

Shyllin的博客 5129

Spark数据倾斜优化-AQE Skewed Join

AQE处理SkewedJoin的原理。

weixin_45626756的博客 1373

Spark AQE

Spark 3.0 添加了自适应查询执行(AQE)、动态分区剪裁(DPP)、扩展的 Join Hints。

记录点点滴滴 1261

Spark 3.0 AQE 调优:Shuffle 分区合并与倾斜 Join

在大数据分布式计算中,Shuffle 是连接 Map 与 Reduce 的关键阶段,静态分区数和数据倾斜常导致长尾任务与资源浪费。AQE(自适应查询执行)通过收集运行时统计信息,在 Shuffle 边界动态调整执行计划,实现小分区合并、倾斜 Key 拆分以及 SortMergeJoin 到 BroadcastJoin 的切换。其技术价值在于把编译期无法感知的数据分布问题转为运行时自适应决策,提升 Spark SQL 与离线 ETL 的稳定性和资源利用率。典型应用包括订单聚合、日志分析等数据量波动场景。关键点

weixin_29047919的博客 265

Spark架构深度解析:Driver-Executor协作与Shuffle内存机制

Apache Spark 不是简单的内存计算引擎,而是一套分层抽象与动态契约并存的分布式执行框架。其核心在于 Driver 与 Executor 的协同调度机制、Shuffle 数据流动的生命线设计,以及 Execution/Storage/User 三层内存的弹性博弈。理解 Catalyst 优化器如何将逻辑计划坍缩为物理算子,掌握 Tungsten 引擎对 UnsafeRow 和序列化的底层控制,才能真正规避 OOM、Stage 卡顿和资源争抢等典型问题。本文聚焦 Spark 架构本质,覆盖微批流处理抽

weixin_30596343的博客 558

spark--一文搞懂AQE

需预先统计收集表信息。目标数据量是64MB。

qq_46893497的博客 650

Spark架构深度解析:Driver-Executor协作与Shuffle生命周期

Apache Spark 不是单机程序的简单扩展,而是一个分布式计算有机体,其核心在于 Driver 与 Executor 的协同控制机制和 Shuffle 数据流转过程。Driver 承担逻辑规划与状态协调,Executor 实现自治式内存与计算隔离,二者通过精细的资源契约与故障恢复模型维持稳定。Shuffle 作为全链路过程,横跨内存、磁盘、网络与调度层,直接决定作业吞吐与容错能力。理解 UnifiedMemoryManager 的 Execution/Storage 内存博弈、SortShuffleM

weixin_29816689的博客 354

为什么 Spark AQE 建议的 64MB 分区,会让你的 Driver 直接暴毙?

摘要: Spark AQE(自适应查询执行)通过动态优化解决静态执行计划的不足,核心功能包括动态合并Shuffle分区、动态切换Join策略和动态优化倾斜Join。然而,其优化机制存在潜在风险: 动态合并Shuffle分区:可能因压缩数据解压后内存膨胀导致OOM,需通过REPARTITION强制分散任务或调整参数避免过度合并。 动态切换Join策略:压缩数据解压后可能超出Driver内存,需降低广播阈值或使用SHUFFLE_MERGE提示。 动态优化倾斜Join:统计信息压缩可能导致误判,需调整参数(如sk

涤生大数据 453

Spark on YARN OOM根因与工业级调优实战

Spark作业在YARN上频繁OOM,本质是数据倾斜与序列化机制协同失效的技术现象。其原理在于Shuffle阶段Key分布不均引发单点内存爆炸,叠加Java默认序列化高开销与Kryo未注册导致的反序列化失败。技术价值体现在显著提升资源利用率(实测从32%升至87%)和任务稳定性。典型应用场景包括电商用户行为日志实时聚合、运营商信令处理及Hive表关联分析。本文聚焦YARN环境下的真实故障归因与可落地的工程解法,涵盖salting去倾斜、Kryo手动注册、JDK17兼容编译等硬核实践。

weixin_34268169的博客 617

windows的rancherDesktop修改镜像源

Rancher Desktop中的“Allowed Images”功能(通常在 Preferences -> Container Engine -> Allowed Images 中找到)是用来定义一个允许拉取和推送的镜像列表的规则,例如,只允许从您组织或特定的受信任的仓库中获取镜像。如果问题依旧,请提供您Rancher Desktop的版本和当前选择的容器运行时,以便进一步协助。总结一下,对于Windows上的Rancher Desktop,如果您使用默认的。,镜像源的配置通常通过一个名为。

zwjapple的专栏 2695

Spark从入门到精通

本文系统介绍了Spark基础架构与核心模块。Spark作为基于内存的分布式计算引擎,具备高效、可扩展的特点,其运行架构由Driver、Executor和ClusterManager协同完成任务调度与执行。通过RDD(弹性分布式数据集)实现数据并行处理,支持转换与动作算子,结合血缘依赖与容错机制保障可靠性。此外,文章详述了SparkSQL的结构化数据处理能力、SparkCore的累加器与广播变量机制,以及Spark在批处理、流处理和机器学习等场景的应用,全面展示了Spark生态圈的技术优势与实践方法。

幸运如你的博客 1878

ArcGIS Pro2.5深度学习使用注意事项

ArcGIS Pro2.5深度学习使用中的注意事项1、ArcGIS Pro2.5深度学习使用中的注意事项1.1、Label深度学习样本工具参数介绍1.2、准备数据参数介绍1.3、训练模型介绍1.4、ArcGIS Pro2.5深度学习推理工具和模型对应关系介绍1.4.1、Classify Objects Using Deep Learning1.4.2、Classify Pixels Using D...

GIS哼哈哈的博客 7200

Spark OOM内存溢出从定位到调优:JVM参数、AQE与Kryo序列化实战

内存溢出(OOM)是分布式计算框架运行中最常见也最棘手的故障之一。在大数据任务中,Executor内存的开销不仅局限于JVM堆内,还涉及堆外Direct Memory和YARN容器总内存的约束。当Shuffle聚合、数据倾斜或缓存策略不当导致内存峰值暴涨时,任务极易触发Container killed或OutOfMemoryError。理解Spark统一内存管理模型、合理调整executor-memory与memoryOverhead、启用AQE自适应执行计划以及采用Kryo序列化,是系统化治理内存瓶颈的关

weixin_29035405的博客 123

NeuralKG的安装及简单demo使用(浙江大学开源知识图谱表示学习工具)

记录NeuralKG的安装和使用,以及解决windows系统下的运行demo.py出现的问题。

m0_46621271的博客 1397

MTK平台TP驱动设备树与GPIO配置实战解析

本文详细解析了MTK平台TP驱动开发中的设备树与GPIO配置实战。通过DCT工具配置GPIO引脚,结合设备树节点定义触摸参数,并分享驱动移植、编译调试技巧及常见问题解决方案,帮助开发者快速掌握MTK触摸驱动开发核心。

ice55的博客 233

spark任务优化参数

本文总结了Apache Spark任务优化的关键参数配置和使用经验。核心内容包括:1) 资源分配参数如driver/executor内存和核数设置;2) 任务执行优化如分区控制、文件大小限制(如maxPartitionBytes参数);3) 数据压缩配置。特别指出不同模块(core/sql)参数差异,SQL模块推荐使用AQE(自适应查询执行)优化。资源分配需根据数据量和计算复杂度平衡,建议动态调整executor数量。同时强调不同Spark发行版(如Kyuubi、阿里EMR)可能有特有参数。官方文档是验证参

别人笑我太疯癫,我笑他人看不穿。 3733

YOLOv11 改进 - SPPF模块 | 发论文神器!LSKA注意力改进SPPF,增强多尺度特征提取能力,高效涨点!!!

本文介绍了Large Separable Kernel Attention (LSKA)模块及其在YOLOv11中的结合应用。LSKA旨在解决Visual Attention Networks (VAN)使用大内核卷积时的计算效率问题,通过将2D深度卷积层的卷积核分解为级联的水平和垂直1-D卷积核,降低了计算复杂度和内存占用,且使VAN更关注物体形状。我们将集成LSKA的SPPF_LSKA模块引入YOLOv11,大量实验表明,改进后的模型在对象识别、检测、分割和稳健性测试上表现出色。

魔改工程师的博客 1279

SPARK中关于HighlyCompressedMapStatus的说明(会造成运行时的数据不精确)

这是来map端任务记录精确分区的阈值,如果大于该阈值,则会记录真实的reduce数据的分区大小,如果小于则记录的是每个reduce大小的平均值(这导致会在reduce获取运行时的数据大小信息时数据不准确的问题,从而导致AQE的效果不是很理想)。采用的是对于数据量小的reduce分区数据采用公用平均值的方式,这在一定程度上能够减缓Driver OOM的概率,中,这样下游的reduce任务如果需要获取上游MapTask的运行情况的时候就会最终调用到。因为对于小数据量的分区,只需要存储一个平均值,而不像。

monkeyboy_tech的博客 1724

Codex入门00-初识神器(小白入门:到底什么是Codex CLI,和ChatGPT写代码有什么不同?)

OpenAI Codex CLI 是 OpenAI 于2025年推出的终端原生AI编程代理,不是在网页里聊天写代码,而是直接在你的终端里自主读代码、改文件、跑命令。它拥有三种安全模式、/goal 持久化任务、云端沙箱执行等独特能力。本文从零介绍 Codex CLI 的核心概念、与 ChatGPT 写代码的本质区别、典型使用场景和费用说明,让你全面认识这个改变开发方式的神器。

yueyeguzhuo的专栏 301

企业财务共享服务中心业务蓝图规划方案【附全文阅读】

《企业财务共享服务中心业务蓝图规划方案》为集团企业提供财务共享中心建设全流程指导,涵盖战略定位、组织架构、业务流程、信息系统、运营管理和变革实施六大维度。方案明确共享中心"交易处理+战略支撑"双重职能,设计专业分组架构,筛选标准化高、价值明确的流程(如核算、资金结算等)纳入共享范围,构建业财一体化平台,并配套绩效与风险管控体系。结合项目型企业管理特点,分阶段推进实施,助力企业实现财务集中化与标准化转型。

cdfunlove的博客 994

Spark分布式计算原理与生产调优实战指南

分布式计算是现代大数据处理的基石,其核心在于突破单机瓶颈,通过协同调度实现数据与计算的高效匹配。其底层逻辑受制于内存、磁盘与网络的三级性能鸿沟,而Spark通过内存计算、DAG执行模型和统一API重构了这一范式。相比传统MapReduce,Spark以Shuffle优化、智能缓存(如MEMORY_AND_DISK_SER)和Catalyst优化器显著提升迭代计算与混合负载效率。在电商用户分析、金融反欺诈等PB级场景中,合理配置spark.sql.adaptive.enabled、Kryo序列化与AQE机制,

baichuan9723的博客 459

手把手教你学SOC芯片架构--NoC(片上网络,Network on Chip)全方位解析

​、​。

MHD0815的博客 863

Spark 3.0 AQE:动态解决数据倾斜与 Shuffle 调优

传统大数据查询引擎依赖编译期统计信息生成执行计划,但统计失真常引发 Shuffle 分区数不合理、Join 策略误选与数据倾斜,导致长尾任务和 OOM。自适应查询执行(AQE)在运行时以 Shuffle 阶段的真实分区大小和记录数为依据,动态合并小分区、将 Sort-Merge Join 转为 Broadcast Join,并自动拆分倾斜 Key。这种边跑边优化机制显著降低人工调参成本,提升 Spark SQL 在 ETL 与聚合关联场景下的稳定性。Spark 3.0 AQE 通过 coalescePart

weixin_28362173的博客 294

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

SparkSession:现代Spark应用的统一入口与核心API实战指南

在大数据处理领域,分布式计算框架是处理海量数据的核心技术。Apache Spark作为主流框架,其设计哲学经历了从多入口到单入口的演进,旨在简化编程模型并提升开发效率。这一演进的核心技术价值在于通过统一的门面模式,整合了资源管理、数据读写、SQL执行等核心功能,降低了学习与维护成本。在实际工程实践中,SparkSession作为现代Spark应用的唯一入口,广泛应用于ETL流水线、交互式数据分析、机器学习管道等场景。本文聚焦于SparkSession的实战应用,深入解析其核心API,并探讨如何通过配置优化与

weixin_33794672的博客 316

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

上一篇: Spark Rebalance hint的倾斜的处理(OptimizeSkewInRebalancePartitions)
下一篇: 记一次Spark cache table导致的数据问题以及思考
鸿乃江边鸟
博客等级 码龄6年 3746粉丝 235原创
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值