1. 项目概述为什么Spring Boot要和Spark on Yarn“搭伙”在某公司做实时推荐系统升级时我接手了一个典型的数据服务场景前端用户行为日志通过Kafka持续流入后端需要每5分钟拉取最新窗口数据跑一次协同过滤模型把生成的Top20推荐结果写入Redis供API调用。最初用纯Spring Boot JDBC轮询MySQL做离线计算单次耗时38秒延迟高、资源独占、无法横向扩展——当日活从2万涨到8万时任务开始频繁超时DB连接池直接打满。这时候“Spring Boot整合Spark基于Yarn运行提交Spark任务”就不是一句技术口号而是必须落地的生产解法。它本质是把业务服务层Spring Boot和大数据计算层Spark on Yarn解耦又贯通Spring Boot不再自己扛计算只负责触发、传参、查状态、取结果真正的数据清洗、特征工程、模型训练全部交给Yarn集群上的Spark Executor并行执行。我实测过同样数据量下Spark on Yarn任务平均耗时压到4.2秒资源利用率提升3.7倍且能随业务增长动态扩缩Worker节点——这才是企业级数据服务该有的弹性底座。这个组合特别适合三类人一是Java后端工程师想快速接入大数据能力不想从头学Scala或PySpark二是数据平台团队需要为业务方提供“点按钮即运行”的分析接口三是中小团队没有专职大数据运维但又得支撑准实时计算需求。它不追求替代专业数仓或流式引擎而是用最小学习成本在Spring生态里稳稳接住Spark这颗“重型计算弹药”。关键词“spring boot整合spark”“spark on yarn”背后其实是Java系开发者对“可控、可维护、可监控”的工程化诉求而不是炫技式的大数据堆砌。2. 整体架构设计与方案选型逻辑2.1 为什么放弃Local模式和Standalone模式死磕Yarn刚接触时我也试过Spark Local模式在Spring Boot里new SparkSession.builder().master(local[*])代码极简本地调试飞快。但一上测试环境就崩——某次处理10GB用户画像数据JVM直接OOM日志里全是GC overhead limit exceeded。根本原因在于Local模式把Driver和Executor全塞进一个JVM进程计算资源被Spring Boot应用本身吃掉大半留给Spark的内存捉襟见肘。接着切到Standalone模式自己搭Spark Master/Worker集群。问题转向运维黑洞——Worker节点挂了没人告警Executor日志分散在各节点磁盘上排查一次Shuffle失败得连SSH翻四台机器。更致命的是它和公司已有的Yarn资源调度器冲突HDFS权限体系也得单独配一套等于重复造轮子。最终选定Yarn模式核心逻辑就三点第一资源复用不打架。Yarn作为统一资源调度器能把Spark、Flink、MapReduce甚至自定义服务都纳管进来。我们集群里Spark占60%内存配额Flink占30%剩下10%留给临时ETL脚本所有任务按需申请Container彻底告别“抢资源”内耗。第二日志和监控有根可循。Yarn Web UI默认端口8088里能直接看到每个Spark Application的ApplicationMaster状态、Container列表、Stderr日志链接配合ELK收集NodeManager日志任何Executor OOM都能秒定位到具体Container ID和堆栈。第三安全合规有兜底。Yarn天然支持Kerberos认证和HDFS ACL我们只需在Spring Boot配置里加几行principal和keytab路径Spark任务就能以指定用户身份读写HDFS审计日志自动记录操作者、时间、路径——这对金融类业务是硬性要求。提示别信“Yarn配置太重”的说法。我们线上集群用CDH 6.3.2Yarn相关配置文件就yarn-site.xml和core-site.xml两份加起来不到200行。真正要花时间的是理解Yarn的Resource Modelvcores/memory per container这决定了你的Spark任务能分到多少算力。2.2 Spring Boot整合Spark的三种姿势为什么选“客户端模式”整合方式无非三种嵌入式模式Spring Boot启动时初始化SparkContext整个应用生命周期内复用。看似省事实则埋雷——SparkContext是重量级对象Shutdown不干净会导致线程泄漏多线程并发提交任务时Driver端容易出现“Task not serializable”异常因闭包捕获了Spring Bean。服务化模式把Spark封装成独立微服务如用Flask暴露REST APISpring Boot通过HTTP调用。解耦彻底但引入网络开销和额外运维点5分钟级任务的延迟从毫秒升到秒级不符合我们SLA要求。客户端模式Client ModeSpring Boot作为纯粹的“任务发起者”每次提交时动态构建spark-submit命令由Yarn分配ApplicationMasterSpark Driver运行在提交节点即Spring Boot服务器Executor在Yarn Worker上启动。这是我们最终选择理由很实在调试友好Driver日志直接输出到Spring Boot控制台断点调试Spark代码毫无障碍资源隔离每个任务Driver内存独立配置--driver-memory不会互相影响故障收敛快某个任务Driver崩溃只影响本次提交不影响Spring Boot主进程和其他任务。注意Client模式要求Spring Boot服务器必须安装Spark客户端spark-submit脚本、Hadoop客户端hadoop、hdfs命令、以及完整的Hadoop配置文件core-site.xml, hdfs-site.xml, yarn-site.xml。别指望只拷几个jar包就能跑通——Yarn通信、HDFS认证、Kerberos票据续期全靠这些二进制和配置驱动。2.3 为什么不用Spark Thrift Server或Livy直连Yarn更可靠社区常提的LivySpark REST Server和Thrift ServerJDBC接口看似更“标准”但我们实测后主动弃用。Livy的问题在于它本质是个代理层Spring Boot发HTTP请求给LivyLivy再调spark-submit。中间多一层故障点翻倍——Livy进程挂了、Livy和Yarn网络不通、Livy配置的spark-home路径错误都会导致任务提交失败而错误日志全在Livy端Spring Boot只能收到个500错误排查链路拉长3倍。Thrift Server更麻烦它长期运行一个SparkSession所有SQL查询共享同一个Driver。我们曾遇到一个慢查询占满Driver线程池后续所有请求排队等待监控看CPU很低但QPS暴跌。而且Thrift Server不支持Spark 3.x的动态资源分配Dynamic AllocationWorker节点无法根据负载自动伸缩。最终坚持“Spring Boot → spark-submit → Yarn”最短路径。虽然要手写命令拼接逻辑但好处是每次提交都是全新Application资源干净错误信息直达源头Yarn RM日志或ApplicationMaster日志参数可精细控制比如对不同任务设置不同--executor-cores避免小任务浪费资源。3. 核心细节解析与实操要点3.1 环境准备Spring Boot服务器不是“裸机”必须预装四大件很多教程跳过这步直接写代码结果90%的人卡在环境配置。Spring Boot服务器我们叫submit-node必须满足四个硬性条件第一Hadoop客户端完整安装。不能只放hadoop-common.jar必须下载对应Hadoop版本的二进制包如hadoop-3.2.1.tar.gz解压后配置HADOOP_HOME并把$HADOOP_HOME/bin加入PATH。验证命令hadoop version输出版本号hdfs dfs -ls /能列出HDFS根目录。缺这步spark-submit会报“Failed to locate the winutils binary”Windows或“No FileSystem for scheme: hdfs”Linux。第二Spark客户端精准匹配。Spark版本必须和Yarn集群的Spark版本严格一致。我们集群用Spark 3.2.1submit-node就必须装同版本。重点检查$SPARK_HOME/conf/spark-defaults.conf里的spark.yarn.jars配置——它指向Yarn上Spark依赖jar包的HDFS路径如hdfs://nameservice1/spark-jars/*。如果路径错Yarn会提示“ClassNotFound in Spark assembly”。第三Kerberos认证凭证到位。开启Kerberos后submit-node需有合法principal如spark-submit/submit-nodeEXAMPLE.COM和对应keytab文件。关键操作# 生成票据缓存 kinit -kt /etc/security/keytabs/spark-submit.keytab spark-submit/submit-nodeEXAMPLE.COM # 验证票据是否有效有效期24小时 klistSpring Boot代码里通过System.setProperty(java.security.krb5.conf, /etc/krb5.conf)指定Kerberos配置。第四网络策略白名单。submit-node必须能直连Yarn ResourceManager8032端口、NodeManager8042端口、HDFS NameNode8020端口。我们曾因安全组没放开8032spark-submit卡在“Connecting to ResourceManager”长达5分钟才超时。实操心得我写了个check-env.sh脚本每次部署新submit-node都先跑一遍检测hadoop/spark命令、klist票据、telnet端口、hdfs ls权限。10行shell解决80%环境问题比看日志猜错因快10倍。3.2 Spark任务打包Jar包里到底该放什么不该放什么这是最容易踩坑的环节。Spark任务本质是Java/Scala程序但打包规则和普通Spring Boot Jar截然不同。必须包含的内容业务逻辑代码如UserRecJob.class所有非provided范围的依赖jar如commons-lang3、fastjsonSpark SQL的DataFrame操作代码注意不要打包spark-sql_2.12Yarn集群已提供Hadoop配置文件core-site.xml等确保任务能正确访问HDFS。必须排除的内容Spark核心jarspark-core_2.12、spark-sql_2.12等。Yarn集群的Spark版本已预装重复打包会导致Classloader冲突常见报错“java.lang.NoSuchMethodError: org.apache.spark.sql.SparkSession.builder()”。Hadoop核心jarhadoop-client、hadoop-common等。同理集群Hadoop版本已固定本地打包会覆盖Yarn的Hadoop类。Log4j桥接器slf4j-log4j12、log4j-over-slf4j。Spark自带log4j桥接器会引发日志框架死锁任务卡在“Setting up executor log4j”不动。Maven打包插件配置实录pom.xml关键段plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.2.4/version executions execution phasepackage/phase goals goalshade/goal /goals configuration !-- 排除Spark和Hadoop核心jar -- filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-INF/*.RSA/exclude /excludes /filter /filters artifactSet excludes excludeorg.apache.spark:spark-core_2.12/exclude excludeorg.apache.spark:spark-sql_2.12/exclude excludeorg.apache.hadoop:hadoop-client/exclude excludeorg.apache.hadoop:hadoop-common/exclude excludeorg.slf4j:slf4j-log4j12/exclude excludelog4j:log4j/exclude /excludes /artifactSet !-- 主类指定 -- transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.job.UserRecJob/mainClass /transformer /transformers /configuration /execution /executions /plugin注意shade插件生成的fat jar命名要带版本号如rec-job-1.0.0.jar方便Yarn UI识别。我们约定所有任务jar上传到HDFS固定路径/spark-jobs/prod/rec-job-1.0.0.jarSpring Boot提交时直接引用此HDFS路径。3.3 Spring Boot任务提交模块不是调API而是拼命令Spring Boot不提供“开箱即用”的Spark提交SDK本质是用Java ProcessBuilder执行spark-submit命令。很多人用RestTemplate调Livy反而绕远路。核心代码结构UserRecService.javapublic class UserRecService { // spark-submit绝对路径避免PATH环境变量问题 private static final String SPARK_SUBMIT_PATH /opt/spark/bin/spark-submit; // Yarn集群配置 private static final String HADOOP_CONF_DIR /etc/hadoop/conf; private static final String SPARK_CONF_DIR /opt/spark/conf; public String submitRecJob(String userId, int topN) throws IOException, InterruptedException { // 构建spark-submit命令参数列表 ListString command new ArrayList(); command.add(SPARK_SUBMIT_PATH); // 基础配置 command.add(--master); command.add(yarn); command.add(--deploy-mode); command.add(client); command.add(--conf); command.add(spark.yarn.appMasterEnv.PYSPARK_PYTHONpython3); command.add(--conf); command.add(spark.yarn.appMasterEnv.PYSPARK_DRIVER_PYTHONpython3); // 资源参数根据任务类型动态调整 command.add(--driver-memory); command.add(2g); command.add(--executor-memory); command.add(4g); command.add(--executor-cores); command.add(2); command.add(--num-executors); command.add(4); // 依赖jarHDFS路径 command.add(--jars); command.add(hdfs://nameservice1/spark-jobs/lib/mysql-connector-java-8.0.26.jar); // 主jar和参数 command.add(--class); command.add(com.example.job.UserRecJob); command.add(hdfs://nameservice1/spark-jobs/prod/rec-job-1.0.0.jar); command.add(userId); // 传递给main方法的第一个参数 command.add(String.valueOf(topN)); // 第二个参数 // 启动进程 ProcessBuilder pb new ProcessBuilder(command); pb.environment().put(HADOOP_CONF_DIR, HADOOP_CONF_DIR); pb.environment().put(SPARK_CONF_DIR, SPARK_CONF_DIR); pb.redirectErrorStream(true); // 合并stdout/stderr便于日志采集 Process process pb.start(); BufferedReader reader new BufferedReader( new InputStreamReader(process.getInputStream())); // 实时读取日志用于前端WebSocket推送 String line; while ((line reader.readLine()) ! null) { log.info([SparkLog] {}, line); // 推送日志到前端 websocketService.sendLog(line); } int exitCode process.waitFor(); if (exitCode 0) { return SUCCESS; } else { throw new RuntimeException(Spark job failed with exit code: exitCode); } } }关键细节说明环境变量注入pb.environment().put(HADOOP_CONF_DIR, ...)必须显式设置否则spark-submit找不到HDFS配置参数顺序敏感--jars必须在--class之前否则Yarn会忽略依赖HDFS路径必须完整hdfs://nameservice1/...不能简写为/spark-jobs/...否则报“File does not exist”日志实时捕获redirectErrorStream(true)让stderr合并到stdout避免日志丢失这对前端展示任务进度至关重要。实操心得我给每个任务加了唯一traceId拼在spark-submit命令最后如--conf spark.app.traceIdabc123这样Yarn UI里一眼就能关联到Spring Boot的请求日志排查问题时不用跨系统对时间戳。4. 实操过程与核心环节实现4.1 从零搭建Yarn集群验证环境CDH 6.3.2精简版生产环境用CDH但开发调试用伪分布式Yarn足够。我在一台16G内存的CentOS 7虚拟机上搭建全程30分钟步骤1安装JDK 1.8和Hadoop 3.2.1# 下载hadoop-3.2.1.tar.gz解压到/opt/hadoop tar -zxvf hadoop-3.2.1.tar.gz -C /opt/ # 配置环境变量/etc/profile export HADOOP_HOME/opt/hadoop export PATH$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin # 格式化NameNode hdfs namenode -format # 启动HDFS start-dfs.sh # 验证hdfs dfs -ls / 应返回Found 1 items步骤2配置Yarn关键四文件etc/hadoop/core-site.xml指定HDFS地址property namefs.defaultFS/name valuehdfs://localhost:9000/value /propertyetc/hadoop/hdfs-site.xml关闭权限检查开发用property namedfs.permissions.enabled/name valuefalse/value /propertyetc/hadoop/yarn-site.xml启用Yarnproperty nameyarn.resourcemanager.hostname/name valuelocalhost/value /property property nameyarn.nodemanager.aux-services/name valuemapreduce_shuffle/value /propertyetc/hadoop/mapred-site.xml指定MapReduce框架property namemapreduce.framework.name/name valueyarn/value /property步骤3启动Yarn并验证# 启动Yarn start-yarn.sh # 检查进程jps应看到ResourceManager、NodeManager # 访问Yarn Web UIhttp://localhost:8088能看到Nodes页显示1个Active Node # 运行官方示例验证 hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-3.2.1.jar wordcount /input /output注意伪分布式模式下Yarn的ApplicationMaster和Executor都在本机启动内存要预留充足。我给虚拟机分配12G内存Yarn配置中yarn.nodemanager.resource.memory-mb设为8192避免Container被Kill。4.2 Spark任务开发一个真实可用的协同过滤推荐Job以UserCF基于用户的协同过滤为例代码必须满足三个生产要求可参数化、可中断、可监控。Job入口UserRecJob.scalaobject UserRecJob extends App { // 从命令行获取参数userId, topN if (args.length 2) { println(Usage: UserRecJob userId topN) System.exit(1) } val userId args(0) val topN args(1).toInt // 初始化SparkSessionYarn模式自动适配 val spark SparkSession.builder() .appName(sUserRecJob-$userId) .getOrCreate() import spark.implicits._ try { // 1. 读取用户行为日志Parquet格式分区表 val actionDF spark.read .parquet(hdfs://localhost:9000/data/actions/) .filter($userId ! userId) // 排除目标用户自身 .filter($actionType click || $actionType buy) // 2. 计算用户相似度余弦相似度简化版 val userSimDF actionDF .groupBy(userId) .agg(collect_list(itemId).as(items)) .withColumn(itemSet, array_distinct($items)) .select($userId, size($itemSet).as(itemCount), $itemSet) .filter($itemCount 5) // 过滤活跃用户 .crossJoin( // 自连接求交集 actionDF.groupBy(userId).agg(collect_list(itemId).as(items)) .withColumn(itemSet, array_distinct($items)) .select($userId.as(otherUserId), $itemSet.as(otherItemSet)) ) .withColumn(commonItems, size(array_intersect($itemSet, $otherItemSet))) .withColumn(similarity, when($commonItems 0, $commonItems / (sqrt($itemCount) * sqrt(size($otherItemSet)))) .otherwise(0.0)) .filter($similarity 0.1) // 相似度阈值 .orderBy(desc(similarity)) .limit(topN) // 3. 获取相似用户喜欢的物品去重排序 val recItems userSimDF .join(actionDF, $otherUserId $userId, inner) .filter($actionType buy) .select($itemId, $similarity) .groupBy($itemId) .agg(sum($similarity).as(score)) .orderBy(desc(score)) .limit(topN) .rdd.map(row row.getString(0)).collect() // 4. 写入Redis通过Jedis val jedis new Jedis(redis-host, 6379) jedis.setex(srec:$userId, 3600, recItems.mkString(,)) jedis.close() println(sUser $userId recommendation completed: ${recItems.length} items) } catch { case e: Exception println(sJob failed: ${e.getMessage}) throw e } finally { spark.stop() } }关键生产特性说明参数化args(0)和args(1)接收Spring Boot传入的userId和topN无需改代码可中断try/catch/finally确保spark.stop()必执行避免Driver残留可监控println日志会被spark-submit捕获Spring Boot实时推送防错设计filter($itemCount 5)过滤低活跃用户避免相似度计算失真similarity 0.1设阈值防止噪声用户干扰。实操心得我给所有Spark Job加了--conf spark.sql.adaptive.enabledtrueSpark 3.0开启自适应查询优化。实测对复杂join场景执行时间平均缩短22%且无需人工调优shuffle partitions。4.3 Spring Boot集成全流程从HTTP触发到结果回调Spring Boot端提供REST API前端调用POST /api/rec/submit即可提交任务Controller层RecController.javaRestController RequestMapping(/api/rec) public class RecController { Autowired private UserRecService userRecService; PostMapping(/submit) public ResponseEntitySubmitResponse submitJob(RequestBody SubmitRequest request) { // 1. 参数校验 if (StringUtils.isBlank(request.getUserId()) || request.getTopN() 0) { return ResponseEntity.badRequest() .body(new SubmitResponse(FAILED, Invalid parameters)); } // 2. 生成唯一traceId String traceId UUID.randomUUID().toString().replace(-, ); // 3. 异步提交避免HTTP请求阻塞 CompletableFuture.supplyAsync(() - { try { String result userRecService.submitRecJob( request.getUserId(), request.getTopN() ); return new SubmitResponse(SUCCESS, traceId); } catch (Exception e) { log.error(Submit job failed, traceId: {}, traceId, e); return new SubmitResponse(FAILED, traceId); } }).thenAccept(response - { // 4. 结果写入数据库供查询状态 recResultMapper.insert(RecResult.builder() .traceId(traceId) .userId(request.getUserId()) .topN(request.getTopN()) .status(response.getStatus()) .createTime(new Date()) .build()); }); return ResponseEntity.ok(new SubmitResponse(SUBMITTED, traceId)); } GetMapping(/status/{traceId}) public ResponseEntityStatusResponse getJobStatus(PathVariable String traceId) { RecResult result recResultMapper.selectByTraceId(traceId); if (result null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(new StatusResponse(result.getStatus(), result.getCreateTime())); } }配套DTO和MapperSubmitRequest包含userId、topN字段SubmitResponse返回statusSUBMITTED/SUCCESS/FAILED和traceIdStatusResponse返回当前状态和创建时间RecResultMapperMyBatis操作MySQL记录每次提交的traceId、状态、时间。前端调用示例curl# 提交任务 curl -X POST http://localhost:8080/api/rec/submit \ -H Content-Type: application/json \ -d {userId:U1001,topN:10} # 响应{status:SUBMITTED,traceId:a1b2c3d4e5f6} # 查询状态轮询 curl http://localhost:8080/api/rec/status/a1b2c3d4e5f6 # 响应{status:SUCCESS,createTime:2023-10-01T10:20:30}注意异步提交用CompletableFuture.supplyAsync()而非Async避免Spring事务上下文传播问题。我们数据库用MySQL 5.7RecResult表加了traceId唯一索引防止重复提交。4.4 生产级监控与日志追踪让Spark任务“看得见、管得住”Yarn原生监控不够细我们加了三层增强第一层Yarn UI基础监控访问http://yarn-rm-host:8088看Application列表点击Application ID查看Running Containers实时Container数量突降说明Executor异常退出Logs点击某个Container的“logs”链接直接看Executor stdoutDiagnostics任务失败时的根因如“Container exited with a non-zero exit code 143”表示被Yarn Kill。第二层Spring Boot日志聚合所有spark-submit日志通过Logback输出到/var/log/spark-submit.log配置Logback滚动策略按天大小双限制appender nameSPARK classch.qos.logback.core.rolling.RollingFileAppender file/var/log/spark-submit.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern/var/log/spark-submit.%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy /rollingPolicy /appender第三层自定义指标埋点在UserRecService中添加Micrometer指标Component public class SparkMetrics { private final Timer sparkSubmitTimer; private final Counter sparkSuccessCounter; private final Counter sparkFailCounter; public SparkMetrics(MeterRegistry registry) { this.sparkSubmitTimer Timer.builder(spark.submit.duration) .description(Time taken to submit and complete Spark job) .register(registry); this.sparkSuccessCounter Counter.builder(spark.job.success) .description(Count of successful Spark jobs) .register(registry); this.sparkFailCounter Counter.builder(spark.job.fail) .description(Count of failed Spark jobs) .register(registry); } public void recordSuccess(long durationMs) { sparkSubmitTimer.record(durationMs, TimeUnit.MILLISECONDS); sparkSuccessCounter.increment(); } public void recordFailure() { sparkFailCounter.increment(); } }Prometheus抓取/actuator/metricsGrafana看板展示“Spark Submit Success Rate”成功率“Spark Avg Duration”平均耗时“Spark Running Jobs”并发数。实操心得我们发现一个隐藏问题——Yarn默认yarn.nodemanager.vmem-pmem-ratio2.1即虚拟内存是物理内存的2.1倍。当Spark Executor申请4G内存时Yarn会按比例分配8.4G虚拟内存。但某些云主机虚拟内存不足导致Container被Kill。解决方案在spark-submit中加--conf yarn.nodemanager.vmem-pmem-ratio1.0强制虚拟内存不超额。5. 常见问题与排查技巧实录5.1 典型问题速查表从报错日志反推根因报错日志片段可能原因排查步骤解决方案ClassNotFoundException: org.apache.spark.sql.SparkSessionSpark核心jar重复打包1.jar -tf rec-job-1.0.0.jar | grep spark-core2. 检查pom.xml中spark依赖scope是否为provided在pom.xml中将spark依赖scope设为scopeprovided/scopeNo FileSystem for scheme: hdfsHADOOP_CONF_DIR未设置或配置文件缺失1.echo $HADOOP_CONF_DIR2.ls $HADOOP_CONF_DIR/core-site.xml3.hdfs dfs -ls /在ProcessBuilder中显式设置pb.environment().put(HADOOP_CONF_DIR, /etc/hadoop/conf)Container exited with a non-zero exit code 143Yarn Kill了超内存Container1. 查Yarn UI中Container的Diagnostics2.yarn logs -applicationId application_12345看AM日志降低--executor-memory或增加--conf spark.yarn.executor.memoryOverhead2048java.lang.IllegalArgumentException: java.net.UnknownHostException: nameservice1HDFS HA配置中nameservice1未解析1.cat /etc/hadoop/conf/core-site.xml确认fs.defaultFS值2.ping nameservice1或nslookup nameservice1在submit-node的/etc/hosts中添加192.168.1.100 nameservice1org.apache.hadoop.security.AccessControlException: Permission denied: userspark, accessWRITE, inode/spark-jobsHDFS权限不足1.hdfs dfs -ls /看/spark-jobs目录属主2.hdfs dfs -chmod -R 777 /spark-jobs开发环境生产环境用hdfs dfs -chown spark:spark /spark-jobs并配置HDFS ACL5.2 我踩过的五个深坑及独家避坑技巧坑1Kerberos票据过期导致任务随机失败现象任务白天正常凌晨2点后大量失败Yarn日志报“GSSException: No valid credentials provided”。根因Kerberos票据默认有效期10小时凌晨2点恰好过期。避坑技巧在submit-node上配置自动续期脚本每5小时刷新一次# /opt/spark/bin/kinit-auto.sh #!/bin/bash kinit -kt /etc/security/keytabs/spark-submit.keytab spark-submit/submit-nodeEXAMPLE.COM # 加入crontab0 */5 * * * /opt/spark/bin/kinit-auto.sh坑2Spark UI端口被占用多个任务冲突现象第二个任务提交后Yarn UI显示ApplicationMaster Running但点进去报“Connection refused”。根因Spark Driver默认用4040端口第二个任务启动时端口被占降级到4041但Yarn UI仍指向4040。避坑技巧强制指定随机端口在spark-submit中加--conf spark.ui.port$(shuf -i 4040-4099 -n 1)坑3HDFS小文件过多任务启动巨慢现象spark-submit执行后卡在“Loading Spark configuration from Hadoop configuration”长达2分钟。根因HDFS上/spark-jobs/lib/目录有2000个小jarlistStatus操作遍历耗时。避坑技巧用Hadoop ArchiveHAR打包依赖hadoop archive -archiveName libs.har -p /spark-jobs/lib /spark-jobs/archives/ # 提交时用--archives hdfs://nameservice1/spark-jobs/archives/libs.har#libs坑4MySQL连接池耗尽Spring Boot假死现象连续提交10个任务后Spring Boot HTTP接口全部超时但进程仍在。根因每个spark-submit进程启动时Spring Boot的Hik