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

素敵なおひげですね

主にWindowsな技術ブログ。最近はPowerShellなネタが多めです。

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

Windows PowerShell

はじめに

きっかけは@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のパケットも計測されると思います

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

PowerShell vscode

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

blogs.msdn.microsoft.com

pglib.hateblo.jp

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

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

Windows版のVisual Studio Codeは32bitアプリケーションです。
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. 日本語表示の問題

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

綺麗な表示の場合

f:id:stknohg:20160610175644p:plain

潰れた表示の場合

f:id:stknohg:20160610175656p:plain

こちらについては、

github.com

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

最後に

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

PowerShellでmountvolコマンドを代用する

PowerShell

ディスクボリュームのマウント・アンマウントやドライブレターの割り当てを行うのに便利なmountvolコマンドですが、本エントリではこれをPowerShellで代用する方法について説明します。

Storage Cmdletの使用

Windows 8/Windows Server 2012以降であれば、Storage Cmdletに追加されたコマンドレットで概ね代用できます。
ただし、一部の機能についてはコマンドレットでは代用できずWMIでWin32_Volumeクラスの機能を利用する必要があります。

PowerShellでmountvolコマンドを代用する

最初にWindows Server 2012 R2でmountvolコマンドのヘルプを見ると以下の様になっています。

PS C:\> mountvol /?
ボリューム マウント ポイントを作成、削除、一覧を表示します。

MOUNTVOL [ドライブ:]パス ボリューム名
MOUNTVOL [ドライブ:]パス /D
MOUNTVOL [ドライブ:]パス /L
MOUNTVOL [ドライブ:]パス /P
MOUNTVOL /R
MOUNTVOL /N
MOUNTVOL /E

    パス        マウント ポイントを常駐させる既存の NTFS ディレクトリ
                を指定します
    ボリューム名
                マウント ポイントのターゲットとなるボリューム名を指定しま
                す。
    /D          指定されたディレクトリからボリューム マウント ポイント
                を削除します。
    /L          指定されたディレクトリのマウントされているボリューム
                の一覧を表示します。
    /P          指定されたディレクトリからボリューム マウント ポイントを削除
                してボリュームをマウント解除し、ボリュームをマウントできな
                くします。
                ボリューム マウント ポイントを作成して、もう一度ボリュームを
                マウントできるようにします。
    /R          システムに存在しないマウント ポイント ディレクトリとレジストリ
                設定を削除します。
    /N          新しいボリュームの自動マウントを無効にします。
    /E          新しいボリュームの自動マウントを再び有効にします。

これからこの各オプションについてどう代用するか説明していきます。*1

ボリューム情報を表示する(MOUNTVOL /L)

ボリュームの表示はGet-Volumeコマンドが使えます。
mountvolコマンドと同等の情報を出力するには以下の様にすればOKです。

Get-Volume | Format-Table ObjectID, DriveLetter -AutoSize

実行例は以下。

PS C:\> Get-Volume | Format-Table ObjectID, DriveLetter -AutoSize

ObjectID                                          DriveLetter
--------                                          -----------
\\?\Volume{a8cbd0fc-83fc-11e4-80b3-806e6f6e6963}\           
\\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\           
\\?\Volume{a8cbd0fd-83fc-11e4-80b3-806e6f6e6963}\           C
\\?\Volume{99d153e8-2248-11e6-80d0-08002724e199}\           D    

ただし、ボリュームをドライブではなくディレクトリ上にマウントした場合はWin32_VolumeクラスのNameプロパティから情報をとる必要があります。
以下はボリューム\\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\D:\OtherDrive\にマウントした場合の例になります。

PS C:\> Get-WmiObject -Class Win32_Volume | Format-Table DeviceID, Name

DeviceID                                                    Name
--------                                                    ----
\\?\Volume{a8cbd0fc-83fc-11e4-80b3-806e6f6e6963}\           \\?\Volume{a8cbd0fc-83fc-11e4-80b3-806e6f6e6963}\
\\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\           D:\OtherDrive\
\\?\Volume{a8cbd0fd-83fc-11e4-80b3-806e6f6e6963}\           C:\
\\?\Volume{99d153e8-2248-11e6-80d0-08002724e199}\           D:\

ドライブレターを割り当てる(MOUNTVOL)

ドライブレターの割り当てについて、当初Set-Volumeが使えるのかなと思っていたのですが、こいつはボリュームのラベルを設定するためのコマンドレットでドライブレターの割り当てには使う事ができませんでした...
かなりわかりにくいのですが、ドライブレターの割り当てはボリュームからではなく、Add-PartitionAccessPathコマンドレットを使ってパーティションからアクセスパスを指定する形で行う必要があります。

上記の例でボリューム\\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\にドライブレターEを割り当てる場合は以下の様にします。

$Drive = "E:\"
$ID = '\\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\'
Get-Volume -ObjectId $ID | Get-Partition | Add-PartitionAccessPath -AccessPath $Drive

結果を確認すると以下の様になりドライブレターが割り当たっていることが確認できます。

PS C:\> Get-Volume | Format-Table ObjectID, DriveLetter -AutoSize

ObjectID                                          DriveLetter
--------                                          -----------
\\?\Volume{a8cbd0fc-83fc-11e4-80b3-806e6f6e6963}\           
\\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\           E
\\?\Volume{a8cbd0fd-83fc-11e4-80b3-806e6f6e6963}\           C
\\?\Volume{99d153e8-2248-11e6-80d0-08002724e199}\           D

