參考整體架構

[ODBC]
DRIVER=SQL Server
UID=frank
Trusted_Connection=Yes
Database=松測
WSID=FRANK
CONNECT=1
SERVER=192.168.1.159 *** 資料庫指向主機的區網位置 ***
DBQ=C:\Program Files\印刷帳務管理系統(SQL)\WNSH.mdb
Site=S *** 指主機而不是工作站 ***
| 項目 | 設定值 | 說明 |
|---|---|---|
| ERP Server | 192.168.1.156 | 安裝印刷帳務管理系統(SQL) 主程式 |
| SQL Server | 192.168.1.159 | 真正的資料庫所在地 |
| 管理工具 | SSMS v18.11.1 | 安裝在 **ERP Server** 上遠端管理 |
| 資料庫名稱 | 松測 | 這次的測試庫 |
| 資料夾 | SQL | 存放 ERP Server 相關代碼 |
| DSN | ODBC 系統資料來源 | ERP 連線的橋樑 |
ERP Server 與 SQL Server 跨伺服器分流實務指南
ERP Server 與 SQL Server 分到不同的實體/虛擬伺服器上,以提高 ERP Server 系統速度!
先前工作-在 SQL Server 設定 SQL 共用夾,方便在 ERP Server 還原資料庫時,找得到要還原的 bak 檔

一、 為什麼要「分家」?
這在資料量少、上線人數少的草創期確實行得通,但隨著公司擴大、交易單據積少成多,就會開始影響效能
- 記憶體爭奪戰(RAM Starvation): SQL Server 是一個對記憶體「要求」的軟體,預設情況下,只要給它實體記憶體,它就會盡可能吃滿以快取資料頁(Data Pages),此時,ERP 系統主程式要在伺服器上處理邏輯計算、產生報表時,就會發現系統根本沒有剩餘的 RAM 可用,只能一直讀寫硬碟 swap 空間,導致整體效能慘不忍睹。
- CPU 資源搶占(CPU Bottleneck): ERP 系統要處理商業邏輯(計算成本、應收帳款、庫存核算),SQL Server 要處理複雜的查詢(Index Scan、Join、Sort),當幾十個使用者同時點擊大型報表,伺服器的 CPU 執行緒會在應用程式與資料庫進程之間頻繁進行上下文切換(Context Switching),大幅衰減運算效率。
- 單點故障風險(Single Point of Failure, SPOF): 一旦這台「單獨」伺服器硬體故障(如主機板燒毀或 POWER 損壞),企業的應用程式與關鍵業務資料庫將一瞬間同時陷入癱瘓,完全沒有緩衝空間。
讓 ERP 專心處理商業邏輯與介面運算,讓 SQL Server 專心做資料查詢與交易寫入,「各司其職,分而治之」,才是打造高可用性、高效能企業 IT 環境的不二法門!讓 SQL Server 在本機 ERP Server(192.168.1.156) 完成遠端 (192.168.1.159) 還原
二、 本次實戰架構與環境設定
1. 網路與伺服器節點配置
- ERP 應用伺服器(ERP Server):
- IP 位址:
192.168.1.156 - 角色: 執行 ERP 主系統、客戶端對接、SQL Server Management Studio (SSMS) 管理介面。
- IP 位址:
- 資料庫伺服器(SQL Server):
- IP 位址:
192.168.1.159 - 角色: 專門運行 MS SQL Server 資料庫引擎,存放業務資料。
- IP 位址:
- 預計管理之資料庫名稱:
松測 - SQL Server 存放 ERP 相關代碼與工作目錄: SQL(192.168.1.159)
2. 管理工具與設定檔細節
- 管理工具: SQL Server Management Studio (SSMS) V18.11.1(安裝於 ERP Server
192.168.1.156)。 - ODBC 連結檔 (DSN 設定):
有了這張清晰的地圖,接下來我們就進入最核心的實作階段!
[ODBC]
DRIVER=SQL Server
UID=frank
Trusted_Connection=Yes
Database=松測
WSID=FRANK
CONNECT=1
SERVER=192.168.1.159
DBQ=C:\Program Files\印刷帳務管理系統(SQL)\WNSH.mdb
Site=S

三、 第一關:通訊管道打通——跨伺服器連線與防火牆設定
既然把 ERP 與 SQL Server 放在兩台不同的伺服器(192.168.1.156 與 192.168.1.159),第一步就是要確保兩者之間的網路通暢,不會被中間的防火牆或 SQL 預設安全機制攔截。
步驟 1:開放 SQL Server (192.168.1.159) 的 TCP/IP 協定
MS SQL Server 在預設安裝時,為了安全性考量,往往會關閉遠端 TCP/IP 連線。
- 登入 SQL Server 伺服器 (
192.168.1.159)。 - 開啟 SQL Server 組態管理員(SQL Server Configuration Manager)。
- 展開 SQL Server 網路組態 -> 點擊 MSSQLSERVER 的協定。
- 找到 TCP/IP 項目,如果狀態為「已停用」,請點右鍵選擇 【啟用】。
- 點擊 TCP/IP 右鍵【內容】,切換到 【IP 位址】 頁籤,拉到最底部的
IPAll,確認 TCP 連接埠 設定為預設的1433。 - 關鍵步驟: 重啟 SQL Server 服務(SQL Server Services -> SQL Server (MSSQLSERVER) -> 右鍵【重新啟動】),設定才會生效!

