agowa338
Goto Top

PowerShell Transpose Objects und Memoryleak

Hallo,

weiß jemand wie ich folgenden Powershell Code etwas optimieren kann?
Ich habe sehr viele Eventlog Einträge > 100.000 die ich verarbeiten will (Domänen Controller), leider kann ich die erforderlichen Daten nur aus dem Xml entnehmen...
$xmlEvent = @(); $xmlEvents = @();
$filter = @{
    Logname   = "Security";  
    ID        = 4624;
    StartTime = (Get-Date).AddDays(-10);
}
$filterSid = "S-1-5-32" # Actually the Sid of a specific service account and not of the System account  
$i = 0;
Get-WinEvent -FilterHashtable $filter | ForEach-Object {
    $xmlEvent = ([XML]$_.ToXml()).Event.EventData.Data;
    $xmlEvents += [psCustomObject][Ordered] @{
        TargetUserSid = [String]($xmlEvent | Where-Object {$_.Name -eq 'TargetUserSid'}).'#text';  
        TargetUserName = [String]($xmlEvent | Where-Object {$_.Name -eq 'TargetUserName'}).'#text';  
        TargetDomainName = [String]($xmlEvent | Where-Object {$_.Name -eq 'TargetDomainName'}).'#text';  
        IpAddress = [String]($xmlEvent | Where-Object {$_.Name -eq 'IpAddress'}).'#text';  
        LogonProcessName = [String]($xmlEvent | Where-Object {$_.Name -eq 'LogonProcessName'}).'#text';  
        AuthenticationPackageName = [String]($xmlEvent | Where-Object {$_.Name -eq 'AuthenticationPackageName'}).'#text';  
        TimeStamp = [String]($_.TimeCreated);
    };
    $i += 1;
    if (!($i % 1000)) {
        $xmlEvents | Where-Object {$_.TargetUserSid -eq $filterSid | Export-Csv -Delimiter ';' -LiteralPath ".\LogonEvent.csv" -Append;  
        Write-Host "`r$i Events processed - Last Event: $($_.TimeCreated)" -NoNewline;  
        $xmlEvents = @();
    };
};

Dieser Teil:
    $i += 1;
    if (!($i % 1000)) {
        $xmlEvents | Where-Object {$_.TargetUserSid -eq $filterSid | Export-Csv -Delimiter ';' -LiteralPath ".\LogonEvent.csv" -Append;  
        Write-Host "`r$i Events processed - Last Event: $($_.TimeCreated)" -NoNewline;  
        $xmlEvents = @();
    };
war mein versuch das Memoryleak zu stopfen, hat nur nicht funktioniert... Vorher habe ich anstelle des Csv Exports die Object weiter über die Pipe geschoben.

Ich denke, dass das Memoryleak durch das Transposen der Tabelle ausgelöst wird, weiß aber nicht wie ich das beheben soll. Abgesehen davon lastet mein Skript aktuell die CPU zu 100% aus, das lässt sich aber vermutlich nicht vermeiden, oder?

Mit freundlichen Grüßen,
agowa338

Content-ID: 316232

Url: https://administrator.de/forum/powershell-transpose-objects-und-memoryleak-316232.html

Ausgedruckt am: 22.12.2024 um 09:12 Uhr

colinardo
Lösung colinardo 25.09.2016 aktualisiert um 12:42:37 Uhr
Goto Top
Hallo Agowa,
ich würde für sowas die Einträge direkt mit einer XPath Query ausfiltern anstatt sie erst alle mit custom objects in ein Array zu parken was natürlich Zeit kostet und den Speicher unnötig füllt:
Get-WinEvent -FilterXPath "Event[System[(EventID=4624)] and EventData[Data[@Name='TargetUserSid'] = 'S-1-5-32']]" -LogName Security | ?{$_.TimeCreated -gt (get-date).AddDays(-10)}  
oder wenn man die Zeit auch noch in die XPath Query einbauen will um es noch weiter zu optimieren:
$starttime = (Get-date).AddDays(-10).Date.ToString('o')  
Get-WinEvent -FilterXPath "Event[System[EventID=4624 and TimeCreated[@SystemTime >= '$starttime']] and EventData[Data[@Name='TargetUserSid'] = 'S-1-5-32']]" -LogName Security  
Grüße Uwe
agowa338
agowa338 25.09.2016 um 12:40:38 Uhr
Goto Top
Den Parameter FilterXPath kannte ich bis jetzt nicht. Meinst du dass FilterHashtable schuld am Memoryleak ist?
Dein Kommando funktioniert bei mir nicht. Ich erhalte nur:
Get-WinEvent : Die Daten sind unzulässig
In Zeile:1 Zeichen:1
+ Get-WinEvent -FilterXPath "Event[System[(EventID=4624)] and EventData[Data[@Name ...  
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-WinEvent], EventLogInvalidDataException
    + FullyQualifiedErrorId : Die Daten sind unzulässig,Microsoft.PowerShell.Commands.GetWinEventCommand
 

Muss hier nicht noch das Log "Security" angegeben werden?
colinardo
colinardo 25.09.2016 aktualisiert um 12:47:27 Uhr
Goto Top
Zitat von @agowa338:
Muss hier nicht noch das Log "Security" angegeben werden?
Jipp hatte ich oben noch nachkorrigiert, sorry.

Meinst du dass FilterHashtable schuld am Memoryleak ist?
Nein, eher das erstellen der Custom-Objects für alle Einträge und das Filtern erst hinterher.
Custom Objects kosten nunmal einiges an Speicher.

Mit XPath filterst du schon vorher die relevanten Einträge heraus, das reduziert die Datenmenge dann schon erheblich.
agowa338
agowa338 25.09.2016 um 13:01:33 Uhr
Goto Top
Danke, jetzt funktioniert es und es ist wirklich sehr viel schneller.

Aber irgendwas ist da immer noch komisch:
Vorher:
CPU 20%
RAM 0,7 von 4,0 GB

Während der Ausführung:
CPU: 100%
RAM 1,6 von 4,0 GB (peak)

Nachher (powershell ist wieder geschlossen):
CPU 20%
RAM 1,6 von 4,0 GB
colinardo
colinardo 25.09.2016 aktualisiert um 13:15:54 Uhr
Goto Top
Aber irgendwas ist da immer noch komisch:
Naja hier kennt ja keiner deine Umgebung face-confused

Zur Info: Garbage-Collection kannst du auch manuell anstoßen.
[GC]::Collect()

Windows gibt Speicher nicht immer direkt wieder frei sondern markiert diesen und behält Teile im Cache wenn man mit NET-Objekten arbeitet, die Freigabe erfolgt nach einiger Zeit auch automatisch.
agowa338
agowa338 25.09.2016 um 13:40:37 Uhr
Goto Top
Zitat von @colinardo:
Windows gibt Speicher nicht immer direkt wieder frei sondern markiert diesen und behält Teile im Cache wenn man mit NET-Objekten arbeitet, die Freigabe erfolgt nach einiger Zeit auch automatisch.
Das heißt im Umkehrschluss, wenn ich im Test durch "[GC]::Collect()" den Speicher frei bekomme, ist alles gut. Ich muss es also nicht explizit ins Skript schreiben, oder? Habe gelesen, mann soll den Garbage-Collector nicht manuell aufrufen, weil das das Timing stören kann.

Hast du eventuell ach noch einen Tipp für die Transposition, oder geht das nicht besser?
colinardo
colinardo 25.09.2016 aktualisiert um 13:57:21 Uhr
Goto Top
Zitat von @agowa338:
Das heißt im Umkehrschluss, wenn ich im Test durch "[GC]::Collect()" den Speicher frei bekomme, ist alles gut.
Der wird auch automatisch durch Windows wieder aus dem Cache wieder freigegeben, geschieht im Hintergrund sobald ein Programm Speicher anfordert. Der manuelle Aufruf ist hier normalerweise nicht nötig, ich nutze Ihn nur wenn z.B. ein Handle auf eine Datei unbedingt geschlossen sein muss (kann in bestimmten Fällen schon mal vorkommen).
Hast du eventuell ach noch einen Tipp für die Transposition, oder geht das nicht besser?
Was meinst du mit Transposition?
Du könntest natürlich auch ohne Custom-Objects arbeiten bzw. diese nicht in einem Array speichern sondern direkt in der Pipeline weitergeben. Man kanns aber auch übertreiben face-wink

Get-WinEvent ist einfach nicht besonders sparsam und eher langsam im Vergleich zu anderen CMDLets.
agowa338
agowa338 25.09.2016 aktualisiert um 14:13:43 Uhr
Goto Top
Zitat von @colinardo:
Hast du eventuell ach noch einen Tipp für die Transposition, oder geht das nicht besser?
Was meinst du mit Transposition?
Ich bekomme von der Zeile
$xmlEvent = ([XML]$_.ToXml()).Event.EventData.Data;
Ja eine "Tabelle" (Ja, ich weiß sind mehrere Objekte und keine Tabelle oder Matrix) mit den Spalten "Name" und "#text" zurück. In der Spalte "Name" stehen aber die eigentlichen AttributNamen und in "#text" die Werte. Deshalb drehe ich die Tabelle um 90° (Transponiere) damit die Überschriften zu den Werten passen.

Beispiel:
Name   #text
______  ______
IpPort  49000
IpAddress 10.0.0.1
...
Wird zu:
IpPort  IpAddress
______  ______
49000  10.0.0.1
...
colinardo
Lösung colinardo 25.09.2016 aktualisiert um 14:43:50 Uhr
Goto Top
Ach so du meinst "Transponieren", OK. Du kannst die Knoten auch mit $xml.SelectSingleNode() auswählen anstatt sie durch eine erneute Pipeline mit where object zu leiten.
Und speichere wie oben die Objekte nicht in einem Array sondern leite sie direkt durch die Pipeline das spart Speicher.

In deiner Variante legst du erst alle Elemente in einem Array ab um sie hinterher erneut zu verarbeiten, wenn du sie stattdessen via Pipeline direkt an export-csv leitest ist das zwar minimal langsamer aber dafür speichereffizienter als alle Daten erst in einer Variablen abzulegen.

Aber wie gesagt Get-WinEvent ist selber einfach nicht sehr effizient, der Optimierung sind da bei bestimmten Datenmengen Grenzen gesetzt.

Du kannst es mal hiermit versuchen:
$starttime = (Get-date).AddDays(-10).Date.ToString('o')  
Get-WinEvent -FilterXPath "Event[System[EventID=4624 and TimeCreated[@SystemTime >= '$starttime']] and EventData[Data[@Name='TargetUserSid'] = 'S-1-5-32']]" -LogName Security | %{  
    $xml = [xml]$_.toXML()
    $ns = (new-Object System.Xml.XmlNamespaceManager $xml.NameTable)
    $ns.AddNamespace("ns",$xml.DocumentElement.NamespaceURI)  
    [pscustomobject]@{
        TargetUserSid=$xml.SelectSingleNode("ns:Event/ns:EventData/ns:Data[@Name = 'TargetUserSid']",$ns).innerText  
        TargetUsername=$xml.SelectSingleNode("ns:Event/ns:EventData/ns:Data[@Name = 'TargetUserName']",$ns).innerText  
        # usw.
    }
} | export-csv '.\LogonEvent.csv' -Delimiter ";" -NoType -Encoding UTF8  
Felder des customobjects nur zur Demo gekürzt.

So ist das ganze Konstrukt eine einzige Pipeline bei der die Objekte nacheinander direkt in die CSV fließen ohne dauerhaft Speicher zu belegen wie ein Array aus Objekten.
agowa338
agowa338 25.09.2016 um 15:36:17 Uhr
Goto Top
Zitat von @colinardo:
Ach so du meinst "Transponieren", OK. Du kannst die Knoten auch mit $xml.SelectSingleNode() auswählen anstatt sie durch eine erneute Pipeline mit where object zu leiten.
Gut, liest sich schon etwas besser. Ich dachte eigentlich, ob es hierfür nicht etwas in dieser art gibt:
$outputObject = $inputObject | Transpose-Object
Aber anscheinend (noch) nicht.
In deiner Variante legst du erst alle Elemente in einem Array ab um sie hinterher erneut zu verarbeiten, wenn du sie stattdessen via Pipeline direkt an export-csv leitest ist das zwar minimal langsamer aber dafür speichereffizienter als alle Daten erst in einer Variablen abzulegen.
Wieso ist eine Pipe langsamer als in Variable speichern und anschließend durch die Pipe exportieren? Ist ja eigentlich ein schritt mehr...
Aber wie gesagt Get-WinEvent ist selber einfach nicht sehr effizient, der Optimierung sind da bei bestimmten Datenmengen Grenzen gesetzt.
Deshalb will ich das ganze ja optimieren, sonst würde es mich vermutlich nicht weiter stören face-wink
colinardo
Lösung colinardo 25.09.2016 aktualisiert um 17:09:48 Uhr
Goto Top
Zitat von @agowa338:
Aber anscheinend (noch) nicht.
Nicht das ich wüsste. Kann man sich aber selbst bauen face-smile, für das obige Beispiel
$obj = [pscustomobject]@{}
$xml.Event.EventData.Data | %{$obj | Add-Member -Membertype NoteProperty -Name $_.Name -Value $_.'#text'}  
Wieso ist eine Pipe langsamer als in Variable speichern und anschließend durch die Pipe exportieren? Ist ja eigentlich ein schritt mehr...
Das liegt an der Funktionsweise der Pipeline. Ein Objekt wandert vom ersten zum letzten CMDLet, bevor das nächste Objekt dran ist (bei manchen CMDLets lässt sich auch festlegen wie viele Objekte gleichzeitig über die Pipeline wandern). Es kommt hier auch auf die Datenmenge an. Eine Pipeline spart bei einer enormen Anzahl an Objekten Speicher wohingegen das Speichern in einer Variablen und das Verarbeiten bei einer überschaubaren Anzahl an Objekten und Verarbeitung per Foreach ohne Pipeline mit
foreach($itm in $items){}
schneller ist weil die Objekte alle direkt aus dem Speicher verarbeitet werden, aber im Gegenzug mehr Speicher beansprucht weil alle Objekte erst in einer Variablen zwischengespeichert werden.

Hier gilt es also abzuwägen ob Speicher sparen Priorität hat oder das letzte Quäntchen Performance auf Kosten einer höheren Speicherauslastung.

https://blogs.technet.microsoft.com/heyscriptingguy/2009/07/22/hey-scrip ...

Möchtest du noch mehr Kontrolle über Speicherauslastung etc. und das letzte Quentchen Performance würde ich dir empfehlen das ganze direkt mit einer nativen App c#/c++ umzusetzen.

Grüße Uwe