Bagaimana cara menjalankan skrip PowerShell saya secara paralel tanpa menggunakan Jobs?

29

Jika saya memiliki skrip yang harus saya jalankan terhadap banyak komputer, atau dengan beberapa argumen yang berbeda, bagaimana saya bisa menjalankannya secara paralel, tanpa harus mengeluarkan ongkos mengeluarkan PSJobStart-Job baru ?

Sebagai contoh, saya ingin menyinkronkan kembali waktu pada semua anggota domain , seperti:

$computers = Get-ADComputer -filter * |Select-Object -ExpandProperty dnsHostName
$creds = Get-Credential domain\user
foreach($computer in $computers)
{
    $session = New-PSSession -ComputerName $computer -Credential $creds
    Invoke-Command -Session $session -ScriptBlock { w32tm /resync /nowait /rediscover }
}

Tapi saya tidak ingin menunggu setiap PSSession untuk terhubung dan menjalankan perintah. Bagaimana ini bisa dilakukan secara paralel, tanpa Jobs?

Mathias R. Jessen
sumber

Jawaban:

51

Pembaruan - Sementara jawaban ini menjelaskan proses dan mekanisme runspaces PowerShell dan bagaimana mereka dapat membantu Anda menangani banyak beban kerja non-sekuensial, sesama pecinta PowerShell Warren 'Cookie Monster' F telah bekerja lebih keras dan memasukkan konsep-konsep yang sama ini ke dalam satu alat. disebut - itu melakukan apa yang saya jelaskan di bawah ini, dan dia telah mengembangkannya dengan sakelar opsional untuk logging dan menyiapkan sesi status termasuk modul yang diimpor, hal-hal yang sangat keren - Saya sangat menyarankan Anda memeriksanya sebelum membangun Anda sendiri solusi mengkilap!Invoke-Parallel


Dengan eksekusi Paralel Runspace:

Mengurangi waktu tunggu yang tak terhindarkan

Dalam kasus spesifik asli, yang dieksekusi yang dijalankan memiliki /nowaitopsi yang mencegah pemblokiran utas pemanggilan sementara pekerjaan (dalam hal ini, sinkronisasi ulang waktu) selesai dengan sendirinya.

Ini sangat mengurangi waktu eksekusi keseluruhan dari perspektif emiten, tetapi menghubungkan ke setiap mesin masih dilakukan secara berurutan. Menghubungkan ke ribuan klien secara berurutan mungkin memakan waktu lama tergantung pada jumlah mesin yang karena satu dan lain hal tidak dapat diakses, karena akumulasi waktu tunggu habis.

Untuk menyiasati harus mengantri semua koneksi berikutnya dalam kasus satu atau beberapa timeout berturut-turut, kita dapat mengirim pekerjaan menghubungkan dan menjalankan perintah untuk memisahkan PowerShell Runspaces, mengeksekusi secara paralel.

Apa itu Runspace?

Sebuah runspace adalah wadah virtual di mana mengeksekusi kode PowerShell Anda, dan mewakili / memegang Lingkungan dari perspektif PowerShell pernyataan / perintah.

Secara umum, 1 Runspace = 1 utas eksekusi, jadi yang kita butuhkan untuk "multi-utas" skrip PowerShell kami adalah kumpulan Runspace yang kemudian dapat dieksekusi secara paralel.

Seperti masalah aslinya, tugas menjalankan perintah beberapa runspace dapat dipecah menjadi:

  1. Membuat RunspacePool
  2. Menetapkan skrip PowerShell atau bagian yang setara dari kode yang dapat dieksekusi ke RunspacePool
  3. Meminta kode secara tidak sinkron (mis. Tidak harus menunggu kode kembali)

Templat RunspacePool

PowerShell memiliki akselerator tipe yang disebut [RunspaceFactory]yang akan membantu kita dalam pembuatan komponen runspace - mari kita mulai bekerja

1. Buat RunspacePool dan Open()itu:

$RunspacePool = [runspacefactory]::CreateRunspacePool(1,8)
$RunspacePool.Open()

