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 技能连载 - 网络故障排查工具集

适用于 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 及以上版本

PowerShell 脚本在处理大数据集、长时间运行的任务或复杂的对象管道时,内存消耗会快速增长。对象在管道中逐个传递,每个中间步骤都会产生新的 .NET 对象,而 .NET 的垃圾回收器(GC)并不总是能及时回收。理解 PowerShell 的内存模型和 GC 机制,对于编写高性能脚本至关重要。

本文将介绍内存诊断方法、GC 调优策略,以及减少内存占用的编码技巧。

内存诊断与监控

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
# 当前进程内存信息
$proc = Get-Process -Id $PID
Write-Host "PowerShell 进程内存分析" -ForegroundColor Cyan
Write-Host " 工作集 (Working Set): $([math]::Round($proc.WorkingSet64 / 1MB, 2)) MB"
Write-Host " 专用字节 (Private Bytes): $([math]::Round($proc.PrivateMemorySize64 / 1MB, 2)) MB"
Write-Host " 虚拟内存: $([math]::Round($proc.VirtualMemorySize64 / 1MB, 2)) MB"
Write-Host " 句柄数: $($proc.HandleCount)"
Write-Host " 线程数: $($proc.Threads.Count)"

# .NET CLR 内存统计
$heap = [GC]::GetTotalMemory($false)
Write-Host "`n.NET 堆内存:$([math]::Round($heap / 1MB, 2)) MB" -ForegroundColor Yellow

# GC 代数统计
Write-Host "`nGC 代数回收次数:" -ForegroundColor Cyan
Write-Host " Gen 0: $([GC]::CollectionCount(0))"
Write-Host " Gen 1: $([GC]::CollectionCount(1))"
Write-Host " Gen 2: $([GC]::CollectionCount(2))"

# 内存消耗函数
function Measure-ScriptMemory {
param([scriptblock]$ScriptBlock, [string]$Label = "Test")

[GC]::Collect()
[GC]::WaitForPendingFinalizers()
$before = [GC]::GetTotalMemory($true)

$sw = [System.Diagnostics.Stopwatch]::StartNew()
& $ScriptBlock
$sw.Stop()

$after = [GC]::GetTotalMemory($false)
$diff = $after - $before

[PSCustomObject]@{
Label = $Label
BeforeMB = [math]::Round($before / 1MB, 2)
AfterMB = [math]::Round($after / 1MB, 2)
DeltaMB = [math]::Round($diff / 1MB, 2)
TimeMs = $sw.ElapsedMilliseconds
Gen0 = [GC]::CollectionCount(0)
Gen1 = [GC]::CollectionCount(1)
Gen2 = [GC]::CollectionCount(2)
}
}

# 对比不同方式的内存消耗
$result1 = Measure-ScriptMemory -Label "数组累加" -ScriptBlock {
$arr = @()
foreach ($i in 1..10000) { $arr += $i }
}

$result2 = Measure-ScriptMemory -Label "List 泛型" -ScriptBlock {
$list = [System.Collections.Generic.List[int]]::new()
foreach ($i in 1..10000) { $list.Add($i) }
}

$result3 = Measure-ScriptMemory -Label "赋值变量" -ScriptBlock {
$result = foreach ($i in 1..10000) { $i }
}

"数组累加", "List 泛型", "赋值变量" | ForEach-Object {
switch ($_) {
"数组累加" { $result1 }
"List 泛型" { $result2 }
"赋值变量" { $result3 }
}
} | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PowerShell 进程内存分析
工作集 (Working Set): 156.78 MB
专用字节 (Private Bytes): 142.34 MB
虚拟内存: 1024.56 MB
句柄数: 523
线程数: 12

.NET 堆内存:34.56 MB

GC 代数回收次数:
Gen 0: 15
Gen 1: 3
Gen 2: 1

Label BeforeMB AfterMB DeltaMB TimeMs Gen0 Gen1 Gen2
----- -------- ------- ------- ------ ---- ---- ----
数组累加 34.56 42.31 7.75 234 18 3 1
List 泛型 42.31 42.34 0.03 12 18 3 1
赋值变量 42.34 42.35 0.01 8 18 3 1

