当前位置: 首页 > 工具软件 > Yan Container > 使用案例 >

SpringBoot + Spark on Yan踩坑记

曹兴贤
2023-12-01
  • 终极目标
    采用SpringBoot集成SparkStreaming+Kafka+Redis+HDFS等组件,打包成jar(下文将之简称为“应用”),运行在Spark on Yan环境,实现离线+实时分布式计算。
    本文中采用Spark on Yan Client模式发布应用。
    参考文献:https://www.cnblogs.com/langfanyun/p/8040136.html
     
  • 第一坑:启动方式与local模式迥异
    local模式(setMaster("local[2]"))下,SpringBoot集成SparkStreaming的app与其他web应用无差异,直接java -jar xxx.jar即可启动。
    Spark on Yan环境下, 必须使用spark-submit等命令,由Spark-Yarn-Client托管应用的jar,否则,应用的jar中缺少向Yarn资源管理器申请资源的模块,无法正常启动。
    【对策】
    1)应用的pom中引入spark-yarn依赖

    <dependency>

        <groupId>org.apache.spark</groupId>

        <artifactId>spark-yarn_${scala.tools.version}</artifactId>

        <version>${spark.core.version}</version>

    </dependency>

    其中scala.tools.version为scala版本,spark.core.version为spark版本

     

    2)sparkConf设置yarn相关配置

    val conf = new SparkConf().setMaster("yarn-client").setAppName(sparkAppName)

    3)启动命令

    /bin/spark-submit --conf spark.yarn.jars="local:/opt/xxx/lib/spark/jars/*,local:/opt/xxx/lib/spark/hive/*,hdfs://xxx:8020/user/mypath/my-service/lib/*" --properties-file /home/mypath/spark.conf --driver-java-options "-Dorg.springframework.boot.logging.LoggingSystem=none -Dspring.profiles.active=test -Dspark.yarn.dist.files=/home/mypath/yarn-site.xml" --class org.springframework.boot.loader.JarLauncher --master yarn --deploy-mode client my-service-1.0.0-SNAPSHOT.jar

    各目录的说明:“/opt/xxx/lib/spark”部分为示例机器安装Spark的目录,根据实际情况修改即可。"hdfs://xxx:8020/user/mypath/my-service/lib"部分为示例机器hdfs上存储应用依赖的库文件的目录,根据实际情况修改即可。“/home/mypath/”部分为示例机器上存放spark配置文件的目录,根据实际情况修改即可。

    各参数的说明:“spark.yarn.jars”用于指定executor容器运行时依赖的jar的目录,特别是“hdfs://xxx:8020/user/mypath/my-service/lib/*”非常关键,后面将会讲到相关的各种坑。“--properties-file”指定spark自定义配置文件,可绕开许多坑,后面也会讲到。“--driver-java-options”用于指定应用的运行环境参数,绕坑专业户,且听下文分解。“–class”必须指定springboot的main所在class,否则就是坑。

     

  • 第二坑:logback和log4j冲突
    spark-submit内部使用log4j作为日志模块,SpringBoot默认采用logback作为日志模块,默认情况下将出现以下各种异常。

    java.lang.ClassCastException: org.apache.logging.log4j.simple.SimpleLoggerContext cannot be cast to org.apache.logging.log4j.core.LoggerContext

    ClassCastException: org.slf4j.impl.Log4jLoggerFactory cannot be cast to ch.qos.logback.classic.LoggerContext

    LoggerFactory is not a Logback LoggerContext but Logback...

    【对策】
    1)pom中绕开所有logback

    <exclusions>

        <exclusion>

            <groupId>ch.qos.logback</groupId>

           <artifactId>logback-classic</artifactId>

        </exclusion>

    </exclusions>

    2)springboot停止尝试加载任何logger

    具体办法是在启动命令中增加java参数org.springframework.boot.logging.LoggingSystem,指定为none即可。

    /bin/spark-submit ... --driver-java-options "-Dorg.springframework.boot.logging.LoggingSystem=none ..." ... my-service-1.0.0-SNAPSHOT.jar

  • 第三坑:GSON版本冲突
    Spark自带的GSON版本可能与SpringBoot依赖的版本冲突,引起如下异常:

    Failed to instantiate [com.google.gson.GsonBuilder

    【对策】
    将Spark自带的GSON库重命名

  • 第四坑:Hadoop版本冲突
    集群环境的Hadoop版本可能与应用依赖的版本冲突,引起如下异常:

    HftpFileSystem cannot access its superinterface

    【原因】应用依赖了2.6.x版本,而集群环境的是3.0.x
    【对策】修改应用pom文件,将hadoop版本改为与集群环境一致

  • 第五坑:运行时抛如下找不到ExecutorLauncher异常

    spark Could not find or load main class ExecutorLauncher

    【原因】一开始命令行未指定“spark.yarn.jars”参数,导致executor节点缺少依赖库

    【对策】命令行指定“spark.yarn.jars”参数,将必要的库目录都加上
     

  • 第六坑:运行时找不到应用中的类

    原因同第五坑

    【对策】

    1)将应用的jar解压,将lib目录下所有jar上传HDFS目录"hdfs://xxx:8020/user/mypath/my-service/lib"

    2)"hdfs://xxx:8020/user/mypath/my-service/lib"追加到命令行“spark.yarn.jars”参数中

    3)删除其中可能与集群环境冲突的包

    /bin/hdfs dfs -rm hdfs://xxx:8020/user/mypath/my-service/lib/spark*

    /bin/hdfs dfs -rm hdfs://xxx:8020/user/mypath/my-service/lib/hadoop*

    /bin/hdfs dfs -rm hdfs://xxx:8020/user/mypath/my-service/lib/scala*

    4)添加集群环境没有的包

    /bin/hdfs dfs -put /home/mypath/my-service/lib/scala-logging-*.jar   /user/mypath/my-service/lib/

       5)spark.conf配置文件中使用“spark.executor.extraLibraryPath”参数指定executor节点的库目录

