Spark-Job启动、Stage划分

server/2024/9/20 3:57:11/ 标签: spark, 大数据, 分布式

一、上下文

《Spark-driver和executor启动过程》详细分析了driver和executor的启动,此时资源已经给我们分配好了,且Application也已经注册完成。下面我们就来看看Spark是如何启动job并根据DAG来划分Stage的

二、Job启动

Spark RDD中的算子分为Transformations 算子和Actions 算子,Transformations 算子只是将RDD在逻辑上进行了转换,只有调用Actions 算子时才会真正执行以上对RDD的所有操作。为什么呢?点进去这些Actions 算子看下就发现它们都调用了SparkContext的runJob() ,因此程序调用几次Actions算子就会启动几个Job,因此一个Application是对应多个Job的。下面继续看下runJob()中做了什么事情。

1、SparkContext

class SparkContext(config: SparkConf) extends Logging {//在RDD中的给定分区集上运行一个函数,并将结果传递给给定的处理程序函数。//这是Spark中所有操作的主要入口点。def runJob[T, U: ClassTag](rdd: RDD[T],func: (TaskContext, Iterator[T]) => U,partitions: Seq[Int],resultHandler: (Int, U) => Unit): Unit = {......//打印启动job的日志logInfo("Starting job: " + callSite.shortForm)//调用dagScheduler的runJob()dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)progressBar.foreach(_.finishAll())//在使用此RDD的作业完成后调用它rdd.doCheckpoint()}}

2、DAGScheduler

DAGScheduler 是面向 stage 调度的高级调度层。它为每个作业计算一个stage的DAG,跟踪这些RDD和stage输出被物化,并找到运行作业的最小时间表。然后,它将stage作为TaskSet(包含完全独立的任务)提交给在集群上运行它们的底层TaskScheduler实现。

Spark stages 是通过在Shuffle边界处打破RDD图来创建的。是一系列具有“窄”依赖关系的RDD操作,如map() 和filter(),这些窄依赖的RDD在每个 stage 都被 pipelined 到一组任务中,但具有shuffle依赖关系的操作需要多个stage(一个stage shuffle write 一组文件,另一个stage shuffle read 这些文件)。最后,每个stage都只会对其他stage进行shuffle依赖,并可能在其中计算多个操作。这些操作的实际 pipelining 发生在各种RDD的RDD.compute()函数中

除了生成阶段的DAG外,DAGScheduler还根据当前缓存状态确定运行每个 Task 的首选位置,并将其传递给低级TaskScheduler。此外,它还可以处理由于shuffle输出文件丢失而导致的故障,在这种情况下,可能需要重新提交之前跑过的stage。非随机文件丢失导致的阶段内故障由TaskScheduler处理,TaskScheduler将在取消整个stage之前重试每个任务几次。

在看源码时,有几个关键概念:

Jobs

        是提交给调度器的顶级工作项,例如,当用户调用类似count() 的操作时,作业将通过submitJob提交。每个Job可能需要执行多个Stage来构建中间数据。

Stages

        是计算Job中中间结果的一组任务,其中每个Task在同一RDD的分区上计算相同的函数。
Stage在shuffle边界处分开,也说明我们必须等待前一阶段完成才能获取输出。有两种类型的阶段:ResultStage,用于执行动作的最后阶段,以及ShuffleMapStage,用于为shuffle写入映射输出文件。如果这些作业重用相同的RDD,则Stage通常在多个Job之间共享。

Tasks

        单独的工作单元,每个任务都发送到一台机器上。

Cache tracking

        DAGScheduler计算出哪些RDD被缓存以避免重新计算它们,并且同样记住哪些shuffle write阶段已经生成了输出文件,以避免重做shuffle write

Preferred locations

        DAGScheduler还根据其底层RDD的首选位置,或缓存或shuffle数据的位置,计算在stage中中的每个Task应该去哪台节点执行

Cleanup

