Metsys Blog

Powershell – Comment optimiser des scripts complexes ? – Partie 1

Cliquez pour évaluer cet article !
0 avis

Dans cet article, vous retrouverez des cas pratiques ou des astuces d’optimisations, sur des scripts PowerShell.

Nous mettrons à disposition un script complet permettant d’inventorier un tenant Office 365 sur 3 connections au lieu d’une et qui peut vous servir de modèle pour vos propres scripts. Vous pourrez ainsi voir la différence de vitesse d’exécution en multithread sur des grandes volumétries.

Aujourd’hui, 4 sujets abordés :

  1. Avoir un éditeur de texte performant
  2. Rappel des latences systèmes
  3. Guide de chargement des objets
  4. Bonnes pratiques sur les scripts PowerShell

Au revoir PowerShell GUI / PowerShell ISE ! Bienvenue Visual Studio Code !

Premier point, je vous conseille un bon éditeur de texte Powershell, y’a pas de secret. Notepad dépanne. Notepad++ est parfait pour les logs ou les databases mais ne sont pas efficaces sur les scripts PowerShell évolués. On gagne du temps avec la complétion des commandes ou la coloration syntaxique sur des gros codes.

Le must : Sapien editor Powershell, malheureusement payant mais réellement le plus efficace que ce soit pour écrire, l’auto complétion ou écrire interfaces Graphiques (GUI) : https://www.sapien.com/software/powershell_studio

Sapien editor Powershell

Une version d’évaluation est disponible pour 45 jours. La version payante coute environ 387 $. Je vous conseil fortement de le tester.

La tendance du moment Visual Code : De Microsoft, gratuit léger, multiplateforme, en évolution, léger et efficace, il ne lui manque que la partie pour créer des GUI pour être parfait.

Pour télécharger Visual Code : https://code.visualstudio.com/download

Visual Studio Code
4

Installer le, et après n’oublier pas de faire « CTRL + P« , et la commande « ext install PowerShell » pour ajouter les fonctionnalités PowerShell évoluées. Redémarrer l’éditeur et vous êtes prêt !

Note : tous les codes sont exécutés en PowerShell 5.1 avec multithread activé. Pour savoir sa version : « $host.version » dans un shell.

Rappel des latences systèmes

Ordre de performances des accès disques (I/O) du plus rapide au plus lent (sans parler de cache spécifique) :

CPU – RAM – SSD – Disque Dur -- Fibre – Réseau LAN – WAN

En base de donnée commune régulièrement rencontrée sur un SI :

Oracle -- SQL server – Fichier local sur disque -- AD – Webservices Exchange Onpremise – Office 365 / Services Cloud (PowerShell ou web services)

A Eviter

Le pire étant les accès réseaux multiples tel que l’interrogation d’un Contrôleur de domaine pour récupération de l’information de chaque objet.

Optimiser en gardant les objets en RAMpour le traitement.

Les accès réseaux sont la principale raison de baisse de performances des scripts, soyez toujours au plus près de vos Databases, référentiel ou annuaires.

Eviter absolument de faire de multiples petites requêtes par éléments -> Récupéré les objets en une seule requête fine précise et traiter après les objets stockés en RAM.

Ayez une requête précise sur la commande Powershell initiale (requête LDAP, Exchange , SQL…), ceci permet une double optimisation:

  1. La lecture de la source à moins de donnée à parcourir (SQL / LDAP…)
  2. Moins de données sont à transférés sur le réseau

Guide de chargement des objets

Rappel technique

La RAM est 5 à 50 fois plus rapide qu’un accès SSD moderne. Et 1000 fois plus rapide que les accès disques physiques.

Les gros blocs sont plus rapides à écrire que les petits blocs car moins d’I/O disques : mieux vaut écrire 5Mo de donnée en une fois sur un support que 50 fois 100 ko. Les performances se dégradent à cause des Accusés de réceptions des blocs sur le file system au niveau des contrôleurs disques ou CPU.

Le processus PowerShell.exe est en 64 bits par défaut, donc normalement capables d’allouer toute la RAM de la machine, et non pas seulement juste 4GO. Il faut en profiter ! Par contre si vous vous rendez compte que l’OS commence à Swaper pour répondre aux demandes de RAM de votre script, il faut mettre un en place un traitement par lot : Par exemple par bloc de 10 000 objets, puis les 10 000 suivants en flux tendus.

