การสร้างความรู้สึกขนานสำหรับแต่ละวัตถุใน 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
ฉันวางแผนที่จะทดลองใช้ฟีเจอร์นี้ต่อไป และอยากได้ยินเกี่ยวกับประสบการณ์การใช้งานของคุณ ฉันเชื่อว่าในที่สุดแล้วชุมชนก็จะมีความเห็นเป็นเอกฉันท์ว่าควรใช้สิ่งนี้เมื่อใด แต่ตอนนี้เรามีความยินดีที่ได้ค้นพบสิ่งนั้น!