PowerShell 技能连载 - 系统诊断脚本集

适用于 PowerShell 5.1 及以上版本

系统管理员和运维工程师在日常工作中,经常需要面对各种系统故障和性能问题。当用户反馈系统卡顿、服务响应缓慢时,快速定位问题根因是恢复服务的关键。传统的排查方式是手动逐项检查——先看 CPU,再查内存,然后翻日志——不仅耗时,还容易遗漏关键线索。

通过 PowerShell 编写系统诊断脚本,可以将这些分散的检查步骤自动化,形成一套标准化的诊断流程。脚本可以在几秒内完成对硬件资源、操作系统状态、网络连接和安全配置的全面扫描,并以结构化报告的形式输出结果,帮助运维人员快速做出判断。

本文提供三个层次的诊断脚本:从硬件性能分析开始,到操作系统与服务状态检查,最后整合为一键全量诊断报告,方便直接集成到运维自动化平台中使用。

硬件与性能诊断

第一个脚本专注于硬件资源层面的诊断。它会采集 CPU 使用率、内存占用、磁盘空间等核心指标,并自动识别占用资源最高的进程,帮助快速定位性能瓶颈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
function Get-HardwareDiagnostic {
[CmdletBinding()]
param(
[double]$CpuThreshold = 80,
[double]$MemoryThreshold = 85,
[double]$DiskThreshold = 90
)

$result = [ordered]@{
Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
ComputerName = $env:COMPUTERNAME
Status = 'Healthy'
Alerts = @()
}

# CPU 使用率检查
$cpuCounter = '\Processor(_Total)\% Processor Time'
$cpuSample1 = (Get-Counter -Counter $cpuCounter -SampleInterval 1 -MaxSamples 3).CounterSamples
Start-Sleep -Seconds 1
$cpuAvg = [math]::Round(($cpuSample1 | Measure-Object -Property CookedValue -Average).Average, 2)

$result['CpuUsage'] = "$cpuAvg%"

if ($cpuAvg -gt $CpuThreshold) {
$result['Status'] = 'Warning'
$result['Alerts'] += "CPU 使用率 ${cpuAvg}% 超过阈值 ${CpuThreshold}%"
}

# 内存使用检查
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem
$totalMem = [math]::Round($osInfo.TotalVisibleMemorySize / 1MB, 2)
$freeMem = [math]::Round($osInfo.FreePhysicalMemory / 1MB, 2)
$usedMemPercent = [math]::Round(($osInfo.TotalVisibleMemorySize - $osInfo.FreePhysicalMemory) / $osInfo.TotalVisibleMemorySize * 100, 2)

$result['Memory'] = @{
TotalGB = $totalMem
FreeGB = $freeMem
UsedPercent = "$usedMemPercent%"
}

if ($usedMemPercent -gt $MemoryThreshold) {
$result['Status'] = 'Warning'
$result['Alerts'] += "内存使用率 ${usedMemPercent}% 超过阈值 ${MemoryThreshold}%"
}

# 磁盘空间检查
$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3'
$diskReport = foreach ($disk in $disks) {
$freePercent = [math]::Round($disk.FreeSpace / $disk.Size * 100, 2)
if ($freePercent -gt (100 - $DiskThreshold)) {
$result['Status'] = 'Warning'
$result['Alerts'] += "磁盘 $($disk.DeviceID) 可用空间仅 ${freePercent}%,低于安全阈值"
}
[ordered]@{
Drive = $disk.DeviceID
TotalGB = [math]::Round($disk.Size / 1GB, 2)
FreeGB = [math]::Round($disk.FreeSpace / 1GB, 2)
FreePercent = "$freePercent%"
}
}
$result['Disks'] = $diskReport

# Top 10 资源占用进程
$topProcesses = Get-Process |
Sort-Object -Property WorkingSet64 -Descending |
Select-Object -First 10 |
ForEach-Object {
[ordered]@{
Name = $_.Name
PID = $_.Id
MemoryMB = [math]::Round($_.WorkingSet64 / 1MB, 2)
CpuSeconds = [math]::Round($_.CPU, 2)
}
}
$result['TopProcesses'] = $topProcesses

return [PSCustomObject]$result
}

# 执行硬件诊断
$hardwareReport = Get-HardwareDiagnostic -CpuThreshold 80 -MemoryThreshold 85 -DiskThreshold 90
$hardwareReport | ConvertTo-Json -Depth 5

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
"Timestamp": "2026-03-16 09:15:32",
"ComputerName": "SRV-PROD-01",
"Status": "Warning",
"Alerts": [
"内存使用率 87.35% 超过阈值 85%",
"磁盘 C: 可用空间仅 8.12%,低于安全阈值"
],
"CpuUsage": "42.67%",
"Memory": {
"TotalGB": 32.00,
"FreeGB": 4.05,
"UsedPercent": "87.35%"
},
"Disks": [
{
"Drive": "C:",
"TotalGB": 256.00,
"FreeGB": 20.78,
"FreePercent": "8.12%"
},
{
"Drive": "D:",
"TotalGB": 1024.00,
"FreeGB": 612.34,
"FreePercent": "59.80%"
}
],
"TopProcesses": [
{
"Name": "sqlservr",
"PID": 4521,
"MemoryMB": 8192.45,
"CpuSeconds": 123456.78
},
{
"Name": "w3wp",
"PID": 3312,
"MemoryMB": 4096.12,
"CpuSeconds": 56789.01
}
]
}

操作系统与服务诊断

第二个脚本聚焦于操作系统层面,自动检查关键 Windows 服务的运行状态、扫描系统事件日志中的异常条目,并检测待安装的系统更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
function Get-OSDiagnostic {
[CmdletBinding()]
param(
[string[]]$CriticalServices = @('WinRM', 'EventLog', 'LanmanServer', 'LanmanWorkstation', 'Schedule'),
[int]$EventLogHours = 24,
[int]$MaxEvents = 50
)

$result = [ordered]@{
Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
ComputerName = $env:COMPUTERNAME
Status = 'Healthy'
Alerts = @()
}

# 操作系统基本信息
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$result['OS'] = @{
Caption = $os.Caption
Version = $os.Version
BuildNumber = $os.BuildNumber
LastBootTime = $os.LastBootUpTime.ToString('yyyy-MM-dd HH:mm:ss')
UptimeDays = [math]::Round((Get-Date) - $os.LastBootUpTime | Select-Object -ExpandProperty TotalDays, 1)
}

# 关键服务状态检查
$serviceReport = foreach ($svcName in $CriticalServices) {
$svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
if ($svc) {
$status = if ($svc.Status -eq 'Running') { 'OK' } else { 'Alert' }
if ($status -eq 'Alert') {
$result['Status'] = 'Warning'
$result['Alerts'] += "服务 $svcName 状态异常: $($svc.Status)"
}
[ordered]@{
Name = $svcName
DisplayName = $svc.DisplayName
Status = $svc.Status.ToString()
StartType = $svc.StartType.ToString()
CheckResult = $status
}
} else {
$result['Alerts'] += "服务 $svcName 未找到"
[ordered]@{
Name = $svcName
DisplayName = 'N/A'
Status = 'NotFound'
StartType = 'N/A'
CheckResult = 'Error'
}
}
}
$result['Services'] = $serviceReport

# 事件日志异常检查(最近 N 小时的错误和警告)
$startTime = (Get-Date).AddHours(-$EventLogHours)
$eventFilter = @{
LogName = 'System'
Level = 2, 3
StartTime = $startTime
}
$errorEvents = Get-WinEvent -FilterHashtable $eventFilter -MaxEvents $MaxEvents -ErrorAction SilentlyContinue

$eventSummary = $errorEvents |
Group-Object -Property ProviderName |
Sort-Object -Property Count -Descending |
Select-Object -First 10 |
ForEach-Object {
[ordered]@{
Source = $_.Name
Count = $_.Count
Examples = ($_.Group | Select-Object -First 2 | ForEach-Object { "$($_.TimeCreated.ToString('HH:mm:ss')) $($_.Message.Substring(0, [math]::Min(80, $_.Message.Length)))" })
}
}
$result['EventLogErrors'] = @{
TimeRange = "最近 ${EventLogHours} 小时"
TotalErrors = if ($errorEvents) { $errorEvents.Count } else { 0 }
TopSources = $eventSummary
}

if ($errorEvents -and $errorEvents.Count -gt 20) {
$result['Status'] = 'Warning'
$result['Alerts'] += "系统事件日志在最近 ${EventLogHours} 小时内有 $($errorEvents.Count) 条错误/警告"
}

# 待安装系统更新检查(需要 PSWindowsUpdate 模块)
$updateAvailable = $false
try {
Import-Module PSWindowsUpdate -ErrorAction Stop
$updates = Get-WindowsUpdate -AcceptAll -Install -AutoReboot:$false -WhatIf 2>$null
if ($updates) {
$updateAvailable = $true
$result['PendingUpdates'] = $updates | ForEach-Object {
@{ Title = $_.Title; Size = $_.Size }
}
$result['Alerts'] += "有 $($updates.Count) 个系统更新待安装"
}
} catch {
$result['PendingUpdates'] = '无法检查(PSWindowsUpdate 模块未安装)'
}

return [PSCustomObject]$result
}

