Windows服务与IIS站点实时监测:C#实现自动恢复
简介面向Windows运维场景的实时监测程序源码项目专门用于解决业务网站或Windows服务运行一段时间后因异常停止、需要人工干预的常见问题。该项目能在监测到Windows服务停止或IIS网站/应用程序池运行中断后自动执行重启操作极大减少业务受影响时间适合运维人员及.NET开发者在故障根因排查前应急使用。压缩包共70个文件约373KB包含23个C#源码文件、4个可直接运行的exe程序、3个DLL帮助库、3个config配置文件以及资源文件、工程配置文件等基于.NET Framework 4和Visual Studio 2022开发。资源不仅仅提供源码工程还收录了IIS操作帮助类、服务重启逻辑、文件与日志操作类、Winform窗体示例既可用于生产环境快速部署也可作为C# Winform及系统服务编程的学习参考。目前已有114人学习使用适合需要临时救场或进行功能扩展的开发者直接下载。1. 实时监测源码项目解决的是:Windows服务停了没人发现、IIS站点假死、应用程序池被回收的日常Windows服务、IIS网站和应用程序池这三样东西平时安静得像不存在一旦出问题第一个知道的人永远是用户不是管理员。我见过太多次这种凌晨场景:一台Windows服务器上的某个关联服务被误停IIS站点直接502;更隐蔽的是应用程序池因为内存上限到达被IIS自动回收站点状态还显示“已启动”但新请求全部卡在队列里。所谓“Windows服务、IIS网站和应用程序池实时监测源码项目”就是做一个后台守护程序把这三类对象的状态、性能数据和恢复动作统一在一个轮询循环里:发现异常自动拉起留日志事后可查。它解决的是运维手里一堆Windows服务器、又不想上重量级监控 agent 的中间地带问题适合IIS部署集中、Windows服务器权限在自己手里的工程师。2. 服务、站点、应用池的数据采集原理:WMI、ServerManager与性能计数器怎么配合别急着写自动拉起先把“读状态”这一步做扎实。状态读不准后面所有动作都是错的。很多人第一步踩坑就是拿注册表或配置文件去猜运行状态结果做出一个“看起来在监控实际啥也没读到”的假系统。这里先说清楚三类对象各自的数据源再给出我常用的读取方式和边界。2.1 读Windows服务状态:ServiceController、WMI和sc query三条路在单机监测场景下我首选 .NET 自带的 ServiceController它在 System.ServiceProcess 命名空间里不需要额外权限代码短出错概率低。先看最核心的一段读取逻辑:// ServiceProbe.cs —— 读取单个Windows服务的实时状态 using System.ServiceProcess; public static class ServiceProbe { public static string ReadState(string serviceName) { using (var controller new ServiceController(serviceName)) { // 强制刷新一次缓存避免拿到上一次轮询的旧状态 controller.Refresh(); return controller.Status.ToString(); } } }逻辑说明:ServiceController 每次调用会去服务控制管理器(SCM)取当前状态Refresh 的作用是清掉内部缓存。返回值可能是 Running、Stopped、StartPending、StopPending、Paused 等。这里要注意 StartPending 和 StopPending 这种中间态它们不代表异常只代表服务正在启动或停止途中。如果轮询周期太短你会频繁读到 StartPending这时候立刻判定“服务挂了”去拉起反而会跟系统自己的启动过程打架。这三条采集路径我按场景划分:单机少量服务:ServiceController 最简单可同步拿到状态和启动类型。批量远程巡检:用 WMI 的 Win32_Service一条查询能拉回几十台机器的服务状态适合做后续扩展。命令行调试:sc query 服务名秒级确认问题出在状态读取还是判定逻辑上。WMI 的写法是Get-WmiObject Win32_Service -ComputerName 10.1.1.2能拿到 State 和 StartMode但 WMI 请求本身有性能开销远程批量拉取时不要低于 30 秒一次否则会把服务器 WMI 服务打到超时。单机场景效果差。2.2 读IIS站点和应用程序池:用Microsoft.Web.Administration而不是自己解析XMLIIS 这块有个血泪经验:不要自己去解析 applicationHost.config 判断站点和池的状态。那个 XML 文件里只能看到配置看不到运行状态。站点是启动还是停止是池正在回收还是已经崩溃配置里一点也不会有。正确做法是用官方管理 API Microsoft.Web.Administration核心入口是 ServerManager 对象。// IISTopologyProbe.cs —— 读取站点和应用程序池的实时状态 using Microsoft.Web.Administration; public static class IISTopologyProbe { public static Liststring GetAbnormalItems() { var problems new Liststring(); // ServerManager 实现了 IDisposable务必 using否则句柄和配置锁会累积 using (var serverManager new ServerManager()) { foreach (var site in serverManager.Sites) { if (site.State ! ObjectState.Started) { problems.Add(site: site.Name : site.State); } } foreach (var pool in serverManager.ApplicationPools) { if (pool.State ! ObjectState.Started) { problems.Add(pool: pool.Name : pool.State); } } } return problems; } }逻辑说明:ServerManager 会从%SystemRoot%\System32\inetsrv\config\applicationHost.config加载配置并关联 IIS 运行时状态。站点和池的 State 都是 ObjectState 枚举正常是 Started异常是 Stopped、Starting、Stopping。这里有两个边界要记住:只靠 State 判断不完整站点 State Started 只能说明“IIS 认为它该运行”不能说明“请求真的能通”。这就是后面要加活性探测的原因。在服务器上引用 Microsoft.Web.Administration 时优先从本机C:\Windows\System32\inetsrv\Microsoft.Web.Administration.dll获取而不是从 NuGet 拉一个版本差异很大的包。如果服务器 IIs 版本是 8.5( Windows Server 2012 R2)开发机却是更高版本本地编译没问题部署过去很容易出现程序集版本不匹配这个见第 5 章。另外IIS 有些子组件没装时ServerManager 读取配置会直接报错。比如服务器上的 Web 服务器角色没勾选“Windows 身份验证”你访问某些站点配置时可能抛异常。生产部署前先确认角色服务里把常用的几个认证组件勾上别等监控跑起来才发现读不了配置。2.3 实时性的现实:轮询间隔、性能计数器和回收事件“实时监测”在 Windows 这套体系里没有真正的事件推送。WMI 的 __InstanceStatusEvent 事件订阅本质是 WMI 服务内部轮询要配置事件消费者复杂度高出错点也多。对大多数业务场景轮询就是最可靠、最好排查的“实时”。我一般设两个独立间隔:状态轮询 10 秒一次性能采样 30 秒一次。10 秒这个间隔能保证服务或池挂掉后最多 10 秒内被感知30 秒的性能采样用来捕捉“假死”。所谓假死就是进程还在但 CPU 被打满或者请求队列堆积状态枚举还是 Started。应用池假死最常见的根源是内存或 CPU 阈值触顶IIS 回收后新进程还没起来或者旧进程没完全退出。单靠 ServerManager 看不到这些要补上性能计数器。常见做法是取 W3WP 进程的% Processor Time和Private Bytes两个计数器连续三次采样都超过阈值才判定异常避免瞬时峰值误杀。3. 把采集逻辑写成可维护的源码:统一模型、状态判定与自动拉起数据能读出来之后就要考虑源码结构了。标题里说的是“源码项目”不是一堆零散脚本所以要注意组织方式。我习惯把工程拆成四个模块:Probe(采集)、Triage(判定)、RecoveryHub(恢复动作)、LogSink(日志存储)。这样每个模块都可以单独测后面接告警、接数据库都不用改主体代码。3.1 先定义一个统一监测对象:服务、站点、应用池的通用字段三类对象虽然来源不同但要进同一个判定和恢复流程最好先统一成一个结构。否则你会写出三套 if 分支后续加监控对象时越改越烂。// MonitorItem.cs —— 三类监测对象的统一模型 public enum MonitorCategory { WindowsService, IISAppPool, IISWebSite } public class MonitorItem { public MonitorCategory Category { get; set; } public string Name { get; set; } public string CurrentState { get; set; } public bool IsAutoStart { get; set; } public DateTime CheckedAt { get; set; } public int ConsecutiveFailures { get; set; } public DateTime? LastRecoveredAt { get; set; } }逻辑说明:Category 区分对象类型Name 存服务名或站点名CurrentState 存这次轮询读到的状态。IsAutoStart 对 Windows 服务特别重要因为只有自启服务才适合自动拉起。ConsecutiveFailures 和 LastRecoveredAt 是为恢复动作准备的用来防止“反复拉起、反复失败”的抖动。3.2 状态判定:不是所有“停止”都该自动拉起这一步最容易判断错。我看到很多项目拿到 Stopped 就立刻重启完全不看服务启动类型。后果就是:一些手动启动的备份脚本服务被监控“好心”拉起导致备份任务重复执行数据库被写坏的事我都见过。正确的判定逻辑要区分三层:Windows 服务:只有 StartType Automatic 才进入自动恢复流程Manual 服务只记录不动作。应用程序池:池可以在 IIS 管理器里设为 AlwaysRunning 或 OnDemand。AlwaysRunning 的池停止后应当恢复OnDemand 的池空闲被回收是正常行为不一定要拉起。IIS 站点:先保证关联的应用池是 Started再检查站点本身。核心判定方法我写成这样:// Triage.cs —— 判定是否值得自动恢复 using System.ServiceProcess; public static class Triage { public static bool ShouldAutoRecover(MonitorItem item) { if (item.Category MonitorCategory.WindowsService) { bool isAutoStart CheckServiceIsAutoStart(item.Name); if (!isAutoStart) return false; // 手动服务不碰 if (item.CurrentState Running) return false; if (item.CurrentState.EndsWith(Pending)) return false; // 正在启动/停止过程中别急着动 return true; } // IIS 对象统一判断连续失败次数 return item.ConsecutiveFailures 3; } private static bool CheckServiceIsAutoStart(string serviceName) { using (var sc new ServiceController(serviceName)) { return sc.StartType ServiceStartMode.Automatic; } } }逻辑说明:EndsWith(Pending) 是为了绕过 StartPending 和 StopPending 中间态。最常见的问题出在服务启动依赖其他服务时它会长时间停在 StartPending如果你这时候也判定为异常就会出现两条启动指令同时在跑最终服务起不来。遇到 Pending 态正确做法是跳过本次动作等下一次轮询再看。3.3 自动拉起动作:先恢复应用池再恢复站点最后恢复服务恢复动作的写法和读取对称:Windows 服务用 ServiceControllerIIS 对象用 ServerManager。但有一个顺序坑很多人翻过车:站点启动依赖应用池池没有起来你先启动站点会收获一个异常或者站点显示 Started 但实际上没有后端进程响应。所以正确顺序永远是先池后站。// RecoveryHub.cs —— 自动拉起动作 using System.ServiceProcess; using Microsoft.Web.Administration; public static class RecoveryHub { public static bool TryRecover(MonitorItem item, out string reason) { reason string.Empty; if (item.Category MonitorCategory.IISAppPool) { using (var mgr new ServerManager()) { var pool mgr.ApplicationPools[item.Name]; if (pool null) { reason application pool not found; return false; } pool.Start(); mgr.CommitChanges(); return true; } } if (item.Category MonitorCategory.IISWebSite) { using (var mgr new ServerManager()) { // 先找关联的应用池并启动 var app mgr.Sites[item.Name]?.Applications[0]; if (app null) { reason site application not found; return false; } var poolName app.ApplicationPoolName; var pool mgr.ApplicationPools[poolName]; if (pool ! null pool.State ! ObjectState.Started) { pool.Start(); mgr.CommitChanges(); } // 等待池稳定后再拉站点 System.Threading.Thread.Sleep(3000); var site mgr.Sites[item.Name]; if (site ! null site.State ! ObjectState.Started) { site.Start(); mgr.CommitChanges(); } return true; } } if (item.Category MonitorCategory.WindowsService) { using (var sc new ServiceController(item.Name)) { try { sc.Start(); sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(20)); return sc.Status ServiceControllerStatus.Running; } catch (Exception ex) { reason ex.Message; return false; } } } reason unknown category; return false; } }逻辑说明:池.Start() 之后 CommitChanges这个提交动作才让 IIS 运行时真正执行。Sleep(3000) 是给池留出启动时间短了站点会抢跑长了拖慢恢复。Windows 服务的 WaitForStatus 超时设为 20 秒超过这个时间要当失败处理并记录原因不能一直等下去。4. 部署成真正在后台运行的Windows服务:安装步骤、账号权限与开机启动源码写完之后最关键的落地动作是把监测程序注册成 Windows 服务。直接双击 exe 是一种方案但生产环境不可控:终端一关进程就没了服务器重启后也不会自动起来。所以部署这一步不可省。4.1 用Topshelf还是Worker Service承载监测循环监测循环需要一个宿主进程。常见做法有三种:承载方式上手难度服务异常恢复日志集成适用场景Topshelf低自带简易恢复自带输出丰富单机小规模、老项目迁移顺手.NET Worker Service中配合 Windows 服务可靠微软官方日志体系好打算长期迭代、要接告警手写 ServiceBase高自己实现逻辑自己写集成到一个大型系统里我自己的习惯是小范围内用 Topshelf因为安装命令短逻辑直观;如果这个项目要接多台服务器、要上配置中心和告警就换 Worker Service。核心监测代码是同一套宿主层替换即可。这里有个原则性的坑:监测程序不要部署在 IIS 的应用池进程里。见过有人为了省事把监控代码挂在一个 IIs 站点下面结果池一回收监控进程跟着没了等于自己监控自己最后全系统躺平。监测程序必须独立成服务生命周期和 IIS 完全隔离。4.2 安装、卸载和开机启动:sc.exe与PowerShell两条路源码编译后是一个 exe 文件。Topshelf 模式下自带安装指令但更通用的方式还是 Windows 服务控制指令。常见做法是先用 sc.exe 创建一个服务再设置失败恢复策略:sc create IISMonitor binPath C:\Monitor\IISMonitor.exe start auto sc description IISMonitor Monitor Windows services and IIS, auto recover sc failure IISMonitor reset 86400 actions restart/5000/restart/10000/restart/30000参数说明:sc create 中binPath和start等号后面必须有一个空格这是 sc.exe 的老毛病写错会直接报参数错误。start auto 表示开机自启。sc failure 设置服务崩溃后的动作意思是第一次失败等 5 秒重启第二次等 10 秒第三次等 30 秒;reset 86400 表示 24 小时内失败计数重置。这一步保证了监控服务本身出了问题也能被系统拉回来。也可以走 PowerShell语义更清晰:New-Service -Name IISMonitor -BinaryPathName C:\Monitor\IISMonitor.exe -StartupType Automatic -DisplayName IIS实时监测服务注意New-Service 不会帮你设置失败恢复策略创建之后还要执行一次sc failure IISMonitor ...。卸载路径同样两条:sc delete IISMonitor或者Remove-Service卸载前记得先停服务。4.3 账号权限:LocalSystem、NetworkService和专用账号怎么选监测服务要读取 IIS 配置还要启动 Windows 服务权限边界要提前想清楚。我按场景给了一张对照表:运行账号能否读取IIS配置能否启动服务风险程度建议LocalSystem能大部分能高权限过大单机推荐清爽省事NetworkService部分受限部分受限中不推荐用于IIS监控专用域账号需额外授权可细化低多机集群时使用如果只是单台 Windows 服务器我一般直接用 LocalSystem省去授权排查。万一监测服务被攻破本地系统权限确实危险但比起因为账号权限问题导致监控瞎掉这个风险在实际内网环境里更容易接受。多台服务器集中监控时用一个专用账号只给目标机器上的服务启动权限和 IIS 配置读取权限别用域管理员。必须记住一点:监测服务所在的主机如果启用了用户访问控制相关策略LocalSystem 下也未必能启动所有服务。启动失败时先看 Windows 事件日志里服务控制管理器记录的日志不要先去猜代码问题。5. 生产环境跑监测器容易踩的坑:五种让人血压升高的异常再好的源码到了生产环境都会遇到一些“怎么本地跑得好好的、一上服务器就翻车”的问题。下面这五类是我实际排查中遇到最多、也最有代表性的记录全部按现象、原因、解决办法三层写清。5.1 读不了IIS配置报“Windows身份验证”等角色组件缺失现象:监测服务在开发机上一切正常部署到服务器后一启动就报错异常信息里提到 IIS 配置读取失败或者缺少必需的角色服务比如“未安装这些必需的 web 服务器角色服务:Windows 身份验证”。服务进程本身没有崩溃但进入不了监测主循环。原因:目标服务器安装 IIS 时用的默认角色没有勾选几个认证相关的子组件。Microsoft.Web.Administration 读取配置时会检查这些角色服务缺了就直接拒绝访问。另一个常见情况是服务器的 IIS 版本和开发机不一致引用了过高版本的 Microsoft.Web.Administration 程序集。解决办法:在服务器上打开“服务器管理器 → 添加角色和功能 → Web 服务器(IIS) → 应用程序开发”把 Windows 身份验证等组件勾上重启 IIS。程序集方面直接从目标服务器的C:\Windows\System32\inetsrv\Microsoft.Web.Administration.dll引用保证版本与运行时完全匹配。5.2 站点显示Started但用户请求全部超时现象:监测器后台一直显示站点状态正常State 枚举是 Started可业务方每隔一段时间就报页面打不开重启应用池后立刻恢复。这种情况最难抓因为状态读数完全正常。原因:应用池被 IIS 自动回收或者 W3WP 进程的 CPU/内存触顶进程假死。问题在于 ServerManager 读到的 Started 只是配置层面的状态它不关心进程是否真的还能处理请求。站点没有真正死掉只是不再响应。解决办法:监测里必须加活性探测。常做的方式是每 15 到 20 秒对站点首页发一个 HTTP HEAD 请求连续三次超时或返回 5xx判定为假死触发应用池回收。同时用性能计数器观察 W3WP 的 CPU 和私有内存连续三次超过阈值再处理避免瞬时尖峰误报。5.3 自动拉起失败后反复重试越拉越乱现象:某个服务启动失败监测器每隔 10 秒就重新拉起一次日志里全是同一条启动失败记录。最后发现服务其实处于 StopPending 状态之前有一条停止指令还没执行完拉起的指令全部排队失败。原因:状态判定逻辑没有处理中间态。StopPending 表示系统正在停止服务这时候调用 Start() 是无效的必须等它完全进入 Stopped 才能重新拉起。另一个原因是恢复动作失败后没有冷却机制导致每次轮询都在做无效操作。解决办法:在 MonitorItem 里维护 LastRecoveredAt 和 ConsecutiveFailures 两个字段。同一个对象在 60 秒内只允许拉起一次连续失败三次后转入“人工处理”状态只告警不动作。把它们设计成参数方便不同服务设置不同冷却周期。5.4 手动启动的服务被“好心”自动拉起现象:一个手动启动的批量任务服务因为备份计划还没到本来处于停止状态结果被监测器当成故障拉起来了。备份任务立即执行业务数据出现问题。原因:判定逻辑没有区分服务的启动类型。很多初版代码只判断状态是不是 Running完全不看启动类型等于把所有停止状态的服务都当成故障。解决办法:严格按第 3 章的原则只有 StartType 是 Automatic 的服务才进入自动恢复流程。手动服务只记录一条状态日志不做任何操作。修改之后监测器的“拉起动作”数量会明显下降这是个正常现象说明误判被拦截了。5.5 站点启动顺序错误导致恢复后依然不可用现象:监测器发现站点停止执行恢复动作站点状态是 Started但访问还是一直 502。手工在 IIS 管理器里点“启动”也是这个现象最后发现要先启动应用池再启动站点。原因:站点的启动依赖应用池。池处于 Stopped 状态时单纯启动站点IIS 会返回配置错误或者状态不一致。启动站点前必须先确保池已是 Started并且等池稳定几秒。解决办法:恢复动作严格按照“先启池、再等三秒、再启站点”的顺序执行。为什么是 3 秒而不是 1 秒或 10 秒:1 秒太短池可能还没完成初始化;10 秒太长业务中断时间被拉长。3 秒加上系统调度延迟足以覆盖大部分场景。如果你监控的池里部署了重应用比如加载大量程序集就把这个间隔调到 5 秒。6. 一个值得做的进阶:监测数据落SQLite,用历史记录验证监测器本身没有历史数据的监测系统只能算一个会跳的脚本。你永远不知道它昨晚到底干了什么也不知道每次恢复动作是不是真的有效。我现在的做法是给监测器加一个 SQLite 落库每轮轮询和每次动作都写一条记录。SQLite 单文件、零依赖、不会因为网络问题停摆对单机监测来说是性价比最高的存储。先建一张表字段尽量简单:CREATE TABLE IF NOT EXISTS monitor_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, name TEXT NOT NULL, state TEXT NOT NULL, action TEXT, message TEXT, ts TEXT NOT NULL );字段说明:category 区分服务、应用池、站点;name 是对象名;state 是本次读取到的状态;action 记录这次周期是否执行了 restart;message 用来存失败原因。这张表不要加索引也没关系数据量一天几千条SQLite 完全扛得住。写入逻辑对应第 3 章的监测循环在每次轮询结束时追加:// SqliteSink.cs —— 把监测历史写入本地SQLite using Microsoft.Data.Sqlite; public static class SqliteSink { public static void Append(string category, string name, string state, string action, string message) { using var conn new SqliteConnection( Data SourceD:\\Monitor\\monitor.db); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText INSERT INTO monitor_history(category, name, state, action, message, ts) VALUES ($c, $n, $s, $a, $m, $t); cmd.Parameters.AddWithValue($c, category); cmd.Parameters.AddWithValue($n, name); cmd.Parameters.AddWithValue($s, state); cmd.Parameters.AddWithValue($a, action ?? string.Empty); cmd.Parameters.AddWithValue($m, message ?? string.Empty); cmd.Parameters.AddWithValue($t, DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss)); cmd.ExecuteNonQuery(); } }参数说明:SQLite 连接串里 Data Source 指向本地文件建议和监测 exe 放在同一目录避免权限问题。每次追加一条记录就新开连接虽然有点重但监测频率是 10 秒一次这点开销可以忽略。如果你把轮询缩短到 2 秒以内就要考虑连接复用。落库之后验证方法就变得很具体。跑一周后执行一条查询:SELECT name, COUNT(*) AS recover_count FROM monitor_history WHERE action restart GROUP BY name ORDER BY recover_count DESC;这条语句能回答一个重要问题:哪些对象在反复被拉起?如果某个应用池一周被恢复了十几次说明它本身存在慢性问题——应用池内存上限偏低、站点代码有内存泄漏。这时候监测器已经从“故障恢复工具”变成了“故障定位线索来源”。我自己的习惯是每个月翻一次 monitor_history把恢复次数最多的前五个对象拿出来做一轮业务确认。再进一步结合 IIS 日志分析把监测器记录的恢复时间点和 IIS 日志里的错误码时间点做对比就能判断出“假死持续了多久”“是哪一类请求触发的”。有了这些数据设置轮询间隔、设置性能计数器阈值才不是玄学。这个方案的投入产出比很高落库只需要几十行代码换来的是对整个 Windows 服务群健康度的长期观察。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →