PySpark +谷歌云存储(wholeTextFiles)

时间:2016-03-24 16:13:58

标签: google-cloud-storage google-compute-engine pyspark google-cloud-dataproc

我正在尝试使用PySpark(Google Dataproc)解析大约100万个HTML文件,并将相关字段写入精简文件。每个HTML文件大约200KB。因此,所有数据大约为200GB。

如果我使用数据的子集,下面的代码工作正常,但运行几个小时,然后在整个数据集上运行时崩溃。此外,未使用工作节点(<5%CPU),因此我知道存在一些问题。

我相信系统在从GCS中摄取数据时会感到窒息。有一个更好的方法吗?此外,当我以这种方式使用wholeTextFiles时,master是否尝试下载所有文件然后将它们发送给执行程序,还是让执行程序下载它们?

def my_func(keyval):
   keyval = (file_name, file_str)
   return parser(file_str).__dict__

data = sc.wholeTextFiles("gs://data/*")
output = data.map(my_func)
output.saveAsTextFile("gs://results/a")

2 个答案:

答案 0 :(得分:3)

要回答您的问题,主人不会读取所有包含的数据,但会在开始工作之前获取所有输入文件的状态。默认情况下,Dataproc将属性“mapreduce.input.fileinputformat.list-status.num-threads”设置为20,以帮助缩短查找时间,但仍然在GCS中为每个文件执行RPC。

看起来你已经发现了一个案例,即使添加线程也没有多大帮助,只是让驱动程序更快地引导到OOM。

扩展如何并行化读取,我有两个想法。

但首先,有点警告:这些解决方案都不是因为它们对包含在glob中的目录非常健壮。您可能希望防止出现在要读取的文件列表中的目录。

第一个是用python和hadoop命令行工具完成的(这也可以用gsutil完成)。下面是一个示例,说明它如何查看并在worker上执行文件列表,将文件内容读成对,最后计算(文件名,文件长度)对:

from __future__ import print_function

from pyspark.rdd import RDD
from pyspark import SparkContext

import sys
import subprocess


def hadoop_ls(file_glob):
  lines = subprocess.check_output(["/usr/bin/hadoop", "fs", "-ls", file_glob]).split("\n")
  files = [line.split()[7] for line in lines if len(line) > 0]
  return files

def hadoop_cat(file):
  return subprocess.check_output(["/usr/bin/hadoop", "fs", "-cat", file]).decode("utf-8")

if __name__ == "__main__":
  if len(sys.argv) < 2:
    print("Provide a list of path globs to read.")
    exit(-1)

  sc = SparkContext()
  # This is just for testing. You'll want to generate a list 
  # of prefix globs instead of having a list passed in from the 
  # command line.
  globs = sys.argv[1:]
  # Desired listing partition count
  lpc = 100
  # Desired 'cat' partition count, should be less than total number of files
  cpc = 1000
  files = sc.parallelize(globs).repartition(lpc).flatMap(hadoop_ls)
  files_and_content = files.repartition(cpc).map(lambda f: [f, hadoop_cat(f)])
  files_and_char_count = files_and_content.map(lambda p: [p[0], len(p[1])])
  local = files_and_char_count.collect()
  for pair in local:
    print("File {} had {} chars".format(pair[0], pair[1]))

我首先从这个子流程解决方案开始,然后使用hadoop_ls和hadoop_cat调用进行分区,看看是否可以获得可接受的内容。

第二种解决方案更复杂,但可能会通过避免许多exec调用来产生更高性能的管道。

在第二个解决方案中,我们将编译一个特殊用途的帮助程序jar,使用初始化操作将该jar复制到所有worker,最后使用我们驱动程序中的帮助程序。

scala jar项目的最终目录结构如下所示:

helper/src/main/scala/com/google/cloud/dataproc/support/PysparkHelper.scala
helper/build.sbt

在我们的PysparkHelper.scala文件中,我们将有一个小的scala类,其功能与上面的纯python解决方案相同。首先,我们将创建文件globs的RDD,然后创建文件名的RDD,最后创建文件名和文件内容对的RDD。

package com.google.cloud.dataproc.support

import collection.JavaConversions._

import org.apache.commons.io.IOUtils
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.{FileSystem, Path}
import org.apache.spark.api.java.{JavaPairRDD, JavaSparkContext}

import java.util.ArrayList
import java.nio.charset.StandardCharsets

