先说明一点,eladmin 虽然定位是后台权限管理系统,但一个后台系统要真正跑得稳、扛得住线上流量,监控和运维才是后置的大头。很多人拿 eladmin 做脚手架,功能点一接,以为能跑就行,结果一上线就抓瞎:SQL 慢查询发现不了、服务器 CPU 被打满也毫无感知、内存泄漏只能靠重启大法硬扛。这篇文章我把自己在 eladmin 基础之上搭建监控与运维体系的经验完整拆一遍,包含 Druid、Prometheus、Grafana、node-exporter 与钉钉告警的对接细节,也都配上实际的配置过程和排错思路,希望能帮大家少走点弯路,直接能照着搭出一套能用的监控环境。
1. 从“跑得起来”到“跑得稳”:eladmin 引入监控的整体思路
eladmin 这套框架相信很多朋友都不陌生,它把 Spring Boot 后台、Vue 前端、代码生成、权限管理这些能力整合得相当顺手。但坦白讲,很多团队用它做项目,重心全都放在功能开发上,监控这块基本靠感觉——应用挂了看日志,内存爆了重启,SQL 慢查询等到用户抱怨才知道。
我在自己负责的某套基于 eladmin 的数据管理平台里,跑了大概三个月“裸奔”阶段后,实在撑不住才下决心补齐监控运维体系。白天用户数一多,数据库连接池告警直接刷屏,后来定位到是某个统计接口每次查询都要跑三张超过百万行的表,连 Druid 监控面板里都能看到 SQL 执行时长动不动就是几秒。那一刻我才意识到,监控不是锦上添花,而是项目能不能稳定交付的地基。
再说清楚一个容易被新手忽略的点:监控运维在全链路体系里,覆盖范围并不是单指应用本身,而是包含三个核心维度。第一是基础设施层,也就是服务器节点的 CPU、内存、磁盘、网络等资源使用情况,这个层面的异常往往预示着容量瓶颈或宿主机故障。第二是应用运行层,对应的是 Java 进程的 JVM 堆内存、GC 频率、线程池状态、接口吞吐量与响应时间等。第三是数据访问层,也就是数据库连接池活跃度、慢 SQL、事务执行时长等,这一层对 eladmin 这种强后台管理系统来说尤其重要,因为大量操作都依赖数据库交互。
选型上,我最终采用的是 Prometheus 加 Grafana 加 Alerts 的组合。eladmin 本身就是 Spring Boot 架构,天然适合通过 Actuator 暴露端点,再由 Prometheus 定期拉取指标;可视化看板统一切到 Grafana,数据源直接对接 Prometheus;告警规则则由 Prometheus 内置的 Alertmanager 统一管理,走钉钉机器人把异常消息推到工作群,团队响应速度比之前盯着日志排空前提升了一个量级。这套组合的好处在于各个组件都是开源方案,社区里能查到的资料和模板非常丰富,节点扩展也灵活,无论是一个实例还是一组集群,架构上都不用推倒重来。
各层分工具体是这样的:基础设施层用 node-exporter 采集宿主机指标,一个小型单体服务在资源耗尽之前也能靠它提前预警;应用层靠 Actuator 暴露 JVM 相关信息;数据层则直接用 eladmin 内置的 Druid 数据源监控能力。三块数据汇总到 Prometheus 后,统一在 Grafana 做仪表盘展示,再配合钉钉告警推送,整套链路覆盖到的恰恰是一个中小型团队最关心的几个问题:服务器资源够不够、应用还活着没有、数据库有没有拖后腿。
这套体系搭完之后,最大的直观感受是省心。以前半夜收到用户反馈系统卡顿,还得先翻日志、再手动连到服务器看资源占用,现在打开 Grafana 先看大盘,再按时间轴回追告警信息,基本几分钟内就能锁定问题区间。团队内非专业的运维同学也能在醒目位置直接看到系统健康度,不用每个人都会读日志。
接下来我按实际的落地顺序,把每一层的监控搭建过程展开说。先从 eladmin 最基础的 Druid 监控讲起,因为它的接入成本最低,反馈却最直接。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 内置 Druid 监控:先看清楚 SQL 和数据源的真实状态
2.1 为什么第一步要先打通 Druid 监控
eladmin 默认集成了 Druid 作为数据库连接池,它在最初设计时就把监控功能内置进去了,但很多项目在实际部署中并没有启用。这其实是一种浪费,因为 Druid 监控面板提供的信息对于后端性能问题定位几乎是零成本的:直接能看到活跃连接数、SQL 执行次数、最慢的几条 SQL 以及事务相关指标。
我当时第一次打开 Druid 面板时,一行统计数据立刻刷新了我的认知——某个列表查询接口的 SQL 在一天之内被调用了近万次,单次执行平均耗时 780 毫秒,而且查询条件里走了全表扫描。如果没有 Druid 监控,这种“隐性慢查询”几乎不可能通过人工 code review 发现。所以我说,在给 eladmin 加 Prometheus 和 Grafana 之前,先把 Druid 监控打开,这一步是性价比最高的监控投入。
2.2 开启 Druid 监控面板的配置要点
eladmin 默认的 Druid 配置里,访问监控页面的 Servlet 可能未开启或者被限制在本地访问。要想远程访问并看到监控数据,需要调整配置。先看依赖是否齐全,pom.xml 里的 druid-spring-boot-starter 一定得有,这一点 eladmin 本身已经带了,不需要额外加。
然后在 application.yml 里修改 Druid 的监控相关配置。我给出一份可以用的参考配置:
yaml复制spring:
datasource:
druid:
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: your-password
reset-enable: false
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat:
enabled: true
slow-sql-millis: 1000
log-slow-sql: true
wall:
enabled: true
这里有两点值得特别留意。第一,slow-sql-millis 设成 1000 毫秒,是让 Druid 把超过 1 秒的查询单独记录下来,这样可以直接在“慢 SQL”选项里看到问题语句,不用每次手动到数据库开慢查询日志。第二,login-username 和 login-password 一定要改,不要用默认值,而且生产环境建议通过防火墙把 /druid 路径限制在办公网 IP 段内,避免数据源信息泄露。
配置改完后,重启服务,浏览器访问 http://你的IP:端口/druid/index.html,输入刚才配置的用户名密码就能进入监控页面。我第一次看到那些统计数据的时候,最大的感受就是信息密度极高:数据源、SQL 监控、SQL 防火墙、Spring 监控、URI 监控,几乎覆盖了日常调优的常用维度。通过 URI 监控可以直接看到哪个接口调用的数据库次数最多、耗时最长,定位性能瓶颈时连链路追踪都不用开。
这一层监控的数据对于后续接入 Prometheus 也是有用的,因为它解决了“数据库这一侧是否健康”的问题,但它不能直接回答“服务器负载高是因为哪个进程”这一层问题,所以还需要把监控视野往系统资源和 JVM 上扩展。
3. 应用与资源监控:Actuator 对接 Prometheus 的完整过程
3.1 给 eladmin 应用暴露可采集的指标端点
Druid 解决的是“数据库里发生了什么”,而应用和资源层的监控解决的是“Java 进程和服务器在承受什么”。这一层我的选型是 Spring Boot Actuator 负责暴露指标,Prometheus 负责定时拉取和存储,Grafana 负责展示和告警联动。
eladmin 的项目结构里,核心应用本身已经依赖于 spring-boot-starter,所以额外引入 Actuator 只需要在 pom.xml 里补一段依赖。引入后第一件事是确认暴露哪些端点,默认 Actuator 只开放 health 和 info,对监控来说完全不够用。我经过几次调整后最终采用的是 prometheus 端点全开配置:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics,heapdump,threaddump
metrics:
export:
prometheus:
enabled: true
tags:
application: eladmin-server
endpoint:
health:
show-details: always
这段配置里有几个容易被忽略但很关键的细节。include 里加了 prometheus,表示让 Actuator 内部集成 micrometer-registry-prometheus,这样会对外暴露一个 /actuator/prometheus 端点,Prometheus 可以直接从这个地址拉取 JVM 等相关指标。metrics.tags.application 是给所有指标打上应用名标签,当你的监控系统里存在多个服务时,这个标签能让你在 Grafana 里轻松把不同应用的指标区分开。show-details: always 让健康检查能返回磁盘空间、数据库连接等详细信息,配告警时很有用。
pom.xml 里需要补的依赖如下:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
要注意版本兼容性,eladmin 依赖的 Spring Boot 版本如果是 2.x,那么 micrometer-registry-prometheus 也会自动匹配对应版本,一般不需要手动指定版本号。但如果你用的是 Spring Boot 3.x,要注意引入坐标的 groupId 已经从 io.micrometer 迁移到 io.micrometer 下面的新模块,网络上的老教程经常会在这里踩坑。
配置文件改完并重启后,先用 curl 验证一下端点通了没有:
bash复制curl http://127.0.0.1:8080/actuator/prometheus | head -n 30
如果返回的是以 # HELP 开头的一系列指标文本,比如 jvm_memory_used_bytes、jvm_gc_pause_seconds、http_server_requests_seconds 等,就说明暴露成功。这时候再打开 Prometheus 的 targets 页面,就能看到这个应用实例已经被标记为 UP 状态。
3.2 Prometheus 的拉取配置与存储要点
Prometheus 本身只是一个时序数据库加采集器,它的配置核心在 prometheus.yml 里的 scrape_configs。针对 eladmin 应用,最基本的配置如下:
yaml复制scrape_configs:
- job_name: 'eladmin-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['192.168.1.100:8080']
labels:
service: eladmin
值得留意的是,如果是单体应用,这里直接写服务器 IP 和端口就行;如果是 K8s 环境,可以换成 kubernetes 的服务发现方式,自动匹配带有特定 label 的 Pod。我这里重点说单体部署模式,因为很多使用 eladmin 的中小型项目仍然用云主机加 Docker Compose 这种轻量部署方案。
一个容易踩的坑是 pull 超时时间。actuator 的 prometheus 端点在首次请求时可能因为 JVM 指标初始化而稍慢,如果 Prometheus 默认的 scrape_timeout 设得太短,会出现频繁的 context deadline exceeded 报错。我把全局配置里的 scrape_timeout 调到 15s,并且给 eladmin 的 job 单独设置 scrape_interval: 30s,这样既能降低对应用端的压力,也足够捕捉到 JVM 内存的渐进变化。
关于存储,默认 Prometheus 用本地磁盘保存数据,15 天保留期对大多数中小团队够用。但要注意磁盘预估,如果指标量特别大,本地盘容易被打满,建议监控盘的容量至少准备 50G 以上,并且设置 retention: 15d,防止积累过久占用空间导致 Prometheus 进程 OOM。
3.3 JVM 与接口指标能帮我们发现什么问题
指标端点和拉取链路打通之后,真正有价值的是日常看板和分析。Grafana 上有一个通用的 JVM 监控模板,ID 大概是 4701,配合 Prometheus 数据源就能直接展示堆内存、非堆内存、GC 次数、GC 耗时等核心数据。我实践下来发现,最值得关注的三个指标分别是:
- jvm_memory_used_bytes:堆内存使用量,如果呈现阶梯式持续上涨且不回落,基本可以判定有内存泄漏或者对象积压。
- jvm_gc_pause_seconds:GC 停顿耗时,如果频繁出现长停顿,接口响应时间会肉眼可见地变长,需要优化堆大小或排查大对象分配。
- http_server_requests_seconds:接口响应时间的分布,用这个指标能看到哪些接口的 TP99 在恶化。
我印象很深的一次排查经历,是通过 JVM 指标发现某个定时任务每次执行后内存都不释放,持续两天内存占用从 1.5G 逐步逼近 4G。后来结合线程快照定位到是某个导出功能把大批量数据加载到了 List 里且没有及时清空引用,修复后内存曲线立刻恢复正常。如果没有监控数据支撑,这种问题排查起来会非常耗时。
接下来是整个监控体系里最直观、也往往是最缺的一块:服务器的资源监控。
4. 服务器资源监控:node-exporter 与核心告警规则配置
4.1 node-exporter 部署与指标采集
应用本身的健康不代表宿主机的健康。一台服务器如果 CPU 持续跑满,即使 Java 进程还活着,接口响应也会变得奇慢无比。所以服务器资源监控是监控体系里躲不开的一环。这里最常用的方案是 node-exporter 加 Prometheus。
node-exporter 是 Prometheus 官方提供的主机指标采集器,部署方式非常简单,二进制或者容器都能跑。我在测试环境直接用的 Docker:
bash复制docker run -d \
--name node-exporter \
--restart=always \
--network=host \
-v /proc:/host/proc:ro \
-v /sys:/host/sys:ro \
-v /:/rootfs:ro \
quay.io/prometheus/node-exporter:latest \
--path.rootfs=/host
--network=host 是因为 node-exporter 默认监听 9100 端口,用 host 模式可以让 Prometheus 直接通过宿主机 IP 访问,不需要额外做端口映射。挂载 /proc、/sys 和 / 目录是为了让容器能够读取宿主机的 CPU、内存、磁盘等数据。
启动后在浏览器访问 http://服务器IP:9100/metrics 能看到一堆 node_ 开头的指标,就说明部署成功。然后在 Prometheus 的 scrape_configs 里添加对应的 job:
yaml复制 - job_name: 'node-exporter'
static_configs:
- targets: ['192.168.1.100:9100']
labels:
host: eladmin-prod-01
我建议从最开始就给不同的机器打上明确的 host 标签,因为在服务器数量变多之后,如果没有标签区分,Grafana 看板上一片混乱,告警消息也没法定位到具体的机器。
4.2 核心告警规则的配置与解释
资源指标收集上来之后,真正的价值在告警规则。Prometheus 的告警规则是通过 rule_files 目录下的 yml 文件定义的。我把自己在 eladmin 项目中使用并持续迭代的一套规则分享一下,按告警级别做了分类。
yaml复制groups:
- name: server-alerts
rules:
- alert: 服务器CPU使用率过高
expr: 100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 > 85
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.host }} CPU 使用率超过 85%"
- alert: 服务器内存使用率过高
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.host }} 内存使用率超过 90%"
- alert: 磁盘空间不足
expr: (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) * 100 < 15
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.host }} 分区 {{ $labels.mountpoint }} 剩余空间不足 15%"
- alert: 磁盘IO等待过高
expr: rate(node_disk_io_time_seconds_total[1m]) > 0.7
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.host }} 磁盘 IO 等待时间过高"
逐个解释一下关键表达式的含义。CPU 使用率那条,先用 rate(node_cpu_seconds_total{mode="idle"}[5m]) 计算出近 5 分钟 CPU 空闲时间的速率,用 100 减去它再取平均就能得到使用率,大于 85% 且持续 10 分钟触发告警,可以有效过滤短时间内的瞬时尖峰。内存使用率直接用 MemAvailable_bytes 除以 MemTotal_bytes,这个指标比单纯的 MemFree 更准确,因为它把缓存也计算在了可用范围内,不容易因为 Linux 的 buff/cache 机制产生误报。磁盘空间那条之所以要排除 tmpfs 和 overlay,是因为容器环境里这些虚拟文件系统会导致大量无意义的告警,特别是 overlay 的可用空间往往和宿主机重叠,容易造成重复告警。磁盘 IO 等待时间这条用 rate 计算每秒 IO 等待时间占比,超过 70% 基本说明磁盘已经成为系统瓶颈。
这些规则文件在 Prometheus 配置里的挂载方式很简单,rule_files 下加入规则文件路径,然后调用 Prometheus 的 reload 接口(curl -X POST http://localhost:9090/-/reload)即可热加载,不需要重启整个服务。每次改完规则后,我习惯立刻在 Prometheus 的 Alert 页面确认规则已经被加载,而不是跑完 curl 就放下,以免语法错误被静默忽略。
4.3 针对 eladmin 的额外告警规则
除了通用的服务器资源告警,针对 eladmin 这种运行大量数据库操作的后台系统,我还加了几个更贴近业务的告警规则,这里一并说一下。
第一个是 JVM 堆内存占用过高告警。当堆内存使用率超过 85% 并持续一段时间,说明可能存在内存泄漏或者堆大小配置不合理:
yaml复制 - alert: JVM堆内存使用率过高
expr: sum(jvm_memory_used_bytes{area="heap"}) / sum(jvm_memory_max_bytes{area="heap"}) * 100 > 85
for: 15m
labels:
severity: warning
annotations:
summary: "eladmin JVM 堆内存使用率超过 85%"
第二个是接口响应时间恶化告警。通过 Actuator 暴露的 http_server_requests_seconds 指标,可以从统计角度捕捉接口性能劣化:
yaml复制 - alert: 接口P95响应时间超过2秒
expr: histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri)) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "接口 {{ $labels.uri }} P95 响应时间超过 2 秒"
这条规则在一些流量较大的系统里很实用,它能捕捉到“平均响应时间还行,但少数请求已经慢到完全不可用”的情况。要注意的是 http_server_requests_seconds_bucket 这个指标只有在 Spring Boot 2.1 之后才能用,eladmin 的版本应该没有问题。
基本监控相关的内容说到这,先把告警部分放一放,重点说说看板和报警的联动落地。因为规则配好之后,如果只有 Prometheus 后台看得到,那依然起不到真正的作用,关键还是实时通知拉通。
5. Grafana 看板与钉钉告警通知的落地细节
5.1 Grafana 数据源配置与看板导入
Grafana 是整个监控体系的“可视化层”,本身不采集数据,所有图表都来源于它对接的数据源。在 Grafana 里添加 Prometheus 数据源的方法非常简单:登录后进入 Configuration 的 Data Sources,选择 Prometheus,填上 Prometheus 服务的 URL(比如 http://192.168.1.100:9090),保存并测试,显示成功即可。
数据源接通后,需要把已有的 Dashboard 模板导入。JVM 监控模板可以用 4701 这个 ID,服务器资源监控模板可以用 1860 或 9276。这里的模板编号在不同版本的 Grafana 里都能从官网社区直接查到。导入时选择对应的数据源,图表里的所有 PromQL 查询会自动关联到你的 Prometheus 实例,不需要手动修改任何表达式,这一点在刚接触 Grafana 时特别方便。
导入模板后,有几个地方建议按自己的需求微调。比如服务器资源看板里,默认会展示所有 host 的指标,如果机器多了会显得杂乱,可以在模板的变量设置里把 host 字段改为自定义标签,或者在 Grafana 的 dashboard settings 里加一个变量下拉框,用 label_values(node_uname_info, hostname) 自动填充所有已采集的主机名。这样在顶部切机器看数据非常方便。
5.2 钉钉告警通知的配置方法
告警通知的推送方式,我最终选的是钉钉机器人。阿里内部大量使用钉钉,群机器人 webhook 接入非常成熟,不需要额外部署客户端,团队在手机上收到告警后可以第一时间响应。而且配置一条链路只需要三步:
第一步,在钉钉群里添加一个自定义机器人。群设置里选择智能群助手,添加机器人,选择自定义,填好机器人名称和关键词安全设置。比如我填的安全关键词是 告警,这样只有消息内容里包含这两个字时群机器人才会正常推送,否则会被钉钉拦截。这一点尤其重要,不少人配完之后发现收不到消息,大概率就是安全设置里关键词没匹配上。
第二步,拿到 Webhook 地址后,在 Grafana 里配置 Notification policy。先添加一个 Contact point,类型选择 DingDing,把 URL 填成 webhook 地址。注意 Grafana 的钉钉集成是基于 webhook 的兼容模式,用官方提供的 DingDing 类型即可,不需要自定义 webhook 格式。
第三步,在 Alert rules 里设置告警规则。创建一条 alert rule,定义查询表达式,比如服务器 CPU 使用率高于 85 持续 5 分钟,标签选上 severity=warning,通知策略关联到刚才配置的钉钉 contact point。保存后等待验证即可。
我实际使用下来,从触发异常到钉钉群收到消息,延迟通常在 10 秒以内。这里给一个建议:告警规则里尽量把 for 参数设置成 5 到 15 分钟,避免因为瞬时抖动导致频繁告警。经历过半夜被一条持续 3 分钟的 CPU 高负载告警拉起来、起床后却发现负载早就恢复正常的情况后,就会明白这个参数的含金量。
另一个实践心得是分级告警。我把告警分成两个级别:warning 和 critical。warning 级别推送到普通运维群,critical 级别额外推送到研发负责人群。因为普通开发人员如果天天被小毛病的告警轰炸,很快就会麻痹,真正出现紧急问题时反而没人关注。分级制度能让不同角色的响应动作变得更清晰。
5.3 Grafana 看板在 eladmin 项目里的自定义补充
导入通用模板能满足大部分需要,但针对 eladmin 这种重数据库交互的后台系统,我还额外做了一块自定义看板,专门展示 Druid 连接池相关指标和关键接口的访问量变化。这块看板的可视化表达式可以写得非常简单——比如用 sum(increase(http_server_requests_seconds_count{uri="/api/xxx"}[1h])) 来查看某个接口的调用次数趋势,或者用 jdbc_connections_active 这类指标追踪连接池活跃连接数。Druid 监控页本身只提供实时快照,而把它们接入 Prometheus 后,就能看到随时间变化的曲线,这在定位“某个时间点连接池被打满”时非常关键。
很多朋友会问,Druid 指标怎么接入 Prometheus。Druid 的监控数据本身是通过它自己的 Servlet 输出的 JSON,和 Prometheus 的文本格式不兼容。比较省事的方案是使用 Druid 提供的 druid-spring-boot-starter 里的 StatFilter 配合 Actuator 的 MetricsServlet 把指标点暴露出去,但由于各版本实现细节不同,直接对接时可能遇到指标为空的情况。我在实际项目中的做法是,对于连接池的关键指标,直接使用 Druid 监控的 JSON API 通过定时任务抓取后转换成 Prometheus 文本格式,再通过一个轻量级的 exporter 暴露给 Prometheus。这样一个开发量很小的 service 就能把 Druid 指标接进 Grafana,虽然比直接内置的 Prometheus 端点多写了一点代码,但胜在稳定可控,且不依赖特定版本的兼容性。
看板整体搭好之后,告警和可视化就都通了。但监控体系还有一个经常被忽略的环节——日志监控。应用一旦在夜间崩溃,如果只靠服务器资源告警,很多时候并不能及时捕捉到崩溃原因,最终还是要靠日志分析。
6. 日志与现场排错:把监控和运维真正联通
6.1 为什么监控链路里必须加日志这一环
Prometheus 加 Grafana 这套组合的强项是数值型指标,比如 CPU、内存、请求量、响应时间,但面对“应用为什么报错”“这条堆栈是在什么操作下才出现的”这类定性问题时,单纯看数值是远远不够的。尤其像 eladmin 这类管理后台,很多业务操作的上下文都体现在日志里,比如用户在某一步操作触发了异常,日志里会打印出对应的堆栈和参数。
我曾在一次生产事故排查中体验到组合拳的作用:当时 Grafana 看板显示 JVM 内存持续上涨,但磁盘和 CPU 都正常,单看内存这条链路基本只能推导出报错数量在增加或代码存在资源未释放的结论。把排查拉长到日志链路后,立刻在 error 日志里看到同一个接口在批量导入数据时抛出了 OOM 异常,最终定位到导入代码里对超大 Excel 做了全量 List 加载。这个例子说明,监控指标能帮你找到“大概在哪个环节”,但最终要定位到具体行代码,还是得依赖日志。
6.2 轻量级日志方案的选型
说到大型监控系统,大家可能第一反应是 ELK 或者 Loki,但对中小团队来说,直接上 ELK 的运维成本有点偏高,至少需要三台机器分别跑 Elasticsearch、Logstash、Kibana,而且 Elasticsearch 本身对 JVM 堆内存就很有要求。如果 eladmin 只部署在一台 4C8G 的云主机上,给它旁边再塞一个 ELK 栈,资源压力会大到跑不动。所以我在自己项目中选了一个更轻的方案——用 filebeat 采集日志,直接推送到一个轻量的 Loki,再用 Grafana 自带的数据源做日志查询。
Loki 最大的特点是不做全文索引,而是基于标签存储日志流,查询时用 LogQL 语法过滤。这种设计让它的资源占用比 Elasticsearch 低一个数量级,部署也只需要单个二进制加配置文件。对于 eladmin 这种日日志量几十万行的项目,Loki 单实例完全扛得住。
Loki 服务端启动后,在 eladmin 所在机器上部署 filebeat。filebeat 配置里指定日志文件路径和 Loki 的地址,核心配置如下:
yaml复制filebeat.inputs:
- type: filestream
enabled: true
paths:
- /home/eladmin/logs/*.log
fields:
app: eladmin
env: prod
output.loki:
host: "192.168.1.100:3100"
labels:
job: "eladmin-logs"
filebeat 会持续监控指定目录下的日志文件,并把每一行日志作为一个 entry 推给 Loki。在 Grafana 里添加 Loki 数据源后,就可以直接在 Explore 页面用 LogQL 查询某段时间内的错误堆栈。比如:
logql复制{app="eladmin", env="prod"} |= "ERROR"
这条查询会返回所有包含 ERROR 关键字的日志行,再配合时间范围选择,很快就能看到异常发生的密集时间区间。如果遇到空指针异常,还能用 |= 后面加类名关键词进一步过滤,把所有包含 NullPointerException 的日志全部捞出来。
6.3 崩溃前的现场保留技巧
监控体系再完善,也防不住极端故障下的应用进程直接挂掉。而进程挂掉后,JVM 的堆内存信息也会随之消失,如果事后想分析是什么对象导致的内存溢出,往往无从下手。所以我在 eladmin 的启动脚本里加了 JVM 参数,让 JVM 在发生 OOM 时自动导出堆转储文件:
bash复制java -Xms2g -Xmx2g \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/home/eladmin/dumps/ \
-jar eladmin-server.jar
这样一旦发生不可控的 OOM,就会在 /home/eladmin/dumps/ 目录下生成一个 .hprof 文件,后续可以用 Eclipse MAT 或 VisualVM 做离线分析。搭配上面的 JVM 内存告警,可以做到“先预警、后兜底”的双保险,即使告警没被及时处理导致进程真的挂了,后续也还有充足的数据来定位根因。
另外,线程快照也值得定期采集。我写了一个简单的 cron 脚本,每天定时把 JVM 的线程堆栈 dump 到本地:
bash复制0 2 * * * jstack $(pgrep -f eladmin-server) > /home/eladmin/thread_dumps/td_$(date +\%Y\%m\%d).txt
线程堆栈在排查死锁、线程池打满、CPU 飙高问题上非常关键。我自己曾遇到过一个非常隐蔽的线程阻塞问题,当时接口大量请求超时,但内存和 GC 都很健康。后来打开线程 dump,发现所有 Tomcat 工作线程都卡在同一个数据库查询的 SocketInputStream.read 上,最终定位到是数据库连接池最大值配置太小,导致线程都在等待获取连接。如果没有保留线程快照的习惯,这种问题根本无从下手。
日志和现场信息补上之后,整个监控体系基本就闭环了。但要知道,告警规则配好只是第一步,过拟合和漏报之间的平衡才是真正考验经验的地方,这也是很多人容易踩的坑。最后我整理了一份避坑清单和优化建议,都是从实际项目中一点点磨出来的。
7. 监控体系跑起来后,我再回头看那些容易踩的坑
7.1 告警规则绕过时的“静默故障”
很多团队的监控刚上线时,告警风暴特别严重,每个小时微信群里都是消息,大家慢慢就养成了“免打扰”的习惯。等到真正严重的故障发生时,反而被当成普通噪音忽略了。我对此的解法是分级和分组:warning 级别告警只通知运维值班人员,critical 级别才推到核心研发群。同时周期性清理不再重要的规则,删除那些只是让人焦虑、却不能引导行动的低价值告警。
一个更隐蔽的坑是“告警规则通过验证但表达式语义错误”。Prometheus 的规则文件在 reload 时做的是语法检查,不是语义检查。node_cpu_seconds_total{mode="idle"} 如果写成了 mode="iowait",语法完全合法,但查出来的数值含义就很不一样,会导致告警条件永远不被触发或频繁误报。我在写完表达式后,习惯先在 Prometheus 的 Graph 页面跑一遍这个表达式,再观察一段时间,确认它反映的行为符合预期之后才正式挂到 rule_files 里。这一步虽然简单,但能节省大量后续排错时间。
7.2 数据采集与存储的成本控制
Prometheus 的指标存储是追加式的,指标量一大,磁盘开销跟着水涨船高。eladmin 若部署在云主机上,磁盘通常又不大,如果不做控制,几个月后监控盘就会被写满。我采取的措施有三个层面:第一,在 node-exporter 采集配置里用 metric_relabel_configs 过滤掉不需要的指标,比如 node_network_receive_bytes_total 和 node_network_transmit_bytes_total 这种高基数且对日常运维价值不大的指标;第二,把 Prometheus 的 retention 时间从默认 15 天缩到 7 天,只看最近一周的数据足够覆盖大多数排障场景;第三,把 scrape_interval 从默认 15s 调整为 30s,对后台管理系统来说完全够用,但整体存储量几乎减半。
7.3 指标口径不一致导致的“数据对不上”
有一次团队在复盘时发现,Grafana 上的接口响应时间和前端看到的耗时差了快一倍,查了半天才发现问题出在指标口径上。Grafana 用的 http_server_requests_seconds 是从 Actuator 采集的,统计的是服务端处理请求的时间,不含网络传输;而前端看到的总耗时是包含网络往返和浏览器渲染的。两者都不能算错,只是口径不同。排查问题时要清楚自己看的是什么指标,跨指标对比做判断时必须先统一口径,否则很容易得出错误结论。
同样的道理也适用于内存指标的解读。Linux 上用 free 命令看到的内存使用率还不到 50%,但 node-exporter 里 node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes 算出来的可用率可能更低,原因是这两个统计口径对“可用内存”的定义不同。所以做告警阈值设计时,一定要基于同一个数据源和同一个表达式持续观察一段时间后再确定,而不是今天看到网上一个阈值就抄过来。
7.4 监控本身的基础设施守护
最后提醒一点:监控系统本身也是系统。Prometheus、Grafana、Loki 都是额外引入的服务,它们同样会出现磁盘不足、进程挂掉、版本升级不兼容等问题。这里我建议至少开一个最小的 HTTP 探活,用外部监控(比如云平台的健康检查)定期请求这几个服务的 metrics 端点,一旦失败就马上通知到人。我在所有环境里都会保证 Prometheus 和 Grafana 的数据目录和程序目录分离,落到系统盘和数据盘两个独立磁盘上,避免监控把数据盘写满后,顺手拖垮了业务应用所在的公共存储。
8. 我的落地效果与下一步优化方向
整套体系上线跑了一个季度之后,效果可以用一组直观数据来衡量。之前平均每周要处理两到三次用户反馈的“系统卡顿”,基本都靠事后翻日志定位;现在通过 Grafana 看板加告警,基本在用户意识到问题之前就已经介入,大多数性能劣化在告警推送后的十几分钟内就被摆平了。最能说明问题的一次是在某次数据清洗任务执行后,磁盘剩余空间从 20% 骤降到 8%,告警在阈值触发后 10 秒内就推到了钉钉群,值班同学登录服务器扩容时,整个业务系统还能正常响应,没有造成任何用户侧不可用的情况。
数据库连接池也做过一次成功调优。此前 Druid 最大连接数配置成了 50,但业务高峰期的并发查询经常把连接全部打满,新请求只能排队等待,接口 P95 一度冲到 4 秒以上。通过看板的连接池活跃连接数趋势,我把最大连接数调整到 100,同时把部分高频查询接口的 SQL 做了拆分和索引优化,P95 稳定降到 800 毫秒左右。如果没有监控数据做依据,这种调整就只能靠猜,改来改去也不一定有效。
接下来我计划把监控体系再往前走两步。第一步是接入 trace 链路,因为当前告警能看出一条请求从哪里开始变慢,但没法回答“慢是因为 Gateway 转发、业务代码还是数据库”这个问题。链路追踪选型我倾向于用 SkyWalking,它和 Spring Boot 的集成比较成熟,对 Druid 连接池和 Web 请求的埋点支持很好,接入成本可控。第二步是提升告警的自愈能力,比如磁盘空间不足时自动清理过期日志,服务挂掉时通过 Supervisor 自动拉起进程,减少人工介入的次数。监控的最终目标不是让我们多收几条告警,而是让团队从“救火”状态中逐渐解放出来,把时间投入到真正有价值的事情上。
