SPARKSQL3.0-Optimizer阶段源码剖析

news2024/10/6 6:44:29

一、前言

阅读本节需要先掌握【SPARKSQL3.0-Analyzer阶段源码剖析】

Optimizer阶段是对Analyzer分析阶段的逻辑计划【logicalPlan】做进一步的优化,将应用各种优化规则对一些低效的逻辑计划进行转换

例如将原本用户不合理的sql进行优化,如谓词下推,列裁剪,子查询共用等。

本质:【optimizer阶段是对 Analyzer生成的LogicalPlan进行优化调整生成新的LogicalPlan】

这个阶段的优化器同样是基于大量的规则(Rule),而大部分的规则都是基于直观或经验而得出的规则,随着spark社区的活跃,此优化器阶段的规则也逐渐完善丰满,可以说每一个优化规则都有其诞生的原因,不可不查。

二、示例

由于上一节的示例在优化器中没有明显sql的优化,我们这次举一个谓词下推的优化过程,其他优化规则可根据源码自行推导

此处示例使用的是dataFrameAPI,其原理和spark.sql(“”)底层大致相同,想深入了解两者区别可以看我的另一篇文章:

代码:

spark
	.range(2)
	.select('id as "_id")
	.filter('_id === 0)
	.explain(true)

打印:

== Parsed Logical Plan ==
'Filter ('_id = 0)
+- Project [id#0L AS _id#2L]
   +- Range (0, 2, step=1, splits=Some(2))

== Analyzed Logical Plan ==
_id: bigint
Filter (_id#2L = cast(0 as bigint))
+- Project [id#0L AS _id#2L]
   +- Range (0, 2, step=1, splits=Some(2))

== Optimized Logical Plan ==
Project [id#0L AS _id#2L]
+- Filter (id#0L = 0)
   +- Range (0, 2, step=1, splits=Some(2))

......

可以看出在Optimized Logical Plan阶段中,Filter谓词节点从父节点下推到了子节点,假设数据源是数据库,那么这一步谓词下推优化可以将过滤条件下沉到数据库层面进行物理过滤,可以明显减少spark读取的数据量和带宽

三、源码

和Analyzer阶段不同,Optimizer阶段需要有action操作才会触发,这也是正常的,因其dateFrame的懒加载特点有关。

比如我们曾经执行:

val dataFrame = spark.sql("SELECT * FROM (SELECT * FROM PERSON WHERE AGE > 19) WHERE AGE > 18")

在sparkSession的sql函数中内部经历了Unanalyze和Analyze阶段后就返回新的DateSet:

def sql(sqlText: String): DataFrame = withActive {
    val tracker = new QueryPlanningTracker
    val plan = tracker.measurePhase(QueryPlanningTracker.PARSING) {
      sessionState.sqlParser.parsePlan(sqlText) // Unresolved阶段
    }
    Dataset.ofRows(self, plan, tracker) 
}

def ofRows(sparkSession: SparkSession, logicalPlan: LogicalPlan, tracker: QueryPlanningTracker)
    : DataFrame = sparkSession.withActive {
    val qe = new QueryExecution(sparkSession, logicalPlan, tracker)
    qe.assertAnalyzed()	// Resolved阶段
    new Dataset[Row](qe, RowEncoder(qe.analyzed.schema)) // 返回给main函数
}

而Optimizer阶段则只有在action操作触发物理执行计划的时候才会调用[比如dataFrame.take(1)],这里我们看QueryExecution类中的操作,如下:

// sparkPlan是物理执行计划,下一节会详细介绍
lazy val sparkPlan: SparkPlan = {
    // 可以看到只有到物理执行计划才会调用assertOptimized函数
    assertOptimized()
    executePhase(QueryPlanningTracker.PLANNING) {
      QueryExecution.createSparkPlan(sparkSession, planner, optimizedPlan.clone())
    }
  }

......
private def assertOptimized(): Unit = optimizedPlan // 调用lazy变量optimizedPlan

......
lazy val optimizedPlan: LogicalPlan = executePhase(QueryPlanningTracker.OPTIMIZATION) {
    // 调用executeAndTrack执行优化策略,参数是withCachedData.clone()克隆结果【其实就是Analyzed-logicalPlan的克隆类】
    sparkSession.sessionState.optimizer.executeAndTrack(withCachedData.clone(), tracker)
  }

......
// withCachedData函数是执行analyzed阶段
lazy val withCachedData: LogicalPlan = sparkSession.withActive {
    assertAnalyzed() // 如果没有解析先进行解析
    assertSupported() 
  	// 将analyzed解析后的logicalPlan克隆一份给cacheManager
    sparkSession.sharedState.cacheManager.useCachedData(analyzed.clone())
  }

......
def assertAnalyzed(): Unit = analyzed

// 上一节讲到的analyzed阶段
lazy val analyzed: LogicalPlan = executePhase(QueryPlanningTracker.ANALYSIS) {
    sparkSession.sessionState.analyzer.executeAndCheck(logical, tracker)
  }

可以看出optimized阶段是在action操作后才会执行,这样符合懒加载的设计初衷

接下来看一下sparkSession.sessionState.optimizer.executeAndTrack的操作

首先sparkSession.sessionState的optimizer是在BaseSessionStateBuilder类中构建的,默认是SparkOptimizer

这块在SparkSession构建一节讲过:

image-20220709183836943

而SparkOptimizer又继承于Optimizer,而Optimizer又继承于RuleExecutor,继承关系如下:

image-20220709183943042

故sparkSession.sessionState.optimizer.executeAndTrack实际上还是走的RuleExecutor类中executeAndTrack函数,这和anlyzer阶段走的函数完全一致,这里贴一下源码:

这里同样也是将所有规则构建为多个批次,并且将所有批次中规则应用于Analyzed LogicalPlan,直到树不再改变或者执行优化的循环次数超过最大限制(spark.sql.optimizer.maxIterations,默认100)

def executeAndTrack(plan: TreeType, tracker: QueryPlanningTracker): TreeType = {
    QueryPlanningTracker.withTracker(tracker) {
      execute(plan) // 调用execute函数
    }
  }

def execute(plan: TreeType): TreeType = {
    var curPlan = plan
    val queryExecutionMetrics = RuleExecutor.queryExecutionMeter
    val planChangeLogger = new PlanChangeLogger()
    val tracker: Option[QueryPlanningTracker] = QueryPlanningTracker.get
    val beforeMetrics = RuleExecutor.getCurrentMetrics()

    // 针对初始输入plan,运行结构完整性检查
    if (!isPlanIntegral(plan)) {
      val message = "The structural integrity of the input plan is broken in " +
        s"${this.getClass.getName.stripSuffix("$")}."
      throw new TreeNodeException(plan, message, null)
    }
	// 遍历不同子类实现的batches中定义的 batchs 变量, 此处batches是用的Optimizer子类的实现
    batches.foreach { batch =>
      // 用来对比执行规则前后,初始的plan有无变化
      val batchStartPlan = curPlan
      var iteration = 1
      var lastPlan = curPlan
      var continue = true

      // 执行直到达到稳定点或者最大迭代次数
      while (continue) {
        curPlan = batch.rules.foldLeft(curPlan) {
          case (plan, rule) =>
            val startTime = System.nanoTime()
            val result = rule(plan)
            val runTime = System.nanoTime() - startTime
            val effective = !result.fastEquals(plan)

            if (effective) {
              queryExecutionMetrics.incNumEffectiveExecution(rule.ruleName)
              queryExecutionMetrics.incTimeEffectiveExecutionBy(rule.ruleName, runTime)
              planChangeLogger.logRule(rule.ruleName, plan, result)
            }
            queryExecutionMetrics.incExecutionTimeBy(rule.ruleName, runTime)
            queryExecutionMetrics.incNumExecution(rule.ruleName)
            tracker.foreach(_.recordRuleInvocation(rule.ruleName, runTime, effective))
            if (!isPlanIntegral(result)) {
              val message = s"After applying rule ${rule.ruleName} in batch ${batch.name}, " +
                "the structural integrity of the plan is broken."
              throw new TreeNodeException(result, message, null)
            }
            result
        }
        iteration += 1
        // 到达最大迭代次数, 不再执行优化
        if (iteration > batch.strategy.maxIterations) {
          // 只对最大迭代次数大于1的情况打log
          if (iteration != 2) {
            val endingMsg = if (batch.strategy.maxIterationsSetting == null) {
              "."
            } else {
              s", please set '${batch.strategy.maxIterationsSetting}' to a larger value."
            }
            val message = s"Max iterations (${iteration - 1}) reached for batch ${batch.name}" +
              s"$endingMsg"
            if (Utils.isTesting || batch.strategy.errorOnExceed) {
              throw new TreeNodeException(curPlan, message, null)
            } else {
              logWarning(message)
            }
          }
          // 检查一次幂等
          if (batch.strategy == Once &&
            Utils.isTesting && !excludedOnceBatches.contains(batch.name)) {
            checkBatchIdempotence(batch, curPlan)
          }
          continue = false
        }
        // plan不变了,到达稳定点,不再执行优化
        if (curPlan.fastEquals(lastPlan)) {
          logTrace(
            s"Fixed point reached for batch ${batch.name} after ${iteration - 1} iterations.")
          continue = false
        }
        lastPlan = curPlan
      }
      planChangeLogger.logBatch(batch.name, batchStartPlan, curPlan)
    }
    planChangeLogger.logMetrics(RuleExecutor.getCurrentMetrics() - beforeMetrics)
    curPlan
  }
    
}

但是注意,这一次用的batches规则可和anlyzer的规则不同,因为RuleExecutor使用的是模版设计模式,batches为抽象函数,由不同子类来实现,此次batches通过多态会调用Optimizer的实现:

image-20220709184425857

这里又调用了defaultBatches

image-20220709184826938

而defaultBatches被Optimizer的子类SparkOptimizer实现:此处会合并父类的defaultBashes 和 子类的规则

image-20220709185048869

但大部分优化器的策略都在父类Optimizer的defaultBashes中,故这里贴一下父类的defaultBatches代码:

def defaultBatches: Seq[Batch] = {
    val operatorOptimizationRuleSet =
      Seq(
        // Operator push down
        PushProjectionThroughUnion,
        ReorderJoin,
        EliminateOuterJoin,
        PushDownPredicates,
        PushDownLeftSemiAntiJoin,
        PushLeftSemiLeftAntiThroughJoin,
        LimitPushDown,
        ColumnPruning,
        InferFiltersFromConstraints,
        // Operator combine
        CollapseRepartition,
        CollapseProject,
        CollapseWindow,
        CombineFilters,
        CombineLimits,
        CombineUnions,
        // Constant folding and strength reduction
        TransposeWindow,
        NullPropagation,
        ConstantPropagation,
        FoldablePropagation,
        OptimizeIn,
        ConstantFolding,
        ReorderAssociativeOperator,
        LikeSimplification,
        BooleanSimplification,
        SimplifyConditionals,
        RemoveDispensableExpressions,
        SimplifyBinaryComparison,
        ReplaceNullWithFalseInPredicate,
        PruneFilters,
        SimplifyCasts,
        SimplifyCaseConversionExpressions,
        RewriteCorrelatedScalarSubquery,
        EliminateSerialization,
        RemoveRedundantAliases,
        RemoveNoopOperators,
        SimplifyExtractValueOps,
        CombineConcats) ++
        extendedOperatorOptimizationRules

    val operatorOptimizationBatch: Seq[Batch] = {
      val rulesWithoutInferFiltersFromConstraints =
        operatorOptimizationRuleSet.filterNot(_ == InferFiltersFromConstraints)
      Batch("Operator Optimization before Inferring Filters", fixedPoint,
        rulesWithoutInferFiltersFromConstraints: _*) ::
      Batch("Infer Filters", Once,
        InferFiltersFromConstraints) ::
      Batch("Operator Optimization after Inferring Filters", fixedPoint,
        rulesWithoutInferFiltersFromConstraints: _*) :: Nil
    }

    val batches = (Batch("Eliminate Distinct", Once, EliminateDistinct) ::
    Batch("Finish Analysis", Once,
      EliminateResolvedHint,
      EliminateSubqueryAliases,
      EliminateView,
      ReplaceExpressions,
      RewriteNonCorrelatedExists,
      ComputeCurrentTime,
      GetCurrentDatabase(catalogManager),
      RewriteDistinctAggregates,
      ReplaceDeduplicateWithAggregate) ::
    Batch("Union", Once,
      CombineUnions) ::
    Batch("OptimizeLimitZero", Once,
      OptimizeLimitZero) ::
    Batch("LocalRelation early", fixedPoint,
      ConvertToLocalRelation,
      PropagateEmptyRelation) ::
    Batch("Pullup Correlated Expressions", Once,
      PullupCorrelatedPredicates) ::
    Batch("Subquery", FixedPoint(1),
      OptimizeSubqueries) ::
    Batch("Replace Operators", fixedPoint,
      RewriteExceptAll,
      RewriteIntersectAll,
      ReplaceIntersectWithSemiJoin,
      ReplaceExceptWithFilter,
      ReplaceExceptWithAntiJoin,
      ReplaceDistinctWithAggregate) ::
    Batch("Aggregate", fixedPoint,
      RemoveLiteralFromGroupExpressions,
      RemoveRepetitionFromGroupExpressions) :: Nil ++
    operatorOptimizationBatch) :+
    Batch("Early Filter and Projection Push-Down", Once, earlyScanPushDownRules: _*) :+
    Batch("Join Reorder", FixedPoint(1),
      CostBasedJoinReorder) :+
    Batch("Eliminate Sorts", Once,
      EliminateSorts) :+
    Batch("Decimal Optimizations", fixedPoint,
      DecimalAggregates) :+
    Batch("Object Expressions Optimization", fixedPoint,
      EliminateMapObjects,
      CombineTypedFilters,
      ObjectSerializerPruning,
      ReassignLambdaVariableID) :+
    Batch("LocalRelation", fixedPoint,
      ConvertToLocalRelation,
      PropagateEmptyRelation) :+
    // The following batch should be executed after batch "Join Reorder" and "LocalRelation".
    Batch("Check Cartesian Products", Once,
      CheckCartesianProducts) :+
    Batch("RewriteSubquery", Once,
      RewritePredicateSubquery,
      ColumnPruning,
      CollapseProject,
      RemoveNoopOperators) :+
    // This batch must be executed after the `RewriteSubquery` batch, which creates joins.
    Batch("NormalizeFloatingNumbers", Once, NormalizeFloatingNumbers)

    // remove any batches with no rules. this may happen when subclasses do not add optional rules.
    batches.filter(_.rules.nonEmpty)
  }

可以看到优化策略有很多,这里贴一下比较重要的一些规则,有些可能已经更名,但并不影响阅读:

image-20220709185437676

关于Batch和Rule类这里不再过多赘述,在Anlyzer阶段已经介绍过,这里我们主要关注谓词下推的优化策略:PushDownPredicates

接下来我们debug下示例中的代码,可以看到执行到了PushDownPredicates.apply函数,并且传入的logicalPlan和示例中的Anlyzer打印结果保持一致

image-20220709190652641

上图中可以看出PushDownPredicates是由三个类组成:CombineFilters、PushPredicateThroughNonJoin、PushPredicateThroughJoin

image-20220709191704419

由于实例中没有涉及到join,故过滤优化条件在PushPredicateThroughNonJoin中实现,这里贴一下部分源码:

object PushPredicateThroughNonJoin extends Rule[LogicalPlan] with PredicateHelper {
  def apply(plan: LogicalPlan): LogicalPlan = plan transform applyLocally

  val applyLocally: PartialFunction[LogicalPlan, LogicalPlan] = {
    // 处理Filter节点下为Project节点的情况
    case Filter(condition, project @ Project(fields, grandChild))
      if fields.forall(_.deterministic) && canPushThroughCondition(grandChild, condition) =>
      val aliasMap = getAliasMap(project)
      project.copy(child = Filter(replaceAlias(condition, aliasMap), grandChild))
    // 处理Filter节点下为Aggregate节点的情况
    case filter @ Filter(condition, aggregate: Aggregate)
      ......
    // 处理Filter节点下为Window节点的情况
    case filter @ Filter(condition, w: Window)
      ......
    // 处理Filter节点下为Union节点的情况
    case filter @ Filter(condition, union: Union) =>
      ......
    // 处理Filter节点下为水位线节点的情况
    case filter @ Filter(condition, watermark: EventTimeWatermark) =>
      ......
    // 处理Filter节点下为其他节点的情况
    case filter @ Filter(_, u: UnaryNode)
      ......
  }

根据示例此处直接走到第一个case:

image-20220709191914038

这里可以看到有两个判断条件:

1、 Project节点下的所有fields必须为确定性field

2、 canPushThroughCondition函数用于判断:condition[过滤条件]的输出和grandChild[子节点]的输出有交集

if fields.forall(_.deterministic) && canPushThroughCondition(grandChild, condition)

两个判断只有第一个判断较难理解:Project节点下的所有fields必须为确定性field,这是为何?

因为如果project里的字段是非确定性的话,下推前和下推后的查询效果不一样

比如:sql里用到了monotonically_increasing_id()函数(产生64位整数自增id的非确定性expression)

select a,b,id from (
  select  A,B,monotonically_increasing_id() as id from 
  testdata2  where a>2 
)tmp  where  b<1

# 如果下推,就相当于:
select a,b,id from (
  select  A,B,monotonically_increasing_id() as id from 
  testdata2  where a>2 and  b<1
)tmp

上面两个sql相比,过滤a>2 和 过滤(a>2 and b<1)两种情况下,该sql的数据得到的对应的自增id的情况是不一样的,其它的不确定函数还有rand()函数, 过滤a>2 和 过滤(a>2 and b<1)两种情况下,取rand() 的效果肯定也是不一样的,故要先判断Project节点下的所有fields必须为确定性field

image-20220709195046874

再回到case Filter:当符合条件后先获取对应关系,随后重新构建logicalPlan关系,完成谓词下推

image-20220709200315458

image-20220709200604507

最终返回的logicalPlan赋值给QueryExecution的optimizedPlan:符合示例打印结果,完成谓词下推

至此optimized阶段结束

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/25672.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PCB设计仿真之探讨源端串联端接

作者&#xff1a;一博科技高速先生成员 孙宜文 上期高速线生简单介绍了反射原理也提到了源端串联端接&#xff0c;笔者借此篇文章再深入探讨下&#xff0c;本文使用Sigrity Topology Explorer 17.4仿真软件。 搭建一个简单的电路模型&#xff0c;给一个上升沿和下降沿均为0.5…

学会使用这些电脑技巧,可以让你在工作中受益无穷

技巧一&#xff1a;设置计算机定时关机 第一步&#xff1a;快捷键win r打开运行窗口。 第二步&#xff1a;输入&#xff1a;shutdown -s -t 3600&#xff0c;其中数字3600表示3600秒&#xff0c;就是设置3600秒后关机。第三步&#xff1a;按确定完成设置。如果你想取消设置&…

吃透这份高并发/调优/分布式等350道面试宝典,已涨30k

前言 这一次的知识体系面试题涉及到 Java 知识部分、性能优化、微服务、并发编程、开源框架、分布式等多个方面的知识点。 写这一套 Java 面试必备系列文章的初衷。 整理自己学过的知识&#xff0c;总结&#xff0c;让其成为一套体系&#xff0c;方便日后查阅。现在不少 Java …

【MySQL运行原理篇】底层运行结构

MySQL整体架构图 简略版图 1.1连接管理 一句话&#xff1a;负责客户端连接服务器的部分 网络连接层, 对客户端的连接处理、安全认证、授权等&#xff0c;每个客户端连接都会在服务端拥有一个线程&#xff0c;每个连接发起的查询都会在对应的单独线程中执行。服务器上维护一…

社交媒体营销策略——如何病毒式传播:增加受众范围的9个技巧

关键词&#xff1a;社交媒体营销、病毒式传播、受众 社交营销人员知道创建病毒式帖子并不是他们最重要的目标。事实上&#xff0c;这可能会分散他们接触目标受众和照顾团队心理健康的注意力。 这并不意味着您无法从病毒式传播的帖子和活动中学到一些东西。战略性病毒式营销可提…

5分钟搞懂https原理

概念 https&#xff08;超文本传输安全协议&#xff09;是一种以安全为基础的HTTP传输通道。 在了解HTTPS之前&#xff0c;我们首先来认识一下http&#xff1a; http&#xff08;超文本传输协议&#xff09;&#xff0c;HTTP是tcp/ip族中的协议之一&#xff0c;也是互联网上…

React项目使用craco(由create-react-app创建项目)

适用&#xff1a;使用 create-react-app 创建项目&#xff0c;不想 eject 项目但想对项目中 wepback 进行自定义配置的开发者。 1.使用 create-react-app创建一个项目&#xff08;已有项目跳过此步&#xff09; $ npx create-react-app my-project 2.进入项目目录&#xff0c;…

一些http和tomcat知识补充

HTTP和HTTPS的区别  概念    HTTP英文全称是Hyper Text Transfer Protocol&#xff0c;超文本传输协议&#xff0c;用于在Web浏览器和网站服务器之间传递信息。 HTTP协议以明文方式发送内容&#xff0c;不提供任何方式的数据加密&#xff0c;如果攻击者截取了Web浏览器和…

D. Sequence and Swaps(思维)

Problem - 1455D - Codeforces 你的任务是使该序列排序&#xff08;如果条件a1≤a2≤a3≤⋯≤an成立&#xff0c;它就被认为是排序的&#xff09;。 为了使序列排序&#xff0c;你可以执行以下操作的任何次数&#xff08;可能是零&#xff09;&#xff1a;选择一个整数i&#…

数据结构 | 带头双向循环链表【无懈可击的链式结构】

不要被事物的表面现象所迷惑~&#x1f333;前言&#x1f333;结构声明&#x1f333;接口算法实现&#x1f34e;动态开辟&初始化【Init】&#x1f34e;尾插【PushBack】&#x1f34e;尾删【PopBack】&#x1f34e;头插【PushFront】&#x1f34e;头删【PopFront】&#x1f4…

思科防火墙应用NAT

♥️作者&#xff1a;小刘在C站 ♥️每天分享云计算网络运维课堂笔记&#xff0c;一起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的&#xff0c;绽放。 目录 一.思科防火墙的NAT 一种有四种&#xff0c; 二.动态NAT 配置 三.动态PAT配置 四…

哪些城市有PMP考试考点?PMP考试考场都在哪?

有不少伙伴对PMP的考试点存在一定的疑问&#xff0c;全国PMP考试具体考点位置是在哪呢&#xff1f; 根据过往常用考点&#xff0c;我们给大家汇总了2022年PMP考试全国考场地址&#xff0c;一起来看看吧&#xff01; 表格信息来自基金会官网11月27日PMP报名通知&#xff0c;仅作…

Spring Security多种用户定义方式

本文内容来自王松老师的《深入浅出Spring Security》&#xff0c;自己在学习的时候为了加深理解顺手抄录的&#xff0c;有时候还会写一些自己的想法。 Spring Security中存在两种类型的AutnenticationManager&#xff0c;一种是全局的AuthenticationManager&#xff0c;一种是局…

原版畅销36万册!世界级网工打造TCP/IP圣经级教材,第5版终现身

关于TCP/IP 现代网络中&#xff0c;以窃取信息或诈骗为目的的网站频频出现&#xff0c;蓄意篡改数据以及信息泄露等犯罪行为也在与日俱增。很多情况下&#xff0c;人们可能会认为人性本善&#xff0c;在享受着网络所带来的便捷性的同时&#xff0c;也就降低了对网络犯罪的设防…

深入理解java虚拟机:类文件结构(2)

文章目录Class类文件结构2.6 方法表集合2.7 属性表集合3. Class文件结构的发展Class类文件结构 接着上一篇&#xff0c;我们继续补充 2.6 方法表集合 Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式&#xff0c;方法表的结构如同字段表一样&#x…

算法设计与分析 SCAU17964 水桶打水

17964 水桶打水 时间限制:1000MS 代码长度限制:10KB 提交次数:25 通过次数:9 题型: 编程题 语言: G;GCC;VC;JAVA Description 有n个人&#xff08;n<100000&#xff09;带着大大小小的水桶容器&#xff08;每人一个水桶&#xff09;排队到r个&#xff08;r<1000&#…

JS基础习题

1.JavaScript变量命名规则 1.在JavaScript中以下&#xff0c;以下哪些变量名是非法的&#xff08;会导致程序报错&#xff09;&#xff1f; 1person name var $orderwera23 uiuiuqwer4_23aser2 A .1person&#xff0c;name B .name&#xff0c;var C .$orderwera23&#xff0c…

怎么自制小程序?【自己制作小程序】

说到制作小程序&#xff0c;有些动手能力比较强的小伙伴可能就在想怎么自制小程序。想自制小程序&#xff0c;至少也是需要会一些编程代码的&#xff0c;如果不懂代码就只能找其他方法了。那么怎么自制小程序呢&#xff1f; 现在市面上有很多的小程序制作工具&#xff0c;可以…

hexo建站新手入门

Hexo是一款基于Node.js的静态博客框架&#xff0c;依赖少易于安装使用&#xff0c;可以方便的生成静态网页托管在GitHub和Coding上&#xff0c;是搭建博客的首选框架。 hexo史上最全搭建教程 Hexo搭建步骤 安装Git安装Node.js安装HexoGitHub创建个人仓库生成SSH添加到GitHub…

几分钟让你了解Linux下文件权限掩码及作用

在说文件权限掩码之前&#xff0c;必须了解下linux下的文件权限&#xff0c;比如在linux下可以通过“ls -l”命令查看文件的权限&#xff0c;输入后可以获得如下显示&#xff1a; drwxrwxr-x 3 linux linux 4096 Jun 6 18:03 test 在显示中“drwxrwxr-x”这十个字母中第一个字母…