垃圾回收与内存释放

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
# 大对象堆(LOH)分析
# 超过 85,000 字节的对象存储在 LOH 中,LOH 的回收代价很高
function Get-LargeObjectStats {
# 使用 GC 内存信息
$totalMemory = [GC]::GetTotalMemory($false)
$heapSize = [System.AppDomain]::CurrentDomain.MonitoringTotalAllocatedMemorySize

Write-Host "总分配内存:$([math]::Round($heapSize / 1MB, 2)) MB" -ForegroundColor Cyan
Write-Host "当前存活内存:$([math]::Round($totalMemory / 1MB, 2)) MB" -ForegroundColor Yellow

# 强制 GC 回收
Write-Host "`n执行完整 GC 回收..." -ForegroundColor Yellow
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
[GC]::Collect() # 二次回收确保 Finalizer 队列的对象被回收

$afterGC = [GC]::GetTotalMemory($true)
$freed = $totalMemory - $afterGC
Write-Host "回收后内存:$([math]::Round($afterGC / 1MB, 2)) MB(释放 $([math]::Round($freed / 1MB, 2)) MB)" -ForegroundColor Green
}

Get-LargeObjectStats

# 处理大文件时的内存优化
function Get-LargeFileHashBatch {
param(
[string]$Path,
[int]$BatchSize = 100
)

$files = [System.IO.Directory]::EnumerateFiles($Path, "*", [System.IO.SearchOption]::AllDirectories)
$batch = @()
$totalProcessed = 0

foreach ($file in $files) {
$info = [System.IO.FileInfo]::new($file)
$hash = (Get-FileHash $file -Algorithm SHA256).Hash

$batch += [PSCustomObject]@{
File = $info.Name
SizeKB = [math]::Round($info.Length / 1KB, 2)
Hash = $hash.Substring(0, 16) + "..."
}

$totalProcessed++

if ($batch.Count -ge $BatchSize) {
# 输出批次结果并释放引用
$batch
$batch = @()

if ($totalProcessed % 500 -eq 0) {
[GC]::Collect()
$mem = [math]::Round([GC]::GetTotalMemory($false) / 1MB, 2)
Write-Host " 已处理 $totalProcessed 个文件,内存:$mem MB" -ForegroundColor DarkGray
}
}
}

# 输出最后一批
if ($batch) { $batch }
Write-Host "`n总计处理:$totalProcessed 个文件" -ForegroundColor Green
}

# 使用流式处理替代一次性加载
function Read-LargeCsvStreaming {
param(
[string]$Path,
[int]$TopN = 10
)

$reader = [System.IO.StreamReader]::new($Path)
$header = $reader.ReadLine()
$columns = $header -split ','

$count = 0
while ($null -ne ($line = $reader.ReadLine()) -and $count -lt $TopN) {
$values = $line -split ','
$obj = [ordered]@{}
for ($i = 0; $i -lt $columns.Count; $i++) {
$obj[$columns[$i].Trim('"')] = $values[$i].Trim('"')
}
[PSCustomObject]$obj
$count++
}

$reader.Close()
$reader.Dispose()
}

Write-Host "`n流式处理避免了将整个文件加载到内存" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
7
8
9
总分配内存:256.78 MB
当前存活内存:42.31 MB

执行完整 GC 回收...
回收后内存:18.45 MB(释放 23.86 MB)
已处理 500 个文件,内存:24.56 MB
已处理 1000 个文件,内存:22.34 MB
总计处理:1234 个文件
流式处理避免了将整个文件加载到内存

性能分析实战

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
# 脚本性能分析器
function Measure-ScriptPerformance {
param(
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock,

[int]$Iterations = 3
)

$results = @()

# 预热(排除 JIT 编译影响)
Write-Host "预热中..." -ForegroundColor DarkGray
& $ScriptBlock | Out-Null

for ($i = 1; $i -le $Iterations; $i++) {
[GC]::Collect()
[GC]::WaitForPendingFinalizers()

$memBefore = [GC]::GetTotalMemory($true)
$sw = [System.Diagnostics.Stopwatch]::StartNew()

& $ScriptBlock | Out-Null

$sw.Stop()
$memAfter = [GC]::GetTotalMemory($false)

$results += [PSCustomObject]@{
Iteration = $i
TimeMs = $sw.ElapsedMilliseconds
MemoryKB = [math]::Round(($memAfter - $memBefore) / 1KB, 2)
Gen0 = [GC]::CollectionCount(0)
}

Write-Host " 第 $i 次:$($sw.ElapsedMilliseconds) ms" -ForegroundColor DarkGray
}

$avg = ($results | Measure-Object TimeMs -Average).Average
$min = ($results | Measure-Object TimeMs -Minimum).Minimum
$max = ($results | Measure-Object TimeMs -Maximum).Maximum

Write-Host "`n性能统计:" -ForegroundColor Cyan
Write-Host " 平均:$([math]::Round($avg, 2)) ms"
Write-Host " 最快:$min ms"
Write-Host " 最慢:$max ms"

return $results
}

