背景信息
程序上的事必须得把背景交代清楚,不然容易变成玄学。
简单的来讲,可以理解为我们的大数据集群中部署了两套Hive服务,暂且叫做HiveA和HiveB吧,可能你会问我为啥要部署两套Hive呢,前面说了这是一种简单的说法,其实HiveA是Apache下原汁原味的Hive,HiveB则是我们基于Apache Hive做了很多的二次开发。
原先HiveA使用集群中公用的Spark服务运行任务,HiveB使用我们内置Spark服务运行任务。内置Spark服务我们也是基于Apache Spark做了一些二次开发,为了简化维护工作,我们把Spark中一些二次开发的功能集成到了Apache Spark中了。于是乎,内置的Spark服务就可以退役了。然后自然而然的事情就是我们的HiveA和HiveB都要使用公用的Spark服务运行任务了。
问题点
需要解决的问题其实很简单,就是HiveA on Spark和HiveB on Spark时需要分别使用不同的hive-site.xml
配置文件,那么如何实现呢,聪明的你肯定知道有两种方式:
--conf key=value
setConf("key","value")
我们知道spark配置加载的时候有一个优先级的,那我们先看看这个优先级是怎么来的,首先打开spark(3.1.3)的代码,在SparkHadoopUtils.scala
的425行:
private[spark] def newConfiguration(conf: SparkConf): Configuration = {
val hadoopConf = new Configuration()
appendS3AndSparkHadoopHiveConfigurations(conf, hadoopConf)
hadoopConf
}
这里就是spark加载配置的地方了,首先是创建一个hadoopConf
,毫无疑问是从classPath
下加载配置文件了,如果你有疑问,那么请问问度娘。然后是调用一个方法,看名字应该是加载S3
,Spark
,Hadoop
,Hive的配置了
,那么各位看官跟着我继续往下看。
private def appendS3AndSparkHadoopHiveConfigurations(
conf: SparkConf,
hadoopConf: Configuration): Unit = {
// Note: this null check is around more than just access to the "conf" object to maintain
// the behavior of the old implementation of this code, for backwards compatibility.
if (conf != null) {
// Explicitly check for S3 environment variables
val keyId = System.getenv("AWS_ACCESS_KEY_ID")
val accessKey = System.getenv("AWS_SECRET_ACCESS_KEY")
if (keyId != null && accessKey != null) {
hadoopConf.set("fs.s3.awsAccessKeyId", keyId)
hadoopConf.set("fs.s3n.awsAccessKeyId", keyId)
hadoopConf.set("fs.s3a.access.key", keyId)
hadoopConf.set("fs.s3.awsSecretAccessKey", accessKey)
hadoopConf.set("fs.s3n.awsSecretAccessKey", accessKey)
hadoopConf.set("fs.s3a.secret.key", accessKey)
val sessionToken = System.getenv("AWS_SESSION_TOKEN")
if (sessionToken != null) {
hadoopConf.set("fs.s3a.session.token", sessionToken)
}
}
appendHiveConfigs(hadoopConf)
appendSparkHadoopConfigs(conf, hadoopConf)
appendSparkHiveConfigs(conf, hadoopConf)
val bufferSize = conf.get(BUFFER_SIZE).toString
hadoopConf.set("io.file.buffer.size", bufferSize)
}
}
很明显是不是,首先是把S3
的配置放到hadoopConf
中,然后依次调用这三个方法appendHiveConfigs
,appendSparkHadoopConfigs
,appendSparkHiveConfigs
,很明显,如果这三个方法中有相同的配置,那么后调用的方法会覆盖先调用的方法中设置的配置项。
优先级由低到高,我们来分析一下,首先是appendHiveConfigs
方法:
private def appendHiveConfigs(hadoopConf: Configuration): Unit = {
hiveConfKeys.foreach { kv =>
hadoopConf.set(kv.getKey, kv.getValue)
}
}
private lazy val hiveConfKeys = {
val configFile = Utils.getContextOrSparkClassLoader.getResource("hive-site.xml")
if (configFile != null) {
val conf = new Configuration(false)
conf.addResource(configFile)
conf.iterator().asScala.toSeq
} else {
Nil
}
}
看到了吧,这个方法是加载hive-site.xml
文件中的 配置的,无需多言,接着往下看appendSparkHadoopConfigs
方法:
private def appendSparkHadoopConfigs(conf: SparkConf, hadoopConf: Configuration): Unit = {
// Copy any "spark.hadoop.foo=bar" spark properties into conf as "foo=bar"
for ((key, value) <- conf.getAll if key.startsWith("spark.hadoop.")) {
hadoopConf.set(key.substring("spark.hadoop.".length), value)
}
if (conf.getOption("spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version").isEmpty) {
hadoopConf.set("mapreduce.fileoutputcommitter.algorithm.version", "1")
}
// Since Hadoop 3.3.1, HADOOP-17597 starts to throw exceptions by default
if (conf.getOption("spark.hadoop.fs.s3a.downgrade.syncable.exceptions").isEmpty) {
hadoopConf.set("fs.s3a.downgrade.syncable.exceptions", "true")
}
// In Hadoop 3.3.1, AWS region handling with the default "" endpoint only works
// in EC2 deployments or when the AWS CLI is installed.
// The workaround is to set the name of the S3 endpoint explicitly,
// if not already set. See HADOOP-17771.
// This change is harmless on older versions and compatible with
// later Hadoop releases
if (hadoopConf.get("fs.s3a.endpoint", "").isEmpty &&
hadoopConf.get("fs.s3a.endpoint.region") == null) {
// set to US central endpoint which can also connect to buckets
// in other regions at the expense of a HEAD request during fs creation
hadoopConf.set("fs.s3a.endpoint", "s3.amazonaws.com")
}
}
这个方法就是加载你通过--conf
或者setConf
设置的参数,不过key
会取截取spark.hadoop.
之后的字符串。接着来看appendSparkHiveConfigs
方法:
private def appendSparkHiveConfigs(conf: SparkConf, hadoopConf: Configuration): Unit = {
// Copy any "spark.hive.foo=bar" spark properties into conf as "hive.foo=bar"
for ((key, value) <- conf.getAll if key.startsWith("spark.hive.")) {
hadoopConf.set(key.substring("spark.".length), value)
}
}
哼哼~~~简单的不用解释。
好了,看到这里应该把saprk
加载配置的流程看明白了吧,想要加载什么配置通过上述的两种方法设置就可以了。你不会天真的以为我写这篇博客就这点东西吧。no! no! no !接着往下看。
不一样的烟火
通过上面的描述,我们知道了怎么设置Spark
参数了,但是别忘了,我们是Hive on Spark
呀,不是直接使用spark-submit
提交任务的,所以我们首先需要找到Hive on Spark
是怎么提交spark
任务的,通过查看HiveServer
的日志我们可以发现下面这段命令:
Running client driver with argv:kinit hive/[email protected] -k -t /etc/security/keytabs/hive.service.keytab; /usr/hdp/current/spark-client/bin/spark-submit --executor-cors 1 --executor-memory 1024m --num-executors 2 --proxy-user test --properties-file /tmp/spark-submit.xxxxxxxx.properties --class org.apache.hive.spark.client.RemoteDriver /usr/hdp/3.0.1.0-187/hiveb/lib/hive-exec-2.3.9.jar --remotes-host xxxxx --remote-port xxxxx --conf hive.spark.client.connect.timeout=1000 --conf hive.spark,client.serverr.connect.timeout=90000 --conf ......
明白了吧,Hive
提交Spark
任务跟我们是一个样的,Spark
面前人人平等,管你是大神还是老六。
这样就简单了,我们直接在Hive
(2.3.9)代码里面全局搜索spark-submit
不就行了吗,找找看是在那里拼接的这段字符串就行了,功夫不负有心人呀,终于让我在SparkClientImpl.java
的startDriver
方法里面找到了。最终通过argv.add()
方法把hive.metastore.uris
等配置加上了。
编译---替换jar包---重启服务---运行任务 ------> 傻眼
直接上报错日志吧:DIGEST-MD5:IO error acquiring password
,这是啥意思,不明白。去spark ui
上看日志去,到executor
页面去看driver
的输出日志,发现几行重复日志Trying to connect to metastore with URI:xxxxxxxxxx,Failed to connect to the MetaStore Server...
,这是连接hive
的metastore
的出错了呀。于是检查下URI
,没错呀,就是我通过--conf
设置的URI
呀,那为什么就连不上了呢。没有办法,去看metastore
的日志吧,在日志中发现了找到这么一条信息:Caused by:org.apache.hadoop.security.token.SecuretManager$InbalidToken:token expired or does not exists:HIVE_DELEGATION_TOKEN owner=user01 ,renewer=hive,realuser=hive/node1@TEST@COM issueDate=xxxxxxxx,maxDate=xxxxxxxx,sequenceNumber=295,masterKeyId=1
.有点意思,Token
这种东西不都是spark
自己维护的吗,怎么还会过期或者不存在呢,首先检查下issueDate
和maxDate
,发现时间没有问题,那就不是过期的事了。那为什么会不存在呢?想想想,使劲想,想不出来个所以然。再仔细的看下hive
中执行失败的堆栈信息,发现报错的代码发生在我们二次开发的这部分中。分析一下吧,因为这代码不是我写的。
分析代码
这部分代码主要流程就是在hive
的RemoteDriver
中初始化了一个SparkSession
,然后使用SparkSession.sql
执行同步hive
分区的sql
命令。ok,那我们先试试不走这段流程的sql
会不会有问题,不出所料,没有问题。那接下来就看下这部分代码就可以了。主要代码如下:
...
SparkConf conf = new SparkConf();
String serverAddress = null;
int serverPort = -1;
Map<String, String> mapConf = Maps.newHashMap();
for (int idx = 0; idx < args.length; idx += 2) {
String key = args[idx];
if (REMOTE_DRIVER_HOST_CONF.equals(key)) {
serverAddress = getArg(args, idx);
} else if (REMOTE_DRIVER_PORT_CONF.equals(key)) {
serverPort = Integer.parseInt(getArg(args, idx));
} else if (REMOTE_DRIVER_CONF.equals(key)) {
String[] val = getArg(args, idx).split("[=]", 2);
conf.set(val[0], val[1]);
} else {
throw new IllegalArgumentException("Invalid command line arguments: "
+ Joiner.on(" ").join(args));
}
}
...
sparkSession = SparkSession.builder().config(conf).enableHiveSupport().getOrCreate();
...
这里的args
就是hive
拼接的spark-submit
的时候添加的参数,可以看到这些参数也设置到了conf
里面,然后通过conf
构造了SparkSession
。有问题吗?没看出来。
瞎搞
把spark conf
下hive-site.xml
文件中的hive.metastore.uris
改为HiveB
的连接。重跑sql
发现可以了,改回去之后又不行了。于是基本确定,还是哪个地方是从hive-site.xml
中加载的配置。会是那里呢,于是在spark
的SparkHadoopUtils.scala#appendS3AndSparkHadoopHiveConfigurations
方法中在appendSparkHiveConfigs(conf, hadoopConf)
这句下面输出一行日志,看下此时获取到的hive.metastore.uris
是什么,结果有两种:
- main线程中输出的日志是从
hive-site.xml
加载的hive.metastore.uris
- driver线程输出的日志是我们通过
--conf
设置的hive.metastore.uris
有意思吧,真滴让人头大,咋还会有两种结果呢。
既然是spark
程序,总绕不过SparkSubmit.scala
这里吧,于是必须得SparkSubmit.scala
讨个说法,在SparkSubmit.scala
中看到了这行代码:val hadoop = conf.getOrElse(SparkHadoopUtils.newConfiguration(sparkConf))
.嗯~~~这不就是加载配置的地方吗,于是输出几行日志,分别查看下args
,sparkConf
中都藏着什么鸟玩意。通过输出的日志发现,args
中有很多参数,其中通过--conf
传递的参数被放在了childArg
中了,sparkConf
中的参数都是从--properties-file
指定的文件中加载的参数,因此SparkConf
中根本没有hive.metastore.uris
配置,所以使用的还是从hive-site.xml
加载的hive.metastore.uris
,到了这里,问题算是解决了,把hive.metastore.uris
配置写入到临时文件--properties-file
中就可以了。
总结
--conf
传递参数是如何传递到driver中的这个问题还没有搞清楚,时间紧任务重,日后再看咯。