spark.executor.extraLibraryPath=hdfs://xxx:8020/user/mypath/my-service/lib/,/opt/xxx/lib/hadoop/lib/native
  • 第七坑:序列化、反序列化方式冲突

    集群环境默认采用kryoserilizer,与应用中默认的JavaSerializer冲突,引起如下异常:

    Tuple20 cannot be cast to scala.collection.Seq

    【对策】修改spark.conf配置文件,指定采用兼容性更好的JavaSerializer(据说性能差一些)

    spark.serializer=org.apache.spark.serializer.JavaSerializer

  • 第八坑:lz4压缩组件版本冲突

    集群环境和应用中的lz4压缩组件版本可能不同,引起如下异常

    java.lang.NoSuchMethodError: net.jpountz.lz4.LZ4BlockInputStream.<init>(Ljava/io/InputStream;Z)

    【对策】修改spark.conf配置文件,指定采用兼容性更好的snappy方式压缩

    spark.io.compression.codec=snappy

  • 第九坑:低版本Spark有概率触发与netty框架不兼容的bug

    低版本Spark(如2.2.2)与netty框架存在兼容性问题,可能出现以下异常。具体场景不明。

    java.io.IOException: Failed to send RPC 6940843803625479358 to /xxx:34735: java.lang.AbstractMethodError: org.apache.spark.network.protocol.MessageWithHeader.touch(Ljava/lang/Object;)Lio/netty/util/ReferenceCounted;

    【原因】低版本Spark的org.apache.spark.network.protocol.MessageWithHeader类没有实例化虚函数touch(obj)

    【对策】使用Spark2.4.0或更高版本

  • 第十坑:应用增加了对HDFS的依赖后,抛出ClassCastException

    java.lang.ClassCastException: cannot assign instance of scala.collection.immutable.List$SerializationProxy to field org.apache.spark.rdd.RDD.org$apache$spark$rdd$RDD$$dependencies_ of type scala.collection.Seq in instance of org.apache.spark.rdd.MapPartitionsRDD

    【原因】SpringBoot默认的打包方式,会将所有依赖的jar打包成一个Fat Jar,运行时很方便,但副作用是其中的Hadoop、Spark相关类库可能与Yarn环境冲突,引起各种奇怪的异常
    【对策】对SpringBoot打的包瘦身
    1)POM文件修改spring-boot-maven-plugin配置(includes),仅打包项目本身代码

    ...
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                       <includes>
                          <include>
                              <groupId>xxx</groupId>
                              <artifactId>yyy-service</artifactId>
                          </include>
                       </includes>
                    </configuration>
                </plugin>
                ...

    2)将依赖的jar上传HDFS(剔除Hadoop、Spark相关类库)
    参考第六坑。解压spring-boot-maven-plugin配置修改前生成的Fat Jar可得到所有依赖的jar。
    3)Yarn环境的Client机器保留一份完整的依赖jar(同样剔除Hadoop、Spark相关类库)
    比如放在/home/xxx/lib目录下
    4)将只含有项目本身代码的jar上传HDFS,Executor反序列化时需要使用

    # xxx-service-1.0.0-SNAPSHOT.jar.original是SpringBoot打包完留在target目录下的只含有项目本身代码的jar
    # 将其重命名为xxx-api.jar,并放在Driver不会访问的目录下,防止与SpringBoot打的包冲突
    mv xxx-service-1.0.0-SNAPSHOT.jar.original otherLib/xxx-api.jar
    # 每次代码有更新时,删除旧版本
    hdfs dfs -rm   /xxx/xxx-service/lib/xxx-api.jar
    # 上传新版本
    hdfs dfs -put /xxx/otherLib/xxx-api.jar   /xxx/xxx-service/lib/

    5)spark-submit命令增加-Djava.ext.dirs参数,确保应用能找到所依赖的所有库文件

    /bin/spark-submit --conf spark.yarn.jars="local:/xxx/lib/spark/jars/*,local:/xxx/lib/spark/hive/*,hdfs://xxx:8020/xxx/xxx-service/lib/*" --properties-file /xxx/spark.conf --driver-java-options "-Dorg.springframework.boot.logging.LoggingSystem=none -Dspring.profiles.active=test -Dspark.yarn.dist.files=/xxx/yarn-site.xml -Djava.ext.dirs=/xxx/lib:$JAVA_HOME/jre/lib/ext:/xxx/lib/spark/jars:/xxx/lib/spark/hive" --class org.springframework.boot.loader.JarLauncher --master yarn --deploy-mode client xxx-service-1.0.0-SNAPSHOT.jar >> console.log 2>&1

     

  • 第十一坑:代码更新后库文件版本冲突
    库文件版本冲突时,会出现如下异常:

    java.io.InvalidClassException (xxx.yyy; local class incompatible: stream classdesc serialVersionUID = xxx, local class serialVersionUID = yyy)

    【对策】保持以下几处库文件版本一致。

    1)spark-submit提交的xxx-service-1.0.0-SNAPSHOT.jar

    2)仅包含项目源码,不含SpringBoot库的jar:hdfs://xxx:8020/xxx/xxx-service/lib/xxx-api.jar

    3)java.ext.dirs参数指定的类库目录/xxx/lib中,不要出现上述两个jar

 

  • spark.conf示例

    spark.authenticate=false

    spark.driver.log.dfsDir=/user/spark/driverLogs

    spark.driver.log.persistToDfs.enabled=true

    spark.dynamicAllocation.enabled=true

    spark.dynamicAllocation.executorIdleTimeout=60

    spark.dynamicAllocation.minExecutors=0

    spark.dynamicAllocation.schedulerBacklogTimeout=1

    spark.eventLog.enabled=true

    spark.io.encryption.enabled=false

    spark.io.compression.codec=snappy

    spark.network.crypto.enabled=false

    spark.serializer=org.apache.spark.serializer.JavaSerializer

    spark.shuffle.service.enabled=true

    spark.shuffle.service.port=7337

    spark.ui.enabled=true

    spark.ui.killEnabled=true

    spark.lineage.log.dir=/data/var/log/spark/lineage

    spark.lineage.enabled=true

    spark.master=yarn

    spark.submit.deployMode=client

    spark.eventLog.dir=hdfs://xxx:8020/user/spark/applicationHistory

    spark.yarn.historyServer.address=http://xxx:18088

    spark.yarn.jars=local:/opt/xxx/lib/spark/jars/*,local:/opt/xxx/lib/spark/hive/*

    spark.driver.extraLibraryPath=/opt/xxx/lib/hadoop/lib/native

    spark.executor.extraLibraryPath=hdfs://xxx:8020/user/mypath/my-service/lib/,/opt/xxx/lib/hadoop/lib/native

    spark.yarn.am.extraLibraryPath=/opt/xxx/lib/hadoop/lib/native

    spark.yarn.config.gatewayPath=/opt/cloudera/parcels

    spark.yarn.config.replacementPath={{HADOOP_COMMON_HOME}}/../../..

    spark.yarn.historyServer.allowTracking=true

    spark.yarn.appMasterEnv.MKL_NUM_THREADS=1

    spark.executorEnv.MKL_NUM_THREADS=1

    spark.yarn.appMasterEnv.OPENBLAS_NUM_THREADS=1

    spark.executorEnv.OPENBLAS_NUM_THREADS=1

    spark.extraListeners=com.cloudera.spark.lineage.NavigatorAppListener

    spark.sql.queryExecutionListeners=com.cloudera.spark.lineage.NavigatorQueryListener

  • yarn-site.xml示例

    <?xml version="1.0" encoding="UTF-8"?>

     

    <!--Autogenerated by Cloudera Manager-->

    <configuration>

      <property>

        <name>yarn.acl.enable</name>

        <value>true</value>

      </property>

      <property>

        <name>yarn.admin.acl</name>

        <value>*</value>

      </property>

      <property>

        <name>yarn.resourcemanager.address</name>

        <value>xxx:8032</value>

      </property>

      <property>

        <name>yarn.resourcemanager.admin.address</name>

        <value>xxx:8033</value>

      </property>

      <property>

        <name>yarn.resourcemanager.scheduler.address</name>

        <value>xxx:8030</value>

      </property>

      <property>

        <name>yarn.resourcemanager.resource-tracker.address</name>

        <value>xxx:8031</value>

      </property>

      <property>

        <name>yarn.resourcemanager.webapp.address</name>

        <value>xxx:8088</value>

      </property>

      <property>

        <name>yarn.resourcemanager.webapp.https.address</name>

        <value>xxx:8090</value>

      </property>

      <property>

        <name>yarn.resourcemanager.client.thread-count</name>

        <value>50</value>

      </property>

      <property>

        <name>yarn.resourcemanager.scheduler.client.thread-count</name>

        <value>50</value>

      </property>

      <property>

        <name>yarn.resourcemanager.admin.client.thread-count</name>

        <value>1</value>

      </property>

      <property>

        <name>yarn.scheduler.minimum-allocation-mb</name>

        <value>1024</value>

      </property>

      <property>

        <name>yarn.scheduler.increment-allocation-mb</name>

        <value>512</value>

      </property>

      <property>

        <name>yarn.scheduler.maximum-allocation-mb</name>

        <value>102881</value>

      </property>

      <property>

        <name>yarn.scheduler.minimum-allocation-vcores</name>

        <value>1</value>

      </property>

      <property>

        <name>yarn.scheduler.increment-allocation-vcores</name>

        <value>1</value>

      </property>

      <property>

        <name>yarn.scheduler.maximum-allocation-vcores</name>

        <value>24</value>

      </property>

      <property>

        <name>yarn.resourcemanager.amliveliness-monitor.interval-ms</name>

        <value>1000</value>

      </property>

      <property>

        <name>yarn.am.liveness-monitor.expiry-interval-ms</name>

        <value>600000</value>

      </property>

      <property>

        <name>yarn.resourcemanager.am.max-attempts</name>

        <value>2</value>

      </property>

      <property>

        <name>yarn.resourcemanager.container.liveness-monitor.interval-ms</name>

        <value>600000</value>

      </property>

      <property>

        <name>yarn.resourcemanager.nm.liveness-monitor.interval-ms</name>

        <value>1000</value>

      </property>

      <property>

        <name>yarn.nm.liveness-monitor.expiry-interval-ms</name>

        <value>600000</value>

      </property>

      <property>

        <name>yarn.resourcemanager.resource-tracker.client.thread-count</name>

        <value>50</value>

      </property>

      <property>

        <name>yarn.application.classpath</name>

        <value>$HADOOP_CLIENT_CONF_DIR,$HADOOP_COMMON_HOME/*,$HADOOP_COMMON_HOME/lib/*,$HADOOP_HDFS_HOME/*,$HADOOP_HDFS_HOME/lib/*,$HADOOP_YARN_HOME/*,$HADOOP_YARN_HOME/lib/*</value>

      </property>

      <property>

        <name>yarn.resourcemanager.scheduler.class</name>

        <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler</value>

      </property>

      <property>

        <name>yarn.scheduler.capacity.resource-calculator</name>

        <value>org.apache.hadoop.yarn.util.resource.DefaultResourceCalculator</value>

      </property>

      <property>

        <name>yarn.resourcemanager.max-completed-applications</name>

        <value>10000</value>

      </property>

      <property>

        <name>yarn.nodemanager.remote-app-log-dir</name>

        <value>/data/tmp/logs</value>

      </property>

      <property>

        <name>yarn.nodemanager.remote-app-log-dir-suffix</name>

        <value>logs</value>

      </property>

    </configuration>

 

 类似资料: