简介:这是一款面向 Windows 运维与监控工程师的 C# 插件,用于在 Icinga2、Icinga、Centreon、Shinken、Naemon 等类 Nagios 监控系统中检查本地 IIS 站点与应用程序池状态,适合需要把 IIS 可用性纳入统一告警体系的中高级读者。插件要求 .NET Framework 3.5 SP1 及以上、IIS 7.0 及以上,并强调只能在本机执行,通常由 Icinga2 代理或 NSCP 调用,且执行账户需具备管理员权限;站点与应用程序池匹配区分大小写,仅支持命名开关。压缩包共 18 个文件、约 36KB,以 8 个 cs 源码文件为核心,辅以 sln 解决方案、csproj 工程文件、config 与 conf 配置示例、xml、README 及 LICENSE 等,便于直接编译、部署和二次调整。已有 202 人学习,可帮助读者快速获得可用的 IIS 监控检查能力、理解参数与权限要求,并参考现成配置完成接入与排错。
1. 凌晨三点的 503:check_iis 补的是哪一块监控盲区
某台 windows-server 上的站点半夜开始返回 503,远程连上 IIS 管理器一看,AppPool 状态是 Started,站点也显示正在运行;面板上一切正常,服务已经不可用。这种“看着活着其实死了”的情况,靠人肉 RDP 加肉眼刷新是抓不住的,回收、按需启动、配置漂移都可能让显示状态和真实可用性脱节。check_iis 是一个用 C# 写的 windows-plugin,只负责一件事:在本机把 IIS 站点和 AppPool 的真实状态读出来,按 Nagios 插件协议输出一行结果,交给 icinga 判定。它适合已经在用 icinga 管一批 windows-server、不打算为 IIS 单独上一套监控的人;也适合只想写几十行 C#,把 monitors-sites、monitor-application-pools 做扎实的人。
2. Nagios 插件协议:一行输出、四个退出码和性能数据怎么拼
Nagios 系监控对插件的宽容度非常低。Icinga2 不关心你内部用了多少行代码、调了多少个接口,它只看两样东西:进程退出码,以及 stdout 的第一行。第一行的格式错了、退出码被包装层吞了、异常栈跑到 stderr 去了,结果就是监控面板上一片 UNKNOWN,而你会以为是 IIS 挂了。check_iis 这类插件的绝大部分“难写”,其实难在协议细节上,而不是难在读取 IIS 状态。
2.1 退出码只有四个,但每个都有代价
Nagios 插件协议把退出码钉死成四个值,多一个都不行:
| 退出码 | 状态 | check_iis 里的判定依据 |
|---|---|---|
| 0 | OK | 目标站点与 AppPool 状态均为 Started |
| 1 | WARNING | 命中排除名单之外的非关键项,或处于维护窗口的停止 |
| 2 | CRITICAL | 目标站点或 AppPool 处于 Stopped、读取失败 |
| 3 | UNKNOWN | 参数解析失败、权限不足、IIS 组件缺失、抛出异常 |
新手最容易犯的错是把所有非 OK 都写死成 2。上线第一周就出问题:一台机器上二十几个 AppPool,其中一半是按需启动的,平时就是 Stopped,一次批量回收直接把告警群刷屏,值班的人三天之后就对这个监控项免疫了。我的做法是把“这个 AppPool 该不该一直活着”做成参数,默认严格(Stopped 即 CRITICAL),但给按需启动的池单独挂一条--warn-on-stopped的服务实例,让它降级成 WARNING 进周报而不是进电话。
UNKNOWN 独立出来的意义也在这里。站点改名、AppPool 被删掉、目标机上根本没装 IIS 管理组件,这些都不是“服务故障”,而是“监控配错了”。混进 CRITICAL 里,你就永远分不清是业务挂了还是自己配错了。
2.2 读 IIS 状态的三种路子:ServerManager、appcmd、WMI
同样是拿站点和 AppPool 状态,三种实现方式在权限、依赖、稳定性上差别很大:
| 方式 | 依赖 | 单次耗时 | 权限敏感度 | 适合场景 |
|---|---|---|---|---|
| Microsoft.Web.Administration | inetsrv 目录下的托管 dll | 几十毫秒 | 高,读 applicationHost.config | 本机检查,追求快和结构化 |
| appcmd.exe list | 系统目录下的可执行文件 | 数百毫秒 | 低一些,进程外执行 | 权限难搞、dll 缺失的机器 |
| WMI(root\WebAdministration) | WMI 服务 | 不稳定,首次很慢 | 中 | 只取个别字段、跨机查询 |
check_iis 走的是第一条路,用ServerManager直接读配置。理由是快、返回的是强类型枚举而不是待解析的文本,而且不需要起子进程;缺点是它构造时会把 applicationHost.config 整个读进来,机器上站点特别多的时候耗时会上到百毫秒级,另外对文件 ACL 敏感,这一点后面避坑章节还会提。
核心读取逻辑大概长这样:
using System; using Microsoft.Web.Administration; static class IisProbe { // 返回站点状态与 AppPool 状态;名字按不区分大小写匹配 public static (string site, string pool) Probe(string siteName, string poolName) { // ServerManager 构造即加载 applicationHost.config,别在循环里反复 new using (var mgr = new ServerManager()) { string siteState = "NotFound"; string poolState = "NotFound"; foreach (var s in mgr.Sites) { if (string.Equals(s.Name, siteName, StringComparison.OrdinalIgnoreCase)) { siteState = s.State.ToString(); // Started / Stopped / Unknown break; } } foreach (var p in mgr.ApplicationPools) { // State 是配置层状态,和 w3wp 进程是否存活不是一回事 if (string.Equals(p.Name, poolName, StringComparison.OrdinalIgnoreCase)) { poolState = p.State.ToString(); // Started / Stopped / Starting break; } } return (siteState, poolState); } } }几个参数和写法上的点值得单独说。StringComparison.OrdinalIgnoreCase不能省,IIS 的名字在实际使用中是不区分大小写的,用==比较会在站点名大小写不一致时静默返回 NotFound,进而变成一个永远 UNKNOWN 的服务。NotFound和Stopped必须分开,前者代表配置层面找不到这个名字,后者才是服务停了;混成一个值,站点改名当天你会收到一堆看起来像故障的假告警。using也不能省,ServerManager持有配置文件的句柄,高频检查下不释放会积累句柄。
还有一个边界要提前想清楚:Site.State反映的是 W3SVC 层面的状态,它等于 Started 不代表 80 端口能通、不代表应用能返回 200;ApplicationPool.State是配置状态,一个 Started 的池下面可能根本没有 w3wp 进程(按需启动还没被请求唤醒)。这个插件定位是“配置与运行状态的守门员”,不是端到端可用性探测,端到端那层得靠 HTTP 检查补齐,两者不冲突。
2.3 一行输出里要塞三样东西:状态、详情、性能数据
协议规定第一行的结构是服务名 状态: 描述 | 性能数据,竖线之前给人看,竖线之后给时序库看。写的时候有个小工具函数会清爽很多:
static int Emit(int code, string message, string perf) { // 只输出一行短文本;多行内容属于 long output,解析器处理方式不同 string line = message; if (!string.IsNullOrEmpty(perf)) line += " | " + perf; Console.Out.WriteLine(line); Console.Out.Flush(); // 进程即将退出,显式 flush 少一个黑匣子 return code; }调用侧大概是这样:Emit(2, "IIS CRITICAL - site=Shop state=Stopped pool=ShopAppPool state=Started", "site_state=0;1;1;0;2 pool_state=1;1;1;0;2")。
性能数据的格式是label=value[UOM];warn;crit;min;max,单位可以省略但分号位置不能乱。site_state=0这种把状态映射成数字的做法,好处是能在趋势图上直接看到状态翻转的时间点;坏处是映射表得写进注释,否则半年后你自己都忘了 0 和 1 谁是谁。我一般固定成 Stopped=0、Started=1、NotFound=-1,并且在描述文本里再写一遍原始枚举名,图上看趋势、文本看现场,两边都不耽误。
还有一条不太有人提的注意点:Console.Out.WriteLine在 Windows 上默认输出\r\n,部分解析器会把\r一起带进最后一个字段。如果你的 perfdata 是竖线后面的第一段,\r会粘在最后一个 label 上,Graphite 那边就多出一个永远对不上的指标名。稳妥做法是显式用Write加"\n",或者在后处理里 Trim 掉。
3. 从源码到 exe:用 csc 和 MSBuild 把 check_iis 编出来
拿到源码之后第一件事不是急着编译,而是先确认目标机到底是什么环境。check_iis 依赖Microsoft.Web.Administration,这个程序集在 .NET Framework 下是随 IIS 管理组件装在%windir%\System32\inetsrv\里的,在 .NET 5 及以上则要走 NuGet 包。两条路都能走通,但落地成本完全不一样。
3.1 目标框架:.NET Framework 4.x 还是 .NET 6+
选 .NET Framework 4.7.2 的理由很实际:目标机是 windows-server,系统自带运行时,编译出来的 exe 拷过去就能跑,不需要装任何东西,也不需要把运行时一起打包。这类插件通常是几十行到几百行代码,用不上新语法,框架版本的收益比成本高。
选 .NET 6 及以上的理由也成立:想用单文件发布把依赖全部塞进一个 exe、想写单元测试、想和团队其他工具共享代码。代价是目标机要么装运行时,要么接受几十兆的自包含发布体积,而且Microsoft.Web.Administration的 NuGet 版本和系统里的 IIS 版本之间偶尔会有字段对不上的情况。
我的默认选择是前者。真要用 SDK 风格的项目文件,大概长这样:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net472</TargetFramework> <Platforms>AnyCPU</Platforms> <AssemblyName>check_iis</AssemblyName> <LangVersion>7.3</LangVersion> </PropertyGroup> <ItemGroup> <!-- 直接指向系统 inetsrv 目录,跟着目标机的 IIS 版本走 --> <Reference Include="Microsoft.Web.Administration"> <HintPath>$(windir)\System32\inetsrv\Microsoft.Web.Administration.dll</HintPath> <Private>false</Private> </Reference> </ItemGroup> </Project>Private设成 false 是关键:这个 dll 由目标机提供,不要让编译器把它复制到输出目录,否则你在开发机上编出来的 exe 会带着一份旧版本 dll,部署到新机器上就跑成Could not load file or assembly。$(windir)是环境变量透传成 MSBuild 属性,在标准的 Windows 机器上都能取到。
不想引入项目系统的话,直接用框架自带的 csc 更快:
:: 在装有 IIS 管理组件的机器上直接编译,不依赖 Visual Studio set CSC=%windir%\Microsoft.NET\Framework64\v4.0.30319\csc.exe set MWADLL=%windir%\System32\inetsrv\Microsoft.Web.Administration.dll "%CSC%" /nologo /target:exe /platform:anycpu /optimize+ ^ /out:check_iis.exe ^ /r:System.dll /r:System.Core.dll /r:"%MWADLL%" ^ Program.cs echo BUILD_EXIT=%ERRORLEVEL%参数逐个说:/platform:anycpu在 64 位系统上会以 64 位运行,读的是 64 位视角下的 applicationHost.config;如果目标机存在 32 位组件、必须跑 32 位,就得显式写/platform:x86,同时把引用路径从System32换成SysWOW64,这两个目录在 64 位系统上指向不同的物理位置,混用是最常见的编译通过、运行报错的原因。/optimize+对启动时间影响很小,加上图个心安。/r:后面路径带空格时必须加引号,Program Files一类路径是编译失败的高发区。
编译完立刻做一次依赖自检:把 exe 单独拷到一个空目录跑一遍,能跑说明依赖是系统提供的;跑不起来说明有东西被一起带出来了,或者目标机缺组件。这一步花三十秒,能省掉后面半小时的远程排查。
3.2 先手动跑:把退出码打出来
插件这东西,最忌讳编完就直接扔给监控系统。中间隔着一层代理进程,你看不到真实输出,也看不到真实退出码,出问题只能靠猜。我习惯先在目标机上手动跑通,把输出和退出码都摊在眼前:
# 直接执行并捕获退出码;$LASTEXITCODE 只对原生命令有效 & .\check_iis.exe --site "Default Web Site" --apppool "DefaultAppPool" Write-Host "EXIT=$LASTEXITCODE" # 再把输出重定向到文件,检查编码和行尾 & .\check_iis.exe --site "Shop" --apppool "ShopAppPool" > out.txt Get-Content .\out.txt -Encoding Byte -TotalCount 8 | ForEach-Object { "{0:X2}" -f $_ }PowerShell 里的$?只告诉你上一条命令成功还是失败,不给你具体的码,要看具体值必须用$LASTEXITCODE。第二段是把前八个字节打出来,正常应该是可见字符的 ASCII 码,如果开头出现EF BB BF,说明输出带了 UTF-8 BOM,某些解析器会把第一个字段名读成带 BOM 的怪东西,perfdata 直接对不上。
手动验证阶段我固定做三次反向测试:手动停掉一个 AppPool,看退出码是不是 2;把站点名故意写错一个字母,看是不是 3 而不是 2;把输出重定向到文件,看是不是只有一行、行尾是不是只有\n。三次都过,这个插件才算能交给监控系统。
3.3 挂到 NSClient++ 与 Icinga2:命令、参数、超时
Windows 上的接线通常是 NSClient++ 提供 NRPE 服务端,把 check_iis 注册成外部脚本,Icinga2 侧用 nrpe 命令调用。NSClient++ 的配置片段:
[/modules] NRPEServer = 1 CheckExternalScripts = 1 [/settings/NRPE/server] allow_arguments = 1 allow_nasty_meta_chars = 0 insecure = 0 [/settings/external scripts] allow_arguments = 1 timeout = 30 [/settings/external scripts/scripts] check_iis = scripts\check_iis.exe --site "$ARG1$" --apppool "$ARG2$"allow_arguments出现了两次,这两个位置不是重复配置:NRPE server 段不开,check_nrpe 传过来的参数会被直接拒掉;external scripts 段不开,脚本进程本身收不到参数。这两个开关是独立的,只开一个的表现是“命令能执行但参数全是空”,非常容易误判成插件 bug。timeout的单位是秒,默认值往往比 Icinga2 侧的检查超时还长,两边不对齐就会出现“插件报 OK、Icinga 报超时 CRITICAL”的矛盾结果,接线时先把两边都写成 30 秒,稳定之后再往下压。
Icinga2 侧的命令定义:
object CheckCommand "nrpe-check_iis" { import "nrpe" vars.nrpe_command = "check_iis" vars.nrpe_args = [ "$iis_site$", "$iis_apppool$" ] timeout = 30s }vars.nrpe_args里传的变量要在服务对象上赋值,不要在这里手拼字符串;站点名带空格的时候,靠 NSClient++ 配置里那对引号兜住,但在 Icinga2 的变量层面拼字符串很容易把引号吃掉。nrpe_command的值必须和 NSClient++ 里脚本的名字完全一致,大小写敏感。
接线完成之后的第一件事不是等告警,而是去 Icinga Web 里手动触发一次Check Now,看返回的Output和Performance Data两个字段是不是都有内容。只有状态文本没有性能数据,说明竖线那一段在传输过程中丢了,通常是引号嵌套的问题。
4. 参数化纳管:站点、AppPool 与阈值怎么填才不抖
一台机器上手工配三个服务还行,三十台机器、每台五个站点就完全不是一回事了。插件本身的参数设计决定了后面能不能批量纳管,这一层如果留了坑,后面每加一台机器都要手工调一次。
4.1 名字匹配:精确、通配、大小写
站点名和 AppPool 名都是人起出来的,带空格、带点、带括号、带中文的都有。匹配逻辑写得太聪明反而容易出事:
static bool Matches(string name, string pattern) { if (string.IsNullOrEmpty(pattern) || pattern == "*") return true; // 只支持后缀通配,够用且不会误伤 if (pattern.EndsWith("*")) return name.StartsWith(pattern.TrimEnd('*'), StringComparison.OrdinalIgnoreCase); return string.Equals(name, pattern, StringComparison.OrdinalIgnoreCase); }不引入正则是有原因的。站点名里出现.、(、[是常态,这些字符在正则里都有特殊含义,一个手滑的[就让整段匹配抛异常,插件直接变成 UNKNOWN。用前缀通配加精确匹配能覆盖九成需求:Shop*抓一批,*抓全量,剩下的靠排除名单处理。
| 匹配方式 | 写法 | 适用场景 | 风险 |
|---|---|---|---|
| 精确匹配 | --apppool ShopAppPool | 核心业务池 | 改名后直接 UNKNOWN |
| 前缀通配 | --apppool Shop* | 一套业务多个池 | 新增池会自动纳入,可能误报 |
| 全量 | --apppool * | 首次摸底排查 | 按需启动的池会持续抖动 |
| 排除名单 | --exclude Api,*_old | 长期排除非关键池 | 排除过多会漏检 |
全量匹配是最好用也最容易踩坑的一档。第一次接手一批 windows-server 的时候,我一般会先跑一遍--apppool *,把结果当作摸底,看清楚这台机器上到底有哪些池、哪些是常驻的、哪些是别人按需拉起来的,然后再收敛成精确名单。直接把全量配置推上生产,等着收抖动告警就行。
4.2 阈值:什么时候该 WARNING
参数表比文字描述更省事,下面这张是我常用的起步配置:
| 参数 | 含义 | 常见取值 | 备注 |
|---|---|---|---|
--site | 站点名,支持前缀通配 | Default Web Site | 留空则只检查 AppPool |
--apppool | AppPool 名,支持通配 | ShopAppPool | *表示全量 |
--warn-on-stopped | 停止时降级为 WARNING | true | 维护窗口期打开 |
--exclude | 排除名单,逗号分隔 | Api,*_old | 先排除再判断 |
--timeout | 插件内部超时(秒) | 10 | 必须小于 Icinga 侧超时 |
--timeout这条要单独强调:插件内部超时应该比 Icinga 侧的检查超时短一截,这样超时的时候插件还有机会输出一行IIS UNKNOWN - timeout after 10s,而不是被 Icinga 直接杀掉、只剩一句没有上下文的 "Timeout exceeded"。前者你能看到是哪台机器哪个站点慢,后者只能看到一个红点。
WARNING 和 CRITICAL 的边界,我一般按业务影响划:直接影响用户请求的站点和池,停止就是 CRITICAL;后台任务、定时作业、运维工具用的池,停止就是 WARNING。这条规则写不进插件,得写进配置模板,所以参数设计上留了--warn-on-stopped这个开关,让同一份 exe 在不同服务实例上表现不同。
4.3 一台到一百台:apply 规则与主机标签
主机对象上把站点和池的对应关系声明清楚,服务用 apply 规则生成,新增机器只需要加一个 Host 对象:
object Host "win-web-01" { import "generic-host" address = "10.0.0.11" vars.os = "windows" vars.iis_sites = [ "Default Web Site", "Shop" ] vars.iis_pools = { "Default Web Site" = "DefaultAppPool" "Shop" = "ShopAppPool" } } apply Service "iis-" + site for (site in host.vars.iis_sites) { import "generic-service" check_command = "nrpe-check_iis" vars.iis_site = site vars.iis_apppool = host.vars.iis_pools[site] assign where host.vars.os == "windows" && host.vars.iis_sites }用字典而不是数组存池名,是为了避免顺序错位。两个数组一旦某次编辑多了一行或者少了一行,站点 A 就会去检查站点 B 的池,告警内容看起来完全合理,排查的时候能让人怀疑人生。
服务名里带站点名是个小技巧,仪表盘上一眼就能定位是哪个站点挂了,不用点进去看详情。但服务名不能带空格和斜杠,Default Web Site这种名字要先做一次替换,比如site.Replace(" ", "_"),否则生成出来的服务对象名会破坏配置解析。
5. 避坑与排查:五个让 check_iis 输出 UNKNOWN 的典型原因
插件写完之后真正花时间的不是功能,是环境。同一份 exe,在开发机上跑得好好的,挂到服务里、拷到生产机上,表现可以完全不同。下面五条是我在实际部署里反复遇到的,按“现象、原因、解决”记下来,下次能省掉不少远程时间。
5.1 权限与身份:手动跑 OK,服务里跑就 UNKNOWN
现象:RDP 登录后手工执行 check_iis.exe,输出正常、退出码 0;挂到 NSClient++ 后面,所有检查项一律 UNKNOWN,输出是Access to the path ... applicationHost.config is denied或者干脆空行。
原因:服务的运行身份和你登录的身份不是一回事。服务账号即便是本地管理员组成员,读取%windir%\System32\inetsrv\config目录也常常被 ACL 挡住,加上 UAC 对交互式会话和服务会话的处理不同,手动能读的文件在服务身份下读不到。另外 32 位进程访问System32时会被重定向到SysWOW64,路径看起来一样,实际指向的目录不同。
解决:先确认身份,在该身份下跑一次whoami /groups看清楚实际权限,再单独给这个账号授予 inetsrv\config 目录的读取权限。不想动 ACL 的话,退一步改用 appcmd 路线:appcmd.exe list apppool /name:xxx输出 XML,进程外执行,权限门槛低不少,代价是解析文本、慢一点。我在权限收紧的环境里一般直接用 appcmd 版本,省事。
5.2 输出与退出码:被包装层吞掉的那一半
现象一:命令行里echo %ERRORLEVEL%明明是 2,Icinga 收到的却是 0。
原因:中间隔了一层.bat或 PowerShell 包装。批处理里最后一条语句如果不是exit /b,返回的是最后一条命令的退出码而不是你想要的;PowerShell 脚本里没写exit $LASTEXITCODE,脚本本身的退出码是 0。
解决:包装层最后显式写exit /b %ERRORLEVEL%或者exit $LASTEXITCODE。更彻底的办法是别包,NSClient++ 里直接指向 exe,少一层就少一个变量。
现象二:插件输出看起来正常,Icinga 报 "Plugin exited with return code 3" 但输出文本是空的。
原因:异常栈打到了 stderr,stdout 一个字都没有。NSClient++ 只把 stdout 的第一行当结果,stderr 里的内容不会出现在检查结果里。
解决:Main外面套一层 try/catch,把异常压成一行IIS UNKNOWN - <异常类型>: <消息>,然后返回 3。这样即使崩了,监控面板上也能看到原因,而不是一个没有上下文的花屏。
5.3 环境差异:从测试机搬到生产机才出现的两个坑
现象一:开发机上编译好的 exe,拷到生产机报Could not load file or assembly Microsoft.Web.Administration。
原因:生产机只装了 IIS 的 Web 服务器角色,没装管理工具,inetsrv目录下根本没有那个 dll;或者装了但项目文件里Private没关,输出目录带着一份版本不匹配的副本。
解决:目标机上先执行Test-Path $env:windir\System32\inetsrv\Microsoft.Web.Administration.dll,返回 False 就说明缺组件,去补装管理组件,或者把同版本的 dll 放到 exe 同目录。顺手确认一下位数:64 位进程读 System32,32 位进程读 SysWOW64,两个目录都得有。
现象二:输出里的中文站点名在 Icinga 上显示成乱码,或者整行被截断。
原因:控制台默认编码在中文 Windows 上是 GBK,而下游按 UTF-8 解码;也可能是输出带了 BOM,第一个字段名被污染。
解决:插件启动时把输出编码显式设成不带 BOM 的 UTF-8,即Console.OutputEncoding = new UTF8Encoding(false),注意是new UTF8Encoding(false)而不是Encoding.UTF8,后者的区别就在那个 BOM。实在改不了代码,用一层chcp 65001的批处理包一下也能顶,但会引入 5.2 里说的退出码问题,得一起处理。
5.4 一个够用的排查顺序
真出问题的时候别乱试,按这个顺序走一遍,基本能定位到层:
- 在目标机上用插件将要使用的那个账号跑一次 exe,拿到原始输出和退出码。
- 用
Test-Path确认Microsoft.Web.Administration.dll存在,并确认 exe 的位数。 - 在 NSClient++ 配置里用
nscp test跑一遍脚本注册,看参数有没有传进去。 - 从 Icinga 侧用
Check Now触发,对比返回的 Output 与第一步的输出是否一致。 - 不一致就往上找包装层,一致但状态不对就回来查阈值和排除名单。
这个顺序的价值在于每一步都有明确产出,不会出现“改了一圈不知道是哪一步生效了”的情况。
6. 进阶用法:把 perfdata 接进趋势图并做反向验证
状态类检查有个通病:它只告诉你现在好不好,不告诉你什么时候开始不好的。check_iis 输出的那一段 perfdata 就是补这个缺口的,把站点状态映射成数字之后送进时序库,翻车时间点、抖动频率、批量回收的影响范围都能直接看出来。我一般固定三个指标:site_state、pool_state、pool_count,前两个值域是 -1 到 1,第三个是这台机器上实际被检查到的池数量。
把状态类指标画成趋势图的时候,warn 和 crit 的取值要跟着映射表走,不能拍脑袋填:
| 指标名 | 含义 | 建议 warn | 建议 crit | 说明 |
|---|---|---|---|---|
site_state | 站点状态 | 1 | 1 | 低于 1 即异常 |
pool_state | 池状态 | 1 | 1 | 同上 |
pool_count | 纳管池数量 | 不设 | 不设 | 只做趋势,数量突变说明配置漂移 |
pool_count不上告警是有意的。它的作用是发现“某台机器上的池数量突然从 5 变成 2”,这通常意味着有人删了配置或者插件读取失败了一部分,属于配置漂移而不是服务故障,看一眼趋势图就能判断,不需要半夜打电话。
上线前我还固定做一次反向验证,故意制造三种情况,确认监控真的会响:
# 1) 停一个池,确认插件退出码变 2(记得验证完立刻启回来) Stop-WebAppPool -Name "ShopAppPool" & .\check_iis.exe --site "Shop" --apppool "ShopAppPool" Write-Host "EXIT_AFTER_STOP=$LASTEXITCODE" Start-WebAppPool -Name "ShopAppPool" # 2) 传一个不存在的站点名,确认是 3 而不是 2 & .\check_iis.exe --site "NoSuchSite" --apppool "ShopAppPool" Write-Host "EXIT_AFTER_TYPO=$LASTEXITCODE" # 3) 传一个非法参数,确认是 3 且有可读的错误文本 & .\check_iis.exe --apppool Write-Host "EXIT_AFTER_BADARG=$LASTEXITCODE"三次验证的意义在于把“告警真的会发出来”这件事从假设变成事实。监控系统最怕的不是配错,是配错了还没人知道,等到真出事的时候才发现这个服务从来没响过。第三项尤其容易被跳过,很多插件在参数缺失时直接抛未捕获异常,退出码是 3 但输出是空的,看起来像通过了。
从那以后我每接一台新的 windows-server,都会强制走一遍这个流程:先手动跑出退出码,再反向制造三种状态,最后才推进生产配置。看起来多花二十分钟,但它把“监控配好了”和“监控真的在工作”这两件事区分开了,后面能省掉的深夜排查远不止二十分钟。希望帮到你。
本文还有配套的精品资源,点击获取