-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-36516][SQL] Support File Metadata Cache for ORC #33748
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
45f4827
a74c793
1a9bd3b
30df269
42d2bfd
0e6c52b
95bae3c
ebb7e0b
c36a569
b02de85
406f91c
3b15a82
2b99983
82ddf4f
1dd174e
a339b1b
7153d2a
c3838e6
59d5bb9
4adeb62
e5f9497
ec8fa1c
db90daf
7327fdb
2907b2c
a7eff43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -967,6 +967,28 @@ object SQLConf { | |
| .booleanConf | ||
| .createWithDefault(false) | ||
|
|
||
| val FILE_META_CACHE_ORC_ENABLED = buildConf("spark.sql.fileMetaCache.orc.enabled") | ||
| .doc("To indicate if enable orc file meta cache, it is recommended to enabled " + | ||
| "this config when multiple queries are performed on the same dataset, default is false.") | ||
| .version("3.3.0") | ||
| .booleanConf | ||
| .createWithDefault(false) | ||
|
|
||
| val FILE_META_CACHE_TTL_SINCE_LAST_ACCESS = | ||
| buildConf("spark.sql.fileMetaCache.ttlSinceLastAccess") | ||
| .version("3.3.0") | ||
| .doc("Time-to-live for file metadata cache entry after last access, the unit is seconds.") | ||
| .timeConf(TimeUnit.SECONDS) | ||
| .createWithDefault(3600L) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shall we reduce this from
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 82ddf4f reduce this from |
||
|
|
||
| val FILE_META_CACHE_MAXIMUM_SIZE = | ||
| buildConf("spark.sql.fileMetaCache.maximumSize") | ||
| .version("3.3.0") | ||
| .doc("Maximum number of file meta entries the file meta cache contains.") | ||
| .intConf | ||
| .checkValue(_ > 0, "The value of fileMetaCache maximumSize must be positive") | ||
| .createWithDefault(1000) | ||
|
|
||
| val HIVE_VERIFY_PARTITION_PATH = buildConf("spark.sql.hive.verifyPartitionPath") | ||
| .doc("When true, check all the partition paths under the table\'s root directory " + | ||
| "when reading data stored in HDFS. This configuration will be deprecated in the future " + | ||
|
|
@@ -3600,6 +3622,8 @@ class SQLConf extends Serializable with Logging { | |
|
|
||
| def parquetVectorizedReaderBatchSize: Int = getConf(PARQUET_VECTORIZED_READER_BATCH_SIZE) | ||
|
|
||
| def fileMetaCacheOrcEnabled: Boolean = getConf(FILE_META_CACHE_ORC_ENABLED) | ||
|
|
||
| def columnBatchSize: Int = getConf(COLUMN_BATCH_SIZE) | ||
|
|
||
| def cacheVectorizedReaderEnabled: Boolean = getConf(CACHE_VECTORIZED_READER_ENABLED) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.spark.sql.execution.datasources | ||
|
|
||
| import java.util.concurrent.TimeUnit | ||
|
|
||
| import com.github.benmanes.caffeine.cache.{CacheLoader, Caffeine} | ||
| import com.github.benmanes.caffeine.cache.stats.CacheStats | ||
| import org.apache.hadoop.conf.Configuration | ||
| import org.apache.hadoop.fs.Path | ||
|
|
||
| import org.apache.spark.SparkEnv | ||
| import org.apache.spark.internal.Logging | ||
| import org.apache.spark.sql.internal.SQLConf | ||
|
|
||
| /** | ||
| * A singleton Cache Manager to caching file meta. We cache these file metas in order to speed up | ||
| * iterated queries over the same dataset. Otherwise, each query would have to hit remote storage | ||
| * in order to fetch file meta before read files. | ||
|
Comment on lines
+31
to
+33
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For same dataset, cannot current caching (persisting) mechanism satisfy the requirement? Could you describe the difference between two and what the benefit of file meta?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @viirya . |
||
| * | ||
| * We should implement the corresponding `FileMetaKey` for a specific file format, for example | ||
| * `ParquetFileMetaKey` or `OrcFileMetaKey`. By default, the file path is used as the identification | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| * of the `FileMetaKey` and the `getFileMeta` method of `FileMetaKey` is used to return the file | ||
| * meta of the corresponding file format. | ||
| */ | ||
| object FileMetaCacheManager extends Logging { | ||
|
|
||
| private lazy val cacheLoader = new CacheLoader[FileMetaKey, FileMeta]() { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To use |
||
| override def load(fileMetaKey: FileMetaKey): FileMeta = { | ||
| logDebug(s"Loading Data File Meta ${fileMetaKey.path}") | ||
| fileMetaKey.getFileMeta | ||
| } | ||
| } | ||
|
|
||
| private lazy val ttlTime = | ||
| SparkEnv.get.conf.get(SQLConf.FILE_META_CACHE_TTL_SINCE_LAST_ACCESS) | ||
|
|
||
| private lazy val maximumSize = | ||
| SparkEnv.get.conf.get(SQLConf.FILE_META_CACHE_MAXIMUM_SIZE) | ||
|
|
||
| private lazy val cache = Caffeine | ||
| .newBuilder() | ||
| .expireAfterAccess(ttlTime, TimeUnit.SECONDS) | ||
|
dongjoon-hyun marked this conversation as resolved.
|
||
| .maximumSize(maximumSize) | ||
| .recordStats() | ||
| .build[FileMetaKey, FileMeta](cacheLoader) | ||
|
|
||
| /** | ||
| * Returns the `FileMeta` associated with the `FileMetaKey` in the `FileMetaCacheManager`, | ||
| * obtaining that the `FileMeta` from `cacheLoader.load(FileMetaKey)` if necessary. | ||
| */ | ||
| def get(fileMeteKey: FileMetaKey): FileMeta = cache.get(fileMeteKey) | ||
|
|
||
| /** | ||
| * Return current snapshot of FileMeta Cache's cumulative statistics | ||
| * include cache hitCount, missCount and so on. | ||
| * This method is only called when testing now. | ||
| */ | ||
| private def cacheStats: CacheStats = cache.stats() | ||
|
dongjoon-hyun marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Use to cleanUp entries in the FileMeta Cache. | ||
| * This method is only called when testing now. | ||
| */ | ||
| private def cleanUp(): Unit = cache.cleanUp() | ||
| } | ||
|
|
||
| abstract class FileMetaKey { | ||
| def path: Path | ||
| def configuration: Configuration | ||
| def getFileMeta: FileMeta | ||
| override def hashCode(): Int = path.hashCode | ||
| override def equals(other: Any): Boolean = other match { | ||
| case df: FileMetaKey => path.equals(df.path) | ||
| case _ => false | ||
|
Comment on lines
+88
to
+90
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, if there will be different types of So besides comparing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then seems we only need a common
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current design is still needed. We need to be able to construct a specific If a unified
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have any suggestions ? @dongjoon-hyun
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| } | ||
|
|
||
| trait FileMeta | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,7 @@ import org.apache.spark.sql.catalyst.InternalRow | |
| import org.apache.spark.sql.catalyst.expressions._ | ||
| import org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection | ||
| import org.apache.spark.sql.execution.datasources._ | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.sources._ | ||
| import org.apache.spark.sql.types._ | ||
| import org.apache.spark.util.{SerializableConfiguration, Utils} | ||
|
|
@@ -154,11 +155,18 @@ class OrcFileFormat | |
|
|
||
| (file: PartitionedFile) => { | ||
| val conf = broadcastedConf.value.value | ||
| val metaCacheEnabled = | ||
| conf.getBoolean(SQLConf.FILE_META_CACHE_ORC_ENABLED.key, false) | ||
|
|
||
| val filePath = new Path(new URI(file.filePath)) | ||
|
|
||
| val fs = filePath.getFileSystem(conf) | ||
| val readerOptions = OrcFile.readerOptions(conf).filesystem(fs) | ||
| val readerOptions = if (metaCacheEnabled) { | ||
| val tail = OrcFileMeta.readTailFromCache(filePath, conf) | ||
| OrcFile.readerOptions(conf).filesystem(fs).orcTail(tail) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happen if the file is replaced?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a very good question!!!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the file name has the timestamp, I think we don't have to worry too much. The names of the new file and the old file are different and they can ensure that they don't read the wrong data.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it is manually file replaced and the file has the same name and the corresponding file meta exists in the cache, an incorrect file meta will be used to read the data. If the data reading fails, the job will fail. But if the data reading happens to be successful, the job will read the wrong data.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, even if there is no |
||
| } else { | ||
| OrcFile.readerOptions(conf).filesystem(fs) | ||
| } | ||
| val resultedColPruneInfo = | ||
| Utils.tryWithResource(OrcFile.createReader(filePath, readerOptions)) { reader => | ||
| OrcUtils.requestedColumnIds( | ||
|
|
@@ -200,6 +208,10 @@ class OrcFileFormat | |
| val requestedDataColIds = requestedColIds ++ Array.fill(partitionSchema.length)(-1) | ||
| val requestedPartitionColIds = | ||
| Array.fill(requiredSchema.length)(-1) ++ Range(0, partitionSchema.length) | ||
| if (metaCacheEnabled) { | ||
| val tail = OrcFileMeta.readTailFromCache(filePath, conf) | ||
| batchReader.setCachedTail(tail) | ||
| } | ||
| batchReader.initialize(fileSplit, taskAttemptContext) | ||
| batchReader.initBatch( | ||
| TypeDescription.fromString(resultSchemaString), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.spark.sql.execution.datasources.orc | ||
|
|
||
| import org.apache.hadoop.conf.Configuration | ||
| import org.apache.hadoop.fs.Path | ||
| import org.apache.orc.OrcFile | ||
| import org.apache.orc.impl.{OrcTail, ReaderImpl} | ||
|
|
||
| import org.apache.spark.sql.execution.datasources.{FileMeta, FileMetaCacheManager, FileMetaKey} | ||
| import org.apache.spark.util.Utils | ||
|
|
||
| case class OrcFileMetaKey(path: Path, configuration: Configuration) | ||
| extends FileMetaKey { | ||
| override def getFileMeta: OrcFileMeta = OrcFileMeta(path, configuration) | ||
| } | ||
|
|
||
| case class OrcFileMeta(tail: OrcTail) extends FileMeta | ||
|
|
||
| object OrcFileMeta { | ||
| def apply(path: Path, conf: Configuration): OrcFileMeta = { | ||
| val fs = path.getFileSystem(conf) | ||
| val readerOptions = OrcFile.readerOptions(conf).filesystem(fs) | ||
| Utils.tryWithResource(new ReaderImpl(path, readerOptions)) { fileReader => | ||
| new OrcFileMeta(new OrcTail(fileReader.getFileTail, fileReader.getSerializedFileFooter)) | ||
|
dongjoon-hyun marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| def readTailFromCache(path: Path, conf: Configuration): OrcTail = | ||
| FileMetaCacheManager.get(OrcFileMetaKey(path, conf)).asInstanceOf[OrcFileMeta].tail | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we take the approach to have a list config for all data sources enabled with file meta cache, instead of one config per data source?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for @viirya 's suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ebb7e0b rename this config entry