web-dev-qa-db-fra.com

ConcurrentModificationException lors de l'utilisation de Spark collectionAccumulator

J'essaie d'exécuter une application Spark sur un cluster à la demande Azure HDInsight et je constate qu'un grand nombre d'exceptions SparkExceptions (causées par ConcurrentModificationExceptions) sont consignées. L'application s'exécute sans ces erreurs lorsque je démarre une instance Spark locale. 

J'ai vu des rapports signalant des erreurs similaires lors de l'utilisation d'accumulateurs et mon code utilise effectivement un CollectionAccumulator. Cependant, j'ai placé des blocs synchronisés partout où je l'utilise, et cela ne fait aucune différence. Le code relatif à l'accumulateur ressemble à ceci:

class MySparkClass(sc : SparkContext) {
    val myAccumulator = sc.collectionAccumulator[MyRecord]

    override def add(record: MyRecord) = {
        synchronized {
            myAccumulator.add(record)
        }
    }

    override def endOfBatch() = {
        synchronized {
            myAccumulator.value.asScala.foreach((record: MyRecord) => {
                processIt(record)
            })
        }
    }
}

Les exceptions ne provoquent pas l'échec de l'application. Cependant, lorsque endOfBatch est appelé et que le code tente de lire les valeurs dans l'accumulateur, il est vide et processIt n'est jamais appelé.

Nous utilisons HDInsight version 3.6 avec Spark version 2.3.0

18/11/26 11:04:37 WARN Executor: Issue communicating with driver in heartbeater
org.Apache.spark.SparkException: Exception thrown in awaitResult: 
    at org.Apache.spark.util.ThreadUtils$.awaitResult(ThreadUtils.scala:205)
    at org.Apache.spark.rpc.RpcTimeout.awaitResult(RpcTimeout.scala:75)
    at org.Apache.spark.rpc.RpcEndpointRef.askSync(RpcEndpointRef.scala:92)
    at org.Apache.spark.executor.Executor.org$Apache$spark$executor$Executor$$reportHeartBeat(Executor.scala:785)
    at org.Apache.spark.executor.Executor$$anon$2$$anonfun$run$1.apply$mcV$sp(Executor.scala:814)
    at org.Apache.spark.executor.Executor$$anon$2$$anonfun$run$1.apply(Executor.scala:814)
    at org.Apache.spark.executor.Executor$$anon$2$$anonfun$run$1.apply(Executor.scala:814)
    at org.Apache.spark.util.Utils$.logUncaughtExceptions(Utils.scala:1988)
    at org.Apache.spark.executor.Executor$$anon$2.run(Executor.scala:814)
    at Java.util.concurrent.Executors$RunnableAdapter.call(Executors.Java:511)
    at Java.util.concurrent.FutureTask.runAndReset(FutureTask.Java:308)
    at Java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.Java:180)
    at Java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.Java:294)
    at Java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.Java:1149)
    at Java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.Java:624)
    at Java.lang.Thread.run(Thread.Java:748)
Caused by: Java.util.ConcurrentModificationException
    at Java.util.ArrayList.writeObject(ArrayList.Java:770)
    at Sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at Sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.Java:62)
    at Sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.Java:43)
    at Java.lang.reflect.Method.invoke(Method.Java:498)
    at Java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.Java:1140)
    at Java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.Java:1496)
    at Java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.Java:1432)
    at Java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.Java:1178)
    at Java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.Java:1548)
    at Java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.Java:441)
    at Java.util.Collections$SynchronizedCollection.writeObject(Collections.Java:2081)
    at Sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at Sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.Java:62)
    at Sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.Java:43)
    at Java.lang.reflect.Method.invoke(Method.Java:498)
    at Java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.Java:1140)
    at Java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.Java:1496)
    at Java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.Java:1432)
    at Java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.Java:1178)
    at Java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.Java:1548)
    at Java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.Java:1509)
    at Java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.Java:1432)
    at Java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.Java:1178)
    at Java.io.ObjectOutputStream.writeArray(ObjectOutputStream.Java:1378)
    at Java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.Java:1174)
    at Java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.Java:1548)
    at Java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.Java:1509)
    at Java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.Java:1432)
    at Java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.Java:1178)
    at Java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.Java:1548)
    at Java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.Java:1509)
    at Java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.Java:1432)
    at Java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.Java:1178)
    at Java.io.ObjectOutputStream.writeArray(ObjectOutputStream.Java:1378)
    at Java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.Java:1174)
    at Java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.Java:1548)
    at Java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.Java:1509)
    at Java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.Java:1432)
    at Java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.Java:1178)
    at Java.io.ObjectOutputStream.writeObject(ObjectOutputStream.Java:348)
    at org.Apache.spark.serializer.JavaSerializationStream.writeObject(JavaSerializer.scala:43)
    at org.Apache.spark.rpc.netty.RequestMessage.serialize(NettyRpcEnv.scala:565)
    at org.Apache.spark.rpc.netty.NettyRpcEnv.ask(NettyRpcEnv.scala:231)
    at org.Apache.spark.rpc.netty.NettyRpcEndpointRef.ask(NettyRpcEnv.scala:523)
    at org.Apache.spark.rpc.RpcEndpointRef.askSync(RpcEndpointRef.scala:91)
    ... 13 more

Le code suivant est un exemple plus autonome qui reproduit le problème. MyRecord est une classe de cas simple contenant uniquement des valeurs numériques. Le code s'exécute localement sans erreur, mais sur un cluster HDInsight, il génère l'erreur ci-dessus.

object MainDemo {
    def main(args: Array[String]) {
        val sparkContext = SparkSession.builder.master("local[4]").getOrCreate().sparkContext
        val myAccumulator = sparkContext.collectionAccumulator[MyRecord]

        sparkContext.binaryFiles("/my/files/here").foreach(_ => {
            for(i <- 1 to 100000) {
                val record = MyRecord(i, 0, 0)
                myAccumulator.add(record)
            }
        })

        myAccumulator.value.asScala.foreach((record: MyRecord) => {
            // we expect this to be called once for each record that we 'add' above,
            // but it is never called
            println(record)
        })
    }
}
7
codebox

Je doute que le fait d'avoir un bloc synchronisé aide vraiment. CustomeAccumulators ou tous les autres accumulateurs ne sont pas thread-safe. Ils n'ont pas vraiment à le faire puisque la méthode DAGScheduler.updateAccumulators que le pilote d'étincelle utilise pour mettre à jour les valeurs des accumulateurs à la fin d'une tâche (avec succès ou en cas d'échec) n'est exécutée que sur un seul thread exécutant la boucle de planification. En outre, ce sont des structures de données en écriture seule pour les travailleurs disposant de leur propre référence d'accumulateur local, alors que l'accès à la valeur d'un accumulateur est uniquement autorisé par le pilote. Et lorsque vous dites que cela fonctionne en mode local car il s’agit d’une machine virtuelle unique, mais qu’en mode cluster, il s’agit d’une instance JVM et d’une instance Java différentes, des appels PRC sont déclenchés pour activer la communication. 

A quoi ressemble votre objet MyRecord et si vous terminez votre ligne avec .value plutôt que d'avoir un itérateur dessus, cela vous aidera. Juste essayer.

myAccumulator.value
1
H Roy