hive由fs.hdfs.impl.disable.cache参数引起的重写分区数据的异常

QQ交流群:335671559,欢迎交流

问题描述:
已有(外部/内部)表test,新建分区时指定数据位置,如下 
alter table test add partition(day='20140101')
location '20140101';
这样会默认在表warehouse路径下生成/{warehouse}/test/20140101/这种格式目录
同时使用命令 desc formatted test partition(day='20140101')可以查看到相应的location为
hdfs://..:../{warehouse}/test/20140101/

然后使用insert overwrite向分区插入数据
insert overwrite table test partition (day='20140101') 
select xx from xx....;

正常情况下一切正常,但是当设置属性fs.hdfs.impl.disable.cachetrue时,会出现以下情况
desc formatted test partition(day='20140101')时发现location变成了以下格式
hdfs://..:../{warehouse}/test/day=20140101/
同时会在hdfs上生成一个新的目录/{warehouse}/test/day=20140101/,而此分区之前的location路径会被删掉,即/{warehouse}/test/20140101/这个路径被删除

解决:
(1)先看hql的执行计划,大概如下
Stage: Stage-1
    Map Reduce
      Alias -> Map Operator Tree:
        dual
          TableScan
            alias: dual
            Select Operator
              expressions:
                    expr: '1'
                    type: string
                    expr: '2'
                    type: string
              outputColumnNames: _col0, _col1
              File Output Operator
                compressed: true
                GlobalTableId: 1
                table:
                    input format: org.apache.hadoop.mapred.TextInputFormat
                    output format: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
                    serde: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe
                    name: default.test
  Stage: Stage-4
    Move Operator
      files:
          hdfs directory: true
          destination: hdfs://hadoop_namenode/tmp/hive-root/hive_2015-01-07_18-07-13_120_2026314954951095577/-ext-10000
Stage: Stage-0
    Move Operator
      tables:
          partition:
            day 20140101
          replace: true  --overwrite
          table:
              input format: org.apache.hadoop.mapred.TextInputFormat
              output format: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
              serde: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe
              name: default.test

由执行计划能看出,前面的mapreduce过程不会影响到表分区路径的新建或删除,而真正影响到数据的操作是Move Operator

(2)找到Move Operator对应的源码task类,  org.apache.hadoop.hive.ql.exec.MoveTask.java
    该类有个方法是move操作时执行的,public int execute(DriverContext driverContext) {...}方法较长,我们只找主要执行到的部分
    // static partitions   对于静态分区,主要有以下方法执行操作
    db.loadPartition(tbd.getSourcePath(),   //来源数据的位置,即mapreduce计算结果的临时目录
        tbd.getTable()  .getTableName(),    //表的名字
         tbd.getPartitionSpec(),   //获得指定的表分区
        tbd  .getReplace(),   //是否采用覆盖的方式,overwrite
        tbd.getHoldDDLTime(),  //if true, force [re]create the partition,没有分区则新建分区
         tbd  .getInheritTableSpecs(),  //修改的分区是否继承之前的属性,默认为true
        isSkewedStoredAsDirs(tbd));  //表是否是分桶表
    
(3)跟踪方法进入到org.apache.hadoop.hive.ql.metadata.Hive.java类,找到对应的方法loadPartition,找到相应代码段
 Partition oldPart = getPartition(tbl, partSpec, false);
 Path oldPartPath = null;
 if(oldPart != null) {
  oldPartPath = oldPart.getDataLocation(); //表分区定义的location,即我们例子中的 /{warehouse}/test/20140101/
 }
          Path newPartPath = null;
  if (inheritTableSpecs) {//默认值为true
  Path partPath = new Path(tbl.getDataLocation(),Warehouse.makePartPath(partSpec));
  newPartPath = new Path(loadPath.toUri().getScheme(), loadPath.toUri().getAuthority(),
  partPath.toUri().getPath());//值为由表的location信息和分区值组成的路径,即我们例子中的/{warehouse}/test/day=20140101/
  if(oldPart != null) {
   /*
   * If we are moving the partition across filesystem boundaries
   * inherit from the table properties. Otherwise (same filesystem) use the
   * original partition location.
   *
   * See: HIVE-1707 and HIVE-2117 for background
   */
   /*fs.hdfs.impl.disable.cache 这个参数 就影响到以下两个操作,决定了oldPartPathFS与loadPathFS 是否指向同一个对象,进而影响到 newPartPath 的值到底取什么
*/
   FileSystem oldPartPathFS = oldPartPath.getFileSystem(getConf());//分区的location
   FileSystem loadPathFS = loadPath.getFileSystem(getConf());//来源数据
   if (oldPartPathFS.equals(loadPathFS)) {
    newPartPath = oldPartPath;
   }
  }
 }else {
  newPartPath = oldPartPath;
 }
newPartPath 这个变量就是决定数据move操作时的目的路径,所以只要确定newPartPath 的值,我们就知道数据是怎么移动的

(4)目标路径的取值
        我们嵌入一下org.apache.hadoop.fs.Path.java类的内容,找到方法getFileSystem(Configuration),研究一下这个方法是怎么实现的

 public FileSystem getFileSystem(Configuration conf)
        throws IOException
    {
        return FileSystem.get(toUri(), conf);
    }
继续跟踪代码FileSystem.get(toUri(), conf),跟到类org.apache.hadoop.fs.FileSystem.java,跟踪方法public static FileSystem get(URI uri, Configuration conf){...},看主要代码段:
  String disableCacheName = String.format("fs.%s.impl.disable.cache", new Object[] {  scheme  }); 
          if(conf.getBoolean(disableCacheName, false)) 
                return createFileSystem(uri, conf); //如果设置了fs.hdfs.impl.disable.cache=true,则每次FileSystem.get(...)时,都是获得一个FileSystem对象,再执行上面的oldPartPathFS.equals(loadPathFS)时,肯定为false
          else 
                return CACHE.get(uri, conf);//如果设置了fs.hdfs.impl.disable.cache=false,则从缓存CACHE中找相应的FileSystem对象,再执行上面oldPartPathFS.equals(loadPathFS)时,为true

根据这段的分析,再执行(3)中的代码时,如下
if (oldPartPathFS.equals(loadPathFS)) {
    newPartPath = oldPartPath;
  }
//如果设置了fs.hdfs.impl.disable.cache=false,则oldPartPathFS.equals(loadPathFS)返回true,newPartPath 取值为oldPartPath,值为上例中的 /{warehouse}/test/20140101/;否则newPartPath 的值保持不变,为/{warehouse}/test/day=20140101/

由于我们在操作中设置了fs.hdfs.impl.disable.cache=true,所以导致newPartPath 值为/{warehouse}/test/day=20140101/

(5) 移动数据,回到类org.apache.hadoop.hive.ql.metadata.Hive.java
/* 由于我们使用的操作是insert overwrite ,所以 replace为true,最终数据就是移动到newPartPath*/
 if (replace) { // 判断是否替换掉原来的数据
  Hive.replaceFiles(loadPath, newPartPath, oldPartPath, getConf());
 } else {
  FileSystem fs = tbl.getDataLocation().getFileSystem(conf);
  Hive.copyFiles(conf, loadPath, newPartPath, fs);
 }
跟踪到方法 void replaceFiles(Path srcf, Path destf, Path oldPath, HiveConf conf){...},看下对数据的操作
这个方法主要有两个操作
1.删除原来的数据 , oldPath ,即我们例子中的/{warehouse}/test/day=20140101/
if (fs2.exists(oldPath)) {
            // use FsShell to move data to .Trash first rather than delete permanently 
            FsShell fshell = new FsShell(); 
            fshell.setConf(conf); 
            fshell.run(new String[]{"-rmr", oldPath.toString()}); 
          }
2.rename源数据到目标路径,完成数据移动,srcf->destf,上例中此时的destf为/{warehouse}/test/day=20140101/
boolean b = renameFile(conf, srcs[0].getPath(), destf, fs, true);

    根据上面的分析,我们可以看出,由于设置了fs.hdfs.impl.disable.cache=true,,无法在缓存中取到FileSystem对象,所以导致newPartPath的值无法取到oldPartPath的值,最终/{warehouse}/test/day=20140101/,所以最终会在hdfs上面新建一个目录,然后删除了oldPartPath原来的数据,导致/{warehouse}/test/20140101/目录及下面的文件都被删除掉,所以出现了上面的情况!

<think>我们正在处理一个关于Spark的错误:FileFormatWriter导致任务中止(jobaborted)。根据用户提供的信息,错误可能与Parquet写入格式有关。同时,用户引用了两个相关的资料:引用[1]提到了一个配置属性`spark.sql.parquet.writeLegacyFormat`,用于控制Spark写入Parquet数据时是否使用与Hive相同的约定;引用[2]描述了一个类似的问题,即读取Parquet文件时出现`ParquetDecodingException`,导致任务中止。分析:用户的问题是在Spark任务中遇到了由FileFormatWriter导致的jobaborted错误。根据引用[1]和[2],这个问题可能与Parquet文件的读写格式兼容性有关。特别是,当Spark写入Parquet文件时,如果使用了与Hive不兼容的格式,那么后续读取(尤其是使用Hive或其他工具)可能会出现问题。引用[1]指出,通过设置`spark.sql.parquet.writeLegacyFormat`为`true`,可以解决写入格式兼容性问题。另外,引用[2]提到了一个具体的错误信息:`parquet.io.ParquetDecodingException:Cannotreadvalueat0inblock`,这通常表示读取Parquet文件时遇到了问题,可能是由于写入格式与读取预期不一致。因此,解决方案可能涉及调整Spark的配置,以确保写入Parquet文件时使用与Hive兼容的格式(即legacyformat)。步骤:1.在Spark任务中,设置配置属性:`spark.sql.parquet.writeLegacyFormat`为`true`。2.重新运行任务,观察问题是否解决。设置方式:-在Spark配置中,可以通过以下方式之一设置:-在Spark-submit命令中:`--confspark.sql.parquet.writeLegacyFormat=true`-在代码中:`spark.conf.set("spark.sql.parquet.writeLegacyFormat","true")`注意:这个设置会影响所有后续的Parquet写入操作,确保写入的格式与Hive兼容。如果用户的环境需要与Hive兼容,则应该使用此设置。另外,用户提到的是“FileFormatWriter”错误,但并没有给出具体的错误信息。因此,我们还需要考虑其他可能的原因。例如,写入过程中可能因为各种原因(如磁盘空间不足、权限问题等)失败。但根据用户引用的内容,我们优先考虑Parquet格式兼容性问题。如果上述方法不能解决问题,建议用户检查完整的错误日志,以确定是否有其他原因导致任务失败。根据以上分析,我们给出如下回答:</think>### Spark ERROR FileFormatWriter 任务中止原因及解决方案 #### 错误原因分析 1. **Parquet格式兼容性问题** Spark与Hive的Parquet写入格式存在差异,当`spark.sql.parquet.writeLegacyFormat=false`(默认值)时,Spark使用新格式写入Parquet文件。若下游系统(如Hive)使用旧格式读取,会导致`ParquetDecodingException`或任务中止[^1][^2]。 **错误特征**:日志中出现 `Can not read value at [position] in block` 或 `FileFormatWriter job aborted`。 2. **并发执行限制** 写入操作可能触发资源限制(如HDFS并发写入限制、YARN资源超限),尤其在大规模数据写入时[^4]。 3. **数据损坏或元数据不一致** 任务失败后残留的临时文件或元数据未清理,导致后续任务读取到不完整/损坏的数据文件。 4. **网络/存储异常** HDFS节点故障、磁盘空间不足或网络中断会导致写入失败。 #### 解决方案 ##### 1. 修复Parquet格式兼容性 ```scala // 在SparkSession初始化时设置 spark.sqlContext.setConf("spark.sql.parquet.writeLegacyFormat", "true") // 或提交任务时指定 spark-submit --conf spark.sql.parquet.writeLegacyFormat=true ... ``` 此配置使Spark使用与Hive兼容的Parquet写入格式[^1][^2]。 ##### 2. 资源与并发优化 - **增加集群资源**:调整 `spark.executor.memory`, `spark.executor.cores` - **限制写入并行度**: ```scala df.repartition(N).write.parquet(...) // 控制写入分区数 ``` - **检查基础设施**:确保HDFS/YARN无资源告警。 ##### 3. 清理残留文件 ```bash # 手动删除任务残留目录 hdfs dfs -rm -r /path/_temporary/* hdfs dfs -rm -r /path/.spark-staging/* ``` ##### 4. 启用写入重试机制 ```scala spark.conf.set("spark.hadoop.fs.hdfs.impl.disable.cache", "true") spark.conf.set("spark.hadoop.parquet.enable.summary-metadata", "false") ``` #### 验证步骤 1. 检查完整错误日志: `grep "FileFormatWriter" spark.log | grep "aborted" -A 50` 2. 使用`parquet-tools`检查文件完整性: ```bash parquet-tools meta /path/to/file.parquet parquet-tools schema /path/to/file.parquet ``` #### 预防措施 - **统一生态版本**:确保Spark/Hive/Parquet版本兼容 - **写入后验证**: ```scala val df = spark.read.parquet(outputPath) assert(df.count() > 0) // 简单数据校验 ``` - **启用检查点**:对流处理任务使用 `df.checkpoint()` > **典型场景**:当Spark写入的Parquet被Hive读取时,必须设置`writeLegacyFormat=true`,否则会因列索引格式不一致引发 `FileFormatWriter` 中止[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值