hive on spark配置加载失败问题分析

背景信息

程序上的事必须得把背景交代清楚,不然容易变成玄学。

简单的来讲,可以理解为我们的大数据集群中部署了两套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)
    }
  }

哼哼~~~简单的不用解释。

扫描二维码关注公众号,回复: 14311364 查看本文章

好了,看到这里应该把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.javastartDriver方法里面找到了。最终通过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...,这是连接hivemetastore的出错了呀。于是检查下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自己维护的吗,怎么还会过期或者不存在呢,首先检查下issueDatemaxDate,发现时间没有问题,那就不是过期的事了。那为什么会不存在呢?想想想,使劲想,想不出来个所以然。再仔细的看下hive中执行失败的堆栈信息,发现报错的代码发生在我们二次开发的这部分中。分析一下吧,因为这代码不是我写的。

分析代码

这部分代码主要流程就是在hiveRemoteDriver中初始化了一个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 confhive-site.xml文件中的hive.metastore.uris改为HiveB的连接。重跑sql发现可以了,改回去之后又不行了。于是基本确定,还是哪个地方是从hive-site.xml中加载的配置。会是那里呢,于是在sparkSparkHadoopUtils.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中的这个问题还没有搞清楚,时间紧任务重,日后再看咯。

猜你喜欢

转载自juejin.im/post/7112263932135342094