lupora
Goto Top

Powershell Copy-Item Script schneller machen

Hallo zusammen,

ich habe ein Powershell Script gebaut was Ordner basierend auf einer Liste von A nach B kopiert.
Teilweise kopiert das Script mehrere Millionen Dateien. Mir kommt der Kopiervorgang recht lang vor verglichen mit einem regulären Copy+Paste.

Hat jemand eine Idee wie man das Script performanter machen könnte?
Das Script holt sich den Dateinamen eines Ordners aus einer Liste und kopiert dann den gesamten Ordner an einen Zielort.
Macht evtl. der Recurse Parameter das ganze so langsam?

Jemand Ideen? face-smile

# Dieses Skript kopiert (!) Ordner basierend auf einer Liste von Quellort zu Zielort.

# Variablen

[STRING] $quelle = "D:\test\root"  

[STRING] $ziel = "D:\test\target"  

[STRING] $liste = "D:\liste.txt"  


# Logik

$counter = 0
Get-Content $liste | 
         Foreach-Object { 
                 copy-item -path "$quelle\$_" -destination "$ziel\$_" -Force -Recurse -verbose  
				
         }


# Piepton Benachrichtigung wenn fertig
[console]::beep(1000,700)
[console]::beep(1000,700)
[console]::beep(2000,1500)


pause

Content-ID: 627793

Url: https://administrator.de/contentid/627793

Ausgedruckt am: 22.11.2024 um 00:11 Uhr

emeriks
emeriks 03.12.2020 um 14:02:50 Uhr
Goto Top
Hi,
meistens dauert die Kopie sehr vieler kleiner Dateien länger, als die von wenigen großen, auch wenn sie jeweils in Summe die selbe Anzahl Bytes haben.

Die könntest es mit Robocopy und Schalter /MT experimentieren.
Oder mehrere Kopie-Vorgänge gleichzeitig starten.
Das hängst jetzt auch davon ab, was Quelle und Ziel für Datenträger sind.

E.
Doskias
Doskias 03.12.2020 um 14:06:48 Uhr
Goto Top
Zitat von @emeriks:

Hi,
meistens dauert die Kopie sehr vieler kleiner Dateien länger, als die von wenigen großen, auch wenn sie jeweils in Summe die selbe Anzahl Bytes haben.

Da hat emeriks mal wieder recht. face-smile Alternative wären mit compress-archive und expand-archive noch zwei Powershell befehle in Reichweite, die viele Dateien zu einer Zusammenpacken. ;)

Also Liste der Dateien nehmen, ein Zip-erstellen, rüber kopieren, entpacken. Ob das allerdings wirklich schneller als der reine Kopiervorgang ist, kann ich nicht abschätzen
emeriks
emeriks 03.12.2020 um 14:10:46 Uhr
Goto Top
Zitat von @Doskias:
Also Liste der Dateien nehmen, ein Zip-erstellen, rüber kopieren, entpacken. Ob das allerdings wirklich schneller als der reine Kopiervorgang ist, kann ich nicht abschätzen
Max. dann, wenn das Entpacken dann auf dem Zielrechner gestartet würde. Sonst macht das keinen Sinn.
Doskias
Doskias 03.12.2020 um 14:18:43 Uhr
Goto Top
das ist richtig. dafür gibt's ja dann aber invoke-command face-smile
erikro
Lösung erikro 03.12.2020 um 19:22:49 Uhr
Goto Top
Moin,

ohja. Meide die Pipe auf foreach-object. So geht es (genug freien Arbeitsspeicher vorausgesetzt) wahrscheinlich erheblich schneller:

$to_copy = get-content $liste
foreach($item in $to_copy) {
    copy-item -path "$quelle\$item" -destination "$ziel\$item" -Force -Recurse -verbose  
}

Liebe Grüße

