Nagios是运维领域广泛使用的监控系统,其插件架构允许用任意可执行程序返回状态和性能数据。然而默认情况下,插件每次执行的结果不会被持久化,历史数据只能依赖外部工具或日志文件。当需要分析趋势、定位间歇性故障或生成容量报告时,缺乏结构化的历史数据会带来很大不便。将SQLite嵌入到Nagios插件中,可以在不额外部署数据库服务的前提下实现数据持久化,本文将通过一个实战项目展示如何做到这一点。

一、理解Nagios插件开发基础
Nagios插件本质上是一个可执行脚本或程序,它通过退出码和标准输出来报告检查结果。退出码有四种约定:0表示状态正常(OK),1表示警告(WARNING),2表示严重(CRITICAL),3表示未知(UNKNOWN)。标准输出的第一行是供管理员阅读的状态摘要,之后可以附加性能数据,性能数据以竖线(|)作为分隔符,格式为“标签=数值[UOM];[警告阈值];[严重阈值];[最小值];[最大值]”。例如一个检查磁盘使用率的插件可能输出:DISK OK - free space: / 3326 MB (45% inode=87%);| /=3326MB;2000;1000;0;8000。Nagios会解析这些内容,将状态码映射到不同颜色显示,并将性能数据交给绘图工具处理。
一个最简单的bash插件可能如下所示。它不涉及任何数据库操作,只是演示输出规范。
#!/bin/bash
# 简单检查 /tmp 目录是否存在
if [ -d /tmp ]; then
echo "TMP OK - /tmp exists"
exit 0
else
echo "TMP CRITICAL - /tmp missing"
exit 2
fi
在实际项目中,插件往往需要执行更复杂的检查逻辑,比如运行系统命令、解析输出、计算阈值等。这些逻辑用Python、Perl或Ruby等语言实现会更加方便。下面的章节我们将采用Python,因为它自带SQLite支持,无需额外安装依赖。
二、SQLite在插件中的角色与数据模型设计
SQLite是一款开源的嵌入式关系型数据库,整个数据库就是一个独立的文件,不需要独立的服务进程。它支持标准的SQL语法,包括事务、索引、触发器等,非常适合作为单机监控数据的存储后端。在Nagios插件中集成SQLite,可以让每次执行检查时自动追加一条记录,记录时间戳、主机名、服务名、检查结果值以及状态码。这样,只要插件持续运行,就能积累起一份完整的历史数据集。
设计数据模型时,考虑通用性非常重要。一个合理的表结构应该能够容纳不同类型的监控指标,而不仅仅针对某一项检查。下面是一个推荐的建表语句,它使用了一个自增主键和几个关键字段,并为时间与主机服务组合建立索引以加速查询。
CREATE TABLE IF NOT EXISTS check_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
check_time TEXT NOT NULL,
host_name TEXT NOT NULL,
service_name TEXT NOT NULL,
status_code INTEGER NOT NULL,
metric_value REAL,
metric_unit TEXT,
output TEXT
);
CREATE INDEX IF NOT EXISTS idx_check_history_time ON check_history(check_time);
CREATE INDEX IF NOT EXISTS idx_check_history_host_service ON check_history(host_name, service_name);
这里check_time使用文本类型存储ISO 8601格式的时间字符串,例如“2025-04-01T10:30:00”,便于阅读和排序。metric_value用于存储数值型性能数据,如果某些检查没有数值可以留空。output保存插件的完整输出文本,方便日后审计。索引的建立可以显著提高按时间范围检索和按主机服务筛选的速度,尤其当数据量增长到几十万行之后。
选择SQLite而不是其他数据库的原因在于它零维护、零部署成本。对于单台Nagios服务器管理几百个主机的情况,SQLite完全能够应对写入和查询负载。后续如果需要扩展到多台采集节点,再考虑集中式数据库也不迟。
三、实战:开发一个带SQLite存储的Nagios插件
接下来我们实现一个具体的插件:检查根分区的磁盘使用率,将使用百分比和剩余空间写入SQLite数据库,并按照Nagios规范输出结果。这里使用Python标准库的shutil获取磁盘信息,sqlite3操作数据库,argparse处理命令行参数。
插件的主要逻辑分为三个步骤:执行检查、写入数据库、输出Nagios格式。执行检查部分使用shutil.disk_usage获取总容量、已用空间和可用空间;写入数据库部分打开SQLite连接,插入一条记录;输出部分根据设定的阈值判断状态码,打印摘要和性能数据。完整代码如下:
#!/usr/bin/env python3
import argparse
import sqlite3
import shutil
import datetime
import sys
DB_PATH = "/var/lib/nagios/check_history.db"
def init_db():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS check_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
check_time TEXT NOT NULL,
host_name TEXT NOT NULL,
service_name TEXT NOT NULL,
status_code INTEGER NOT NULL,
metric_value REAL,
metric_unit TEXT,
output TEXT
)
""")
cur.execute("CREATE INDEX IF NOT EXISTS idx_check_time ON check_history(check_time)")
conn.commit()
conn.close()
def main():
parser = argparse.ArgumentParser(description='Check disk usage with SQLite storage')
parser.add_argument('-w', '--warning', type=int, default=80, help='Warning threshold in percent')
parser.add_argument('-c', '--critical', type=int, default=90, help='Critical threshold in percent')
parser.add_argument('-p', '--path', default='/', help='Mount point to check')
args = parser.parse_args()
# 获取磁盘使用情况
total, used, free = shutil.disk_usage(args.path)
used_percent = (used / total) * 100
# 确定Nagios状态码
if used_percent >= args.critical:
status_code = 2
status_text = "CRITICAL"
elif used_percent >= args.warning:
status_code = 1
status_text = "WARNING"
else:
status_code = 0
status_text = "OK"
# 写入SQLite
init_db()
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
now = datetime.datetime.now().isoformat()
cur.execute(
"INSERT INTO check_history (check_time, host_name, service_name, status_code, metric_value, metric_unit, output) VALUES (?, ?, ?, ?, ?, ?, ?)",
(now, "localhost", "disk_usage", status_code, used_percent, "%", f"{status_text} - {args.path} usage {used_percent:.1f}%")
)
conn.commit()
conn.close()
# 输出Nagios格式
print(f"DISK {status_text} - {args.path} usage {used_percent:.1f}% | usage={used_percent:.1f}%;{args.warning};{args.critical};0;100")
sys.exit(status_code)
if __name__ == "__main__":
main()
这个插件可以在Nagios配置中直接调用,例如:check_disk_sqlite.py -w 80 -c 90 -p /。每次Nagios执行该命令,就会自动向SQLite数据库追加一条记录。管理员随后可以使用任意SQL客户端查询历史数据,比如统计过去24小时内磁盘使用率超过90%的次数:SELECT COUNT(*) FROM check_history WHERE status_code = 2 AND check_time > datetime('now', '-1 day');。
需要注意,插件中的数据库路径必须对Nagios运行用户可写。通常Nagios以nagios用户运行,因此需要预先创建目录并赋予权限:mkdir -p /var/lib/nagios && chown nagios:nagios /var/lib/nagios。另外,如果同一时刻有多个插件进程同时写入,SQLite默认的锁机制可能会导致短暂的写入延迟,但单机监控场景下冲突很少发生。
四、生产环境优化与错误处理
随着数据不断累积,SQLite数据库文件会不断增长,如果不加控制可能占用大量磁盘空间。一个实用的策略是定期清理旧数据,例如只保留最近90天的记录。可以通过在插件中加入删除逻辑,或者使用外部定时任务执行DELETE FROM check_history WHERE check_time < datetime('now', '-90 days');然后运行VACUUM回收空间。更优雅的方式是利用SQLite的触发器或分区表,但普通场景下定期清理已经足够。
对于写入性能,SQLite默认使用DELETE日志模式,每次写入都需要同步磁盘。开启WAL(Write-Ahead Logging)模式可以显著提升并发读写能力,并减少磁盘I/O。在初始化数据库时执行PRAGMA journal_mode=WAL;即可。WAL模式会额外创建两个文件(-wal和-shm),备份时需要一并处理。
错误处理也不容忽视。如果SQLite数据库文件损坏或权限错误,插件应该返回UNKNOWN状态而不是崩溃。上述代码中的init_db和插入操作没有捕获异常,生产环境应改用try-except包裹,并在异常时输出错误信息并退出状态码3。此外,当磁盘使用率计算失败(例如挂载点不存在)时,也应捕获OSError并返回UNKNOWN。这样监控系统就不会因为一个插件的内部错误而误报服务状态。
另一个优化点是避免每次插入都打开和关闭数据库连接。对于高频率检查(比如每分钟一次),频繁的开关连接会带来不必要的开销。可以在插件中复用连接,或者采用单例模式。不过考虑到Nagios的调度周期一般不低于1分钟,这种开销通常可以接受。如果确实需要极高频率的监控,可以考虑使用一个常驻进程收集数据,而不是由Nagios直接调用插件。
总之,SQLite为Nagios插件提供了一种轻量且可靠的本地持久化方案。通过合理设计表结构、优化数据库参数并做好错误处理,可以在不增加运维复杂度的前提下,让监控数据发挥更大价值。这个实战项目也为其他需要嵌入式存储的监控场景提供了可借鉴的思路。