步驟 2:配置 Windows 防火牆規則
許多 IT 人員折騰半天連不上,80% 是被 Windows 防火牆硬生生檔了下來。
在 SQL Server 伺服器 (192.168.1.159) 的 Windows 防火牆中:
- 新增輸入規則(Inbound Rule)。
- 選擇 【連接埠 (Port)】,指定 TCP 1433,允許連線。
- 如果有使用 SQL Server Browser 服務或動態連接埠,建議另外開放 UDP 1434。
- 在 ERP Server (
192.168.1.156) 開啟 Command Prompt (cmd),輸入:telnet 192.168.1.159 1433如果畫面變黑或顯示連線成功,恭喜你,通訊大門已經順利開啟!
(Port 1433) 是 SQL Server 預設通訊埠
建議安全作法
PS C:\Users\Administrator>New-NetFirewallRule -DisplayName “SQL 1433 from ERP Server (192.168.1.156)” -Direction Inbound
-Protocol TCP -LocalPort 1433
-RemoteAddress 192.168.1.156 `
-Action Allow
這是一段 Windows PowerShell 的指令,用途是在系統的防火牆(Windows Defender Firewall)中新增一條「允許(Inbound)進站規則」,專門放行來自 ERP 伺服器 (IP: 192.168.1.156) 對本機 SQL Server(預設連接埠 TCP 1433)的連線存取。
以下為指令的逐行/逐個參數詳細拆解,以及所有縮寫的英文全文:
一、 指令結構與參數詳細解析
語法說明: PowerShell 中的反引號
`(Backtick)是換行續行符號,代表下一行的內容與這一行是同一條指令。
1. New-NetFirewallRule
- 意義: PowerShell 的 Cmdlet 指令,用來建立一條新的網路防火牆規則。
2. -DisplayName “SQL 1433 from ERP Server (192.168.1.156)”
- 意義: 設定該條規則在「進站規則(Inbound Rules)」清單中顯示的名稱,這只是給管理者看的標籤,方便日後維護與辨識用途。
3. -Direction Inbound
- 意義: 指定封包的方向為進站(Inbound),也就是規範「外面主機嘗試連線進來這台電腦」的行為。
4. -Protocol TCP
- 意義: 指定此規則套用的通訊協定為 TCP,SQL Server 資料庫的傳輸主要採用 TCP 協定。
5. -LocalPort 1433
- 意義: 指定本機(目的地)開放的連接埠(Port)為 1433,1433 是 Microsoft SQL Server 的預設監聽連接埠。
6. -RemoteAddress 192.168.1.156
- 意義: 設定來源端(遠端)的 IP 位址限制,只有來自 IP 為
192.168.1.156的電腦發出的封包才能命中此規則;其他 IP 發起的 1433 連線會被排除(套用預設阻擋機制)。
7. -Action Allow
- 意義: 符合上述條件(TCP、Port 1433、來自 192.168.1.156)的網路連線,發揮的動作是允許(Allow)通過。
二、 各簡稱與技術名詞英文全文
| 簡稱 / 名詞 | 英文全文 | 中文解釋 |
| PS | PowerShell | 微軟開發的高階自動化與管理命令列工具。 |
| Net | Network | 網路。 |
| SQL | Structured Query Language | 結構化查詢語言,此處通常指 Microsoft SQL Server 資料庫服務。 |
| ERP | Enterprise Resource Planning | 企業資源計劃系統。 |
| TCP | Transmission Control Protocol | 傳輸控制協定(一種可靠導向的網路連線協定)。 |
| IP | Internet Protocol | 網際網路協定(用於網路位址定址,如 192.168.1.156)。 |
| Port | Port (非簡稱) | 連接埠 / 通訊埠(作業系統用來區分不同應用程式服務的通訊管道)。 |
💡 總結這條命令的作用
執行這條命令後,這台 Windows 伺服器會開啟一扇專屬後門:只給 192.168.1.156 這台 ERP 伺服器存取本機的 1433 SQL 資料庫服務,既能滿足業務系統連線需求,又不會將 1433 連接埠暴露給整個區域網路的其他未授權裝置,是一種兼顧效能與資安的實務設定方式。
四、 第二關:DSN 檔案剖析與 ODBC 跨機連線設定
在我們的案例中,ERP 系統使用了一個 DSN (Data Source Name) 設定檔來尋找資料庫,讓我們來仔細剖析這段關鍵的 DSN 內容:
[ODBC]
DRIVER=SQL Server
UID=frank *** frank 只是測試用 ***
Trusted_Connection=Yes
Database=松測
WSID=FRANK *** FRANK 只是測試用 ***
CONNECT=1
SERVER=192.168.1.159
DBQ=C:\Program Files\印刷帳務管理系統(SQL)\WNSH.mdb
Site=S *** Server 的意思 ***
關鍵參數深度解讀:
SERVER=192.168.1.159: 這是分散式架構的靈魂!它告訴 ERP 系統,不要在本地(127.0.0.1或localhost)找資料,而是直接透過網路封包,前往 IP 為192.168.1.159的 SQL Server 資料庫主機請求資料庫服務。Database=松測: 指定連接的資料庫名稱為「松測」。UID=frank與Trusted_Connection=Yes: 這裡包含了混合認證機制。Trusted_Connection=Yes代表使用 Windows 驗證模式(Domain/Windows 帳號),若 ERP 與 SQL 未加入同一個 Active Directory 網域,建議改用 SQL 混合驗證(設定Trusted_Connection=No,並帶入 SQL 專屬帳號frank與對應密碼)。DBQ=...WNSH.mdb: 這是早期許多台灣傳統 ERP(如印刷帳務管理系統)常見的雙模架構,可能作為前端暫存檔或 Access 轉接介面,但在後端交易上,核心已正式交由192.168.1.159的 SQL Server 處理。
五、 第三關:跨機超操控!在 ERP Server 透過 SSMS 還原資料庫
現在進入本篇最精采、也最容易讓人產生觀念混淆的重頭戲——「如何在 ERP Server (192.168.1.156) 上,利用 SSMS V18.11.1,將資料庫還原至 SQL Server (192.168.1.159) 且指定存放於 “SQL" 資料夾?」
🚨 觀念陷阱警報: 在操作跨機還原時,一定要記住:「SSMS 只是操控遙控器,真正的檔案讀取與寫入動作,全部發生在遠端的 SQL Server (
192.168.1.159) 上!」
實戰步驟 1:開啟 SSMS V18.11.1 並遠端連線
打開 SSMS V18.11.1 及遠端連線後所看到的檔案總管會是 SQL Server 端的,所以要把相關要用的代碼放到 SQL Server 的 “SQL” 及設共用才能存取
- 在 ERP Server (
192.168.1.156) 前,開啟 SQL Server Management Studio V18.11.1。 - 在「連線到伺服器」視窗中:
- 伺服器類型: 資料庫引擎 (Database Engine)
- 伺服器名稱: 輸入遠端 SQL Server IP
192.168.1.159 - 驗證方式: 選擇 Windows 驗證或 SQL Server 驗證(帳號
frank)。
- 點擊【連線】。成功連入後,你在 SSMS 物件總管看到的,就是遠端
192.168.1.159主機上的資料庫狀態。









