PROJET AUTOBLOG


IT-Connect

Site original : IT-Connect

⇐ retour index

PowerShell et ForEach-Object Parallel : traitement des objets en parralèle

jeudi 7 mai 2020 à 13:00

I. Présentation

A l'occasion de la sortie de PowerShell 7 en ce début d'année 2020, Microsoft a ajouté une nouvelle fonctionnalité à sa commande "ForEach-Object". Il s'agit d'une fonctionnalité qui sert à paralléliser le traitement des objets reçus du pipeline grâce à plusieurs jobs exécutés en simultanés.

Dans son comportement classique, une boucle ForEach-Object traite tous les objets reçus par le pipeline de manière séquentielle, c'est-à-dire un par un, dans l'ordre. Avec le paramètre -Parallel, la boucle ForEach-Object est désormais en mesure de traiter plusieurs objets en parallèle.

L'objectif étant de réduire le temps de traitement et d'exécution de vos scripts, mais attention, cette fonctionnalité ne doit pas être utilisée systématique car cela pourrait bien créer l'effet inverse. Explications.

Note : cette option fonctionne seulement pour la boucle de type ForEach-Object lorsqu'elle est utilisée à la suite du pipeline. Elle n'existe pas à ce jour pour une boucle de type ForEach(){} où l'on détermine la collection d'objets en amont, par exemple sous cette forme : ForEach($element in $collection){ # instructions }.

II. Utilisation du paramètre -Parallel

Pour commencer, nous allons voir comment s'utilise ce paramètre au travers d'un exemple tout simple. Cela nous donne l'occasion de comparer le comportement avec ou sans ce paramètre avec un cas de figure où je suis sûr qu'il sera bénéfique 😉

L'exemple est le suivant : nous avons une collection qui contient les valeurs de 1 à 5 (1..5) et pour chaque valeur, nous allons écrire dans la console "Numéro <valeur>" et marquer une pause d'une seconde entre chaque.

Avec l'écriture classique d'une boucle ForEach-Object, cela donne :

1..5 | ForEach-Object { "Numéro $_"; Start-Sleep -Seconds 1 }

Dans la console, nous obtenons le résultat attendu, à savoir :

Numéro 1
Numéro 2
Numéro 3
Numéro 4
Numéro 5

Grâce à la commande Measure-Command, nous allons calculer le temps d'exécution de ce bloc afin d'avoir un temps de référence. Cette action s'effectue simple via :

(Measure-Command { 1..5 | ForEach-Object { "Numéro $_"; Start-Sleep -Seconds 1 } }).Seconds

Le résultat est : 5, c'est-à-dire 5 secondes. C'est cohérent puisque nous avons 5 valeurs et qu'il y a une pause d'une seconde à chaque fois.

Maintenant, nous allons ajouter une dose de parallélisation pour exécuter ce même bloc. Nous allons utiliser deux paramètres : Parallel et ThrottleLimit. Le premier paramètre sert à activer la parallélisation sur la boucle ForEach-Object alors que le second indique le nombre de script blocs à exécuter en même temps. Par défaut, ThrottleLimit = 5.

La syntaxe est la suivante :

1..5 | ForEach-Object -Parallel { "Numéro $_"; Start-Sleep -Seconds 1 } -ThrottleLimit 5

Le résultat retourné par cette commande est le même qu'avec la méthode séquentielle. En revanche, il est intéressant de calculer le temps d'exécution de cette commande afin de voir si la parallélisation est bénéfique.

(Measure-Command { 1..5 | ForEach-Object -Parallel { "Numéro $_"; Start-Sleep -Seconds 1 } -ThrottleLimit 5 }).Seconds

Miracle ! Le temps d'exécution est passé à 1 seconde seulement ! C'est logique car avec la parallélisation nous avons autorisé l'exécution de 5 scripts blocs en même temps (ThrottleLimit) donc la pause de 1 seconde incluse au traitement de chaque objet, n'a pas impactée l'objet suivant puisque tout s'est fait en parallèle.

III. Les cas d'usage du paramètre -Parallel

Une boucle ForEach-Object exécutée avec le paramètre -Parallel s'appuie sur le principe des espaces de travail PowerShell appelés Runspace pour faire tourner plusieurs tâches en parallèle.

Il faut prioriser l'utilisation de cette option sur les machines équipées d'un processeur avec plusieurs cœurs afin d'optimiser les performances et ne pas risquer de saturer l'hôte. Pour utiliser cette option, il faut également déterminer si cela a un intérêt en fonction de l'action réalisée par votre boucle ForEach-Object. Cela s'applique principalement dans les deux cas suivants :

📌 Une boucle qui attend après quelque chose : si pour le traitement de chaque objet, vous attendez la fin d'une opération ou vous devez ajouter une temporisation, ce temps perdu peut-être peut-être limité grâce à la parallélisation. C'est sur un exemple de ce type, très simplifié, que portait le premier exemple de cet article.

📌 Traitement d'une quantité importante de données : si pour chaque objet vous devez exécuter plusieurs traitements dans le bloc d'instructions et que ce sont des opérations longues, vous pouvez envisager d'utiliser la parallélisation pour en lancer plusieurs en même temps. Exemples : traitement sur des lots de fichiers, des fichiers journaux ou exécution d'actions sur des hôtes distants.

Dans certains cas, la parallélisation n'a pas d'intérêt et peut même allonger le temps d'exécution de votre script. En fait, le temps de création d'un nouvel espace de travail pour chaque instance demande de la ressource et du temps, donc cela peut alourdir votre traitement plus de l'optimiser.

Pour optimiser les performances et le temps de traitement, vous devez également ajuster le paramètre ThrottleLimit pour autoriser plus ou moins d'espace de travail à se créer en parallèle sur votre machine.

IV. L'isolation d'un espace de travail

Lorsque l'on s'appuie sur l'option Parallel, cela va utiliser le principe des espaces de travail : pour chaque traitement lancé, un espace de travail est créé et utilisé pour réaliser le traitement de l'objet. Ce contexte d'exécution crée une isolation de chaque espace de travail, ce qui n'est pas neutre : le runspace n'accède pas aux variables de votre programme principal car il est isolé.

Prenons un exemple, à partir de celui vu précédemment. Nous allons définir une variable $data avec une valeur toute simple en dehors de la boucle ForEach-Object... Et nous allons appeler cette variable pour l'afficher dans la console. Ce qui donne :

$data = "IT-Connect"
1..5 | ForEach-Object -Parallel { "$data - Numéro $_"; Start-Sleep -Seconds 1 } -ThrottleLimit 5

Si l'on exécute ce code, on obtient la sortie ci-dessous. Ce qui prouve que l'espace de travail n'a pas accès à notre variable.

- Numéro 1
- Numéro 2
- Numéro 3
- Numéro 4
- Numéro 5

Pour qu'une variable soit accessible à l'intérieur de l'espace de travail, nous devons utiliser le mot clé $using: en préfixe. Par exemple pour la variable $data l'appel sera le suivant : $using:data.

Si l'on applique cette méthode à notre exemple précédent, cela donne :

1..5 | ForEach-Object -Parallel { "$using:data - Numéro $_"; Start-Sleep -Seconds 1 } -ThrottleLimit 5

Cette fois-ci le retour de la console correspond à notre attente. Voyez par vous-même :

Néanmoins, il est important de préciser que cette méthode est contraire au principe d'isolation des runspaces et crée une violation de l'isolation. En effet, la variable se retrouve partagée entre les espaces de travail : soyez donc vigilant à l'usage que vous faites de cette variable.

En conclusion, je dirais que cette fonctionnalité de parallélisation peut s'avérer intéressante dans de nombreux cas, tout dépend du contenu de votre bloc d'instructions pour déterminer si cela est pertinent ou non de l'utiliser. Des tests seront à réaliser pour trouver le scénario le plus intéressant, que ce soit en terme d'utilisation du compute, que du temps. Pour cela, la commande Measure-Command est votre alliée.