PowerShell技术文章翻译#1 -- Queueing Theory,献给繁忙的系统管理员
从今天起,我会转载翻译一些自己认为不错的PowerShell技术类的文章和博客,这些文章可能来自一些技术网站,PowerShell官方博客,或者是Microsoft PowerShell Team的一些开发人员的个人博客。就从下面这篇开始吧 :)
Lee Holmes -- Blog: Precision Computing
今年最大的脚本编程盛会就快结束了--Scirpting Games 2012! 这次盛会聚集了很多难题,大量的挑战者,当然,为数更多的评论。作为一个客约评论员,我被要求写一个PowerShell脚本来监察会议环境中所有服务器的正常运行时间。为了尽量的高效,我决定使用PowerShell里的WMI jobs来同时处理对32台服务器的查询。最神奇的地方是,用Windows PowerShell Jobs并行执行32个任务并不只是把完成任务的速度提高了32倍,而是更多!其中的缘由就是计算机科学的一个分支--“Queueing Theory.”好吧,我就从PowerShell里的两个核心思想说起:Jobs and Streaming.
#PowerShell Jobs and Streaming# - 网络相对很慢,而并行Jobs可不是
当运行一个以网络通信为瓶颈(Network-bound)的操作时,例如从一长串服务器列表中获取每台服务器的Win32_OperatingSystem calss,你的计算机会把这项操作的绝大多数时间浪费在通信上:等待连接建立,等待远端服务器的应答,等待数据被传输回来。
为了解决这个问题,Windows PowerShell 2.0引入了“Jobs”的概念(译者注:跟乔帮主木有关系^_^)-- 主要被用在远程操作,WMI,和事件机制中。当你把一针对多台机器的操作作为Job来运行时,Windows PowerShell会把你的命令分配给多个工作线程并行执行,每个工作线程处理针对一台机器的操作。当一个工作线程完成操作时(比如说针对一台特定机器的远程WMI查询),Windows PowerShell会再分给这个线程针对另一台机器的操作任务。默认情况下,Window PowerShell会为一个Job并发启动32个工作线程。
我们前面提到了,神奇之处在于这种方法将操作执行速度提升了超过32倍,那么这个“Queueing Theory”到底是什么呢?下面我就为各位繁忙的系统管理员简洁的解释一下...
我们做一个想象中的实验。假设我们需要对92台服务器一个接一个的做一个查询,而第一台和第二台机器正在重启,因此针对它们的查询需要一分钟的时间才会返回“查询失败”。剩下的90台机器每台都会在2秒钟内返回查询结果。
这个查询总共的时间会是5分钟(1min+1min+90*2sec),平均每次成功的查询的时间是3.3秒(5*60/90)。这个问题就像是在一个食品店里,排在唯一一个收银台最前面的两个人在用一麻袋毛票付钱,而后面的顾客只能等着干着急:一个人的延迟影响到了队中的每个人。
现在,想想Windows PowerShell里并行Jobs的作用。最前面那两个正在重启的机器占住了我们两个线程,好吧,但是我们还有30个去处理剩下那90台机器,查询的总时间是6秒钟(90/30*2sec),也就是说平均每次成功查询的时间是0.07秒(6sec/90)。这可是比一个接一个查询要快了50倍!很酷,不是吗?这就像在那个食品店里,人们排在同一个队中,但有了32个收银员。
#PowerShell Jobs and Streaming# - Streaming很重要 (译者注:Streaming对于很多习惯用Linux的人不会陌生,在shell中,一个命令执行的结果可以通过Pipeline被作为下一个命令的输入,这样下一个命令不需要等待前个命令结束就可以基于前个命令已经产生的输出开始执行。Windows PowerShell把Streaming提升到了一个新的水平:Linux Shell中输出的只能是文本,当然管道传递的也只是文本,而Windows PowerShell中的输出是.Net object,管道传递的也是live object。不仅如此,Linux Shell管道中的每个命令都是以进程的方式执行,而PowerShell管道中的命令都是在同一个线程中执行,这样更能节省操作系统中context转换的开销。)
考虑到这很可能是一个运行时间很长的脚本,我们当然不希望运行结束再获得结果的输出,最好是一有结果或者进度信息就能让我们知道。为了达到这个目标,我做的第一步就是用好-Verbose和-Progress。在这个脚本处理每台机器时都会给出进度信息,告诉你现在在处理哪台机器。如果运行这个脚本时你给出了-Verbose参数,你会获得更多的细节--具体的说,在把每台机器的运行时间按特定格式写入CSV文件时,也把这个时间以醒眼的颜色输出在屏幕上。
这种方法解决了一个我常见到的问题:有些脚本强制输出一些进度信息或者调试信息,一个夸张些的例子就是过度使用Write-Host命令。当你运行这种脚本时,你屏幕上会显示很多很多的输出,并且没有办法让它“闭嘴”。你很难从屏幕上把有用的信息分辨出来,并且这种脚本也没办法正常的被其他脚本使用。
我的脚本除了灵活动态的输出进度信息,还要能够stream其结果输出。我当然可以等待运行结束,收集所有结果后再把它们保存到CSV里,但按那种方式,如果脚本在运行中被取消,那么已经产生的结果就丢失了。不过我们可以选择stream结果输出,一旦有结果产生,就把其写入CSV文件,这样你就可以用下面这个命令方便的在操作继续的同时,查看已经获取的查询结果了:
Get-Content -Path 20120409_Uptime.csv -Wait
想象一下,如果你的脚本要花一个小时的时间完成所有的操作,你就可以通过这种streaming的方法在所有操作完成前查看已生成结果了。
Jobs和Streaming -- 这两个非常有用的技术可以最大程度的优化运行时间较长脚本的执行效率。现在,该是我给出我脚本代码的时候了...(译者注:作者的脚本代码是针对PowerShell V2编写的,所以用到了Wait-Job $j | Receive-Job来获取结果输出。事实上Wait-Job并不能最大化的发挥Streaming的作用,它会阻塞执行直到这个Job完全完成,如果套用文章上面那个92台服务器的例子,也就是要等头两台重启的机器返回“操作失败”后,才能继续执行Receive-Job。显然这样不好,所以在PowerShell V3里,Receive-Job添加了一个参数-Wait,使得Receive-Job能立刻输出已经产生的结果,然后继续等待还未产生的结果。这样的话,在那个92台服务器的例子里,我们就可以在继续等待头两台重启机器的同时,对剩下那90个成功返回的查询结果进行处理了:Receive-Job $j -Wait | Foreach-Object { /* Process the results here */ })
############################################################################## ## ## Get-DistributedUptime ## ############################################################################## <# .SYNOPSIS Retrieves the uptime information (as of 8:00 AM local time) for the list of computers defined in the $computers variable. Output is stored in a date-stamped CSV file in the "My Documents" folder, with a name ending in "_Uptime.csv". .EXAMPLE Get-DistributedUptime #> param( ## Overwrites the output file, if it exists [Parameter()] [Switch] $Force ) ## Set up common configuration options and constants $reportStart = Get-Date -Hour 8 -Minute 0 -Second 0 $outputPath = Join-Path ([Environment]::GetFolderPath("MyDocuments")) ` ("{0:yyyyddMM}_Uptime.csv" -f $reportStart) ## See if the file exists. If it does (and the user has not specified -Force), ## then exit because the script has already been run today. if(Test-Path $outputPath) { if(-not $Force) { Write-Verbose "$outputPath already exists. Exiting" return } else { Remove-Item $outputPath } } ## Get the list of computers. If desired, this list could be ready from ## a test file as well: ## $computers = Get-Content computers.txt $computers = "EDLT1","EDLT2","EDLT3","EDLT4" ## Start the job to process all of the computers. This makes 32 ## connections at a time, by default. $j = Get-WmiObject Win32_OperatingSystem -ComputerName $computers -AsJob ## While the job is running, process its output do { ## Wait for some output, then retrieve the new output $output = @(Wait-Job $j | Receive-Job) foreach($result in $output) { ## We got a result, start processing it Write-Progress -Activity "Processing" -Status $result.PSComputerName ## Convert the DMTF date to a .NET Date $lastbootupTime = $result.ConvertToDateTime($result.LastBootUpTime) ## Subtract the time the report run started. If the system ## booted after the report started, ignore that for today. $uptimeUntilReportStart = $reportStart - $lastbootupTime if($uptimeUntilReportStart -lt 0) { $uptimeUntilReportStart = New-TimeSpan } ## Generate the output object that we're about to put ## into the CSV. Add a call to Select-Object at the end ## so that we can ensure the order. $outputObject = New-Object PSObject -Property @{ ComputerName = $result.PSComputerName; Days = $uptimeUntilReportStart.Days; Hours = $uptimeUntilReportStart.Hours; Minutes = $uptimeUntilReportStart.Minutes; Seconds = $uptimeUntilReportStart.Seconds; Date = "{0:M/dd/yyyy}" -f $reportStart } | Select ComputerName, Days, Hours, Minutes, Seconds, Date Write-Verbose $outputObject ## Append it to the CSV. If the CSV doesn't exist, create it and ## PowerShell will create the header as well. if(-not (Test-Path $outputPath)) { $outputObject | Export-Csv $outputPath -NoTypeInformation } else { ## Otherwise, just append the data to the file. Lines ## zero and one that we are skipping are the header ## and type information. ($outputObject | ConvertTo-Csv)[2] >> $outputPath } } } while($output)