我正在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,其中包含我尝试运行该日志时看到的一些异常日志。
答案 0 :(得分:3)
感谢@soapergem添加日志记录和选项。我添加一个答案(尝试一个),因为它可能比评论要长一点:)
总结:
spark-submit
和配置选项并不奇怪最后一个问题:
如果我以“客户端”部署模式而不是“集群”部署模式运行代码,那么我的代码似乎可以工作?这对这里的任何人都有暗示吗?
有关差异的更多信息,请检查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
命令)即可:
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
主要要点是,主体中编写的所有代码行均立即执行,而无需等待上一行完成