ฉันจะรันสคริปต์ 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 ผู้คลั่งไคล้ PowerShell 'Cookie Monster' Fได้ผ่านไปแล้ว เรียกว่า - มันเป็นสิ่งที่ฉันอธิบายด้านล่างและเขาได้ขยายด้วยสวิตช์ตัวเลือกสำหรับการบันทึกและสถานะเซสชันที่เตรียมไว้รวมถึงโมดูลที่นำเข้าสิ่งที่ยอดเยี่ยมจริง ๆ - ฉันขอแนะนำให้คุณตรวจสอบก่อนสร้างโซลูชันเงา!Invoke-Parallel


ด้วยการประมวลผล Parallel Runspace:

การลดเวลารอที่หลีกเลี่ยงไม่ได้

ในกรณีเฉพาะดั้งเดิมไฟล์เรียกทำงานที่เรียกใช้งานมี/nowaitตัวเลือกที่ป้องกันการบล็อกเธรดที่เรียกใช้ในขณะที่งาน (ในกรณีนี้เวลาซิงโครไนซ์อีกครั้ง) จะเสร็จสิ้นด้วยตัวเอง

สิ่งนี้ช่วยลดเวลาการดำเนินการโดยรวมจากมุมมองของผู้ออกตราสารได้มาก แต่การเชื่อมต่อกับแต่ละเครื่องยังคงดำเนินการตามลำดับ การเชื่อมต่อกับไคลเอนต์หลายพันเครื่องตามลำดับอาจใช้เวลานานขึ้นอยู่กับจำนวนของเครื่องที่มีสาเหตุหนึ่งหรือไม่สามารถเข้าถึงได้อีกเนื่องจากการหมดเวลาของการรอคอยสะสม

หากต้องการหลีกเลี่ยงการต่อคิวการเชื่อมต่อที่ตามมาทั้งหมดในกรณีที่มีการหมดเวลาติดต่อกันเพียงครั้งเดียวหรือสองสามครั้งเราสามารถจัดส่งงานของการเชื่อมต่อและเรียกใช้คำสั่งเพื่อแยก PowerShell Runspaces ออกจากกัน

Runspace คืออะไร

Runspaceเป็นภาชนะเสมือนในที่รันรหัส PowerShell ของคุณและเป็นตัวแทน / ถือสิ่งแวดล้อมจากมุมมองของคำสั่ง PowerShell / คำสั่ง

ในแง่กว้าง 1 Runspace = 1 เธรดของการประมวลผลดังนั้นสิ่งที่เราต้อง "มัลติเธรด" สคริปต์ PowerShell ของเราคือชุดของ Runspaces ที่สามารถเปิดใช้งานแบบขนาน

เช่นเดียวกับปัญหาดั้งเดิมงานของคำสั่งที่ใช้เรียกใช้งานหลายพื้นที่ทำงานสามารถแบ่งย่อยได้เป็น:

  1. สร้าง RunspacePool
  2. การกำหนดสคริปต์ PowerShell หรือรหัสที่สามารถใช้งานได้เทียบเท่ากับ RunspacePool
  3. เรียกใช้โค้ดแบบอะซิงโครนัส (เช่นไม่ต้องรอให้โค้ดส่งคืน)

เทมเพลต RunspacePool

PowerShell มีตัวเร่งความเร็วชนิดที่เรียก[RunspaceFactory]ว่าจะช่วยเราในการสร้างส่วนประกอบของรันสเปซ - มาทำให้ใช้งานได้

1. สร้าง RunspacePool และสร้างOpen():

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

ทั้งสองมีปากเสียงส่งผ่านไปCreateRunspacePool(), 1และ8เป็นต่ำสุดและสูงสุดจำนวน runspaces ได้รับอนุญาตให้ดำเนินการในเวลาใดก็ตามให้เรามีประสิทธิภาพสูงสุดระดับการขนาน 8

2. สร้างอินสแตนซ์ของ PowerShell แนบรหัสที่สามารถเรียกใช้งานได้และกำหนดให้กับ RunspacePool ของเรา:

