如何确保我的Apache Spark设置代码仅运行一次?

时间:2019-07-03 22:05:19

标签: scala apache-spark

我正在Scala中编写一个Spark作业,该作业读取S3上的实木复合地板文件,进行一些简单的转换,然后将它们保存到DynamoDB实例中。每次运行时,我们都需要在Dynamo中创建一个新表,因此我编写了一个Lambda函数来负责表的创建。我的Spark作业要做的第一件事是生成一个表名,调用我的Lambda函数(将新表名传递给它),等待表被创建,然后正常进行ETL步骤。

但是,好像我的Lambda函数始终被调用两次。我无法解释。这是代码示例:

def main(spark: SparkSession, pathToParquet: String) {

  // generate a unique table name
  val tableName = generateTableName()

  // call the lambda function
  val result = callLambdaFunction(tableName)

  // wait for the table to be created
  waitForTableCreation(tableName)

  // normal ETL pipeline
  var parquetRDD = spark.read.parquet(pathToParquet)
  val transformedRDD = parquetRDD.map((row: Row) => transformData(row), encoder=kryo[(Text, DynamoDBItemWritable)])
  transformedRDD.saveAsHadoopDataset(getConfiguration(tableName))
  spark.sparkContext.stop()
}

等待表创建的代码非常简单,如您所见:

def waitForTableCreation(tableName: String) {
  val client: AmazonDynamoDB = AmazonDynamoDBClientBuilder.defaultClient()
  val waiter: Waiter[DescribeTableRequest] = client.waiters().tableExists()
  try {
    waiter.run(new WaiterParameters[DescribeTableRequest](new DescribeTableRequest(tableName)))
  } catch {
      case ex: WaiterTimedOutException =>
        LOGGER.error("Timed out waiting to create table: " + tableName)
        throw ex
      case t: Throwable => throw t
  }
}

lambda调用同样简单:

def callLambdaFunction(tableName: String) {
  val myLambda = LambdaInvokerFactory.builder()
    .lambdaClient(AWSLambdaClientBuilder.defaultClient)
    .lambdaFunctionNameResolver(new LambdaByName(LAMBDA_FUNCTION_NAME))
    .build(classOf[MyLambdaContract])
  myLambda.invoke(new MyLambdaInput(tableName))
}

就像我说的那样,当我在这段代码上运行spark-submit时,它确实的确实现了Lambda函数。但是我无法解释为什么它两次击中它。结果是我在DynamoDB中配置了两个表。

在将其作为Spark作业运行的上下文中,等待步骤似乎也失败了。但是,当我对等待的代码进行单元测试时,它似乎可以正常工作。它成功阻塞,直到表准备就绪。

起初,我认为spark-submit可能正在将此代码发送到所有工作节点,并且它们独立地运行了整个过程。最初,我有一个Spark集群,其中有1个主机和2个工人。但是,我在具有1个主控和5个工作人员的另一个群集上进行了测试,然后又再次准确地两次调用了Lambda函数,然后显然未能等待表创建,因为它在调用Lambda之后不久就死了。

有人知道Spark可能在做什么吗?我缺少明显的东西吗?

更新:这是我的spark-submit args,可在EMR的“步骤”标签上看到。

  

spark-submit-部署模式集群--class com.mypackage.spark.MyMainClass s3://my-bucket/my-spark-job.jar

这是我的getConfiguration函数的代码:

def getConfiguration(tableName: String) : JobConf = {
  val conf = new Configuration()
  conf.set("dynamodb.servicename", "dynamodb")
  conf.set("dynamodb.input.tableName", tableName)
  conf.set("dynamodb.output.tableName", tableName)
  conf.set("dynamodb.endpoint", "https://dynamodb.us-east-1.amazonaws.com")
  conf.set("dynamodb.regionid", "us-east-1")
  conf.set("mapred.output.format.class", "org.apache.hadoop.dynamodb.write.DynamoDBOutputFormat")
  conf.set("mapred.input.format.class", "org.apache.hadoop.dynamodb.read.DynamoDBInputFormat")
  new JobConf(conf)
}

这里还有a Gist,其中包含我尝试运行该日志时看到的一些异常日志。

3 个答案:

答案 0 :(得分:3)

感谢@soapergem添加日志记录和选项。我添加一个答案(尝试一个),因为它可能比评论要长一点:)

