最強高速並列爆速Ping

頭わるわるタイトル

複数のホストに対して、並列でPingを実行し、結果をまとめて出力するPowerShellスクリプトです。
`Powershell 5.1`以上の環境で動作します。

まず最初に、、、完成品のコードが↓になります。
`Target.txt` に、適当に対象のホストやIPを入れてps1を実行すると、疎通確認が行われます。
Ping結果は `Result.txt` に出力され、同時にコンソールにも表示されます。
```powershell
Add-Type -TypeDefinition @"
using System;
using System.IO;
using System.Threading;
using System.Collections.Concurrent;

public class FileLogger {
    private static readonly object _lock = new object();
    public static void WriteLog(string path, string message) {
        int retryCount = 0;
        while (retryCount < 5) {
            try {
                lock (_lock) {
                    File.AppendAllText(path, message + Environment.NewLine, System.Text.Encoding.UTF8);
                }
                break;
            } catch {
                Thread.Sleep(10);
                retryCount++;
            }
        }
    }

    public static void WriteConsole(string message) {
        lock (_lock) {
            Console.WriteLine(message);
        }
    }
}
"@ -Language CSharp

# ファイル名設定
$targetFile = "$PSScriptRoot\Target.txt"  # 入力ファイル
$resultFile = "$PSScriptRoot\Result.txt"  # 出力ファイル
Clear-Content $resultFile

# ホストリスト取得
$targets = Get-Content $targetFile
$totalTargets = $targets.Count

# 結果保存用のスレッドセーフ辞書
$syncHash = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new()

# 並列処理（Runspace）
$runspaces = @()
$pool = [runspacefactory]::CreateRunspacePool(1, 100)  # 最大100並列
$pool.Open()

$completedCount = 0  # 進捗管理用カウンタ

foreach ($target in $targets) {
    $runspace = [powershell]::Create().AddScript({
        param ($hostName, $syncHash)

        $status = if (Test-Connection -ComputerName $hostName -Count 1 -Quiet) { "Alive" } else { "Dead" }
        $message = "$hostName`t$status"  # タブ区切り

        # コンソール出力
        [FileLogger]::WriteConsole($message)

        # 結果をスレッドセーフな辞書に格納
        $syncHash[$hostName] = $message

    }).AddArgument($target).AddArgument($syncHash)
    
    $runspace.RunspacePool = $pool
    $runspaces += @{ Pipe = $runspace; Async = $runspace.BeginInvoke() }
}

# 全Runspaceの完了を待機しながら進捗表示
foreach ($runspaceData in $runspaces) {
    $runspaceData.Pipe.EndInvoke($runspaceData.Async)
    $runspaceData.Pipe.Dispose()

    # 進捗を更新
    $completedCount++
    $progressPercent = [math]::Round(($completedCount / $totalTargets) * 100, 2)
    Write-Progress -Activity "Ping 実行中..." -Status "$completedCount / $totalTargets 完了" -PercentComplete $progressPercent
}

$pool.Close()
$pool.Dispose()

# **Target.txt の順番通りに Result.txt に書き込み**
Remove-Item -Path $resultFile -ErrorAction Ignore  # 既存のファイルを削除
Write-Host "Ping結果をResult.txtに書き込んでいます"

$completedCount = 0  # 進捗管理用カウンタをリセット
foreach ($target in $targets) {
    if ($syncHash.ContainsKey($target)) {
        [FileLogger]::WriteLog($resultFile, $syncHash[$target])
    }

    # 進捗を更新
    $completedCount++
    $progressPercent = [math]::Round(($completedCount / $totalTargets) * 100, 2)
    Write-Progress -Activity "ログ書き込み中..." -Status "$completedCount / $totalTargets 完了" -PercentComplete $progressPercent
}

# 完了メッセージ
Write-Progress -Activity "ログ書き込み中..." -Status "完了" -Completed
Write-Host "処理が完了しました。"

# 結果ファイルを開く
ii $resultFile
```

内部的にはRunspacePoolを使って並列化しているので、ホスト数が多い場合でも高速に処理が完了します。
並列処理なので、途中に疎通不可の端末がちらほらあるせいでPingがちょくちょく止まる・・・みたいなことも起きません。
1000台程度の台数であれば14~5秒ぐらいで結果が取れます。

作り的には↓のような感じになっています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2604770/181e0f87-adbb-ac2b-4f57-d5b277d972c5.png)
.batはPowerShellを実行するために置いてるだけなので、別に無くても大丈夫です（過去記事で紹介済み）
右クリックとかでPowerShellを直接実行する形でも問題ありません。

## ファイル構成
```
FastPing.bat     <-- ps1を実行する用のbat
FastPing.ps1     <-- 上記のPowerShellスクリプト
Target.txt       <-- ホスト名やIPアドレスを記載したテキストファイル
Result.txt       <-- スクリプト実行後に作成される結果ファイル
```

* Target.txt（例）
```
192.168.0.1
192.168.0.2
hostA
hostB
```

* Result.txt（例）
```
192.168.0.1  Alive
192.168.0.2  Dead
hostA        Alive
hostB        Alive
```

## 主なポイント
### 1． `Add-Type` でC#クラスを読み込み
```c#
Add-Type -TypeDefinition @"
using System;
using System.IO;
using System.Threading;
using System.Collections.Concurrent;

public class FileLogger {
    private static readonly object _lock = new object();
    public static void WriteLog(string path, string message) {
        int retryCount = 0;
        while (retryCount < 5) {
            try {
                lock (_lock) {
                    File.AppendAllText(path, message + Environment.NewLine, System.Text.Encoding.UTF8);
                }
                break;
            } catch {
                Thread.Sleep(10);
                retryCount++;
            }
        }
    }

    public static void WriteConsole(string message) {
        lock (_lock) {
            Console.WriteLine(message);
        }
    }
}
"@ -Language CSharp
```

* ログファイルへの出力とコンソールへの出力を安全におこなうためのC#クラスを定義しています。
* .WriteLog() はファイルを追記モードで書き込みますが、万一同時アクセスが衝突したときにリトライするロジックが入っています。
* .WriteConsole() はマルチスレッド環境でも行ロックを行い、コンソールへの同時出力の衝突を回避します。


### 2．`RunspacePool` を使った並列実行
```powershell
$pool = [runspacefactory]::CreateRunspacePool(1, 100)
$pool.Open()
```

* CreateRunspacePool(最小スレッド数, 最大スレッド数) でランスペースプールを作成しています。
* ここでは最大100まで並列スレッドを使う設定になっています。より多くのホストを高速にPingしたい場合は数を増やしたり、サーバー負荷が気になる場合は小さくするなど調整してください。


### 3．スレッドセーフな辞書`ConcurrentDictionary`で結果を一時保存
```
$syncHash = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new()
```

* 各スレッドから同時書き込みが行われるため、通常のハッシュテーブルでは衝突が起きやすいです。
ConcurrentDictionaryを使うことで衝突を回避しています。

### 5．出力結果の整形
```powershell
# Target.txt の順番通りに Result.txt に書き込み
foreach ($target in $targets) {
    if ($syncHash.ContainsKey($target)) {
        [FileLogger]::WriteLog($resultFile, $syncHash[$target])
    }
}
```

* 並列処理した結果は順番が保証されないため、最後に改めて Target.txt の並び順で結果を書き込みます。
* Result.txt は実行のたびに内容がクリアされて、最新結果のみが保存される設計です。