Viết script tự động restart service windows cho IIS & SQL Server chuẩn Production
2 giờ sáng. Điện thoại réo liên hồi từ hệ thống cảnh báo Zabbix. Hệ thống API Gateway báo lỗi 500 Internal Server Error hàng loạt. Bạn lật đật mở laptop, remote vào server và nhận ra tiến trình w3wp.exe của IIS đang bị treo cứng, trong khi Database SQL Server thì cạn kiệt RAM gây deadlock. Phải mất đến 15 phút loay hoay xử lý thủ công, gỡ node lỗi ra khỏi Load Balancer, khởi động lại dịch vụ và chờ hệ thống làm nóng (warm-up) trở lại. Đối với các dự án eCommerce trong những ngày hội mua sắm lớn hay các hệ thống API chịu tải cao, 15 phút ngừng hoạt động đó quy ra một lượng doanh thu khổng lồ bị bốc hơi.
Nếu bạn đã từng trải qua sự cố này, bạn sẽ hiểu rằng việc ngồi canh server bằng sức người là một phương án vận hành rủi ro. Việc sở hữu một script tự động restart service windows chạy ngầm, hoạt động như một tiến trình giám sát độc lập và chính xác tuyệt đối là điều bắt buộc. Vậy làm thế nào để xây dựng một Watchdog script bằng PowerShell không chỉ tự động phục hồi dịch vụ mà còn miễn nhiễm với tình trạng rò rỉ bộ nhớ (Memory Leak) khi chạy 24/7?
Ám ảnh thời gian ngừng hoạt động và sự bất lực của Windows Service Recovery mặc định
Trong môi trường thực tế, hệ thống hiếm khi ngừng hoạt động một cách dứt khoát. Vấn đề nan giải thực sự của giới quản trị viên hệ thống nằm ở những trạng thái mất kết nối lâm sàng.
Khi một backend IIS bị lỗi rò rỉ bộ nhớ hoặc kẹt luồng (thread starvation), nó không ngừng hoạt động ngay lập tức. Hệ thống Load Balancer (như Nginx hay HAProxy) phía trước vẫn tiếp tục đẩy request vào node lỗi nếu không được cấu hình Active Health Check đủ khắt khe. Kết quả là người dùng cuối liên tục nhận lỗi timeout, lượng truy cập thất bại tăng cao. Tương tự với SQL Server, khi dịch vụ này bị buộc khởi động lại, hiện tượng khởi động lạnh (Cold Start) sẽ xảy ra. Vùng nhớ đệm (Buffer Pool) hoàn toàn trống rỗng, khiến các truy vấn thay vì đọc từ RAM mất vài mili-giây sẽ phải đọc trực tiếp từ đĩa cứng. Chỉ số I/O Disk spike chạm đỉnh IOPS, kéo theo độ trễ toàn hệ thống tăng vọt trong suốt 15-60 phút đầu tiên.
Nhiều kỹ sư vận hành thường phó mặc nhiệm vụ phục hồi cho tính năng Recovery mặc định trong services.msc của Windows. Tuy nhiên, tính năng này có một điểm yếu rất lớn: Nó chỉ kích hoạt hành động restart khi tiến trình (process) thực sự dừng hoạt động và trả về mã lỗi chấm dứt (exit code).
Nếu dịch vụ IIS hoặc SQL gặp lỗi deadlock nội bộ, kẹt ở trạng thái StopPending (đang dừng) hoặc StartPending (đang khởi động), hệ điều hành Windows thực chất vẫn ghi nhận tiến trình (.exe) đang chạy. Service Control Manager (SCM) của Windows chỉ thả một cảnh báo timeout (Event ID 7011) vào Event Viewer chứ không hề coi đó là một Failure để kích hoạt Recovery. Hệ thống cứ thế treo lơ lửng, và Uptime của máy chủ sụt giảm nghiêm trọng.

Tính năng Recovery mặc định của Windows hoàn toàn bỏ sót các tiến trình bị treo ngầm, khiến hệ thống Load Balancer liên tục trả về lỗi 500.
Ngoài lỗi tràn RAM của dịch vụ, nếu máy chủ của bạn thường xuyên bị khởi động lại không rõ nguyên nhân, bạn có thể tham khảo thêm hướng dẫn chẩn đoán và khắc phục từ A-Z tình trạng VPS bị treo hoặc tự reboot.
Kiến trúc của một Watchdog script thực chiến bằng PowerShell
Để khắc phục nhược điểm của hệ điều hành, chúng ta cần một script tự động restart service windows chủ động thăm dò trạng thái mỗi 60 giây. Script này không chờ tiến trình ngừng hoạt động hoàn toàn, mà tự nó sẽ kiểm tra xem dịch vụ có đang thực sự phản hồi ổn định hay không.

Kiến trúc vòng lặp chuẩn xác của Watchdog Script tích hợp cơ chế chống trôi thời gian (Time-drift) và Pre/Post Hook can thiệp kịp thời lỗi treo process.
Bắt đúng bệnh: Nhận diện mọi trạng thái treo (hung/paused) thay vì chỉ stopped
Thay vì dùng các lệnh bộ lệnh batch truyền thống, PowerShell với Cmdlet Get-Service là công cụ tối ưu. Object ServiceController trả về từ lệnh này chứa thuộc tính .Status phản ánh chính xác trạng thái thực của dịch vụ.
Logic cốt lõi không phải là tìm kiếm trạng thái Stopped, mà là phủ định trạng thái an toàn: if ($service.Status -ne 'Running'). Cách viết này giúp bắt trọn mọi biến cố bất thường: dù dịch vụ đang Paused, StopPending hay Stopped, script đều sẽ ghi nhận là lỗi và tiến hành quy trình can thiệp phục hồi.
Cơ chế pre/post hook: Xử lý dứt điểm w3wp.exe (IIS) và thời gian phục hồi cho Buffer Pool
Khi phát hiện sự cố, việc vội vàng gọi lệnh Restart-Service thường dẫn đến thất bại, đặc biệt với các dịch vụ phức tạp.
Đối với Web Server IIS, giới vận hành thường có thói quen dùng lệnh iisreset. Tuy nhiên, khuyến cáo mạnh mẽ không nên lạm dụng iisreset. Lệnh này có thể thất bại trong việc triệt tiêu hoàn toàn các worker process (w3wp.exe) nếu chúng đang kẹt, đồng thời có nguy cơ làm mất cấu hình đang ghi dở. Giải pháp an toàn nhất trong script là chạy một Pre-Restart Hook sử dụng lệnh NET STOP w3svc và sau đó là NET START w3svc. Lệnh này ép hệ thống phải dọn dẹp tuần tự các dịch vụ phụ thuộc, giải phóng hoàn toàn port 80/443 trước khi khởi động lại.
Đối với Database Engine, SQL Server (MSSQLSERVER) đòi hỏi sự cẩn trọng cao hơn. Khởi động lại SQL Server là một quá trình tiêu tốn nhiều tài nguyên. Sau khi tiến trình SQL bật lên, nó phải trải qua giai đoạn Recovery Phase để khôi phục nhật ký từ WAL. Việc liên tục khởi động lại dồn dập (restart storm) khi SQL vừa mới khởi động sẽ làm hỏng cấu trúc Database. Trên các phiên bản SQL Server 2019/2022, tính năng Accelerated Database Recovery (ADR) đã tối ưu thời gian phục hồi từ vài tiếng xuống còn dưới 1 phút. Kèm theo đó, công nghệ Buffer Pool Parallel Scan giúp các máy chủ nhiều RAM quét bộ nhớ khởi động cực nhanh. Tuy nhiên, script Watchdog vẫn phải được lập trình một khoảng Cooldown tối thiểu 15-30 giây giữa các lần khởi động lại để Database có đủ thời gian khởi tạo an toàn.
Chống trôi thời gian (Time-Drift Prevention) để vòng lặp chuẩn xác 60 giây
Một lỗi phổ biến khi viết script là sử dụng Start-Sleep -Seconds 60 một cách cứng nhắc ở cuối vòng lặp. Nếu các logic kiểm tra dịch vụ, ghi log, gửi cảnh báo tiêu tốn mất 4 giây, chu kỳ thực tế sẽ bị kéo giãn thành 64 giây. Khi chạy liên tục trong thời gian dài, chu kỳ giám sát của Watchdog sẽ bị sai lệch hoàn toàn.
Để script đạt độ chính xác cao, thuật toán bù trừ độ trễ (Time-Drift Prevention) phải được áp dụng:
$cycleStart = Get-Date
# ... Thực thi các tác vụ kiểm tra (mất X giây) ...
# Tính toán chính xác thời gian còn lại cần ngủ để tròn 60s
$elapsed = ((Get-Date) - $cycleStart).TotalSeconds
$sleepTime = [Math]::Max(0, 60 - $elapsed)
if ($sleepTime -gt 0) { Start-Sleep -Seconds $sleepTime }
Trái tim chống rò rỉ: Tối ưu RAM ảo với [System.GC]::Collect()
Phần cứng máy chủ có mạnh đến đâu cũng sẽ gặp vấn đề trước những vòng lặp vô hạn while($true) viết bằng PowerShell. Khi script chạy liên tục 24/7, mỗi chu kỳ đi qua sẽ sinh ra hàng loạt biến tạm, chuỗi string ghi log, hay các object WMI.
Vấn đề cốt lõi là trình quản lý bộ nhớ (Garbage Collector – GC) mặc định của .NET không tự động giải phóng toàn bộ rác trong một tiến trình không bao giờ tắt. Hậu quả là dung lượng RAM của tiến trình PowerShell tăng dần theo thời gian, gây ra tình trạng Memory Leak, cuối cùng là lỗi Out of Memory làm ngắt chính tiến trình giám sát.
Kỹ thuật tối ưu nhất cho một script dạng chuyên nghiệp là làm chủ được hệ thống dọn rác 3 nhịp và nén Large Object Heap (LOH):
- Nhịp 1 (
[System.GC]::Collect()): Script ép GC quét toàn bộ 3 thế hệ bộ nhớ (Gen0, Gen1, Gen2). Những object không còn sử dụng sẽ được đánh dấu và đưa vào hàng đợi đặc biệt. - Nhịp 2 (
[System.GC]::WaitForPendingFinalizers()): Đây là lệnh tạm dừng luồng chạy hiện tại. Script chờ hệ thống ngầm của .NET xử lý xong các phương thức hủy (finalizer) của những object đang kẹt lại, đảm bảo các tài nguyên unmanaged (như file log đang mở, socket mạng) được giải phóng hoàn toàn. - Nhịp 3 (
[System.GC]::Collect()): Quét lần cuối. Những dữ liệu từ hàng đợi finalizer vừa được giải phóng nay mới chính thức bị thu hồi vùng nhớ, trả lại không gian trống cho RAM máy chủ.
Bên cạnh đó, các object kích thước lớn (vượt quá 85KB, như chuỗi log quá dài) sẽ bị đẩy vào Large Object Heap (LOH). Theo thời gian, LOH bị phân mảnh, khiến độ trễ (latency) của GC vọt từ 0.2ms lên 20ms, làm tăng tải CPU. Để xử lý triệt để, script cần ép .NET nén gọn mảng LOH lại:
[System.Runtime.GCSettings]::LargeObjectHeapCompactionMode = [System.Runtime.GCLargeObjectHeapCompactionMode]::CompactOnce
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
[System.GC]::Collect()
Đoạn code ngắn gọn này chính là cơ chế vững chắc giúp Watchdog script của bạn hoạt động liên tục nhiều tháng liền mà chỉ tiêu tốn khoảng 15MB RAM.

Kỹ thuật dọn rác 3 nhịp bằng [System.GC]::Collect() kết hợp nén LOH là cốt lõi giúp script chạy ngầm hàng tháng trời mà chỉ tốn vài chục MB RAM.
Mã nguồn hoàn chỉnh: Script Watchdog giám sát và tự động phục hồi dịch vụ
Dưới đây là mã nguồn ServiceWatchdog.ps1 hoàn chỉnh được tối ưu chuyên sâu, tích hợp thuật toán dọn rác RAM chống rò rỉ bộ nhớ và cơ chế bù trừ thời gian:
<#
.SYNOPSIS
Script tự động restart service windows - Tích hợp chống rò rỉ RAM và bù trôi thời gian
Hỗ trợ kiểm tra chuyên sâu trạng thái IIS (w3svc) và SQL Server (MSSQLSERVER)
#>
# ==========================================
# 1. CẤU HÌNH THÔNG SỐ (Thay đổi tại đây)
# ==========================================
$ServicesToWatch = @("w3svc", "MSSQLSERVER") # Danh sách dịch vụ cần giám sát
$LogPath = "C:\Scripts\Logs\Watchdog.log"
$CheckIntervalSec = 60 # Chu kỳ kiểm tra (60 giây)
$RestartCooldown = 20 # Thời gian nghỉ sau khi tác động (20 giây)
# Đảm bảo thư mục log tồn tại
$LogDir = Split-Path -Path $LogPath
if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir | Out-Null }
Function Write-WatchdogLog {
param([string]$Message, [string]$Type = "INFO")
$TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$LogMessage = "[$TimeStamp] [$Type] $Message"
Write-Output $LogMessage
Add-Content -Path $LogPath -Value $LogMessage
}
Write-WatchdogLog "Khởi động tiến trình Watchdog Service Monitor thành công." "START"
# ==========================================
# 2. VÒNG LẶP GIÁM SÁT VÔ HẠN (PRODUCTION)
# ==========================================
while ($true) {
$CycleStart = Get-Date
foreach ($ServiceName in $ServicesToWatch) {
$Service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if (-not $Service) {
Write-WatchdogLog "Dịch vụ '$ServiceName' không tồn tại trên hệ thống này." "WARNING"
continue
}
# Bắt toàn bộ các trạng thái không phải 'Running' (Stopped, Paused, Pending...)
if ($Service.Status -ne 'Running') {
Write-WatchdogLog "Phát hiện dịch vụ '$ServiceName' bất thường. Trạng thái hiện tại: $($Service.Status)" "ALERT"
# --- PRE-HOOK Xử lý đặc thù cho Web Server IIS ---
if ($ServiceName -eq "w3svc") {
Write-WatchdogLog "Tiến hành giải phóng worker process w3wp.exe..." "HOOK"
# Ngắt tuần tự để dọn dẹp triệt để lock port
net stop wuauserv /y 2>&1 | Out-Null
Stop-Process -Name "w3wp" -Force -ErrorAction SilentlyContinue
}
# --- THỰC THI KHỞI ĐỘNG LẠI DỊCH VỤ ---
Write-WatchdogLog "Đang tiến hành kích hoạt lại dịch vụ '$ServiceName'..." "ACTION"
# Sử dụng lệnh net start để tận dụng cơ chế khởi động tuần tự của hệ điều hành
net start $ServiceName 2>&1 | Out-Null
# Đợi hệ thống phản hồi và cập nhật trạng thái mới
Start-Sleep -Seconds 5
$Service.Refresh()
if ($Service.Status -eq 'Running') {
Write-WatchdogLog "Dịch vụ '$ServiceName' đã phục hồi trạng thái hoạt động thành công." "SUCCESS"
} else {
Write-WatchdogLog "Không thể tự khởi động lại dịch vụ '$ServiceName'. Cần sự can thiệp thủ công!", "CRITICAL"
}
# Khoảng nghỉ Cooldown để tránh tình trạng dồn dập tài nguyên (Restart Storm)
Start-Sleep -Seconds $RestartCooldown
}
}
# ==========================================
# 3. QUẢN LÝ RAM: ÉP CHỐNG RÒ RỈ BỘ NHỚ
# ==========================================
# Ép nén Large Object Heap (LOH) dọn dẹp chuỗi string ghi log quá dài
[System.Runtime.GCSettings]::LargeObjectHeapCompactionMode = [System.Runtime.GCLargeObjectHeapCompactionMode]::CompactOnce
# Quy trình dọn rác 3 nhịp tối ưu bộ nhớ ẩn của tiến trình PowerShell
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
[System.GC]::Collect()
# ==========================================
# 4. THUẬT TOÁN BÙ TRÔI THỜI GIAN
# ==========================================
$Elapsed = ((Get-Date) - $CycleStart).TotalSeconds
$RemainingSleep = [Math]::Max(0, $CheckIntervalSec - $Elapsed)
if ($RemainingSleep -gt 0) {
Start-Sleep -Seconds $RemainingSleep
}
}
Triển khai script chạy ngầm vĩnh viễn không sợ tắt nhầm
Viết script tối ưu mới chỉ là một nửa chặng đường. Triển khai cấu hình lên server thế nào để an toàn, không hiển thị cửa sổ Console và khởi động cùng hệ điều hành là một yếu tố quan trọng khác. Nhiều người chọn Task Scheduler, nhưng phương pháp này thiếu cơ chế giám sát chủ động (không tự động chạy lại nếu script bị ngắt).
Giải pháp đáng tin cậy là đóng gói kịch bản PowerShell này thành một Windows Service thực thụ thông qua công cụ mã nguồn mở NSSM (Non-Sucking Service Manager). NSSM cung cấp bộ tham số cấu hình mạnh mẽ giúp bảo vệ tiến trình khỏi việc bị gián đoạn.
Khi thiết lập qua NSSM, các tham số bắt buộc cần cấu hình bao gồm:
- Path:
powershell.exe - Arguments:
-ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File "C:\Scripts\ServiceWatchdog.ps1"(Tham sốNoProfilegiúp script khởi chạy cực nhanh vì không tải các cấu hình tài khoản người dùng không cần thiết). - AppRestartDelay: Tại tab Details của NSSM, hãy cấu hình tham số này khoảng 3000ms. Điều này có nghĩa là nếu bản thân script bị ngắt vì một lý do ngoại cảnh, NSSM sẽ đợi 3 giây rồi mới kích hoạt lại. Nó ngăn chặn vòng lặp khởi động liên tục làm quá tải CPU.
Với NSSM, Watchdog của bạn trở thành một dịch vụ hệ thống chuẩn của Windows, có quyền chạy dưới tài khoản SYSTEM, tránh được các thao tác tắt nhầm cửa sổ CMD trên màn hình remote.
Kết hợp Watchdog với Load Balancer / Proxy (tối ưu nâng cao)
Để đạt được Uptime tiệm cận 99.99%, chỉ khởi động lại dịch vụ ở backend là chưa đủ. Bạn phải đồng bộ nhịp hoạt động của kịch bản Watchdog với hệ thống phân luồng lượng truy cập phía trước (Proxy/Load Balancer như HAProxy hoặc Nginx).
Khi Watchdog phát hiện IIS/SQL bị lỗi và quyết định khởi động lại, trong khoảng thời gian đó, backend tạm thời mất kết nối. Nếu Nginx vẫn liên tục đẩy request vào, người dùng sẽ gặp lỗi không mong muốn.
- Cắt lượng truy cập tức thì (Circuit Breaking): Cấu hình Nginx với thông số
max_fails=3vàfail_timeout=30s. Khi Watchdog dừng dịch vụ w3svc, port 80/443 lập tức đóng. Nginx gửi request lỗi 3 lần sẽ tự động cách ly node này, điều hướng toàn bộ lượng truy cập sang các node dự phòng khác. Lượng request bị rớt sẽ được giảm thiểu tối đa. - Đưa lượng truy cập trở lại nhẹ nhàng (Gradual Ramp/Slowstart): Sau khi Watchdog restart thành công, Database và Application cần thời gian làm nóng (warm-up). Trong HAProxy, cấu hình
rise=3kết hợp cờslowstart=60sép Load Balancer phải kiểm tra thành công ít nhất 3 lần liên tiếp, sau đó mới tăng dần lượng request vào node vừa khôi phục. Nhờ vậy, Buffer Pool của SQL Server có đủ không gian để nạp dữ liệu từ đĩa lên RAM, tránh tình trạng quá tải ngay khi vừa hoạt động lại.

Đóng gói Watchdog bằng NSSM và thiết lập cơ chế Circuit Breaker trên Load Balancer giúp phân luồng lượng truy cập kịp thời, hỗ trợ hệ thống đạt Uptime tiệm cận 99.99%.
Câu hỏi thường gặp (FAQ)
1. Việc gọi [System.GC]::Collect() liên tục có làm chậm CPU của máy chủ không?
Không. Lệnh thu hồi vùng nhớ chỉ kích hoạt định kỳ (ví dụ: mỗi 10 phút) hoặc khẩn cấp khi RAM vượt ngưỡng quy định (ví dụ 100MB). Mức tiêu thụ tài nguyên CPU cho tác vụ này gần như không đáng kể.
2. Script Watchdog PowerShell chạy liên tục có tốn RAM không?
Rất tối ưu. Nhờ kỹ thuật quản lý bộ nhớ 3 nhịp và ép nén LOH (Large Object Heap), script chạy ngầm trong thời gian dài chỉ tiêu tốn mức RAM ổn định khoảng 15 – 20MB.
3. Làm sao để chạy script PowerShell ẩn hoàn toàn dưới nền?
Giải pháp tốt nhất là sử dụng NSSM để thiết lập kịch bản thành Windows Service. Nếu sử dụng Task Scheduler, cần thêm tham số -WindowStyle Hidden vào ô Arguments để không hiển thị cửa sổ dòng lệnh.
4. Tại sao tôi không nên dùng Task Scheduler để chạy kịch bản này?
Task Scheduler thiếu cơ chế tự phục hồi (Self-healing). Nếu tiến trình PowerShell dừng đột ngột do lỗi, nó sẽ ngừng hoạt động hoàn toàn cho đến lần kích hoạt theo lịch tiếp theo. Triển khai qua NSSM giúp script tự động khởi động lại chính nó nếu bị lỗi.
5. Script này có hoạt động với SQL Server Named Instance không?
Có. Bạn chỉ cần điều chỉnh tên dịch vụ thành MSSQL$TEN_INSTANCE (ví dụ: MSSQL$SQLEXPRESS). Lưu ý sử dụng dấu backtick (`) để escape ký tự $ trong code PowerShell.
6. Khởi động lại dịch vụ liên tục có gây hỏng Database không?
Có rủi ro nếu khởi động dồn dập trong khoảng thời gian ngắn (Restart storm). Đó là lý do kịch bản bắt buộc phải được cấu hình biến RestartCooldown (nghỉ 15-30 giây) để Buffer Pool của SQL Server có thời gian phục hồi và khởi tạo an toàn.
Kết luận
Tóm lại, một script tự động restart service windows được tối ưu chuyên sâu bằng PowerShell, đảm bảo không gặp memory leak bằng kỹ thuật GC và được triển khai đồng bộ cùng Load Balancer chính là lớp bảo vệ vững chắc cho hệ thống. Phương pháp này giảm thiểu can thiệp thủ công từ quản trị viên, đảm bảo sự ổn định của dự án trước những sự cố thời gian ngừng hoạt động ngoài giờ hành chính.
Hãy rà soát lại các kịch bản PowerShell hiện tại trên server của bạn, áp dụng ngay thuật toán quản lý LOH để nâng cấp hạ tầng. Và đừng quên tích hợp kịch bản này vào hệ sinh thái 10 script PowerShell quản trị VPS Windows Server 2025 để đưa khả năng tự động hóa hệ thống lên mức tối đa!
Tài liệu tham khảo
- Fundamentals of garbage collection – .NET | Microsoft Learn
- Large object heap (LOH) on Windows – .NET | Microsoft Learn
- Error when you use IISReset.exe to restart IIS – Internet Information Services | Microsoft Learn
- Operations that trigger buffer pool scan may run slowly on large-memory computers – SQL Server | Microsoft Learn
- NSSM – the Non-Sucking Service Manager






