Spark中读parquet文件是怎么实现的

89 篇文章 10 订阅
68 篇文章 0 订阅

背景

最近在整理了一下 spark对Parquet的写文件的过程,也是为了更好的理解和调优Spark相关的任务,
因为对于Spark来说,任何一个事情都不是独立的存在的,比如说parquet文件的rowgroup设置的大小对读写的影响,以及parquet写之前排序对读parquet的影响,以及向量化读取等等
本文基于Spark 3.5

分析

我们以FileSourceScanExec的doExecute方法 为切口进行分析:

  protected override def doExecute(): RDD[InternalRow] = {
    val numOutputRows = longMetric("numOutputRows")
    if (needsUnsafeRowConversion) {
      inputRDD.mapPartitionsWithIndexInternal { (index, iter) =>
        val toUnsafe = UnsafeProjection.create(schema)
        toUnsafe.initialize(index)
        iter.map { row =>
          numOutputRows += 1
          toUnsafe(row)
        }
      }
    } else {
      inputRDD.mapPartitionsInternal { iter =>
        iter.map { row =>
          numOutputRows += 1
          row
        }
      }
    }
  }

这里的needsUnsafeRowConversion判断如果是ParquetSource,且配置了spark.sql.parquet.enableVectorizedReader为‘true’(默认就是true),则会进行unsafeRow的转换,当然这里的好处就是节约内存以及能够减少GC
对于inputRDD来说,就是创建了读取parquet的RDD:
具体的见:ParquetFileFormat.buildReaderWithPartitionValues方法,涉及到的代码多,所以只解读关键的几个部分:

  • fileRooter的读取
        val fileFooter = if (enableVectorizedReader) {
          // When there are vectorized reads, we can avoid reading the footer twice by reading
          // all row groups in advance and filter row groups according to filters that require
          // push down (no need to read the footer metadata again).
          ParquetFooterReader.readFooter(sharedConf, file, ParquetFooterReader.WITH_ROW_GROUPS)
        } else {
          ParquetFooterReader.readFooter(sharedConf, file, ParquetFooterReader.SKIP_ROW_GROUPS)
        }
    

这里enableVectorizedReader如果是true的话, fileFooter 只会得到所属Task的FileMetaData信息,其中只包括了所属Task的需要读取的parquet RowGroups,具体的数据流如下:

ParquetFooterReader.readFooter
   ||
   \/
readFooter
   ||
   \/
fileReader.getFooter
   ||
   \/
readFooter(file, options, f, converter)
   ||
   \/
converter.readParquetMetadata
   ||
   \/
filter.accept(new MetadataFilterVisitor)

