前言

本文起因于近段时间在做的项目,百亿级数据的存储与实时检索。项目主要采用HBase存储全量数据,Elasticsearch作为二级索引库,数据入库采用Spark批量写入,最终数据通过微服务平台API接口对外提供实时线上访问。

  • HBase端采用saveAsNewHadoopFile算子生成HFile文件,然后通过HBase自带bulk load方式进行加载
  • Elasticsearch端采用批量bulk方式写入数据

具体方案由于工作原因,不便透漏,此处不再详细叙述,感兴趣的可以至 留言板 或者 随心聊 模块私信本人。

下面主要叙述项目上线后 Spark on yarn 运行时 Jar 包加载异常的解决方案。

生产异常报错

由于表入库时需要读取 Mysql 数据库当前表的配置信息,因此项目中添加了 Mysql 驱动的依赖,测试环境从开发到上线均正常运行。但一上生产通过调度工具调用起来直接就报错,我们这上线换版出现问题是比较严重的,所以当时还是小小惊了一下的,然后抓紧时间通过电话排查问题(此时我已经回家了)。

1.查看运行日志

1
2
3
4
// 主要日志
java.sql.SQLException: No suitable driver

后续日志省略...

很明显就是找不到合适的驱动,明显是挺明显,但是很懵逼…
明显的是驱动加载的问题,懵逼的是测试环境运行居然顺畅无阻。

懵逼归懵逼但还是要抓紧时间解决问题,中间又让同事在测试环境同步进行排查,并解压检查上线包里的mysql驱动是否package进去了,但是无疾而终。由于连接mysql这块的代码不是我开发的,具体情况也不太清楚,只知道测试环境跑的没有问题。没办法,只好想着先通过临时指定驱动包(submit提交时通过参数指定)的方式进行解决,但通过网上查到的方式,似乎有的可以有的不行,而我们这的生产环境又不让做任何尝试性的测试操作(很扯dan)。由于不能百分百的确定可以,所以这种方式打算暂时放弃。后来想了想干脆直接把mysql驱动包放到${SPARK_HOME}/jars目录下得了,这样在运行时肯定是会被加载到的。结果自然是可以运行了,但是对于源代码运行异常,还得需要进一步排查。

第二天到公司后,在测试环境开始分析可以运行的原因,最后发现测试环境每台服务器 ${JAVA_HOME}/jre/lib/ext 目录下都存在 mysql-jdbc 的驱动包(不知道哪位大佬先前埋下的坑,目前也无从查起),把驱动包手动移除之后再重新跑Spark任务,也开始报和生产一样的错,而生产环境后来检查了环境之后,发现JAVA_HOME目录下是没有这个包的。所以就可以解释生产异常而测试通过了。

环境不一致归不一致,但是理论上项目中已经加载了mysql驱动的依赖,并且也确定发版后驱动包也包含在内,那么还出现运行时加载异常的问题,就大致可以定位以下两种情况:

  • 读取mysql部分的代码问题(代码中未明确指定Driver)
  • spark-submit脚本提交任务时参数(jar包加载)问题

Spark on yarn加载的jar

在进行试验之前先了解下 spark on yarn 运行时会加载的jar包:

  • spark-submit中指定的–jars
  • $SPARK_HOME/jars下的jar包
  • yarn提供的jar包
  • spark-submit通过参数spark.driver/executor.extraClassPath指定的jar包

spark-submit中指定jar

当使用如下的脚本提交应用时,会将应用本身的jar以及–jar指定的jar包上传到集群中。

1
2
3
4
./bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--jars a.jar,b.jar,c.jar \
http://path/to/examples.jar \

–jar是以逗号分隔的jar包列表,不支持直接使用目录。
–jar上传的包会包含在Driver和Executor的classpath中

$SPARK_HOME/jars下的jar包

提交应用时,会将$SPARK_HOME/jars下的所有jar包打成一个zip包,上传到集群中。“打zip上传”这个操作会在每次提交应用时执行,会有一点的性能损耗。

yarn提供的jar包

在yarn-site.xml中会配置yarn.application.classpath,包含hadoop相关的一些包,这些包也会在应用提交的时候被加载。

1
2
3
4
<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>

通过参数指定的jar包

1
2
3
4
spark.executor.extraClassPath #显式地将jar包注册到executor的classpath中
spark.driver.extraClassPath #与executor配置项同理
spark.driver.userClassPathFirst=true
spark.executor.userClassPathFirst=true

通过extraClassPath指定jar包的方式和之前通过 –jars 差不多,只不过extraClassPath可以通过指定目录的方式来指定,如/cdh/jars/*。
另外extraClassPath可以通过配置userClassPathFirst来保证用户指定的jar包先被加载,这在解决冲突时是作用很大的。

实验测试结果

在明确了上一部分的各种加载方式之后,针对驱动包的加载进行了测试,其结果如下:

spark-submit主要测试参数

1
2
3
4
--jars ${jdbcpath}
--driver-class-path ${jdbcpath}
--conf spark.driver.extraClassPath=${jdbcpath}
--conf spark.executor.extraClassPath=${jdbcpath}

代码读取mysql数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
val url: String = "jdbc:mysql://IP:port/dbName"
val userPass: String = "user=xxx&password=xxx"

//方式一:不显示指定Driver
spark.read.jdbc(s"$url?$userPass", "tableName", new Properties())

//方式二
val properties: Properties = new Properties()
properties.put("driver", "com.mysql.jdbc.Driver")
properties.put("user", "xxx")
properties.put("password", "xxx")
spark.read.jdbc(url, "tableName", properties)

//方式三
spark.read.format("jdbc")
.option("url", url)
.option("driver", "com.mysql.jdbc.Driver")
.option("user", "xxx")
.option("password", "xxx")
.option("dbtable", "tableName")
.load()

最终测试结论

1. 驱动放入 SPARK_HOME/jars 或者 JAVA_HOME/jre/lib/ext 目录
不用重启任何服务,代码中不显示指定Driver,且spark-submit不用额外指定任何参数可以运行;
但不建议将驱动包放至JAVA_HOME中,避免引起较为严重的冲突问题。

2. 代码中指定Driver
spark on yarn client 和 cluster 模式提交任务均可运行成功,且spark-submit中无需额外指定驱动包相关路径参数。

3. 代码中不指定Driver
1) client模式提交

1
2
3
4
5
6
7
8
spark-submit提交需要显示指定以下参数,jdbcpath需设置为本地路径
--driver-class-path ${jdbcpath}

--jars ${jdbcpath}
--conf spark.driver.extraClassPath=${jdbcpath}
--conf spark.executor.extraClassPath=${jdbcpath}

jdbcpath 为 HDFS路径时 或 其它参数情况运行报错:java.sql.SQLException: No suitable driver

2) cluster模式提交

1
2
3
4
5
6
7
8
9
spark-submit提交指定以下参数,jdbcpath需设置为HDFS路径
--driver-class-path ${jdbcpath}

--jars ${jdbcpath}
--conf spark.driver.extraClassPath=${jdbcpath}
--conf spark.executor.extraClassPath=${jdbcpath}

jdbcpath 为 本地路径时 或 其它参数情况运行报错,YARN上报错日志:
Diagnostics: User class threw exception: java.sql.SQLException: No suitable driver