Ceci est un élément clé que doit maîtriser un ingénieur système, il doit trouver la bonne limite de RAM sans mettre à mal la gestion mémoire de la machine Hôte. 

Un processus Powershell.exe qui swap sur une machine de production est non admis, dans mes développements.

Les étapes optimales sont :

  • Un script doit lire les différentes sources (fichier plat, base SQL, contrôleur de domaine, Web services…) au début de l’initialisation.
  • Traiter le maximum d’objet en RAM (renommage, comparaison, suppression, validation…)
  • Si ce n’est pas suffisant refaire un second bloc jusqu’à la fin. Ceci peut être un traitement infini.
  • Stocker les changements quelques part (Base SQL par exemple pour réutilisation rapide)
  • Ecriture d’un gros bloc sur le périphérique de sortie (AD, fichier, network…). Si la destination ne peut traiter, il doit découper avec des blocs d’écritures les plus gros possibles.

Exemple : concaténation d’objets utilisateurs de deux AD et écriture dans un fichier au format « .CSV » :

Le premier AD contient 10 000 utilisateurs, le second 60 000 utilisateurs. Certains sont communs, nous cherchons à sortir un fichier CSV avec toutes les caractéristiques assemblées des environnements pour les utilisateurs présents dans les deux sources.

  • Cas par un exemple d’une forêt de ressource Exchange avec une forêt de comptes.
  • En premier choisir une clé commune de comparaison sur un attribut fini/connu/fixe (« Samaccoutname », « UPN », « mail », »employeeID »,…).
  • Lecture de chaque AD avec mise en cache de toute les valeurs de chaque AD dans 2 tables de Hashs.
  • Le nombre d’éléments étant volumineux, nous choisissons de couper les requêtes par lot de 2 lettres dans l’alphabet. Ainsi nous requêterons chaque DC en demandant tous les utilisateurs qui ont le champ mail commençant par la lettre A ou B.
  • Nous comparons les résultats de chaque DC avec l’autre forêt
  • Nous écrivons des valeurs communes par bloc de 250 utilisateurs sur le fichier final de sortie CSV
  • On recommence sur le lot suivant, avec les lettres C et D et ainsi de suite jusqu’à épuisement des objets retournés

A éviter :

  • Faire un appel à la source toute les 1 commande pour comparer (perfs catastrophiques)
  • Ne pas utiliser un type d’objet optimiser pour l’écriture et la lecture rapide. Ici l’objet Table de hash est bien plus rapide qu’Array par exemple car nous disposons d’un attribut commun de comparaison.
  • Ecrire les valeurs au fur et à mesure les résultats. Les petites écritures sur disques sont très consommatrices en temps disque ou CPU (il faut une confirmation pour chaque accès par le système de fichier, très consommateur). à Mettre en cache en ram et écrire un gros bloc sur le disque. Recommencer l’écriture par bloc tant qu’il y a des résultats.
# Exemple optimisé:

# Récupération de tous les utilisateurs en une seule fois (un seul appel réseau)

# et stockage en Ram
$allusersAD1 = Get-ADUser -Filter * -domaincontroller SERVER1.contoso.com
$SourceAD2 = Get-ADUser -Filter * -domaincontroller DC2.fabrikam.local
Foreach ($otherUser in $SourceAD2)
{
Foreach ($currentUser in $allusersAD1)
{

# Comparaison pour $CurrentUser
If ($currentUser. SamAccountName -like "$(otherUser).SamAccountName*")
{

# Traitement si identique

# …
Break  # Break car objet unique, évite de finir la boucle à passe à l'objet suivant

# ne consomme pas la fin de la boucle
}
} # fin foreach AD1
} # Fin foreach AD2

### ------------------------------- ###
# Exemple non optimisé

