Hey, Scripting Guy!サーバーの稼働時間を計算する

Microsoft Scripting Guys

コードのダウンロード: HeyScriptingGuy2008_12.exe(152 KB)

稼働しているか稼働していないか、これはかなり明確なことのように思えますが、サーバーの稼働時間に関してはその限りではありません。稼働時間を知るには、ダウンタイムを知る必要があります。ほぼすべてのネットワーク管理者は、サーバーの稼働時間について心配しています (言い換えれば、ダウンタイムについて心配しているとき以外は稼働時間について心配しています)。その理由は、ほとんどの管理者には目標とする稼働時間があり、稼働状況レポートを上層部に提出する必要があるからです。

でも、それがどうしたというのでしょうか。稼働時間は Win32_OperatingSystem WMI クラスを使用すれば確認できるように思われます。このクラスには、このような操作を大幅に単純化してくれる、LastBootUpTime と LocalDateTime という 2 つのプロパティが含まれています。皆さんは、この作業は LocalDateTime から LastBootUptime を減算するだけで完了し、その後夕食前にゴルフの 9 ホールを回りきることだってできると考えるでしょう。

そこで、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 時刻値を減算しようとすると返されるエラー (図をクリックすると拡大表示されます)

このエラー メッセージはちょっとした誤解を招く可能性があります。"Bad numeric constant" (数値定数が正しくありません) ですって。どういうことでしょうか。数値と定数の定義はわかりますが、これが時刻とどう関係しているのでしょうか。

奇妙なエラー メッセージが表示されたときは、そのスクリプトで解析しようとしているデータを直接確認するのが一番です。また一般に、使用されているデータの型を 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

ある時刻を別の時刻から減算する必要がある場合は、文字列値ではなく時刻値を使用するようにしてください。この変換は、ConvertToDateTime メソッドを使用して簡単に行うことができます。このメソッドは、Windows PowerShell によってすべての WMI クラスに追加されます。

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

ある時刻値を別の時刻値から減算すると、System.TimeSpan オブジェクトのインスタンスが残ります。つまり、あまり多くの計算を実行することなく、稼働時間に関する情報を表示できます。必要な操作は、表示するプロパティを選択することだけです (できれば稼働時間は TotalMilliseconds ではなく TotalDays で計算した方がよいでしょう)。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

この方法の問題は、サーバーを前回再起動した後の稼働時間のみが返されることです。つまり、ダウンタイムは計算されません。稼働時間はダウンタイムから導き出されます。つまり、稼働時間を計算するには、まずダウンタイムを知る必要があります。

サーバーのダウンタイムの長さを知るには、どうすればよいでしょうか。それには、サーバーの起動時刻とシャットダウン時刻を知る必要があります。この情報はシステム イベント ログから取得できます。サーバーやワークステーション上で開始される最初のプロセスの 1 つはイベント ログです。また、サーバーがシャットダウンされるときに最後に停止するプロセスの 1 つもイベント ログです。このようなそれぞれの開始イベントと停止イベントによって、イベント ログが開始されるときにイベント ID 6005 が生成され、イベント ログが停止するときにイベント ID 6006 が生成されます。図 2 は、イベント ログの開始例を示しています。

fig02.gif

図 2 コンピュータの起動直後に開始されるイベント ログ サービス (画像をクリックすると拡大表示されます)

システム ログから 6005 と 6006 のイベントを収集して並べ替え、停止時刻から開始時刻を減算することによって、サーバーを再起動してからもう一度再起動するまでの間に発生したサーバーのダウンタイムの長さを特定できます。その後、特定した時間を対象期間の長さ (分単位) から減算すると、サーバーの稼働率を算出できます。これが CalculateSystemUpTimeFromEventLog.ps1 スクリプトで使用される手法です (図 3 参照)。

図 3 CalculateSystemUpTimeFromEventLog

#---------------------------------------------------------------
# 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 ステートメントを使用して、いくつかのコマンド ライン パラメータを定義します。パラメータの値は、コマンド ラインからスクリプトを実行するときに変更できます。1 つ目のパラメータは $NumberOfDays です。これを使用すると、稼動状況レポートに使用する日数を指定できます (このスクリプトでは、皆さんがパラメータの値を指定することなくスクリプトを実行できるように、既定値の 30 日を指定しています。もちろん、この値は必要に応じて変更できます)。

2 つ目の [switch]$debug は、スイッチ パラメータです。スクリプトを実行するとき、このパラメータをコマンド ラインに追加すると、スクリプトから特定のデバッグ情報を取得できます。この情報は、スクリプトから取得した結果が正確であるかどうかを確認するのに役立ちます。たとえば、6006 のイベント ログ サービス停止メッセージが表示されない場合がありますが、これはおそらくサーバーで発生した致命的な障害によって、イベント ログへの書き込みができない状態になったためです。その結果、スクリプトが稼働時間の値を別の稼働時間の値から減算することになり、結果にずれが生じることになります。