class PysparkHelper extends Serializable {
  def wholeTextFiles(
    context: JavaSparkContext,
    paths: ArrayList[String],
    partitions: Int): JavaPairRDD[String, String] = {

    val globRDD = context.sc.parallelize(paths).repartition(partitions)
    // map globs to file names:
    val filenameRDD = globRDD.flatMap(glob => {
      val path = new Path(glob)
      val fs: FileSystem = path.getFileSystem(new Configuration)
      val statuses = fs.globStatus(path)
      statuses.map(s => s.getPath.toString)
    })
    // Map file name to (name, content) pairs:
    // TODO: Consider adding a second parititon count parameter to repartition before
    // the below map.
    val fileNameContentRDD = filenameRDD.map(f => {
      Pair(f, readPath(f, new Configuration))
    })

    new JavaPairRDD(fileNameContentRDD)
  }

  def readPath(file: String, conf: Configuration) = {
    val path = new Path(file)
    val fs: FileSystem = path.getFileSystem(conf)
    val stream = fs.open(path)
    try {
      IOUtils.toString(stream, StandardCharsets.UTF_8)
    } finally {
      stream.close()
    }
  }
}

helper / build.sbt文件看起来像这样:

organization := "com.google.cloud.dataproc.support"
name := "pyspark_support"
version := "0.1"
scalaVersion := "2.10.5"
libraryDependencies +=  "org.apache.spark" % "spark-core_2.10" % "1.6.0" % "provided"
libraryDependencies +=  "org.apache.hadoop" % "hadoop-common" % "2.7.1" % "provided"
exportJars := true

然后我们可以用sbt:

构建帮助器
$ cd helper && sbt package

输出辅助jar应该是target / scala-2.10 / pyspark_support_2.10-0.1.jar

我们现在需要将这个jar放到我们的集群上,为此,我们需要做两件事:1)将jar上传到GCS,2)在GCS中创建一个初始化动作,将jar复制到集群节点。

为了便于说明,我们假设您的水桶名为MY_BUCKET(在此插入适当的海象相关模因)。

$ gsutil cp target/scala-2.10/pyspark_support_2.10-0.1.jar gs://MY_BUCKET/pyspark_support.jar

创建初始化操作(让我们称之为pyspark_init_action.sh,根据需要替换MY_BUCKET):

#!/bin/bash

gsutil cp gs://MY_BUCKET/pyspark_support.jar /usr/lib/hadoop/lib/

最后将初始化操作上传到GCS:

$ gsutil cp pyspark_init_action.sh gs://MY_BUCKET/pyspark_init_action.sh

现在可以通过将以下标志传递给gcloud来启动集群:

--initialization-actions gs://MY_BUCKET/pyspark_init_action.sh

在构建,上传和安装我们的新库后,我们最终可以在pyspark中使用它:

from __future__ import print_function

from pyspark.rdd import RDD
from pyspark import SparkContext
from pyspark.serializers import PairDeserializer, UTF8Deserializer

import sys

class DataprocUtils(object):

  @staticmethod
  def wholeTextFiles(sc, glob_list, partitions):
    """
    Read whole text file content from GCS.
    :param sc: Spark context
    :param glob_list: List of globs, each glob should be a prefix for part of the dataset.
    :param partitions: number of partitions to use when creating the RDD
    :return: RDD of filename, filecontent pairs.
    """
    helper = sc._jvm.com.google.cloud.dataproc.support.PysparkHelper()
    return RDD(helper.wholeTextFiles(sc._jsc, glob_list, partitions), sc,
               PairDeserializer(UTF8Deserializer(), UTF8Deserializer()))

if __name__ == "__main__":
  if len(sys.argv) < 2:
    print("Provide a list of path globs to read.")
    exit(-1)

  sc = SparkContext()
  globs = sys.argv[1:]
  partitions = 10
  files_and_content = DataprocUtils.wholeTextFiles(sc, globs, partitions)
  files_and_char_count = files_and_content.map(lambda p: (p[0], len(p[1])))
  local = files_and_char_count.collect()
  for pair in local:
    print("File {} had {} chars".format(pair[0], pair[1]))

答案 1 :(得分:0)

谢谢!我尝试了第一种方法。它工作,但由于exec调用和RPC / auth开销不是很高效。在32节点群集上运行大约需要10个小时。我能够在使用Amazon s3连接器的aws上使用databricks在4节点集群上运行30分钟。似乎那里的开销要少得多。我希望谷歌能够提供更好的方法将数据从GCS提取到Spark。