Spark submit求PI:分布式计算工作流验证实战

📅 发布时间:2026/10/13 13:54:56
Spark submit求PI:分布式计算工作流验证实战
1. 项目概述用Spark submit跑一个“求PI”的经典分布式计算实验你可能在刚接触大数据生态时就听过这么一句话“学Hadoop先写WordCount学Spark必跑求PI。”这句话不是玩笑——它背后藏着分布式计算最朴素也最精妙的逻辑内核。今天我们要做的就是用最标准、最贴近生产环境的方式通过spark-submit命令把一个计算圆周率π的程序真正提交到Spark集群上运行。这不是玩具代码而是你未来调试ETL任务、验证集群连通性、压测资源调度能力的第一块试金石。这个项目标题里“Spark submit”是动作“求PI”是目标二者组合起来实际指向的是一个端到端的分布式计算工作流验证场景。它不涉及业务数据清洗不依赖外部数据库但完整覆盖了本地开发→打包编译→集群资源申请→Driver启动→Executor分发→任务切片→Shuffle协调→结果聚合→日志回传→退出码校验——整条链路。我带过的几十个新人几乎都在这个环节卡过有人卡在jar包没包含依赖有人卡在master地址写错成local[*]还有人卡在YARN队列权限不足却只看到“Application failed with exit code: 137”这种玄学报错。所以这篇内容不是教你“怎么算π”而是带你把“spark-submit求PI”这件事从命令行敲下去的那一刻起每一个字符、每一个参数、每一个返回值都看得明明白白、调得稳稳当当。它适合三类人第一类是刚配好伪分布式Spark环境、想确认“我的集群是不是真活了”的新手第二类是准备面试大数据岗位、需要手写并解释Spark执行流程的求职者第三类是运维或平台工程师需要用最小开销验证新上线的K8s Spark Operator或YARN HA集群是否具备基础作业调度能力。不需要你懂蒙特卡洛原理但你要知道为什么用4 * (hits / total)能逼近π不需要你会写Scala但你要清楚--class后面跟的到底是主类全限定名还是对象名不需要你部署过ZooKeeper但你要明白--conf spark.masteryarn和--master yarn-client的区别在哪。接下来的内容全部基于真实集群环境反复验证所有参数值、错误日志、排查路径都来自某高校大数据实验室的模拟项目X和某公司离线计算平台的日常巡检记录。2. 核心设计思路与方案选型解析2.1 为什么是“求PI”而不是WordCount或PageRank很多人会疑惑既然WordCount更简单为什么教程总爱用求PI答案藏在任务特征里。WordCount本质是MapReduce范式下的典型宽依赖shuffle-heavy而求PI是一个计算密集型低通信开销结果可验证的黄金组合。计算密集型每个task执行的是大量随机数生成条件判断累加CPU占用高能有效暴露Executor GC压力、JVM参数不合理等问题低通信开销不需要跨节点传输中间键值对只有最终的sum(hits)和sum(total)两个Long值需要从Executor拉回Driver网络瓶颈几乎为零排除了网络配置干扰结果可验证π≈3.1415926… 是确定值只要total样本量足够大比如10亿结果偏差超过0.001就能立刻定位是代码逻辑错误、精度丢失还是集群时钟不同步导致的随机种子失效。我试过用10万次采样跑100遍结果稳定在3.14±0.02换成10亿次误差收敛到3.141592±0.000003。这种确定性是业务数据无法提供的。某次帮某公司排查K8s Pod被OOMKilled问题就是靠把求PI的total从1e8调到1e10让单个Executor内存从512MB涨到2GB直接复现了GC停顿超阈值的场景——这比任何监控图表都直观。2.2 为什么必须用spark-submit而不是spark-shell或IDEA本地运行spark-shell是交互式REPL它启动的是local模式的SparkContext所有计算都在本机JVM内完成根本没走集群调度器IDEA本地运行同理只是个Java进程。而spark-submit是Spark官方唯一支持的生产级作业提交入口它强制经过以下流程Client进程读取--master参数连接集群资源管理器YARN/K8s/Standalone向ResourceManager申请ContainerYARN或PodK8s启动Driver进程Driver解析DAG向ResourceManager申请Executor资源ResourceManager分配资源后NodeManager/Launcher启动Executor JVMExecutor反向注册到Driver建立Netty通信通道Driver分发TaskSetExecutor执行Task结果通过BlockManager回传。这个过程里任何一个环节出错都会在spark-submit的stdout/stderr中留下明确线索。比如Failed to connect to master→ 网络不通或master地址错误Application application_171... failed 2 times due to AM Container→ YARN ApplicationMaster启动失败可能是内存超限或jar包缺失NoClassDefFoundError: org/apache/spark/SparkConf→ 提交的jar未包含Spark核心依赖常见于Maven scope设置错误。提示永远不要用spark-shell --master yarn来“假装”集群运行。它只是把shell进程提交到YARN但后续所有sc.parallelize(...).count()仍是在Driver本地执行Executor根本没参与计算。真正的集群模式必须用spark-submit --master yarn --deploy-mode cluster。2.3 方案选型蒙特卡洛 vs. BBP公式 vs. Chudnovsky算法求π有上百种算法但只有蒙特卡洛法Monte Carlo被Spark官方示例采用。原因很实在算法并行友好度数据依赖精度控制实现复杂度Spark适配性蒙特卡洛★★★★★无各task独立采样通过total数量调节极简10行代码原生支持无需额外库BBP公式★★☆☆☆需要全局迭代状态固定位数如计算第n位中等需大数运算需自定义Accumulator易出错Chudnovsky★☆☆☆☆强依赖前序计算结果高精度百万位复杂多项式展开FFT不适合Spark DAG更适合MPI蒙特卡洛的核心思想是在边长为2的正方形内随机撒点统计落在内切圆半径1内的比例。正方形面积4圆面积π所以π ≈ 4 × (圆内点数 / 总点数)。这个逻辑天然支持“分而治之”——10亿次采样可以拆成1000个task每个task算100万次最后sum汇总。没有shuffle没有join没有reduceByKey只有map reduce完美契合Spark最基础的执行模型。某导师曾用这个例子给研究生讲“什么是良好的并行算法”好的并行算法不是看它多快而是看它失败成本有多低。蒙特卡洛中一个Executor宕机只损失1/1000的采样重跑该task即可而Chudnovsky算法中一个节点失败整个百万位计算前功尽弃。这就是工程思维和纯数学思维的本质区别。3. 核心细节解析与实操要点3.1 代码实现从单机版到集群版的三处关键改造很多初学者直接复制网上“Spark求PI”的Scala代码一提交就报错。问题往往出在三个被忽略的细节上第一处随机数生成器必须线程安全且可重现错误写法val r new scala.util.Random() // 全局单例多线程并发调用会出问题 val x r.nextDouble()正确写法每个task创建独立实例val hits sc.parallelize(1 to numSamples).map { i val r new scala.util.Random(i) // 用task索引做seed保证可重现 val x r.nextDouble() val y r.nextDouble() if (x*x y*y 1) 1 else 0 }.sum()为什么用i做seed因为parallelize(1 to N)生成的RDD分区是确定的每个partition的task id也是确定的。这样即使Executor重启只要numSamples不变随机序列就完全一致结果可复现。我踩过的坑某次用System.nanoTime()做seed结果在YARN集群上因各节点时钟漂移导致不同Executor生成的随机数分布不均π值偏差达0.05。第二处避免Driver端内存溢出OOM错误写法val data sc.parallelize(1 to 1000000000) // 直接生成10亿个IntDriver内存炸了正确写法用range替代parallelizeval data sc.range(0, numSamples, 1, partitions) // range是lazy的不占Driver内存sc.range底层调用的是RangePartitioner它只存储起始、结束、步长三个long值每个partition的iterator在Executor上才真正生成数字。而parallelize(List(1,2,3...))会先把整个List加载到Driver内存10亿个Int就是4GB远超默认的1G Driver内存。第三处结果聚合必须用Action不能只定义RDD错误写法val pi 4.0 * hits / numSamples // hits是RDD[Long]这里只是定义了转换没触发计算 println(sPi is roughly $pi) // 输出的是org.apache.spark.rdd.RDDxxxxx不是数值正确写法显式调用collect()或sum()val pi 4.0 * hits.sum() / numSamples // sum()是Action强制触发计算 println(sPi is roughly $pi)这是Spark新手最高频的误解。RDD是惰性求值的map、filter只是记录血缘Lineage只有count()、collect()、saveAsTextFile()这类Action才会真正执行。spark-submit提交后如果没看到“Job finished”日志大概率就是漏了Action。3.2 Maven依赖配置scope设置的生死线pom.xml中Spark核心依赖的scope必须设为provideddependency groupIdorg.apache.spark/groupId artifactIdspark-sql_2.12/artifactId version3.4.1/version scopeprovided/scope !-- 关键 -- /dependency为什么因为Spark集群的每个Executor节点都已经预装了Spark运行时jar包位于$SPARK_HOME/jars/。如果你把spark-sql_2.12-3.4.1.jar打进自己的application jar会出现两种灾难类冲突Executor加载自己jar里的SparkConf.class和集群jar里的同名类发生冲突抛LinkageError体积爆炸一个Spark 3.4.1的assembly jar约120MB加上你的业务代码动辄200MB上传到HDFS/YARN耗时增加5倍某次线上发布因此超时被自动回滚。provided的作用是编译时可用打包时不包含。Maven命令mvn clean package -DskipTests生成的jar用jar -tf target/pi-calc-1.0.jar | grep spark应该完全看不到Spark相关class。注意provided只适用于Spark核心依赖。如果你用了commons-math3做高级统计或者jackson-databind解析JSON这些第三方库必须设为compile默认否则Executor会报ClassNotFoundException。3.3 打包技巧shade插件的必要性与陷阱当你的代码依赖了非Spark自带的库比如用Apache Commons Math做更精确的随机采样就必须用Maven Shade Plugin打包成“fat jar”。但这里有个致命陷阱不要盲目relocate所有包。错误配置plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId configuration relocations relocation patternorg.apache.commons/pattern shadedPatternshaded.org.apache.commons/shadedPattern /relocation /relocations /configuration /plugin问题在于commons-math3内部通过ServiceLoader加载UnivariateStatistic实现类它依赖META-INF/services/下的文件路径。如果把org.apache.commons.math3.stat.descriptive整个relocateServiceLoader就找不到服务描述文件运行时报NoSuchElementException。正确做法只relocate你的业务包保留第三方库原路径relocations relocation patterncom.example.pi/pattern !-- 只relocate你自己代码的包 -- shadedPatternshaded.com.example.pi/shadedPattern /relocation /relocations然后用mvn clean compile shade:shade生成jar。生成后务必检查jar -tf target/pi-calc-1.0-shaded.jar | grep math3应该能看到org/apache/commons/math3/目录证明没被错误relocate。4. 实操过程与核心环节实现4.1 环境准备四步确认法绕过90%的连接失败在敲下spark-submit之前必须完成四步原子级验证。我把它称为“四步确认法”某公司SRE团队已将其写入《Spark平台上线Checklist》。第一步确认Spark版本兼容性执行spark-submit --version输出必须与集群版本一致。常见坑本地装了Spark 3.3.0集群是3.4.1提交时会报java.lang.NoSuchMethodError: org.apache.spark.sql.SparkSession.builder()。这是因为3.4.1新增了config(spark.sql.adaptive.enabled,true)等方法3.3.0的client找不到。解决方案下载与集群完全一致的Spark二进制包官网archive解压后用其bin/spark-submit。不要用brew install apache-spark或apt-get install spark那些是社区维护的通用版版本滞后且配置不匹配。第二步确认Master URL可达性对YARN集群# 检查ResourceManager是否存活 curl -I http://rm-host:8088/ws/v1/cluster/info 2/dev/null | head -1 # 应返回 HTTP/1.1 200 OK # 检查YARN队列状态 yarn queue -status default # 应返回 Queue State : RUNNING对Standalone集群# 检查Master Web UI curl -s http://master-host:8080/json/ | jq .activeapps 2/dev/null # 应返回 [] 或包含正在运行的应用列表第三步确认HDFS路径可写YARN/K8s必需YARN模式下Spark会把application jar、log4j配置等上传到HDFS临时目录默认/user/spark/applicationHistory。执行hdfs dfs -mkdir -p /user/$USER/spark-test hdfs dfs -chmod 777 /user/$USER/spark-test hdfs dfs -put target/pi-calc-1.0.jar /user/$USER/spark-test/ hdfs dfs -ls /user/$USER/spark-test/ # 应看到jar文件成功上传如果报Permission denied说明HDFS ACL未开通需联系平台管理员。第四步确认Driver与Executor网络互通这是最隐蔽的坑。Driver启动后会监听一个随机端口如37221Executor通过spark.driver.host和spark.driver.port连接它。如果Driver在Docker内网Executor在物理机或反之就会出现Connection refused。验证方法# 在Driver所在机器查监听端口 netstat -tuln | grep :37221 # 在Executor所在机器尝试telnet telnet driver-host 37221 # 应显示 Connected to driver-host.若不通需在spark-submit中显式指定--conf spark.driver.host$(hostname -i) \ --conf spark.driver.bindAddress0.0.0.0 \ --conf spark.driver.port372214.2 标准提交命令详解每个参数都是救命稻草下面是一条生产环境可用的spark-submit命令我们逐参数拆解spark-submit \ --master yarn \ --deploy-mode cluster \ --name pi-calc-prod \ --class com.example.pi.PiEstimator \ --driver-memory 2g \ --driver-cores 2 \ --executor-memory 4g \ --executor-cores 2 \ --num-executors 10 \ --conf spark.yarn.maxAppAttempts2 \ --conf spark.sql.adaptive.enabledtrue \ --conf spark.serializerorg.apache.spark.serializer.KryoSerializer \ --conf spark.kryoserializer.buffer.max512m \ hdfs://namenode:8020/user/spark/pi-calc-1.0.jar \ 1000000000--master yarn指定资源管理器为YARN。如果是K8s写k8s://https://k8s-api:6443Standalone写spark://master:7077。--deploy-mode cluster关键cluster模式下Driver运行在YARN Container内Client进程提交后立即退出client模式下Driver在Client机器上运行Client必须保持在线。生产环境必须用cluster否则Client断网作业失败。--name pi-calc-prod作业名称会显示在YARN ResourceManager Web UI的Applications列表中便于追踪。--class com.example.pi.PiEstimator主类全限定名。注意如果是Scala单例对象必须写objectName$JVM字节码规则但Spark官方示例已处理直接写类名即可。--driver-memory 2gDriver内存。求PI作业Driver只做汇总2G足够但如果是SQL解析DAG优化的复杂作业需调到4G。--executor-memory 4g每个Executor堆内存。蒙特卡洛计算不占内存但Spark自身框架需预留1G所以4G是安全下限。--num-executors 10申请10个Executor。结合--executor-cores 2总vCore20符合YARN队列的资源配额某公司default队列上限20vCore。--conf spark.yarn.maxAppAttempts2YARN应用最大重试次数。默认为1即失败一次就终止。设为2可容忍一次AM Container启动失败如临时磁盘满。--conf spark.serializerorg.apache.spark.serializer.KryoSerializer启用Kryo序列化。比Java默认序列化快10倍内存占用减半。必须配合--conf spark.kryoserializer.buffer.max512m否则大数据量时会报Buffer overflow。hdfs://.../pi-calc-1.0.jarjar包HDFS路径。绝对路径不能用相对路径或本地file:///。1000000000程序参数即numSamples10亿。提示所有--conf参数都可以写在$SPARK_HOME/conf/spark-defaults.conf中避免每次提交都重复。但--master、--class、jar路径这些动态参数必须命令行指定。4.3 日志分析实战从“Application submitted”到“Pi is roughly”spark-submit的stdout/stderr是诊断黄金来源。我们按时间线梳理关键日志段阶段1Client提交毫秒级23/05/15 10:23:41 INFO Client: Submitting application application_1715768621234_0001 to ResourceManager 23/05/15 10:23:41 INFO YarnClientImpl: Submitted application application_1715768621234_0001看到这两行说明Client已成功将ApplicationSubmissionContext发送给YARN RM。如果卡在这里检查YARN RM是否存活、Client机器能否访问RM地址。阶段2AM Container启动秒级23/05/15 10:23:42 INFO YarnClientSchedulerBackend: Application application_1715768621234_0001 has started running. 23/05/15 10:23:42 INFO YarnClientSchedulerBackend: Application application_1715768621234_0001 requires 2 executor(s), and max 2 executor(s) allowed.此时YARN已分配AM Container并启动了Spark ApplicationMaster。如果长时间没出现检查YARN队列资源是否被占满yarn top、或AM Container内存超限YARN日志中搜Container exited with a non-zero exit code 143通常是OOM。阶段3Executor启动与注册秒级23/05/15 10:23:45 INFO YarnClientSchedulerBackend: Registered executor NettyRpcEndpointRef(spark-client://Executor) (192.168.1.101:39221) with ID 1 23/05/15 10:23:45 INFO YarnClientSchedulerBackend: Registered executor NettyRpcEndpointRef(spark-client://Executor) (192.168.1.102:39222) with ID 2 ...每行代表一个Executor成功连接Driver。ID从1开始递增。如果只看到ID 1~5但--num-executors 10说明剩余5个Executor申请失败去YARN RM Web UI看“Pending Containers”数量。阶段4任务执行与结果输出分钟级23/05/15 10:24:10 INFO DAGScheduler: Job 0 finished: sum at PiEstimator.scala:45, took 25.345249 s Pi is roughly 3.141592653589793Job 0 finished是DAGScheduler打印的表示整个job执行完毕。后面的Pi is roughly...是你代码中的println说明结果已正确计算并输出。此时可以放心集群连通性、资源调度、计算逻辑、结果聚合全部通过。注意spark-submit命令行本身会在Job完成后立即退出返回码为0。你可以用echo $?验证。非0返回码如1、137、143对应不同错误类型详见Spark官方文档Exit Code Reference。4.4 参数调优实录10亿样本下的性能对比在某公司离线平台YARN10台32C64G物理机上我们对10亿次采样做了四组参数对比结果如下配置组合Executor内存Executor核数Executor数量总耗时CPU平均利用率备注A默认2g120142s45%--executor-memory 2g --executor-cores 1 --num-executors 20B推荐4g21098s68%内存翻倍核数翻倍Executor减半减少调度开销C激进8g45112s72%单Executor负载过重GC时间占比升至22%D保守1g140165s38%过多Executor导致YARN调度排队启动延迟高结论很清晰B配置是最佳平衡点。原因有三内存充足4g Executor内存留足2g给Spark框架Shuffle、Broadcast、Cache2g给任务计算避免频繁GC核数合理2核Executor能充分利用现代CPU的超线程1核太浪费4核在蒙特卡洛这种无锁计算中收益递减数量适中10个ExecutorYARN调度延迟1sExecutor间无数据依赖不存在“木桶效应”。实测中把--executor-cores从2改成3总耗时反而增加到105s。因为Spark的TaskScheduler默认按core数分配task3核Executor会同时运行3个task但蒙特卡洛计算是纯CPU bound上下文切换开销抵消了并行收益。这印证了一个经验法则对于计算密集型任务Executor cores数物理CPU核心数/2 是较优起点。5. 常见问题与排查技巧实录5.1 经典错误速查表从现象到根因我把过去三年遇到的求PI提交失败案例整理成一张速查表。当你看到某个现象直接对照“可能原因”和“验证命令”5分钟内定位问题。现象可能原因验证命令解决方案Exception in thread main java.lang.NoClassDefFoundError: org/apache/spark/SparkConfjar包未声明providedscope或Spark依赖版本不匹配jar -tf your-app.jar | grep SparkConf检查pom.xml确保Spark依赖scopeprovided用spark-submit --version确认版本Application application_171... failed 2 times due to AM ContainerYARN AM Container内存不足或jar包HDFS路径不可读yarn logs -applicationId application_171... | grep -A5 -B5 ExitCode增加--driver-memory检查HDFS路径权限确认spark.yarn.jars指向正确的Spark jars19/05/15 10:23:45 ERROR YarnClientSchedulerBackend: Yarn reported application application_171... has already completedClient进程未退出重复提交同一applicationIdyarn application -list | grep pi-calc杀掉残留Client进程或改用--name加时间戳避免重名WARN TaskSetManager: Lost task 3.0 in stage 0.0 (TID 3, executor-1, executor 1): java.lang.OutOfMemoryError: Java heap space单个Executor内存不足或Kryo buffer太小yarn logs -applicationId ... | grep OutOfMemoryError增加--executor-memory添加--conf spark.kryoserializer.buffer.max512mERROR TransportRequestHandler: Error while invoking RpcHandler#receive() for one-way messageDriver与Executor网络不通或spark.driver.host配置错误telnet driver-host 37221在Executor机器执行显式设置--conf spark.driver.hostxxx --conf spark.driver.port37221这张表不是凭空编的。比如第二行“AM Container失败”某次故障中YARN日志显示ExitCode: 143这是Linux OOM Killer杀进程的信号。我们立刻检查yarn.nodemanager.resource.memory-mb发现集群配置为8G但AM Container默认申请1G而spark.driver.memory设了2G超限了。解决方案是显式指定--driver-memory 1g或调高YARN NodeManager内存上限。5.2 日志深度挖掘技巧三步定位“幽灵错误”有些错误不报异常但结果明显不对比如π2.718。这时需要深入日志用三步法挖掘第一步确认任务是否真的分布式执行在Driver日志中搜索Starting task23/05/15 10:24:01 INFO TaskSetManager: Starting task 0.0 in stage 0.0 (TID 0, executor-1, partition 0, PROCESS_LOCAL, 2125 bytes) 23/05/15 10:24:01 INFO TaskSetManager: Starting task 1.0 in stage 0.0 (TID 1, executor-2, partition 1, PROCESS_LOCAL, 2125 bytes) ...如果只看到TID 0且executor全是localhost说明根本没走集群还在local模式。检查--master参数是否被覆盖比如代码里硬编码了SparkConf.setMaster(local[*])。第二步验证采样总数是否准确在Executor日志中搜索task metrics23/05/15 10:24:05 INFO TaskSetManager: Finished task 0.0 in stage 0.0 (TID 0) in 2456 ms on executor-1 (1/10) 23/05/15 10:24:05 INFO TaskSetManager: Metrics for TID 0: executorDeserializeTime - 12 ms resultSerializationTime - 3 ms jvmGCTime - 45 ms inputBytes - 0 B outputBytes - 8 B recordsRead - 100000000 # 关键每个task应读取1e8条10亿/10recordsRead字段告诉你这个task处理了多少样本。如果全是100000000说明数据均匀切分如果有的是100000000有的是0说明sc.range分区不均需调整partitions参数。第三步检查随机数分布是否均匀在代码中加入采样统计val stats sc.parallelize(1 to numSamples).map { i val r new scala.util.Random(i) val x r.nextDouble() val y r.nextDouble() ((x*10).toInt, (y*10).toInt) // 把[0,1)映射到[0,10)的整数格子 }.countByKey() // 统计每个格子的点数 println(sGrid distribution: ${stats.take(20).mkString(,)})理想情况下100个格子10x10的计数应接近1e610亿/100。如果某个格子计数为0或某些格子明显偏高说明随机数生成器有问题比如seed冲突需换用ThreadLocalRandom或SecureRandom。5.3 生产环境避坑指南五个血泪教训这些不是文档写的是我和团队在真实生产环境中用服务器宕机、作业超时、数据污染换来的教训教训1永远不要在代码里写死sc.parallelize(1 to 1000000000)某次上线开发把numSamples写死在代码里测试环境用1e6生产环境忘了改结果parallelize试图在Driver内存加载10亿个IntDriver OOM整个YARN队列被拖慢。正确做法所有参数必须通过args传入spark-submit ... pi-calc.jar 1000000000。教训2--deploy-mode client在生产环境等于埋雷Client模式下Driver进程绑定在提交机器上。某次网络抖动提交机断网Driver失联但YARN以为AM还活着Executor持续运行直到超时被Kill浪费了2小时计算资源。生产环境必须用cluster模式Driver由YARN托管。教训3Kryo序列化必须注册自定义类如果代码里用了自定义case class如case class Sample(x: Double, y: Double)不注册会导致序列化失败。必须在SparkConf中val conf new SparkConf().setAppName(pi) conf.set(spark.serializer, org.apache.spark.serializer.KryoSerializer) conf.registerKryoClasses(Array(classOf[Sample]))否则报java.lang.ClassNotFoundException: com.example.pi.Sample即使jar包里有这个class。教训4YARN队列配额不是“最大值”而是“保障值”yarn.scheduler.capacity.root.default.maximum-capacity100不代表你能用满100%而是指当其他队列空闲时你可以借到100%。实际提交时YARN会按capacity保障容量分配资源。某次配置capacity20但maximum-capacity100结果--num-executors 50只分配到10个因为YARN认为你只能保障20%资源。教训5时间戳日志比系统日志更可靠YARN日志默认用系统