コマンド ラインで $debug 変数を指定すると、その変数は Variable: ドライブに配置されます。この場合、$debugPreference の値は continue に設定されます。つまり、スクリプトが実行され続け、Write-Debug に指定されたすべての値が表示されます。$debugPreference の既定値は SilentlyContinue なので、$debugPreference の値を continue に設定しなかった場合、スクリプトが実行されても、Write-Debug に指定された値はすべて表示されません。

スクリプトが実行されると、結果の出力として、発生した各 6005 および 6006 イベント ログ エントリ (図 4 参照) と、算出された稼働時間が表示されます。この情報を使用して、結果が正確であるかどうかを確認できます。

fig04.gif

図 4 デバッグ モードでは、合計稼働時間に加算された各時刻値の記録が表示される (画像をクリックすると拡大表示されます)

次に、System.TimeSpan オブジェクトのインスタンスを作成します。New-Object コマンドレットを使用して、日付の差分計算に使用する既定の TimeSpan オブジェクトを作成することもできます。

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

ただし、Windows PowerShell では TimeSpan オブジェクトを作成するための New-TimeSpan コマンドレットが提供されるので、これを使用する方が理にかなっています。このコマンドレットを使用すると、スクリプトを簡単に読み込むことができます。作成されるオブジェクトは、New-Object を使用して作成される TimeSpan オブジェクトに相当します。

次に、$currentTime をはじめとするいくつかの変数を初期化します。$currentTime は、現在の時刻と日付の値を保持するために使用します。この情報は、Get-Date コマンドレットから取得できます。

$currentTime = get-Date

次に、起動とシャットダウンのイベント ID 番号を保持する 2 つの変数を初期化します。この処理は必須ではありませんが、2 つの変数が文字列リテラル値として埋め込まれないようにすれば、コードの読み込みやトラブルシューティングがより簡単になります。

次に、$minutesInPeriod という変数を作成します。この変数は、対象期間の分単位の長さを特定するために使用される計算の結果を保持します。

$minutesInPeriod = (24*60)*$NumberOfDays

最後に、$startingDate 変数を作成する必要があります。この変数は、報告期間の開始時刻を表す System.DateTime オブジェクトを保持します。この日時は、報告期間の開始日の午前 0 時です。

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

変数を作成したら、イベント ログからイベントを取得して、クエリの結果を $events 変数に格納します。イベント ログに対してクエリを実行するには、Get-EventLog コマンドレットを使用し、ログの名前として "system" を指定します。Windows PowerShell 2.0 では、–source パラメータを使用して、Where-Object コマンドレットで選別する必要がある情報の量を減らすことができます。Windows PowerShell 1.0 ではそのような選択肢が提供されないので、クエリによって返されたフィルタ処理されていないすべてのイベントを並べ替える必要があります。そのため、イベントを Where-Object コマンドレットにパイプして、該当するイベント ログ エントリを選別します。Where-Object フィルタを使用すれば、Scripting Guys が皆さんにパラメータを保持するための変数を作成してもらった理由がわかるでしょう。

コマンドの読み込みは、文字列リテラルを使用した場合よりもはるかに簡単になります。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 コマンドレットにオブジェクトをパイプし、その結果を変数に再度格納しても、オブジェクトの繰り返し処理を実行してその結果をハッシュ テーブルに格納すれば、そのリストの順序が並べ替え処理後のまま維持されるかどうかはわかりません。

こういったデバッグしにくい面倒な問題を回避するには、オブジェクトの既定のコンストラクタを使用して、System.Collections.SortedList オブジェクトのインスタンスを作成します。既定のコンストラクタは、日付を時系列に従って並べ替えるよう並べ替え済みリストに指示します。空の並べ替え済みリスト オブジェクトを $sortedList 変数に格納します。

$sortedList = New-object system.collections.sortedlist

並べ替え済みリスト オブジェクトを作成したら、プロパティに値を設定する必要があります。これを行うには、ForEach ステートメントを使用して、$entries 変数に格納されているイベント ログ エントリのコレクションを処理します。コレクションを処理するとき、$event 変数によってコレクション内での現在の位置が追跡されます。Add メソッドを使用して、System.Collections.SortedList オブジェクトに 2 つのプロパティを追加します。並べ替え済みリストには、キーと値のプロパティを追加できます (配列のようにコレクションにインデックスを設定できる点を除けば、これは Dictionary オブジェクトと似ています)。timeGenerated プロパティをキーとして追加し、eventID を値のプロパティとして追加します。

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

次に、サーバーの現在の稼働時間を計算します。これを行うには、並べ替え済みリスト内の最新のイベント ログ エントリを使用します。これに該当するのは常に 6005 のインスタンスです。その理由は、最新のエントリが 6006 であれば、サーバーはまだ稼働していないことになるからです。インデックスが 0 から始まるので、最新のエントリは Count-1 になります。

