読者です 読者をやめる 読者になる 読者になる

素敵なおひげですね

PowerShellを中心に気分で書いているブログです。

Test-Connectionが遅い理由と対策方法について

はじめに

きっかけは@Pyromaniaさんのこのツイートから。

ツイートではLinuxサーバーについて触れていますが時間がかかるのはWindowsに対しても同様です。

本エントリではPowerShellにおけるPingであるTest-Connectionコマンドレットの動作が遅い理由とその対策について触れていきます。

Test-Connectionの実装

ILSpy等でTest-Connectionの実装を調べてみると、Test-ConnectionはWMIのWin32_PingStatusクラスを使ってPingを行っており、厳密には一致していませんが以下の様なWQLを内部で発行しています。

SELECT *
  FROM Win32_PingStatus
 WHERE Address = `[-Destination]` //複数宛先ある場合はORで連結
   AND TimeToLive = [-TimeToLive]
   AND BufferSize = [-BufferSize]

単純にWin32_PingStatusを使うだけであればICMPプロトコルしか使わないため、最初に触れた様なNetBIOS Name Queryは発行されません。
コマンドレットの実装だけを見ると遅くなる要因が無い様に見受けられます。

Test-Connectionが遅い理由

ではどこでNetBIOS Name Queryが発行されているのかというと、その原因はTest-Connectionの戻り値にあります。

Test-Connectionでは-Quietパラメーターを指定しない場合はWin32_PingStatusクラス*1のオブジェクトをそのまま返します。

この戻り値に対してPowerShell側でIPV4AddressおよびIPV6AddressというScriptPropertyが付与されており、Get-Memberを使って定義を確認してみると、

PS C:\> Test-Connection 192.168.133.12 -Count 1 | Get-Member -View Extended | Format-List

・・・(中略)・・・

TypeName   : System.Management.ManagementObject#root\cimv2\Win32_PingStatus
Name       : IPV4Address
MemberType : ScriptProperty
Definition : System.Object IPV4Address {get=$iphost = [System.Net.Dns]::GetHostEntry($this.address)
                         $iphost.AddressList | ?{ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork
              } | select -first 1;}

TypeName   : System.Management.ManagementObject#root\cimv2\Win32_PingStatus
Name       : IPV6Address
MemberType : ScriptProperty
Definition : System.Object IPV6Address {get=$iphost = [System.Net.Dns]::GetHostEntry($this.address)
                         $iphost.AddressList | ?{ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork
             V6 } | select -first 1;}

とプロパティ内で[System.Net.Dns]::GetHostEntry()メソッドを発行しIPアドレスの逆引きをしていることがわかります。
このメソッドは内部でgethostbyaddr()関数を使用していますのでNetBIOS Name Queryを含めた名前解決*2が実行され、その結果待ちにより処理が遅くなってしまうのです。

この問題は、

Please feel free to provide feedback or file bugs here.
  • 1 vote
  • 0 comments

Test-Connection Performance With IP and Output

Votes from Connect: 3

Original Date Submitted: 7/23/2015 12:20:40 AM

Description:
********Contact Information********
Handle: John.Bevan
Site Name: PowerShell
Feedback ID: 1578010
***************************************

Frequency: PowerShell ISE
Regression: Run the below code / vie...

windowsserver.uservoice.com

と既にフィードバックされているのですが、あまり対応される空気を感じません...
Test-Connectionは普通に使うと遅いものと割り切るのが現実的な気がします。

対策方法

Test-Connectionコマンドレットの遅さに対しては幾つかの対策を行う事ができますので以下に記載していきます。

1. Pingコマンドを使う

身も蓋もない方法ですが、単純にコンソール上からPingの結果だけ見たいのであればTest-Connectionを使わずに従来通りPingコマンドを使うのが一番手っ取り早いでしょう。

2. -Quietオプションを使う

スクリプト中でPingによる疎通確認を行いその結果だけが必要であれば-Quietオプションを指定するのが現実的です。
-Quietオプションを指定した場合のTest-Connectionの戻り値はBoolean型になりますので遅延の原因であるIPV4AddressIPV6Addressプロパティを気にせずに済みます。

3. コマンドレットの結果を変数に代入する

Test-Connectionの結果を使いつつ遅延を防ぎたい場合は、実行結果を一度変数に代入するのが効果的です。

Test-ConnectionではIPV4AddressIPV6Addressプロパティがコンソール上での表示対象になっており、表示の際にプロパティへのアクセスが発生して[System.Net.Dns]::GetHostEntry()メソッドが呼び出されてしまいます。
一旦結果を変数に代入すればIPV4AddressIPV6Addressプロパティへのアクセスを抑止できます。

4. Select-Objectで出力するプロパティを絞る

前項の方法と考え方は同じです。
コンソール上でTest-Connectionを使う場合、Select-Objectを使ってコンソールに表示するプロパティを絞ることでIPV4AddressIPV6Addressプロパティへのアクセスを抑えることができます。

コンソールに表示させなければ良いので、Select-Objectの代わりにFormat-*なコマンドレットで絞っても同様の効果を得ることができます。

【2016/06/17追記】5. Remove-TypeDataを使う

本エントリを公開後、どうにかしてIPV4AddressIPV6Addressプロパティを削除できないか調べたところ、Remove-TypeDataが使えることがわかりました。

Remove-TypeDataPowerShellで独自に追加された型データの情報を削除するコマンドレットになります。
削除はそのセッション中のみ有効でpowershell.exeを再起動するなどして新しいセッションができると型データの情報は復活します。

最初にWin32_PingStatusクラスの型System.Management.ManagementObject#root\cimv2\Win32_PingStatusの型データの情報を確認してみると、