# 对比字符串拼接方式
Write-Host "=== 字符串拼接对比 ===" -ForegroundColor Yellow

Measure-ScriptPerformance -Iterations 3 -ScriptBlock {
$s = ""
foreach ($i in 1..1000) { $s += "Line $i`n" }
} | Out-Null

Measure-ScriptPerformance -Iterations 3 -ScriptBlock {
$sb = [System.Text.StringBuilder]::new()
foreach ($i in 1..1000) { $sb.AppendLine("Line $i") | Out-Null }
$sb.ToString()
} | Out-Null

执行结果示例:

1
2
3
4
5
6
7
8
9
预热中...
1 次:45 ms
第 2 次:42 ms
第 3 次:40 ms

性能统计:
平均:42.33 ms
最快:40 ms
最慢:45 ms

注意事项

  1. 数组 += 陷阱:PowerShell 中 $arr += $item 每次都创建新数组,复杂度 O(n^2),大数据集必须用 List 泛型
  2. GC 非万能:显式调用 [GC]::Collect() 有其代价,不应过度使用,主要依赖 GC 的自动回收
  3. 大对象处理:超过 85KB 的对象进入 LOH,LOH 的碎片化是内存泄漏的常见原因
  4. 流式处理:处理大文件时使用 StreamReader/EnumerateFiles 代替 Get-Content/Get-ChildItem,避免一次性加载
  5. 对象释放:使用 IDisposable 对象(如 Stream、Connection)后务必调用 Dispose() 或使用 using 语句
  6. 32 位限制:Windows PowerShell 5.1(32 位)进程有 2GB 内存上限,处理大数据时应使用 64 位 PowerShell

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 服务可能正常,诊断时应结合多种协议综合判断

PowerShell 技能连载 - 网络诊断与排查

适用于 PowerShell 5.1 及以上版本

网络故障是运维工作中最常见的排障场景——“连不上数据库”、”网站打不开”、”文件共享超时”。PowerShell 内置了丰富的网络诊断命令,从基本的 ping、端口检测到 DNS 解析、路由追踪和 TCP 连接测试,可以快速定位网络问题的层级(物理层、链路层、网络层、传输层、应用层)。

本文将讲解 PowerShell 网络诊断的系统化方法,以及如何构建一键式排障脚本。

基础连通性测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Test-Connection(PowerShell 的 ping)
Test-Connection -ComputerName google.com -Count 4 |
Select-Object Address, @{N='延迟ms'; E={$_.ResponseTime}}, Status |
Format-Table -AutoSize

# Test-NetConnection(更强大的网络测试)
Test-NetConnection -ComputerName blog.vichamp.com -Port 443 |
Select-Object ComputerName, RemoteAddress, RemotePort, TcpTestSucceeded, PingSucceeded |
Format-List

# 批量测试多台服务器的连通性
$servers = @('web-01.internal', 'db-01.internal', 'api-01.internal', 'redis-01.internal')

$results = foreach ($server in $servers) {
$test = Test-NetConnection -ComputerName $server -WarningAction SilentlyContinue
[PSCustomObject]@{
Server = $server
IP = $test.RemoteAddress
Ping = $test.PingSucceeded
Port = if ($test.TcpTestSucceeded) { 'Open' } else { 'Closed/Filtered' }
}
}

$results | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Address   延迟ms Status
------- ------ ------
google.com 12 True
google.com 15 True
google.com 11 True
google.com 14 True

ComputerName : blog.vichamp.com
RemoteAddress : 185.199.108.153
RemotePort : 443
TcpTestSucceeded : True
PingSucceeded : True

Server IP Ping Port
------ -- ---- ----
web-01.internal 192.168.1.101 True Open
db-01.internal 192.168.1.201 True Open
api-01.internal 192.168.1.102 True Open
redis-01.internal 192.168.1.202 False Closed/Filtered

DNS 诊断