Dua argumen yang diteruskan ke CreateRunspacePool(), 1dan 8adalah jumlah minimum dan maksimum runspace diizinkan untuk dieksekusi pada waktu tertentu, memberi kita tingkat paralelisme maksimum 8 yang efektif .

2. Buat sebuah instance dari PowerShell, lampirkan beberapa kode yang dapat dieksekusi untuk itu dan tetapkan ke RunspacePool kami:

Contoh PowerShell tidak sama dengan powershell.exeproses (yang sebenarnya adalah aplikasi Host), tetapi objek runtime internal yang mewakili kode PowerShell untuk dieksekusi. Kita bisa menggunakan [powershell]akselerator tipe untuk membuat contoh PowerShell baru di dalam 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. Aktifkan instance PowerShell secara asinkron menggunakan APM:

Menggunakan apa yang dikenal dalam terminologi pengembangan .NET sebagai Model Pemrograman Asynchronous , kita dapat membagi permohonan perintah menjadi Beginmetode, untuk memberikan "lampu hijau" untuk mengeksekusi kode, dan Endmetode untuk mengumpulkan hasil. Karena kami dalam hal ini tidak benar-benar tertarik pada umpan balik (kami tidak menunggu output dari w32tmanyways), kami dapat membuat karena dengan hanya memanggil metode pertama

$PSinstance.BeginInvoke()

Membungkusnya dalam RunspacePool

Menggunakan teknik di atas, kita dapat membungkus iterasi berurutan untuk membuat koneksi baru dan menjalankan perintah jarak jauh dalam aliran eksekusi paralel:

$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()
}

Dengan asumsi bahwa CPU memiliki kapasitas untuk mengeksekusi semua 8 runspace sekaligus, kita harus dapat melihat bahwa waktu eksekusi sangat berkurang, tetapi dengan biaya keterbacaan skrip karena metode yang agak "canggih" yang digunakan.


Menentukan tingkat parallism yang optimal:

Kita dapat dengan mudah membuat RunspacePool yang memungkinkan untuk mengeksekusi 100 runspace secara bersamaan:

[runspacefactory]::CreateRunspacePool(1,100)

Tetapi pada akhirnya, semuanya tergantung pada berapa banyak unit eksekusi yang dapat ditangani oleh CPU lokal kami. Dengan kata lain, selama kode Anda dieksekusi, tidak masuk akal untuk mengizinkan lebih banyak runspaces daripada yang Anda miliki dengan prosesor logis untuk mengirimkan eksekusi kode.

Berkat WMI, ambang ini cukup mudah untuk ditentukan:

$NumberOfLogicalProcessor = (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors
[runspacefactory]::CreateRunspacePool(1,$NumberOfLogicalProcessors)

Jika, di sisi lain, kode yang Anda jalankan sendiri memerlukan banyak waktu tunggu karena faktor eksternal seperti latensi jaringan, Anda masih dapat memperoleh manfaat dari menjalankan runspace yang lebih simultan daripada yang Anda miliki dengan prosesor logis, jadi Anda mungkin ingin menguji dari kisaran kemungkinan runspaces maksimum untuk menemukan titik impas :

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
}
Mathias R. Jessen
sumber
4
Jika pekerjaan menunggu di jaringan, misalnya Anda menjalankan perintah PowerShell di komputer jarak jauh, Anda bisa dengan mudah melampaui jumlah prosesor logis sebelum Anda menekan hambatan CPU.
Michael Hampton
Ya itu benar. Mengubahnya sedikit dan memberikan contoh untuk pengujian
Mathias R. Jessen
Bagaimana memastikan semua pekerjaan selesai di akhir? (Mungkin perlu sesuatu setelah semua blok skrip selesai)
sjzls
@NickW Pertanyaan bagus. Saya akan melakukan tindak lanjut untuk melacak pekerjaan dan "memanen" output potensial hari ini, tetap disini
Mathias R. Jessen
1
@ MathiasR.Jessen Jawaban yang sangat bagus! Menantikan pembaruan.
Signal15
5

Menambah diskusi ini, yang hilang adalah kolektor untuk menyimpan data yang dibuat dari runspace, dan variabel untuk memeriksa status runspace, yaitu apakah sudah selesai atau tidak.