PS C:\> (Get-TypeData System.Management.ManagementObject#root\cimv2\Win32_PingStatus).Members | ft -AutoSize

Key         Value
---         -----
IPV4Address System.Management.Automation.Runspaces.ScriptPropertyData
IPV6Address System.Management.Automation.Runspaces.ScriptPropertyData

の様にIPV4AddressIPV6Addressプロパティがあることがわかります。
これに対して、Remove-TypeDataを以下の様に実行します。

PS C:\> Remove-TypeData System.Management.ManagementObject#root\cimv2\Win32_PingStatus

これでSystem.Management.ManagementObject#root\cimv2\Win32_PingStatusに対する型データは消えるのでTest-Connectionを実行してもIPV4AddressIPV6Addressプロパティが付与されることは無くなります。


【2016/06/20追記】

あえとすさんよりUpdate-FormatDataを使った方法を指摘して頂きました。

tech.blog.aerie.jp

上のエントリ内で指摘されている様にIPV4AddressIPV6Addressプロパティに依存する処理があった場合Remove-TypeDataを使う方法ではエラーとなってしまうので対応としては確かに乱暴だと思います。
Remove-TypeDataは極力使わずUpdate-FormatDataを使う方が良いでしょう。

【追記ここまで】


実際に確認してみる

簡単な環境で動作を確認してみましたのでその結果を記載します。
2台のWindows Server 2012 R2(PowerShell 4.0)の仮想マシンfromsv(192.168.133.11)からwindestsv(192.168.133.12)に対してPingおよびTest-Connectionを実行し、その結果をWiresharkでキャプチャしました。
簡単のためにIPV6は無効にし、DNSの設定*3もしていません。

検証環境として雑だと自分でも思っていますので結果については軽く見てもらえると助かります。

0. Test-Connectionを使った場合

まずはTest-Connectionを普通に使った場合を見てみます。
試行回数はデフォルトの4回を一応明示しています。DNSは設定していませんが念のためにipconfig /flushdnsをしています。

ipconfig /flushdns
Test-Connection 192.168.133.12 -Count 4

実行結果はこんな感じです。

f:id:stknohg:20160616223307p:plain

IPV4AddressIPV6Addressプロパティがコンソールに表示されアクセスされるのが遅延の原因であるためMeasure-Commandによる時刻計測はしていません。
処理時間はパケットキャプチャの結果で判断しています。

キャプチャの結果は以下となります。

f:id:stknohg:20160616223545p:plain

(中略)

f:id:stknohg:20160616223621p:plain

ICMPパケット(赤色の部分)の合間にNetBIOS Name Queryのパケットが流れていることが分かります。
4回目のPingが終わった最後のパケットでは約22秒も経過していることがわかります。

圧倒的な遅さです(

1. Pingコマンドを使う

ここから各対策の結果を記載していきます。
最初はPingコマンドの結果です。

ipconfig /flushdns
ping 192.168.133.12 -n 4

f:id:stknohg:20160616222450p:plain

f:id:stknohg:20160616222656p:plain

当然ですがICMPパケットしかありません。
時間は約3秒でこれがユーザーが期待する普通の結果でしょう。

2. -Quietオプションを使う

次に-Quietオプションを使った場合です。

ipconfig /flushdns
Test-Connection 192.168.133.12 -Count 4 -Quiet

f:id:stknohg:20160616224352p:plain

f:id:stknohg:20160616224405p:plain

こちらもICMPパケットしか飛ばさないのでPingコマンドと同等の時間となっています。

3. コマンドレットの結果を変数に代入する

コマンドの結果を変数に代入した場合です。

ipconfig /flushdns
$Results = Test-Connection 192.168.133.12 -Count 4

f:id:stknohg:20160616224647p:plain

f:id:stknohg:20160616224703p:plain

この場合もICMPパケットしか飛ばしていません。
ここで以下の様にプロパティにアクセスしてみると、

$Results[0].ResponseTime

$Results[0].IPV4Address

f:id:stknohg:20160616224855p:plain

IPV4Addressプロパティにアクセスした時点で以下の様にNetBIOS Name Queryが発行されました。

f:id:stknohg:20160616225009p:plain

4. Select-Objectで出力するプロパティを絞る

最後は出力するプロパティを絞った場合です。
画面表示の都合、Select-Objectの代わりにFormat-Tableを使いました。

ipconfig /flushdns
Test-Connection 192.168.133.12 -Count 4 | ft Address,ResponseTime -AutoSize

f:id:stknohg:20160616225322p:plain

f:id:stknohg:20160616225335p:plain

こちらも対策の効果が出ていることがわかります。

【2016/06/17追記】5. Remove-TypeDataを使う

Remove-TypeDataを使ってIPV4AddressIPV6Addressプロパティを削除した場合の結果です。

ipconfig /flushdns
Remove-TypeData System.Management.ManagementObject#root\cimv2\Win32_PingStatus
Test-Connection 192.168.133.12 -Count 4

f:id:stknohg:20160617122706p:plain

f:id:stknohg:20160617122721p:plain

コンソール上IPV4AddressIPV6Addressの表示欄はありますが値が設定されておらず、キャプチャの結果もICMPパケットしか飛んでいないことがわかります。
もちろん処理時間も改善されています。

最後に

とりあえずこんな感じです。

率直に言ってIPV4AddressIPV6AddressScriptPropertyを付けたのは万死に値するレベルの失策だと思います。
互換性を考えると実現は難しでしょうが、このプロパティを無くすかデフォルトの表示項目から外してほしい感じです。

*1:PowerShell上は System.Management.ManagementObject#root\cimv2\Win32_PingStatus という型で表現されています

*2:厳密にはHostsファイル、DNS、NetBIOSでの名前解決が行われます

*3:実環境ではDNSのパケットも計測されると思います