DNS 解析问题是网络故障的高发区:

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
# DNS 解析
Resolve-DnsName -Name blog.vichamp.com | Format-Table -AutoSize

# 查看所有 DNS 记录类型
Resolve-DnsName -Name contoso.com -Type ANY |
Select-Object Name, Type, Section, Data |
Format-Table -AutoSize

# 查看当前 DNS 服务器配置
Get-DnsClientServerAddress -AddressFamily IPv4 |
Where-Object { $_.InterfaceAlias -notmatch 'Loopback' } |
Select-Object InterfaceAlias, ServerAddresses |
Format-Table -AutoSize

# 刷新 DNS 缓存
Clear-DnsClientCache
Write-Host "DNS 缓存已清除" -ForegroundColor Green

# DNS 解析对比(用不同 DNS 服务器)
function Compare-DnsResolution {
param([string]$Domain)

$servers = @{
'默认' = $null
'Google' = '8.8.8.8'
'Cloudflare' = '1.1.1.1'
'AliDNS' = '223.5.5.5'
}

foreach ($name in $servers.Keys) {
try {
$params = @{ Name = $Domain }
if ($servers[$name]) { $params.Server = $servers[$name] }

$result = Resolve-DnsName @params -ErrorAction Stop |
Select-Object -First 1

[PSCustomObject]@{
DNS服务器 = $name
IP地址 = $result.IPAddress
类型 = $result.Type
TTL = $result.TTL
}
} catch {
[PSCustomObject]@{
DNS服务器 = $name
IP地址 = "解析失败: $($_.Exception.Message)"
}
}
}
}

Compare-DnsResolution -Domain "blog.vichamp.com" | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Name             Type  Section Data
---- ---- ------- ----
blog.vichamp.com CNAME Name victorwoo.github.io

InterfaceAlias ServerAddresses
-------------- ---------------
以太网 {192.168.1.1}

DNS服务器 IP地址 类型 TTL
--------- ------ ---- ---
默认 185.199.108.153 CNAME 3600
Google 185.199.108.153 CNAME 3600
Cloudflare 185.199.108.153 CNAME 3600
AliDNS 185.199.108.153 CNAME 3600

TCP 端口扫描

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
function Test-TcpPort {
<#
.SYNOPSIS
测试目标主机的 TCP 端口连通性
#>
param(
[Parameter(Mandatory)]
[string]$ComputerName,

[int[]]$Ports = @(22, 80, 443, 3389, 5985, 5986, 8080, 1433, 3306, 6379),

[int]$TimeoutMs = 3000
)

$results = foreach ($port in $Ports) {
$tcpClient = New-Object System.Net.Sockets.TcpClient
$asyncResult = $tcpClient.BeginConnect($ComputerName, $port, $null, $null)
$waited = $asyncResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)

$isOpen = $false
if ($waited) {
try {
$tcpClient.EndConnect($asyncResult)
$isOpen = $true
} catch {
$isOpen = $false
}
}

$tcpClient.Close()

$serviceName = switch ($port) {
22 { 'SSH' }
80 { 'HTTP' }
443 { 'HTTPS' }
3389 { 'RDP' }
5985 { 'WinRM-HTTP' }
5986 { 'WinRM-HTTPS' }
8080 { 'HTTP-Alt' }
1433 { 'MSSQL' }
3306 { 'MySQL' }
6379 { 'Redis' }
default { 'Unknown' }
}

[PSCustomObject]@{
Port = $port
Service = $serviceName
Status = if ($isOpen) { 'Open' } else { 'Closed' }
}
}

$results | Format-Table -AutoSize
}

# 扫描目标服务器的常用端口
Test-TcpPort -ComputerName "192.168.1.100" -Ports @(22, 80, 443, 3389, 5985, 1433, 6379)

执行结果示例:

1
2
3
4
5
6
7
8
9
Port Service     Status
---- ------- ------
22 SSH Closed
80 HTTP Open
443 HTTPS Open
3389 RDP Open
5985 WinRM-HTTP Open
1433 MSSQL Open
6379 Redis Closed