イベントの生成時刻の値を取得するには、並べ替え済みリストのキーのプロパティを確認する必要があります。インデックス値を取得するには、Count プロパティを使用し、そこから 1 を減算します。その後、先ほど値を設定した $currenttime 変数に格納されている日時値から、6005 イベントが生成された時刻を減算します。スクリプトがデバッグ モードで実行された場合のみ、この計算の結果が出力されます。このコードは次のようになります。

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

ここからは、並べ替え済みリスト オブジェクトを処理して、サーバーの稼働時間を計算します。System.Collections.SortedList オブジェクトを使用するので、リストにインデックスを設定できるという特性を活用します。それには、For ステートメントを使用します。先ほど、現在の稼働時間を計算するために Count-1 を使用したので、ここでは Count-2 から開始します。

稼働時間を取得するために逆方向にカウントするので、For ステートメントの 2 つ目の部分で指定されている条件は、"項目が 0 以上の場合" になります。For ステートメントの 3 つ目の部分では -- を使用します。これは、$item の値を 1 減らすことを示しています。–debug スイッチを使用してスクリプトを実行する場合は、Write-Debug コマンドレットを使用して、インデックス番号の値を出力します。また、文字 `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 (つまり起動のイベント ID) である場合は、先ほど算出したダウンタイムの値から開始時刻を減算して稼働時間を算出します。この値を $uptime 変数に格納します。デバッグ モードの場合は、Write-Debug コマンドレットを使用して値を画面に出力できます。

 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} を使用して、数値を 2 桁まで出力します。次に、稼働時間 (分単位) を、レポートに記入する期間の長さ (分単位) で除算して、ダウンタイムの割合を算出します。同じ形式指定子を使用して、値を小数第 2 位まで出力します。おまけとして、稼働率を算出し、両方の値を出力します。

"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 で公開されているその他の "Hey, Scripting Guy" コラムを参照するか、スクリプト センターを参照してください。

バージョンに関する問題

ラップトップで CalculateSystemUptimeFromEventLog.ps1 スクリプトをテストしていたとき、編集に携わっている Michael Murgolo が最も面倒なエラーに遭遇しました。友人の 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.

"Cannot index into a null array" (null 配列にインデックスを付けることはできません) というエラーは、配列が正しく作成されなかったことを示しています。そこで、配列を作成するコードを注意深く調べました。

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

結局、そのコードに問題はありませんでした。エラーの原因は何だったのでしょうか。

次に、SortedList オブジェクトを調べることにしました。そのために、System.Collections.SortedList クラスを作成し、そこにいくつかの情報を追加する単純なスクリプトを記述しました。ここでは、keys プロパティを使用して keys のリストを出力しました。これがそのコードです。

$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 の両方で動作します。PowerShell 1.0 向けのスクリプトについては、keys コレクションの繰り返し処理を実行するのではなく GetKey を使用するようにコードを変更します。PowerShell 2.0 向けのスクリプトには、Windows PowerShell 2.0 を必要とするタグを追加します。Windows PowerShell 1.0 がインストールされているコンピュータでこのスクリプトを実行しようとすると、単にスクリプトが終了するだけで、エラーは発生しません。

Michael は、バグではありませんが設計上検討する必要があることについても指摘しました。彼が気付いたのは、コンピュータを休止状態またはスリープ モードにしていると、スクリプトが稼働時間を正しく検出できないことです。これはそのとおりですが、私たちはこのような状況に遭遇することはありません。

実際のところ、私は自分のラップトップ コンピュータやデスクトップ コンピュータの稼働時間には関心がなく、確認する必要があるのはサーバーの稼働時間のみです。私はこれまで、スリープ モードや休止状態になっているサーバーを見たことがありません。もちろん、サーバーがスリープ モードや休止状態になる可能性もあり、これはデータ センターの電力を節約するという観点から見れば興味深いことですが、私はまだそのような状況に遭遇したことはありません。サーバーを休止状態にする場合はご一報ください。私の連絡先は Scripter@Microsoft.com (英語のみ) です。

Dr. Scripto のスクリプト パズル

パズルを解く能力だけでなく、スクリプト作成スキルもテストする月に 1 度の課題です。

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

解答:

Dr. Scripto のスクリプト パズル

解答: 2008 年 12 月: PowerShell のコマンド

fig12.gif

Ed Wilson はマイクロソフトのシニア コンサルタントであり、有名なスクリプトの専門家です。マイクロソフト認定トレーナーでもあり、彼が世界中のマイクロソフト プレミア サポートの顧客向けに開催している Windows PowerShell のワークショップは好評です。これまでに Windows スクリプトに関するいくつかの書籍を含む 8 冊の書籍を執筆し、その他にも多くの書籍の執筆に携わっています。Ed は 20 種類を超えるこの業界の資格を持っています。

Craig Liebendorfer は言葉を巧みに操る、マイクロソフトのベテラン Web 編集者です。Craig は毎日言葉にかかわって給料を受け取ることができる仕事が存在することが、いまだに信じられないと思っています。彼が大好きなことの 1 つは場違いなユーモアなので、まさにこのコラムにうってつけの人物です。彼は自分の立派な娘が、自分の人生における最も偉大な功績であると考えています。