# Lecture sur l'AD 2 objet par objet envoyé dans le PIPE
$SourceAD2 = Get-ADUser -Filter * -domaincontroller DC2.fabrikam.local
Foreach ($otherUser in $SourceAD2)
{

# Comparaison pour savoir si la personne existe dans le second AD

# un appel système réseau pour avoir la réponse, temps de traitement maintenant dépendant du réseau…

$exist = Get-ADUser -Identity $otherUser.SamAccountName
# Si existe
If ($exist)
{

# Traitement
# …

}
} # Fin foreach AD2

Bonnes pratiques sur les scripts PowerShell (script orienté client)

Quand vous commencez un script PowerShell, pensez directement que ca sera un script de production. Et implémentez une fonctionnalité de test, éviter d’avoir plusieurs versions du code à maintenir, c’est source d’erreur fréquentes de retro portage de code.

  • Créer un dossier fixe pour lui: c:\scripts\ScriptAction_XXX1
  • Placer tous les fichiers utiles du script dedans (logs, Credentials, résultats…)
  • Un fichier texte de recommandation contenant : Société, Nom, Version, descriptif du script, Version du PowerShell nécessaire, module nécessaire à installer, droits nécessaires, fichier du script principal de lancement, exemple de lancement en production et en test.
Arborescence

Sur chaque fichier PS1 :

  • Header de votre société avec votre nom et une version et/ou une date (actualisée !)
  • Des plugins existent pour gérer les headers dans Visual Studio Code.

Dans chaque fichier PowerShell du script, toujours protégé sont codes avec cette commande simple (pour tous les fichiers, le principal comme les modules, les class ou les threads) :

Set-strictmode -version Latest
  • Evite les erreurs de typos dans les scripts, les variables non initialisées, les variables sans nom, etc…
  • Pour faire simple vous éviterons les problèmes « Bizarres » de vos scripts. Force les bonnes pratiques et une syntaxe propre du code

Ref: https://technet.microsoft.com/fr-FR/library/dd347614.aspx

Coder sans, c’est chercher les problèmes ! Simple et efficace

Securite

Dans le fichier principal de lancement (le « Main » file), mettre en place des paramètres commun à tous vos scripts :

  • -cls : nettoyer l’écran
  • -verbose : afficher les commandes en Verbose, permet d’afficher le contenu en plus des commandes write-verbose
  • -testmode : vous permet de switcher entre un mode de test plus rapide et plus verbose avec un mode de production. On utilise ainsi le même code avec des particularités différentes. Apporte beaucoup de souplesse.
  • -nologfile : Par défaut, vos scripts doivent logger toutes les actions, les verboses, les warnings, les erreurs et les résultats. Pour plus de rapidité, vous pouvez éviter ces logs explicitement.

Récupérer le chemin courant et nom du script lancé, pour avoir des chemins absolus au lieu de relatifs. Ceci évite les erreurs sur les chargements / écriture de sortie. Ceci permet de construire des chemins absolus utilisés par certaines Cmdlets.

$currentFolderScript split-path -parent $MyInvocation.MyCommand.Definition

Utiliser des commentaires constructifs ! Ils ne doivent pas décrire l’action technique de la ligne (le programmeur qui relie connait le langage aussi) mais le but recherché!

Typiquement un début de script commence ainsi :

<#
* @Author: Damien C.
* @Date: 2017-03-16 15:25:39
* @Last Modified by:   Damien C.
* @Last Modified time: 2017-03-16 15:25:39
*
* V3 Test: changement dans les classes / threads
*
#>

# Test multiples connections on Office 365 for performances
# Paramètres de lancement
Param ([switch]$verbose = $false, [switch]$testmode = $false, [switch]$cls = $false,
[switch]$noLogsFile = $false)

# Force code propre
Set-strictmode -version Latest

# Si parametre verbose, on affiche tous les Write-verbose
if ($verbose)
{
$VerbosePreference = 'Continue'
}

# Si parametre CLS, on nettoye la console
if ($cls)
{
Clear-Host
}

# Suite du code …

Notez cet article

Vous avez aimé cet article ?

Rendez-le plus visible auprès des internautes en lui mettant une bonne note.

Cliquez pour évaluer cet article !
0 avis

Articles pouvant vous intéresser

RETEX CERT

Tout d’abord, en termes d’éthique et pour respecter la confidentialité des sujets aussi sensibles que