# 执行操作系统诊断
$osReport = Get-OSDiagnostic -CriticalServices @('WinRM', 'EventLog', 'LanmanServer', 'LanmanWorkstation', 'Schedule', 'Spooler') -EventLogHours 24
$osReport | ConvertTo-Json -Depth 5

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
"Timestamp": "2026-03-16 09:16:45",
"ComputerName": "SRV-PROD-01",
"Status": "Warning",
"Alerts": [
"服务 Spooler 状态异常: Stopped",
"系统事件日志在最近 24 小时内有 47 条错误/警告"
],
"OS": {
"Caption": "Microsoft Windows Server 2022 Datacenter",
"Version": "10.0.20348",
"BuildNumber": "20348",
"LastBootTime": "2026-03-10 03:00:00",
"UptimeDays": 6.3
},
"Services": [
{
"Name": "WinRM",
"DisplayName": "Windows Remote Management (WS-Management)",
"Status": "Running",
"StartType": "Automatic",
"CheckResult": "OK"
},
{
"Name": "Spooler",
"DisplayName": "Print Spooler",
"Status": "Stopped",
"StartType": "Automatic",
"CheckResult": "Alert"
}
],
"EventLogErrors": {
"TimeRange": "最近 24 小时",
"TotalErrors": 47,
"TopSources": [
{
"Source": "Microsoft-Windows-Disk",
"Count": 18,
"Examples": [
"08:32:15 The device, \\Device\\Harddisk1\\DR1, has a bad block.",
"09:15:22 The device, \\Device\\Harddisk1\\DR1, has a bad block."
]
}
]
},
"PendingUpdates": "无法检查(PSWindowsUpdate 模块未安装)"
}

综合诊断报告

第三个脚本将前面的各项检查整合为一键全量诊断,计算健康评分,并生成可读性更好的 HTML 报告,方便通过邮件或 Web 平台分享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
function Invoke-FullSystemDiagnostic {
[CmdletBinding()]
param(
[string]$OutputPath = "$env:TEMP\SystemDiagnosticReport.html",
[double]$CpuThreshold = 80,
[double]$MemoryThreshold = 85,
[double]$DiskThreshold = 90
)

Write-Host "开始全量系统诊断..." -ForegroundColor Cyan
$diagStart = Get-Date

# 采集各项指标
Write-Host " [1/4] 采集硬件指标..." -ForegroundColor Gray
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$cpuSample = (Get-Counter -Counter '\Processor(_Total)\% Processor Time' -SampleInterval 1 -MaxSamples 3).CounterSamples
$cpuAvg = [math]::Round(($cpuSample | Measure-Object -Property CookedValue -Average).Average, 2)
$memUsedPercent = [math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize * 100, 2)
$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3'

Write-Host " [2/4] 检查服务状态..." -ForegroundColor Gray
$criticalSvcs = @('WinRM', 'EventLog', 'LanmanServer', 'LanmanWorkstation', 'Schedule')
$svcResults = foreach ($name in $criticalSvcs) {
$s = Get-Service -Name $name -ErrorAction SilentlyContinue
@{ Name = $name; Status = if ($s) { $s.Status.ToString() } else { 'NotFound' } }
}

Write-Host " [3/4] 扫描事件日志..." -ForegroundColor Gray
$startTime = (Get-Date).AddHours(-24)
$errors = @(Get-WinEvent -FilterHashtable @{ LogName = 'System'; Level = 2; StartTime = $startTime } -MaxEvents 100 -ErrorAction SilentlyContinue)
$warnings = @(Get-WinEvent -FilterHashtable @{ LogName = 'System'; Level = 3; StartTime = $startTime } -MaxEvents 100 -ErrorAction SilentlyContinue)

Write-Host " [4/4] 计算健康评分..." -ForegroundColor Gray

# 健康评分计算(满分 100)
$score = 100

# CPU 扣分(每超阈值 1% 扣 0.5 分,最多扣 20 分)
$cpuDeduction = [math]::Min(20, [math]::Max(0, ($cpuAvg - $CpuThreshold) * 0.5))
$score -= $cpuDeduction

# 内存扣分
$memDeduction = [math]::Min(20, [math]::Max(0, ($memUsedPercent - $MemoryThreshold) * 0.5))
$score -= $memDeduction

# 磁盘扣分
foreach ($disk in $disks) {
$diskUsedPercent = [math]::Round(($disk.Size - $disk.FreeSpace) / $disk.Size * 100, 2)
if ($diskUsedPercent -gt $DiskThreshold) {
$score -= [math]::Min(10, [math]::Max(0, ($diskUsedPercent - $DiskThreshold) * 0.3))
}
}

# 服务异常扣分(每个异常服务扣 5 分,最多扣 25 分)
$stoppedSvcs = @($svcResults | Where-Object { $_.Status -ne 'Running' })
$svcDeduction = [math]::Min(25, $stoppedSvcs.Count * 5)
$score -= $svcDeduction

# 事件日志错误扣分
$logDeduction = [math]::Min(15, [math]::Round($errors.Count / 5, 0))
$score -= $logDeduction

$score = [math]::Max(0, [math]::Round($score, 0))

# 确定总体状态
$overallStatus = if ($score -ge 80) { 'Healthy' } elseif ($score -ge 50) { 'Warning' } else { 'Critical' }

$diagDuration = [math]::Round(((Get-Date) - $diagStart).TotalSeconds, 1)

# 生成 HTML 报告
$html = @"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>系统诊断报告 - $($env:COMPUTERNAME) - $(Get-Date -Format 'yyyy-MM-dd')</title>
<style>
body { font-family: 'Segoe UI', sans-serif; margin: 20px; background: #f5f5f5; }
.container { max-width: 960px; margin: auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
h1 { color: #0078d4; border-bottom: 2px solid #0078d4; padding-bottom: 10px; }
.score { font-size: 48px; font-weight: bold; text-align: center; padding: 20px; }
.score.healthy { color: #107c10; }
.score.warning { color: #ff8c00; }
.score.critical { color: #d13438; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
th { background: #0078d4; color: white; }
tr:nth-child(even) { background: #f9f9f9; }
.badge { padding: 3px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }
.badge-ok { background: #dff6dd; color: #107c10; }
.badge-warn { background: #fff4ce; color: #ff8c00; }
.badge-error { background: #fde7e9; color: #d13438; }
</style>
</head>
<body>
<div class="container">
<h1>系统诊断报告</h1>
<p>计算机: <strong>$($env:COMPUTERNAME)</strong> | 生成时间: <strong>$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</strong> | 耗时: ${diagDuration}s</p>
<div class="score $overallStatus.ToLowerInvariant()">$score / 100</div>
<p style="text-align:center; font-size:18px;">总体状态: <strong>$overallStatus</strong></p>

<h2>CPU 使用率</h2>
<p>平均使用率: <strong>${cpuAvg}%</strong> (阈值: ${CpuThreshold}%)</p>

<h2>内存使用率</h2>
<p>已用: <strong>${memUsedPercent}%</strong> (阈值: ${MemoryThreshold}%)</p>

<h2>磁盘空间</h2>
<table>
<tr><th>驱动器</th><th>总容量</th><th>可用空间</th><th>可用百分比</th></tr>
$(foreach ($d in $disks) {
$freePct = [math]::Round($d.FreeSpace / $d.Size * 100, 2)
$badge = if ($freePct -gt 20) { 'badge-ok' } elseif ($freePct -gt 10) { 'badge-warn' } else { 'badge-error' }
"<tr><td>$($d.DeviceID)</td><td>$([math]::Round($d.Size/1GB,2)) GB</td><td>$([math]::Round($d.FreeSpace/1GB,2)) GB</td><td><span class=`"badge $badge`">${freePct}%</span></td></tr>"
})

<h2>关键服务状态</h2>
<table>
<tr><th>服务名</th><th>状态</th></tr>
$(foreach ($s in $svcResults) {
$badge = if ($s.Status -eq 'Running') { 'badge-ok' } else { 'badge-error' }
"<tr><td>$($s.Name)</td><td><span class=`"badge $badge`">$($s.Status)</span></td></tr>"
})

<h2>事件日志摘要(最近 24 小时)</h2>
<p>错误: <strong>$($errors.Count)</strong> 条 | 警告: <strong>$($warnings.Count)</strong> 条</p>

</div>
</body>
</html>
"@

$html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force
Write-Host "诊断完成!健康评分: $score / 100 [$overallStatus]" -ForegroundColor $(if ($overallStatus -eq 'Healthy') { 'Green' } elseif ($overallStatus -eq 'Warning') { 'Yellow' } else { 'Red' })
Write-Host "HTML 报告已保存至: $OutputPath" -ForegroundColor Cyan

return [PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
Score = $score
Status = $overallStatus
CpuUsage = "$cpuAvg%"
MemoryUsage = "$memUsedPercent%"
Errors24h = $errors.Count
Warnings24h = $warnings.Count
ReportPath = $OutputPath
}
}

# 执行全量诊断并生成报告
$report = Invoke-FullSystemDiagnostic -OutputPath "$env:TEMP\SystemDiagnosticReport.html"
$report

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
开始全量系统诊断...
[1/4] 采集硬件指标...
[2/4] 检查服务状态...
[3/4] 扫描事件日志...
[4/4] 计算健康评分...
诊断完成!健康评分: 72 / 100 [Warning]
HTML 报告已保存至: C:\Users\admin\AppData\Local\Temp\SystemDiagnosticReport.html

ComputerName : SRV-PROD-01
Score : 72
Status : Warning
CpuUsage : 42.67%
MemoryUsage : 87.35%
Errors24h : 23
Warnings24h : 31
ReportPath : C:\Users\admin\AppData\Local\Temp\SystemDiagnosticReport.html

注意事项

  1. 运行权限:部分检查(如事件日志查询、服务状态枚举)需要管理员权限。建议以提升模式启动 PowerShell,或将脚本加入计划任务以 SYSTEM 身份运行。

  2. 性能影响:CPU 使用率采样需要短暂等待(默认 3 秒采样周期),在极端高负载场景下,脚本本身的执行也会消耗资源。生产环境可调整 -MaxSamples 参数降低采样频率。

  3. 跨平台兼容:本文脚本以 Windows 平台为主,使用了 Win32_OperatingSystemGet-Counter 等 Windows 专用命令。如需在 Linux 上运行,应替换为 /proc/meminfovmstat 等原生命令的封装。

  4. 健康评分的阈值:评分算法中的扣分权重(CPU 20 分、内存 20 分、磁盘 10 分、服务 25 分、日志 15 分)可根据实际运维需求调整。核心服务密集型环境应提高服务权重的扣分比例。

  5. HTML 报告安全:生成的 HTML 报告包含服务器名称、资源数据等敏感信息,传输时应通过内部网络或加密渠道分享,避免直接暴露在公网上。

  6. 定时执行建议:可以将 Invoke-FullSystemDiagnostic 配合 Windows 计划任务或 CI/CD 流水线定时执行,每天生成一份诊断报告。当健康评分低于设定阈值时自动触发告警通知,实现主动式运维监控。

PowerShell 技能连载 - WinRM 高级配置与排错

适用于 PowerShell 5.1 及以上版本

WinRM 为什么总是连不上?

WinRM(Windows Remote Management)是 PowerShell Remoting 的底层协议,基于 WS-Management 标准。它允许管理员在远程计算机上执行命令、传输文件和管理配置。在企业环境中,WinRM 是批量运维的核心基础设施——从 Ansible 的 Windows 模块到 Azure Arc 的本地代理,都依赖它正常工作。

然而在实际部署中,WinRM 的”能连上”往往需要多个条件同时满足:服务运行、监听器配置正确、防火墙放行、认证协议匹配、权限授予。其中任何一个环节出问题,都会导致连接失败,而错误信息往往晦涩难懂,比如 Access is deniedThe WinRM client cannot process the requestWS-Man could not connect

本文将从配置加固、连接排错、受限端点三个方面,系统讲解 WinRM 的高级管理技巧,帮助你快速定位和解决远程管理中的常见故障。

WinRM 配置与安全加固

在正式使用 WinRM 之前,合理的初始配置至关重要。默认的 Enable-PSRemoting -Force 虽然能快速启用,但在生产环境中还需要关注监听器类型、传输加密和服务账户限制等方面。

以下脚本展示了 WinRM 的一次性安全配置流程,包括检查服务状态、配置 HTTPS 监听器以及限制远程访问的服务账户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 第一步:确保 WinRM 服务运行并设为自动启动
$service = Get-Service -Name WinRM -ErrorAction SilentlyContinue
if ($service.Status -ne 'Running') {
Start-Service -Name WinRM
Write-Host "WinRM 服务已启动"
}
Set-Service -Name WinRM -StartupType Automatic

# 第二步:检查现有监听器
$listeners = Get-ChildItem -Path WSMan:\localhost\Listener
Write-Host "当前监听器数量: $($listeners.Count)"

foreach ($listener in $listeners) {
$protocol = Get-Item -Path "$($listener.PSPath)\Transport" |
Select-Object -ExpandProperty Value
$port = Get-Item -Path "$($listener.PSPath)\Port" |
Select-Object -ExpandProperty Value
$addr = Get-Item -Path "$($listener.PSPath)\Address" |
Select-Object -ExpandProperty Value
Write-Host " 协议: $protocol 端口: $port 地址: $addr"
}

# 第三步:创建 HTTPS 监听器(需要先准备证书)
$cert = Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object {
$_.EnhancedKeyUsageList.FriendlyName -contains 'Server Authentication' -and
$_.NotAfter -gt (Get-Date)
} |
Sort-Object -Property NotAfter -Descending |
Select-Object -First 1

if ($cert) {
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS `
-Address * -CertificateThumbprint $cert.Thumbprint -Force
Write-Host "HTTPS 监听器已创建,证书指纹: $($cert.Thumbprint)"

# 配置防火墙规则放行 5986 端口
$fwRule = Get-NetFirewallRule -DisplayName 'WinRM HTTPS' -ErrorAction SilentlyContinue
if (-not $fwRule) {
New-NetFirewallRule -DisplayName 'WinRM HTTPS' `
-Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow
Write-Host "防火墙规则已添加 (TCP 5986)"
}
} else {
Write-Warning "未找到有效的服务器认证证书,跳过 HTTPS 监听器创建"
}

# 第四步:限制允许远程连接的用户组
Set-Item -Path WSMan:\localhost\Config\MaxConcurrentOperationsPerUser -Value 50
Set-Item -Path WSMan:\localhost\Service\MaxConnections -Value 100
Write-Host "并发限制已配置(每用户 50,总计 100)"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
WinRM 服务已启动
当前监听器数量: 1
协议: HTTP 端口: 5985 地址: *
Directory: WSMan:\localhost\Listener

Type Keys Name
---- ---- ----
Container {Transport=HTTPS, Address=*} Listener_2026309012345

HTTPS 监听器已创建,证书指纹: A1B2C3D4E5F6789012345678901234ABCD...
防火墙规则已添加 (TCP 5986)
并发限制已配置(每用户 50,总计 100)

连接排错工具集

当 WinRM 连接失败时,手动逐项排查非常耗时。下面这个诊断脚本将常见的检查步骤整合在一起,能够快速定位问题所在——从网络连通性到认证协议,再到 WinRM 服务配置,一目了然。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
function Test-WinRMConnection {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ComputerName,

[ValidateSet('HTTP', 'HTTPS')]
[string]$Transport = 'HTTPS',

[System.Management.Automation.PSCredential]$Credential
)

$port = if ($Transport -eq 'HTTPS') { 5986 } else { 5985 }
$results = [ordered]@{}

# 检查 1:DNS 解析
try {
$ip = [System.Net.Dns]::GetHostAddresses($ComputerName) |
Select-Object -First 1 -ExpandProperty IPAddressToString
$results['DNS 解析'] = "成功 ($ip)"
} catch {
$results['DNS 解析'] = "失败: $($_.Exception.Message)"
}

# 检查 2:TCP 端口连通性
$tcp = New-Object System.Net.Sockets.TcpClient
try {
$tcp.Connect($ComputerName, $port)
$results['TCP 端口'] = "成功 ($port 开放)"
} catch {
$results['TCP 端口'] = "失败: 端口 $port 不可达"
} finally {
$tcp.Close()
}

# 检查 3:TrustedHosts 配置
$trustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
if ($trustedHosts -eq '*' -or $trustedHosts -split ',' -contains $ComputerName) {
$results['信任主机'] = "已信任 (当前值: $trustedHosts)"
} else {
$results['信任主机'] = "未信任 (当前值: $trustedHosts)"
}

# 检查 4:WinRM 测试连接
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck
$testParams = @{
ComputerName = $ComputerName
SessionOption = $sessionOption
ErrorAction = 'Stop'
}
if ($Transport -eq 'HTTPS') {
$testParams['UseSSL'] = $true
}
if ($Credential) {
$testParams['Credential'] = $Credential
}

try {
$session = New-PSSession @testParams
$results['远程会话'] = '成功'
Remove-PSSession -Session $session
} catch {
$results['远程会话'] = "失败: $($_.Exception.Message)"
}

# 输出诊断报告
Write-Host "`n========== WinRM 连接诊断报告 ==========" -ForegroundColor Cyan
Write-Host "目标: $ComputerName`n传输: $Transport`n端口: $port`n"
foreach ($key in $results.Keys) {
$status = $results[$key]
$icon = if ($status -match '成功|已信任') { '[OK]' } else { '[!!]' }
$color = if ($status -match '成功|已信任') { 'Green' } else { 'Red' }
Write-Host ("{0,-12} {1} {2}" -f "[$key]", $icon, $status) -ForegroundColor $color
}
Write-Host "========================================" -ForegroundColor Cyan

return $results
}

# 使用示例
Test-WinRMConnection -ComputerName 'SRV01.contoso.com' -Transport HTTPS

执行结果示例:

1
2
3
4
5
6
7
8
9
10
========== WinRM 连接诊断报告 ==========
目标: SRV01.contoso.com
传输: HTTPS
端口: 5986

[DNS 解析] [OK] 成功 (10.0.1.50)
[TCP 端口] [OK] 成功 (5986 开放)
[信任主机] [!!] 未信任 (当前值: )
[远程会话] [!!] 失败: The WinRM client cannot process the request because the server name is not in the TrustedHosts list.
========================================

上面的诊断结果清楚地指出了问题:目标服务器不在 TrustedHosts 列表中。对于非域环境,需要将远程主机名加入信任列表,或者使用 HTTPS 配合正确的证书验证。

常见的错误码及其含义如下:

错误码 含义 解决方向
0x80070005 Access Denied 检查凭据、用户组权限
0x80338012 WinRM 服务未运行 远程启动 WinRM 服务
0x80338125 证书验证失败 检查证书有效性或使用 SkipCACheck
0x80338104 WinRM 无法处理请求 检查 TrustedHosts 和认证协议

Constrained Endpoint:受限会话配置

在多人协作的运维团队中,直接给所有成员完整的远程 PowerShell 权限风险很高。Constrained Endpoint(受限端点)允许你创建自定义的会话配置,精确控制远程用户可以执行哪些命令——这既满足了权限最小化原则,又为审计提供了完整的操作日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 定义受限会话的角色能力(Role Capability)
$roleCapDir = "$env:ProgramFiles\WindowsPowerShell\RoleCapabilities"
if (-not (Test-Path $roleCapDir)) {
New-Item -Path $roleCapDir -ItemType Directory -Force
}

# 创建角色能力文件
$roleCapParams = @{
Path = Join-Path $roleCapDir 'Maintenance.psrc'
VisibleCmdlets = @(
'Get-Process', 'Get-Service', 'Restart-Service',
'Get-EventLog', 'Get-WinEvent',
'Get-Volume', 'Get-Partition'
)
VisibleFunctions = @('Get-SystemUptime', 'Get-DiskHealth')
VisibleProviders = @('FileSystem')
VisibleExternalCommands = @('whoami.exe', 'hostname.exe')
ScriptsToProcess = @()
AliasDefinitions = @(
@{ Name = 'gsv'; Value = 'Get-Service' }
)
FunctionDefinitions = @(
@{
Name = 'Get-SystemUptime'
ScriptBlock = {
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
[PSCustomObject]@{
LastBoot = $os.LastBootUpTime
UptimeDays = [math]::Round($uptime.TotalDays, 2)
}
}
}
)
}
New-PSRoleCapabilityFile @roleCapParams
Write-Host "角色能力文件已创建: Maintenance.psrc"

# 创建受限会话配置
$cred = Get-Credential -Message '输入受限端点的运行账户'
$sessionParams = @{
Name = 'Maintenance'
RunAsCredential = $cred
RoleDefinitions = @{ 'CONTOSO\ServerOps' = @{ RoleCapabilities = 'Maintenance' } }
TranscriptDirectory = 'C:\Transcripts'
RunAsVirtualAccount = $true
MaximumReceivedDataSizePerCommandMB = 10
MaximumReceivedObjectSizeMB = 5
SessionType = 'RestrictedRemoteServer'
}
try {
Register-PSSessionConfiguration @sessionParams -Force
Write-Host "受限端点 'Maintenance' 注册成功"

# 验证配置
$config = Get-PSSessionConfiguration -Name Maintenance
Write-Host " 运行账户: $($config.RunAsUser)"
Write-Host " 虚拟账户: $($config.RunAsVirtualAccount)"
Write-Host " 审计目录: $($config.TranscriptDirectory)"

# 重启 WinRM 使配置生效
Restart-Service -Name WinRM -Force
Write-Host "WinRM 服务已重启,配置生效"
} catch {
Write-Error "注册受限端点失败: $($_.Exception.Message)"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
角色能力文件已创建: Maintenance.psrc

Location: C:\Program Files\WindowsPowerShell\RoleCapabilities

Name Value
---- -----
VisibleCmdlets {Get-Process, Get-Service, Restart-Service, Get-EventLog...}
VisibleFunctions {Get-SystemUptime, Get-DiskHealth}
VisibleProviders {FileSystem}

cmdlet Register-PSSessionConfiguration at command pipeline position 1
Supply values for the following parameters:

受限端点 'Maintenance' 注册成功
运行账户: CONTOSO\SvcMaintenance
虚拟账户: True
审计目录: C:\Transcripts
WinRM 服务已重启,配置生效

注册完成后,团队成员可以通过以下方式连接受限端点:

1
Enter-PSSession -ComputerName SRV01 -ConfigurationName Maintenance

连接后只能使用预定义的命令集,所有操作都会被记录到 C:\Transcripts 目录下的转录文件中,方便事后审计。

注意事项

  1. HTTPS 监听器必须有有效证书。如果使用自签名证书,客户端需要通过 SkipCACheck 选项跳过 CA 验证,但这会降低安全性。在生产环境中应使用企业 CA 或公共 CA 签发的证书,确保证书的主题名称(或 SAN)与服务器主机名匹配。

  2. TrustedHosts 是安全风险点。将 TrustedHosts 设为 * 等于信任所有远程主机,仅适用于测试环境。在域环境中应依赖 Kerberos 认证,避免修改 TrustedHosts;在工作组环境中至少明确指定目标主机名。

  3. Constrained Endpoint 使用虚拟账户RunAsVirtualAccount = $true 会让会话使用临时创建的虚拟账户运行,该账户在会话结束后自动销毁,比直接使用真实服务账户更安全。但虚拟账户无法访问网络资源,如需跨机器操作需配合 gMSA(组托管服务账户)。

  4. 防火墙规则需双向检查。WinRM 使用 HTTP(5985)和 HTTPS(5986)两个端口。确保远程主机的入站规则放行了对应端口,同时检查本地网络出站策略和中间链路的防火墙设备。

  5. 转录文件会持续增长。配置 TranscriptDirectory 后,所有远程会话的操作都会被完整记录。建议配合定期归档脚本清理过期的转录文件,避免磁盘空间耗尽。

  6. WinRM 配置修改后需重启服务。无论是修改监听器、注册新的会话配置还是更改服务参数,都需要执行 Restart-Service -Name WinRM -Force 才能生效。注意这会断开所有现有的远程会话,应在维护窗口操作。

PowerShell 技能连载 - 网络故障排查工具集

适用于 PowerShell 5.1 及以上版本

网络故障是运维工作中最常见也最令人头疼的问题类型。当用户反馈”系统连不上”时,问题可能出在 DNS 解析、防火墙规则、TCP 端口不通、SSL 证书过期、路由环路等任何一个环节。传统做法是依次打开命令行窗口,手动执行 ping、tracert、nslookup、telnet 等工具,逐一排除可能的原因——这个过程既繁琐又容易遗漏关键检查项。

更糟糕的是,不同工具的输出格式各异,很难快速汇总为一份完整的诊断结论。当你在深夜被叫起来处理紧急故障时,最需要的是一个能一键完成所有网络层检测、并直接给出问题定位的工具,而不是在多个黑窗口之间来回切换、靠经验猜测瓶颈在哪一跳。

PowerShell 提供了 Test-NetConnectionResolve-DnsNameTest-Connection 等原生网络 cmdlet,配合 .NET 的 System.Net.SocketsSystem.Net.Security 类,完全可以构建一套功能完备的网络诊断工具集。本文将从连接性测试、DNS 诊断、综合诊断报告三个层面,展示如何用 PowerShell 打造一键式网络故障排查方案。

连接性测试:TCP 端口扫描与延迟测量

排查网络故障的第一步是确认目标主机各端口的可达性。下面的脚本封装了 TCP 连接测试、HTTP/HTTPS 探测和延迟测量功能,支持批量检测多个目标的多个端口,并以结构化对象返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
function Test-NetworkConnectivity {
<#
.SYNOPSIS
测试网络连接性:TCP 端口扫描、HTTP 探测、延迟测量
.PARAMETER ComputerName
目标主机名或 IP 地址,支持数组
.PARAMETER Ports
要测试的 TCP 端口列表
.PARAMETER TimeoutMs
连接超时时间(毫秒),默认 3000
.PARAMETER TestHttp
是否对 80/443 端口执行 HTTP/HTTPS 探测
#>
param(
[Parameter(Mandatory)]
[string[]]$ComputerName,
[int[]]$Ports = @(22, 80, 443, 3389, 8080),
[int]$TimeoutMs = 3000,
[switch]$TestHttp
)

$results = @()

foreach ($target in $ComputerName) {
Write-Host "`n[*] 正在测试目标: $target" -ForegroundColor Cyan

# ICMP 延迟测量
$ping = Test-Connection -ComputerName $target -Count 4 -ErrorAction SilentlyContinue
if ($ping) {
$avgLatency = ($ping | Measure-Object -Property ResponseTime -Average).Average
$jitter = [math]::Round(($ping | Measure-Object -Property ResponseTime -StandardDeviation).StandardDeviation, 2)
$packetLoss = [math]::Round((1 - ($ping.Count / 4)) * 100, 1)
Write-Host " ICMP: 平均延迟 ${avgLatency}ms, 抖动 ${jitter}ms, 丢包率 ${packetLoss}%" -ForegroundColor Green
} else {
$avgLatency = $null
$jitter = $null
$packetLoss = 100
Write-Host " ICMP: 目标不可达" -ForegroundColor Red
}

# TCP 端口扫描
foreach ($port in $Ports) {
$tcpClient = New-Object System.Net.Sockets.TcpClient
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

try {
$asyncResult = $tcpClient.BeginConnect($target, $port, $null, $null)
$waited = $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)

if ($waited) {
$tcpClient.EndConnect($asyncResult)
$stopwatch.Stop()
$status = "Open"
$latencyMs = $stopwatch.ElapsedMilliseconds
$color = "Green"
} else {
$stopwatch.Stop()
$status = "Timeout"
$latencyMs = $TimeoutMs
$color = "Yellow"
}
} catch {
$stopwatch.Stop()
$status = "Closed"
$latencyMs = $stopwatch.ElapsedMilliseconds
$color = "Red"
} finally {
$tcpClient.Close()
}

$httpInfo = $null
if ($TestHttp -and $status -eq "Open" -and $port -in @(80, 443)) {
$scheme = if ($port -eq 443) { "https" } else { "http" }
try {
$response = Invoke-WebRequest -Uri "${scheme}://${target}" `
-UseBasicParsing -TimeoutSec 5 -ErrorAction Stop
$httpInfo = @{
StatusCode = $response.StatusCode
Server = $response.Headers["Server"]
}
} catch {
$httpInfo = @{ StatusCode = $_.Exception.Response.StatusCode.Value__ }
}
}

$result = [PSCustomObject]@{
Target = $target
Port = $port
Status = $status
LatencyMs = $latencyMs
ICMPaAvgMs = $avgLatency
ICMPJitter = $jitter
ICMPLossPct = $packetLoss
HttpInfo = $httpInfo
Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
$results += $result

$httpStr = if ($httpInfo) { " | HTTP $($httpInfo.StatusCode)" } else { "" }
Write-Host " 端口 ${port}: ${status} (${latencyMs}ms)${httpStr}" -ForegroundColor $color
}
}

return $results
}

# 执行示例:检测 Web 服务器的常见端口
Test-NetworkConnectivity -ComputerName "example.com" -Ports @(22, 80, 443, 8080) -TestHttp
1
2
3
4
5
6
7
8
9
10
11
12
13
[*] 正在测试目标: example.com
ICMP: 平均延迟 42.25ms, 抖动 3.17ms, 丢包率 0%
端口 22: Timeout (3000ms)
端口 80: Open (38ms) | HTTP 200
端口 443: Open (45ms) | HTTP 200
端口 8080: Closed (1ms)

Target Port Status LatencyMs ICMPaAvgMs ICMPJitter ICMPLossPct HttpInfo Timestamp
------ ---- ------ --------- ---------- ---------- ----------- -------- ---------
example.com 22 Timeout 3000 42.25 3.17 0 2026-02-10 08:15:23
example.com 80 Open 38 42.25 3.17 0 {StatusCode, Server} 2026-02-10 08:15:23
example.com 443 Open 45 42.25 3.17 0 {StatusCode, Server} 2026-02-10 08:15:24
example.com 8080 Closed 1 42.25 3.17 0 2026-02-10 08:15:24

从输出结果可以看到,脚本一次性完成了 ICMP 延迟测试和 4 个端口的 TCP 连接检测,并对 80 和 443 端口额外执行了 HTTP 探测,返回了状态码。这种结构化的输出结果可以直接导出为 CSV 或 HTML 报告,方便归档和对比。

DNS 诊断:解析链路与缓存检查

DNS 解析故障常常伪装成网络不通——域名解析不到、解析到了错误地址、或者解析延迟过高都会导致业务异常。下面的脚本实现了完整的 DNS 诊断链路:递归解析追踪、本地缓存检查、SOA 记录验证和区域传输测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
function Test-DnsResolution {
<#
.SYNOPSIS
执行全面的 DNS 诊断:解析追踪、缓存检查、区域传输测试
.PARAMETER DomainName
要诊断的域名
.PARAMETER DnsServer
指定 DNS 服务器(默认使用系统配置)
.PARAMETER RecordTypes
要查询的记录类型列表
#>
param(
[Parameter(Mandatory)]
[string]$DomainName,
[string]$DnsServer,
[string[]]$RecordTypes = @("A", "AAAA", "CNAME", "MX", "NS", "TXT", "SOA")
)

Write-Host "`n========== DNS 诊断报告 ==========" -ForegroundColor Cyan
Write-Host "目标域名: $DomainName"
Write-Host "诊断时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "===================================`n"

# 1. 解析各类型记录
Write-Host "[1] DNS 记录查询" -ForegroundColor Yellow
foreach ($rtype in $RecordTypes) {
try {
$params = @{ Name = $DomainName; Type = $rtype; ErrorAction = "Stop" }
if ($DnsServer) { $params["Server"] = $DnsServer }

$records = Resolve-DnsName @params
foreach ($rec in $records) {
$section = $rec.Section
$ttl = if ($rec.TTL) { "$($rec.TTL)s" } else { "N/A" }
$value = switch ($rtype) {
"A" { $rec.IPAddress }
"AAAA" { $rec.IPAddress }
"MX" { "$($rec.Preference) $($rec.NameExchange)" }
"NS" { $rec.NameHost }
"TXT" { $rec.Strings -join " " }
"SOA" { "$($rec.PrimaryServer) Serial=$($rec.Serial)" }
"CNAME"{ $rec.NameHost }
default { $rec.IPAddress }
}
Write-Host " ${rtype}: $value (TTL: $ttl, Section: $section)" -ForegroundColor Green
}
} catch {
Write-Host " ${rtype}: 未找到记录 ($($_.Exception.Message))" -ForegroundColor DarkGray
}
}

# 2. DNS 解析计时
Write-Host "`n[2] 解析性能测试" -ForegroundColor Yellow
$timings = @()
for ($i = 1; $i -le 5; $i++) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
try {
$null = Resolve-DnsName -Name $DomainName -Type A -ErrorAction Stop
$sw.Stop()
$timings += $sw.ElapsedMilliseconds
Write-Host " 第 ${i} 次: $($sw.ElapsedMilliseconds)ms" -ForegroundColor Green
} catch {
$sw.Stop()
$timings += $sw.ElapsedMilliseconds
Write-Host " 第 ${i} 次: 失败 ($($sw.ElapsedMilliseconds)ms)" -ForegroundColor Red
}
}
$avgTime = [math]::Round(($timings | Measure-Object -Average).Average, 2)
$maxTime = ($timings | Measure-Object -Maximum).Maximum
Write-Host " 平均: ${avgTime}ms, 最大: ${maxTime}ms" -ForegroundColor Cyan

# 3. NS 服务器可达性检查
Write-Host "`n[3] NS 服务器可达性" -ForegroundColor Yellow
try {
$nsRecords = Resolve-DnsName -Name $DomainName -Type NS -ErrorAction Stop
foreach ($ns in $nsRecords) {
$nsHost = $ns.NameHost
if ($nsHost) {
$ping = Test-Connection -ComputerName $nsHost -Count 2 -ErrorAction SilentlyContinue
if ($ping) {
$latency = [math]::Round(($ping | Measure-Object -Property ResponseTime -Average).Average, 2)
Write-Host " ${nsHost}: 在线 (${latency}ms)" -ForegroundColor Green
} else {
Write-Host " ${nsHost}: 不可达" -ForegroundColor Red
}
}
}
} catch {
Write-Host " 无法获取 NS 记录" -ForegroundColor Red
}

# 4. SOA 序列号检查(判断 DNS 是否同步)
Write-Host "`n[4] SOA 序列号一致性" -ForegroundColor Yellow
try {
$soa = Resolve-DnsName -Name $DomainName -Type SOA -ErrorAction Stop | Select-Object -First 1
Write-Host " 主服务器: $($soa.PrimaryServer)" -ForegroundColor Green
Write-Host " 序列号: $($soa.Serial)" -ForegroundColor Green
Write-Host " 刷新间隔: $($soa.Refresh)s" -ForegroundColor Green
Write-Host " 重试间隔: $($soa.Retry)s" -ForegroundColor Green
Write-Host " 过期时间: $($soa.Expire)s" -ForegroundColor Green
} catch {
Write-Host " 无法获取 SOA 记录" -ForegroundColor Red
}

Write-Host "`n========== 诊断完成 ==========`n"
}

# 执行示例:诊断 vichamp.com 的 DNS 配置
Test-DnsResolution -DomainName "vichamp.com"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
========== DNS 诊断报告 ==========
目标域名: vichamp.com
诊断时间: 2026-02-10 08:20:15
===================================

[1] DNS 记录查询
A: 185.199.108.153 (TTL: 300s, Section: Answer)
A: 185.199.109.153 (TTL: 300s, Section: Answer)
A: 185.199.110.153 (TTL: 300s, Section: Answer)
A: 185.199.111.153 (TTL: 300s, Section: Answer)
AAAA: 未找到记录
MX: 10 mail.vichamp.com (TTL: 3600s, Section: Answer)
NS: dns1.p01.nsone.net (TTL: 86400s, Section: Additional)
NS: dns2.p01.nsone.net (TTL: 86400s, Section: Additional)
TXT: v=spf1 include:_spf.google.com ~all (TTL: 3600s, Section: Answer)
SOA: dns1.p01.nsone.net Serial=2026021001 (TTL: 86400s, Section: Authority)

[2] 解析性能测试
第 1 次: 18ms
第 2 次: 12ms
第 3 次: 11ms
第 4 次: 13ms
第 5 次: 10ms
平均: 12.8ms, 最大: 18ms

[3] NS 服务器可达性
dns1.p01.nsone.net: 在线 (85.23ms)
dns2.p01.nsone.net: 在线 (92.17ms)

[4] SOA 序列号一致性
主服务器: dns1.p01.nsone.net
序列号: 2026021001
刷新间隔: 43200s
重试间隔: 7200s
过期时间: 1209600s

========== 诊断完成 ==========

DNS 诊断脚本输出了完整的解析链路信息:A 记录指向 GitHub Pages 的 4 个 IP 地址,MX 记录配置了邮件服务器,TXT 记录包含了 SPF 策略。解析延迟平均 12.8ms,属于正常范围。SOA 序列号可以用来判断主从 DNS 是否同步——如果多个 NS 服务器的序列号不一致,说明区域传输可能存在问题。

综合诊断报告:一键检测与 HTML 输出

在故障排查实战中,我们通常需要一份涵盖所有网络层面的完整诊断报告。下面的脚本将连接性测试和 DNS 诊断整合为一个一键执行的入口,并生成带样式的 HTML 报告,方便分享给团队成员或归档留存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
function Invoke-NetworkHealthCheck {
<#
.SYNOPSIS
一键网络健康检查:综合连通性、DNS、路由追踪,生成 HTML 报告
.PARAMETER Target
目标主机名或 IP 地址
.PARAMETER OutputPath
HTML 报告输出路径
#>
param(
[Parameter(Mandatory)]
[string]$Target,
[string]$OutputPath = ".\NetworkReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
)

$reportTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$sections = @()

# ---- 第一部分:ICMP 连通性 ----
$icmpResults = @()
$pings = Test-Connection -ComputerName $Target -Count 10 -ErrorAction SilentlyContinue
if ($pings) {
$stats = $pings | Measure-Object -Property ResponseTime -Average -Minimum -Maximum -StandardDeviation
$icmpResults += "状态: 在线"
$icmpResults += "平均延迟: $([math]::Round($stats.Average, 2))ms"
$icmpResults += "最小延迟: $($stats.Minimum)ms"
$icmpResults += "最大延迟: $($stats.Maximum)ms"
$icmpResults += "抖动(Jitter): $([math]::Round($stats.StandardDeviation, 2))ms"
$icmpResults += "丢包率: $([math]::Round((1 - ($pings.Count / 10)) * 100, 1))%"
} else {
$icmpResults += "状态: 目标不可达"
}

# ---- 第二部分:关键端口检测 ----
$portResults = @()
$criticalPorts = @{
22 = "SSH"
53 = "DNS"
80 = "HTTP"
443 = "HTTPS"
3389 = "RDP"
}
foreach ($port in $criticalPorts.Keys) {
$tcpClient = New-Object System.Net.Sockets.TcpClient
try {
$asyncResult = $tcpClient.BeginConnect($Target, $port, $null, $null)
$waited = $asyncResult.AsyncWaitHandle.WaitOne(3000, $false)
if ($waited) {
$tcpClient.EndConnect($asyncResult)
$portResults += "$($criticalPorts[$port]) ($port): 开放"
} else {
$portResults += "$($criticalPorts[$port]) ($port): 超时"
}
} catch {
$portResults += "$($criticalPorts[$port]) ($port): 关闭"
} finally {
$tcpClient.Close()
}
}

# ---- 第三部分:DNS 解析 ----
$dnsResults = @()
try {
$dnsRecs = Resolve-DnsName -Name $Target -Type A -ErrorAction Stop
foreach ($rec in $dnsRecs) {
if ($rec.IPAddress) {
$dnsResults += "$($rec.Name) -> $($rec.IPAddress) (TTL: $($rec.TTL)s)"
}
}
} catch {
$dnsResults += "解析失败: $($_.Exception.Message)"
}

# ---- 第四部分:路由追踪(简化版 MTR) ----
$hopResults = @()
for ($ttl = 1; $ttl -le 15; $ttl++) {
$ping = Test-Connection -ComputerName $Target -Count 1 -TTL $ttl -ErrorAction SilentlyContinue
if ($ping) {
$hopIP = $ping.Address
$hopLatency = $ping.ResponseTime
$hopResults += "跳 ${ttl}: ${hopIP} (${hopLatency}ms)"
if ($hopIP -eq $Target -or $hopIP -match $Target) { break }
} else {
$hopResults += "跳 ${ttl}: * * *"
}
}

# ---- 生成 HTML 报告 ----
$html = @"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>网络诊断报告 - $Target</title>
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 40px; background: #f5f5f5; }
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
h2 { color: #2980b9; margin-top: 30px; }
.container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.section { margin: 20px 0; padding: 15px; background: #fafafa; border-left: 4px solid #3498db; }
.ok { color: #27ae60; }
.warn { color: #f39c12; }
.fail { color: #e74c3c; }
.timestamp { color: #7f8c8d; font-size: 0.9em; }
</style>
</head>
<body>
<div class="container">
<h1>网络诊断报告</h1>
<p class="timestamp">目标: $Target | 生成时间: $reportTime</p>

<h2>ICMP 连通性</h2>
<div class="section">
$($icmpResults | ForEach-Object { "<p>$_</p>" })
</div>

<h2>关键端口状态</h2>
<div class="section">
$($portResults | ForEach-Object {
if ($_ -match "开放") { "<p class=`"ok`">$_</p>" }
elseif ($_ -match "超时") { "<p class=`"warn`">$_</p>" }
else { "<p class=`"fail`">$_</p>" }
})
</div>

<h2>DNS 解析</h2>
<div class="section">
$($dnsResults | ForEach-Object { "<p>$_</p>" })
</div>

<h2>路由追踪</h2>
<div class="section">
$($hopResults | ForEach-Object { "<p>$_</p>" })
</div>

</div>
</body>
</html>
"@

$html | Out-File -FilePath $OutputPath -Encoding UTF8
Write-Host "诊断报告已生成: $OutputPath" -ForegroundColor Green

# 同时输出控制台摘要
Write-Host "`n===== 网络健康检查摘要 =====" -ForegroundColor Cyan
Write-Host "目标: $Target"
$icmpResults | ForEach-Object { Write-Host " $_" }
Write-Host "`n端口状态:" -ForegroundColor Yellow
$portResults | ForEach-Object { Write-Host " $_" }
Write-Host "`n路由路径:" -ForegroundColor Yellow
$hopResults | ForEach-Object { Write-Host " $_" }
Write-Host "============================`n"
}

# 执行示例:一键检查并生成 HTML 报告
Invoke-NetworkHealthCheck -Target "blog.vichamp.com"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
===== 网络健康检查摘要 =====
目标: blog.vichamp.com
状态: 在线
平均延迟: 38.5ms
最小延迟: 32ms
最大延迟: 51ms
抖动(Jitter): 5.23ms
平均延迟: 38.5ms
丢包率: 0%

端口状态:
SSH (22): 关闭
DNS (53): 关闭
HTTP (80): 开放
HTTPS (443): 开放
RDP (3389): 关闭

路由路径:
跳 1: 192.168.1.1 (1ms)
跳 2: 10.0.0.1 (5ms)
跳 3: 61.149.212.1 (8ms)
跳 4: * * *
跳 5: * * *
跳 6: 185.199.108.153 (38ms)

诊断报告已生成: .\NetworkReport_20260210_082500.html
============================

综合诊断脚本整合了 ICMP 连通性分析、关键端口检测、DNS 解析验证和简化的路由追踪,最终将所有结果汇总为一份带颜色标注的 HTML 报告。在端口状态部分,开放的端口用绿色标注、超时的用橙色、关闭的用红色,一目了然。路由追踪可以快速定位网络延迟发生的位置——如果某一跳延迟突然增大,说明瓶颈就在该节点。

注意事项

  1. 跨平台差异Test-Connection 在 PowerShell 7 (Core) 中行为与 Windows PowerShell 5.1 不同。5.1 默认使用 WMI 的 Win32_PingStatus,而 PowerShell 7 使用 .NET 的 System.Net.NetworkInformation.Ping,返回对象属性名称可能不同。生产环境中建议统一使用 PowerShell 7 以保证一致性。

  2. 防火墙对 ICMP 的影响:很多云服务器和安全设备会禁用 ICMP 回显请求(ping 不通但服务正常)。因此判断服务可用性时应以 TCP 端口检测结果为准,ICMP 仅作为参考指标,不要因为 ping 不通就判定服务宕机。

  3. DNS 缓存干扰:Windows 会缓存 DNS 解析结果,短时间内多次查询同一域名会命中本地缓存而非递归查询,导致”解析很快”的假象。如需测试真实解析性能,先执行 Clear-DnsClientCache 清除缓存,或在脚本中用 -DnsServer 参数指定公共 DNS(如 8.8.8.8)绕过本地缓存。

  4. 路由追踪的局限性:PowerShell 的 Test-Connection -TTL 功能远不如 Linux 的 mtr 或 Windows 的 pathping 强大。对于复杂的路由问题,建议用 mtr --report 做持续统计,或使用 traceroute 结合 tcptraceroute(基于 TCP SYN 的路由追踪,能穿透屏蔽 ICMP 的防火墙)。

  5. HTML 报告安全性:脚本生成的 HTML 报告中包含目标主机的 IP 地址、开放端口等敏感信息。不要将报告直接上传到公开位置,传输时建议使用加密通道或先压缩加密。如果目标地址是内网 IP,报告还可能泄露内网拓扑结构。

  6. 超时与并发控制:对多个目标进行批量检测时,如果采用串行方式逐个测试,总耗时会随目标数量线性增长。可以考虑使用 ForEach-Object -Parallel(PowerShell 7)或 .NET 的 System.Threading.Tasks.Parallel 类来实现并发检测,但要注意控制并发度(建议不超过 20 个并发连接),避免被目标主机的防火墙判定为端口扫描而封禁。

PowerShell 技能连载 - 网络诊断工具

适用于 PowerShell 5.1 及以上版本

在日常运维中,网络问题往往是最难定位的一类故障。接口掉线、带宽异常、HTTP 响应变慢——这些问题如果不能快速发现并定位,就会影响业务连续性。传统的图形化网络工具虽然直观,但难以批量执行和自动化巡检。

PowerShell 提供了全面的网络诊断能力:从网卡状态检测、HTTP 请求质量测量,到延迟丢包统计和实时流量监控,都可以通过脚本完成。更重要的是,这些脚本可以集成到定时任务或 CI/CD 流水线中,实现网络健康状态的持续观测。

本文将围绕四个实用场景,介绍如何用 PowerShell 构建一套轻量但实用的网络诊断工具集。

网络接口健康检查

首先,了解本机网卡的基本状态是排查网络问题的第一步。下面的脚本汇总所有活跃网卡的 IP 地址、链路状态、速率和已发送/接收的字节数,帮助你快速判断网络接口层是否正常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Get-NetworkInterfaceHealth {
<#
.SYNOPSIS
获取本机所有活跃网络接口的健康状态
#>

$adapters = Get-NetAdapter |
Where-Object { $_.Status -eq 'Up' -and $_.InterfaceType -notmatch 'Loopback' }

$results = foreach ($adapter in $adapters) {
$ipConfig = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv2 -ErrorAction SilentlyContinue |
Select-Object -First 1

$stats = Get-NetAdapterStatistics -Name $adapter.Name -ErrorAction SilentlyContinue

[PSCustomObject]@{
名称 = $adapter.Name
接口描述 = $adapter.InterfaceDescription
链路速率 = "$($adapter.LinkSpeed)"
IPv4地址 = if ($ipConfig) { $ipConfig.IPAddress } else { '未分配' }
已接收字节 = if ($stats) { '{0:N2} GB' -f ($stats.ReceivedBytes / 1GB) } else { 'N/A' }
已发送字节 = if ($stats) { '{0:N2} GB' -f ($stats.SentBytes / 1GB) } else { 'N/A' }
MAC地址 = $adapter.MacAddress
}
}

$results | Format-Table -AutoSize
$results
}

Get-NetworkInterfaceHealth

执行结果示例:

1
2
3
4
名称     接口描述                            链路速率   IPv4地址       已接收字节  已发送字节  MAC地址
---- ---------- -------- -------- ---------- ---------- --------
以太网 Intel(R) Ethernet Connection I219-V 1 Gbps 192.168.1.100 12.35 GB 3.28 GB AA:BB:CC:DD:EE:FF
Wi-Fi Intel(R) Wi-Fi 6 AX200 866.7 Mbps 192.168.1.101 5.71 GB 1.04 GB 11:22:33:44:55:66

HTTP 响应质量检测

仅仅知道端口是否可达是不够的,很多时候我们需要深入了解 HTTP 服务的响应质量,包括首字节时间(TTFB)、总耗时、状态码以及重定向链。这些指标对 Web 服务运维至关重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function Test-HttpQuality {
<#
.SYNOPSIS
测试 HTTP/HTTPS 服务的响应质量
#>
param(
[Parameter(Mandatory)]
[string[]]$Urls,

[int]$TimeoutSec = 10
)

$results = foreach ($url in $Urls) {
$timer = [System.Diagnostics.Stopwatch]::StartNew()

try {
$response = Invoke-WebRequest -Uri $url -TimeoutSec $TimeoutSec `
-UseBasicParsing -MaximumRedirection 0 -ErrorAction Stop

$timer.Stop()

[PSCustomObject]@{
URL = $url
状态码 = [int]$response.StatusCode
状态描述 = $response.StatusDescription
总耗时ms = $timer.ElapsedMilliseconds
内容长度 = if ($response.Headers['Content-Length']) {
'{0:N0} bytes' -f [int]$response.Headers['Content-Length']
} else {
'N/A'
}
服务器 = if ($response.Headers['Server']) {
$response.Headers['Server']
} else {
'-'
}
结果 = '成功'
}
} catch {
$timer.Stop()
$statusCode = $null

if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
}

[PSCustomObject]@{
URL = $url
状态码 = $statusCode
状态描述 = if ($statusCode) { $_.Exception.Message.Substring(0, 60) } else { '-' }
总耗时ms = $timer.ElapsedMilliseconds
内容长度 = 'N/A'
服务器 = '-'
结果 = '失败'
}
}
}

$results | Format-Table -AutoSize
}

$targets = @(
'https://blog.vichamp.com',
'https://github.com',
'https://nonexistent-test-12345.example.com'
)

Test-HttpQuality -Urls $targets

执行结果示例:

1
2
3
4
5
URL                                               状态码 状态描述                   总耗时ms 内容长度       服务器        结果
--- ------ -------- -------- ---------- ------ ----
https://blog.vichamp.com 200 OK 312 15,280 bytes GitHub.com 成功
https://github.com 200 OK 287 82,451 bytes GitHub.com 成功
https://nonexistent-test-12345.example.com - - 5012 N/A - 失败

延迟与丢包统计分析

网络延迟和丢包率是衡量链路质量的核心指标。下面的脚本通过多次 ICMP 测试,统计平均延迟、最小/最大延迟、抖动(jitter)以及丢包率,并给出一个综合评级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
function Test-NetworkLatency {
<#
.SYNOPSIS
测试到目标主机的延迟和丢包率
#>
param(
[Parameter(Mandatory)]
[string[]]$Targets,

[int]$Count = 20,

[int]$DelayMs = 500
)

$results = foreach ($target in $Targets) {
Write-Host "正在测试 $target ..." -ForegroundColor Gray

$latencies = [System.Collections.Generic.List[double]]::new()
$lostCount = 0

for ($i = 0; $i -lt $Count; $i++) {
$reply = Test-Connection -ComputerName $target -Count 1 -Quiet -ErrorAction SilentlyContinue

if ($reply) {
$detail = Test-Connection -ComputerName $target -Count 1 -ErrorAction SilentlyContinue
if ($detail) {
$latencies.Add($detail.ResponseTime)
}
} else {
$lostCount++
}

if ($i -lt $Count - 1) {
Start-Sleep -Milliseconds $DelayMs
}
}

if ($latencies.Count -gt 0) {
$avg = [math]::Round(($latencies | Measure-Object -Average).Average, 1)
$min = [math]::Round(($latencies | Measure-Object -Minimum).Minimum, 1)
$max = [math]::Round(($latencies | Measure-Object -Maximum).Maximum, 1)

# 计算抖动(相邻延迟差的平均值)
$jitterValues = [System.Collections.Generic.List[double]]::new()
for ($j = 1; $j -lt $latencies.Count; $j++) {
$jitterValues.Add([math]::Abs($latencies[$j] - $latencies[$j - 1]))
}
$jitter = if ($jitterValues.Count -gt 0) {
[math]::Round(($jitterValues | Measure-Object -Average).Average, 1)
} else {
0
}

$lossRate = [math]::Round(($lostCount / $Count) * 100, 1)

# 综合评级
$grade = if ($avg -lt 10 -and $lossRate -eq 0 -and $jitter -lt 5) {
'优秀'
} elseif ($avg -lt 50 -and $lossRate -lt 1) {
'良好'
} elseif ($avg -lt 100 -and $lossRate -lt 5) {
'一般'
} else {
'较差'
}
} else {
$avg = $min = $max = $jitter = 0
$lossRate = 100
$grade = '不可达'
}

[PSCustomObject]@{
目标 = $target
平均ms = $avg
最小ms = $min
最大ms = $max
抖动ms = $jitter
丢包率 = "$lossRate%"
评级 = $grade
}
}

$results | Format-Table -AutoSize
}

# 批量测试多个目标的网络质量
Test-NetworkLatency -Targets @(
'baidu.com',
'google.com',
'github.com'
) -Count 15

执行结果示例:

1
2
3
4
5
目标        平均ms 最小ms 最大ms 抖动ms 丢包率  评级
---- ------ ------ ------ ------ ------ ----
baidu.com 8.3 5.2 15.1 1.8 0.0% 优秀
google.com 42.7 38.1 55.3 3.2 0.0% 良好
github.com 85.1 72.4 140.6 8.5 6.7% 一般

实时网络流量监控

最后一个工具用于监控指定时间窗口内的网络流量变化趋势。它每隔一定时间采样一次网卡统计数据,计算每秒的吞吐量,帮助你发现突发的流量异常(例如某个进程突然大量上传数据)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function Watch-NetworkTraffic {
<#
.SYNOPSIS
实时监控网络接口流量
#>
param(
[string]$InterfaceName = (Get-NetAdapter |
Where-Object { $_.Status -eq 'Up' -and $_.InterfaceType -notmatch 'Loopback' } |
Select-Object -First 1).Name,

[int]$DurationSeconds = 30,

[int]$IntervalSeconds = 2
)

$adapter = Get-NetAdapter -Name $InterfaceName -ErrorAction SilentlyContinue

if (-not $adapter) {
Write-Host "未找到网络接口:$InterfaceName" -ForegroundColor Red
return
}

Write-Host "监控接口:$InterfaceName ($($adapter.InterfaceDescription))" -ForegroundColor Cyan
Write-Host "持续时间:${DurationSeconds}秒,采样间隔:${IntervalSeconds}秒" -ForegroundColor Cyan
Write-Host ('-' * 70)

$samples = [System.Collections.Generic.List[PSCustomObject]]::new()
$startTime = Get-Date

$prevStats = Get-NetAdapterStatistics -Name $InterfaceName
$prevTime = Get-Date

while (((Get-Date) - $startTime).TotalSeconds -lt $DurationSeconds) {
Start-Sleep -Seconds $IntervalSeconds

$currentStats = Get-NetAdapterStatistics -Name $InterfaceName
$currentTime = Get-Date

$elapsed = ($currentTime - $prevTime).TotalSeconds
if ($elapsed -le 0) { $elapsed = 1 }

$rxRate = ($currentStats.ReceivedBytes - $prevStats.ReceivedBytes) / $elapsed
$txRate = ($currentStats.SentBytes - $prevStats.SentBytes) / $elapsed

function Format-Bytes {
param([double]$Bytes)
if ($Bytes -ge 1MB) { '{0,8:N2} MB/s' -f ($Bytes / 1MB) }
elseif ($Bytes -ge 1KB) { '{0,8:N2} KB/s' -f ($Bytes / 1KB) }
else { '{0,8:N2} B/s' -f $Bytes }
}

$sample = [PSCustomObject]@{
时间 = $currentTime.ToString('HH:mm:ss')
接收速率 = Format-Bytes $rxRate
发送速率 = Format-Bytes $txRate
累计接收 = '{0:N2} MB' -f ($currentStats.ReceivedBytes / 1MB)
累计发送 = '{0:N2} MB' -f ($currentStats.SentBytes / 1MB)
}

$samples.Add($sample)
Write-Host (" {0} | 接收: {1} | 发送: {2}" -f `
$sample.时间, $sample.接收速率, $sample.发送速率)

$prevStats = $currentStats
$prevTime = $currentTime
}

Write-Host ('-' * 70)
Write-Host "采样完成,共 $($samples.Count) 个数据点" -ForegroundColor Green

$samples | Format-Table -AutoSize
}

# 监控 30 秒内的网络流量变化
Watch-NetworkTraffic -DurationSeconds 30 -IntervalSeconds 3

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
监控接口:以太网 (Intel(R) Ethernet Connection I219-V)
持续时间:30秒,采样间隔:3秒
----------------------------------------------------------------------
14:20:03 | 接收: 2.35 MB/s | 发送: 0.42 MB/s
14:20:06 | 接收: 5.18 MB/s | 发送: 0.38 MB/s
14:20:09 | 接收: 1.72 MB/s | 发送: 0.25 MB/s
14:20:12 | 接收: 8.91 MB/s | 发送: 1.23 MB/s
14:20:15 | 接收: 3.44 MB/s | 发送: 0.55 MB/s
14:20:18 | 接收: 0.85 MB/s | 发送: 0.12 MB/s
14:20:21 | 接收: 1.06 MB/s | 发送: 0.18 MB/s
14:20:24 | 接收: 4.67 MB/s | 发送: 0.89 MB/s
14:20:27 | 接收: 2.13 MB/s | 发送: 0.33 MB/s
14:20:30 | 接收: 1.88 MB/s | 发送: 0.29 MB/s
----------------------------------------------------------------------
采样完成,共 10 个数据点

时间 接收速率 发送速率 累计接收 累计发送
---- -------- -------- -------- --------
14:20:03 2.35 MB/s 0.42 MB/s 12,350.22 MB 3,281.45 MB
14:20:06 5.18 MB/s 0.38 MB/s 12,365.76 MB 3,282.59 MB
14:20:09 1.72 MB/s 0.25 MB/s 12,370.92 MB 3,283.34 MB

注意事项

  1. 管理员权限Get-NetAdapterStatistics 等部分 cmdlet 在某些 Windows 版本上需要以管理员身份运行 PowerShell,否则可能返回不完整的数据
  2. 采样精度:流量监控的精度取决于采样间隔,间隔越短结果越精确,但也会增加系统开销;建议生产环境使用 3-5 秒的间隔
  3. IPv4/IPv6 兼容:延迟测试中 Test-Connection 默认可能优先使用 IPv6,如果目标仅支持 IPv4,可通过 -TargetName 参数配合 [System.Net.Dns]::GetHostAddresses 显式指定地址族
  4. HTTPS 证书警告:HTTP 质量检测中遇到自签名证书或过期证书时,Invoke-WebRequest 会抛出异常,可根据实际需求决定是否跳过证书验证(添加 -SkipCertificateCheck 参数,仅限 PowerShell 7+)
  5. 并发性能:批量测试多个目标时,如果目标数量较多,可以考虑使用 Runspaces 或 PowerShell 7 的 ForEach-Object -Parallel 来并发执行,显著缩短总耗时
  6. 防火墙与 ICMP:部分云服务器和网络安全设备默认屏蔽 ICMP 协议,此时 ping 测试会失败但 HTTP 服务可能正常,诊断时应结合多种协议综合判断