ค้นหาเว็บไซต์

การสร้างความรู้สึกขนานสำหรับแต่ละวัตถุใน PowerShell 7


โดย คุณคงเคยได้ยินเกี่ยวกับพารามิเตอร์ -parallel ใหม่กับ ForEach-Object ในตัวอย่างล่าสุดของ PowerShell 7 เป็นการส่วนตัว ฉันรอสิ่งนี้มานานแล้ว ฉันเคยใช้เฉพาะเวิร์กโฟลว์ PowerShell เนื่องจากมีวิธีการเรียกใช้คำสั่งแบบขนาน การมีฟีเจอร์นี้เป็นส่วนหนึ่งของภาษาถือเป็นส่วนเสริมที่น่ายินดี แต่นี่ไม่ใช่เวทย์มนตร์และมีผลกระทบในโลกแห่งความเป็นจริงเมื่อคุณใช้มัน พารามิเตอร์ -Parallel จะหมุนชุดของ runspaces และเรียกใช้ scriptblock ของคุณในแต่ละชุด การวิ่งขนานกันไม่ได้หมายความว่าเป็นระเบียบ

นี่คือตัวอย่างการทำงานของพารามิเตอร์ใหม่

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

ท่อตรง

นี่คือตัวอย่างไปป์ไลน์แบบตรงไปตรงมาโดยใช้ ForEach-Object

Measure-Command {
    1..5000 | ForEach-Object { [math]::Sqrt($_) * 2 }
}

ฉันใช้เวลา 34 มิลลิวินาทีจึงจะเสร็จสมบูรณ์ นี่เป็นคำสั่งเดียวกัน แต่ทำงานโดยใช้ -parallel

Measure-Command {
    1..5000 | ForEach-Object -parallel { [math]::Sqrt($_) * 2 }
}

การดำเนินการนี้ใช้เวลา 89 วินาที เห็นได้ชัดว่ามีค่าใช้จ่ายสูงที่เกี่ยวข้องเมื่อใช้พารามิเตอร์นี้

อย่างไรก็ตาม ยังมีช่องว่างสำหรับการปรับปรุง ตามค่าเริ่มต้น คำสั่งจะหมุน runspaces ครั้งละ 5 อัน คุณสามารถเพิ่มหรือลดค่านี้ได้โดยใช้พารามิเตอร์ -ThrottleLimit

Measure-Command {
    1..5000 | ForEach-Object -parallel { [math]::Sqrt($_) * 2 } -throttlelimit 25
}

เวอร์ชันนี้เสร็จสิ้นภายในเวลาเกือบ 5 วินาที! โดยการเปรียบเทียบ โดยใช้ตัวแจงนับ ForEach:

Measure-Command {
    foreach ($x in (1..5000)) { [math]::Sqrt($x) * 2 }
}

ใช้เวลาประมาณ 7 มิลลิวินาที

การเรียกใช้ Scriptblocks

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

$t = {
    param ([string]$Path)
    Get-ChildItem -path $path -recurse -file -erroraction SilentlyContinue |
        Measure-Object -sum -Property Length -Maximum -Minimum |
        Select-Object @{Name = "Computername"; Expression = { $env:COMPUTERNAME } },
        @{Name = "Path"; Expression = { Convert-Path $path } },
        Count, Sum, Maximum, Minimum
}

Measure-Command {
    "c:\work", "c:\scripts", "d:\temp", "C:\users\jeff\Documents", "c:\windows" | 
    ForEach-Object -process { Invoke-Command -scriptblock $t -argumentlist $_ }
}

การเรียกใช้สิ่งนี้ในเครื่องบนเดสก์ท็อป Windows 10 ของฉันที่มีหน่วยความจำ 32GB ใช้เวลามากกว่า 34 วินาทีเล็กน้อย นี่เป็นคำสั่งเดียวกันโดยใช้ -Parallel

Measure-Command {
    "c:\work", "c:\scripts", "d:\temp", "C:\users\jeff\Documents", "c:\windows" | ForEach-Object -parallel {
        $path = $_
        Get-ChildItem -path $path -recurse -file -erroraction SilentlyContinue |
            Measure-Object -sum -Property Length -Maximum -Minimum |
            Select-Object @{Name = "Computername"; Expression = { $env:COMPUTERNAME } },
            @{Name = "Path"; Expression = { Convert-Path $path } },
            Count, Sum, Maximum, Minimum

    } 
}

คุณควรสังเกตเห็นความแตกต่างอย่างมากในไวยากรณ์ เนื่องจากแต่ละบล็อกสคริปต์แบบขนานกำลังทำงานอยู่ในรันสเปซใหม่ จึงเป็นเรื่องยากที่จะส่งผ่านตัวแปรเช่น $t มีวิธีแก้ไขปัญหานี้ สำหรับฉัน ทางออกที่ง่ายที่สุดสำหรับการสาธิตคือการคัดลอก scriptblock

คำสั่งนี้ใช้เวลาประมาณ 50 วินาทีในการรัน แม้ว่าฉันจะเหยียบขีดจำกัดคันเร่งเป็น 6 มันก็เสร็จภายในเวลาประมาณ 20 วินาที

คำสั่งระยะไกล

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

Measure-Command {
    'srv1', 'srv2', 'win10', 'dom1', 'srv1', 'srv2', 'win10', 'dom1' | ForEach-Object {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 } -MaxEvents 100 -ComputerName $_
    }
}

ฉันมีเครื่องเสมือนเพียงไม่กี่เครื่องที่จะทดสอบ ดังนั้นฉันจึงทำซ้ำคำสั่งกับเครื่องเหล่านั้น เสร็จสิ้นภายในเวลาประมาณ 3 วินาที เมื่อเปรียบเทียบกับแนวทาง -Parallel:

Measure-Command {
    'srv1', 'srv2', 'win10', 'dom1', 'srv1', 'srv2', 'win10', 'dom1' | ForEach-Object -parallel {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 } -MaxEvents 100 -ComputerName $_
    }
}

ซึ่งใช้เวลาไม่ถึง 1 วินาที เรามาเพิ่มขนาดตัวอย่างกันดีกว่า

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1')*5 | 
    ForEach-Object {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
    }
}

ใช้เวลา 9.7 วินาทีเมื่อเทียบกับสิ่งนี้:

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1')*5  |
        ForEach-Object -parallel {
            Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
        }
}

ซึ่งเสร็จสิ้นภายใน 3.7 วินาที

PowerShell ในระดับ

เรามาดูกันว่าสิ่งนี้มีลักษณะอย่างไรในวงกว้าง ฉันจะพยายามจำลองเวลาแฝงของเซิร์ฟเวอร์และเครือข่ายด้วย

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1') * 100 | ForEach-Object {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
        #simulate network/server latency
        Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
    }
}

บนเดสก์ท็อปของฉัน การดำเนินการนี้ใช้เวลานานกว่า 18 นาที ในการดำเนินการให้เสร็จสิ้น โดยส่งคืนข้อมูลเกือบ 41,000 รายการ นี่คือเวอร์ชัน PowerShell 7 เริ่มต้นที่ใช้ -parallel:

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1') * 100 | ForEach-Object -parallel {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
        Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
    }
}

ตอนนี้คำสั่งเสร็จสิ้นใน 3 นาที 35 วินาที

ค่าจำกัดคันเร่งเริ่มต้นอาจมีการเปลี่ยนแปลง Jeffrey Snover บอกว่าเขาคิดว่ามันควรจะสูงกว่านี้ และฉันคิดว่าฉันเห็นด้วย มาดูกันว่าจะเกิดอะไรขึ้นเมื่อใช้ค่า ThrottleLimit เริ่มต้นจาก Inrigg-Command

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1') * 100 | ForEach-Object -parallel {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
        Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
    } -throttlelimit 32
}

เสร็จภายใน 49 วินาที!

ทางเลือกเวิร์กโฟลว์

สำหรับการเปรียบเทียบครั้งสุดท้าย นี่คือขั้นตอนการทำงานคร่าวๆ ที่เป็นที่ยอมรับซึ่งใกล้เคียงกับตัวอย่างอื่นๆ ฉันเปลี่ยนมาใช้ Get-Eventlog เพราะฉันไม่คิดว่าเวิร์กโฟลว์ชอบส่งแฮชการกรองผ่านการเชื่อมต่อแบบซีเรียลไลซ์

Workflow GetLogs {

    Param()

    Sequence {
        $computers = ('srv1.company.pri', 'srv2.company.pri', 'win10.company.pri', 'dom1.company.pri') * 100
        foreach -parallel ($computer in $computers) {

            inlinescript {
                Write-Verbose "$(Get-Date) Processing $using:computer"
                Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
                Get-Eventlog -LogName system -EntryType error, Warning -ComputerName $using:computer
            } #inline
        } #foreach
    } #sequence
} #workflow
}

$d = getlogs -pscomputername localhost -ErrorAction SilentlyContinue

นี่ไม่ใช่สิ่งที่เวิร์กโฟลว์ PowerShell ตั้งใจไว้ แม้ว่าฉันคาดว่าหลาย ๆ คนจะลองใช้เวิร์กโฟลว์ในลักษณะนี้ก็ตาม คำสั่งนี้เสร็จสิ้นในเวลา 10:05 แม้ว่ามันอาจจะได้รับการปรับปรุงให้ดีขึ้นด้วยขีดจำกัดคันเร่งที่สูงขึ้นก็ตาม

ข้อสรุป

ข้อสรุปเดียวที่คุณสามารถทำได้คือคุณต้องทำการทดสอบของคุณเองเพื่อพิจารณาว่าการใช้ -parallel นั้นสมเหตุสมผลสำหรับคำสั่งหรือโปรเจ็กต์ของคุณหรือไม่ โดยส่วนตัวแล้ว สิ่งที่ฉันใช้คือ -parallel ทำงานได้ดีที่สุดสำหรับการดำเนินงานขนาดใหญ่ที่มีความแปรปรวนสูง คุณอาจได้ผลลัพธ์ที่เปรียบเทียบได้เมื่อเขียนคำสั่งของคุณเพื่อปรับขนาดด้วย Inrigg-Command

$computers = ('srv1', 'srv2', 'win10', 'dom1') * 100
$d = Invoke-Command {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  
        Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
} -throttlelimit 32 -ComputerName $computers

ใช่ มีค่าใช้จ่ายเนื่องจาก PSSessions ต้องได้รับการตั้งค่าและรื้อถอน การดำเนินการนี้เสร็จสิ้นในเวลา 1:56 สำหรับฉัน ซึ่งยังคงเป็นประสิทธิภาพที่ยอมรับได้ และเป็นโบนัส ซึ่งไม่จำเป็นต้องใช้ PowerShell 7

ฉันวางแผนที่จะทดลองใช้ฟีเจอร์นี้ต่อไป และอยากได้ยินเกี่ยวกับประสบการณ์การใช้งานของคุณ ฉันเชื่อว่าในที่สุดแล้วชุมชนก็จะมีความเห็นเป็นเอกฉันท์ว่าควรใช้สิ่งนี้เมื่อใด แต่ตอนนี้เรามีความยินดีที่ได้ค้นพบสิ่งนั้น!

บทความที่เกี่ยวข้อง