filter.accept(new MetadataFilterVisitor就会根据对应的filter类型进行不同的操作:

FileMetaDataAndRowGroupOffsetInfo fileMetaDataAndRowGroupInfo = filter.accept(new MetadataFilterVisitor<FileMetaDataAndRowGroupOffsetInfo, IOException>() {
      @Override
      public FileMetaDataAndRowGroupOffsetInfo visit(NoFilter filter) throws IOException {
        FileMetaData fileMetadata = readFileMetaData(from, footerDecryptor, encryptedFooterAAD);
        return new FileMetaDataAndRowGroupOffsetInfo(fileMetadata, generateRowGroupOffsets(fileMetadata));
      }

      @Override
      public FileMetaDataAndRowGroupOffsetInfo visit(SkipMetadataFilter filter) throws IOException {
        FileMetaData fileMetadata = readFileMetaData(from, true, footerDecryptor, encryptedFooterAAD);
        return new FileMetaDataAndRowGroupOffsetInfo(fileMetadata, generateRowGroupOffsets(fileMetadata));
      }

      @Override
      public FileMetaDataAndRowGroupOffsetInfo visit(OffsetMetadataFilter filter) throws IOException {
        FileMetaData fileMetadata = readFileMetaData(from, footerDecryptor, encryptedFooterAAD);
        // We must generate the map *before* filtering because it modifies `fileMetadata`.
        Map<RowGroup, Long> rowGroupToRowIndexOffsetMap = generateRowGroupOffsets(fileMetadata);
        FileMetaData filteredFileMetadata = filterFileMetaDataByStart(fileMetadata, filter);
        return new FileMetaDataAndRowGroupOffsetInfo(filteredFileMetadata, rowGroupToRowIndexOffsetMap);
      }

      @Override
      public FileMetaDataAndRowGroupOffsetInfo visit(RangeMetadataFilter filter) throws IOException {
        FileMetaData fileMetadata = readFileMetaData(from, footerDecryptor, encryptedFooterAAD);
        // We must generate the map *before* filtering because it modifies `fileMetadata`.
        Map<RowGroup, Long> rowGroupToRowIndexOffsetMap = generateRowGroupOffsets(fileMetadata);
        FileMetaData filteredFileMetadata = filterFileMetaDataByMidpoint(fileMetadata, filter);
        return new FileMetaDataAndRowGroupOffsetInfo(filteredFileMetadata, rowGroupToRowIndexOffsetMap);
      }
    });
  • 如果是 ParquetFooterReader.SKIP_ROW_GROUPS ,则是走的SkipMetadataFilter这条filter,则只会拿出rowgroup的信息和rowgrups的的行数

  • 如果是 enableVectorizedReader,也就是会走 RangeMetadataFilter这个Filter,则会调用filterFileMetaDataByMidpoint,该方法会根据Task分配的数据是否覆盖了Rowgroups的中点来纳入到该task的读取的数据中来,具体的可以见:Spark-读取Parquet-为什么task数量会多于Row Group的数量

  • vectorizedReader的创建

            vectorizedReader.initialize(split, hadoopAttemptContext, Option.apply(fileFooter))
            logDebug(s"Appending $partitionSchema ${file.partitionValues}")
            vectorizedReader.initBatch(partitionSchema, file.partitionValues)
            if (returningBatch) {
              vectorizedReader.enableReturningBatches()
            }
    
            // UnsafeRowParquetRecordReader appends the columns internally to avoid another copy.
            iter.asInstanceOf[Iterator[InternalRow]]
    
    • vectorizedReader.initialize
      重要的点是这个主要是涉及到 parquet messageType到 ParquetColumn的转换,主要是ParquetToSparkSchemaConverter converter = new ParquetToSparkSchemaConverter(configuration)这个的配置

    • vectorizedReader.initBatch
      这里面主要涉及到了根据memModeOFF_HEAP还是ON_HEAP模式来构造不同的ColumnVector,其中
      如果是ON_HEAP,则会创建OnHeapColumnVector,用jvm数据的形式存储
      如果是OFF_HEAP,则会创建OffHeapColumnVector,这里涉及到的对象都是都是用unsafe api来操作,这里涉及到一个有意思的点:

       Platform.putByte(null, data + rowId, value);
       Platform.putInt(null, data + 4L * rowId, value)
      

      也就是说 无论是put什么 里面的第一个参数是为null,这个其实在Unsafed方法 putInt(Object o, long offset, int x)类中有提到

       Fetches a value from a given Java variable. More specifically, fetches a field or array element within the given object o at the given offset, or (if o is null) from the memory address whose numerical value is the given offset.
      

      也就是说如果传入的第一个参数为null,则会以offset作为地址,而在OffHeapColumnVector中对应的put当法中涉及到的offset就是data这个变量会在
      OffHeapColumnVector构造函数中的reserveInternal方法中赋值,这其中涉及到unsafe.allocateMemory方法会返回分配的内存地址

    • 具体迭代获取InternalRow
      这里的迭代获取主要是通过 vectorizedReader.getCurrentValue方法实现的,也就是会返回columnarBatch,但是这里的columnarBatch赋值是通过
      vectorizedReader.nextKeyValue方法实现的,该方法会被RecordReaderIterator.hasNext调用,vectorizedReader.nextKeyValue的数据流如下:

       VectorizedParquetRecordReader.nextBatch
           ||
           \/
          checkEndOfRowGroup    =>                               初始化  PageReadStore pages = reader.readNextRowGroup(); 
           ||                                                                         ||
           \/                                                                         \/
          columnReader.readBatch(num, leafCv.getValueVector()   initColumnReader(pages, cv); // columnVectors 设置ParquetColumnVector 里面包括了rowgroup里的所有page
           ||
           \/
          readPage
           ||
           \/
          pageReader.readPage()
           ||
           \/
          decompressor.decompress //  之类会进行解压
      
           
      

      decompressor.decompressdecompressorChunk.readAllPagesdescriptor.metadata.getCodec()传进来的,也就是从元数据里面读取的
      具体的向量化的读取,细节比较多,包括批量读取definition levelsrepetition levels等,这些读者自行分析

      注意:为什么 FileSourceScanExec中inputRDDs返回的类型是RDD[InternalRow] ,而vectorizedReader.getCurrentValue返回的类型是ColumnarBatch 也能运行,那是因为 我们在运行的时候,会有ColumnarToRow,他最终调用的是FileSourceScanExec.doExecuteColumnar,如下图:
      在这里插入图片描述

jvm会对Iterator[InternalRow]进行类型擦除,也就是说所有Iterator[InternalRow]在编译的时候会编译成Iterator[Object],会在运行时获取真正的类型

`FileScanRDD` 中的`compute方法` 最后有个
```
    iterator.asInstanceOf[Iterator[InternalRow]] // This is an erasure hack.

```
  • 16
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要根据Parquet文件建表,可以按照以下步骤进行操作: 1. 打开Spark Shell或者使用Spark应用程序。 2. 使用Spark SQL的create table语句创建表,例如: ``` spark.sql("CREATE TABLE my_table USING parquet OPTIONS (path 'path/to/parquet/file')") ``` 其中,my_table是表名,path/to/parquet/file是Parquet文件的路径。 3. 如果需要指定表的schema,可以使用如下语句: ``` spark.sql("CREATE TABLE my_table (column1 INT, column2 STRING) USING parquet OPTIONS (path 'path/to/parquet/file')") ``` 其中,column1和column2是表的列名,INT和STRING是列的数据类型。 4. 如果Parquet文件中包含了表的schema信息,可以使用如下语句: ``` spark.sql("CREATE TABLE my_table USING parquet OPTIONS (path 'path/to/parquet/file', inferSchema 'true')") ``` 其中,inferSchema 'true'表示从Parquet文件中推断出表的schema信息。 5. 完成以上步骤后,就可以使用Spark SQL的select语句查询表中的数据了,例如: ``` spark.sql("SELECT * FROM my_table") ``` ### 回答2: Spark可以通过使用Spark SQL的API来根据Parquet文件建表。 Parquet文件是一种用于存储和处理大规模数据的列式存储格式。它支持高效的压缩和查询,并且可以与Spark非常好地集成。 首先,我们需要在Spark中导入所需的库和类。可以使用以下代码完成此操作: ```scala import org.apache.spark.sql.SparkSession import org.apache.spark.sql.DataFrame ``` 接下来,我们需要创建一个SparkSession对象,该对象将用于与Spark进行交互。可以使用以下代码完成此操作: ```scala val spark = SparkSession.builder() .appName("ParquetTableCreation") .getOrCreate() ``` 然后,我们可以利用SparkSession对象读取Parquet文件,并将其转换为DataFrame。可以使用以下代码完成此操作: ```scala val parquetFileDF = spark.read.parquet("path/to/parquet/file") ``` 在这里,"path/to/parquet/file"应替换为实际的Parquet文件路径。 最后,我们可以使用DataFrame的createOrReplaceTempView方法将DataFrame注册为一个临时表。可以使用以下代码完成此操作: ```scala parquetFileDF.createOrReplaceTempView("tableName") ``` 在这里,"tableName"应替换为你想要为表指定的名称。 这样,我们就成功地根据Parquet文件Spark中建立了一个临时表。可以使用Spark SQL的语法来查询和处理这个表中的数据。 ### 回答3: 使用Spark可以根据Parquet文件建立表格。首先,我们需要创建一个SparkSession,它是与Spark的交互入口。 ```python from pyspark.sql import SparkSession # 创建SparkSession spark = SparkSession.builder \ .appName("ParquetTableCreation") \ .getOrCreate() ``` 接下来,我们可以使用Spark的`read.parquet()`方法加载Parquet文件,并将其转换为一个DataFrame对象。 ```python # 加载Parquet文件为DataFrame df = spark.read.parquet("path/to/parquet/file.parquet") ``` 此时,我们可以使用DataFrame的`createOrReplaceTempView()`方法将其注册为一个临时视图,以便我们可以使用SQL查询对其进行操作。 ```python # 将DataFrame注册为临时视图 df.createOrReplaceTempView("parquet_table") ``` 现在,我们就可以使用Spark的SQL语法查询这个临时表。 ```python # 查询数据 query = "SELECT * FROM parquet_table" result = spark.sql(query) # 展示结果 result.show() ``` 最后,我们可以使用DataFrame的`write.saveAsTable()`方法将DataFrame保存为永久表。 ```python # 保存DataFrame为永久表 df.write.saveAsTable("permanent_table") ``` 上述步骤将会根据Parquet文件创建一个或多个具有相应表结构的表格,并允许我们对其中数据进行各种操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值