寄木細工のデータをdate
&hour
でパーティション化しました。フォルダー構造:
events_v3
-- event_date=2015-01-01
-- event_hour=2015-01-1
-- part10000.parquet.gz
-- event_date=2015-01-02
-- event_hour=5
-- part10000.parquet.gz
テーブルを作成しましたraw_events
via sparkですが、クエリを実行しようとすると、すべてのディレクトリでフッターがスキャンされ、1日分のデータしかクエリしていなくても、最初のクエリの速度が低下します。
クエリ:select * from raw_events where event_date='2016-01-01'
同様の問題: http://mail-archives.Apache.org/mod_mbox/spark-user/201508.mbox/%3CCAAswR-7Qbd2tdLSsO76zyw9tvs-Njw2YVd36bRfCG3DKZrH0tw@mail.gmail.com%3E (しかし、古い)
ログ:
App > 16/09/15 03:14:03 main INFO HadoopFsRelation: Listing leaf files and directories in parallel under: s3a://bucket/events_v3/
350日分のデータがあるため、350タスクを生成します。
schemaMerge
を無効にし、読み取るスキーマも指定したので、それは私が見ているパーティションに行くことができますが、なぜすべてのリーフファイルを出力する必要があるのですか? 2つのエグゼキューターでリーフファイルをリストすると10分かかり、クエリの実際の実行には20秒かかります
コードサンプル:
val sparkSession = org.Apache.spark.sql.SparkSession.builder.getOrCreate()
val df = sparkSession.read.option("mergeSchema","false").format("parquet").load("s3a://bucket/events_v3")
df.createOrReplaceTempView("temp_events")
sparkSession.sql(
"""
|select verb,count(*) from temp_events where event_date = "2016-01-01" group by verb
""".stripMargin).show()
sparkが読み込まれるディレクトリが与えられるとすぐに、listLeafFiles
(org/Apache/spark/sql/execution/datasources/fileSourceInterfaces.scala)への呼び出しが発行されます)。コールfs.listStatus
これは、ファイルとディレクトリのリストを取得するためにAPI呼び出しを行います。各ディレクトリに対して、このメソッドが再度呼び出されます。これは、ディレクトリがなくなるまで再帰的に行われます。これは、設計上、HDFSシステムで適切に機能します。ただし、リストファイルはRPC呼び出しであるため、s3ではうまく機能しません。他のS3では、プレフィックスによってすべてのファイルを取得するサポートがありました。
たとえば、1時間分のデータと1つのサブディレクトリを持つ1年分のデータと上記のディレクトリ構造があり、10個のサブディレクトリがある場合、365 * 24 * 10 = 87kのAPI呼び出しが存在する場合、これは138個のAPI呼び出しに削減できます。 137000ファイルのみ。各s3 api呼び出しは1000ファイルを返します。
コード:org/Apache/hadoop/fs/s3a/S3AFileSystem.Java
public FileStatus[] listStatusRecursively(Path f) throws FileNotFoundException,
IOException {
String key = pathToKey(f);
if (LOG.isDebugEnabled()) {
LOG.debug("List status for path: " + f);
}
final List<FileStatus> result = new ArrayList<FileStatus>();
final FileStatus fileStatus = getFileStatus(f);
if (fileStatus.isDirectory()) {
if (!key.isEmpty()) {
key = key + "/";
}
ListObjectsRequest request = new ListObjectsRequest();
request.setBucketName(bucket);
request.setPrefix(key);
request.setMaxKeys(maxKeys);
if (LOG.isDebugEnabled()) {
LOG.debug("listStatus: doing listObjects for directory " + key);
}
ObjectListing objects = s3.listObjects(request);
statistics.incrementReadOps(1);
while (true) {
for (S3ObjectSummary summary : objects.getObjectSummaries()) {
Path keyPath = keyToPath(summary.getKey()).makeQualified(uri, workingDir);
// Skip over keys that are ourselves and old S3N _$folder$ files
if (keyPath.equals(f) || summary.getKey().endsWith(S3N_FOLDER_SUFFIX)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring: " + keyPath);
}
continue;
}
if (objectRepresentsDirectory(summary.getKey(), summary.getSize())) {
result.add(new S3AFileStatus(true, true, keyPath));
if (LOG.isDebugEnabled()) {
LOG.debug("Adding: fd: " + keyPath);
}
} else {
result.add(new S3AFileStatus(summary.getSize(),
dateToLong(summary.getLastModified()), keyPath,
getDefaultBlockSize(f.makeQualified(uri, workingDir))));
if (LOG.isDebugEnabled()) {
LOG.debug("Adding: fi: " + keyPath);
}
}
}
for (String prefix : objects.getCommonPrefixes()) {
Path keyPath = keyToPath(prefix).makeQualified(uri, workingDir);
if (keyPath.equals(f)) {
continue;
}
result.add(new S3AFileStatus(true, false, keyPath));
if (LOG.isDebugEnabled()) {
LOG.debug("Adding: rd: " + keyPath);
}
}
if (objects.isTruncated()) {
if (LOG.isDebugEnabled()) {
LOG.debug("listStatus: list truncated - getting next batch");
}
objects = s3.listNextBatchOfObjects(objects);
statistics.incrementReadOps(1);
} else {
break;
}
}
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Adding: rd (not a dir): " + f);
}
result.add(fileStatus);
}
return result.toArray(new FileStatus[result.size()]);
}
/org/Apache/spark/sql/execution/datasources/fileSourceInterfaces.scala
def listLeafFiles(fs: FileSystem, status: FileStatus, filter: PathFilter): Array[FileStatus] = {
logTrace(s"Listing ${status.getPath}")
val name = status.getPath.getName.toLowerCase
if (shouldFilterOut(name)) {
Array.empty[FileStatus]
}
else {
val statuses = {
val stats = if(fs.isInstanceOf[S3AFileSystem]){
logWarning("Using Monkey patched version of list status")
println("Using Monkey patched version of list status")
val a = fs.asInstanceOf[S3AFileSystem].listStatusRecursively(status.getPath)
a
// Array.empty[FileStatus]
}
else{
val (dirs, files) = fs.listStatus(status.getPath).partition(_.isDirectory)
files ++ dirs.flatMap(dir => listLeafFiles(fs, dir, filter))
}
if (filter != null) stats.filter(f => filter.accept(f.getPath)) else stats
}
// statuses do not have any dirs.
statuses.filterNot(status => shouldFilterOut(status.getPath.getName)).map {
case f: LocatedFileStatus => f
// NOTE:
//
// - Although S3/S3A/S3N file system can be quite slow for remote file metadata
// operations, calling `getFileBlockLocations` does no harm here since these file system
// implementations don't actually issue RPC for this method.
//
// - Here we are calling `getFileBlockLocations` in a sequential manner, but it should not
// be a big deal since we always use to `listLeafFilesInParallel` when the number of
// paths exceeds threshold.
case f => createLocatedFileStatus(f, fs.getFileBlockLocations(f, 0, f.getLen))
}
}
}
Gauravの答えを明確にするために、そのコードはHadoopブランチ2からのものであり、おそらくHadoop 2.9( HADOOP-13208 を参照)まで表面化しません。そして誰かがSparkを更新してその機能を使用する必要があります(これはHDFSを使用してコードに害を及ぼすことはなく、そこで高速化を示さないだけです)。
考慮すべき点の1つは、オブジェクトストアのファイルレイアウトを適切にする方法です。
最後に、Apache Hadoop、Apache Sparkおよび関連プロジェクトはすべてオープンソースです。寄稿は歓迎されます。これはコードだけでなく、ドキュメント、テスト、そしてこのパフォーマンスに関するものについては、実際のデータセット:問題の原因(およびデータセットレイアウト)の詳細を教えても、興味深いです。