【问题标题】:How to handle operations on local files over multiple ParDo transforms in Apache Beam / Google Cloud DataFlow如何在 Apache Beam / Google Cloud DataFlow 中通过多个 ParDo 转换处理对本地文件的操作
【发布时间】:2021-03-11 09:55:00
【问题描述】:

我正在为 Google Cloud Dataflow 开发一个 ETL 管道,其中我有几个分支 ParDo 转换,每个转换都需要一个本地音频文件。然后将分支结果合并并导出为文本。

这最初是在单台机器上运行的 Python 脚本,我正在尝试使用 GC 数据流来适应 VM 工作者并行化。

提取过程从单个 GCS 存储桶位置下载文件,然后在转换完成后将其删除,以控制存储。这是由于需要对文件进行本地访问的预处理模块。这可以通过自己重写一些预处理库来重新设计以处理字节流而不是文件 - 但是,这方面的一些尝试并不顺利,我想首先探索如何处理并行化的本地Apache Beam / GC Dataflow 中的文件操作,以便更好地理解框架。

在这个粗略的实现中,每个分支都会下载和删除文件,并进行大量双重处理。在我的实现中,我有 8 个分支,因此每个文件都被下载和删除 8 次。是否可以将 GCS 存储桶安装在每个工作人员身上,而不是从远程下载文件?

或者是否有另一种方法可以确保将正确的文件引用传递给工作人员,以便:

  • 单个DownloadFilesDoFn()可以批量下载
  • 然后将PCollection中的本地文件引用扇出到所有分支
  • 然后最后一个 CleanUpFilesDoFn() 可以删除它们
  • 如何并行化本地文件引用?

如果无法避免本地文件操作,Apache Beam / GC Dataflow 的最佳分支ParDo 策略是什么?


为简单起见,我现有实现的一些示例代码带有两个分支。

# singleton decorator
def singleton(cls):
  instances = {}
  def getinstance():
      if cls not in instances:
          instances[cls] = cls()
      return instances[cls]
  return getinstance

@singleton
class Predict():
  def __init__(self, model):
    '''
    Process audio, reads in filename 
    Returns Prediction
    '''
    self.model = model

  def process(self, filename):
      #simplified pseudocode
      audio = preprocess.load(filename=filename)
      prediction = inference(self.model, audio)
      return prediction

class PredictDoFn(beam.DoFn):
  def __init__(self, model):
    self.localfile, self.model = "", model
    
  def process(self, element):
    # Construct Predict() object singleton per worker
    predict = Predict(self.model)

    subprocess.run(['gsutil','cp',element['GCSPath'],'./'], cwd=cwd, shell=False)
    self.localfile = cwd + "/" + element['GCSPath'].split('/')[-1]

    res = predict.process(self.localfile)
    return [{
        'Index': element['Index'], 
        'Title': element['Title'],
        'File' : element['GCSPath'],
        self.model + 'Prediction': res
        }]    
  def finish_bundle(self):
    subprocess.run(['rm',self.localfile], cwd=cwd, shell=False)


# DoFn to split csv into elements (GSC bucket could be read as a PCollection instead maybe)
class Split(beam.DoFn):
    def process(self, element):
        Index,Title,GCSPath = element.split(",")
        GCSPath = 'gs://mybucket/'+ GCSPath
        return [{
            'Index': int(Index),
            'Title': Title,
            'GCSPath': GCSPath
        }]

管道的简化版本:

with beam.Pipeline(argv=pipeline_args) as p:
    files = 
        ( 
        p | 'Read From CSV' >> beam.io.ReadFromText(known_args.input)
          | 'Parse CSV into Dict' >> beam.ParDo(Split())
        )
    # prediction 1 branch
    preds1 = 
        (
          files | 'Prediction 1' >> beam.ParDo(PredictDoFn(model1))
        )
    # prediction 2 branch
    preds2 = 
        (
          files | 'Prediction 2' >> beam.ParDo(PredictDoFn(model2))
        )
    
    # join branches
    joined = { preds1, preds2 }

    # output to file
    output = 
        ( 
      joined | 'WriteToText' >> beam.io.Write(beam.io.textio.WriteToText(known_args.output))
        )

【问题讨论】:

  • 这对我来说看起来很专业。我认为这可能已经是最佳解决方案。多年来,我一直在使用类似但不太干净的东西,GCS 访问步骤并不是一个重大的延迟或问题的根源。
  • 当我从 Dataflow 访问 GCS 中的文件时,我使用 google.cloud.storage.Client(...).download_blob_to_file(),我发现它比 subprocess 调用 gsutil 干净得多
  • 感谢 Steven - 我将使用它而不是 gsutil - 我能问一下管道分支时如何处理文件操作吗?实际上,我在管道中有 7 个以上的分支,用于处理数 TB 的数据。目前每个分支都在下载和删除同一个文件

标签: python google-cloud-platform google-cloud-dataflow apache-beam


【解决方案1】:

为了避免重复下载文件,可以将文件内容放入pCollection中。

class DownloadFilesDoFn(beam.DoFn):
  def __init__(self):
     import re
     self.gcs_path_regex = re.compile(r'gs:\/\/([^\/]+)\/(.*)')

  def start_bundle(self):
     import google.cloud.storage
     self.gcs = google.cloud.storage.Client()

  def process(self, element):
     file_match = self.gcs_path_regex.match(element['GCSPath'])
     bucket = self.gcs.get_bucket(file_match.group(1))
     blob = bucket.get_blob(file_match.group(2))
     element['file_contents'] = blob.download_as_bytes()
     yield element
     

那么 PredictDoFn 变为:

class PredictDoFn(beam.DoFn):
  def __init__(self, model):
    self.model = model

  def start_bundle(self):
    self.predict = Predict(self.model)
    
  def process(self, element):
    res = self.predict.process(element['file_contents'])
    return [{
        'Index': element['Index'], 
        'Title': element['Title'],
        'File' : element['GCSPath'],
        self.model + 'Prediction': res
        }]   

和管道:

with beam.Pipeline(argv=pipeline_args) as p:
    files = 
        ( 
        p | 'Read From CSV' >> beam.io.ReadFromText(known_args.input)
          | 'Parse CSV into Dict' >> beam.ParDo(Split())
          | 'Read files' >> beam.ParDo(DownloadFilesDoFn())
        )
    # prediction 1 branch
    preds1 = 
        (
          files | 'Prediction 1' >> beam.ParDo(PredictDoFn(model1))
        )
    # prediction 2 branch
    preds2 = 
        (
          files | 'Prediction 2' >> beam.ParDo(PredictDoFn(model2))
        )
    
    # join branches
    joined = { preds1, preds2 }

    # output to file
    output = 
        ( 
      joined | 'WriteToText' >> beam.io.Write(beam.io.textio.WriteToText(known_args.output))
        )

【讨论】:

  • 很棒的史蒂文,谢谢!这也有助于我了解 Dataflow 中 blob 的使用。让我试一试,然后我会将其标记为已接受
  • 嗨 Steven,您对在 Setup 中保留 GCS 客户端初始化而不是 start_bundle 有何看法?
  • 我认为调用中存在语法错误 - 我收到 TypeError: 're.Pattern' object is not callable
  • @Sach 我认为你是对的:根据stackoverflow.com/a/50068377/7359502 应该在设置中使用我的工作中不使用设置,为什么我不使用设置我不记得了。跨度>
  • 我一直在为整个管道使用全局客户端实例,但我猜在设置中为每个 DoFn 实例化客户端是最佳/更好的做法?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-25
  • 1970-01-01
相关资源
最近更新 更多