Aktualizacja - Podczas gdy ta odpowiedź wyjaśnia proces i mechanikę przestrzeni roboczych PowerShell oraz sposób, w jaki mogą one pomóc w wielowątkowym niesekwencyjnym obciążeniom, kolega z PowerShell, Warren „Cookie Monster” F , posunął się o krok dalej i włączył te same koncepcje w jedno narzędzie o nazwie - robi to, co opisuję poniżej, i od tego czasu rozszerzył go o opcjonalne przełączniki do rejestrowania i stanu przygotowanej sesji, w tym zaimportowane moduły, naprawdę fajne rzeczy - zdecydowanie zalecamy sprawdzenie tego przed zbudowaniem własnego błyszczącego rozwiązania!Invoke-Parallel
Z równoległym wykonywaniem Runspace:
Skrócenie nieuniknionego czasu oczekiwania
W oryginalnym konkretnym przypadku wywoływany plik wykonywalny ma /nowait
opcję, która zapobiega blokowaniu wątku wywołującego, gdy zadanie (w tym przypadku synchronizacja czasu) kończy się samo.
To znacznie skraca całkowity czas wykonania z punktu widzenia emitentów, ale połączenie z każdą maszyną wciąż odbywa się w kolejności sekwencyjnej. Łączenie się z tysiącami klientów w sekwencji może zająć dużo czasu w zależności od liczby maszyn, które z tego lub innego powodu są niedostępne z powodu kumulacji czasu oczekiwania.
Aby obejść się w kolejce wszystkich kolejnych połączeń w przypadku pojedynczego lub kilku kolejnych przekroczeń czasu, możemy wysłać zadanie łączenia i wywoływania poleceń w celu oddzielenia obszarów roboczych programu PowerShell, wykonując je równolegle.
Co to jest Runspace?
Obszar roboczy to wirtualny kontener, w którym wykonuje się kod programu PowerShell, i reprezentuje / przechowuje środowisko z perspektywy instrukcji / polecenia PowerShell.
W szerokim ujęciu, 1 Runspace = 1 wątek wykonania, więc wszystko, czego potrzebujemy do „wielowątkowego” naszego skryptu PowerShell, to zbiór Runspaces, które mogą z kolei być wykonywane równolegle.
Podobnie jak w przypadku pierwotnego problemu, zadanie wywoływania poleceń wielu obszarów roboczych można podzielić na:
- Tworzenie RunspacePool
- Przypisywanie skryptu PowerShell lub równoważnego fragmentu kodu wykonywalnego do RunspacePool
- Wywołaj kod asynchronicznie (tzn. Nie musisz czekać na zwrócenie kodu)
Szablon RunspacePool
PowerShell ma akcelerator typu o nazwie [RunspaceFactory]
, który pomoże nam w tworzeniu komponentów obszaru roboczego - uruchommy go
1. Utwórz RunspacePool i Open()
to:
$RunspacePool = [runspacefactory]::CreateRunspacePool(1,8)
$RunspacePool.Open()
Dwa argumenty przekazane do CreateRunspacePool()
, 1
i 8
to minimalna i maksymalna liczba obszarów roboczych, które mogą zostać wykonane w danym momencie, co daje nam efektywny maksymalny stopień równoległości równy 8.
2. Utwórz instancję PowerShell, dołącz do niej kod wykonywalny i przypisz go do naszego RunspacePool:
Wystąpienie programu PowerShell nie jest tym samym co powershell.exe
proces (który tak naprawdę jest aplikacją hosta), ale wewnętrzny obiekt wykonawczy reprezentujący kod programu PowerShell do wykonania. Możemy użyć [powershell]
akceleratora typu, aby utworzyć nową instancję PowerShell w PowerShell:
$Code = {
param($Credentials,$ComputerName)
$session = New-PSSession -ComputerName $ComputerName -Credential $Credentials
Invoke-Command -Session $session -ScriptBlock {w32tm /resync /nowait /rediscover}
}
$PSinstance = [powershell]::Create().AddScript($Code).AddArgument($creds).AddArgument("computer1.domain.tld")
$PSinstance.RunspacePool = $RunspacePool
3. Wywołaj instancję PowerShell asynchronicznie przy użyciu APM:
Używając tego, co znane jest w terminologii programistycznej .NET jako Asynchronous Programming Model , możemy podzielić wywołanie polecenia na Begin
metodę, aby dać „zielone światło” do wykonania kodu i End
metodę zbierania wyników. Ponieważ w tym przypadku tak naprawdę nie jesteśmy zainteresowani żadnymi opiniami (i tak nie czekamy na dane wyjściowe w32tm
), możemy to zrobić, po prostu wywołując pierwszą metodę
$PSinstance.BeginInvoke()
Podsumowanie w RunspacePool
Używając powyższej techniki, możemy owinąć sekwencyjne iteracje tworzenia nowych połączeń i wywoływania polecenia zdalnego w równoległym przepływie wykonania:
$ComputerNames = Get-ADComputer -filter * -Properties dnsHostName |select -Expand dnsHostName
$Code = {
param($Credentials,$ComputerName)
$session = New-PSSession -ComputerName $ComputerName -Credential $Credentials
Invoke-Command -Session $session -ScriptBlock {w32tm /resync /nowait /rediscover}
}
$creds = Get-Credential domain\user
$rsPool = [runspacefactory]::CreateRunspacePool(1,8)
$rsPool.Open()
foreach($ComputerName in $ComputerNames)
{
$PSinstance = [powershell]::Create().AddScript($Code).AddArgument($creds).AddArgument($ComputerName)
$PSinstance.RunspacePool = $rsPool
$PSinstance.BeginInvoke()
}
Zakładając, że procesor ma zdolność do wykonywania wszystkich 8 przestrzeni roboczych jednocześnie, powinniśmy być w stanie zauważyć, że czas wykonania jest znacznie skrócony, ale kosztem czytelności skryptu ze względu na raczej „zaawansowane” metody.
Określenie optymalnego stopnia paraliżu:
Możemy łatwo stworzyć RunspacePool, który pozwala na wykonanie 100 przestrzeni roboczych jednocześnie:
[runspacefactory]::CreateRunspacePool(1,100)
Ale pod koniec dnia wszystko sprowadza się do liczby jednostek wykonawczych, które może obsłużyć nasz lokalny procesor. Innymi słowy, dopóki twój kod jest wykonywany, nie ma sensu zezwalać na więcej przestrzeni roboczych niż na procesory logiczne, do których można wysłać wykonanie kodu.
Dzięki WMI ten próg jest dość łatwy do ustalenia:
$NumberOfLogicalProcessor = (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors
[runspacefactory]::CreateRunspacePool(1,$NumberOfLogicalProcessors)
Jeśli z drugiej strony sam kod, który sam wykonujesz, wymaga dużo czasu oczekiwania z powodu czynników zewnętrznych, takich jak opóźnienie sieci, nadal możesz korzystać z większej liczby jednoczesnych przestrzeni roboczych niż z procesorów logicznych, więc prawdopodobnie powinieneś przetestować zakresu możliwych maksymalnych przestrzeni roboczych w celu znalezienia progu rentowności :
foreach($n in ($NumberOfLogicalProcessors..($NumberOfLogicalProcessors*3)))
{
Write-Host "$n: " -NoNewLine
(Measure-Command {
$Computers = Get-ADComputer -filter * -Properties dnsHostName |select -Expand dnsHostName -First 100
...
[runspacefactory]::CreateRunspacePool(1,$n)
...
}).TotalSeconds
}