简介: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.PerformanceCounter | CPU、内存、连接数等运行时指标 | 管理员权限 | 低 | 深入采集性能数据时的补充手段 |
2.2 为什么核心逻辑放在 Microsoft.Web.Administration
Microsoft.Web.Administration 是微软官方提供的 IIS 管理 API,ServerManager 类就是整个操作的入口。它用起来有点像操作一个“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 组件。如果开发机器上根本没装 IIS,ServerManager 在某些操作下会抛异常。所以最好在装了 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 List<AppPoolStatus> GetAppPoolStatuses() { var result = new List<AppPoolStatus>(); 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 List<SiteStatus> GetSiteStatuses() { var result = new List<SiteStatus>(); 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(List<AppPoolStatus> pools, List<SiteStatus> 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:PublishSingleFile=true这里我特别强调: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 2>&1"然后你就可以在服务器上配一个简单的文件监控,当 check_result.log 中出现"State": "Stopped"或者"IsPortOpen": false时触发告警。如果你的团队用的是 Zabbix,也可以用 Zabbix Agent 的自定义 key 来调用这个插件,把输出解析成监控项。
5. 常见问题与排查技巧实录
实际使用过程中,总有一些文档不会明确写的细节。这节我整理成速查表,并展开讲讲几个高频问题的排查思路。
5.1 高频问题速查表
| 现象 | 可能原因 | 排查方式 |
|---|---|---|
| 运行时报 UnauthorizedAccessException | 当前用户不是管理员 | 用管理员身份运行或检查计划任务的“使用最高权限运行” |
| 状态栏显示 Started 但端口探测失败 | 端口被占用或站点绑定的是特定主机头 | netstat -ano;浏览器按主机头访问试试 |
| AppPool 显示已停止,但回收后自动停止 | 应用程序池的失败计数超限 | 查看事件查看器里 WAS 事件的退出码 |
| 性能计数器读出来一直是 0 | 计数器未预热 | 第一次 NextValue() 后延时1秒再读第二次 |
| 网站在服务器上正常,远程访问 503 | AppPool 的“启用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.5(Windows 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.config:
<system.webServer> <httpProtocol> <customHeaders> <remove name="X-Powered-By" /> </customHeaders> </httpProtocol> <security> <requestFiltering removeServerHeader="true" /> </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 @{LogName='System'; ProviderName='WAS'; StartTime=(Get-Date).AddMinutes(-10)} | Select-Object -First 5这个信息拼在一起,排查效率能翻倍。
第三,如果要做多服务器监控,不建议直接在一台机器上跑多个插件进程。更好的做法是把 check_iis 做成“被监控端”,通过约定的命令行接口输出 JSON,由统一的监控平台去拉取。这样每台服务器上只部署一个轻量 agent 或计划任务,数据汇总在上层完成,架构会清爽很多。
最后再分享一个小技巧:开发这类运维小工具时,保持参数解析的简单直观很重要。我之前做过一版“智能”插件,可以根据机器名自动判断要监控哪些站点,结果部署到新服务器上总是不符合预期,排查了半天发现是配置文件格式的问题。后来干脆改成最简单的显式参数传递,反而再也没出过错。在运维工具的设计里,“不要聪明过头”从来都是美德。
check_iis 这个项目本身不大,但如果能真正打磨到稳定、无脑、可复用,它的价值就会远远超过那几百行代码。希望这篇拆解对你有参考作用——改天如果你在写类似的监控小工具时遇到坑,欢迎回来一起聊聊。
本文还有配套的精品资源,点击获取