Spark源码解析(三)任务提交流程分析

Spark源码和调优简介 Spark Core 作者:calvinrzluo,腾讯 IEG 后台开发工程师本文基于 Spark 2.4.4 版本的源码,试图分析其 Core 模块的部分实现原理,其中如有错误,请指正。为了简化论述,将部... 阅读详情

通过学习Spark源码为了更深入的了解Spark。主要按照以下流程进行Spark的源码分析,包含了Spark集群的启动以及任务提交的执行流程:

  1. Spark RPC分析
  2. start-all.sh
  3. Master启动分析
  4. Work启动分析
  5. spark-submit.sh脚本分析
  6. SparkSubmit分析
  7. SparkContext初始化

5.spark-submit.sh脚本分析

通过spark-submit.sh提交任务,查看详细信息,如下:

set CLASS=org.apache.spark.deploy.SparkSubmit
"%~dp0spark-class2.cmd" %CLASS% %*

6.SparkSubmit分析

这里看到是通过SparkSubmit这个类进行任务的提交,找到这个类的main方法,如下:

override def main(args: Array[String]): Unit = {
	val submit = new SparkSubmit() {
		self =>
		
		override protected def parseArguments(args: Array[String]): SparkSubmitArguments = {
			new SparkSubmitArguments(args) {
				override protected def logInfo(msg: => String): Unit = self.logInfo(msg)
				
				override protected def logWarning(msg: => String): Unit = self.logWarning(msg)
			}
		}
		
		override protected def logInfo(msg: => String): Unit = printMessage(msg)
		
		override protected def logWarning(msg: => String): Unit = printMessage(s"Warning: $msg")
		
		/**
		 *   注释:
		 */
		override def doSubmit(args: Array[String]): Unit = {
			try {
				/**
				 *   注释:
				 */
				super.doSubmit(args)
			} catch {
				case e: SparkUserAppException => exitFn(e.exitCode)
			}
		}
		
	}
	
	/**
	 *   注释: 通过 SparkSubmit 的 doSubmit 提交
	 */
	submit.doSubmit(args)
}

 这里是通过super.doSubmit方法提交。

def doSubmit(args: Array[String]): Unit = {
	// Initialize logging if it hasn't been done yet. Keep track of whether logging needs to
	// be reset before the application starts.
	val uninitLog = initializeLogIfNecessary(true, silent = true)
	
	val appArgs = parseArguments(args)
	if (appArgs.verbose) {
		logInfo(appArgs.toString)
	}
	
	/**
	 *   注释:
	 *   根据你的 spark-submit 的命令来决定到底执行那个方法
	 *   $CLASS 参数
	 */
	appArgs.action match {
		// TODO_MA 注释: 提交 app 运行
		case SparkSubmitAction.SUBMIT => submit(appArgs, uninitLog)
		case SparkSubmitAction.KILL => kill(appArgs)
		case SparkSubmitAction.REQUEST_STATUS => requestStatus(appArgs)
		case SparkSubmitAction.PRINT_VERSION => printVersion()
	}
}

 最终是通过JavaMainApplication进行任务的提交

private[deploy] class JavaMainApplication(klass: Class[_]) extends SparkApplication {
    
    /**
     *   注释: 启动 JavaMainApplication
     */
    override def start(args: Array[String], conf: SparkConf): Unit = {
        
        /**
         *   注释: 通过反射获取 自定义业务逻辑主类的 main 方法实例
         *
         *   new Array[String](0).getClass = $CLASS  =  SparkPi
         */
        val mainMethod = klass.getMethod("main", new Array[String](0).getClass)
        if (!Modifier.isStatic(mainMethod.getModifiers)) {
            throw new IllegalStateException("The main method in the given main class must be static")
        }
        
        val sysProps = conf.getAll.toMap
        sysProps.foreach { case (k, v) => sys.props(k) = v }
        
        /**
         *   注释: 执行 main 方法
         *   才会跳转到 SparkPi 的 main 方法
         */
        mainMethod.invoke(null, args)
    }
    
}

7.SparkContext初始化

