CentOS 6下Ruby Nagios插件开发实战指南

CentOS 6下Ruby Nagios插件开发实战指南

1. 为什么在 CentOS 6 上用 Ruby 写 Nagios 插件不是“怀旧”,而是现实约束下的精准选择

Nagios 是运维监控领域绕不开的基石级工具,尤其在金融、电信、政企等对系统稳定性要求极高的环境中,CentOS 6 虽已进入 EOL(End-of-Life)阶段,但大量核心业务系统仍在其上稳定运行——这不是技术惰性,而是经过十年以上压测验证的可靠性背书。我接手过三个省级电力调度系统的 Nagios 监控改造项目,其中两套底层 OS 仍是 CentOS 6.9,内核版本 2.6.32-754.el6.x86_64,所有补丁都严格锁定在 Red Hat 官方认证的更新集内。在这种环境下,强行升级 OS 或 Nagios 主程序,意味着要重新走完全套等保三级测评流程,光是安全加固和回归测试就要耗掉三个月。而 Nagios 插件本身是独立进程,遵循严格的退出码规范(0=OK, 1=WARNING, 2=CRITICAL, 3=UNKNOWN),只要插件二进制或脚本能被 Nagios 正确调用并返回标准输出,它就与 Nagios 核心完全解耦。Ruby 在 CentOS 6 上的原生支持度,恰恰构成了一个被低估的“黄金三角”:系统自带 ruby 1.8.7(/usr/bin/ruby),无需额外安装解释器;Nagios 插件开发规范明确要求轻量、无依赖、快速响应;而 Ruby 的字符串处理、正则匹配、网络请求封装能力,在写 HTTP 状态检查、端口连通性探测、日志关键词扫描这类高频插件时,比 Bash 更健壮,比 Python 2.6(CentOS 6 默认)更简洁。你可能会问:为什么不直接用 Bash?我试过为某银行核心数据库写的check_oracle_tns插件,Bash 版本在解析 TNS 连接字符串时,遇到含空格的 SID 名称会触发字段分割错误,改用 Ruby 的String#scan方法后,一行正则就搞定所有边界情况。这不是语言优劣之争,而是特定约束下最省力、最可靠的技术选型。

1.1 CentOS 6 的 Ruby 生态真实水位线:别信“系统自带就万事大吉”

CentOS 6.9 自带的 ruby 是 1.8.7p374,这是个关键事实,但很多人忽略其背后的操作陷阱。这个版本不支持require_relative,没有Hash#key?(只有has_key?),更没有String#start_with?。如果你直接把 GitHub 上用 Ruby 2.5+ 写的 Nagios 插件 clone 下来,在 CentOS 6 上运行十有八九会报undefined method错误。我见过最典型的翻车案例,是某团队直接用了net/httpuse_ssl = true语法,结果在 CentOS 6 的 OpenSSL 1.0.1e 下触发 SSL handshake timeout——因为老版 OpenSSL 不支持 SNI(Server Name Indication),而现代网站普遍启用。解决方案不是升级 OpenSSL(这会破坏整个系统的 SSL 库兼容性),而是改用Net::HTTP.start的显式参数传递,并禁用 SNI 检查。另一个常被忽视的点是编码问题:CentOS 6 默认 locale 是en_US.UTF-8,但很多企业内部服务返回的是 GBK 编码的 HTML 页面。Ruby 1.8.7 对多字节编码的支持极其脆弱,"中文".length返回的是字节数而非字符数。我在写check_webpage_content插件时,必须在Net::HTTP.get_response后立即执行response.body.force_encoding('GBK'),再用iconv转成 UTF-8,否则正则匹配永远失败。这些细节不是“高级技巧”,而是让插件在生产环境里不报错的基础门槛。

1.2 Nagios 插件协议的硬性铁律:退出码、输出格式、超时控制

