web-dev-qa-db-fra.com

Comment effectuer une opération sur chaque exécuteur une fois dans spark

J'ai un modèle weka stocké dans S3 qui est d'une taille d'environ 400 Mo. Maintenant, j'ai un ensemble d'enregistrement sur lequel je veux exécuter le modèle et effectuer des prédictions.

Pour effectuer des prédictions, ce que j'ai essayé c'est,

  1. Téléchargez et chargez le modèle sur le pilote en tant qu'objet statique, diffusez-le à tous les exécuteurs. Effectuez une opération de carte sur la prédiction RDD. ----> Ne fonctionne pas, comme dans Weka pour effectuer la prédiction, l'objet modèle doit être modifié et la diffusion nécessite une copie en lecture seule.

  2. Téléchargez et chargez le modèle sur le pilote en tant qu'objet statique et envoyez-le à l'exécuteur lors de chaque opération de mappage. -----> Fonctionne (pas efficace, comme dans chaque opération de carte, je passe un objet de 400 Mo)

  3. Téléchargez le modèle sur le pilote et chargez-le sur chaque exécuteur et mettez-le en cache. (Je ne sais pas comment faire ça)

Quelqu'un a-t-il une idée de comment puis-je charger le modèle sur chaque exécuteur une fois et le mettre en cache afin que pour les autres enregistrements, je ne le charge pas à nouveau?

26
Neha

Vous avez deux options:

1. Créez un objet singleton avec un val paresseux représentant les données:

    object WekaModel {
        lazy val data = {
            // initialize data here. This will only happen once per JVM process
        }
    }       

Ensuite, vous pouvez utiliser la valeur paresseuse dans votre fonction map. Le lazy val garantit que chaque JVM de travail initialise sa propre instance des données. Aucune sérialisation ou diffusion ne sera effectuée pour data.

    elementsRDD.map { element =>
        // use WekaModel.data here
    }

Avantages

  • est plus efficace, car il vous permet d'initialiser vos données une fois par instance JVM. Cette approche est un bon choix lorsque vous devez initialiser un pool de connexions de base de données par exemple.

Inconvénients

  • Moins de contrôle sur l'initialisation. Par exemple, il est plus difficile d'initialiser votre objet si vous avez besoin de paramètres d'exécution.
  • Vous ne pouvez pas vraiment libérer ou libérer l'objet si vous en avez besoin. Habituellement, c'est acceptable, car le système d'exploitation libérera les ressources à la fin du processus.

2. Utilisez la méthode mapPartition (ou foreachPartition) sur le RDD au lieu de simplement map.

Cela vous permet d'initialiser tout ce dont vous avez besoin pour la partition entière.

    elementsRDD.mapPartition { elements =>
        val model = new WekaModel()

        elements.map { element =>
            // use model and element. there is a single instance of model per partition.
        }
    }

Avantages:

  • Offre plus de flexibilité dans l'initialisation et la désinitialisation des objets.

Inconvénients

  • Chaque partition créera et initialisera une nouvelle instance de votre objet. Selon le nombre de partitions dont vous disposez par instance JVM, cela peut ou non être un problème.
26
Dia Kharrat

Voici ce qui a fonctionné pour moi encore mieux que l'initialiseur paresseux. J'ai créé un pointeur de niveau objet initialisé à null et j'ai laissé chaque exécuteur l'initialiser. Dans le bloc d'initialisation, vous pouvez avoir du code à exécution unique. Notez que chaque lot de traitement réinitialisera les variables locales mais pas celles au niveau de l'objet.

object Thing1 {
  var bigObject : BigObject = null

  def main(args: Array[String]) : Unit = {
    val sc = <spark/scala magic here>
    sc.textFile(infile).map(line => {
      if (bigObject == null) {
         // this takes a minute but runs just once
         bigObject = new BigObject(parameters)  
      }
      bigObject.transform(line)
    })
  }
}

Cette approche crée exactement un gros objet par exécuteur, plutôt que le seul gros objet par partition d'autres approches.

Si vous placez var bigObject: BigObject = null dans l'espace de noms de la fonction principale, il se comporte différemment. Dans ce cas, il exécute le constructeur bigObject au début de chaque partition (ie. Batch). Si vous avez une fuite de mémoire, cela tuera éventuellement l'exécuteur testamentaire. La collecte des ordures devrait également faire plus de travail.

1
Dale Johnson

Voici ce que nous faisons habituellement

  1. définir un client singleton qui fait ce genre de choses pour s'assurer qu'un seul client est présent dans chaque exécuteur

  2. avoir une méthode getorcreate pour créer ou récupérer les informations du client, nous vous proposons généralement une plate-forme de service commune que vous souhaitez servir pour plusieurs modèles différents, puis nous pouvons utiliser comme concurrentmap pour garantir threadsafe et computeifabsent

  3. la méthode getorcreate sera appelée à l'intérieur du niveau RDD comme transform ou foreachpartition, alors assurez-vous que init se produit au niveau de l'exécuteur

0
Zhang Rui