这里以一个案例为入口进入源码查看实现过程的细节。

val spark: SparkSession = SparkSession.builder.appName("Spark").getOrCreate()

在getOrCreate()中可以看到Spark的创建,如下:

/**
 *   注释: 重要的地方:创建SparkContext
 */
// No active nor global default session. Create a new one.
val sparkContext: SparkContext = userSuppliedContext.getOrElse {
	// TODO_MA 注释: 创建 SparkConf 中
	val sparkConf = new SparkConf()
	options.foreach { case (k, v) => sparkConf.set(k, v) }
	
	// set a random app name if not given.
	if (!sparkConf.contains("spark.app.name")) {
		sparkConf.setAppName(java.util.UUID.randomUUID().toString)
	}
	
	/**
	 *   注释: 创建 SparkContext
	 */
	SparkContext.getOrCreate(sparkConf) // Do not update `SparkConf` for existing `SparkContext`, as it's shared by all sessions.
}

再次进入getOrCreate(sparkConf)方法会看到如下代码:

def getOrCreate(config: SparkConf): SparkContext = {
	// Synchronize to ensure that multiple create requests don't trigger an exception
	// from assertNoOtherContextIsRunning within setActiveContext
	SPARK_CONTEXT_CONSTRUCTOR_LOCK.synchronized {
		if (activeContext.get() == null) {
			/**
			 *   注释: new SparkContext(config)
			 */
			setActiveContext(new SparkContext(config), allowMultipleContexts = false)
		} else {
			if (config.getAll.nonEmpty) {
				logWarning("Using an existing SparkContext; some configuration may not take effect.")
			}
		}
		activeContext.get()
	}
}

可以看到SparkContext的创建是通过其构造方法创建,从这个类中提取SparkContext的初始化步骤:

  1. 创建Spark执行环境SparkEnv;
  2. 创建并且初始化Spark UI;
  3. hadoop相关配置以及Executor环境变量的设置;
  4. 创建任务调度TaskScheduler;  -----重要
  5. 创建和启动DAGScheduler;   -----重要
  6. TaskScheduler的启动;  -----重要
  7. 初始化管理器BlockManager(BlockManager是存储体系的主要组件之一)
  8. 启动测量系统MetricsSystem;
  9. 创建和启动Executor分配管理器ExecutorAllocationManager;
  10. ContextCleaner的启动和创建。
  11. Spark环境更新
  12. 创建DAGSchedulerSource和BlockManagerSource;
  13. 将SparkContext标记激活

SparkContext初始化这里主要查看以下几点:

1.TaskScheduler,实现类是:TaskSchedulerImpl

2.DagScheduler

DAGSchedulerEventProcessLoop初始化和启动两件事

3.SchedulerBackEnd

有两个成员变量

1、负责和master通信:DriverEndPoint

2、负责和worker通信:StandaloneAppClient

在代码中如下:

/**
 *  注释:第四步:创建任务调度TaskScheduler
 *  如果集群管理器是standalone模式:该方法返回(StandaloneSchedulerBackend,TaskSchedulerImpl)
 */
// Create and start the scheduler
val (sched, ts) = SparkContext. createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts

/**
 *  注释:第五步:创建和启动DAGScheduler
 *  1、内部初始化了一个:DAGSchedulerEventProcessLoop 用来处理各种任务
 *  2、在 DAGSchedulerEventProcessLoop 创建的时候,构造函数的内部的最后一句代码执行了 DAGSchedulerEventProcessLoop的启动。
 *  将来任务的提交,取消等,都会发送一个事件给 DAGSchedulerEventProcessLoop
 *  从而触发 dagScheduler.onRecevie() 的运行。
 */
_dagScheduler = new DAGScheduler(this)
_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)

/**
 *  注释:第六步:TaskScheduler的启动,主要任务:backend.start()
 */
// start TaskScheduler after taskScheduler sets DAGScheduler reference in DAGScheduler's constructor
_taskScheduler.start()

这里先查看下DAGScheduler初始化,核心代码如下:

/**
 *   注释:重点 这段代码执行了第一件事。
 *   初始化了一个事件处理器
 *   真正的来处理一些事件:  JobSubmitted  JobCancelled
  *
 */
private[spark] val eventProcessLoop = new DAGSchedulerEventProcessLoop(this)

/**
 *   注释: 之前看到有地方把 SchedulerBackEnd 设置成 TaskScheduler 的成员变量
 *   现在这儿把 DAGScheduler 也设置成 TaskScheduler 的成员变量
 */
taskScheduler.setDAGScheduler(this)

DAGSchedulerEventProcessLoop继承EventLoop。EventLoop中有开启线程来处理DAGScheduler

/**
 *   注释: driver 中,初始化了一个  dagSchedudelr
 *   它里面又初始化了一个 eventThread 专门用来处理  JobSubmitted
 */
// Exposed for testing.
private[spark] val eventThread = new Thread(name) {
	setDaemon(true)
	
	override def run(): Unit = {
		try {
			while (!stopped.get) {
				
				// TODO_MA 注释:获取消息
				// TODO_MA 注释:一定要注意:当 sparkContext 还没有初始化好的时候,是不执行 sc.runJob 提交任务的。
				// TODO_MA 注释:当执行 sc.runJob(sc) 的时候,就会提交 Job 到这儿来。
				val event = eventQueue.take()
				try {
					/**
					 *   注释:根据事件的类型,调用不同的 handleXXX 方法来进行处理。
					 *   当接收到任务提交的时候: event = JobSubmitted
					 */
					onReceive(event)
				} catch {
					case NonFatal(e) => try {
						onError(e)
					} catch {
						case NonFatal(e) => logError("Unexpected error in " + name, e)
					}
				}
			}
		} catch {
			case ie: InterruptedException => // exit even if eventQueue is not empty
			case NonFatal(e) => logError("Unexpected error in " + name, e)
		}
	}
	
}

可以在DAGScheduler中找到通过eventProcessLoop.start()来执行这段代码。onReceive方法最终是通过DAGSchedulerEventProcessLoop的onReceive方法来执行。详细流程如下图所示

TaskScheduler的启动执行如下:

  override def start() {

    /**
      * 注释:启动 StandalonedSchedulerBackend
      * SchedulerBackend backend 在 TaskSchedulerImpl 初始化的时候实例化的
      * 实现类是:StandaloneSchedulerBackend
      */
    backend.start()

    if (!isLocal && conf.getBoolean("spark.speculation", false)) {
      logInfo("Starting speculative execution thread")
      speculationScheduler.scheduleWithFixedDelay(new Runnable {
        override def run(): Unit = Utils.tryOrStopSparkContext(sc) {
          // TODO_MA 注释:推测执行
          checkSpeculatableTasks()
        }
      }, SPECULATION_INTERVAL_MS, SPECULATION_INTERVAL_MS, TimeUnit.MILLISECONDS)
    }
  }

这里主要看backend.start方法,实际的执行如下:核心是创建DriverEndpointRef,即有创建和启动DriverEndpoint。

override def start() {
	val properties = new ArrayBuffer[(String, String)]
	for ((key, value) <- scheduler.sc.conf.getAll) {
		if (key.startsWith("spark.")) {
			properties += ((key, value))
		}
	}
	
	/**
	 *   注释: 创建一个 DriverEndPoint
	 *   负责跟 master 打交道的
	 */
	// TODO (prashant) send conf instead of properties
	driverEndpoint = createDriverEndpointRef(properties)
}

在StandaloneSchedulerBackend中通过调用父类的start() 方法启动一个 DriverEndPoint,又有启动StandaloneAppClient,如下:

/**
  *   注释:里面维护了一个 DriverEndPoint 主要用来向 Executor分发任务
  */
client = new StandaloneAppClient(sc.env.rpcEnv, masters, appDesc, this, conf)
client.start()