#Add an collector object that will store the data
$Object = New-Object 'System.Management.Automation.PSDataCollection[psobject]'

#Create a variable to check the status
$Handle = $PSinstance.BeginInvoke($Object,$Object)

#So if you want to check the status simply type:
$Handle

#If you want to see the data collected, type:
$Object
Nate Stone
sumber
3

Lihat PoshRSJob . Ini menyediakan fungsi yang sama / mirip dengan fungsi * -Job asli, tetapi menggunakan Runspace yang cenderung jauh lebih cepat dan lebih responsif daripada pekerjaan Powershell standar.

Rosco
sumber
1

@ mathias-r-jessen memiliki jawaban yang bagus meskipun ada detail yang ingin saya tambahkan.

Max Threads

Secara teori, utas harus dibatasi oleh jumlah pemroses sistem. Namun, saat menguji AsyncTcpScan saya mencapai kinerja yang jauh lebih baik dengan memilih nilai yang jauh lebih besar MaxThreads. Jadi mengapa modul itu memiliki -MaxThreadsparameter input. Perlu diingat bahwa mengalokasikan terlalu banyak utas akan menghambat kinerja.

Pengembalian Data

Mendapatkan data kembali dari yang ScriptBlockrumit. Saya telah memperbarui kode OP dan mengintegrasikannya ke dalam apa yang digunakan untuk AsyncTcpScan .

PERINGATAN: Saya tidak dapat menguji kode berikut. Saya membuat beberapa perubahan pada skrip OP berdasarkan pengalaman saya bekerja dengan cmdlets Direktori Aktif.

# Script to run in each thread.
[System.Management.Automation.ScriptBlock]$ScriptBlock = {

    $result = New-Object PSObject -Property @{ 'Computer' = $args[0];
                                               'Success'  = $false; }

    try {
            $session = New-PSSession -ComputerName $args[0] -Credential $args[1]
            Invoke-Command -Session $session -ScriptBlock { w32tm /resync /nowait /rediscover }
            Disconnect-PSSession -Session $session
            $result.Success = $true
    } catch {

    }

    return $result

} # End Scriptblock

function Invoke-AsyncJob
{
    [CmdletBinding()]
    param(
        [parameter(Mandatory=$true)]
        [System.Management.Automation.PSCredential]
        # Credential object to login to remote systems
        $Credentials
    )

    Import-Module ActiveDirectory

    $Results = @()

    $AllJobs = New-Object System.Collections.ArrayList

    $AllDomainComputers = Get-ADComputer -Filter * -Properties dnsHostName

    $HostRunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(2,10,$Host)

    $HostRunspacePool.Open()

    foreach($DomainComputer in $AllDomainComputers)
    {
        $asyncJob = [System.Management.Automation.PowerShell]::Create().AddScript($ScriptBlock).AddParameters($($($DomainComputer.dnsName),$Credentials))

        $asyncJob.RunspacePool = $HostRunspacePool

        $asyncJobObj = @{ JobHandle   = $asyncJob;
                          AsyncHandle = $asyncJob.BeginInvoke()    }

        $AllJobs.Add($asyncJobObj) | Out-Null
    }

    $ProcessingJobs = $true

    Do {

        $CompletedJobs = $AllJobs | Where-Object { $_.AsyncHandle.IsCompleted }

        if($null -ne $CompletedJobs)
        {
            foreach($job in $CompletedJobs)
            {
                $result = $job.JobHandle.EndInvoke($job.AsyncHandle)

                if($null -ne $result)
                {
                    $Results += $result
                }

                $job.JobHandle.Dispose()

                $AllJobs.Remove($job)
            } 

        } else {

            if($AllJobs.Count -eq 0)
            {
                $ProcessingJobs = $false

            } else {

                Start-Sleep -Milliseconds 500
            }
        }

    } While ($ProcessingJobs)

    $HostRunspacePool.Close()
    $HostRunspacePool.Dispose()

    return $Results

} # End function Invoke-AsyncJob
phbits
sumber