しばたテックブログ

気分で書いている技術ブログです。

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-TypeDataはPowerShellで独自に追加された型データの情報を削除するコマンドレットになります。
削除はそのセッション中のみ有効で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のパケットも計測されると思います

Visual Studio CodeのターミナルをPowerShellに変える際に注意すべきこと

【2017/10/13追記】

Visual Studio Codeのバージョンも新しくなり、本エントリの注意点もあまり役に立たなくなってしまいました。

せっかくなのでVisual Studio Code PowerShell拡張の設定に関する以下のエントリのリンクを張っておきます。
Visual Studio Code上でPowerShellの挙動が変わった・おかしいと感じた時は下記エントリの設定を確認してみるのも良いかと思います。

blog.shibata.tech

blog.shibata.tech

【追記ここまで】


Visual Studio Codeのバージョン1.2.0からターミナル機能が搭載され、早速このターミナルをPowerShellに変える方法が紹介されています。

https://blogs.msdn.microsoft.com/ayatokura/2016/06/10/vscode_terminal_powershell/blogs.msdn.microsoft.com

pglib.hateblo.jp

本エントリではターミナルをPowerShellに変える際に注意すべきことについて記載します。

1. powershell.exeのアーキテクチャ

Windows版のVisual Studio Codeは32bitアプリケーションです。


【2017/07/21追記】

Visual Studio Code Ver.1.14.1からWindowsにおいても64bit版が提供されました。
以下の内容は32bit版のVisual Studio Codeに関するものとなります。

【追記ここまで】


64bitOSで上記エントリの手順を単純に設定して、

"terminal.integrated.shell.windows": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" 

とすると、起動されるpowershell.exeは32bit版となります。

これはWOW64のシステムフォルダに対するリダイレクション機能によりC:\Windows\System32フォルダへのアクセスがC:\Windows\SysWOW64にリダイレクトされるためです。
リダイレクションの詳細についてはこちらを参考にしてください。

例としてターミナルを起動してみると、以下の図の様に$PSHOMEはWOW64配下になりIntPtrのサイズは4Byteとなっています。

f:id:stknohg:20160610175247p:plain

このため64bitOS上で64bit版のpowershell.exeを起動したい場合は、

"terminal.integrated.shell.windows": "C:\\Windows\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe" 

と、C:\Windows\Sysnativeフォルダを指定する必要があります。
こうすることで32bitアプリから64bit版のpowershell.exeを起動することができます。

f:id:stknohg:20160610175344p:plain

2. 日本語表示の問題

【2016/07/08追記】 この問題はバージョン1.3.0で解決されました。


現時点でのターミナルでは日本語の表示に難がある様でターミナルのウィンドウサイズによって文字が潰れて表示されてしまう場合があります。

綺麗な表示の場合

f:id:stknohg:20160610175644p:plain

潰れた表示の場合

f:id:stknohg:20160610175656p:plain

こちらについては、

github.com

でCJKまとめた形で対応中の様です。
Issue自体はクローズに向かっているので次のリリースでは直っているんじゃないかと思います。

3. 日本語入力の問題

【2016/08/05追記】 この問題はバージョン1.4.0で解決されました。

1.4.0ではこの他にターミナル上でのコピペがサポートされる様になりました。
コピーしたい範囲を選択して、Ctrl+Shift+Cでコピー、Ctrl+Shift+Vでペーストできます。
右クリックでメニューも出ます。


最初にこのエントリを書いた当時は気がつかなかったのですが、現時点ではターミナル上でIMEが有効にならない問題があります。

github.com

こちらはバージョン1.4.0で修正予定だそうです。

最後に

とりあえずこんな感じです。
他にも何かあれば追記していきます。