news 2026/10/22 8:16:22

Spring Boot整合Spark on Yarn实战:Java后端接入大数据计算

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot整合Spark on Yarn实战:Java后端接入大数据计算

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.2,Yarn相关配置文件就yarn-site.xml和core-site.xml两份,加起来不到200行。真正要花时间的是理解Yarn的Resource Model(vcores/memory per container),这决定了你的Spark任务能分到多少算力。

2.2 Spring Boot整合Spark的三种姿势,为什么选“客户端模式”?

整合方式无非三种:

  • 嵌入式模式:Spring Boot启动时初始化SparkContext,整个应用生命周期内复用。看似省事,实则埋雷——SparkContext是重量级对象,Shutdown不干净会导致线程泄漏;多线程并发提交任务时,Driver端容易出现“Task not serializable”异常(因闭包捕获了Spring Bean)。
  • 服务化模式:把Spark封装成独立微服务(如用Flask暴露REST API),Spring Boot通过HTTP调用。解耦彻底,但引入网络开销和额外运维点,5分钟级任务的延迟从毫秒升到秒级,不符合我们SLA要求。
  • 客户端模式(Client Mode):Spring Boot作为纯粹的“任务发起者”,每次提交时动态构建spark-submit命令,由Yarn分配ApplicationMaster,Spark 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更可靠

社区常提的Livy(Spark REST Server)和Thrift Server(JDBC接口)看似更“标准”,但我们实测后主动弃用。Livy的问题在于:它本质是个代理层,Spring Boot发HTTP请求给Livy,Livy再调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 Allocation),Worker节点无法根据负载自动伸缩。

最终坚持“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.1,submit-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-node@EXAMPLE.COM)和对应keytab文件。关键操作:

# 生成票据缓存 kinit -kt /etc/security/keytabs/spark-submit.keytab spark-submit/submit-node@EXAMPLE.COM # 验证票据是否有效(有效期24小时) klist

Spring Boot代码里通过System.setProperty("java.security.krb5.conf", "/etc/krb5.conf")指定Kerberos配置。

第四,网络策略白名单。submit-node必须能直连Yarn ResourceManager(8032端口)、NodeManager(8042端口)、HDFS NameNode(8020端口)。我们曾因安全组没放开8032,spark-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、fastjson);
  • Spark SQL的DataFrame操作代码(注意:不要打包spark-sql_2.12,Yarn集群已提供);
  • Hadoop配置文件(core-site.xml等),确保任务能正确访问HDFS。

必须排除的内容:

  • Spark核心jar:spark-core_2.12、spark-sql_2.12等。Yarn集群的Spark版本已预装,重复打包会导致Classloader冲突,常见报错:“java.lang.NoSuchMethodError: org.apache.spark.sql.SparkSession.builder()”。
  • Hadoop核心jar:hadoop-client、hadoop-common等。同理,集群Hadoop版本已固定,本地打包会覆盖Yarn的Hadoop类。
  • Log4j桥接器:slf4j-log4j12、log4j-over-slf4j。Spark自带log4j,桥接器会引发日志框架死锁,任务卡在“Setting up executor log4j”不动。

Maven打包插件配置实录(pom.xml关键段):

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <!-- 排除Spark和Hadoop核心jar --> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> <artifactSet> <excludes> <exclude>org.apache.spark:spark-core_2.12</exclude> <exclude>org.apache.spark:spark-sql_2.12</exclude> <exclude>org.apache.hadoop:hadoop-client</exclude> <exclude>org.apache.hadoop:hadoop-common</exclude> <exclude>org.slf4j:slf4j-log4j12</exclude> <exclude>log4j:log4j</exclude> </excludes> </artifactSet> <!-- 主类指定 --> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.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.jar,Spring Boot提交时直接引用此HDFS路径。

3.3 Spring Boot任务提交模块:不是调API,而是拼命令

Spring Boot不提供“开箱即用”的Spark提交SDK,本质是用Java ProcessBuilder执行spark-submit命令。很多人用RestTemplate调Livy,反而绕远路。

核心代码结构(UserRecService.java):