อินสแตนซ์ของ PowerShell ไม่เหมือนกับpowershell.exeกระบวนการ (ซึ่งเป็นแอปพลิเคชัน Host จริงๆ) แต่เป็นวัตถุรันไทม์ภายในที่แสดงรหัส 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. เรียกใช้อินสแตนซ์ PowerShell แบบอะซิงโครนัสโดยใช้ APM:

การใช้สิ่งที่เป็นที่รู้จักกันในคำศัพท์การพัฒนา. NET เป็นAsynchronous Programming Modelเราสามารถแบ่งการร้องขอของคำสั่งเป็นBeginวิธีการให้ "ไฟเขียว" เพื่อรันโค้ดและEndวิธีการรวบรวมผลลัพธ์ เนื่องจากเราในกรณีนี้ไม่สนใจข้อเสนอแนะใด ๆ (เราไม่รอผลลัพธ์จากw32tmanyways) เราสามารถทำการครบกำหนดได้เพียงแค่เรียกวิธีแรก

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

สมมติว่าซีพียูมีความสามารถในการดำเนินการทั้ง 8 รันสเปซในครั้งเดียวเราจะสามารถเห็นได้ว่าเวลาในการประมวลผลลดลงอย่างมาก แต่มีค่าใช้จ่ายในการอ่านสคริปต์เนื่องจากวิธีการ "ขั้นสูง"


การกำหนดระดับสูงสุดของความเท่าเทียมกัน:

เราสามารถสร้าง RunspacePool ที่อนุญาตให้เรียกใช้งาน 100 runspaces ในเวลาเดียวกันได้อย่างง่ายดาย:

[runspacefactory]::CreateRunspacePool(1,100)

แต่ในตอนท้ายของวันมันทั้งหมดลงมาถึงจำนวนหน่วยปฏิบัติการซีพียูของเราสามารถจัดการได้ กล่าวอีกนัยหนึ่งตราบใดที่โค้ดของคุณกำลังทำงานมันไม่เหมาะสมที่จะอนุญาตให้รันสเปซมากกว่าที่คุณมีตัวประมวลผลเชิงตรรกะเพื่อส่งการเรียกใช้โค้ดไป

ขอบคุณ 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 คำถามที่ดี ฉันจะติดตามผลการติดตามงานและ "เก็บเกี่ยว" ผลลัพธ์ที่เป็นไปได้ในภายหลังในวันนี้คอยติดตาม
Mathias R. Jessen

1
@ MathiasR.Jessen คำตอบที่ดีมากเขียน! รอคอยที่จะมีการปรับปรุง
สัญญาณ 15

5

สิ่งที่ขาดหายไปคือตัวรวบรวมเพื่อจัดเก็บข้อมูลที่สร้างขึ้นจาก runspace และตัวแปรเพื่อตรวจสอบสถานะของ runspace นั่นคือจะเสร็จสมบูรณ์หรือไม่

#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 แต่ใช้ Runspaces ซึ่งมีแนวโน้มที่จะเร็วและตอบสนองมากขึ้นกว่างาน Powershell มาตรฐาน


1

@ mathias-r-jessen มีคำตอบที่ดีแต่มีรายละเอียดที่ฉันต้องการเพิ่ม

Max Threads

ในทฤษฎีเธรดควรถูก จำกัด ด้วยจำนวนตัวประมวลผลระบบ อย่างไรก็ตามในขณะที่ทดสอบAsyncTcpScanฉันได้รับประสิทธิภาพที่ดีขึ้นโดยเลือกค่าที่MaxThreadsมากกว่า ดังนั้นทำไมโมดูลนั้นจึงมี-MaxThreadsพารามิเตอร์อินพุต โปรดทราบว่าการจัดสรรเธรดจำนวนมากเกินไปจะเป็นอุปสรรคต่อประสิทธิภาพการทำงาน

ส่งคืนข้อมูล

การรับข้อมูลกลับจากอุปกรณ์ScriptBlockนั้นยุ่งยาก ผมได้ปรับปรุงรหัส OP และบูรณาการมันเป็นสิ่งที่ถูกนำมาใช้สำหรับAsyncTcpScan

คำเตือน: ฉันไม่สามารถทดสอบรหัสต่อไปนี้ ฉันทำการเปลี่ยนแปลงสคริปต์ OP ตามประสบการณ์ของฉันที่ทำงานกับ cmdlet ของ 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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.