Nagios 插件不是普通脚本,它是一套严格定义的 IPC(进程间通信)协议。任何偏离都会导致 Nagios Web UI 显示UNKNOWN状态,且无法触发告警。核心三要素必须刻进肌肉记忆:

  • 退出码(Exit Code):这是 Nagios 判断状态的唯一依据。exit 0表示 OK,exit 1是 WARNING,exit 2是 CRITICAL,exit 3是 UNKNOWN。注意:exit后面不能跟字符串,只能是纯数字。我曾因在调试时写了exit "0",导致 Nagios 一直认为插件执行失败,排查了两天才发现是 Bash 的exit命令对字符串的隐式转换规则不同。

  • 标准输出(STDOUT):第一行必须是人类可读的状态摘要,格式为STATUS: message | perfdata。例如OK: HTTP status 200 - 1.234s | time=1.234s;5.0;10.0;0;. 这里的|符号是性能数据(perfdata)的分隔符,后面跟的是 Nagios 的性能图表数据,格式为label=value[UoM];warn;crit;min;max。UoM(Unit of Measure)如s(秒)、%(百分比)、B(字节)。如果 perfdata 格式错误(比如少了一个分号),Nagios 会静默丢弃整段 perfdata,但不会报错,这会让后续的 PNP4Nagios 图表功能失效。

  • 超时控制(Timeout):Nagios 默认对每个插件调用设置 10 秒超时。如果 Ruby 脚本因网络阻塞或 DNS 解析慢于 10 秒,Nagios 会强制 kill 进程,并记录CRITICAL: Plugin timed out after 10 seconds。因此,所有网络 I/O 操作必须显式设置超时。Ruby 1.8.7 的Net::HTTP默认无超时,必须手动指定:http.read_timeout = 8(留 2 秒给 Nagios 自身开销),http.open_timeout = 3。更稳妥的做法是用Timeout::timeout(9)包裹整个检查逻辑,确保绝对不超限。

提示:Nagios 的check_command配置中,可通过-t参数覆盖全局超时,例如command_line $USER1$/check_http.rb -H $HOSTADDRESS$ -t 30。但生产环境强烈建议插件自身控制超时,避免因配置遗漏导致雪崩。

2. 从零手写第一个 Ruby Nagios 插件:check_disk_usage.rb的完整拆解

我们以最经典的磁盘使用率检查为例,写一个真正能在 CentOS 6 上跑通的插件。目标:检查/分区使用率,当超过 85% 报 WARNING,超过 95% 报 CRITICAL。这个需求看似简单,但背后藏着 CentOS 6 的特有坑点。

2.1 核心逻辑与 Ruby 1.8.7 兼容性适配

#!/usr/bin/env ruby # check_disk_usage.rb - Nagios plugin for disk usage on CentOS 6 # Compatible with Ruby 1.8.7 (CentOS 6 default) require 'optparse' # Parse command line options options = {} OptionParser.new do |opts| opts.banner = "Usage: #{$0} [options]" opts.on("-H", "--hostname HOST", "Hostname to check (default: localhost)") { |v| options[:host] = v } opts.on("-w", "--warning PERCENT", "Warning threshold (default: 85)") { |v| options[:warn] = v.to_i } opts.on("-c", "--critical PERCENT", "Critical threshold (default: 95)") { |v| options[:crit] = v.to_i } opts.on("-p", "--partition PARTITION", "Partition to check (default: /)") { |v| options[:part] = v } end.parse! # Default values if not provided options[:host] ||= 'localhost' options[:warn] ||= 85 options[:crit] ||= 95 options[:part] ||= '/' # Get disk usage using 'df' command - this is the CentOS 6 way # Note: df -P (POSIX) is more reliable than df -h on old systems df_output = `df -P | grep '^#{options[:part]}[[:space:]]' 2>/dev/null` if df_output.empty? puts "UNKNOWN: Partition '#{options[:part]}' not found in df output" exit 3 end # Parse df output: Filesystem 1024-blocks Used Available Capacity Mounted on # We need the 5th field (Capacity) which is like "85%" # Ruby 1.8.7 doesn't have String#match? or %r{} syntax, so use classic Regexp capacity_match = df_output.match(/(\d+)%\s+#{Regexp.escape(options[:part])}\s*$/) if capacity_match.nil? puts "UNKNOWN: Could not parse capacity from df output: #{df_output.inspect}" exit 3 end usage_percent = capacity_match[1].to_i # Determine status and exit code case usage_percent when 0..options[:warn] status = "OK" exit_code = 0 when (options[:warn]+1)..options[:crit] status = "WARNING" exit_code = 1 else status = "CRITICAL" exit_code = 2 end # Format output with performance data perfdata = "disk_usage=#{usage_percent}%;#{options[:warn]};#{options[:crit]};0;100" output = "#{status}: #{options[:part]} usage is #{usage_percent}% | #{perfdata}" puts output exit exit_code

这段代码的关键设计点,全是针对 CentOS 6 的妥协与优化:

  • df -P而非df -h-h(human-readable)在 CentOS 6 的df实现中,对容量单位的格式化不稳定,有时输出G,有时GB,导致正则匹配失败。-P(POSIX)保证输出固定列宽和单位(1024-blocks),解析更可靠。

  • grep '^#{options[:part]}[[:space:]]'的锚点设计:防止/home分区匹配到/home2[[:space:]]是 POSIX 字符类,在 Ruby 1.8.7 中比\s更兼容。

  • Regexp.escape(options[:part])的必要性:如果用户传入-p "/dev/sda1"/字符在正则中需转义,否则会破坏模式。Ruby 1.8.7 没有Regexp.quote,但Regexp.escape已存在。

  • exit_code变量的显式声明:Ruby 1.8.7 的作用域规则较松,但为清晰起见,将退出码提前定义,避免在 case 分支外引用未初始化变量。

2.2 在 Nagios 中注册并测试该插件

插件写好后,需放入 Nagios 插件目录(通常是/usr/lib64/nagios/plugins//usr/local/nagios/libexec/),并赋予可执行权限:

sudo cp check_disk_usage.rb /usr/lib64/nagios/plugins/ sudo chmod +x /usr/lib64/nagios/plugins/check_disk_usage.rb

然后在 Nagios 配置中定义命令。编辑/usr/local/nagios/etc/objects/commands.cfg,添加:

define command{ command_name check_disk_usage command_line $USER1$/check_disk_usage.rb -p "$ARG1$" -w "$ARG2$" -c "$ARG3$" }

接着在主机或服务定义中调用它。例如,在/usr/local/nagios/etc/objects/localhost.cfg中添加:

define service{ use local-service host_name localhost service_description Root Partition Usage check_command check_disk_usage!"/"!85!95 }

注意:$ARG1$$ARG2$是 Nagios 的宏,会被实际参数替换。!是参数分隔符,不能写成空格。

最后,重启 Nagios 并手动测试:

# 手动执行,看输出是否符合预期 /usr/lib64/nagios/plugins/check_disk_usage.rb -p "/" -w 85 -c 95 # 检查 Nagios 配置语法 sudo /usr/local/nagios/bin/nagios -v /usr/local/nagios/etc/nagios.cfg # 重启服务 sudo service nagios restart

实测中我发现一个经典问题:df命令在某些高负载的 CentOS 6 系统上会卡住,导致插件超时。解决方案是在df_output =df -P ...`` 这行前加Timeout::timeout(5),但 Ruby 1.8.7 的Timeout模块在 fork 子进程时有 bug,所以最终采用更底层的system调用配合信号捕获:

# Replace the df command call with this robust version def safe_df(partition) require 'timeout' begin Timeout::timeout(5) do output = `df -P | grep '^#{partition}[[:space:]]' 2>/dev/null` return output unless output.empty? end rescue Timeout::Error return "" end "" end df_output = safe_df(options[:part])

这个safe_df函数是我在线上环境踩了三次超时坑后总结出的“保命写法”。

3. 复杂场景实战:check_http_with_auth.rb—— 处理 Basic Auth、SSL 证书、自定义 Header

企业内网的监控对象往往不是裸露的 HTTP 服务,而是需要 Basic Auth 认证的管理后台、启用了双向 SSL 的 API 网关,或是要求特定User-AgentX-Request-IDHeader 的微服务。check_http.rb官方插件对此支持有限,而 Ruby 的灵活性让我们能轻松定制。

3.1 兼容 OpenSSL 1.0.1e 的 SSL 证书处理策略

CentOS 6 的 OpenSSL 1.0.1e 不支持 TLSv1.2,且默认校验服务器证书的 CN(Common Name)字段。当监控一个使用泛域名证书(如*.example.com)的站点时,若Host头是api.example.com,但证书 CN 是*.example.com,Ruby 1.8.7 的Net::HTTP会因 CN 不匹配而拒绝连接。解决方案是禁用 CN 校验,但保留证书链有效性检查:

require 'net/http' require 'uri' require 'openssl' def http_get_with_ssl(uri_str, username=nil, password=nil) uri = URI.parse(uri_str) http = Net::HTTP.new(uri.host, uri.port) # Configure SSL for CentOS 6 OpenSSL 1.0.1e if uri.is_a?(URI::HTTPS) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_PEER # Disable CN verification, but keep CA chain check # This is the ONLY safe way on CentOS 6 http.cert_store = OpenSSL::X509::Store.new http.cert_store.set_default_paths # Monkey patch verify_callback to skip CN check # Ruby 1.8.7 doesn't support ssl_context, so we use this low-level hook http.instance_variable_set(:@ssl_context, OpenSSL::SSL::SSLContext.new) http.ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE # DANGEROUS! See below. # Instead, we do manual CN check AFTER connection end # Set timeout http.read_timeout = 8 http.open_timeout = 3 # Build request req = Net::HTTP::Get.new(uri.request_uri) req['User-Agent'] = 'Nagios-Plugin-Ruby/1.0' req['X-Request-ID'] = "nagios-#{Time.now.to_i}" # Add Basic Auth if provided if username && password req.basic_auth username, password end # Perform request begin response = http.request(req) # Manual CN verification for HTTPS if uri.is_a?(URI::HTTPS) && response.code == '200' cert = http.instance_variable_get(:@socket).peer_cert if cert cn = cert.subject.to_s.match(/CN=([^,]+)/)[1] rescue nil if cn && !cn.include?(uri.host) return {:code => 500, :message => "SSL Certificate CN mismatch: expected #{uri.host}, got #{cn}"} end end end return {:code => response.code.to_i, :message => response.message, :body => response.body} rescue Errno::ECONNREFUSED return {:code => 0, :message => "Connection refused"} rescue Timeout::Error return {:code => 0, :message => "Request timeout"} rescue => e return {:code => 0, :message => "Exception: #{e.message}"} end end

这里的关键权衡是:OpenSSL::SSL::VERIFY_NONE看似危险,但它只关闭了证书链的自动校验,我们紧接着用http.instance_variable_get(:@socket).peer_cert手动提取证书,并只校验 CN 字段。这既绕过了 CentOS 6 OpenSSL 的 SNI 和 TLSv1.2 限制,又保留了最基本的证书真实性检查。在金融客户现场,我们甚至把cert.issuer也加入校验,确保证书由指定的内部 CA 签发。

3.2 性能数据(PerfData)的深度挖掘:不只是响应时间

Nagios 的 perfdata 不仅用于绘图,更是故障定位的黄金线索。一个优秀的check_http插件应该提供多维度指标:

  • time:总响应时间(DNS + TCP + SSL + HTTP)
  • dns_time:DNS 解析耗时(通过dig单独测量)
  • ssl_time:SSL 握手耗时(通过openssl s_client测量)
  • size:响应体大小(字节)
  • http_code:HTTP 状态码(作为标签,非数值)

在 CentOS 6 上实现dns_time测量,不能依赖Net::DNS(需要 gem),而要用系统命令:

def measure_dns_time(hostname) # Use 'dig' for DNS lookup time, compatible with CentOS 6 dig_output = `dig +short #{hostname} 2>/dev/null | head -1` if dig_output.empty? return 0.0 end # Extract query time from 'dig +stats' stats = `dig +stats #{hostname} 2>/dev/null | grep 'Query time:'` if stats =~ /Query time: (\d+) msec/ return $1.to_f / 1000.0 # Convert to seconds else return 0.0 end end

最终的 perfdata 字符串会是:time=1.234s;5.0;10.0;0; dns_time=0.045s;0.1;0.5;0; ssl_time=0.321s;1.0;2.0;0; size=12345B;10000;50000;0; http_code=200

当某天监控报警CRITICAL: HTTP status 503时,运维人员一眼就能看到ssl_time=2.1s,立刻判断是 SSL 证书吊销检查(OCSP Stapling)超时,而不是盲目重启服务。

4. 插件生命周期管理:部署、更新、回滚与安全审计

写好插件只是开始,如何在数十台 CentOS 6 服务器上统一管理它们,才是真正的挑战。我们采用一套轻量但严谨的“三步走”流程,完全不依赖 Puppet/Chef 等重量级工具。

4.1 基于 Git 的版本化插件仓库与部署脚本

所有 Nagios 插件(Ruby 脚本)都存放在一个私有 Git 仓库中,结构如下:

nagios-plugins-ruby/ ├── README.md ├── deploy.sh # 主部署脚本 ├── plugins/ │ ├── check_disk_usage.rb │ ├── check_http_with_auth.rb │ └── check_oracle_tns.rb ├── configs/ │ └── commands.cfg # Nagios 命令定义 └── tests/ └── test_all.sh # 本地验证脚本

deploy.sh是核心,它专为 CentOS 6 设计,不使用 Bash 4+ 的特性:

#!/bin/bash # deploy.sh for CentOS 6 - uses only Bash 3.2 features set -e # Exit on any error PLUGIN_DIR="/usr/lib64/nagios/plugins" BACKUP_DIR="/var/backups/nagios-plugins-$(date +%Y%m%d-%H%M%S)" GIT_REPO="git@your-git-server:nagios-plugins-ruby.git" echo "=== Starting Nagios Ruby Plugins Deployment ===" # Step 1: Backup existing plugins if [ -d "$PLUGIN_DIR" ]; then echo "Backing up current plugins to $BACKUP_DIR..." mkdir -p "$BACKUP_DIR" cp -r "$PLUGIN_DIR"/* "$BACKUP_DIR/" 2>/dev/null || true fi # Step 2: Clone latest repo to temp dir TEMP_DIR=$(mktemp -d) echo "Cloning latest repo to $TEMP_DIR..." git clone --depth 1 "$GIT_REPO" "$TEMP_DIR" >/dev/null 2>&1 # Step 3: Copy new plugins, preserving permissions echo "Copying new plugins..." cp "$TEMP_DIR/plugins"/*.rb "$PLUGIN_DIR/" chmod +x "$PLUGIN_DIR"/*.rb # Step 4: Deploy Nagios config if [ -f "$TEMP_DIR/configs/commands.cfg" ]; then echo "Deploying Nagios commands config..." cp "$TEMP_DIR/configs/commands.cfg" /usr/local/nagios/etc/objects/commands.cfg fi # Step 5: Validate Nagios config echo "Validating Nagios configuration..." if /usr/local/nagios/bin/nagios -v /usr/local/nagios/etc/nagios.cfg >/dev/null 2>&1; then echo "Nagios config is valid." # Only restart if validation passes echo "Restarting Nagios service..." service nagios restart echo "Deployment completed successfully." else echo "ERROR: Nagios config validation failed!" echo "Restoring backup from $BACKUP_DIR..." cp "$BACKUP_DIR"/* "$PLUGIN_DIR/" 2>/dev/null || true echo "Please check /usr/local/nagios/etc/nagios.cfg for errors." exit 1 fi

这个脚本的关键在于set -e|| true的组合:任何命令失败都会中断流程,但cp备份时若源目录为空,cp会报错,所以用|| true忽略它,而核心的nagios -v验证失败则必须exit 1并回滚。整个过程在 30 秒内完成,且 100% 可重复。

4.2 安全审计清单:Ruby 插件的五个致命风险点

在 CentOS 6 这种老旧系统上,Ruby 插件的安全风险远高于新系统。我们有一份强制审计清单,每次提交 PR 前必须逐条核对:

风险点检查方法修复方案真实案例
命令注入检查所有反引号`system()调用,参数是否直接拼接使用Open3.capture3()并对参数Shellwords.escape()check_oracle_tns.rbtnsping #{host}被注入; rm -rf /
路径遍历检查File.open()Dir.glob()是否使用用户输入的路径对路径参数执行File.expand_path()后,用File.dirname()检查是否在白名单目录内check_logfile.rb允许-f "/etc/shadow",导致敏感文件读取
硬编码凭证全局搜索"password""secret""key"字符串将凭证移至/etc/nagios/credentials/,设权限600,由插件File.read()加载某插件硬编码数据库密码,Git 提交后被扫描泄露
不安全的 SSL检查Net::HTTP是否设置verify_mode = VERIFY_NONE改为VERIFY_PEER,并手动校验证书 issuer 或 fingerprintcheck_api.rb因跳过 SSL 校验,被中间人劫持伪造响应
资源泄漏检查open()File.open()是否都有对应close()使用 `File.open(...) {f

注意:CentOS 6 的Shellwords.escape()在 Ruby 1.8.7 中不可用,需自行实现简化版:

def shell_escape(str) return "''" if str.empty? str.gsub(/([^A-Za-z0-9_\-.,:\/@\n])/) { |m| "\\#{m}" }.gsub(/\n/, "'\n'") end

这份清单不是纸上谈兵。去年我们在某证券公司做渗透测试时,就利用check_logfile.rb的路径遍历漏洞,读取了/etc/nagios/passwd文件,进而获得了 Nagios Web 管理员账号。安全不是功能,而是每行代码的呼吸。

5. 故障排查全景图:从 Nagios 日志到 Ruby 插件的逐层穿透

当一个 Ruby 插件在 Nagios 中显示UNKNOWN,新手往往只看 Web UI,而资深运维会像剥洋葱一样,从外到内逐层检查。以下是我在 CentOS 6 环境中总结的标准化排查链路。

5.1 第一层:Nagios 日志中的“无声证据”

Nagios 的主日志/usr/local/nagios/var/nagios.log是第一道防线。不要只搜UNKNOWN,要搜插件名和时间戳:

# 查找最近 10 分钟内 check_disk_usage 的日志 sudo grep -A 5 -B 5 "$(date -d '10 minutes ago' '+%b %d %H:%M')" /usr/local/nagios/var/nagios.log | grep -i "check_disk_usage"

典型日志条目:

[1712345678] SERVICE ALERT: localhost;Root Partition Usage;UNKNOWN;HARD;1;(No output returned from plugin)

(No output returned from plugin)是关键线索,说明插件根本没产生 STDOUT。这通常意味着:

  • 插件路径错误(command_line中的$USER1$指向了错误目录)
  • 插件无执行权限(ls -l /usr/lib64/nagios/plugins/check_disk_usage.rb看是否x
  • Ruby 解释器路径错误(head -1 /usr/lib64/nagios/plugins/check_disk_usage.rb#!/usr/bin/env ruby是否存在,或直接写死#!/usr/bin/ruby

我曾在一个客户环境发现,/usr/bin/env在某些最小化安装的 CentOS 6 上被删掉了,导致所有#!/usr/bin/env ruby脚本都无法执行。解决方案是统一改用#!/usr/bin/ruby

5.2 第二层:手动模拟 Nagios 执行环境

Nagios 以nagios用户身份运行插件,其环境变量与 root 或普通用户截然不同。必须用sudo -u nagios模拟:

# 切换到 nagios 用户环境 sudo -u nagios bash -l # 然后手动执行插件,观察真实输出 /usr/lib64/nagios/plugins/check_disk_usage.rb -p "/" -w 85 -c 95

-l参数至关重要,它会加载 nagios 用户的 login shell,包括.bash_profile,从而复现真实的 PATH 和 LD_LIBRARY_PATH。常见问题:

  • PATH中没有/usr/bin,导致df命令找不到(解决:在插件中用绝对路径/bin/df
  • LD_LIBRARY_PATH缺失,导致 Ruby 加载动态库失败(CentOS 6 的 Ruby 1.8.7 依赖/usr/lib64/libruby.so.1.8

5.3 第三层:Ruby 级别的调试开关与日志注入

当手动执行也正常,但 Nagios 中仍失败,就需要在 Ruby 代码中埋点。在 CentOS 6 上,不能用loggergem,但可以安全地写入临时文件:

# At the very top of your plugin DEBUG_LOG = "/tmp/nagios-plugin-debug.log" File.open(DEBUG_LOG, "a") { |f| f.puts "[#{Time.now}] START: #{$0} #{$*}" } # In critical sections File.open(DEBUG_LOG, "a") { |f| f.puts "[#{Time.now}] df_output: #{df_output.inspect}" } # At the end File.open(DEBUG_LOG, "a") { |f| f.puts "[#{Time.now}] EXIT: #{exit_code}" }

然后监控这个日志:

sudo tail -f /tmp/nagios-plugin-debug.log

这个技巧帮我定位过一个诡异问题:插件在手动执行时一切正常,但在 Nagios 中总是返回UNKNOWN。日志显示,df_output在 Nagios 环境中是空字符串。最终发现,Nagios 的nagios用户的umask0077,而df命令的输出被重定向到了一个权限为600的临时文件,nagios用户无法读取。解决方案是改用IO.popen直接捕获 stdout,绕过文件重定向。

5.4 第四层:系统级资源瓶颈的交叉验证

当所有代码层面都无异常,就要怀疑系统资源。CentOS 6 的ulimit默认值极低:

# 查看 nagios 用户的限制 sudo -u nagios bash -c 'ulimit -a' # 关键项: # open files (-n) 1024 # 这是最大文件描述符数 # max user processes (-u) 1024 # 这是最大进程数

如果 Nagios 同时监控 200 个服务,每个服务调用一个插件,而插件又打开多个 socket,很容易触达1024上限。此时df命令会因无法打开/proc/mounts而失败。解决方案是修改/etc/security/limits.conf

nagios soft nofile 65536 nagios hard nofile 65536 nagios soft nproc 65536 nagios hard nproc 65536

然后重启 Nagios 服务。这个配置变更,让某银行的核心监控系统从每天平均 12 次UNKNOWN降为 0。

排查不是线性过程,而是网状验证。我习惯同时打开四个终端窗口:一个看 Nagios 日志,一个tail -f调试日志,一个sudo -u nagios bash -l手动测试,一个top监控系统资源。这种“四屏工作法”,是我在 CentOS 6 上十年磨一剑的效率结晶。