构建一键式排障脚本

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
function Invoke-NetworkTroubleshooter {
<#
.SYNOPSIS
一键式网络排障脚本
#>
param(
[Parameter(Mandatory)]
[string]$Target,

[int]$Port = 443
)

Write-Host "========== 网络排障:$Target ==========" -ForegroundColor Cyan

# Step 1: DNS 解析
Write-Host "`n[1/5] DNS 解析" -ForegroundColor Yellow
try {
$dns = Resolve-DnsName -Name $Target -ErrorAction Stop
$ip = ($dns | Where-Object { $_.Type -eq 'A' } | Select-Object -First 1).IPAddress
if (-not $ip) {
$ip = ($dns | Select-Object -First 1).IPAddress
}
Write-Host " 解析结果:$Target => $ip" -ForegroundColor Green
} catch {
Write-Host " DNS 解析失败:$($_.Exception.Message)" -ForegroundColor Red
Write-Host " 建议:检查 DNS 配置或尝试使用其他 DNS 服务器" -ForegroundColor Yellow
return
}

# Step 2: ICMP Ping
Write-Host "`n[2/5] Ping 测试" -ForegroundColor Yellow
$ping = Test-Connection -ComputerName $ip -Count 3 -Quiet
if ($ping) {
$latency = (Test-Connection -ComputerName $ip -Count 1).ResponseTime
Write-Host " Ping 成功,延迟:${latency}ms" -ForegroundColor Green
} else {
Write-Host " Ping 失败(可能被防火墙阻止)" -ForegroundColor Yellow
}

# Step 3: TCP 端口测试
Write-Host "`n[3/5] TCP 端口测试 ($Port)" -ForegroundColor Yellow
$tcp = Test-NetConnection -ComputerName $ip -Port $Port -WarningAction SilentlyContinue
if ($tcp.TcpTestSucceeded) {
Write-Host " 端口 $Port 连接成功" -ForegroundColor Green
} else {
Write-Host " 端口 $Port 连接失败" -ForegroundColor Red
Write-Host " 建议:检查防火墙规则或目标服务是否运行" -ForegroundColor Yellow
}

# Step 4: 路由追踪
Write-Host "`n[4/5] 路由追踪 (tracert)" -ForegroundColor Yellow
$trace = tracert -d -h 15 -w 1000 $ip 2>$null
$trace | Select-Object -First 15 | ForEach-Object { Write-Host " $_" }

# Step 5: HTTP 测试(如果是 Web 服务)
if ($Port -in @(80, 443, 8080, 8443)) {
Write-Host "`n[5/5] HTTP 测试" -ForegroundColor Yellow
$protocol = if ($Port -in @(443, 8443)) { 'https' } else { 'http' }
try {
$response = Invoke-WebRequest -Uri "${protocol}://${Target}" `
-TimeoutSec 10 -UseBasicParsing -MaximumRedirection 0 -ErrorAction Stop
Write-Host " HTTP $($response.StatusCode) - $($response.StatusDescription)" -ForegroundColor Green
} catch {
$statusCode = $_.Exception.Response.StatusCode
if ($statusCode) {
Write-Host " HTTP $([int]$statusCode) - $($statusCode)" -ForegroundColor Yellow
} else {
Write-Host " HTTP 请求失败:$($_.Exception.Message)" -ForegroundColor Red
}
}
}

Write-Host "`n========== 排障完成 ==========" -ForegroundColor Cyan
}

# 一键排查网络问题
Invoke-NetworkTroubleshooter -Target "blog.vichamp.com" -Port 443

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
========== 网络排障:blog.vichamp.com ==========

[1/5] DNS 解析
解析结果:blog.vichamp.com => 185.199.108.153

[2/5] Ping 测试
Ping 成功,延迟:45ms

[3/5] TCP 端口测试 (443)
端口 443 连接成功

[4/5] 路由追踪 (tracert)
1 <1 ms <1 ms <1 ms 192.168.1.1
2 5 ms 3 ms 3 ms 10.0.0.1
...

[5/5] HTTP 测试
HTTP 200 - OK

========== 排障完成 ==========

注意事项

  1. 防火墙影响:ICMP ping 被阻止不代表服务不可用,始终结合 TCP 端口测试判断
  2. DNS 缓存:排查 DNS 问题时先执行 Clear-DnsClientCache 清除缓存
  3. 超时设置:给网络测试设置合理的超时,避免脚本无限等待
  4. IPv4/IPv6:某些环境可能有 IPv6 相关问题,使用 -AddressFamily IPv4 强制使用 IPv4
  5. 权限要求:部分网络诊断命令(如路由追踪)需要管理员权限
  6. 安全合规:端口扫描功能仅用于授权的网络诊断和安全审计,不得用于未授权的扫描