Spark | 基于源码解析SparkSession初始化过程 Spark.version: 2.4.4 Apache Spark 2.0引入了SparkSession,其为用户提供了一个统一的切入点来使用Spark的各项功能,例如不再需要显式地创建SparkConf, SparkContext 以及 SQLContext,因为这些对象已经封装在SparkSession中。另外SparkSession允许用户通过它调用DataFrame和Dataset相关API来编写Spark程序。 其次SparkSession通过生成器设计模式(Builder Design Pa 阅读详情

相关推荐

一文带你解析Spark的源码:从Spark-Submit任务提交到资源分配Executor

随着互联网的告诉发展,每天会有大量的数据产生,传统的数据分析手段已经不能满足现在的需求。Apache Spark是专为大规模数据处理而设计的快速通用的计算引擎,现在形成一个高速发展应用广泛的生态系统。Spark由于其基于内存的计算模式,能够很好的应用于各个应用场景,包括离线数据分析、在线 数据分析和机器学习等。要想深入的了解Spark,学习其源码实现是必不可少的过程。 在本场 Chat 中,会讲到...

GitChat 4737

【原创】大叔经验分享(65)spark读取不到hive表

spark 2.4.3 spark读取hive表,步骤: 1)hive-site.xml hive-site.xml放到$SPARK_HOME/conf下 2)enableHiveSupport SparkSession.builder.enableHiveSupport().getOrCreate() 3) 测试代码 val sparkConf = new Sp...

weixin_30652897的博客 926

【spark床头书系列】spark-shell 任务提交任务参数选项说明示例源码权威详解

spark-shell 任务提交任务参数选项说明示例源码详解

wang2leee的博客 1592

【spark】spark异常:execute, tree:XXXX