參考建議-關鍵的「檔案」設定——把資料庫存到 DB Server(192.168.1.159) 規劃的的 “SQL” 資料夾,其他相關代碼放”SQL” 資料夾下如 “DBworkarea”
這一步是效能最佳化的核心,當你選好備份檔後,先別急著按確定,切換到左側的「檔案」頁面(或有些版本叫「選項」)。
你會看到「將資料庫檔案還原為」的列表,通常會有兩個檔案:一個是資料檔(.mdf),一個是記錄檔(.ldf)。系統預設會把它們還原到 SQL Server 的預設目錄,這往往就是造成磁碟 I/O 瓶頸。
請手動修改「還原為」欄位的路徑,把它們指向你規劃的資料夾。例如:
C:\SQL\松測.mdf*** 此時的C:\SQL是指 SQL Server 的C:\SQL***C:\SQL\松測_log.ldf*** 此時的C:\SQL是指 SQL Server 的C:\SQL***
這樣做有兩個好處:
- 磁碟分離:如果
SQL資料夾是位於 192.168.1.159 這台機器上獨立的實體磁碟或 SSD,那就等於是把資料庫的讀寫壓力跟作業系統分開,以提高效能。 - 路徑統一:強制把資料庫檔案放在一個你清楚知道的地方,未來要備份或搬移都方便。
設定完成後,按下「確定」,SSMS 就會把指令送到 192.168.1.159,開始還原,你會看到進度條跑動,此時 192.168.1.156 ERP Server 完全不會感到任何負擔,因為它只是在「監工」。
實戰步驟 2:資料庫備份檔 (.bak) 的擺放位置
假設你手上有一個備份檔要還原成「松測」資料庫:
- 錯誤做法: 把
松測.bak放在 ERP Server (192.168.1.156) 的C:\磁碟機,然後在 SSMS 選擇還原。SQL Server (192.168.1.159) 會找不到檔案而報錯! - 正確做法: 必須將
松測.bak複製到 SQL Server (192.168.1.159) 本地的硬碟 中(例如C:\SQL\松測.bak),或者透過網路共享路徑(UNC Path)讓 SQL Server 服務帳戶有權限讀取。
六、 驗證與測試:如何確定還原成功?
當 SSMS 跳出「資料庫 ‘松測’ 還原成功」的提示視窗後,我們需要進行「階段驗證法」,確認系統已順利在新架構下運作:
1. 檔案實體確認
前往 SQL Server 伺服器 (192.168.1.159),打開檔案資源管理器,進入 C:\SQL\ 資料夾,你應該會看到生成的 松測.mdf 與 松測_log.ldf 檔案,且檔案建立時間為剛才還原的時間點。
2. SSMS 狀態檢查
在 ERP Server (192.168.1.156) 的 SSMS 中,重新整理【資料庫】清單,點開「松測」->【資料表】,隨機執行一次查詢:
SELECT TOP 100 * FROM [松測].[dbo].[客戶資料表];
確認資料能順利讀取且無亂碼狀況。
七、 專家調校指南:跨機分散後的效能優化建議
將 ERP 與 SQL Server 分散在兩台伺服器後,雖然解決了運算資源搶占問題,但同時也引入了一個新的變數——「網路延遲(Network Latency)」。
以前 ERP 與 SQL 在同一台伺服器時,資料交換走的是 Shared Memory(記憶體直接存取),速度極快;現在變成了跨機通訊,資料必須封包化經由網卡(NIC)傳輸。為了防止網路成為新的瓶頸
升級區域網路硬體(Gigabit / 10GbE)
確保 ERP Server (192.168.1.156) 與 SQL Server (192.168.1.159) 之間的網路交換器(Switch)支援 Gigabit (1000 Mbps) 以上規格,且網線採用 Cat.6 以上規格。如果企業條件允許,建議為這兩台伺服器配置 獨立的實體網卡,建立專用的備份/資料傳輸通道(Heartbeat / Backup Network)。
從真實 SQL 壓測數據看「多機分流」的必要性
引言: 想像一下,如果你的餐廳突然湧入 10 位顧客同時點了最繁複的手工菜,廚房裡卻只有一名主廚獨撐大局,會發生什麼事?出菜速度慢、廚師累癱、顧客抱怨連連!在 IT 系統的世界裡,單台伺服器面對高併發流量時,也面臨著一模一樣的困境。今天我們就用一份真實的 SQL 壓力測試數據,帶大家看看「單機硬撐」與「多機分流」的致命差距!
SQL Server 還原時機:比對 “松柏” 相對安裝程式的 SQL 版本(A)及客戶的松柏的系統版本(B)
若 A(如 115.10.02) 大於 B(如 114.09.28) 才要還原 SQL Server,否則只要貼上本機的主程式就可(松柏公司版本於大於客戶版本)
📊 真實壓測揭密:高併發連線的衝擊
SQL Server + ERP Server 雙機同時正式壓力測試工具(免安裝模組純 ADO.NET 版)
在 PowerShell 執行
# ============================================================
# SQL Server + ERP Server 雙機同時正式壓力測試工具(免安裝模組純 ADO.NET 版)
# ============================================================
# 功能:
# 1. 監控 ERP Server (本機 192.168.1.156) CPU / RAM
# 2. 遠端監控 SQL Server (192.168.1.159) OS CPU / RAM
# 3. 監控 SQL Server 內部 CPU (Signal Waits) / Memory / 連線數 (純 ADO.NET)
# 4. 對 SQL Server 建立高併發壓力連線 (純 ADO.NET)
# 5. 可選 ERP 端 CPU 模擬負載(模擬應用邏輯)
# 6. 完整 CSV + 統計報告
# ============================================================
# --------------------【一、測試參數】--------------------
$ErpServer = "192.168.1.156" # 本機(ERP Server / 腳本執行端)
$SqlServerInstance = "192.168.1.159" # 遠端 SQL Server
$Database = "松測" # 目標資料庫
$IntegratedSecurity = $true
$Concurrency = 20 # 併發連線數(建議先從 10~20 開始)
$DurationSec = 60 # 測試時間(秒)
$MonitorIntervalSec = 2 # 監控採樣間隔(秒)
$EnableErpCpuLoad = $true # 是否同時對 ERP Server 加壓(模擬應用邏輯)
$ResultFolder = "C:\SQL_PerfTest"
# -------------------- 建立結果資料夾 --------------------
if (-not (Test-Path $ResultFolder)) {
New-Item -Path $ResultFolder -ItemType Directory -Force | Out-Null
}
$TimeStamp = Get-Date -Format "yyyyMMdd_HHmmss"
$ErpHostLog = Join-Path $ResultFolder "ERP_Host_$TimeStamp.csv"
$SqlOsLog = Join-Path $ResultFolder "SQL_OS_$TimeStamp.csv"
$SqlInternalLog = Join-Path $ResultFolder "SQL_Internal_$TimeStamp.csv"
$StatPath = Join-Path $ResultFolder "Summary_$TimeStamp.txt"
Write-Host ""
Write-Host "====================================================" -ForegroundColor Cyan
Write-Host " SQL + ERP 雙機同時壓力測試工具(免模組強化版)" -ForegroundColor Cyan
Write-Host "====================================================" -ForegroundColor Cyan
Write-Host "ERP Server : $ErpServer (本機監控 + 可選 CPU 負載)" -ForegroundColor White
Write-Host "SQL Server : $SqlServerInstance (遠端監控 + 併發壓測)" -ForegroundColor White
Write-Host "Database : $Database" -ForegroundColor White
Write-Host "Concurrency : $Concurrency" -ForegroundColor White
Write-Host "Duration : $DurationSec 秒" -ForegroundColor White
Write-Host "ERP CPU Load : $EnableErpCpuLoad" -ForegroundColor White
Write-Host "結果資料夾 : $ResultFolder" -ForegroundColor White
Write-Host ""
# --------------------【二、ERP Host 監控 Job (192.168.1.156)】--------------------
Get-Job -Name "ErpHostMonitor" -ErrorAction SilentlyContinue | Remove-Job -Force
Write-Host ">>> [1] 啟動 ERP Server ($ErpServer) CPU / RAM 監控..." -ForegroundColor Yellow
$ErpMonitorJob = Start-Job -Name "ErpHostMonitor" -ScriptBlock {
param($LogFile, $Interval)
"Timestamp,CPU_Usage_Pct,Available_RAM_MB" | Out-File -FilePath $LogFile -Encoding UTF8
while ($true) {
$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
try {
$Cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
$RamMB = [math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1024, 2)
"$Time,$([math]::Round($Cpu,2)),$RamMB" | Out-File -FilePath $LogFile -Append -Encoding UTF8
} catch {
"$Time,0,0" | Out-File -FilePath $LogFile -Append -Encoding UTF8
}
Start-Sleep -Seconds $Interval
}
} -ArgumentList $ErpHostLog, $MonitorIntervalSec
# --------------------【三、SQL Server OS 遠端監控 Job (192.168.1.159)】--------------------
Get-Job -Name "SqlOsMonitor" -ErrorAction SilentlyContinue | Remove-Job -Force
Write-Host ">>> [2] 啟動 SQL Server OS ($SqlServerInstance) 遠端監控..." -ForegroundColor Yellow
$SqlOsJob = Start-Job -Name "SqlOsMonitor" -ScriptBlock {
param($SqlHost, $LogFile, $Interval)
"Timestamp,SQL_OS_CPU_Pct,SQL_OS_Available_RAM_MB" | Out-File -FilePath $LogFile -Encoding UTF8
while ($true) {
$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
try {
$Cpu = (Get-CimInstance -ComputerName $SqlHost Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
$RamMB = [math]::Round((Get-CimInstance -ComputerName $SqlHost Win32_OperatingSystem).FreePhysicalMemory / 1024, 2)
"$Time,$([math]::Round($Cpu,2)),$RamMB" | Out-File -FilePath $LogFile -Append -Encoding UTF8
} catch {
"$Time,0,0" | Out-File -FilePath $LogFile -Append -Encoding UTF8
}
Start-Sleep -Seconds $Interval
}
} -ArgumentList $SqlServerInstance, $SqlOsLog, $MonitorIntervalSec
# --------------------【四、SQL 內部指標監控 Job (純 ADO.NET)】--------------------
Get-Job -Name "SqlInternalMonitor" -ErrorAction SilentlyContinue | Remove-Job -Force
Write-Host ">>> [3] 啟動 SQL Server 內部資源監控 ($SqlServerInstance)..." -ForegroundColor Yellow
$SqlInternalJob = Start-Job -Name "SqlInternalMonitor" -ScriptBlock {
param($Server, $DB, $LogFile, $Interval)
"Timestamp,SQL_CPU_SignalWaits_Pct,SQL_Used_Memory_MB,SQL_User_Connections" | Out-File -FilePath $LogFile -Encoding UTF8
$ConnStr = "Server=$Server;Database=$DB;Integrated Security=True;TrustServerCertificate=True;Connect Timeout=15;"
while ($true) {
$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
try {
$conn = New-Object System.Data.SqlClient.SqlConnection($ConnStr)
$conn.Open()
# 1. Signal Waits (CPU 壓力指標)
$cmd1 = $conn.CreateCommand()
$cmd1.CommandText = "SELECT ISNULL(CAST(100.0 * SUM(signal_wait_time_ms) / NULLIF(SUM(wait_time_ms),0) AS DECIMAL(5,2)), 0) FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0;"
$SignalPct = $cmd1.ExecuteScalar()
# 2. Memory 使用量
$cmd2 = $conn.CreateCommand()
$cmd2.CommandText = "SELECT physical_memory_in_use_kb FROM sys.dm_os_process_memory"
$MemKB = $cmd2.ExecuteScalar()
$MemMB = if ($MemKB) { [math]::Round($MemKB / 1024, 2) } else { 0 }
# 3. 使用者連線數
$cmd3 = $conn.CreateCommand()
$cmd3.CommandText = "SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1"
$Conns = $cmd3.ExecuteScalar()
$conn.Close()
"$Time,$SignalPct,$MemMB,$Conns" | Out-File -FilePath $LogFile -Append -Encoding UTF8
} catch {
"$Time,0,0,0" | Out-File -FilePath $LogFile -Append -Encoding UTF8
}
Start-Sleep -Seconds $Interval
}
} -ArgumentList $SqlServerInstance, $Database, $SqlInternalLog, $MonitorIntervalSec
Start-Sleep -Seconds 3
Write-Host ">>> 所有監控已啟動" -ForegroundColor Green
# --------------------【五、壓力查詢語句】--------------------
$SqlQuery = @"
DECLARE @Start DATETIME = GETDATE();
WHILE DATEDIFF(SECOND, @Start, GETDATE()) < $DurationSec
BEGIN
-- 混合讀寫與 CPU 密集運算壓力
SELECT TOP 1000 object_id, name, type_desc
FROM sys.all_objects
ORDER BY NEWID();
SELECT TOP 500 *
FROM sys.dm_exec_query_stats
ORDER BY total_worker_time DESC;
WAITFOR DELAY '00:00:00.010';
END
"@
# --------------------【六、啟動 SQL 併發壓力】--------------------
Write-Host ""
Write-Host ">>> [4] 啟動對 SQL Server ($SqlServerInstance) 的 $Concurrency 併發壓力測試..." -ForegroundColor Yellow
$SqlJobs = @()
for ($i = 1; $i -le $Concurrency; $i++) {
$Job = Start-Job -Name "SqlStress_$i" -ScriptBlock {
param($Server, $DB, $Query, $Duration, $JobNumber)
$ConnString = "Server=$Server;Database=$DB;Integrated Security=True;TrustServerCertificate=True;Connect Timeout=15;"
$Connection = New-Object System.Data.SqlClient.SqlConnection($ConnString)
$Command = New-Object System.Data.SqlClient.SqlCommand($Query, $Connection)
$Command.CommandTimeout = $Duration + 60
try {
$Connection.Open()
$Start = Get-Date
$null = $Command.ExecuteNonQuery()
$Elapsed = [math]::Round(((Get-Date) - $Start).TotalSeconds, 2)
[PSCustomObject]@{ Job=$JobNumber; Status="Success"; Duration=$Elapsed; Error="" }
} catch {
[PSCustomObject]@{ Job=$JobNumber; Status="Failed"; Duration=0; Error=$_.Exception.Message }
} finally {
if ($Connection.State -ne "Closed") { $Connection.Close() }
$Connection.Dispose()
$Command.Dispose()
}
} -ArgumentList $SqlServerInstance, $Database, $SqlQuery, $DurationSec, $i
$SqlJobs += $Job
}
# --------------------【七、可選 ERP CPU 模擬負載 (192.168.1.156)】--------------------
$ErpLoadJob = $null
if ($EnableErpCpuLoad) {
Write-Host ">>> [5] 同時啟動 ERP Server ($ErpServer) CPU 模擬負載..." -ForegroundColor Yellow
$ErpLoadJob = Start-Job -Name "ErpCpuLoad" -ScriptBlock {
param($Duration)
$End = (Get-Date).AddSeconds($Duration)
while ((Get-Date) -lt $End) {
$x = 0
1..50000 | ForEach-Object { $x += $_ } # 簡單 CPU 密集運算
}
} -ArgumentList $DurationSec
}
# --------------------【八、等待測試結束並收集結果】--------------------
Write-Host ""
Write-Host ">>> 測試進行中,請稍候 $DurationSec 秒..." -ForegroundColor Cyan
Wait-Job -Job $SqlJobs -Timeout ($DurationSec + 90) | Out-Null
$Results = $SqlJobs | Receive-Job
$SuccessCount = ($Results | Where-Object Status -eq "Success").Count
$FailCount = ($Results | Where-Object Status -eq "Failed").Count
$AvgDuration = if ($SuccessCount -gt 0) { [math]::Round(($Results | Where-Object Status -eq "Success" | Measure-Object Duration -Average).Average, 2) } else { 0 }
$MaxDuration = if ($SuccessCount -gt 0) { ($Results | Where-Object Status -eq "Success" | Measure-Object Duration -Maximum).Maximum } else { 0 }
# --------------------【九、清理背景工作】--------------------
Write-Host ">>> 清理背景工作..." -ForegroundColor Yellow
Get-Job | Stop-Job -ErrorAction SilentlyContinue
Get-Job | Remove-Job -Force -ErrorAction SilentlyContinue
# --------------------【十、產生統計報告】--------------------
$Summary = @"
====================================================
SQL + ERP 雙機壓力測試報告
測試時間:$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
====================================================
ERP Server : $ErpServer (192.168.1.156)
SQL Server : $SqlServerInstance (192.168.1.159)
Database : $Database
Concurrency : $Concurrency
Duration : $DurationSec 秒
ERP CPU Load : $EnableErpCpuLoad
【SQL 壓力結果】
成功連線數 : $SuccessCount
失敗連線數 : $FailCount
平均執行時間 : $AvgDuration 秒
最長執行時間 : $MaxDuration 秒
【檔案位置】
ERP Host 監控 : $ErpHostLog
SQL OS 監控 : $SqlOsLog
SQL 內部監控 : $SqlInternalLog
本報告 : $StatPath
建議後續動作:
1. 用 Excel 開啟三個 CSV,觀察 CPU / Memory / 連線數曲線
2. 比較分離前後的峰值差異
3. 若 SignalWaitPct 長期 > 20% 或連線數暴增,需檢查索引與查詢
====================================================
"@
$Summary | Out-File -FilePath $StatPath -Encoding UTF8
Write-Host $Summary -ForegroundColor Green
Write-Host ">>> 測試完成!請查看 $ResultFolder" -ForegroundColor Cyan
本報告針對 2026-09-30 15:38:28 執行的 SQL Server + ERP Server 雙機壓力測試數據(包含 Summary_20260930_153828.txt、ERP_Host_20260930_153828.csv、SQL_Internal_20260930_153828.csv 與 SQL_OS_20260930_153828.csv)進行完整交叉分析。
雙機壓力測試分析報告
1. 測試概要與總體結果
- 測試時間:2026-09-30 15:38:29 ~ 15:39:37
- ERP 伺服器:
192.168.1.156(測試執行端 + CPU 負載模擬) - SQL 伺服器:
192.168.1.159(目標資料庫:松測) - 壓力設定:併發數 $Concurrency = 20$,持續時間 $Duration = 60$ 秒
核心指標彙整表
| 項目 / 監控端 | 成功 / 失敗數 | 平均 / 最長執行時間 | CPU 使用率 (%) | 記憶體狀況 | 備註說明 |
| SQL 併發任務 | 20 成功 / 0 失敗 | 59.65s / 60.16s | – | – | 併發任務 100% 順利完成 |
| ERP 主機 (156) | – | – | 平均 62.36% (最高 96%) | 可用 RAM 平均 1,419.38 MB | 成功模擬應用程式高算力負載 |
| SQL 內部 (159) | – | – | Signal Wait 平均 4.13% | 使用 RAM 平均 326.74 MB | 連線數由 10 穩定升至 30 峰值 |
| SQL OS (159) | – | – | 數據為 0% (權限問題) | 數據為 0 MB (權限問題) | WMI 遠端監控未開通或被阻擋 |
2.詳細分析與觀察
- SQL 壓力測試表現(優良):
- 穩定性:20 個併發連線全數成功完成,無任何超時或連線中斷(失敗數 0)。
- 連線變化:監控顯示使用者連線數(
SQL_User_Connections)在測試啟動後 10 秒內快速由 10 提升至 30(含背景連線),並保持滿載狀態直到測試結束。 - CPU 壓力指標:Signal Wait 保持在 4.13%(遠小於警戒值 20%),代表 SQL Server CPU 排隊延遲極低,算力十分充裕。
- 記憶體佔用:SQL 進程記憶體穩定維持在 326 MB ~ 327 MB 之間,變化極小。
- ERP 主機負載表現(符合預期):
- CPU 模擬負載:啟用
$EnableErpCpuLoad = $true後,ERP 主機 CPU 使用率由初始 27% 衝高至 96% 峰值,測試全程平均維持在 62.36%,成功達到雙機同時加壓的模擬效果。 - 記憶體配置:可用記憶體由 2,019 MB 降至 1,320 MB 附近,穩定供給壓測程序使用。
- CPU 模擬負載:啟用
為何「分流負載小於單機」?(關鍵技術原因)
- 消除 CPU 搶佔(Context Switching 降低): 在單機架構下,若 ERP 主機 CPU 飆高至 96%,SQL Server 的 Worker Threads 會被迫等待 CPU 排程,導致
SignalWaitPct(CPU 等待時間比例)暴增至 20%~50% 以上;分流後,SQL 端的 Signal Wait 僅有 4.13%,代表 SQL Server 能以近乎 100% 的效率專心處理 SQL 查詢。 - 記憶體快取不失效: 單機架構下 ERP 處理大批次資料會搶奪系統 RAM,導致 OS 強制壓縮 SQL Server 的記憶體;分流後,SQL 端記憶體極度平穩(維持在 326~327 MB),確保查詢快取與索引鎖定機制運作順暢。
⚠️ 單機負載(Single Node)的致命風險
將所有業務壓力集中在單一台伺服器上,就像把所有的雞蛋放在同一個籃子裡,存在三大隱患:
- 運算瓶頸極低: 只要遇到促銷活動、月底結帳或突發流量,CPU 瞬間飽和,使用者只能眼睜睜看著網頁轉圈圈。
- 無彈性擴充空間: 單機升級(Vertical Scaling)有硬體極限且成本昂貴,無法隨時因應瞬間暴增的業務需求。
- 單點故障(Single Point of Failure, SPOF): 一旦這台機器因為硬體故障、藍當或記憶體耗盡而宕機,整個 ERP 系統將立刻陷入停擺,造成巨大的營運損失。
💡 多機分流的 3 大核心好處:
- 大幅降低單機 CPU 與記憶體壓力:分散請求後,每台伺服器都能維持在 30%~40% 的黃金健康區間,反應速度(Latency)更敏捷,使用者體驗大幅提升。
- 輕鬆應對突發流量(Horizontal Scaling):當未來業務量暴增時,我們只需要持續增加「伺服器節點」,就能像積木一樣無縫擴充系統容量,再也不怕流量尖峰。
- 實現 99.99% 高可用性(High Availability):負載平衡器具備健康檢查機制,若其中一台伺服器發生故障,流量會自動轉移至其他正常的機器,實現業務零中斷!
🎯 總結與架構優化建議
給企業 IT 與系統架構師的建議:
- 短期優化: 針對高 CPU 消耗的查詢(如隨機排序、無索引的大表掃描)進行 SQL 語法與索引優化,降低單次查詢的算力開銷。
- 長期規劃: 儘速導入 讀寫分離(Read-Only Replica) 或 Web/API 應用層負載平衡(Load Balancer) 架構,透過多機分流打造高韌性、高併發且不中斷的企業級系統環境!
(參考)伺服器負載平衡 (Load Balancer)」的示意圖

八、 結語:擺脫卡頓,迎向高效能企業 IT
透過本篇的詳細圖文與實戰演練,我們成功將原本擠在同一台機器上的 ERP 系統(192.168.1.156)與 SQL Server 資料庫(192.168.1.159)完美拆分。
我們不僅實現了 跨伺服器的 DSN ODBC 配置,更示範了如何透過 SSMS V18.11.1 遠端操控還原「松測」資料庫至指定的 SQL 目錄。
IT 系統架構的升級,往往不需要盲目追求高昂的新硬體,只要「架構放對位置,資源合理分配」,老舊的 ERP 系統也能煥發新生,告別卡頓、死機的噩夢!
你的公司 ERP 系統還在「單打獨鬥」嗎?快把這篇文章分享給你們的系統管理員,一起動手幫伺服器分家吧!

小編提醒:日常 SQL Server 備份可參考另一篇
享技水工坊 TechX Studio,資訊.音樂人同學會 – 如何設定 SQL SERVER 2008 R2 自動備份排程作業呢?

============================================================
SQL Server + ERP Server 雙機同時正式壓力測試工具(免安裝模組純 ADO.NET 版)
============================================================
功能:
1. 監控 ERP Server (本機 192.168.1.156) CPU / RAM
2. 遠端監控 SQL Server (192.168.1.159) OS CPU / RAM
3. 監控 SQL Server 內部 CPU (Signal Waits) / Memory / 連線數 (純 ADO.NET)
4. 對 SQL Server 建立高併發壓力連線 (純 ADO.NET)
5. 可選 ERP 端 CPU 模擬負載(模擬應用邏輯)
6. 完整 CSV + 統計報告
============================================================
——————–【一、測試參數】——————–
$ErpServer = “192.168.1.156” # 本機(ERP Server / 腳本執行端)
$SqlServerInstance = “192.168.1.159” # 遠端 SQL Server
$Database = “松測” # 目標資料庫
$IntegratedSecurity = $true
$Concurrency = 20 # 併發連線數(建議先從 10~20 開始)
$DurationSec = 60 # 測試時間(秒)
$MonitorIntervalSec = 2 # 監控採樣間隔(秒)
$EnableErpCpuLoad = $true # 是否同時對 ERP Server 加壓(模擬應用邏輯)
$ResultFolder = “C:\SQL_PerfTest”
——————– 建立結果資料夾 ——————–
if (-not (Test-Path $ResultFolder)) {
New-Item -Path $ResultFolder -ItemType Directory -Force | Out-Null
}
$TimeStamp = Get-Date -Format “yyyyMMdd_HHmmss”
$ErpHostLog = Join-Path $ResultFolder “ERP_Host_$TimeStamp.csv”
$SqlOsLog = Join-Path $ResultFolder “SQL_OS_$TimeStamp.csv”
$SqlInternalLog = Join-Path $ResultFolder “SQL_Internal_$TimeStamp.csv”
$StatPath = Join-Path $ResultFolder “Summary_$TimeStamp.txt”
Write-Host “”
Write-Host “====================================================” -ForegroundColor Cyan
Write-Host ” SQL + ERP 雙機同時壓力測試工具(免模組強化版)” -ForegroundColor Cyan
Write-Host “====================================================” -ForegroundColor Cyan
Write-Host “ERP Server : $ErpServer (本機監控 + 可選 CPU 負載)” -ForegroundColor White
Write-Host “SQL Server : $SqlServerInstance (遠端監控 + 併發壓測)” -ForegroundColor White
Write-Host “Database : $Database” -ForegroundColor White
Write-Host “Concurrency : $Concurrency” -ForegroundColor White
Write-Host “Duration : $DurationSec 秒” -ForegroundColor White
Write-Host “ERP CPU Load : $EnableErpCpuLoad” -ForegroundColor White
Write-Host “結果資料夾 : $ResultFolder” -ForegroundColor White
Write-Host “”
——————–【二、ERP Host 監控 Job (192.168.1.156)】——————–
Get-Job -Name “ErpHostMonitor” -ErrorAction SilentlyContinue | Remove-Job -Force
Write-Host “>>> [1] 啟動 ERP Server ($ErpServer) CPU / RAM 監控…” -ForegroundColor Yellow
$ErpMonitorJob = Start-Job -Name “ErpHostMonitor” -ScriptBlock {
param($LogFile, $Interval)
“Timestamp,CPU_Usage_Pct,Available_RAM_MB” | Out-File -FilePath $LogFile -Encoding UTF8
while ($true) {
$Time = Get-Date -Format “yyyy-MM-dd HH:mm:ss”
try {
$Cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
$RamMB = [math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1024, 2)
“$Time,$([math]::Round($Cpu,2)),$RamMB” | Out-File -FilePath $LogFile -Append -Encoding UTF8
} catch {
“$Time,0,0” | Out-File -FilePath $LogFile -Append -Encoding UTF8
}
Start-Sleep -Seconds $Interval
}
} -ArgumentList $ErpHostLog, $MonitorIntervalSec
——————–【三、SQL Server OS 遠端監控 Job (192.168.1.159)】——————–
Get-Job -Name “SqlOsMonitor” -ErrorAction SilentlyContinue | Remove-Job -Force
Write-Host “>>> [2] 啟動 SQL Server OS ($SqlServerInstance) 遠端監控…” -ForegroundColor Yellow
$SqlOsJob = Start-Job -Name “SqlOsMonitor” -ScriptBlock {
param($SqlHost, $LogFile, $Interval)
“Timestamp,SQL_OS_CPU_Pct,SQL_OS_Available_RAM_MB” | Out-File -FilePath $LogFile -Encoding UTF8
while ($true) {
$Time = Get-Date -Format “yyyy-MM-dd HH:mm:ss”
try {
$Cpu = (Get-CimInstance -ComputerName $SqlHost Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
$RamMB = [math]::Round((Get-CimInstance -ComputerName $SqlHost Win32_OperatingSystem).FreePhysicalMemory / 1024, 2)
“$Time,$([math]::Round($Cpu,2)),$RamMB” | Out-File -FilePath $LogFile -Append -Encoding UTF8
} catch {
“$Time,0,0” | Out-File -FilePath $LogFile -Append -Encoding UTF8
}
Start-Sleep -Seconds $Interval
}
} -ArgumentList $SqlServerInstance, $SqlOsLog, $MonitorIntervalSec
——————–【四、SQL 內部指標監控 Job (純 ADO.NET)】——————–
Get-Job -Name “SqlInternalMonitor” -ErrorAction SilentlyContinue | Remove-Job -Force
Write-Host “>>> [3] 啟動 SQL Server 內部資源監控 ($SqlServerInstance)…” -ForegroundColor Yellow
$SqlInternalJob = Start-Job -Name “SqlInternalMonitor” -ScriptBlock {
param($Server, $DB, $LogFile, $Interval)
“Timestamp,SQL_CPU_SignalWaits_Pct,SQL_Used_Memory_MB,SQL_User_Connections” | Out-File -FilePath $LogFile -Encoding UTF8
$ConnStr = "Server=$Server;Database=$DB;Integrated Security=True;TrustServerCertificate=True;Connect Timeout=15;"while ($true) {
$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
try {
$conn = New-Object System.Data.SqlClient.SqlConnection($ConnStr)
$conn.Open()
# 1. Signal Waits (CPU 壓力指標)
$cmd1 = $conn.CreateCommand()
$cmd1.CommandText = "SELECT ISNULL(CAST(100.0 * SUM(signal_wait_time_ms) / NULLIF(SUM(wait_time_ms),0) AS DECIMAL(5,2)), 0) FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0;"
$SignalPct = $cmd1.ExecuteScalar()
# 2. Memory 使用量
$cmd2 = $conn.CreateCommand()
$cmd2.CommandText = "SELECT physical_memory_in_use_kb FROM sys.dm_os_process_memory"
$MemKB = $cmd2.ExecuteScalar()
$MemMB = if ($MemKB) { [math]::Round($MemKB / 1024, 2) } else { 0 }
# 3. 使用者連線數
$cmd3 = $conn.CreateCommand()
$cmd3.CommandText = "SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1"
$Conns = $cmd3.ExecuteScalar()
$conn.Close()
"$Time,$SignalPct,$MemMB,$Conns" | Out-File -FilePath $LogFile -Append -Encoding UTF8
} catch {
"$Time,0,0,0" | Out-File -FilePath $LogFile -Append -Encoding UTF8
}
Start-Sleep -Seconds $Interval
}
} -ArgumentList $SqlServerInstance, $Database, $SqlInternalLog, $MonitorIntervalSec
Start-Sleep -Seconds 3
Write-Host “>>> 所有監控已啟動” -ForegroundColor Green
——————–【五、壓力查詢語句】——————–
$SqlQuery = @”
DECLARE @Start DATETIME = GETDATE();
WHILE DATEDIFF(SECOND, @Start, GETDATE()) < $DurationSec
BEGIN
— 混合讀寫與 CPU 密集運算壓力
SELECT TOP 1000 object_id, name, type_desc
FROM sys.all_objects
ORDER BY NEWID();
SELECT TOP 500 *FROM sys.dm_exec_query_stats
ORDER BY total_worker_time DESC;
WAITFOR DELAY '00:00:00.010';
END
“@
——————–【六、啟動 SQL 併發壓力】——————–
Write-Host “”
Write-Host “>>> [4] 啟動對 SQL Server ($SqlServerInstance) 的 $Concurrency 併發壓力測試…” -ForegroundColor Yellow
$SqlJobs = @()
for ($i = 1; $i -le $Concurrency; $i++) {
$Job = Start-Job -Name “SqlStress_$i” -ScriptBlock {
param($Server, $DB, $Query, $Duration, $JobNumber)
$ConnString = “Server=$Server;Database=$DB;Integrated Security=True;TrustServerCertificate=True;Connect Timeout=15;”
$Connection = New-Object System.Data.SqlClient.SqlConnection($ConnString)
$Command = New-Object System.Data.SqlClient.SqlCommand($Query, $Connection)
$Command.CommandTimeout = $Duration + 60
try {
$Connection.Open()
$Start = Get-Date
$null = $Command.ExecuteNonQuery()
$Elapsed = [math]::Round(((Get-Date) – $Start).TotalSeconds, 2)
[PSCustomObject]@{ Job=$JobNumber; Status=”Success”; Duration=$Elapsed; Error=”” }
} catch {
[PSCustomObject]@{ Job=$JobNumber; Status=”Failed”; Duration=0; Error=$_.Exception.Message }
} finally {
if ($Connection.State -ne “Closed”) { $Connection.Close() }
$Connection.Dispose()
$Command.Dispose()
}
} -ArgumentList $SqlServerInstance, $Database, $SqlQuery, $DurationSec, $i
$SqlJobs += $Job
}
——————–【七、可選 ERP CPU 模擬負載 (192.168.1.156)】——————–
$ErpLoadJob = $null
if ($EnableErpCpuLoad) {
Write-Host “>>> [5] 同時啟動 ERP Server ($ErpServer) CPU 模擬負載…” -ForegroundColor Yellow
$ErpLoadJob = Start-Job -Name “ErpCpuLoad” -ScriptBlock {
param($Duration)
$End = (Get-Date).AddSeconds($Duration)
while ((Get-Date) -lt $End) {
$x = 0
1..50000 | ForEach-Object { $x += $_ } # 簡單 CPU 密集運算
}
} -ArgumentList $DurationSec
}
——————–【八、等待測試結束並收集結果】——————–
Write-Host “”
Write-Host “>>> 測試進行中,請稍候 $DurationSec 秒…” -ForegroundColor Cyan
Wait-Job -Job $SqlJobs -Timeout ($DurationSec + 90) | Out-Null
$Results = $SqlJobs | Receive-Job
$SuccessCount = ($Results | Where-Object Status -eq “Success”).Count
$FailCount = ($Results | Where-Object Status -eq “Failed”).Count
$AvgDuration = if ($SuccessCount -gt 0) { [math]::Round(($Results | Where-Object Status -eq “Success” | Measure-Object Duration -Average).Average, 2) } else { 0 }
$MaxDuration = if ($SuccessCount -gt 0) { ($Results | Where-Object Status -eq “Success” | Measure-Object Duration -Maximum).Maximum } else { 0 }
——————–【九、清理背景工作】——————–
Write-Host “>>> 清理背景工作…” -ForegroundColor Yellow
Get-Job | Stop-Job -ErrorAction SilentlyContinue
Get-Job | Remove-Job -Force -ErrorAction SilentlyContinue
——————–【十、產生統計報告】——————–
$Summary = @”
SQL + ERP 雙機壓力測試報告
測試時間:$(Get-Date -Format “yyyy-MM-dd HH:mm:ss”)
ERP Server : $ErpServer (192.168.1.156)
SQL Server : $SqlServerInstance (192.168.1.159)
Database : $Database
Concurrency : $Concurrency
Duration : $DurationSec 秒
ERP CPU Load : $EnableErpCpuLoad
【SQL 壓力結果】
成功連線數 : $SuccessCount
失敗連線數 : $FailCount
平均執行時間 : $AvgDuration 秒
最長執行時間 : $MaxDuration 秒
【檔案位置】
ERP Host 監控 : $ErpHostLog
SQL OS 監控 : $SqlOsLog
SQL 內部監控 : $SqlInternalLog
本報告 : $StatPath
建議後續動作:
1. 用 Excel 開啟三個 CSV,觀察 CPU / Memory / 連線數曲線
2. 比較分離前後的峰值差異
3. 若 SignalWaitPct 長期 > 20% 或連線數暴增,需檢查索引與查詢
“@
$Summary | Out-File -FilePath $StatPath -Encoding UTF8
Write-Host $Summary -ForegroundColor Green
Write-Host “>>> 測試完成!請查看 $ResultFolder” -ForegroundColor Cyan
1452392664 2026-10-08