Share via


您好,腳本專家! 計算伺服器正常執行時間

Microsoft 脚本专家

代碼下載位置: HeyScriptingGuy2008_12.exe(152 KB)

運行是運行,停機是停機。 看起來似乎是涇渭分明 — 但談到伺服器正常執行時間,可就不是這樣了。 要知道正常執行時間,就需要知道停機時間。 幾乎每位網路系統管理員都很關心伺服器正常執行時間。 (若非如此,則是很關心伺服器停機時間)。 大多數管理員都設有正常執行時間目標,並且需要向上層管理人員提交正常執行時間報告。

這有什麼大不了的?似乎您只要使用 Win32_OperatingSystem WMI 類提供的兩個屬性,即 LastBootUpTime 和 LocalDateTime,就可以輕鬆完成這樣的操作。 您認為,只要將 LocalDateTime 減去 LastBootUptime 就萬事大吉了,甚至還可以在晚餐前打場九洞高爾夫球。

因此您啟動 Windows PowerShell 來查詢 Win32_OperatingSystem WMI 類,並選擇屬性,如下所示:

PS C:\> $wmi = Get-WmiObject -Class Win32_OperatingSystem
PS C:\> $wmi.LocalDateTime - $wmi.LastBootUpTime

但是當您運行這些命令後,顯示的並非易於理解的伺服器正常執行時間,而是如 圖 1 所示的令人鬱悶的錯誤消息。

fig01.gif

圖 1 嘗試減去 WMI UTC 時間值後返回錯誤消息(按一下圖像可查看大圖)

此錯誤消息也許會誤導人:“數位常量無效。 ”什麼?您知道數位是什麼,也知道常量是什麼,但這些跟時間有什麼關係?

面對異常的錯誤消息時,最好的方法就是直接查看腳本試圖解析的資料。 而且在使用 Windows PowerShell 時,往往也要注意所使用的資料類型。

要檢查腳本所使用的資料,您可以直接將它顯示在螢幕上。 結果如下:

PS C:\> $wmi.LocalDateTime
20080905184214.290000-240

這個數位看起來有點奇怪,這是哪種格式的日期?為了搞清楚,我們使用 GetType 方法。 GetType 的優點在於它幾乎總是可供使用。 您只需調用它即可。 問題的根源找到了 — LocalDateTime 值被報告成字串,而非 System.DateTime 值:

PS C:\> $wmi.LocalDateTime.gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True     True     String
System.Object

如果您需要讓兩個時間相減,請確保您使用的是時間值,而不是字串。 辦法很簡單,就是使用 Windows PowerShell 添加到所有 WMI 類的 ConvertToDateTime 方法:

PS C:\> $wmi = Get-WmiObject -Class Win32_OperatingSystem
PS C:\> $wmi.ConvertToDateTime($wmi.LocalDateTime) –
$wmi.ConvertToDateTime($wmi.LastBootUpTime)

當您讓兩個時間值相減時,生成的是 System.TimeSpan 物件的實例。 這表示您不必執行一大堆演算法就可以選擇如何顯示正常執行時間資訊。 您只需要選擇要顯示的屬性(希望您是以 TotalDays 而非 TotalMilliseconds 來計數正常執行時間)。 下麵是 System.TimeSpan 物件的預設顯示:

Days              : 0
Hours             : 0
Minutes           : 40
Seconds           : 55
Milliseconds      : 914
Ticks             : 24559148010
TotalDays         : 0.0284249398263889
TotalHours        : 0.682198555833333
TotalMinutes      : 40.93191335
TotalSeconds      : 2455.914801
TotalMilliseconds : 2455914.801

此方法的問題在於它只告訴您自上次重新開機以來,伺服器運行了多長時間。 它並不會計算停機時間。 運行與停機是同一事物的兩種表像就體現在這裡 — 為了計算正常執行時間,您首先要知道停機時間。

那麼,您要如何計算伺服器停機的時間長度?若要執行此操作,您需要知道伺服器何時啟動以及何時關閉。 您可以從系統事件日誌獲取該資訊。 在伺服器或工作站上最先開始啟動的進程之一就是事件日誌,事件日誌也是當伺服器關閉時最後停止運行的一個進程。 這些開始/停止事件分別生成一個事件 ID — 當事件日誌開始時生成的事件 ID 是 6005,停止時則是 6006。 圖 2 顯示的是事件日誌開始的示例。

fig02.gif