本人菜鸡一只,在写代码的时候,经常会遇到这样或者那样的报错,而且脑子还不好,容易忘事,所以得用烂笔头记下来,希望以后能够越来越不蠢! 最近在跑一段spark的时候,报了异常,大概上就是这样: execute, tree: Exchange hashpartitioning(字段1#2374, 字段2#2378, 字段3#2373, 5000) +- *HashAggregate(keys=[...

lsr40的博客 4290

Spark源码系列(五)Spark Submit任务提交

Spark源码系列:Spark Submit任务提交 文章目录Spark源码系列:Spark Submit任务提交客户端任务提交源码解析org.apache.spark.deploy.SparkSubmit总结 前面几篇文章讲的是DAGScheduler,分析的是spark任务提交后的Stage划分。一开始没有想到按照整体任务提交的流程去写系列源码文章,所以还是写博客经验有所欠缺呀。那么从这篇文章开始我们从Spark任务提交开始,研究Spark内部是如何运行的,Spark任务是如何从开始运行到结束的。 Sp

JKerving的博客 1102

从源码角度看 Spark 任务提交流程(上)

前言  最近阅读一下 Spark 的部分源码,在这一过程中通过源码结合之前所了解的相关内容,能够对之前知识进行完整的梳理也能更一进步了解 Spark 运行的底层逻辑,由于阅读源码是一个较为艰深的过程遂将其记录下来方便日后回顾,本篇我们来讲一下我们的 Spark on Yarn 在提交一个任务后俩个框架为我们做了些什么。 Spark 向 Yarn 提交任务的流程  在 Linux 上安装完 Spark 后都会用一个官方提供的 example 来测一测我们的 Spark 安装成功了,比如下面这段命令: bin/

an_vana的博客 321

Spark2.0 yarn方式启动报错

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34302798的博客 486

Livy任务提交源码解析

文章目录简介任务路由代码断任务远程Driver上建立RpcServerLivyServer接收客户端提交代码断任务LivyServer向远程Driver的RpcServer提交任务远程Driver的RpcServer接收任务Driver执行代码断任务Batch任务LivyServer接收batch任务创建BatchSession以提交Spark任务创建SparkYarnApp实例以操作远程Spark任务 简介 Livy是基于Spark的开源REST服务,它能够通过REST的方式将代码段或是序列化的二进制代码

qq_27639777的博客 1399

Spark源码解析(一):Spark执行流程和脚本

Spark执行流程 Spark带注释源码 对于整个Spark源码分析系列,我将带有注释的Spark源码和分析的文件放在我的GitHub上Spark源码剖析欢迎大家fork和star 过程描述: 1.通过Shell脚本启动Master,Master类继承Actor类,通过ActorySystem创建并启动。 2.通过Shell脚本启动Worker,Worker类继承Actor类,通过A...

Oeljeklaus的博客 6344

Java Web提交任务到Spark Standalone集群并监控

Java Web提交任务到Spark Standalone集群并监控 环境 工程下载路径 Spark任务提交流程 问题及问题解决 后记Java Web提交任务到Spark Standalone集群并监控1. 环境 软件 版本 备注 IDEA 14.1.5 JDK 1.8 Spark 1.6.0 工程maven引用 Spark cdh5.7.3

fansy1990的专栏 6580

Calibre提参实战:如何用hcell和xcell文件精准屏蔽模块(附避坑指南)

本文详细解析了Calibre提参过程中hcell和xcell文件的应用技巧,帮助工程师精准屏蔽模块以优化寄生参数提取。通过对比hcell与xcell的核心差异、图形界面与命令行操作指南,以及常见问题排查方法,提升芯片后端设计效率。附避坑指南,解决EETOP论坛热议的模块丢失问题。

weixin_26795617的博客 440

Spark任务提交流程(整理版)

一、client提交任务到RM. 二、RM启动AM. 三、AM启动Driver线程, 并向RM申请资源. 四、RM返回可用资源列表. 五、AM通过nmClient启动Container, 并且启动ExecutorBackend后台进程. 六、Executor反向注册给Driver 七、Executor启动任务

不积跬步无以至千里 2218

微信小程序报错:“errno“:600001,“errMsg“:“uploadFile:fail errcode:-105 cronet_error_code:-105 error_msg:net::

uploadFile:fail errcode:-105 cronet_error_code:-105 error_msg:net::ERR_NAME_NOT_RESOLVED,uniApp微信小程序使用uni.uploadFile出现报错,微信小程序如何解决wx.uploadFile报错,如何解决这个问题,微信小程序真机报错,微信小程序报错"errno":600001,"errMsg":"uploadFile:fail errcode:-105 cronet_error_code:-105 error_m

✅ 精选推荐文章 3828

Spark 任务提交流程涉及源码说明

本篇主要阐述 Spark on Yarn 任务提交源码分析说的流程,目的在于了解任务提交的大概流程;其主要是想通过对 Spark 任务提交流程与涉及到的源码了解;在遇到问题的时候可以快速定位到是由什么环节导致的,从而可以快速排查问题并予以解决。 ......

Small_Ran的博客 1090

BioEmu 扩散模型加速传统分子动力学(MD)模拟

BioEmu 是一种创新的生成式深度学习系统,通过高效采样和准确预测蛋白质的动态机制,为蛋白质科学和药物设计提供了新的工具。它在多构象采样、MD 模拟拟合和蛋白质稳定性预测方面表现出色,同时显著降低了计算成本。BioEmu 的开发为未来蛋白质动态研究和应用奠定了坚实基础。

weixin_42357472的博客 320

论文翻译:2024 High-Confidence Computing A survey on large language model (LLM) security and privacy: The

大型语言模型(LLMs),如ChatGPT和Bard,已经彻底改变了自然语言理解和生成。它们具有深度语言理解能力、类似人类的文本生成能力、上下文意识和强大的问题解决技能,使它们在各个领域(例如搜索引擎、客户支持、翻译)中变得不可或缺。同时,LLMs也在安全领域获得了关注,揭示了安全漏洞并展示了它们在安全相关任务中的潜力。本文探讨了LLMs与安全性和隐私的交集。具体来说,我们研究了LLMs如何积极影响安全性和隐私,与它们使用相关的潜在风险和威胁,以及LLMs内部的固有漏洞。

CSPhD-winston的博客 1722

Spark源码分析之任务提交流程(Client)

文章目录提交命令任务提交流程任务提交初流程YarnClusterApplication提交集群流程提交过程用户Yarn-Cluster提交shell命令提交给SparkSubmit类的cmd命令提交给集群启动driver的命令任务运行结果上传到hdfs的文件整个任务运行日志 提交命令 假定Yarn-Cluster方式提交: ./bin/spark-submit \ --class org.apac...

Haiwi-Song 1286

YOLO11改进 | 卷积模块 | 轻量化GSConv替换普通的conv

yolo11,GSConv,卷积模块

kay_545 3958

Spark源码剖析——SparkSubmit提交流程

文章目录Spark源码剖析——SparkSubmit提交流程当前环境与版本前言Shell命令部分参数解析 MainSparkSubmit Spark源码剖析——SparkSubmit提交流程 当前环境与版本 环境 版本 JDK java version “1.8.0_231” (HotSpot) Sacla Scala-2.11.12 Spark spark-2.4.4 ...

蒋含竹的博客 2478

SAP FAGLFLEXT 是 SAP 新总账的核心余额表,存储多维度的汇总余额数据。核心结论:该表按 “科目 + 会计年度 + 期间 + 维度” 存储累计余额,字段可分为基础标识

FAGLFLEXT是SAP新总账的核心余额表,采用"科目+年度+期间+维度"结构存储多维汇总数据。其字段分为基础标识(科目、年度等)、维度细分(公司代码、利润中心等15个维度)、金额数值(本外币余额)、辅助控制(状态标识)四大类,支持新总账的灵活核算。该表通过凭证自动更新,与旧总账GLT0相比显著扩展了维度核算能力,各维度需在科目主数据或Ledger配置中启用才能使用。

金牌架构师 454

华为OD机试 - 打印机队列 - 优先队列(Python/JS/C/C++ 双机位C卷 200分)

这个问题要求模拟打印机的文件调度过程,关键在于正确处理每个打印机队列中的文件优先级和文件进入队列的顺序。优先级处理:每个文件有一个 1 到 10 的优先级,数字越大优先级越高,打印机应首先处理优先级最高的文件。在打印机 1 的队列中插入了 3 个文件,分别有不同的优先级,其中编号 3 的文件优先级最高,先被取出。因为打印的文件内容有轻重缓急之分,所以队列中的文件有1~10 不同的 优先级,其中数字越大优先级越高。在打印机 2 的队列中插入了 1 个文件,优先级为 1,因此它被首先取出。

学Java,找哪吒 1079

Spark源码解析03-Submit提交流程及Driver启动流程

1、前言 由前面的文章Spark基础06-Spark client和cluster提交流程我们已经知道了Spark client和cluster提交模式流程 启动Driver进程,并向集群管理器注册应用程序 集群资源管理器根据任务配置文件分配并启动Executor Executor启动之后反向到Driver注册,Driver已经获取足够资源可以运行 Driver开始执行main函数,Spark查询为懒执行,当执行到action算子时开始反向推算,根据宽依赖进行stage的划分,随后每一个stage对应一个

Linzx的学习笔记 1167

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

Spark源码之任务提交流程

执行脚本提交任务,实际是启动一个 SparkSubmit 的 JVM 进程;SparkSubmit 类中的 main 方法反射调用 YarnClusterApplication 的 main 方法;YarnClusterApplication 创建 Yarn 客户端,然后向 Yarn 服务器发送执行指令:bin/java ApplicationMaster;Yarn 框架收到指令后会在指定的 NM 中启动 ApplicationMaster;ApplicationMaster 启动 Driver 线程,执

weixin_46429290的博客 1161

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Spark任务提交源码

本文主要介绍了spark源码提交的流程,从SparkSubmit类main方法跟踪到spark主要实体driver和executor的创建过程,记录了整个过程中比较重要的节点的源码,以及对相关方法的功能实现进行了简要介绍。

ralph_wren的博客 3649

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

spark任务远程提交到yarn的两种方式

spark任务提交到yarn源码解析和集成

m0_38130834的博客 5914
上一篇: Spark源码解析(二)集群启动脚本分析
下一篇: Spark源码解析(四)第一部分总结
Yuan_CSDF
博客等级 码龄10年 169粉丝 130原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值