80、数据库监控系统:指标采集(QPS/TPS/连接数)、告警阈值、Grafana集成
数据库监控这事儿,说实话,我早年吃过不少亏。那时候项目上线,数据库跑着跑着突然就挂了,查日志才发现QPS早就飙到天上去了,连接数也爆了。从那以后,我养成了一个习惯——任何生产环境的数据库,必须先搭好监控。今天咱们就聊聊,怎么用C语言写一个轻量级的数据库监控系统,把QPS、TPS、连接数这些核心指标抓出来,再配上告警和Grafana展示。
一、指标采集:QPS、TPS、连接数
先说说QPS和TPS。QPS是每秒查询数,TPS是每秒事务数。这两个指标,说白了就是看数据库忙不忙。我一般用gettimeofday()配合计数器来实现,每秒钟统计一次差值。
连接数就更直接了——每次客户端连上来就+1,断开就-1。但有个坑:连接泄漏。我曾经遇到过一个服务,连接池没释放,连接数一路涨到数据库上限,直接拒绝服务。所以监控连接数,其实是在监控你的代码质量。
核心思路:用三个全局变量做计数器,一个定时器每秒触发一次,计算差值并记录。
// 监控指标结构体
typedef struct {
long long qps; // 每秒查询数
long long tps; // 每秒事务数
int conn_count; // 当前连接数
int conn_max; // 历史最大连接数
double avg_latency_ms; // 平均延迟(毫秒)
} db_metrics_t;
// 全局计数器(实际项目中建议用原子操作)
static volatile long long g_query_count = 0;
static volatile long long g_trans_count = 0;
static volatile int g_conn_count = 0;
// 每次查询/事务时调用
void inc_query_count() {
__sync_fetch_and_add(&g_query_count, 1);
}
void inc_trans_count() {
__sync_fetch_and_add(&g_trans_count, 1);
}
// 连接建立/断开时调用
void on_connect() {
__sync_fetch_and_add(&g_conn_count, 1);
}
void on_disconnect() {
__sync_fetch_and_add(&g_conn_count, -1);
}
小技巧:用__sync_fetch_and_add做原子操作,比加锁轻量得多。多线程环境下不会出现计数错乱。
二、定时采集与阈值告警
采集不能只靠手动调用,得有个定时器。我习惯用setitimer()或者timerfd_create(),每秒触发一次。在回调函数里,把上一秒的计数差值算出来,就是QPS/TPS。
告警阈值这块,我踩过坑。一开始我设了固定值,比如QPS超过10000就告警。结果业务高峰期天天报警,运维都麻木了。后来我改成动态阈值——根据过去5分钟的平均值,超过2倍标准差才告警。这样误报少了很多。
// 定时采集回调(每秒执行一次)
void collect_metrics(int signum) {
static long long last_query = 0;
static long long last_trans = 0;
long long cur_query = g_query_count;
long long cur_trans = g_trans_count;
db_metrics_t metrics;
metrics.qps = cur_query - last_query;
metrics.tps = cur_trans - last_trans;
metrics.conn_count = g_conn_count;
// 更新历史最大值
static int max_conn = 0;
if (metrics.conn_count > max_conn) {
max_conn = metrics.conn_count;
}
metrics.conn_max = max_conn;
// 告警判断
if (metrics.qps > QPS_THRESHOLD) {
log_alert("QPS 超过阈值: %lld > %d", metrics.qps, QPS_THRESHOLD);
}
if (metrics.conn_count > CONN_THRESHOLD) {
log_alert("连接数超过阈值: %d > %d", metrics.conn_count, CONN_THRESHOLD);
}
// 写入日志或发送到Grafana
write_metrics_to_file(&metrics);
last_query = cur_query;
last_trans = cur_trans;
}
注意:定时器回调里不要做耗时操作,比如写数据库、发网络请求。我一般只做计数和简单判断,把日志写入放到另一个线程里处理。
三、数据输出与Grafana集成
采集到的数据怎么给Grafana?常见的方式有两种:
- 写日志文件:用
fprintf()按固定格式写入,然后让Prometheus的node_exporter或者Grafana的日志插件去采集。 - 直接暴露HTTP接口:用libmicrohttpd或者自己写个简单的socket监听,返回JSON格式的指标数据。
我个人更推荐第二种。为什么呢?因为日志文件有轮转问题,采集延迟也大。直接暴露HTTP接口,Grafana可以实时拉取,延迟在毫秒级。
// 输出JSON格式的指标数据(供Grafana拉取)
void metrics_to_json(char *buf, size_t size) {
db_metrics_t m = get_current_metrics(); // 获取最新指标
snprintf(buf, size,
"{"
"\"qps\":%lld,"
"\"tps\":%lld,"
"\"conn_count\":%d,"
"\"conn_max\":%d,"
"\"avg_latency_ms\":%.2f"
"}",
m.qps, m.tps, m.conn_count, m.conn_max, m.avg_latency_ms
);
}
// 简单的HTTP响应处理
void handle_http_request(int client_fd) {
char json[256];
metrics_to_json(json, sizeof(json));
char response[512];
snprintf(response, sizeof(response),
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %zu\r\n"
"\r\n"
"%s",
strlen(json), json
);
write(client_fd, response, strlen(response));
close(client_fd);
}
Grafana配置要点:
- 数据源选择「Prometheus」或「JSON API」
- 如果是JSON API,需要安装Grafana的JSON数据源插件
- 面板类型选「Stat」或「Graph」,展示QPS/TPS/连接数
- 告警规则可以在Grafana里配置,也可以直接在C代码里触发
四、避坑指南与实战经验
做监控系统,有几个坑我不得不提:
- 计数器溢出:QPS如果达到百万级,32位int很快会溢出。一定要用
long long或者uint64_t。 - 时间同步问题:如果你用多台机器采集,时间不同步会导致数据对不上。建议统一用NTP同步。
- 监控本身的开销:采集代码不能影响业务性能。我见过有人用
printf打日志,结果监控线程把CPU吃满了。建议用异步写入。 - 告警风暴:阈值设置不合理,或者网络抖动,可能会触发大量告警。我一般加一个「静默期」——同一个指标5分钟内只告警一次。
我的习惯:在代码里加一个#define MONITOR_DEBUG 1开关,开发阶段打开可以看到详细日志,上线后关掉只保留关键告警。这样既方便调试,又不会影响性能。
最后说一句,监控系统不是搭完就完事了。你得定期看看告警日志,调整阈值。我每个月都会回顾一次监控数据,看看有没有异常模式。数据库监控,说白了就是给你的数据库装个「心电图」,平时看着正常,一旦波形不对,马上就能发现。
公众号:蓝海资料掘金营,微信deep3321