1. 博客系统跑挂了,我却只能靠猜:为什么ZrLog需要一套监控体系
熟悉ZrLog的朋友都知道,它是一款基于Java的轻量级博客系统,部署简单、开箱即用,一个Spring Boot应用加一个MySQL就能跑起来。但正因为"简单起步"的特性,很多人对它的运维投入是严重不足的——我自己也是这样。早期博客放在一台单机上,偶尔卡一下、慢几秒,重启一下也就过去了。直到有一天首页直接打不开,登进服务器一看,Java进程CPU已经跑满了,内存快耗尽,MySQL的连接数高得吓人。关键是,我完全不知道这是什么时候开始的、是哪个请求触发的、系统资源是怎么一步步被耗尽的。
那是一次印象深刻的"火场逃生",全程靠盲猜和乱试,花了大半天才定位到一个递归查询接口在特定文章页面被循环调用。这之后我意识到,博客再小、访问量再低,只要是跑在公网上的服务,就得有一套"事前可观测、事后可追溯"的监控体系。尤其当架构升级为高可用模式,前置Nginx负载均衡、后端双ZrLog实例、MySQL独立部署之后,光靠眼睛盯已经完全不现实了。三个系统之间的依赖关系、每个节点的健康状态、JVM内部的内存和GC行为、数据库的连接和慢查询情况,这些指标不量化上墙,运维就永远是在打盲拳。
我最终选定的方案就是Prometheus加Grafana。Prometheus负责采集和存储时序指标,Grafana负责把指标变成可视化的图表和告警面板,这套组合是当前开源监控领域最成熟、社区案例最多的路线。这篇文章会完整记录我从零搭建ZrLog高可用架构监控系统的全过程,包括架构设计、组件安装、核心指标配置、告警规则编写以及这半年跑下来踩过的坑。如果你也在用ZrLog或者类似的小型Java应用,这篇文章应该能帮你少走不少弯路,至少能让你在下一次故障到来之前,先把"眼睛"装好。
正文部分我尽可能按照"动手搭一遍"的顺序来写,但会在关键节点停下来解释为什么这样做,而不是单纯堆命令。这年头优质的中文博客已经够少了,希望大家不光是把ZrLog用起来,也能把它运维得明明白白。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 监控目标拆解:高可用架构下,到底有哪些东西必须盯着
动工之前先把需求理清楚。很多人上来就装Prometheus,装上之后却不知道配什么指标,最后监控面板上只有一堆默认图。我的经验是,先想清楚监控对象和指标分层,再去做安装配置,效率高得多。
2.1 ZrLog高可用架构下的一天:从用户请求到数据库查询
先简单画一下我的部署结构。用户访问博客,请求先到Nginx,Nginx根据负载策略把请求转发到两个ZrLog实例(实际是同一套代码跑在两个端口,后端连同一个MySQL)。ZrLog本身负责文章渲染、页面生成、评论交互等业务逻辑,而这些逻辑最终都要落到MySQL的读写上。生产环境里再加一层Redis做缓存,但有些静态页面是ZrLog直接渲染的,缓存只覆盖部分高频查询。
从用户请求到数据库查询,中间任何一环出问题,表现都是"博客打开慢"。但慢的根因可能完全不同:可能是Nginx连接数被打满,可能是两个ZrLog实例其中一个假死导致请求全部堆积到另一个,可能是JVM在做Full GC导致停顿,也可能是MySQL的慢查询堵住了连接池。没有监控的时候,这些问题看起来都是"博客卡了",只能一台一台机器上去排查。有了监控之后,每一层的关键指标都会被独立记录,慢在哪一层一目了然。
2.2 四个层次的监控指标:系统、JVM、数据库、外部探测
我最终把监控对象分成四个层面:
| 层面 | 采集方式 | 核心指标 |
|---|---|---|
| 系统层 | node_exporter | CPU使用率、内存、磁盘空间、网络IO、inode使用量 |
| 应用层(JVM) | Spring Boot Actuator + Micrometer | 堆内存、GC暂停时间、线程数、HTTP请求延迟和QPS |
| 数据库层 | mysqld_exporter | 连接数、慢查询数、缓冲池命中率、主从延迟 |
| 外部探测 | blackbox_exporter | 首页响应码、HTTPS证书到期天数、TCP连通性 |
四个层面各有各的价值,缺一不可。系统层解决的是"机器资源够不够"的问题,应用层解决的是"代码跑得健康不健康"的问题,数据库层解决的是"数据读写有没有成为瓶颈"的问题,外部探测解决的是"用户视角下服务到底通不通"的问题。
我见过不少人只装了node_exporter,CPU和内存都有监控了就觉得万事大吉。但对Java应用来说,JVM的GC行为和堆内存趋势才是更需要关注的,因为很多Java应用的死法不是机器资源耗尽,而是在某个瞬间堆内存撑爆,或者因为GC停顿导致请求全部超时。而数据库层的慢查询监控,则是定位接口变慢最直接的线索。所以我建议,如果你打算把监控体系搭起来,这四个层面至少要做到系统层和应用层,数据库和外部探测视情况逐步加上。
3. Prometheus本体安装:能跑起来很简单,跑得稳需要抠细节
Prometheus的安装本身并不复杂,官方提供了二进制包和Docker镜像两种主流方式。但"能跑起来"和"跑得稳"是两回事,我在这里把我实际采用的方式和遇到过的问题一并说清楚。
3.1 用systemd托管二进制版Prometheus,而不是直接跑Docker
我的环境是两台4核8G的云服务器,一台跑Nginx加两个ZrLog实例,另一台跑MySQL和监控组件。最初图省事,直接用Docker Compose把Prometheus和Grafana拉起来,运行了大概两周,发现Docker容器里的Prometheus经常出现内存缓慢爬升的迹象,虽然不影响使用,但排查起来多一层容器隔离的环境。后来我干脆把Prometheus改成二进制部署,用systemd做守护,实测下来内存稳定很多,日志管理也更顺手。
决定用二进制部署的另一个考虑是:监控系统本身不能太依赖容器环境。如果你的生产环境也是一个极简的云服务器,Docker都不是必装组件,那多装一个Docker纯粹为了跑监控反而显得多余。直接下载官方二进制包,写一个systemd unit文件,开机自启、崩溃自拉,这套方案最轻、最稳。
bash复制# 下载Prometheus官方二进制包,这里以2.53.0版本为例
wget https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz
tar xzf prometheus-2.53.0.linux-amd64.tar.gz
mv prometheus-2.53.0.linux-amd64 /opt/prometheus
解压出来之后,目录里有一个prometheus主程序和一个promtool工具。promtool是个好东西,后面配置告警规则或者校验配置文件的语法时,都得靠它来检查。
systemd配置文件的写法大概是这样的:
ini复制[Unit]
Description=Prometheus Server
Documentation=https://prometheus.io/docs/introduction/overview/
After=network-online.target
[Service]
Type=simple
User=prometheus
Group=prometheus
ExecStart=/opt/prometheus/prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus \
--storage.tsdb.retention.time=30d \
--web.enable-lifecycle
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=multi-user.target
注意这里面的几个参数,每一个都有讲究。--storage.tsdb.path决定了时序数据的存储位置,一定要放到独立的数据盘,别和系统盘混在一起,否则监控数据写满磁盘会导致整个系统崩掉。--storage.tsdb.retention.time=30d是数据保留时间,博客系统的量级一天大概也就几十MB到几百MB,保留30天非常宽裕。--web.enable-lifecycle这个参数要特别提一下,它允许通过API热加载配置文件,这样每次改完prometheus.yml不用重启进程,执行一条curl -X POST http://localhost:9090/-/reload就能生效,对后调试非常有用。
提示:在生产环境,最好给Prometheus单独建一个系统用户,不要用root直接跑。我见过不少人图省事,所有进程全部root运行,一旦Prometheus被攻击者拿到shell,整个服务器就沦陷了。单独建用户、限制目录权限,安全性会好很多。
3.2 prometheus.yml配置:全局参数与四个采集目标
Prometheus是否正常工作,核心在配置文件。我的prometheus.yml长这样:
yaml复制global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'zrlog-monitoring'
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['localhost:9100']
- job_name: 'zrlog-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['127.0.0.1:8082', '127.0.0.1:8083']
- job_name: 'mysql-exporter'
static_configs:
- targets: ['localhost:9104']
下面逐个解释。scrape_interval是全局采集间隔,我设15秒,意味着Prometheus每15秒会向所有配置的target发起一次HTTP请求拉取指标。这个值对博客场景来说足够,既能保证数据的及时性,又不会给服务器造成太大负担。evaluation_interval是告警规则的评估间隔,也设15秒,和采集间隔保持一致,方便排查。
job_name是分组名称,同一个job下的所有target会被视为同一类服务。我在zrlog-app这个job里配了两个地址,对应ZrLog的两个实例端口。这里有个很重要的细节:metrics_path设成了/actuator/prometheus,这是Spring Boot Actuator暴露Prometheus格式指标的默认路径,不配的话Prometheus会默认去抓/metrics,那就会返回404,导致这个job的采集状态一直是红色的DOWN。
3.3 验证Prometheus状态:Target页面和命令行工具
配置文件写好后,用promtool校验一下语法:
bash复制/opt/prometheus/promtool check config /etc/prometheus/prometheus.yml
输出SUCCESS后启动服务,打开http://<服务器IP>:9090/targets,就能看到所有配置的采集目标。状态列如果是绿色的UP,说明Prometheus已经成功采集到指标;如果是红色的DOWN,点进去能看到具体的报错信息,比如超时、404、连接拒绝等。
我调试的时候习惯先在Prometheus自带的Graph页面上验证指标是否存在。比如输入up,执行查询,返回的结果里会有一组时间序列,每个target一个点,值为1表示正常采集、0表示采集失败。这一步确认无误之后,再进到下一步的Grafana,能省掉很多后面来回跳转查问题的麻烦。
4. JVM与应用层指标采集:ZrLog是Java应用,这个必须单独讲
Spring Boot应用暴露Prometheus指标,标准做法是引入Micrometer的Prometheus注册表依赖,配合Actuator自动把JVM、HTTP、日志等指标暴露出来。ZrLog本身是基于Spring Boot开发的,我检查了一下,它的依赖里已经包含了Actuator和Micrometer的基建,只是默认没有暴露Prometheus端点,需要在配置层面打开。
4.1 让Spring Boot Actuator输出Prometheus格式指标
在ZrLog的application.yml里加上这样一段:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
metrics:
export:
prometheus:
enabled: true
改完配置重启ZrLog,访问http://127.0.0.1:8082/actuator/prometheus,如果能看到一堆jvm_开头、http_server_requests_开头的指标文本,说明集成就成功了。
Micrometer这套机制其实挺巧妙的。Spring Boot应用内部运行时的状态——堆内存、GC次数、线程数、HTTP请求计数和耗时分布——被统一采集抽象成了Meter,再通过Prometheus的Registry接口输出成一套标准格式。也就是说,你的应用不需要感知Prometheus的存在,它只是暴露一个HTTP端点,Prometheus按固定周期来拉取。应用是被动方,监控系统是主动方,这种拉取(pull)模式的好处是,监控系统自己掌握采集节奏,不会因为应用侧繁忙而堆积请求。
依赖方面,理论上是需要加micrometer-registry-prometheus的:
xml复制<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
不过不同版本的Spring Boot对Micrometer的兼容性处理不一样,如果你的ZrLog版本比较老,建议先确认一下自带Micrometer版本和Prometheus注册表的兼容性。我遇到过的情况是,不加依赖时/actuator/prometheus端点根本不存在,加了之后才有输出。
4.2 我长期盯着的5个JVM核心指标
有了指标数据源,接下来就是选哪些指标放到监控面板上。JVM相关的指标非常多,Micrometer默认暴露的就有几十个,但实际运维中真正高频用到的其实就那么几个,我逐个解释它们的含义和用途。
- jvm_memory_used_bytes:JVM使用的内存量,按内存区域分开。我主要看
heap(堆内)部分,这个指标能直观反映博客应用的内存水位。如果堆内存持续走高不回落,说明可能有内存泄漏或者缓存没有释放。 - jvm_gc_pause_seconds:GC暂停时间。这个指标是分桶的,通常配合
rate()函数看每秒GC暂停耗时。如果Full GC的频率突然增加,每次停顿都超过1秒,用户访问时就能明显感到卡顿,这往往是应用假死的前兆。 - http_server_requests_seconds_max:最近一次的HTTP请求处理耗时。注意,这个是最大值,单看容易受到偶发慢请求的影响,所以我通常配合
histogram_quantile计算P99延迟,看的是大多数请求的耗时水平。 - jvm_threads_live_threads:存活线程数。如果线程数持续增长,大概率是线程池没有正确回收,也就是所谓的线程泄漏,这种问题在长时间运行的Java应用里一点也不罕见。
- jvm_classes_loaded_classes:已加载的类数量。这个指标在排查类加载器泄漏时非常有用,正常情况加载类数量应该稳定在一个水平,如果持续上升,可能是在反复部署或者某个框架的类加载器没有释放。
来看几个我实际用到的查询表达式。第一个是"博客首页的QPS":
promql复制sum(rate(http_server_requests_seconds_count{uri="/"}[5m]))
_count是计数器类型的指标,统计的是总共处理了多少个请求,rate函数把它换算成每秒平均QPS。第二个是"P99响应延迟":
promql复制histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le))
这个表达式稍微复杂一点,但理解了也不难:_bucket是延迟直方图的分桶计数,histogram_quantile根据这些分桶估算出分位数值。P99延迟大于2秒的时候,基本就能确定用户体验已经明显变差了,这时候再去看对应的系统资源指标,往往能找到根因。
4.3 一次真实故障:JVM Full GC导致首页卡顿的定位过程
这里分享一次让我对JVM监控彻底改观的真实经历。有一次博客突然变得非常卡,首页加载要十几秒,Nginx层面看上游响应超时,但我登录服务器看CPU和内存,使用率都不高,完全看不出异常。当时的我如果没有JVM监控,大概率又要开始重启大法了。
但这次不一样。我打开Grafana的JVM面板,发现老年代(Old Gen)内存曲线在故障时间点附近直线飙升,同时jvm_gc_pause_seconds的Full GC计数明显上涨,每次都停顿了2秒以上。顺着时间线往前推,发现老年代内存飙升的前几分钟,访问量有一个小幅波动,触发了某个页面的缓存清理逻辑。继续查代码,发现这个缓存清理逻辑里有一个对文章列表的递归查询,每个分类下上千篇文章都会走一次数据库全表查询,结果全部加载到内存里构建树结构。文章少的时候没问题,文章累计到一定数量后,一次清理就能把老年代撑爆。
定位到根因之后,修复其实很简单,给递归查询加了个深度限制,同时优化了缓存清理的策略。但如果没有JVM监控,这种问题可能要在"卡了重启、重启好了、过几天又卡"的循环里折腾好几个星期。监控的意义就在于此——它把看不到的运行时状态变成一条条曲线,让问题从"凭感觉猜"变成"用数据找"。
5. 系统资源与MySQL监控:让node_exporter和mysqld_exporter各司其职
应用层的监控解决的是"代码层的问题",但代码跑在系统之上、数据落在数据库里,这两层的健康同样决定博客的稳定性。所以我单独拿出一节来写系统层和数据库层的监控配置。
5.1 node_exporter部署:CPU、内存、磁盘、网络一个都不能少
node_exporter是Prometheus官方提供的系统指标采集器,装在每一台需要监控的服务器上,启动后监听9100端口,把操作系统的CPU、内存、磁盘、网络等指标以Prometheus格式暴露出来。部署方式同样是二进制加systemd:
bash复制wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
tar xzf node_exporter-1.8.2.linux-amd64.tar.gz
mv node_exporter-1.8.2.linux-amd64 /opt/node_exporter
node_exporter的systemd配置比Prometheus简单很多:
ini复制[Unit]
Description=Node Exporter
After=network.target
[Service]
Type=simple
User=node_exporter
ExecStart=/opt/node_exporter/node_exporter \
--web.listen-address=:9100 \
--collector.systemd
Restart=on-failure
[Install]
WantedBy=multi-user.target
每个参数都不白设。--web.listen-address=:9100指定监听端口,不改就是默认9100,但写出来更明确。--collector.systemd是收集systemd服务的运行状态,这个很有用,后面配置告警时可以检查ZrLog的systemd服务是否活着。
系统层的核心指标里,磁盘是比较容易踩坑的。node_exporter默认暴露的磁盘相关指标有node_filesystem_avail_bytes和node_filesystem_size_bytes,这两个指标按照文件系统挂载点分开。但问题在于,Linux系统上会有很多伪文件系统,比如tmpfs、overlay、sysfs等,如果不加过滤直接计算磁盘使用率,会出现一堆"假告警",明明是临时目录的容量很小,覆盖率却显示90%以上。我实际使用的磁盘使用率查询是这样的:
promql复制(1 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs"})) * 100
用fstype正则过滤掉伪文件系统,只统计真实的磁盘分区(ext4、xfs等)。这一点在配置磁盘告警时非常重要,不加过滤的话,tmpfs类型的挂载点会让你收到一堆无意义的告警。
5.2 mysqld_exporter部署与监控账号配置
MySQL的监控用官方推荐的mysqld_exporter。部署之前,先在MySQL里建一个专用账号,不要用root账号去采集指标。我创建账号和授权用的SQL如下:
sql复制CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'Cx_Exporter_2024' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';
WITH MAX_USER_CONNECTIONS 3这个选项很关键,它限制了exporter账号最多只能建立3个连接。为什么要控制这个?因为mysqld_exporter如果出现频繁重连的异常情况,不会额外占用数据库连接资源,避免监控本身成为数据库的负担。
mysqld_exporter连接MySQL的方式,是通过/root/.my.cnf文件或者是环境变量传入用户名和密码:
ini复制[client]
user=exporter
password=Cx_Exporter_2024
启动的时候用--config.my-cnf指定这个文件:
bash复制/opt/mysqld_exporter/mysqld_exporter \
--config.my-cnf=/etc/mysql/.my.cnf \
--web.listen-address=:9104
数据库层的核心指标里,我最关心两个:mysql_global_status_threads_connected(当前连接数)和mysql_global_status_slow_queries(慢查询累计数)。连接数超过MySQL的max_connections上限时,新的请求会直接报错;慢查询的数量如果突然飙升,说明某条SQL或者索引出了问题。这两个指标都会在Grafana的面板上单独展示。
5.3 磁盘告警规则的具体配置思路
热搜词里专门提到了"磁盘告警规则是在如何配置和",我在这里把这一块展开详细讲一下。
磁盘告警我实际配置了两条规则:一条是磁盘使用率告警,一条是inode使用率告警。很多人只配了磁盘使用率而忽略了inode,这是个常见的盲区——磁盘明明还有空间,但因为inode耗尽导致无法创建新文件,网站一样会挂。我见过某台服务器因为日志文件切分过于频繁,把inode用完了,SSH登录都成问题,非常被动。
磁盘使用率告警规则我这样写:
yaml复制groups:
- name: zrlog-disk-alerts
rules:
- alert: DiskUsageHigh
expr: (1 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs"})) * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} 磁盘使用率超过85%"
description: "当前磁盘使用率已达 {{ $value | humanizePercentage }},请尽快清理磁盘空间。"
for: 5m表示只有当这个条件持续满足5分钟才会触发告警。为什么要加这个持续条件?因为磁盘使用率在某些场景下可能瞬间波动一下,比如日志写了个大文件又立刻被删除,这种瞬间抖动如果立即告警会非常烦人。持续5分钟才告警,能过滤掉大量无意义的瞬时波动。
inode使用率告警的表达式:
promql复制node_filesystem_files_free{fstype!~"tmpfs|overlay|squashfs"} / node_filesystem_files{fstype!~"tmpfs|overlay|squashfs"} < 0.15
含义是剩余inode数占总inode数的比例低于15%时触发,for: 5m同样加上。
6. Grafana可视化:从数据到用户能看懂的图表面板
Prometheus把指标采集回来了,但原始数据是一串串数字,直接看PromQL结果不直观。Grafana要做的就是把这些人能读懂的趋势图、仪表盘、状态面板组织起来,形成一套真正的"监控大屏"。
6.1 数据源配置:打通Prometheus到Grafana的通道
Grafana的安装可以用Docker或者二进制。相比之下Grafana用Docker部署更方便,因为它的配置和插件管理都在容器里做得比较干净。我用的是Docker方式:
bash复制docker run -d \
--name=grafana \
-p 3000:3000 \
-v grafana-storage:/var/lib/grafana \
-e "GF_SECURITY_ADMIN_PASSWORD=your_strong_password" \
grafana/grafana:11.0.0
数据卷挂载到grafana-storage,在Docker那边自动管理。如果后续要升级Grafana版本,只要换镜像版本、重新启动容器,数据卷里的配置和面板都会保留,实测升级无损。
第一次打开Grafana,默认地址是http://<服务器IP>:3000,默认账号密码是admin/admin,登录后第一时间改密码。左侧菜单进入Configuration -> Data Sources -> Add data source,选择Prometheus,在URL一栏填http://localhost:9090(如果Grafana和Prometheus在同一台机器上),然后点击Save & Test,出现绿色提示说明连通成功。
注意:如果Grafana和Prometheus不在同一台机器,URL要填Prometheus所在机器的IP加端口,并且在Prometheus配置文件里要注意
--web.listen-address不能默认只监听localhost,否则其他机器访问不到。
6.2 导入现成的Dashboard还是自己写?两种思路我都说
面对Grafana面板,新手最容易有的疑问是:这么多图表,难道都要一个一个手动配置吗?
实际上Grafana社区有大量现成的Dashboard模板,你只需要知道模板ID,就可以一键导入。Dashboard -> Import -> 输入ID -> Load,然后选择数据源,几秒钟就完成。我常用的模板ID和用途贴一下:
| Dashboard ID | 名称 | 监控对象 |
|---|---|---|
| 1860 | Node Exporter Full | 系统层(CPU/内存/磁盘/网络) |
| 4701 | JVM (Micrometer) | Spring Boot应用层 |
| 7362 | MySQL Overview | 数据库层 |
导入模板之后要做的一件事:确认模板使用的指标名和你的Prometheus里实际存在的指标名一致。不同版本、不同exporter的指标命名可能会差几个字符,比如老版本的node_cpu在较新版本中改成了node_cpu_seconds_total。如果导入后面板上全是"No data",先检查是不是指标名对不上。我习惯在Grafana的Explore页面里输入node_cpu_seconds_total试试有没有数据,再决定哪个模板能用。
如果模板导入后总有些不如意,就自己写面板。Grafana面板的核心配置很简单,选一个Panel,选择Prometheus数据源,在查询框里写PromQL表达式,选择图表类型(时间序列、仪表盘、柱状图等)。比如我想看"每小时文章访问量TOP10",就可以写:
promql复制topk(10, sum(rate(http_server_requests_seconds_count{handler!=""}[1h])) by (uri))
然后把它放在Dashboard的显眼位置。自己写面板的好处是可以完全贴合自己的业务需求——模板是别人按通用场景设计的,而你的博客的流量特征、接口特征只有你自己最清楚。
6.3 sum by与sum without:聚合操作里的玄机
热搜词里出现了"grafana sum by",这确实是用Grafana写PromQL时最容易被绕晕的地方。我来把这两个函数彻底讲明白。
sum by (label)表示按某个标签分组对结果求和,sum without (label)表示除了某个标签之外的所有维度求和。举个例子,sum by (uri)(rate(http_server_requests_seconds_count[5m]))得到的是按接口URI分组的QPS,每个URI一条曲线。而sum without (instance)(rate(http_server_requests_seconds_count[5m]))得到的是所有实例总QPS,同时去掉了instance这个标签的区分。
两者的结果在大多数场景下一样,差异只在对哪个标签做保留还是排除。但有一个让我印象深刻的坑:网上有个流行的JVM面板模板用的是sum without (instance)配合JVM内存指标,导入面板后内存曲线出现了一条奇怪的"总和"曲线,看起来像是堆内存总量,其实是把所有内存区(heap、nonheap)全部加在一起了。后来我改成sum by (area)按内存区域分组,才得到正确的区分。所以实际使用中,我强烈建议根据业务诉求来决定用哪种聚合方式——如果你需要按某个维度拆开看,就直接想到sum by;如果你只是想去除干扰标签,就用sum without。
7. 告警规则体系:不是配个规则就完事,要能真正拨动人
监控的最终价值是告警——出了事能第一时间通知到人。这一节我把告警的完整链路讲清楚:Prometheus负责产生告警事件,Alertmanager负责把告警事件推送出去(邮件、钉钉、企业微信、Webhook等),Grafana也可以作为告警展示层。
7.1 Alertmanager部署与邮件通知配置
Alertmanager是Prometheus生态里负责处理告警的组件,它的核心职责是接收Prometheus推送过来的告警,然后按照路由规则把告警消息发送到不同的接收人。安装方式依然是二进制加systemd。
bash复制wget https://github.com/prometheus/alertmanager/releases/download/v0.27.0/alertmanager-0.27.0.linux-amd64.tar.gz
tar xzf alertmanager-0.27.0.linux-amd64.tar.gz
mv alertmanager-0.27.0.linux-amd64 /opt/alertmanager
Alertmanager的配置文件alertmanager.yml里最关键的两个部分是路由和接收器。我最初用邮件接收告警,配置如下:
yaml复制global:
smtp_smarthost: 'smtp.example.com:465'
smtp_from: 'alert@example.com'
smtp_auth_username: 'alert@example.com'
smtp_auth_password: 'your_smtp_password'
smtp_require_tls: false
route:
group_by: ['alertname', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'email'
receivers:
- name: 'email'
email_configs:
- to: 'ops@example.com'
参数含义我简单解释一下:group_wait是同一组告警的等待时间,意思是说同一类的告警在30秒内只发一次;group_interval是组内新告警的发送间隔;repeat_interval是重复告警的发送间隔——同一个告警持续存在时,4小时发一次,避免半夜三更被同一个告警轰炸。
需要说明的是,Prometheus的告警推送依赖alerting配置段,要在Prometheus主配置文件里加上:
yaml复制alerting:
alertmanagers:
- static_configs:
- targets: ['localhost:9093']
7.2 四类核心告警规则的PromQL与参数详解
我把实际使用的告警规则按照严重程度分成了四类。每一条都用得上,也都有对应的故障场景。
第一类:服务可用性告警
yaml复制- alert: ZrLogInstanceDown
expr: up{job="zrlog-app"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "ZrLog实例 {{ $labels.instance }} 已下线"
description: "Prometheus连续2分钟无法从 {{ $labels.instance }} 抓取到指标,请立即检查ZrLog进程状态。"
这个规则通过up指标判断实例是否在线。up是Prometheus对每个采集目标自动生成的指标,1表示正常、0表示采集失败。job="zrlog-app"限定只看ZrLog的采集目标。
第二类:JVM堆内存告警
yaml复制- alert: HeapMemoryHigh
expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.9
for: 10m
labels:
severity: warning
annotations:
summary: "ZrLog堆内存使用率超90%"
description: "堆内存使用率已超过90%并持续10分钟,可能存在内存泄漏或缓存未回收,建议查看JVM GC情况。"
这里要注意,jvm_memory_max_bytes在某些情况下可能取不到,尤其是一些容器环境里JVM没有设置-XX:MaxRAMPercentage的限制时。所以我在JVM启动参数里加过-XX:MaxRAMPercentage=75,让堆内存上限等于容器可用内存的75%,这个参数对避免指标缺失很关键。
第三类:磁盘和CPU告警
yaml复制- alert: HighCPUUsage
expr: 100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
for: 10m
labels:
severity: warning
CPU使用率用rate(node_cpu_seconds_total{mode="idle"}[5m])计算空闲CPU占比,然后用100减掉得到使用率。这里用rate而不是直接看瞬时值,是因为CPU使用率本身是一个变化很快的指标,取5分钟平均能避免偶发尖峰导致误报。
第四类:MySQL连接数和慢查询告警
yaml复制- alert: MySQLConnectionsHigh
expr: mysql_global_status_threads_connected > 200
for: 5m
labels:
severity: warning
annotations:
summary: "MySQL连接数超过200"
description: "当前MySQL连接数已达 {{ $value }},请检查是否有慢查询或连接泄漏。"
连接数阈值根据你的MySQL实例规格来定,如果max_connections是500,那200作为预警阈值比较合理;如果机器内存小、max_connections只给了200,那阈值就得降一半。
7.3 告警规则里的for参数,背后藏着一次误报事故
刚开始配置告警时,我犯过一个错误:CPU告警规则里的for参数设置的是1分钟,结果半夜被钉钉告警轰炸过好多次。原因是有个定时备份任务每天凌晨会短暂占用大量CPU,持续一两分钟就结束了。这种瞬时尖峰对博客服务本身没有影响,但我的告警规则不知道,只要超过90%持续1分钟就报警。
经过这次教训,我把CPU告警的for改成了10分钟,同时把备份任务的时间段单独排除了。这里要说明我的一个设计原则:告警规则里的for参数,本质上是在告诉系统"这种现象持续多久我才能认定为故障"。太短,容易被瞬时抖动干扰;太长,又会让真正的故障迟迟得不到通知。我个人实践下来的参考值是:CPU和内存的for设为5~10分钟,服务可用性的for设为1~2分钟,磁盘使用率这种缓慢变化的指标for可以设为10~30分钟。你可以根据自己的场景调整,但方向应该是"宁可晚报几分钟,也不要被假警报搞得疲于奔命"。
8. 这半年跑下来,我踩过的那些值得记录的坑
监控体系搭建本身不算复杂,但把它稳定运行起来,半年里我还是踩了不少坑。有些坑在上面的章节里已经顺带提到过,这一节集中梳理一下,希望能帮你提前避开。
8.1 Grafana默认时区导致告警时间偏差8小时
我的服务器是UTC时区,但我和大多数用户一样在北京时间区工作。刚开始用Grafana时,告警通知里显示的时间是UTC,每天看告警记录都特别别扭,排查问题时还得在心里加8个小时。后来发现Grafana有一个全局时区设置,在Administration -> Default preferences里把Timezone改成Asia/Shanghai,所有面板和告警的时间显示就都统一到北京时间了。这个设置极其容易忽略,但影响很大。如果你也把时间显示不正常,优先检查这一项。
8.2 数据源连通正常但面板显示No data,问题出在指标名
有段时间面板上JVM相关的图突然全部显示No data,但Prometheus的Targets页面显示采集正常,Explore里输入jvm_memory_used_bytes也能查到数据。排查了半天,发现是Grafana面板里用的指标名是旧版本的jvm_memory_bytes_used,而新版Micrometer已经把指标名改成了jvm_memory_used_bytes。模板是网上找的,模板作者用的Micrometer版本比较老,和实际版本不匹配。
这个问题也让我养成了一个习惯:拿到任何一套Dashboard模板,先把模板里的指标名在Explore里验一遍,确认有数据再导入。一旦发现No data,优先检查指标名是否一致,而不是怀疑网络或权限问题。
8.3 Prometheus数据保留期设置太短,想看历史趋势发现一片空白
Prometheus默认数据保留时间是15天。我有一次想复盘三个月前的一次故障,打开面板发现那个时间段全是一片空白,这才意识到数据早就被清理掉了。对博客场景来说,15天完全不够,我后来在启动参数里加了--storage.tsdb.retention.time=30d,条件允许的话甚至建议保留到60天。时序数据占用的磁盘空间并没有想象中那么大,博客一天几万次请求的指标量也就几百MB,一个月也就10GB左右,一块数据盘完全放得下。
8.4 告警阈值拍脑袋定,结果天天被假警报折磨
这个问题其实上面已经提到过告警for参数的部分,但阈值本身也有讲究。我第一版CPU告警阈值设置的是80%,结果那段时间博客正好被搜索引擎爬虫高频抓取,CPU经常到达85%左右,告警邮件一封接一封,半天后我就把阈值调到了90%。调完之后的体会是:阈值设置不能靠"感觉",最好先跑一周监控,观察正常业务下的资源使用基线,再基于基线加上安全余量来定阈值。基线CPU是70%,告警阈值设为85%才有意义;基线本来就长期在85%徘徊,告警阈值设90%等于没有告警。
8.5 关注告警渠道的接通率,别让告警石沉大海
用邮件做告警接收方,最大的问题是手机端不一定第一时间看到。有一次磁盘告警发出去了,但我开会没看邮件,直到用户反馈博客上传图片失败才发现磁盘早就满了。后来我把告警渠道改成了钉钉机器人Webhook,通过Alertmanager的webhook_configs推送到钉钉群,手机也能实时收到。转换方式很简单,在Alertmanager的receivers里加一个webhook配置块,指向钉钉机器人的地址,再将default receiver改成这个webhook即可。个人博客这种轻量级运维场景,用钉钉或企业微信机器人比邮件更加高效。
8.6 exporter端口暴露到公网,等于把服务器首页送给全网扫描
这是一个安全性的坑。最初我的Prometheus、Grafana、各个exporter都是直接放在公网IP上的,这些服务的端口像9100、9090、3000,全部可以被外网直接访问。网络扫描器很容易发现这些端口,而且它们往往存在一些已知的未授权访问风险。网络安全法现在是认真执法的,我不想因为一个开放的9100端口惹出麻烦。现在的做法是,所有监控组件都只监听内网地址,需要远程访问Grafana时走SSH隧道或者只在SSH客户端所在的网段放行端口。具体操作上,node_exporter和mysqld_exporter的启动参数都加上--web.listen-address=127.0.0.1:9100这样的限制,Prometheus和Grafana的端口同样在安全组里只放行内网来源。平时自己看监控就SSH隧道转发一下,既安全又省事。
9. 这套监控体系跑起来之后,我最大的感受是什么
从一个跑在单机上的轻量级博客,到一套带高可用架构、带完整监控体系的小型系统,这个过程让我对"运维"两个字有了完全不同的理解。说实话,在搭建这套监控之前,我一直觉得博客这种小项目,能用就行,出了问题重启大法解决一切。但经历过几次"盲人摸象"式的排障之后,我彻底改变了这种想法——监控不是给大厂用的专属机制,越是小型的独立服务,越需要靠监控数据来弥补运维人力不足的短板。毕竟大厂有专门的SRE团队可以随时待命,我们个人站长遇到问题只能靠自己和搜索引擎,监控能在第一时间告诉我们"哪里出了事",这就能省下大量盲目的排查时间。
这套方案的落地成本我认为是完全值得的。两台低配云服务器,Prometheus加Grafana加三个exporter,总共占用的内存大约500MB,一个月跑下来稳定性和资源占用都在可接受范围内。相比它在关键时刻发挥的作用——比如那次通过JVM监控定位到递归查询问题,比如磁盘告警避免了数据丢失,这个成本已经低到可以忽略不计了。
如果你也在用ZrLog,或者任何一个小型Java博客系统,我建议你照着这篇文章的步骤把整套监控搭起来。不用贪多求全,先跑通Prometheus加Grafana加node_exporter这套最小闭环,把系统层和存活状态监控起来,然后逐步加上JVM指标和数据库指标。每加一个指标,就多一层对系统的理解。等到某一天你的博客突然变慢,而你能在十分钟内通过监控面板直接看到是数据库慢查询、还是JVM GC停顿、还是磁盘空间告急的时候,你会感谢当初动手搭建这套体系的自己。