Erik
emeriks
emeriks 04.12.2020 um 08:04:50 Uhr
Goto Top
Zitat von @erikro:
ohja. Meide die Pipe auf foreach-object. So geht es (genug freien Arbeitsspeicher vorausgesetzt) wahrscheinlich erheblich schneller:
Kannst Du das bitte mal begründen?
Mal abgesehen davon, dass hier doch das Gros der Dauer durch die Kopiervorgänge verursacht werden dürfte und nicht durch die Verkettung der Ausgaben oder die Enumeration der Elemente.
colinardo
colinardo 04.12.2020 aktualisiert um 11:32:24 Uhr
Goto Top
Servus,
das ist eine schöne Aufgabe für Parallelisierung, natürlich immer nur so weit es Quell- und Zieldatenträger sowie Netzwerkbandbreite es auch zulassen. Threadanzahl lässt sich über den Parameter -ThrottleLimit weiter unten steuern.
$quelle = 'D:\test\root'  
$ziel = 'D:\test\target'  
$liste = 'D:\liste.txt'  

function %% {
    [CmdletBinding()]
    param(
        [parameter(mandatory=$true,ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][object[]]$InputObject,
        [parameter(mandatory=$true)][ValidateNotNullOrEmpty()][scriptblock]$Process,
        [parameter(mandatory=$false)][ValidateNotNullOrEmpty()][int]$ThrottleLimit = [System.Environment]::ProcessorCount,
        [parameter(mandatory=$false)][switch]$showprogress,
        [parameter(mandatory=$false)][ValidateNotNullOrEmpty()][hashtable]$params
    )
    begin{
        $rspool = [runspacefactory]::CreateRunspacePool(1,$ThrottleLimit)
        $rspool.ApartmentState = 'STA'  
        $rspool.Open()
        $jobs = New-Object System.Collections.ArrayList
        $objects = New-Object System.Collections.ArrayList
    }
    process{
        $InputObject | %{[void]$objects.Add($_)}
    }
    end{
        foreach ($obj in $objects){
            $ps = [Powershell]::Create()
            $ps.RunspacePool = $rspool
            [void]$ps.AddScript($Process).AddParameters(@{'_'=$obj;params=$params})  
            $job = $ps.BeginInvoke()
            [void]$jobs.Add(([pscustomobject]@{Handle = $job; Powershell = $ps}))
        }

        write-verbose "Waiting for all jobs to complete."  
        while(($jobs | ?{!$_.Handle.IsCompleted})){
            if ($showprogress.IsPresent){
                $completed = ($jobs | ?{$_.Handle.IsCompleted}).Count
                Write-Progress -Activity $PSCmdlet.MyInvocation.InvocationName -Status "Total of $($objects.Count) objects." -PercentComplete (($completed / $objects.Count) * 100) -CurrentOperation "Completed $completed of  $($objects.Count)."  
            }
        }
        # get results of jobs
        $results = $jobs | %{
            $_.Powershell.EndInvoke($_.handle)
            $_.Powershell.Dispose()
        }
        # cleanup
        $rspool.Close();$rspool.Dispose()
        $results
    }
}

gc $liste | ?{Test-Path "$quelle\$_"} | %% -ThrottleLimit 8 -showprogress -params @{quelle=$quelle;ziel=$ziel} -Process {  
    param($_,$params)
    copy-item -LiteralPath "$($params.Quelle)\$_" -Destination $params.ziel -Recurse -Force  
}

Das Liste kann man natürlich alternativ auch an Robocopy verfüttern und es mit mehreren Threads laufen lassen (unterstützt es ja nativ via Parameter).

Grüße Uwe
erikro
erikro 04.12.2020 um 16:46:58 Uhr
Goto Top
Moin,

Zitat von @emeriks:

Zitat von @erikro:
ohja. Meide die Pipe auf foreach-object. So geht es (genug freien Arbeitsspeicher vorausgesetzt) wahrscheinlich erheblich schneller:
Kannst Du das bitte mal begründen?

Gerne. Guckst Du hier:
https://devblogs.microsoft.com/scripting/getting-to-know-foreach-and-for ...

Liebe Grüße

Erik
emeriks
emeriks 07.12.2020 um 08:17:08 Uhr
Goto Top
Zitat von @erikro:
Gerne. Guckst Du hier:
Danke!
Lupora
Lupora 20.12.2020 um 15:36:08 Uhr
Goto Top
Hallo Uwe,

danke für deine ausführliche Erklärung!

Eine Frage: Ich habe herausgefunden das es mit Powershell 7.x eine ERweiterung für das for-each CMDLEt gibt.
Dort kann man ejtzt den Parameter -parallel mitgeben.

Wäre das dasselbe wie dein Script?
colinardo
colinardo 20.12.2020 aktualisiert um 15:52:04 Uhr
Goto Top
Servus
Zitat von @Lupora:
Eine Frage: Ich habe herausgefunden das es mit Powershell 7.x eine ERweiterung für das for-each CMDLEt gibt.
Dort kann man ejtzt den Parameter -parallel mitgeben.

Wäre das dasselbe wie dein Script?
Ja, mein obiges Skript ist nur eine nachgebaute leicht abgewandelte Variante die auch in den älteren PS Varianten läuft, beide nutzen aber die Runspace Pools für das Multithreading.
Lupora
Lupora 20.12.2020 um 19:21:42 Uhr
Goto Top
Spannend, danke für die Info!

Ich habe aktuell ein Skript gebaut was ein Verzeichnis rekursiv interriert und bassierend auf dem Dateinamen einen Ordner anlegt und dort Dateien hinkopiert. Wenn ich das Script startet kopiert es circa 12 Ordner pro Minute vollständig. (Hinweis: Alle Ordner haben gleichviel Dateien die alle gleichgroß sind).
Nach mehreren Stunden wird das Skript immer langsamer und kopiert teilweise nur noch 3-4 Ordner pro Minute.
Bei dem Script wollte ich jetzt mit "-parallel" rumprobieren ob ich es beschleunigen kann.

Siehst du auf Anhieb warum das Script so inperformant ist? (Kopiervorgang geht von SSD auf SSD).
[STRING] $quelle = "E:\X"  
[STRING] $ziel = "M:\X"  

Get-Childitem -Path $quelle -File -Recurse -include "*.jpg" |  

Foreach-Object {

   # Make the world easier
   $filename = $_.name
   $filepath = $_
   $gesplitteterDateiname = $filename.Split("_")[3]  
   
   # Prüfen ob Ordner bereits erstellt und ggf. Ordner erstellen
   if(!(Test-Path -path ($ziel+"\$gesplitteterDateiname")))    
    {  
        new-item ($ziel+"\$gesplitteterDateiname") -type Directory  
        $counterfolder++         
    }
    else 
{ 
#Write-Host "Ordner bereits erstellt" 
}

   # Datei kopieren an Zielpfad
   copy-item $filepath -destination ($ziel+"\$gesplitteterDateiname") -verbose  

   }
colinardo
colinardo 20.12.2020 aktualisiert um 20:25:24 Uhr
Goto Top
Ja, erstens entferne die überflüssigen Variablendeklarationen in der Schleife die Kosten viel Zeit, zweitens lies den Hinweis der Kollegen zu Foreach-Object im Vergleich zu Foreach wenn es nicht mit -parallel Parameter ausgeführt wird. Foreach-Object ohne -parallel spart zwar Speicher ist dafür aber eben insgesamt langsamer, auch wenn es im Vergleich zu einem Foreach Konstrukt schneller anfängt zu verarbeiten, weil bei letzterem erst mal die gesamte Struktur durchlaufen wird und erst anschließend über ein Array itteriert wird.
Lupora
Lupora 20.12.2020 um 21:09:03 Uhr
Goto Top
Danke, werde ich versuchen umsetzen. Was ist aber noch nicht verstehe ist warum das Script am Anfang so mega performant läuft.
Und gegen Ende immer langsamer wird :/

Müsste es dann nicht durhcgehend langsam sein?
colinardo
colinardo 20.12.2020 aktualisiert um 22:15:43 Uhr
Goto Top
SSDs haben meist einen integrierten DRAM Cache der für eine gewisse Datenmenge zu Beginn eine konstant hohe Schreibrate gewährleistet, wenn der aber voll ist fällt die Schreibrate meist stark ab, das kann eine Möglichkeit sein. Die zweite ist das sich durch das massenhafte erneute Zuweisen der Variablen der Speicher füllt und die Garbage-Collection die Verzögerung verursacht. Das alles kannst du im Performance Monitor überprüfen.