public 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命令参数列表 List<String> 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_PYTHON=python3"); command.add("--conf"); command.add("spark.yarn.appMasterEnv.PYSPARK_DRIVER_PYTHON=python3"); // 资源参数(根据任务类型动态调整) 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"); // 依赖jar(HDFS路径) 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.traceId=abc123),这样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> <name>fs.defaultFS</name> <value>hdfs://localhost:9000</value> </property>
  • etc/hadoop/hdfs-site.xml:关闭权限检查(开发用)
    <property> <name>dfs.permissions.enabled</name> <value>false</value> </property>
  • etc/hadoop/yarn-site.xml:启用Yarn
    <property> <name>yarn.resourcemanager.hostname</name> <value>localhost</value> </property> <property> <name>yarn.nodemanager.aux-services</name> <value>mapreduce_shuffle</value> </property>
  • etc/hadoop/mapred-site.xml:指定MapReduce框架
    <property> <name>mapreduce.framework.name</name> <value>yarn</value> </property>

步骤3:启动Yarn并验证

# 启动Yarn start-yarn.sh # 检查进程:jps应看到ResourceManager、NodeManager # 访问Yarn Web UI:http://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.scala):

object 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 // 初始化SparkSession(Yarn模式自动适配) val spark = SparkSession.builder() .appName(s"UserRecJob-$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(s"rec:$userId", 3600, recItems.mkString(",")) jedis.close() println(s"User $userId recommendation completed: ${recItems.length} items") } catch { case e: Exception => println(s"Job 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.enabled=true(Spark 3.0+),开启自适应查询优化。实测对复杂join场景,执行时间平均缩短22%,且无需人工调优shuffle partitions。

4.3 Spring Boot集成全流程:从HTTP触发到结果回调

Spring Boot端提供REST API,前端调用POST /api/rec/submit即可提交任务:

Controller层(RecController.java):

@RestController @RequestMapping("/api/rec") public class RecController { @Autowired private UserRecService userRecService; @PostMapping("/submit") public ResponseEntity<SubmitResponse> 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 ResponseEntity<StatusResponse> 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和Mapper:

  • SubmitRequest:包含userId、topN字段;
  • SubmitResponse:返回status(SUBMITTED/SUCCESS/FAILED)和traceId;
  • StatusResponse:返回当前状态和创建时间;
  • RecResultMapper:MyBatis操作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.7,RecResult表加了traceId唯一索引,防止重复提交。

4.4 生产级监控与日志追踪:让Spark任务“看得见、管得住”

Yarn原生监控不够细,我们加了三层增强:

第一层:Yarn UI基础监控

  • 访问http://yarn-rm-host:8088,看Application列表;
  • 点击Application ID,查看:
    • Running Containers:实时Container数量,突降说明Executor异常退出;
    • Logs:点击某个Container的“logs”链接,直接看Executor stdout;
    • Diagnostics:任务失败时的根因(如“Container exited with a non-zero exit code 143”表示被Yarn Kill)。

第二层:Spring Boot日志聚合

  • 所有spark-submit日志通过Logback输出到/var/log/spark-submit.log;
  • 配置Logback滚动策略(按天+大小双限制):
    <appender name="SPARK" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>/var/log/spark-submit.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>/var/log/spark-submit.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</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/metrics,Grafana看板展示:

  • “Spark Submit Success Rate”(成功率);
  • “Spark Avg Duration”(平均耗时);
  • “Spark Running Jobs”(并发数)。

实操心得:我们发现一个隐藏问题——Yarn默认yarn.nodemanager.vmem-pmem-ratio=2.1,即虚拟内存是物理内存的2.1倍。当Spark Executor申请4G内存时,Yarn会按比例分配8.4G虚拟内存。但某些云主机虚拟内存不足,导致Container被Kill。解决方案:在spark-submit中加--conf yarn.nodemanager.vmem-pmem-ratio=1.0,强制虚拟内存不超额。

5. 常见问题与排查技巧实录

5.1 典型问题速查表:从报错日志反推根因

报错日志片段可能原因排查步骤解决方案
ClassNotFoundException: org.apache.spark.sql.SparkSessionSpark核心jar重复打包1.jar -tf rec-job-1.0.0.jar | grep spark-core
2. 检查pom.xml中spark依赖scope是否为provided
在pom.xml中将spark依赖scope设为<scope>provided</scope>
No FileSystem for scheme: hdfsHADOOP_CONF_DIR未设置或配置文件缺失1.echo $HADOOP_CONF_DIR
2.ls $HADOOP_CONF_DIR/core-site.xml
3.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的Diagnostics
2.yarn logs -applicationId application_12345看AM日志
降低--executor-memory,或增加--conf spark.yarn.executor.memoryOverhead=2048
java.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 nameservice1
org.apache.hadoop.security.AccessControlException: Permission denied: user=spark, access=WRITE, inode="/spark-jobs"HDFS权限不足1.hdfs dfs -ls /看/spark-jobs目录属主
2.hdfs dfs -chmod -R 777 /spark-jobs(开发环境)
生产环境用hdfs dfs -chown spark:spark /spark-jobs,并配置HDFS ACL

5.2 我踩过的五个深坑及独家避坑技巧

坑1:Kerberos票据过期导致任务随机失败
现象:任务白天正常,凌晨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-node@EXAMPLE.COM # 加入crontab:0 */5 * * * /opt/spark/bin/kinit-auto.sh

坑2:Spark 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)