ちなみに、Win32_Volumeクラスを使ってドライブレターを割り当てる場合はAddMountPointメソッドを使います。
Storage Cmdletが使えない環境では以下の様にすれば良いでしょう。

$Drive = "E:\"
$ID = '\\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\'
$Volume = Get-WmiObject -Class Win32_Volume | Where-Object { $_.DeviceID -eq $ID }
$Volume.AddMountPoint($Drive)

ドライブレターの割り当てを解除する(MOUNTVOL /D)

ドライブレターの割り当てを解除するにはRmove-PartitionAccessPathコマンドレットを使います。
ドライブレターがわかっていればGet-Partitionから直ちにパーティションの情報はとれますので、以下の様にしてアクセスパスを削除してやればドライブレターも解除されます。

Get-Partition -DriveLetter "E" | Remove-PartitionAccessPath -AccessPath "E:\"

実行結果は以下となりドライブレターの割り当てがなくなっていることがわかります。

PS C:\> Get-Volume | Format-Table ObjectID, DriveLetter -AutoSize

ObjectID                                          DriveLetter
--------                                          -----------
\\?\Volume{a8cbd0fc-83fc-11e4-80b3-806e6f6e6963}\           
\\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\           
\\?\Volume{a8cbd0fd-83fc-11e4-80b3-806e6f6e6963}\           C
\\?\Volume{99d153e8-2248-11e6-80d0-08002724e199}\           D

また、Win32_Volumeクラスを使う場合はDriveLetterプロパティを$nullにすればOKです。
設定後Put()メソッドを使い変更を確定させる必要があります。

$Drive = "E:"
$Volume = Get-WmiObject -Class Win32_Volume | Where-Object { $_.DriveLetter -eq $Drive }
$Volume.DriveLetter = $null
$Volume.Put()

ボリュームをアンマウントする(MOUNTVOL /P)

ボリュームをアンマウントするのはStorage Cmdletから出来ず、Win32_VolumeクラスのDismount()メソッドを使うしかない様です。

Dismount()メソッドを使うには、あらかじめドライブレターを解除しておく必要があり、MOUNTVOL /Pと同等にするには第2引数を$trueにする必要があります。
これらをまとめると以下の様にすることでアンマウントできます。

$Drive = "E:"
$Volume = Get-WmiObject -Class Win32_Volume | Where-Object { $_.DriveLetter -eq $Drive }
$Volume.DriveLetter = $null
$Volume.Put()
$Volume.Dismount($false, $true)

尚、MOUNTVOL /DMOUNTVOL /Pの違いについては以下の記事を参考にしてください。

blogs.technet.microsoft.com

この記事の内容を踏まえて実行結果を確認すると以下の様になります。

PS C:\> Get-Volume | Format-Table ObjectID, DriveLetter -AutoSize

ObjectID                                          DriveLetter
--------                                          -----------
\\?\Volume{a8cbd0fc-83fc-11e4-80b3-806e6f6e6963}\           
\\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\           
\\?\Volume{a8cbd0fd-83fc-11e4-80b3-806e6f6e6963}\           C
\\?\Volume{99d153e8-2248-11e6-80d0-08002724e199}\           D

PS C:\> mountvol

・・・(中略)・・・

現在のマウント ポイントとボリューム名の考えられる値:

    \\?\Volume{a8cbd0fc-83fc-11e4-80b3-806e6f6e6963}\
        *** マウント ポイントなし ***

    \\?\Volume{2ddbd13b-25b8-11e6-80d2-08002724e199}\
        *** ボリューム マウント ポイントが作成されるまでマウントできません ***

    \\?\Volume{a8cbd0fd-83fc-11e4-80b3-806e6f6e6963}\
        C:\

    \\?\Volume{99d153e8-2248-11e6-80d0-08002724e199}\
        D:\

新しいボリュームの自動マウントの設定を行う(MOUNTVOL /N)(MOUNTVOL /E)

新しいボリュームの自動マウント設定(MOUNTVOL /NおよびMOUNTVOL /EまたはDISKPART AUTOMOUNT)について、PowerShellで代替するにはレジストリの値を直接変更するしかない様です。

該当するレジストリキーは、Mountvol /N and diskpart automount disableKB822653によれば、

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MountMgr\NoAutoMountで、
0が"自動マウントする"、1が"自動マウントしない"との事です。

ただ、私が検証した範囲だとレジストリの変更とDISKPART AUTOMOUNTコマンドの結果が同期しませんでした...
原因は特定できなかったのですがこの程度であればPowerShellで代替せずmountvolコマンドをそのまま使った方が手っ取り早く確実でしょう。

システムに存在しないマウントポイント設定をクリアする(MOUNTVOL /R)

残念ながらMOUNTVOL /RPowerShellで代替することはできない様で、この場合は素直にmountvolコマンドを使うしかないです。
正直なところあまり使用頻度の高いコマンドとも思えませんので代替できなくても問題ないと思います。

最後に

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

ここまでの内容でわかるかと思いますが、mountvolコマンドとStorage Cmdlet、Win32_Volumeクラスの機能は綺麗に対比できる感じにはなっていません。

なので現実的にPowerShellで何かしようとする場合は両方を使い分ける必要があると思います。
基本はStorage Cmdletを使いつつ、どうしようもない場合はmountvolコマンドで対処するのが良いのではないでしょうか。

Storage cmdletで統一して操作できる様になればうれしいのですが、それはもうしばらく先の話になりそうです。

*1:MOUNTVOL /Sオプションについては情報を集めることができなかったので今回はスルーします...