总结:

最后一个问题:

  

如果我以“客户端”部署模式而不是“集群”部署模式运行代码,那么我的代码似乎可以工作?这对这里的任何人都有暗示吗?

有关差异的更多信息,请检查https://community.hortonworks.com/questions/89263/difference-between-local-vs-yarn-cluster-vs-yarn-c.html。在您的情况下,看起来在客户端模式下执行spark-submit的计算机具有与EMR作业流程不同的IAM策略。我的假设是您的工作流程角色不允许dynamodb:Describe*,这就是为什么500 code(从您的要旨中)得到例外的原因:

Caused by: com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException: Requested resource not found: Table: EmrTest_20190708143902 not found (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ResourceNotFoundException; Request ID: V0M91J7KEUVR4VM78MF5TKHLEBVV4KQNSO5AEMVJF66Q9ASUAAJG)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleErrorResponse(AmazonHttpClient.java:1712)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1367)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeHelper(AmazonHttpClient.java:1113)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.doExecute(AmazonHttpClient.java:770)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeWithTimer(AmazonHttpClient.java:744)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.execute(AmazonHttpClient.java:726)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutor.access$500(AmazonHttpClient.java:686)
    at com.amazonaws.http.AmazonHttpClient$RequestExecutionBuilderImpl.execute(AmazonHttpClient.java:668)
    at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:532)
    at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:512)
    at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.doInvoke(AmazonDynamoDBClient.java:4243)
    at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.invoke(AmazonDynamoDBClient.java:4210)
    at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.executeDescribeTable(AmazonDynamoDBClient.java:1890)
    at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.describeTable(AmazonDynamoDBClient.java:1857)
    at org.apache.hadoop.dynamodb.DynamoDBClient$1.call(DynamoDBClient.java:129)
    at org.apache.hadoop.dynamodb.DynamoDBClient$1.call(DynamoDBClient.java:126)
at org.apache.hadoop.dynamodb.DynamoDBFibonacciRetryer.runWithRetry(DynamoDBFibonacciRetryer.java:80)

要确认这一假设,您可以执行一部分来创建表并在本地等待创建(此处没有Spark代码,只需对主要功能执行简单的java命令)即可:

  • 首次执行时,请确保您具有所有权限。 IMO将是dynamodb:Describe*上的Resources: *(如果是原因,AFAIK您应出于最小特权原则在生产中使用Resources: Test_Emr*
  • 对于第二次执行,删除dynamodb:Describe*并检查是否获得了与要旨中相同的堆栈跟踪

答案 1 :(得分:3)

我在群集模式(v2.4.0)中也遇到了相同的问题。我通过使用SparkLauncher(而不是spark-submit.sh)以编程方式启动我的应用程序来解决此问题。您可以将lambda逻辑移入用于启动spark应用的主要方法,如下所示:

def main(args: Array[String]) = {
    // generate a unique table name
    val tableName = generateTableName()

    // call the lambda function
    val result = callLambdaFunction(tableName)

    // wait for the table to be created
    waitForTableCreation(tableName)

    val latch = new CountDownLatch(1);

    val handle = new SparkLauncher(env)
        .setAppResource("/path/to/spark-app.jar")
        .setMainClass("com.company.SparkApp")
        .setMaster("yarn")
        .setDeployMode("cluster")
        .setConf("spark.executor.instances", "2")
        .setConf("spark.executor.cores", "2")
        // other conf ... 
        .setVerbose(true)
        .startApplication(new SparkAppHandle.Listener {
            override def stateChanged(sparkAppHandle: SparkAppHandle): Unit = {
                latch.countDown()
            }

            override def infoChanged(sparkAppHandle: SparkAppHandle): Unit = {

            }
        })  

    println("app is launching...")
    latch.await()
    println("app exited")
}

答案 2 :(得分:2)

您的spark作业在实际创建表之前就开始了,因为一一定义操作并不意味着它们将等到上一个操作完成

您需要更改代码,以使与spark相关的块在创建表之后开始,而要实现它,您必须使用for-comprehension以确保完成每个步骤,或者将spark管道放入创建表后调用waiter的回调(如果有的话,很难说)

您还可以使用andThen或简单的map

主要要点是,主体中编写的所有代码行均立即执行,而无需等待上一行完成