坑3:HDFS小文件过多,任务启动巨慢
现象:spark-submit执行后,卡在“Loading Spark configuration from Hadoop configuration”长达2分钟。
根因:HDFS上/spark-jobs/lib/目录有2000+个小jar,listStatus操作遍历耗时。
避坑技巧:用Hadoop Archive(HAR)打包依赖:

hadoop archive -archiveName libs.har -p /spark-jobs/lib /spark-jobs/archives/ # 提交时用--archives hdfs://nameservice1/spark-jobs/archives/libs.har#libs

坑4:MySQL连接池耗尽,Spring Boot假死
现象:连续提交10个任务后,Spring Boot HTTP接口全部超时,但进程仍在。
根因:每个spark-submit进程启动时,Spring Boot的Hik

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 8:15:39

双技术栈在线订餐系统:SSM与Django共用MySQL的架构设计与实战

第一次把这个订餐管理系统完整跑通的时候&#xff0c;我的第一反应不是“终于能下单了”&#xff0c;而是“两套后端居然真的能在一张订单表上协作得这么干净”。这个系统本身其实就是典型的在线订餐业务&#xff1a;用户在小程序或者网页上浏览菜品、加购物车、下单支付&#…

作者头像 李华
网站建设 2026/10/22 8:15:06

Symphony实战:构建多编码代理协同与安全沙箱管理平台

1. 这项目到底解决了什么问题 1.1 编码代理的“散养”困境 过去一年多&#xff0c;编码代理的发展速度远超多数人的预期。它从一个只能补全半行代码的辅助工具&#xff0c;进化成能理解整个仓库、独立写单元测试、自己跑持续集成流程的“半独立工程师”。很多技术团队手里同时…

作者头像 李华
网站建设 2026/10/22 8:14:58

Elasticsearch容器化部署:Docker镜像选型与多节点集群实践

1. 为什么现在装Elasticsearch&#xff0c;Docker几乎成了默认选项&#xff1f;我第一次在某高校实验室部署Elasticsearch时&#xff0c;用的是官网下载的tar包&#xff0c;手动解压、改配置、调JVM参数、写systemd服务文件……整整花了两天。中间因为Java版本不匹配导致集群启…

作者头像 李华
网站建设 2026/10/22 8:14:30

AI模型冷启动优化实践:从磁盘IO到量化压缩

上个月在调一个图像检测服务的冷启动性能&#xff0c;模型只有 200 多 MB&#xff0c;不算大&#xff0c;但每次进程重启后第一次请求都要等 6 秒多。翻日志发现&#xff0c;80% 的时间都花在了“AI 模型加载”上&#xff1a;从磁盘读文件、反序列化权重、初始化运行时上下文&a…

作者头像 李华
网站建设 2026/10/22 8:14:09

快速排序指针移动方向详解:从分区目标推导i找大j找小

快排学到指针移动方向的时候&#xff0c;我卡了整整一个晚上。网上教程要么直接给口诀&#xff0c;要么默认你懂分区目标&#xff0c;死活想不明白“i找大、j找小”和“i找小、j找大”到底怎么选。后来我拿笔把两个方向都推了一遍&#xff0c;又把key取左、key取右的情况都试了…

作者头像 李华
网站建设 2026/10/22 8:14:00

PowerShell管理计划任务全攻略:脚本化注册查询更新删除

你有没有遇到过这种情况&#xff1a;接手一台服务器&#xff0c;打开“任务计划程序”&#xff0c;看到一堆任务&#xff0c;不知道是谁建的、为什么要在这个时间跑、改过什么设置。我反正是受够了&#xff0c;所以从某一年开始&#xff0c;凡是需要周期执行的事情&#xff0c;…

作者头像 李华