작업을 사용하지 않고 PowerShell 스크립트를 병렬로 실행하려면 어떻게합니까?


29

여러 컴퓨터에 대해 실행해야하거나 여러 개의 다른 인수로 실행해야하는 스크립트가있는 경우 새 PSJobStart-Job 을 생성하는 오버 헤드없이 ?

예를 들어 모든 도메인 구성원의 시간을 다시 동기화하고 싶습니다. 다음과 같이 싶습니다.

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

그러나 각 PSSession이 명령을 연결하고 호출하기를 기다리지 않습니다. 잡스없이 어떻게 이것을 병렬로 할 수 있습니까?

답변:


51

업데이트 -이 답변에서는 PowerShell 실행 영역의 프로세스 및 메커니즘과 이들이 멀티 스레드 비 순차 워크로드를 지원하는 방법에 대해 설명하지만 동료 PowerShell 애호가 인 Warren 'Cookie Monster'F 는이 같은 개념을 하나의 도구로 통합했습니다. 호출 된 -아래에서 설명하는 작업을 수행 한 후 가져온 모듈, 실제로 멋진 물건을 포함하여 로깅 및 준비된 세션 상태를위한 옵션 스위치로 확장했습니다 . 광택 한 솔루션을 구축하기 전에 체크 아웃 하는 것이 좋습니다 !Invoke-Parallel


병렬 런 스페이스 실행

피할 수없는 대기 시간 단축

원래의 특정 경우, 호출 된 실행 파일 /nowait에는 작업 (이 경우 시간 재 동기화)이 자체적으로 완료되는 동안 호출 스레드를 차단 하는 옵션이 있습니다.

이렇게하면 발급자 관점에서 전체 실행 시간이 크게 단축되지만 각 컴퓨터에 대한 연결은 여전히 ​​순차적으로 수행됩니다. 시간 초과 대기 누적으로 인해 액세스 할 수없는 여러 가지 머신 수에 따라 수천 개의 클라이언트에 순서대로 연결하는 데 시간이 오래 걸릴 수 있습니다.

단일 또는 몇 번의 연속 시간 초과가 발생할 경우 모든 후속 연결을 대기열에 넣지 않기 위해 명령을 연결하고 호출하는 작업을 별도의 PowerShell Runspace에 병렬로 실행하여 전달할 수 있습니다.

런 스페이스 란 무엇입니까?

실행 영역은 가상 컨테이너에서 파워 쉘 코드가 실행하고, 대표 / PowerShell은 문 / 명령의 관점에서 환경을 보유하고 있습니다.

넓은 의미에서 1 Runspace = 1 실행 스레드이므로 PowerShell 스크립트를 "멀티 스레드"하는 데 필요한 모든 Runspace 컬렉션은 병렬로 실행될 수 있습니다.

원래 문제와 같이 여러 실행 영역 명령을 호출하는 작업은 다음과 같이 나눌 수 있습니다.

  1. RunspacePool 만들기
  2. PowerShell 스크립트 또는 이에 상응하는 실행 코드를 RunspacePool에 할당
  3. 코드를 비동기 적으로 호출하십시오 (예 : 코드가 리턴 될 때까지 기다리지 않아도 됨)

RunspacePool 템플릿

PowerShell에는 [RunspaceFactory]Runspace 구성 요소를 만드는 데 도움 이되는 형식 가속기 가 있습니다.

1. RunspacePool을 작성하고 다음을 수행 Open()하십시오.

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

두 인수에 전달 CreateRunspacePool(), 1그리고 8우리에게 효과적인 제공, 최소 및 주어진 시간에 실행할 수 실행 영역의 최대 수입니다 최대 병렬 처리 수준 (8)을.

2. PowerShell 인스턴스를 생성하고 실행 코드를 첨부 한 다음 RunspacePool에 할당합니다.

PowerShell의 인스턴스는 powershell.exe프로세스 (실제로 호스트 응용 프로그램) 와 동일하지 않지만 실행할 PowerShell 코드를 나타내는 내부 런타임 개체입니다. [powershell]유형 가속기를 사용하여 PowerShell 내에 새 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. APM을 사용하여 PowerShell 인스턴스를 비동기 적으로 호출합니다.

