Làm cách nào để chạy song song các tập lệnh PowerShell mà không sử dụng Jobs?


29

Nếu tôi có một tập lệnh mà tôi cần chạy trên nhiều máy tính hoặc với nhiều đối số khác nhau, làm thế nào tôi có thể thực thi nó song song mà không phải chịu chi phí phát sinh PSJobStart-Job mới ?

Ví dụ: tôi muốn đồng bộ hóa lại thời gian trên tất cả các thành viên tên miền , như vậy:

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

Nhưng tôi không muốn đợi mỗi PSSession kết nối và gọi lệnh. Làm thế nào điều này có thể được thực hiện song song, không có Jobs?

Câu trả lời:


51

Cập nhật - Trong khi câu trả lời này giải thích quy trình và cơ chế của không gian chạy PowerShell và cách chúng có thể giúp bạn khối lượng công việc không tuần tự đa luồng, đồng nghiệp PowerShell aficionado Warren 'Cookie Monster' F đã đi xa hơn và kết hợp các khái niệm tương tự này vào một công cụ duy nhất được gọi - nó thực hiện những gì tôi mô tả bên dưới, và anh ấy đã mở rộng nó với các công tắc tùy chọn để ghi nhật ký và trạng thái phiên chuẩn bị bao gồm các mô-đun nhập khẩu, công cụ thực sự tuyệt vời - Tôi thực sự khuyên bạn nên kiểm tra trước khi xây dựng giải pháp sáng bóng của riêng bạn!Invoke-Parallel


Với thực thi Parallel Runspace:

Giảm thời gian chờ đợi không thể bỏ qua

Trong trường hợp cụ thể ban đầu, lệnh được thực thi có một /nowaittùy chọn ngăn chặn luồng gọi trong khi công việc (trong trường hợp này, đồng bộ hóa lại thời gian) tự kết thúc.

Điều này giúp giảm đáng kể thời gian thực hiện tổng thể từ góc độ nhà phát hành, nhưng việc kết nối với từng máy vẫn được thực hiện theo thứ tự. Việc kết nối với hàng ngàn khách hàng theo trình tự có thể mất nhiều thời gian tùy thuộc vào số lượng máy vì lý do này hay lý do khác không thể truy cập được, do sự tích lũy của thời gian chờ.

Để giải quyết việc phải xếp hàng tất cả các kết nối tiếp theo trong trường hợp một hoặc một vài lần hết thời gian liên tiếp, chúng ta có thể gửi công việc kết nối và gọi các lệnh để phân tách các không gian PowerShell, thực thi song song.

Runspace là gì?

Một Runspace là container ảo trong đó mã thực thi PowerShell của bạn, và đại diện / giữ môi trường từ quan điểm của một PowerShell tuyên bố / lệnh.

Theo nghĩa rộng, 1 Runspace = 1 luồng thực thi, vì vậy tất cả những gì chúng ta cần để "đa luồng" tập lệnh PowerShell của chúng ta là một tập hợp các Không gian chạy có thể lần lượt thực thi song song.

Giống như vấn đề ban đầu, công việc gọi các lệnh nhiều không gian có thể được chia thành:

  1. Tạo một RunspacePool
  2. Chỉ định tập lệnh PowerShell hoặc một đoạn mã thực thi tương đương cho RunspacePool
  3. Gọi mã không đồng bộ (nghĩa là không phải đợi mã trở lại)

Mẫu RunspacePool

PowerShell có một trình tăng tốc kiểu được gọi là [RunspaceFactory]sẽ hỗ trợ chúng ta trong việc tạo ra các thành phần runspace - hãy để nó hoạt động

1. Tạo một RunspacePool và Open()nó:

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

Hai đối số truyền cho CreateRunspacePool(), 18là mức tối thiểu và số lượng tối đa runspaces phép thực hiện tại bất kỳ thời điểm nào, cho chúng ta một cách hiệu quả tối đa mức độ song song của 8.

2. Tạo một phiên bản của PowerShell, đính kèm một số mã thực thi vào nó và gán nó cho RunspacePool của chúng tôi:

Một phiên bản của PowerShell không giống như powershell.exequy trình (thực sự là một ứng dụng Máy chủ), nhưng là một đối tượng thời gian chạy bên trong đại diện cho mã PowerShell để thực thi. Chúng tôi có thể sử dụng trình [powershell]tăng tốc loại để tạo phiên bản PowerShell mới trong 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. Gọi đối tượng PowerShell không đồng bộ bằng APM:

Sử dụng thuật ngữ được biết đến trong thuật ngữ phát triển .NET làm Mô hình lập trình không đồng bộ , chúng ta có thể chia lời gọi của một lệnh thành một Beginphương thức, để đưa ra "đèn xanh" để thực thi mã và một Endphương thức để thu thập kết quả. Vì trong trường hợp này chúng tôi không thực sự quan tâm đến bất kỳ phản hồi nào (chúng tôi không chờ đầu ra từ mọi cách w32tm), chúng tôi có thể thực hiện bằng cách chỉ cần gọi phương thức đầu tiên

$PSinstance.BeginInvoke()

Gói nó trong RunspacePool

Sử dụng kỹ thuật trên, chúng ta có thể gói các vòng lặp tuần tự để tạo các kết nối mới và gọi lệnh từ xa trong luồng thực thi song song:

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

Giả sử rằng CPU có khả năng thực thi tất cả 8 không gian chạy cùng một lúc, chúng ta sẽ có thể thấy rằng thời gian thực hiện được giảm đáng kể, nhưng với chi phí dễ đọc của tập lệnh do các phương thức khá "tiên tiến" được sử dụng.


Xác định mức độ tối ưu của thị sai:

Chúng tôi có thể dễ dàng tạo RunspacePool cho phép thực hiện 100 không gian chạy cùng một lúc:

[runspacefactory]::CreateRunspacePool(1,100)

Nhưng vào cuối ngày, tất cả chỉ ra có bao nhiêu đơn vị thực thi CPU cục bộ của chúng tôi có thể xử lý. Nói cách khác, miễn là mã của bạn đang thực thi, sẽ không có ý nghĩa khi cho phép nhiều không gian chạy hơn bạn có bộ xử lý logic để gửi thực thi mã đến.

Nhờ có WMI, ngưỡng này khá dễ xác định:

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

Mặt khác, nếu mã mà bạn đang thực thi phải chịu nhiều thời gian chờ đợi do các yếu tố bên ngoài như độ trễ mạng, bạn vẫn có thể hưởng lợi từ việc chạy nhiều không gian chạy hơn so với bộ xử lý logic, vì vậy bạn có thể muốn kiểm tra phạm vi tối đa có thể runspaces để tìm hòa vốn :

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
}

4
Nếu các công việc đang chờ trên mạng, ví dụ: bạn đang chạy các lệnh PowerShell trên các máy tính từ xa, bạn có thể dễ dàng vượt xa số lượng bộ xử lý logic trước khi bạn gặp bất kỳ tắc nghẽn CPU nào.
Michael Hampton

Vâng, đó là sự thật. Thay đổi một chút và cung cấp một ví dụ để thử nghiệm
Mathias R. Jessen

Làm thế nào để đảm bảo tất cả các công việc được thực hiện vào cuối? (Có thể cần một cái gì đó sau khi tất cả các khối kịch bản kết thúc)
sjzls

@NickW Câu hỏi tuyệt vời. Tôi sẽ theo dõi để theo dõi các công việc và "thu hoạch" sản lượng tiềm năng sau ngày hôm nay, hãy theo dõi
Mathias R. Jessen

1
@ MathiasR.Jessen Câu trả lời được viết rất tốt! Trông chờ vào bản cập nhật.
Tín hiệu

5

Thêm vào cuộc thảo luận này, cái còn thiếu là một bộ sưu tập để lưu trữ dữ liệu được tạo từ runspace và một biến để kiểm tra trạng thái của runspace, tức là nó đã hoàn thành hay chưa.

#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

3

Kiểm tra PoshRSJob . Nó cung cấp các chức năng tương tự / tương tự như các hàm * -Job gốc, nhưng sử dụng Runspaces có xu hướng nhanh hơn và phản ứng nhanh hơn nhiều so với các công việc Powershell tiêu chuẩn.


1

@ mathias-r-jessen có một câu trả lời tuyệt vời mặc dù có những chi tiết tôi muốn thêm vào.

Chủ đề tối đa

Trong lý thuyết chủ đề nên được giới hạn bởi số lượng bộ xử lý hệ thống. Tuy nhiên, trong khi thử nghiệm AsyncTcpScan, tôi đã đạt được hiệu suất tốt hơn nhiều bằng cách chọn giá trị lớn hơn nhiều cho MaxThreads. Vì vậy, tại sao mô-đun đó có một -MaxThreadstham số đầu vào. Hãy nhớ rằng việc phân bổ quá nhiều chủ đề sẽ cản trở hiệu suất.

Trả lại dữ liệu

Lấy lại dữ liệu từ ScriptBlocklà khó khăn. Tôi đã cập nhật mã OP và tích hợp nó vào mã được sử dụng cho AsyncTcpScan .

CẢNH BÁO: Tôi không thể kiểm tra mã sau đây. Tôi đã thực hiện một số thay đổi đối với tập lệnh OP dựa trên kinh nghiệm của tôi khi làm việc với các lệnh ghép ngắn Active Directory.

# 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.