圖 2 事件日誌服務在電腦啟動後立即開始運行(按一下圖像可查看大圖)

通過從系統日誌收集 6005 和 6006 事件,對它們進行排序,然後開始時間與停止時間相減,您就可以確定伺服器在重新開機期間停機的時間長度。 如果您再從此處的期間分鐘數減去此停機時間長度,就可以計算出伺服器正常執行時間的百分比。 這就是 CalculateSystemUpTimeFromEventLog.ps1 腳本中採用的方法,如圖 3 所示。

圖 3 CalculateSystemUpTimeFromEventLog 3

#---------------------------------------------------------------
# CalculateSystemUpTimeFromEventLog.ps1
# ed wilson, msft, 9/6/2008
# Creates a system.TimeSpan object to subtract date values
# Uses a .NET Framework class, system.collections.sortedlist to sort the events from eventlog.
#---------------------------------------------------------------
#Requires -version 2.0
Param($NumberOfDays = 30, switch$debug)

if($debug) { $DebugPreference = " continue" }

timespan$uptime = New-TimeSpan -start 0 -end 0
$currentTime = get-Date
$startUpID = 6005
$shutDownID = 6006
$minutesInPeriod = (24*60)*$NumberOfDays
$startingDate = (Get-Date -Hour 00 -Minute 00 -Second 00).adddays(-$numberOfDays)

Write-debug "'$uptime $uptime" ; start-sleep -s 1
write-debug "'$currentTime $currentTime" ; start-sleep -s 1
write-debug "'$startingDate $startingDate" ; start-sleep -s 1

$events = Get-EventLog -LogName system | 
Where-Object { $_.eventID -eq  $startUpID -OR $_.eventID -eq $shutDownID `
  -and $_.TimeGenerated -ge $startingDate } 

write-debug "'$events $($events)" ; start-sleep -s 1

$sortedList = New-object system.collections.sortedlist

ForEach($event in $events)
{
 $sortedList.Add( $event.timeGenerated, $event.eventID )
} #end foreach event
$uptime = $currentTime - $sortedList.keys$($sortedList.Keys.Count-1)
Write-Debug "Current uptime $uptime"

For($item = $sortedList.Count-2 ; $item -ge 0 ; $item -- )
{ 
 Write-Debug "$item `t `t $($sortedList.GetByIndex($item)) `t `
   $($sortedList.Keys$item)" 
 if($sortedList.GetByIndex($item) -eq $startUpID)
 {
  $uptime += ($sortedList.Keys$item+1 - $sortedList.Keys$item)
  Write-Debug "adding uptime. `t uptime is now: $uptime"
 } #end if  
} #end for item 

