C#实现IIS监控插件:实时检查站点与应用程序池状态
简介check_iis是一款用C#编写的IIS监控插件专为需要在Icinga2、Icinga、Centreon、Shinken、Naemon等Nagios系监控平台中巡检Windows IIS站点的运维人员设计。它在本机执行Sites与AppPool检查需由代理或执行程序以管理员身份调用并支持.NET 3.5及4.0环境。资源共18个文件以8个cs源码文件为主另含config/conf配置、sln解决方案、csproj工程文件、XML及license等压缩包仅36KB结构紧凑便于直接阅读和二次开发。目前已有200人学习下载。通过这份资源读者可以快速上手check_iis的配置与编译理解站点和应用程序池匹配的命名开关及大小写规则也可基于源码调整监控逻辑适合对Windows监控插件开发有兴趣的工程师参考学习。1. 项目概述check_iis 到底在监视什么先说明一下背景。做服务器运维或者偏运维的开发几乎都遇到过这种情况生产环境里的 IIS 站点毫无征兆地挂了用户反馈打不开页面你打开服务器一看发现应用程序池AppPool已经处于“已停止”状态。更头疼的是你根本不知道它是何时停止的也没办法第一时间知道——只能等用户来报障。check_iis 这个插件项目要解决的就是这类问题。它是一个用 C# 编写的监控插件专门盯住本地计算机上的 IIS 站点和应用程序池把它们的运行状态、响应情况、资源占用实时采集出来。它的定位很明确轻量级、易部署、围绕 IIS 场景做深做透而不是像大型监控平台那样什么都管。它的适用人群非常清晰负责 Windows Server IIS 运维的工程师、写 C# 上位机或者内部工具、需要把 IIS 状态接入到现有监控体系的开发人员还有那些被“半夜站点挂了没人知道”折腾过的朋友。如果你只是管理一两台服务器手动打开 IIS 管理器看一眼也行但如果你有几十个站点、十几个 AppPool人肉巡检完全不现实这类小工具的价值就体现出来了。我自己的看法是这个项目最难能可贵的不是技术本身有多高级而是它把一个运维痛点拆得很清楚不是“监控整个服务器”而是“只监控 IIS 站点和 AppPool”范围小、目标明确、代码量可控、可维护性强。这种做小做精的思路恰恰是很多半吊子监控项目缺少的。2. 监视 IIS 的技术路径选型为什么是这几种方案要写一个 IIS 监控插件第一步不是写代码而是确定怎么去拿 IIS 的状态数据。Windows 平台上C# 开发者有往下这几条成熟的技术路径可选我对比后列出了各自的优劣势。2.1 三条主流技术路线对比技术方案获取的数据范围权限要求开发难度适用场景Microsoft.Web.Administration站点、AppPool 状态和配置信息管理员权限低API 封装得很友好插件首选通用方案WMI 查询进程、服务、性能计数器等系统数据视查询类目而定中查询语句需要调试需要额外拿性能指标时组合使用System.Diagnostics.PerformanceCounterCPU、内存、连接数等运行时指标管理员权限低深入采集性能数据时的补充手段2.2 为什么核心逻辑放在 Microsoft.Web.AdministrationMicrosoft.Web.Administration 是微软官方提供的 IIS 管理 APIServerManager 类就是整个操作的入口。它用起来有点像操作一个“IIS 的内存快照”把站点集合和应用程序池集合都暴露给你直接遍历就能拿到每个实体和它的 State 属性。用这个 API 最大的理由是省心。你不用去解析 IIS 的配置文件applicationHost.config——虽然本质上这个 API 就是把配置映射成了对象模型但它帮你把序列化和反序列化做了。而且 State 属性不是简单的字符串是一个 ObjectState 枚举取值有 Started、Stopping、Stopped、Starting、Unknown 这几种判断逻辑非常干净。相比自己去读 WMI 里 Win32_Service 的 State 字段再用数字状态码映射这种方式出错概率小得多。提示关键细节是ServerManager 每次实例化都会重新读取配置。如果你在循环里反复 new ServerManager性能会很差。正确做法是只实例化一次操作完统一释放。2.3 性能计数器作为补充仅靠 Microsoft.Web.Administration能拿到的是“状态”数据——运行着还是停着这属于是/否的判断够用但信息量不够。真正要判断站点是不是“健康”还需要连接数、当前请求数、最近一分钟的请求成功率这类运行时数据。这时候就要用到性能计数器。IIS 相关的性能计数器类别主要是 Web Service 和 W3SVC_W3WP。Web Service 类别下面有针对每个站点以站点名称命名的实例的 Current Connections、Total Bytes Sent、Total Method Requests 等计数器W3SVC_W3WP 则按照工作进程实例提供 CPU 和内存占用数据。用 PerformanceCounter 这个类读取即可核心代码并不复杂PerformanceCounter counter new PerformanceCounter( Web Service, Current Connections, Default Web Site, .); float currentConnections counter.NextValue();注意一个问题某些计数器第一次调用 NextValue() 返回的是 0需要间隔一小段时间再取第二次才能拿到真实值。这不是 bug是计数器机制本身就是这么设计的——差值型计数器必须经过两次采样才能算出速率。所以插件在设计采样逻辑时一定要考虑“预热”的过程。从方案选型来看我实际的建议是主体用 Microsoft.Web.Administration如果你只是想知道“站点挂没挂”这一个就够了但如果你想把插件接到 Zabbix 或自己的告警系统里加上性能计数器让告警信息里不仅写着“站点停了”还写着“停止前连接数异常飙升”对定位问题会有很大帮助。3. 核心实现写一个可靠的 check_iis 插件明确了技术路径下面进入正题。我这里展示的是我在类似项目里沉淀下来的一套实现方式整体代码量不大但每一步都有值得注意的细节。3.1 项目结构与依赖引入先用 .NET 创建一个控制台应用目标框架建议用 .NET 6 或 .NET 8这样在 Windows Server 2019/2022 上部署比较省事也方便做单文件发布。项目的主要依赖只有一个dotnet add package Microsoft.Web.Administration这个包在 NuGet 上直接可用但是有个潜在的坑它默认依赖系统的 IIS 组件。如果开发机器上根本没装 IISServerManager 在某些操作下会抛异常。所以最好在装了 IIS 的机器上开发或者退一步在开发机上安装 IIS 管理脚本和工具功能。项目结构可以拆成三个核心文件Program.cs入口负责参数解析和调度。IisMonitor.cs核心监控逻辑封装 ServerManager 和性能计数器操作。Reporter.cs输出结果支持 Console 和文件两种方式。3.2 读取 AppPool 状态的核心代码应用程序池状态检测是整个插件最核心、也是运维最关心的功能。我直接给出一个可用的实现using Microsoft.Web.Administration; public class AppPoolStatus { public string Name { get; set; } public string State { get; set; } public string RuntimeVersion { get; set; } public string ManagedPipelineMode { get; set; } public long? CurrentWorkerProcessId { get; set; } } public class IisMonitor : IDisposable { private readonly ServerManager _serverManager; public IisMonitor() { _serverManager new ServerManager(); } public ListAppPoolStatus GetAppPoolStatuses() { var result new ListAppPoolStatus(); foreach (ApplicationPool pool in _serverManager.ApplicationPools) { var status new AppPoolStatus { Name pool.Name, State pool.State.ToString(), RuntimeVersion pool.ManagedRuntimeVersion, ManagedPipelineMode pool.ManagedPipelineMode.ToString() }; // 尝试获取工作进程 ID便于精确定位问题 try { WorkerProcess wp pool.WorkerProcesses.FirstOrDefault(); status.CurrentWorkerProcessId wp?.ProcessId; } catch { // 某些系统权限不足时拿不到 WorkerProcesses这里不阻塞主流程 status.CurrentWorkerProcessId null; } result.Add(status); } return result; } public void Dispose() { _serverManager.Dispose(); } }注意三个细节。一是遍历 ApplicationPools 拿 State 很快但 pool.WorkerProcesses 是懒加载的如果池子已经停止了它返回空集合是正常现象不用当成异常处理。二是每次检测完一定要释放 ServerManager否则会有句柄泄漏长时间跑下来内存持续增长。三要注意 ManagedPipelineMode 的取值是 Integrated 还是 Classic有些老站点跑在 Classic 模式下如果监控脚本把 Classic 当异常报警就会产生大量误报——这个我在后面常见问题里还会展开。3.3 读取站点状态与绑定的实现站点检测除了状态还要关注绑定和端口是否有效。有些场景下站点显示已启动但因为端口被其他程序占用了实际上服务不可用——如果监控逻辑里没有端口探测这个环节根本发现不了这种问题。我建议在状态检测之外加一个 TCP 连通性检查拿到站点的绑定信息IP、端口、主机头然后用 TcpClient 尝试连接。这个检查是锦上添花但能把监控深度提升一个层次。public ListSiteStatus GetSiteStatuses() { var result new ListSiteStatus(); foreach (Site site in _serverManager.Sites) { var status new SiteStatus { Name site.Name, State site.State.ToString(), Bindings site.Bindings.Select(b ${b.Protocol}://{b.BindingInformation}).ToList() }; // 若站点已启动额外做端口连通性验证 if (site.State ObjectState.Started) { status.IsPortOpen CheckPort(site.Bindings.FirstOrDefault()?.EndPoint); } result.Add(status); } return result; }注意BindingInformation 的格式是“IP:端口:主机头”比如*:80:www.example.com。如果你用字符串去解析端口务必用EndPoint属性这个属性是 API 帮你解析好的直接拿 Port 即可不要自己 split 字符串——自己解析在 IPv6 地址上会翻车。3.4 输出格式与告警集成设计插件毕竟是给人用的输出格式直接决定实用程度。我见过一些工具把原始 JSON 哗啦啦地输出到终端看的人头皮发麻。check_iis 我建议做成“参数决定输出格式”的方式--apppool只检测应用程序池输出状态摘要。--site只检测站点。--json输出结构化 JSON便于接入 Zabbix、Prometheus 的 exporter 机制。--threshold可选CPU/内存使用率告警阈值。具体输出代码长这样static void PrintSummary(ListAppPoolStatus pools, ListSiteStatus sites) { Console.WriteLine( IIS Monitor Summary ); Console.WriteLine($[AppPools] Total: {pools.Count}, Started: {pools.Count(p p.State Started)}); foreach (var pool in pools.Where(p p.State ! Started)) { Console.WriteLine($[WARN] AppPool {pool.Name} is {pool.State}); } Console.WriteLine($[Sites] Total: {sites.Count}, Started: {sites.Count(s s.State Started)}); foreach (var site in sites.Where(s s.State ! Started || !s.IsPortOpen)) { Console.WriteLine($[WARN] Site {site.Name} is {site.State}, PortOpen{site.IsPortOpen}); } }输出逻辑的核心思路是只高亮异常项。正常状态下输出简短、安静异常时给出明确的项目名和状态值。这个设计经验来自真实运维场景——前半夜没人盯着屏幕看完整日志必须让异常信息一眼跳出来。4. 实操过程编译、部署与运行验证代码写完了落地部署环节同样有很多讲究。这里把我实际操作中的完整流程走一遍。4.1 发布与部署要点发布时建议直接做单文件发布dotnet publish -c Release -r win-x64 --self-contained false -p:PublishSingleFiletrue这里我特别强调Framework-dependent 模式即--self-contained false在服务器上最省心因为只需要目标机器装了对应版本的 .NET Runtime发布文件体积很小。但前提是你要确保生产服务器上已经装好了对应版本的 .NET。如果你不想依赖服务器环境就用--self-contained true发布产物会大一些但拷过去就能跑。运维场景里两条路都有人走没有绝对的对错。部署目录我建议放在C:\Tools\CheckIis\路径不要带空格不要放中文目录否则后续接入计划任务或者第三方监控软件时可能出现引号转义问题。别问我怎么知道的——我在这上面踩过一次路径带空格导致 Nagios 的远程命令传参时被拆成了两段。4.2 权限配置为什么必须管理员权限check_iis 在运行的时候有两个地方牵扯到权限。一是通过 Microsoft.Web.Administration 读取 AppPool 的 WorkerProcesses这个操作需要管理员权限。普通用户打开这个集合时不会直接报错而是返回空集合——这很容易让人误判为“当前没有工作进程”。二是性能计数器的读取需要用户属于“Performance Monitor Users”组。所以部署的时候我建议用这两种方式处理手动巡检直接用管理员身份运行命令行。计划任务把插件配成每隔5分钟运行一次计划任务里勾选“使用最高权限运行”并且使用专用服务账号而不是当前登录用户。如果只是在命令行里手动跑一次可以这样验证CheckIis.exe --site --json成功的话会输出类似下面的内容{ Sites: [ { Name: Default Web Site, State: Started, Bindings: [http://*:80:], IsPortOpen: true } ] }如果看到 IsPortOpen 为 false 而 State 是 Started说明站点监听异常优先排查端口占用和防火墙规则。4.3 接入计划任务的配置示例用计划任务做定时巡检是最轻量的集成方案不需要额外装任何服务。创建一个每5分钟运行一次的计划任务命令行参数设置成--site --apppool --json然后把输出重定向到日志文件。这里的要注意的一个细节是计划任务里重定向输出和手动命令行里不一样必须通过 cmd /c 包一层否则输出重定向会不生效cmd /c C:\Tools\CheckIis\CheckIis.exe --site --apppool --json C:\Tools\CheckIis\logs\check_result.log 21然后你就可以在服务器上配一个简单的文件监控当 check_result.log 中出现State: Stopped或者IsPortOpen: false时触发告警。如果你的团队用的是 Zabbix也可以用 Zabbix Agent 的自定义 key 来调用这个插件把输出解析成监控项。5. 常见问题与排查技巧实录实际使用过程中总有一些文档不会明确写的细节。这节我整理成速查表并展开讲讲几个高频问题的排查思路。5.1 高频问题速查表现象可能原因排查方式运行时报 UnauthorizedAccessException当前用户不是管理员用管理员身份运行或检查计划任务的“使用最高权限运行”状态栏显示 Started 但端口探测失败端口被占用或站点绑定的是特定主机头netstat -ano浏览器按主机头访问试试AppPool 显示已停止但回收后自动停止应用程序池的失败计数超限查看事件查看器里 WAS 事件的退出码性能计数器读出来一直是 0计数器未预热第一次 NextValue() 后延时1秒再读第二次网站在服务器上正常远程访问 503AppPool 的“启用32位应用程序”设置与站点不匹配核对站点应用的位数切换 AppPool 的 32 位开关5.2 经典案例站点没挂但就是访问不了我遇到过最典型的“隐蔽故障”是这样的IIS 管理器里站点显示“已启动”AppPool 也显示“已启动”但客户端访问时直接报 503 Service Unavailable。用插件一检查状态全绿但 IsPortOpen 正常——因为 80 端口确实有监听只是监听者不是 IIS 的工作进程。排查到最后发现是服务器上装了另一个 Web 服务把 80 端口抢走了。IIS 因为启动时端口绑定失败但状态没来得及更新就出现了这种“逻辑上已启动、物理上没监听”的诡异局面。这个案例给我最大的教训是监控 IIS 不能只用 API 读状态必须结合端口探测。这也是我在前面的代码里强烈建议加 TcpClient 检查的原因——它能抓出这些 API 看不到的异常.另一个值得注意的坑IIS 站点绑定里如果设置了非 80 端口而你写死了只检查 80那么每次检测都会误报“站点不可用”。要解决这个问题最稳妥的办法是直接读取 site.Bindings 拿到实际端口再去做 TCP 连接检测。别写任何硬编码端口。5.3 关于 IIS 版本差异的注意事项不同版本的 IIS插件表现差异很大。IIS 7.0 / 7.5Windows Server 2008 / 2008 R2老机器上Microsoft.Web.Administration 读取 ApplicationPool 的状态有一些历史包袱当你用 ServerManager 实例化后随意修改配置再保存可能意外改写 applicationHost.config 的格式导致整个 IIS 配置失效。所以检查类工具读取就纯读取不要调用 CommitChanges()——这是一个铁律。即使只读在老版本上也要小心ServerManager.Dispose() 之前如果开启了编辑模式内部状态没有正确释放仍可能触发写操作。你不确定的时候直接用 using 包住不要手动管理生命周期。Windows Server 2012 及以上的 IIS 8.0这些坑都少了很多API 也稳定了可以直接放心用。还有一点IIS 响应头里默认会暴露 X-Powered-By: ASP.NET 和 Server 版本信息。内部监控不影响但这属于基本的安全加固项。我在部署监控插件时习惯顺带把 IIS 的响应头版本信息隐藏掉操作位置在 IIS 管理器的“HTTP 响应标头”模块或者直接配置 web.configsystem.webServer httpProtocol customHeaders remove nameX-Powered-By / /customHeaders /httpProtocol security requestFiltering removeServerHeadertrue / /security /system.webServer这个配置只针对单个站点。如果你想在服务器级别全局移除就需要在 applicationHost.config 里做配置或者下载专门的 URL Rewrite 模块来处理。这算一个小的加分项和 check_iis 配合使用效果更好——站点本身的安全状态也在监控范围之外做了加固。6. 最后的几个经验和补充技巧分享几个我在实际使用中攒下的细节经验不一定都在代码里但都直接影响插件好不好用。第一采集频率不要贪快。IIS 站点和 AppPool 的状态不是一个瞬息万变的数据没必要 1 秒钟查一次。5 分钟一次足够覆盖绝大多数故障场景。查得太频繁反而会给服务器带来额外的资源开销尤其在应用池很多50个以上的情况下ServerManager 的初始化成本不可忽略。如果你确实需要秒级感知故障应该在 AppPool 回收时间或者事件查看器上做文章而不是靠轮询。第二告警一定要带上下文。检查结果里如果只输出“Default Web Site 已停止”收到告警的同事还是会一脸懵——是手动停止的还是崩溃导致的有没有对应的 Windows 事件日志所以建议在插件输出里顺便调用一句 PowerShell 去查最近的系统事件把 WAS 或 W3SVC 来源的最近几条错误事件附在告警信息后面。Get-WinEvent -FilterHashtable {LogNameSystem; ProviderNameWAS; StartTime(Get-Date).AddMinutes(-10)} | Select-Object -First 5这个信息拼在一起排查效率能翻倍。第三如果要做多服务器监控不建议直接在一台机器上跑多个插件进程。更好的做法是把 check_iis 做成“被监控端”通过约定的命令行接口输出 JSON由统一的监控平台去拉取。这样每台服务器上只部署一个轻量 agent 或计划任务数据汇总在上层完成架构会清爽很多。最后再分享一个小技巧开发这类运维小工具时保持参数解析的简单直观很重要。我之前做过一版“智能”插件可以根据机器名自动判断要监控哪些站点结果部署到新服务器上总是不符合预期排查了半天发现是配置文件格式的问题。后来干脆改成最简单的显式参数传递反而再也没出过错。在运维工具的设计里“不要聪明过头”从来都是美德。check_iis 这个项目本身不大但如果能真正打磨到稳定、无脑、可复用它的价值就会远远超过那几百行代码。希望这篇拆解对你有参考作用——改天如果你在写类似的监控小工具时遇到坑欢迎回来一起聊聊。本文还有配套的精品资源点击获取