        当Job完成后,所有依赖的数据结构都会被清除,防止OOM

private[spark] class DAGScheduler(private[scheduler] val sc: SparkContext,private[scheduler] val taskScheduler: TaskScheduler,listenerBus: LiveListenerBus,mapOutputTracker: MapOutputTrackerMaster,blockManagerMaster: BlockManagerMaster,env: SparkEnv,clock: Clock = new SystemClock())extends Logging {def runJob[T, U](...) {//提交要给Job给调度器val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)ThreadUtils.awaitReady(waiter.completionFuture, Duration.Inf)}}def submitJob[T, U](rdd: RDD[T],func: (TaskContext, Iterator[T]) => U,partitions: Seq[Int],callSite: CallSite,resultHandler: (Int, U) => Unit,properties: Properties): JobWaiter[U] = {// 获取rdd的所有分区数量val maxPartitions = rdd.partitions.length......//等待DAGScheduler作业完成的对象。当任务完成时,它将其结果传递给给定的处理函数。val waiter = new JobWaiter[U](this, jobId, partitions.size, resultHandler)//向线程组提交一个 JobSubmitted 事件//在目标RDD上提交了一个产生结果的作业eventProcessLoop.post(JobSubmitted(jobId, rdd, func2, partitions.toArray, callSite, waiter,Utils.cloneProperties(properties)))waiter}}private[scheduler] class DAGSchedulerEventProcessLoop(dagScheduler: DAGScheduler)extends EventLoop[DAGSchedulerEvent]("dag-scheduler-event-loop") with Logging {private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>//处理 JobSubmitted 事件 转交给 dagScheduler 处理dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)}}

三、Stage划分

Stage是一组并行Task,它们都计算需要作为Spark作业的一部分运行的相同函数,
其中所有Task都具有相同的shuffle依赖关系。调度器运行的每个Task 
DAG在Shuffle发生的边界处被划分为Stage,然后DAGScheduler按拓扑顺序运行这些阶段。

每个 Stage 可以是 ShuffleMapStage ,在这种情况下,其Task的结果被输入到其他Stage,
也可以是ResultStage,在该情况下,它的任务通过在RDD上运行函数直接计算Spark动作
(例如count()、save()等)。对于 ShuffleMapStage ,还跟踪每个输出分区所在的节点。

1、以Stage为单位一层一层递归寻找

private[spark] class DAGScheduler(......)extends Logging {private[scheduler] def handleJobSubmitted(jobId: Int,finalRDD: RDD[_],func: (TaskContext, Iterator[_]) => _,partitions: Array[Int],callSite: CallSite,listener: JobListener,properties: Properties): Unit = {var finalStage: ResultStage = nulltry {//创建第一个 Stage finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)} catch {.......}//作业已提交,内部数据清理。barrierJobIdToNumTasksCheckFailures.remove(jobId)val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)clearCacheLocs()//提交finalStage 但首先要递归提交该阶段依赖的所有父stagesubmitStage(finalStage)}//递归方法private def submitStage(stage: Stage): Unit = {val jobId = activeJobForStage(stage)if (jobId.isDefined) {if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {//将 finalStage 传入 获得其 依赖的 Stageval missing = getMissingParentStages(stage).sortBy(_.id)//如果该Stage上一层没有Stage了说明到头了,开始提交这个最初的第一个stageif (missing.isEmpty) {如果上一层没有Stage了那么就提交这层起始的stagesubmitMissingTasks(stage, jobId.get)} else {for (parent <- missing) {//如果Stage前面还有Stage,就继续提交,看是否还有上一层StagesubmitStage(parent)}waitingStages += stage}}} else {abortStage(stage, "No active job for stage " + stage.id, None)}}private def getMissingParentStages(stage: Stage): List[Stage] = {val missing = new HashSet[Stage]  //缺失的Stageval visited = new HashSet[RDD[_]] //访问过的RDD// 我们在这里手动维护一个堆栈,以防止递归访问引起的StackOverflowError//因为递归需要一直将方法压栈 ,最后回归时再弹栈,如果递归太深很容易导致栈溢出val waitingForVisit = new ListBuffer[RDD[_]]waitingForVisit += stage.rdd //等待访问的RDD,最开始就是一个Job的最后那个RDDdef visit(rdd: RDD[_]): Unit = {if (!visited(rdd)) {visited += rdd //这个rdd 已经访问过了val rddHasUncachedPartitions = getCacheLocs(rdd).contains(Nil)if (rddHasUncachedPartitions) {//从这个循环我们可以看出,rdd的依赖是多个的,也就是一个rdd可能来自于1-多个rdd的数据//我们详细看下rdd的依赖关系for (dep <- rdd.dependencies) {dep match {case shufDep: ShuffleDependency[_, _, _] =>//如果RDD和父RDD是Shuffle依赖,就创建一个ShuffleMapStageval mapStage = getOrCreateShuffleMapStage(shufDep, stage.firstJobId)// 只有在基于推送的混洗合并完成后,才将mapStage标记为可用于混洗输出。// 如果没有,后续ShuffleMapStage将不会从合并的输出中读取,因为MergeStatuses不可用。if (!mapStage.isAvailable || !mapStage.shuffleDep.shuffleMergeFinalized) {//添加到在丢失的Stage Set中missing += mapStage} else {//如果跳过并首次被访问,则转发nextAttemptId。否则一旦重试,//  1) 阶段信息中的内容变得扭曲,例如任务编号、输入字节、e.t.c//  2) 第一次尝试从0-idx开始,不会标记为重试mapStage.increaseAttemptIdOnFirstSkip()}case narrowDep: NarrowDependency[_] =>//当这个RDD和它的父RDD时窄依赖时,放到栈中,继续往下寻找waitingForVisit.prepend(narrowDep.rdd)}}}}}//循环这个栈,看是否还有RDD,如果还有就说明还没有窄依赖的父RDDwhile (waitingForVisit.nonEmpty) {visit(waitingForVisit.remove(0))}//返回该stage上一层的stage列表missing.toList}}

2、获取与父RDDs的依赖关系

abstract class RDD[T: ClassTag](@transient private var _sc: SparkContext,@transient private var deps: Seq[Dependency[_]]) extends Serializable with Logging {//我们的依赖关系和分区将通过调用下面的子类方法获得,并在我们被检查点时被覆盖@volatile private var dependencies_ : Seq[Dependency[_]] = _//获取此RDD的依赖关系列表,同时考虑RDD是否为检查点。final def dependencies: Seq[Dependency[_]] = {//OneToOneDependency 是表示父RDD和子RDD的分区之间的一对一依赖关系。checkpointRDD.map(r => List(new OneToOneDependency(r))).getOrElse {if (dependencies_ == null) {stateLock.synchronized {if (dependencies_ == null) {//在此处才真正去获取此RDD的依赖关系dependencies_ = getDependencies}}}dependencies_}}//由子类实现,以返回此RDD如何依赖父RDD。此方法只会被调用一次,因此在其中实现耗时的计算是安全的。//因此不同形态的rdd 获取依赖关系的方法也不同,它的子类有//CoGroupedRDD 有OneToOneDependency (当父子RDD分区数一样时,也就是可以认为调整分区数来调整某些场景的宽窄依赖) 也有 ShuffleDependency//CoalescedRDD 只有 NarrowDependency//ShuffledRDD  只有ShuffleDependency,需要序列化管理器//SubtractedRDD 有OneToOneDependency (当父子RDD分区数一样时) 也有 ShuffleDependency//CartesianRDD  只有固定的两个NarrowDependency//UnionRDD RangeDependency 它 继承了 NarrowDependency 时窄依赖 (表示父RDD和子RDD中分区范围之间的一对一依赖关系)//CoGroupedRDD 、SubtractedRDD  宽窄依赖都有可能,可以通过父子分区数调节//CoalescedRDD 、CartesianRDD  、UnionRDD 都是窄依赖关系 //ShuffledRDD  必然是宽依赖,因为都开始准备序列化管理器拉数据了protected def getDependencies: Seq[Dependency[_]] = deps}//继承了一个NarrowDependency 有一个未实现的方法 即: 获取父rdd的分区数量
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

 我们根据一个例子来捋一下

/*** 该程序只作为学习用,没有任何业务知识哈*/
object StageDivision {def main(args: Array[String]): Unit = {val conf = new SparkConf().setAppName("StageDivision").setMaster("local")val sc = new SparkContext(conf)val sourceRdd1 = sc.textFile("file/word_count_data.txt")val sourceRdd2 = sc.textFile("file/word_count_data2.txt")val sourceRdd3 = sc.textFile("file/word_count_data3.txt")val stepOneRdd = sourceRdd1.filter(_.length>10).flatMap(_.split(",")).map(x=>{(x,1)}).groupByKey().map(data=>{(data._1,data._2.filter(_.>(5)).toList.length)})val stepTwoRdd = sourceRdd2.flatMap(_.split(",")).map(x=>{(x,10)})val stepThreeRdd = sourceRdd3.flatMap(_.split(",")).map(x=>{(x,20)}).reduceByKey(_+_)val unionStepOneTwoRdd = stepOneRdd.union(stepTwoRdd)val countRdd = unionStepOneTwoRdd.reduceByKey(_+_).union(stepThreeRdd)val resultMap = countRdd.collect().toMapsc.stop()}

该程序的DAG如下图:

1、submitStage(Stage3)

2、根据RDD依赖关系找上一层Stage:Stage2和Stage1

3、执行submitStage(Stage2)和submitStage(Stage1)

4、根据RDD依赖关系找Stage2和Stage1的上一层Stage,

        Stage2没有上一层了,执行submitMissingTasks(stage2, jobId.get)

        Stage1还有上一层Stage:执行submitStage(Stage0)

5、根据RDD依赖关系找Stage0的上一层Stage

        Stage0没有上一层了,执行submitMissingTasks(stage0, jobId.get)

注意:其中只有Stage3为ResultStage,其他Stage均为ShuffleMapStage

四、提交Task

当根据RDD的依赖关系划分完Stage,就开始从每个分支的最外层Stage提交Task了,即执行submitMissingTasks(最外层Stage)

private[spark] class DAGScheduler(...){//决定任务是否可以将输出提交到HDFS。使用“第一提交者获胜”策略//OutputCommitCoordinator在 driver 和 executor 中都被实例化。在executor 上,它配置了对driver OutputCommitCoordinatorEndpoint的引用,因此提交输出的请求将被转发到driver的OutputCommitCoordinator。private [scheduler] val outputCommitCoordinator = env.outputCommitCoordinatorprivate def submitMissingTasks(stage: Stage, jobId: Int): Unit = {//计算出分区ID的索引Seq。val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()//当一个Stage 启动会有DAGScheduler调度,并对Stage进行初始化stage match {case s: ShuffleMapStage =>outputCommitCoordinator.stageStart(stage = s.id, maxPartitionId = s.numPartitions - 1)case s: ResultStage =>outputCommitCoordinator.stageStart(stage = s.id, maxPartitionId = s.rdd.partitions.length - 1)//计算每一个分区的所有位置val taskIdToLocations: Map[Int, Seq[TaskLocation]] = try {stage match {case s: ShuffleMapStage =>partitionsToCompute.map { id => (id, getPreferredLocs(stage.rdd, id))}.toMapcase s: ResultStage =>partitionsToCompute.map { id =>val p = s.partitions(id)(id, getPreferredLocs(stage.rdd, p))}.toMap}}//为此 Stage 创建新的尝试 ,也就是Stage有失败重试的机制stage.makeNewStageAttempt(partitionsToCompute.size, taskIdToLocations.values.toSeq)//为 Task 广播二进制文件,用于将 Task 分派给 executor。//请注意,我们广播了RDD的序列化副本,对于每个Task,我们将对其进行反序列化,//这意味着每个任务都会得到一个不同的RDD副本。这在可能修改闭包中引用的对象状态的任务之间提供了更强的隔离。//这在Hadoop中是必要的,因为JobConf/Configuration对象不是线程安全的。var taskBinary: Broadcast[Array[Byte]] = nullvar partitions: Array[Partition] = nulltry {//对于ShuffleMapTask 序列化并广播  (rdd, shuffleDep)//对于ResultTask 序列化并广播 (rdd, func)var taskBinaryBytes: Array[Byte] = nullRDDCheckpointData.synchronized {taskBinaryBytes = stage match {case stage: ShuffleMapStage =>JavaUtils.bufferToArray(closureSerializer.serialize((stage.rdd, stage.shuffleDep): AnyRef))case stage: ResultStage =>JavaUtils.bufferToArray(closureSerializer.serialize((stage.rdd, stage.func): AnyRef))}partitions = stage.rdd.partitions}//如果Task序列化后的大小 > 1 M 就会发出警告if (taskBinaryBytes.length > TaskSetManager.TASK_SIZE_TO_WARN_KIB * 1024) {logWarning(s"Broadcasting large task binary with size " +s"${Utils.bytesToString(taskBinaryBytes.length)}")}//将序列化后的Task广播到每个Executor//有多分区就有多少Task,虽然每个Task会按照分区位置移动到最佳的Executor,但是它们的计算逻辑是一样的,因此可以直接广播taskBinary = sc.broadcast(taskBinaryBytes)}//ShuffleMapStage  ---对应---> ShuffleMapTask//ResultStage   ----对应--->  ResultTaskval tasks: Seq[Task[_]] = try {val serializedTaskMetrics = closureSerializer.serialize(stage.latestInfo.taskMetrics).array()stage match {case stage: ShuffleMapStage =>partitionsToCompute.map { id =>new ShuffleMapTask(...)}case stage: ResultStage =>partitionsToCompute.map { id =>new ResultTask(...)}}}//提交任务//会调用 TaskSchedulerImpl.submitTasks() 是在构建SparkContext时就指定好的taskScheduler.submitTasks(new TaskSet(...))}//获取与特定RDD的分区关联的局部性信息//该方法时一个递归方法,知道找到这个Stage的最除的那个rdd,然后获取每个分区的最佳位置//根据不同rdd的类型有不同的算法,我们单拿出来分析下private[spark]def getPreferredLocs(rdd: RDD[_], partition: Int): Seq[TaskLocation] = {getPreferredLocsInternal(rdd, partition, new HashSet)}private def getPreferredLocsInternal(rdd: RDD[_],partition: Int,visited: HashSet[(RDD[_], Int)]): Seq[TaskLocation] = {//如果分区已被访问,则无需重新访问if (!visited.add((rdd, partition))) {return Nil}//如果分区已经缓存了,那么返回缓存的地址//如果你的rdd会被多个rdd使用,那么可以缓存起来,其他rdd使用时可以直接从缓存拿数据val cached = getCacheLocs(rdd)(partition)if (cached.nonEmpty) {return cached}// 获取分区的首选位置,同时考虑RDD是否为检查点。// 如果rdd设置了首选位置,那么直接使用它val rddPrefs = rdd.preferredLocations(rdd.partitions(partition)).toListif (rddPrefs.nonEmpty) {return rddPrefs.filter(_ != null).map(TaskLocation(_))}//如果RDD具有窄依赖关系,请选择具有任何放置偏好的第一个窄依赖关系的第一个分区。//理想情况下,我们会根据传输大小进行选择,但现在就可以了。//递归方法:同一个Stage的分区也都是窄依赖的,因此需要获取到这个Stage的第一个RDD的那个分区中的最佳位置rdd.dependencies.foreach {case n: NarrowDependency[_] =>for (inPart <- n.getParents(partition)) {val locs = getPreferredLocsInternal(n.rdd, inPart, visited)if (locs != Nil) {return locs}}case _ =>}Nil}}

Task判断分发到哪个Executor

从上面我们已经知道Task的个数 = 这个Stage的最后的那个RDD的分区数量,且需要对每个分区进行递归找到这个Stage最初的那个RDD,找到其数据的存放位置,如果这个位置所在节点也正好有启动的存活的Executor,那么这就是Task要分发的目的地了,下面我们详细看下不同RDD类型对应的位置计算逻辑

1、HadoopRDD

  private[spark] def convertSplitLocationInfo(infos: Array[SplitLocationInfo]): Option[Seq[String]] = {Option(infos).map(_.flatMap { loc =>val locationStr = loc.getLocationif (locationStr != null && locationStr != "localhost") {//优先取在内存中存放的数据节点上的Excutorif (loc.isInMemory) {logDebug(s"Partition $locationStr is cached by Hadoop.")Some(HDFSCacheTaskLocation(locationStr).toString)} else {Some(HostTaskLocation(locationStr).toString)}} else {None}})}

2、ShuffledRDD

  //返回在给定Shuffle中运行给定map输出分区的首选主机,即该分区输出最多的节点。//如果映射输出是预合并的,那么如果合并率高于阈值,则返回合并块所在的节点。def getPreferredLocationsForShuffle(dep: ShuffleDependency[_, _, _], partitionId: Int): Seq[String] = {val shuffleStatus = shuffleStatuses.get(dep.shuffleId).orNullif (shuffleStatus != null) {//检查map输出是否已预合并,合并比率是否高于阈值。如果是这样,合并块的位置是首选位置//判断是否启用基于推送的Shuffle//满足以下条件://    提交应用程序以在YARN模式下运行//    已启用外部洗牌服务//    IO加密已禁用//    序列化器(如KryoSerialer)支持重新定位序列化对象val preferredLoc = if (pushBasedShuffleEnabled) {shuffleStatus.withMergeStatuses { statuses =>val status = statuses(partitionId)val numMaps = dep.rdd.partitions.lengthif (status != null && status.getNumMissingMapOutputs(numMaps).toDouble / numMaps<= (1 - REDUCER_PREF_LOCS_FRACTION)) {Seq(status.location.host)} else {Nil}}} else {Nil}if (preferredLoc.nonEmpty) {preferredLoc} else {//是否计算reduce任务的局部偏好 spark.shuffle.reduceLocality.enabled 默认 true//rdd的分区数 < 1000 (map和reduce任务的数量,超过此数量,我们不会根据map输出大小分配首选位置。我们限制了分配首选位置的作业的大小,因为按大小计算顶部位置变得昂贵。)//也就是如果shuffle过程中分区数大于1000就没有什么最佳位置的概念了,也就造成了速度的不确定性if (shuffleLocalityEnabled && dep.rdd.partitions.length < SHUFFLE_PREF_MAP_THRESHOLD &&dep.partitioner.numPartitions < SHUFFLE_PREF_REDUCE_THRESHOLD) {val blockManagerIds = getLocationsWithLargestOutputs(dep.shuffleId, partitionId,dep.partitioner.numPartitions, REDUCER_PREF_LOCS_FRACTION)if (blockManagerIds.nonEmpty) {blockManagerIds.get.map(_.host)} else {Nil}} else {Nil}}} else {Nil}}

五、总结 

1、线性解析程序中的代码,遇到Action算子调用SparkContext的runJob(),有几个Action算子就会产生几个Job

2、转交给DAGScheduler提交Job

3、DAGScheduler先为调用Action算子的RDD创建一个ResultStage

4、以ResultStage为其实递归调用submitStage(Stage)获取上一层的Stage,直到没有依赖关系(详细请看第三部分:Stage划分)(只有最后一个Stage叫ResultStage,其他Stage叫ShuffleMapStage )

5、从最前一层的Stage依次处理,计算分区数量以及每份分区对应数据的最佳位置的节点上的Executor,因为最终Task是要发到Executor执行的(每个Stage的第一个RDD类型不同计算最佳位置的方式也不同,详细看第四部分中的:Task判断分发到哪个Executor)

6、将Task逻辑序列化并交由TaskScheduler进行调度


http://www.ppmy.cn/server/106574.html

相关文章

游戏学(ludology):概念、现状与发展展望?

游戏学&#xff08;ludology&#xff09;&#xff1a;概念、现状与发展展望&#xff1f; 李升伟 以下是关于“游戏学&#xff08;ludology&#xff09;&#xff1a;概念、现状与发展展望”的综述&#xff1a; 一、概念 游戏学&#xff08;ludology&#xff09;是专门研究…

Neo4j - CQL简介

简述 CQL 代表密码查询语言。就像 Oracle 数据库有查询语言 SQL&#xff0c;Neo4j 有 CQL 作为查询语言。 Neo4j CQL 是 Neo4j 图形数据库的查询语言。是一种声明性模式匹配语言。遵循类似 SQL 的语法。语法非常简单并且是人类可读的格式。 像 Oracle SQL Neo4j CQL 具有执…

数学基础 -- 微积分之分部积分法

分部积分法 分部积分法&#xff08;Integration by Parts&#xff09;是求解定积分和不定积分的一种重要方法&#xff0c;通常用于积分中包含两个函数的乘积的情况。分部积分法的基本原理来自于微积分中的乘积法则。 分部积分法的公式如下&#xff1a; ∫ u d v u ⋅ v − ∫…

锂电池规格 —— 参数解读

目 录 锂电池规格一、充电参数二、放电参数 锂电池规格 现在人们日常使用的电子产品&#xff0c;它的续航都是依赖可以充放电的锂电池。锂电池的能量是其续航能力的重要指标。 锂电池表面贴的标签&#xff0c;通常印有多少mAh&#xff0c;多少V&#xff0c;以及多少Wh这些字样…

华为云App合规检测常见问题(app安全评估报告+资料)

App合规检测任务状态显示失败如何处理&#xff1f; App合规检测任务状态显示失败的常见原因有&#xff1a; 应用文件解析异常&#xff1a;应用文件本身存在不完整、结构异常等问题&#xff0c;导致服务无法正常解析。提供正确的应用文件重新创建扫描任务即可。 应用文件上传中损…

《Spring Boot 集成 Swagger:打造高效接口文档与开发体验》

Swagger 一.导语&#xff1a; ​ 相信无论是前端还是后端开发&#xff0c;都或多或少地被接口文档折磨过。前端经常抱怨后端给的接口文档与实际情况不一致。后端又觉得编写及维护接口文档会耗费不少精力&#xff0c;经常来不及更新。其实无论是前端调用后端&#xff0c;还是后…

百度网盘电脑版播放视频没有声音 - 解决方案

问题 百度网盘电脑版播放网盘中的视频没有声音&#xff0c;这可能是因为网盘的音量设置较小导致的。下文介绍解决方案。 解决方案 确保电脑系统音量开启&#xff0c;并调整到合适音量。使用本地视频播放器播放本地视频&#xff0c;检查扬声器是否工作。在百度网盘中播放网盘…

大数据基础:数仓架构演变

文章目录 数仓架构演变 一、传统离线大数据架构 二、​​​​​​Lambda架构 三、Kappa架构 四、​​​​​​​​​​​​​​混合架构 五、湖仓一体架构 六、流批一体架构 数仓架构演变 20世纪70年代&#xff0c;MIT(麻省理工)的研究员致力于研究一种优化的技术架构&…

C++类和对象(总篇)

文章目录 C类和对象1、类的定义1.1类定义格式1.2访问限定符1.3类域 2、实例化2.1实例化概念 3、this指针4、类的默认成员函数5、构造函数5.1构造函数的特点5.2实例分析 6、析构函数6.1析构函数的特点6.2实例分析 7、拷贝构造函数7.1 拷贝构造函数的特点7.2实例分析7.3浅拷贝和深…

金融基础知识-股票投资

以下内容仅供学习参考 股票投资 如果没有特别指出&#xff0c;那么本部分所讲的业务都是指A股业务。 1. 股票买卖 参与主体 投资者个人&#xff0c;基金公司&#xff0c;保险公司&#xff0c;保险资产管理公司&#xff0c;其他机构券商交易所结算公司 业务流程T日&#xf…

智能便携式环境噪声监测系统:集成快速傅里叶变换与噪声源识别算法(代码详解)

一、项目概述 在现代城市中&#xff0c;环境噪声污染已成为一个不容忽视的问题。为了帮助政府和相关部门有效监测和管理噪声污染&#xff0c;我们开发了一款便携式环境噪声监测系统。该系统可以实时采集周围环境的音频信号&#xff0c;分析噪声水平&#xff0c;并通过无线通信…

企业级NoSql数据库 --- Redis集群

什么是 redis ? Redis是一个开源的、遵循BSD协议的、基于内存的而且目前比较流行的键值数据库(key-value database)&#xff0c;是一个非关系型数据库&#xff0c;redis 提供将内存通过网络远程共享的一种服务&#xff0c;提供类似功能的还有memcached&#xff0c;但相比memc…

自动化分支合并:一键切换到Master并完成合并操作的脚本

在日常开发中,团队协作往往需要频繁地将功能分支合并到主干(master)分支。这一过程虽然看似简单,但在多个项目中手动切换分支、拉取最新代码、解决潜在冲突、再进行合并的操作,既耗时又容易出错。 为了解决这一问题,我们可以编写一个简单而高效的自动化脚本,让你在终端…

基于R语言遥感随机森林建模与空间预测

随机森林作为一种集成学习方法&#xff0c;在处理复杂数据分析任务中特别是遥感数据分析中表现出色。通过构建大量的决策树并引入随机性&#xff0c;随机森林在降低模型方差和过拟合风险方面具有显著优势。在训练过程中&#xff0c;使用Bootstrap抽样生成不同的训练集&#xff…

详谈进程等待

目录 前言1. 进程等待的必要性1.1 进程等待的定义 2. 如何进行进程等待2.1 wait 单进程2.2 wait 多进程2.3 status && 退出情况2.3.1 status 参数构成2.3.2 简证 status 参数构成2.3.3 进程等待失败2.3.4 宏调用查看退出信息 3. 进程等待的原理 前言 本篇文章继上一篇…

android aar适配uniapp

最近有商户需要接入我们sdk&#xff0c;但是我们都是android或者ios原生的&#xff0c;直接用又不能用&#xff0c;需要做适配&#xff0c;本文就教你一步步实现android aar适配uniapp。 官方参考教程&#xff1a;开发者须知 | uni小程序SDK 但是官方写的比较繁琐&#xff0c;好…

Windows安装nexus 私服仓库(6)

1.私服 安装 配置nexus 下载你需要的环境 Windows 第一个文件夹是运行程序 第二个文件夹是工作空间 去运行exe 这个端口是可以改 点击进去 修改完重启就可以了 2.私服资源获取 然后登录nexus 账号是admin 密码在这个文件里 然后登录后提醒你修改密码 只能修改密码 名称不…

协议汇总 TCP、UDP、Http、Socket、Web Scoket、Web Service、WCF、API

TCP&#xff1a;   &#xff08;1&#xff09;位于OSI传输层&#xff0c;基于soap&#xff08;信封&#xff09;协议&#xff1b; &#xff08;2&#xff09;数据格式是xml、Json&#xff1b; &#xff08;3&#xff09;是面向连接的&#xff0c;需要先建立连接&#xff1b…

CRUD的最佳实践,联动前后端,包含微信小程序,API,HTML等(二)

CRUD老生常谈&#xff0c;但是我搜索了一圈&#xff0c;发觉几乎是着重在后端&#xff0c;也就是API部分&#xff01; 无外乎2个思路 1.归总的接口&#xff0c;比如一个接口&#xff0c;实现不同表的CRUD 2.基于各自的表&#xff0c;使用代码生成器实现CRUD 个人来说是推荐2&am…

Linux下单网卡配置多个路由ip方法

Linux下配置网卡ip别名何谓ip别名 用windows的话说&#xff0c;就是为一个网卡配置多个ip。 什么场合增加ip别名能派上用场&#xff1f; 布网需要、多ip访问测试、特定软件对多ip的需要 下面通过几个例子简单介绍一下如何使用ifconfig命令给网卡配置ip别名。 一、首先为服务器…