"Total up time on $env:computername since $startingDate is " + "{0:n2}" -f `
  $uptime.TotalMinutes + " minutes."
$UpTimeMinutes = $Uptime.TotalMinutes
$percentDownTime = "{0:n2}" -f (100 - ($UpTimeMinutes/$minutesInPeriod)*100)
$percentUpTime = 100 - $percentDowntime

"$percentDowntime% downtime and $percentUpTime% uptime."

該腳本一開始使用 Param 語句來定義幾個命令列參數,從命令列運行該腳本時,您可以更改這些參數值。 第一個參數 $NumberOfDays 可使您指定用於正常執行時間報告的不同天數。 (請注意,我在該腳本中提供的預設值為 30 天,因此即使您不為該參數提供任何值,也可以運行該腳本。 當然,您可以根據需要來更改此值)。

第二個參數 switch$debug 是切換參數,如果在運行腳本時將此參數添加到命令列上,可使您通過腳本獲取特定的調試資訊。 此資訊能讓您對於從腳本獲取的結果更有把握。 有時候可能不顯示 6006 事件日誌服務停止消息,這可能是因為伺服器遭遇了災難性故障,因而無法寫入事件日誌,導致腳本讓兩個正常執行時間值相減,影響了最終結果。

$debug 變數是通過命令列提供的,它將在變數上顯示:drive。 在此情況下,$debugPreference 變數值將設為“continue”,也就是說,腳本會繼續運行,而提供給 Write-Debug 的所有值都會顯示出來。 請注意,$debugPreference 的值預設為 silentlycontinue,因此如果您未將 $debugPreference 的值設為“continue”,則該腳本將繼續運行,但是提供給 Write-Debug 的值將為 silent(也就是說,將不會顯示這些值)。

當腳本運行時,生成的輸出會列出每一個 6005 和 6006 事件日誌條目(如圖 4 所示),並顯示正常執行時間計算。 通過該資訊,您可以確認結果的準確性。

fig04.gif

圖 4 調試模式顯示了添加到正常執行時間計算的每一個時間值的記錄(按一下圖像可查看大圖)

下一步是創建 System.TimeSpan 物件的實例。 您可以使用 New-Object cmdlet 來創建將用於執行日期差異計算的預設 timespan 物件:

PS C:\> timespan$ts = New-Object system.timespan

但是 Windows PowerShell 實際上有一個可用來創建 timespan 物件的 New-TimeSpan cmdlet,因此我們有道理使用此 cmdlet。 使用此 cmdlet 會讓腳本更容易閱讀,而且所創建的物件與使用 New-Object 創建的 timespan 物件是等效的。

現在,您可以初始化一些變數,就從用來表示當前時間與日期值的 $currentTime 開始。 從 Get-Date cmdlet 可獲取此資訊:

$currentTime = get-Date

接著,初始化用來表示啟動和關閉 eventID 數位的兩個變數。 您不一定非得進行此初始化,不過如果您避免以字串文字值的形式嵌入這兩個變數,讀取代碼和進行故障排除會更容易。

下一步是創建名為 $minutesInPeriod 的變數,用於表示計算結果以便用來確認這段期間的分鐘數:

$minutesInPeriod = (24*60)*$NumberOfDays

最後,您需要創建 $startingDate 變數,此變數用於表示代表報告期間開始時間的 System.DateTime 物件。 日期將是該期間的開始日期那天的午夜:

$startingDate = (Get-Date -Hour 00 -Minute 00 -Second 00).adddays(-$numberOfDays)

創建這些變數之後,您將在事件日誌中檢索事件,並將查詢結果存儲在 $events 變數中。 使用 Get-EventLog cmdlet 來查詢事件日誌並將“system”指定為日誌名稱。 在 Windows PowerShell 2.0 中,您可以使用 –source 參數來減少需要從 Where-Object cmdlet 中排除的資訊量。 但是在 Windows PowerShell 1.0 中,在這方面您無法選擇,因此必須逐一整理通過查詢返回的未經篩選的事件。 將事件輸送到 Where-Object cmdlet 以篩選出適當的事件日誌條目。 當您檢查 Where-Object 篩選器時,您將會明白腳本專家要求您創建變數以保存參數的原因。

比起使用字串文字值,您能夠更輕鬆地閱讀命令。 get eventID 等同于 $startUpID 或 $shutDownID。 您也應確保事件日誌條目的 timeGenerated 屬性大於或等於 $startingDate,如下所示:

$events = Get-EventLog -LogName system |
Where-Object { $_.eventID -eq  $startUpID -OR $_.eventID -eq $shutDownID -and $_.TimeGenerated -ge $startingDate }

請記住,此命令只能本地運行。 在 Windows PowerShell 2.0 中,您可以使用 –computerName 參數來遠端運行此命令。

下一步是創建排序的清單物件。 為什麼呢?因為當您遍歷事件集合時,無法保證事件日誌條目的報告順序。 即使將物件輸送到 Sort-Object cmdlet,並將結果保存回變數中,當您遍歷物件並將結果存儲到雜湊表時,您也無法確定清單會保持排序步驟的結果。

為了回避這些棘手又難以調試的問題,您可使用物件的預設構造函數來創建 System.Collections.SortedList 物件的實例。 預設的構造函數告訴排序的清單按時間順序對日期進行排序。 將空白的排序清單物件存儲到 $sortedList 變數中:

$sortedList = New-object system.collections.sortedlist

創建排序的清單物件後,您需要填充此物件。 為此,可使用 ForEach 語句並遍歷存儲在 $entries 變數中的事件日誌條目的集合。 遍歷集合時,$event 變數會記錄您在集合中的位置。 您可以使用 add 方法將兩個屬性添加到 System.Collections.SortedList 物件中。 排序的清單允許您添加 key 屬性和 value 屬性(類似于 Dictionary 物件,只不過您還可以像對陣列一樣對集合進行索引)。 將 timegenerated 屬性添加為 key 屬性,將 eventID 添加為 value 屬性:

ForEach($event in $events)
{
 $sortedList.Add( $event.timeGenerated,
 $event.eventID )
} #end foreach event

接著,您將計算伺服器的當前正常執行時間。 為此,您需要使用排序清單中最新的事件日誌條目。 請注意,最新的事件日誌條目始終是 6005 實例,因為如果它是 6006,就表示伺服器仍處於停機狀態。 由於索引是從零開始的,因此最新的條目應該是計數 –1。

要檢索時間生成的值,您需要查看排序清單的 key 屬性。 若要獲取索引值,請使用 count 屬性,並使其減一。 然後,從您早期填充的 $currenttime 變數中存儲的日期時間值減去 6005 事件的生成時間。 只有在調試模式下運行腳本,您才可以顯示此計算結果。 這段代碼如下所示:

$uptime = $currentTime -
$sortedList.keys$($sortedList.Keys.Count-1)
Write-Debug "Current uptime $uptime"

現在該遍歷排序的清單物件並計算伺服器的正常執行時間了。 由於使用的是 System.Collections.Sorted 清單物件,您可以利用能對清單進行索引這個優勢。 為此,可使用 for 語句,從計數 -2 開始,因為之前我們已使用計數 -1 來計算當前的正常執行時間長度。

我們將反向計數來獲取正常執行時間,因此在 for 語句的第二個位置中指定的條件是當項大於或等於 0 時。 在 for 語句的第三個位置使用 --,這將使 $item 的值逐次遞減一。 如果腳本以 –debug 開關運行,您可以使用 Write-Debug cmdlet 顯示出索引編號的值。 您也可以使用 `t 字元切換來顯示出 timegenerated 時間值。 這段代碼如下所示:

For($item = $sortedList.Count-2 ; $item -ge 
  0 ; $item--)
{ 
 Write-Debug "$item `t `t $($sortedList.
 GetByIndex($item)) `t `
   $($sortedList.Keys$item)" 

如果 eventID 值等於 6005(也就是啟動 eventID 值),您可以從先前的停機值減去開始時間來計算正常執行時間的長度。 將此值存儲在 $uptime 變數中。 如果以調試模式運行,您可以使用 Write-Debug cmdlet 將這些值顯示在螢幕上:

 if($sortedList.GetByIndex($item) -eq $startUpID)
 {
  $uptime += ($sortedList.Keys$item+1 
  - $sortedList.Keys$item)
  Write-Debug "adding uptime. `t uptime is now: $uptime"
 } #end if  
} #end for item

最後,您需要生成報告。 從系統環境變數電腦獲取電腦名稱。 您可使用存儲在 $startingdate 值中的當前時間並顯示該期間正常執行時間的總分鐘數。 您可使用格式說明符 {0:n2} 將數值顯示為兩位數位。 接著計算停機時間百分比,方法是將正常執行時間分鐘數除以報告所佔用的分鐘數。 使用相同的格式說明符將數值顯示為兩位數位。 為了增添些樂趣,您也可以計算正常執行時間的百分比,然後顯示出兩個值,如下所示:

"Total up time on $env:computername since $startingDate is " + "{0:n2}" -f `
  $uptime.TotalMinutes + " minutes."
$UpTimeMinutes = $Uptime.TotalMinutes
$percentDownTime = "{0:n2}" -f (100 - ($UpTimeMinutes/$minutesInPeriod)*100)
$percentUpTime = 100 - $percentDowntime
"$percentDowntime% downtime and $percentUpTime% uptime."

現在腳本專家要言歸正傳,討論最先提出的問題:正常運行時,何時會停機?現在您應該明白,要討論正常執行時間就一定要考慮停機時間。 如果您覺得有趣,不妨查看 TechNet 上的更多 “您好,腳本專家”專欄,或訪問 腳本中心

版本問題

特約編輯 Michael Murgolo 在他的可擕式電腦上測試 Calculate­System­Up­timeFromEventLog.ps1 腳本時,遇到了一個非常惱人的錯誤。 我將此腳本交給朋友 Jit 進行測試,他也遇到了相同的錯誤。 是什麼錯誤呢?如下所示:

PS C:\> C:\fso\CalculateSystemUpTimeFromEventLog.ps1
Cannot index into a null array.
At C:\fso\CalculateSystemUpTimeFromEventLog.ps1:36 char:43 + $uptime = 
$currentTime - $sortedList.keys$ <<<< ($sortedList.Keys.Count-1)
Total up time on LISBON since 09/02/2008 00:00:00 is 0.00 minutes.
100.00% downtime and 0% uptime.

“無法對空陣列進行索引”,該錯誤指示陣列的創建不正確。 因此我研究了創建該陣列的代碼:

ForEach($event in $events)
{
 $sortedList.Add( $event.timeGenerated,
 $event.eventID )
} #end foreach event

結果表明,代碼沒問題。 導致出現此錯誤的原因究竟是什麼呢?

接著我決定查看 SortedList 物件。 為此,我編寫了一個簡單腳本來創建 System.Collections.SortedList 類的實例,並在其中添加了一些資訊。 此時,我使用 key 屬性顯示出索引鍵的清單。 該代碼如下所示:

$aryList = 1,2,3,4,5
$sl = New-Object Collections.SortedList
ForEach($i in $aryList)
{
 $sl.add($i,$i)
}

$sl.keys

這段代碼在我的電腦上能夠正常運行,但在 Jit 的電腦上運行失敗。 真是差勁!但至少它為我指出了正確的方向。 原來是 Windows PowerShell 1.0 中的 System.Collections.SortedList 有問題。 而我剛好運行的是尚未發佈的最新版 Windows PowerShell 2.0,在此版本中已經修復該錯誤,因此代碼能正常運行。

那麼,我們的腳本該如何應變?我發現,SortedList 類有一個名為 GetKey 的方法,此方法對 Windows PowerShell 1.0 和 Windows PowerShell 2.0 都適用。 因此針對 1.0 版本的腳本,我們讓代碼改用 GetKey 而非遍歷索引鍵集合。 在 2.0 版本的腳本中,我們添加一個要求 Windows PowerShell 2.0 版的標記。 如果您試圖在 Windows PowerShell 1.0 機器上運行此腳本,該腳本會直接退出,而不會出現錯誤。

Michael 還指出了一些與設計相關的考慮事項,不過這些並非錯誤。 他提到電腦在處於休眠或睡眠狀態時,腳本將無法正確檢測正常執行時間。 確實是這樣,因為我們並未檢測或查找這些事件。

不過事實上,我並不關心自己的可擕式電腦或桌上型電腦的正常執行時間。 我只關心伺服器上的正常執行時間,而我還沒碰到過伺服器睡眠或休眠的情況。 當然這有可能發生,使用這種方法來節約資料中心的電能也許是一種很有趣的方式,只是到目前為止我還都沒碰到過這種情況。 如果您的伺服器處於休眠狀態,請記得告訴我哦。 您可以通過電子郵件與我聯繫: Scripter@Microsoft.com

Scripto 博士的腳本謎題

每月挑戰不僅測試您解決謎題的能力,而且測試您的腳本編寫技能。

2008 年 12 月:PowerShell 命令

以下清單包含 21 個 Windows PowerShell 命令。 框中包含與清單相同的命令,但是被隱藏起來了。 您的工作就是找出這些以水準、垂直、對角(正向或反向)方式隱藏的命令。

EXPORT-CSV FORMAT-LIST FORMAT-TABLE
GET-ACL GET-ALIAS GET-CHILDITEM
GET-LOCATION INVOKE-ITEM MEASURE-OBJECT
NEW-ITEMPROPERTY OUT-HOST OUT-NULL
REMOVE-PSSNAPIN SET-ACL SET-TRACESOURCE
SPLIT-PATH START-SLEEP STOP-SERVICE
SUSPEND-SERVICE WRITE-DEBUG WRITE-WARNING

\\msdnmagtst\MTPS\TechNet\issues\en\2008\12\HeyScriptingGuy - 1208\Figures\puzzle.gif

答案是:

Scripto 博士的腳本謎題

答案是:2008 年 12 月:PowerShell 命令

fig12.gif

Ed Wilson 是 Microsoft 的高級顧問,也是知名的腳本專家。 他是 Microsoft 認證培訓師,為世界各地的 Microsoft Premier 客戶組織召開了廣受歡迎的 Windows PowerShell 研討會。 他曾編寫了八本著作,其中有多本著作探討 Windows 腳本;此外,他也為許多其他書籍做出了貢獻。 Ed 擁有 20 多個行業證書。

Craig Liebendorfer 是語言藝術家,也是 Microsoft Web 的長期編輯。 Craig 一直無法相信他可以每天靠舞文弄墨來維持生計。 無厘頭式的幽默是他的最愛之一,因此他應該非常適合這個工作。 Craig 認為美麗動人的女兒是自己一生最大的成就。