.NET 개발 용어로 알려진 비동기 프로그래밍 모델 을 사용하여 명령 Begin실행을 코드에 실행하는 "녹색 표시"를 제공하는 End메소드 와 결과를 수집 하는 메소드로 분할 할 수 있습니다 . 이 경우 우리는 실제로 피드백에 관심이 없기 때문에 ( w32tm어쨌든 출력을 기다리지 않습니다 ) 첫 번째 메소드를 호출하면됩니다.

$PSinstance.BeginInvoke()

RunspacePool에 싸서

위의 기술을 사용하여 새 연결을 작성하고 병렬 실행 플로우에서 원격 명령을 호출하는 순차적 반복을 랩핑 할 수 있습니다.

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

CPU에 8 개의 실행 영역을 한 번에 모두 실행할 수있는 용량이 있다고 가정하면 실행 시간이 크게 단축되지만 사용 된 "고급"방법으로 인해 스크립트의 가독성이 떨어짐을 알 수 있습니다.


최적의 병렬화 정도 결정 :

100 개의 실행 영역을 동시에 실행할 수있는 RunspacePool을 쉽게 만들 수 있습니다.

[runspacefactory]::CreateRunspacePool(1,100)

그러나 하루가 끝나면 로컬 CPU가 처리 할 수있는 실행 단위 수로 결정됩니다. 다시 말해, 코드가 실행되는 한 논리 프로세서가 코드 실행을 디스패치하는 것보다 더 많은 실행 영역을 허용하는 것은 의미가 없습니다.

WMI 덕분에이 임계 값을 결정하기가 매우 쉽습니다.

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

반면에, 네트워크 대기 시간과 같은 외부 요인으로 인해 자체적으로 실행중인 코드에 대기 시간이 많이 걸리더라도 논리 프로세서보다 더 많은 동시 실행 영역을 실행하면 이점을 얻을 수 있으므로 테스트하고 싶을 것입니다 손익 분기를 찾을 수있는 가능한 최대 실행 공간 범위 :

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
작업이 네트워크에서 대기중인 경우 (예 : 원격 컴퓨터에서 PowerShell 명령을 실행하는 경우) CPU 병목 현상이 발생하기 전에 여러 논리 프로세서를 쉽게 처리 할 수 ​​있습니다.
Michael Hampton

사실입니다. 조금 변경하고 테스트를위한 예제를 제공했습니다
Mathias R. Jessen

마지막에 모든 작업을 수행하는 방법? (모든 스크립트 블록이 완료된 후 무언가가 필요할 수 있습니다)
sjzls

@NickW 좋은 질문입니다. 나는 작업과 "수확을"추적에 추적을 다하겠습니다 잠재적 출력 후 오늘, 숙박 조정
마티아스 R. Jessen

1
@ MathiasR.Jessen 잘 쓰여진 답변! 업데이트를 기대합니다.
Signal15

5

이 토론에 추가로, 누락 된 것은 실행 영역에서 작성된 데이터를 저장하는 콜렉터와 실행 영역의 상태를 확인하는 변수, 즉 완료 여부입니다.

#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

PoshRSJob을 확인하십시오 . 기본 * -Job 함수와 동일 / 유사한 기능을 제공하지만 표준 Powershell 작업보다 훨씬 빠르고 반응이 빠른 Runspace를 사용합니다.


1

@ mathias-r-jessen은 추가하고 싶은 세부 사항이 있지만 훌륭한 답변 이 있습니다.

최대 스레드

이론적으로 스레드는 시스템 프로세서의 수에 의해 제한되어야합니다. 그러나 AsyncTcpScan 을 테스트하는 동안에 대해 더 큰 값을 선택하여 성능이 훨씬 향상되었습니다 MaxThreads. 따라서 해당 모듈에 -MaxThreads입력 매개 변수 가있는 이유는 무엇입니까? 너무 많은 스레드를 할당하면 성능이 저하됩니다.

데이터 반환

데이터를 다시 가져 오는 ScriptBlock것은 까다 롭습니다. OP 코드를 업데이트하고 AsyncTcpScan 에 사용 된 것과 통합했습니다 .

경고 : 다음 코드를 테스트 할 수 없습니다. Active Directory cmdlet을 사용한 경험을 바탕으로 OP 스크립트를 일부 변경했습니다.

# 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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.