Prometheus + SpringBoot 监控体系搭建:从指标采集到告警推送
2026/9/13 16:42:15 网站建设 项目流程

先说个真事。上个月深夜两点半,手机突然开始连环震。群里甲方项目经理连着发了好几条语音,语气焦灼:订单查询接口大面积超时,前端页面直接白屏,用户电话快打爆了。我爬起来查日志、看中间件状态、看数据库连接数,折腾了快四十分钟才定位到是连接池被一条慢 SQL 打满。事后复盘才发现,这台 SpringBoot 服务从上线起就没有接入任何监控系统——CPU 飙到多少不知道,GC 频率有没有异常不知道,接口 P99 延迟曲线更是查无可查。全靠用户投诉来触发故障响应,这体验实在酸爽。

那次之后,我把 Prometheus 监控 SpringBoot 应用的整套链路重新梳理了一遍,从指标采集、自定义埋点、Grafana 可视化到告警推送,一步不落做完了。这篇就把我的完整方案和踩坑记录写出来,适合刚准备给 SpringBoot 服务上监控、或者已经接了 Prometheus 但只会看个 CPU 使用率的同学参考。我会把为什么这么配、参数为什么这么设也一并讲清楚,而不是丢给你一堆配置文件就不管了。

1. 为什么偏偏是 Prometheus + SpringBoot:这套组合解决的三个核心问题

先回答一个很多人不好意思问的问题:市面上监控方案那么多,Zabbix、SkyWalking、CAT 都有人在用,为什么 SpringBoot 监控最主流的组合偏偏是 Prometheus?

1.1 拉取模型天然契合云原生部署形态

Prometheus 的核心设计是 pull 模型——由 Prometheus 服务端定期去各个目标地址抓取指标。这和传统 push 模型的监控方案有本质区别。对于微服务架构,服务实例的扩缩容是常态,Prometheus 配合服务发现机制,新实例起来就自动纳入采集,实例下线就自动从监控目标里摘除,全程不需要在业务代码里关心"往哪里上报"这个问题。

SpringBoot 这边刚好有个天然的配合点:Actuator 组件本身就暴露了 /actuator 端口,加一个 micrometer-registry-prometheus 依赖后,/actuator/prometheus 就会输出 Prometheus 格式的指标数据。双方都是开放标准,几乎零成本对接。

不过 pull 模型也有个实际痛点,后面我会细说——计划任务类的短生命周期任务,跑几秒就退出了,Prometheus 还没来得及抓就没了。这是后话,先记住结论:常规的 Web 服务用 pull 足够,短任务需要额外方案。

1.2 Micrometer 把"指标方言"统一成了普通话

SpringBoot 应用并不是直接往 Prometheus 里写数据,中间隔着一层 Micrometer。这层抽象非常关键。Micrometer 是 JVM 平台的指标门面库,类似 Slf4j 在日志体系里的位置——你写代码时面向 Micrometer 的 API 埋点,实际输出到哪个监控后端,由运行时挂的依赖决定。

这意味着你今天用 Prometheus,明天想换成 InfluxDB、Datadog 或者云厂商的监控服务,业务代码一行都不用改,只要换一个注册中心的依赖就行。我在实际项目里确实遇到过客户要求从自建 Prometheus 迁移到云上托管版监控服务的情况,业务服务端的改动量基本为零,这就是 Micrometer 抽象层的价值。

另一个容易被低估的好处是:Micrometer 提供了一套统一的数据类型——Counter、Gauge、Timer、DistributionSummary。这套类型语义清晰,Timer 帮你自动记录耗时分布和计数,Counter 帮你处理单调递增的计数器逻辑,不需要你在业务代码里手动做防重置之类的操作。

1.3 生态完整度和二次开发空间

Prometheus 的生态优势在监控领域几乎是碾压级的。Grafana 对 Prometheus 数据源的支持最成熟,社区有大量现成的 SpringBoot 监控面板可以直接导入。告警方面 Alertmanager 支持路由树、静默、分组等机制,能应对复杂通知策略。

而且 Prometheus 的数据模型基于标签维度,这给了查询极大的灵活度。同一个指标 http_server_requests_seconds_count,你可以按 uri 分组看每个接口的请求量,也可以按 method 看不同请求方法的占比,还可以按 outcome 看成功和失败的数量对比。这些都是靠多维标签实现的,而 Zabbix 这类以主机为核心的传统监控,在业务维度的细粒度分析上就吃力得多。

所以我的结论很明确:新项目或者已有 SpringBoot 服务需要补充监控能力,Prometheus + Micrometer 这条链路是成本最低、上限最高的方案。下面直接进入实操。

2. 环境起底:Prometheus 安装、SpringBoot 接入与第一个指标

废话不多说,先把最小可用的监控链路跑通。这一节我按"服务端→业务端→验证"的顺序来,每一步都会给出配置文件和验证方法。

2.1 服务端:Prometheus 的选型和启动

Prometheus 是单二进制文件,下载解压就能跑,不依赖 JDK,也不依赖数据库。官网下载页面有 linux、windows、darwin 各个平台的包。我自己习惯用 Docker 方式部署,尤其是环境不止一套的时候,容器化最省心。

先提供一个最简的 docker-compose 配置:

version: '3.8' services: prometheus: image: prom/prometheus:v2.53.0 container_name: prometheus restart: always ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.retention.time=15d' volumes: prometheus-data:

启动之后浏览器访问 http://localhost:9090,能看到 Prometheus 自带的查询页面,就说明服务端没问题了。这时候还没接入任何采集目标,所以 Targets 页面是空的,Status 页面却能看到自身的监控指标。

存储保留时间这个参数值得多说一句。--storage.tsdb.retention.time=15d 是常用配置,但具体值要结合你的磁盘容量和数据精度需求来定。Prometheus 默认采集 15 秒一个点,单台服务几万个时间序列的情况下,一天的原始数据大约占用 1-2GB 磁盘。如果磁盘只有 50GB,保留 15 天可能都会紧张。我见过有团队直接把保留时间设成 90 天,结果磁盘告警比业务告警先爆了。

2.2 业务端:SpringBoot 引入两个依赖

SpringBoot 侧要引入的依赖非常明确。以 Maven 为例:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>

SpringBoot 的依赖管理 BOM 会帮你选好 micrometer-registry-prometheus 的版本,所以这里不需要写 version。有一点要注意:SpringBoot 2.x 用的是 Micrometer 1.x,SpringBoot 3.x 用的是 Micrometer 1.10+,它们之间的包名路径有差异(javax 和 jakarta)。你只要保证 actuator 和 registry 都通过同一个 SpringBoot 版本引入,版本冲突的问题基本不会出现。

引入依赖后,还有一步关键配置——开放 Actuator 的 Web 端点。SpringBoot 2.x 之后,Actuator 的所有端点默认只暴露 health,其他端点包括 prometheus 都需要显式配置才开放。

在 application.yml 里加入:

management: endpoints: web: exposure: include: health,info,prometheus metrics: tags: application: order-service

这里 management.metrics.tags.application 是个非常有用的全局标签配置。它会把这个标签附加到该应用暴露的所有指标上,以后你在一个 Prometheus 实例里监控多个服务时,就能用 application 标签区分指标来源了。建议从第一天就加上,别等指标多了再补。

2.3 验证链路:三个必须看的点

依赖配好、应用启动后,验证链路有三个关键点:

第一个验证点是 Actuator 端点是否真的输出 Prometheus 格式数据。浏览器或 curl 访问 http://localhost:8080/actuator/prometheus,能看到类似这样的输出片段:

# HELP jvm_memory_used_bytes Used bytes of a given JVM memory pool. # TYPE jvm_memory_used_bytes gauge jvm_memory_used_bytes{area="heap",id="G1 Eden Space",} 4.3356216E7 jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 3.8372408E7 # HELP http_server_requests_seconds # TYPE http_server_requests_seconds summary http_server_requests_seconds_count{exception="None",method="GET",status="200",uri="/actuator/prometheus",} 1.0

看到这段输出,说明业务端已经准备好了。里面已经能看到 JVM 内存指标、HTTP 请求指标。

第二个验证点是 Prometheus 侧能否成功拉取。修改 prometheus.yml,加入采集任务:

scrape_configs: - job_name: 'springboot-app' metrics_path: '/actuator/prometheus' static_configs: - targets: ['host.docker.internal:8080'] labels: instance: 'order-service-dev'

这里 target 的地址要特别注意。如果你的 Prometheus 跑在 Docker 容器里,而 SpringBoot 应用跑在宿主机上,那么 target 不能写 localhost。容器里的 localhost 指向容器自身,访问不到宿主机。Docker Desktop 环境可以用 host.docker.internal 这个特殊域名,Linux 部署则可能需要 --network=host 或者直接填宿主机 IP。这个坑我在早期部署时踩过一次,Prometheus 的 Targets 页面一直显示 DOWN,折腾半天发现是网络命名空间的问题,不是程序的问题。

配置改完之后,到 Prometheus 的 http://localhost:9090/targets 页面,能看到名为 springboot-app 的采集任务,State 是 UP,说明拉取成功。

第三个验证点是查询能力。回到 Prometheus 的查询页面,输入 jvm_memory_used_bytes 或者 process_cpu_usage 这类指标,点 Execute,能看到数据。到这里,从 SpringBoot 到 Prometheus 的最小链路已经通了,接下来才能聊更高阶的内容。

3. 指标不只是 JVM:Micrometer 的类型体系与命名规则

链路通了之后,下一个问题接踵而至:Actuator 自动暴露的那些指标,到底哪些有用?每个指标背后代表什么含义?这是很多初学者面对 Prometheus 页面上一大串指标时最懵的地方。

3.1 开箱即用的四类指标:JVM、系统、HTTP、数据源

Micrometer 和 Actuator 集成后,会自动注册一大批指标。按功能切分,大致可以分成四组:

第一组是 JVM 指标。jvm_memory_used_bytes 表示各内存池的使用量,jvm_gc_pause_seconds 记录 GC 耗时分布,jvm_threads_live_threads 是存活线程数。这一组指标的作用是在没有额外 APM 工具的情况下,快速判断 JVM 层面的健康状态。比如老年代内存持续增长且 GC 后不下降,大概率是内存泄漏的早期信号。

第二组是系统指标。process_cpu_usage 是进程 CPU 使用率,system_cpu_usage 是整机 CPU 使用率,process_uptime_seconds 是进程运行时长。这组指标里 process_uptime_seconds 很容易被忽略,但它对检测进程意外重启很有用——配合告警规则,进程一旦重启就能感知到。

第三组是 HTTP 指标。http_server_requests_seconds 是 summary 类型指标,下面有 _count 和 _sum 两个子序列,还带 uri、method、status、outcome 这些标签。这组指标是排查接口性能问题的核心。延迟分布信息在 _bucket 子序列里,需要用 histogram_quantile 函数算出 P95、P99 分位数。这部分后面 Grafana 章节我会给出完整 PromQL 表达式。

第四组是数据源指标。如果你的 SpringBoot 服务配置了 HikariCP 连接池(SpringBoot 2.x 后默认用它),Micrometer 会自动采集 hikaricp_connections_active、hikaricp_connections_pending 等指标。hikaricp_connections_pending 尤为重要,它表示等待获取连接的线程数,一旦持续大于 0,说明连接池被打满了。

3.2 四种基础类型:Counter、Gauge、Timer、DistributionSummary

Actuator 自动上报的指标只是 Micrometer 能力的一部分,真正体现这套体系价值的,是你可以用 Micrometer API 定义自己的业务监控。但要用好,先得理解这四种基础类型。

Counter 是单调递增计数器,常见误用是拿它记录当前值。比如你要统计"当前在线人数",用 Counter 就不合适,因为人数会降,而 Counter 只增不减。这种场景该用 Gauge,它的值可以任意上下波动,表示某一时刻的瞬时快照。内存使用量、队列长度、在线人数,都是 Gauge 的典型场景。

Timer 用来记录耗时和次数,内部会同时维护计数和总耗时两个量。它的核心价值在于可以计算平均耗时,再配合 percentiles 或 histogram 配置算分位数。我在项目里最常用的写法是给核心接口加 @Timed 注解,让方法级耗时自动进入 Timer。比如支付回调接口的耗时分布,直接关系到用户体验和上游超时重试的概率。

DistributionSummary 用于记录事件大小分布,比如订单金额、响应体大小。它和 Timer 的底层机制几乎一样,区别只是 Timer 专门处理耗时事件,DistributionSummary 处理任意数值事件。

3.3 命名规则与格式转换

Micrometer 有个好习惯:代码里用驼峰命名,输出到 Prometheus 时自动转换成下划线格式。比如我在代码里写 registry.counter("order.create.count"),输出到 Prometheus 的指标名是 order_create_count。

另一个容易迷糊的问题是单位。Prometheus 官方推荐指标名携带单位信息,Micrometer 在命名上会自动附加单位后缀和转换。你写 registry.timer("http.request.time"),输出时可能变成 http_request_time_seconds,单位秒会自动映射。这就避免了不同组件上报相同逻辑指标时单位混乱的问题。

还有一点关于 http_server_requests_seconds 的分位数。默认情况下,Micrometer 对 Timer 类型会生成一组 _bucket 序列,这些 bucket 是预定义的延迟档位。Prometheus 的 histogram_quantile 函数可以基于这些 bucket 算出近似分位数。这个近似值在数据量足够时非常准确。我第一次算 P99 时曾经对结果存疑,后来用压测工具对比,误差在可接受范围内,这才放心在告警规则里使用分位数。

4. 自定义业务指标:从埋点到 PromQL 查询实战

很多团队把 Prometheus 接好之后,监控内容长期停留在 JVM 和系统负载层面,业务指标一个没有。这有点可惜——框架层面的指标告诉你"服务健康",但业务指标才能告诉你"业务是否正常"。

4.1 注入 MeterRegistry,写一个最简单的自定义指标

SpringBoot 的自动配置会把 MeterRegistry 作为 Bean 管理起来。你可以在任意组件里注入它,然后开始埋点。

先看最常见的一个场景:统计下单请求量。代码大概长这样:

@Service public class OrderService { private final MeterRegistry meterRegistry; public OrderService(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } public void createOrder(OrderRequest request) { // 业务逻辑 // 埋点:订单创建成功 meterRegistry.counter("order.create.success", "channel", request.getChannel(), "region", request.getRegion()) .increment(); } }

这里的关键设计是标签。channel 和 region 是业务维度,Prometheus 会为每个标签组合生成独立的时间序列。查询时你可以按渠道过滤、按区域聚合,这比单纯统计一个总数值的信息量大得多。

再看看耗时的场景。用一个 Timer 记录外部支付接口的调用耗时:

Timer timer = Timer.builder("pay.gateway.request") .publishPercentiles(0.5, 0.95, 0.99) .register(meterRegistry); String result = timer.record(() -> payGateway.call(request));

publishPercentiles 配置会在 Prometheus 端生成 pay_gateway_request_seconds{quantile="0.99"} 的序列,这是 client-side 分位数估算。它的好处是不用写 histogram_quantile 就能直接查 P99,缺点是每个 quantile 都是额外的时间序列,会略微增加存储压力。客户端分位数和服务器端分位数各有优劣,我个人的习惯是:核心场景用客户端分位数方便快速开发,需要精确分析时再用 histogram_quantile。

4.2 @Timed 注解:埋点也能很优雅

如果不想在每个方法里手动写 Timer 的创建和记录逻辑,Spring 提供了 @Timed 注解这种声明式方案。在方法上加上注解即可:

@Timed(name = "order.create.time", description = "Time spent creating order", percentiles = {0.5, 0.95, 0.99}) @PostMapping("/orders") public OrderResponse createOrder(@RequestBody OrderRequest request) { // ... }

要注意的是,@Timed 注解默认不会生效。Micrometer 的注解支持需要额外启用,SpringBoot 应用里需要配置:

@Configuration public class MetricsConfig { @Bean public TimedAspect timedAspect(MeterRegistry registry) { return new TimedAspect(registry); } }

同时要打开 Spring 的切面代理能力。如果类没有被 Spring AOP 代理(比如 private 方法,或者同类内部方法调用),@Timed 就不会触发。我遇到过把 @Timed 放在 private 方法上的情况,运行半天后发现一个指标都没有,排查了半天才发现是代理问题。注解只能放在 public 方法上,且最好从外部调用,这是 Spring AOP 的常识,但在埋点场景里特别容易被忽略。

4.3 标签基数:最隐蔽的性能陷阱

自定义埋点时最容易埋下的雷是标签基数失控。每个标签组合都会产生一条独立的时间序列,如果标签的可选值非常多,时间序列数量会爆炸式增长。

举一个反面教材。有人给用户维度的指标打了 userId 标签,然后在 Prometheus 里面按用户维度去查。一百万个用户就意味着一百万条时间序列,每条序列每 15 秒产生一个点,一天的存储量轻松上百 GB,查询响应也会被拖垮。

正确的做法是给标签值做归约。比如渠道标签只需要十几种枚举值,这是安全的。如果需要按用户维度分析,建议预聚合之后输出到单独的指标,而不是让 Prometheus 直接采集高基数标签。我通常会把标签值的基数上限控制在几千以内,如果一上来就设计万级基数的标签,这个方案大概率会被打回重构。

4.4 PromQL 查询实战:从指标到结论

指标埋完之后,PromQL 是把数据变成结论的最后一公里。这里给出几个我平时使用频率最高的查询表达式,可以直接抄走。

第一个是接口 P99 延迟:

histogram_quantile( 0.99, sum by (le, uri) ( rate(http_server_requests_seconds_bucket{application="order-service"}[5m]) ) )

这个查询的本质是先计算每 5 分钟的请求速率,然后按 uri 和 le(bucket 上限)聚合,最后通过 histogram_quantile 估算 P99 值。

第二个是接口错误率:

sum( rate(http_server_requests_seconds_count{status=~"5.."}[5m]) ) / sum( rate(http_server_requests_seconds_count[5m]) )

分子是 5xx 状态码的请求速率,分母是所有请求的速率,相除即得错误率。

第三个是 JVM 内存使用率:

sum(jvm_memory_used_bytes{area="heap"}) / sum(jvm_memory_max_bytes{area="heap"})

这个查询用于判断堆内存是否接近上限,搭配告警规则可以有效预判 OOM 风险。需要注意的是 jvm_memory_max_bytes 对部分内存池可能是未定义值,查询结果里会出现 NaN 的序列,可以通过增加条件过滤掉。

PromQL 的语法细节很多,但核心思路是:先用 rate 把计数器变成速率,再用 sum by 按维度聚合,最后用 histogram_quantile 处理分位数。把这个套路记下来,大部分业务查询场景都能覆盖。

5. Grafana 可视化:从一键导入到按需定制面板

指标采集上来之后,面向报表展示的最后一环就是 Grafana。Grafana 本身不存数据,它只是从 Prometheus 拉取数据并渲染成图表。但它的可视化能力,直接决定监控数据能不能被团队里的其他人看明白。

5.1 数据源配置与现成面板导入

Grafana 安装方式同样推荐 Docker:

grafana: image: grafana/grafana:10.4.0 ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin123

启动后访问 3000 端口,默认账号 admin,密码通过环境变量设置。第一次登录建议先改密码。

接着在 Grafana 里添加 Prometheus 数据源,URL 填 http://prometheus:9090 或者宿主机 IP。要注意,如果 Grafana 也跑在容器里,这里同样存在 localhost 指向容器自身的问题。容器网络内可以用服务名 prometheus,跨容器网络则用宿主机 IP。

添加数据源之后,最省事的方法是导入现成的 SpringBoot 监控面板。Grafana 的 Dashboards 页面里,通过 Import 输入面板 ID 可以直接从官方库拉取。我经常用的是 12900 这个 ID,它覆盖了 JVM 内存、GC、HTTP 延迟、线程状态等最常见的维度。还有 4701 这个经典面板,适合 2.x 版本的 SpringBoot 应用,展示维度也很全。

导入面板后要做两件事:更新数据源为刚配置的 Prometheus,还有确认面板里的指标过滤条件是否与你的服务匹配。如果你在 application.yml 里加了 metrics.tags.application 标签,面板查询里通常也需要加上 application=xxx 的过滤条件,否则会把所有服务的指标揉在一起展示,完全没法看。

5.2 定制一个自己的核心服务视图

现成面板的问题是信息太全,什么都有,反而不利于聚焦。我建议团队在现成面板基础上,再定制一个"核心服务健康看板",只放最关键的几块:

  • 当前请求 QPS 和错误率趋势
  • 核心接口 P95/P99 延迟
  • JVM 堆内存和非堆内存使用量
  • 活跃线程数和阻塞线程数
  • 数据源连接池活跃连接数和等待线程数
  • 进程 CPU 使用率和文件描述符数量

每一个面板背后都对应一个 PromQL 查询。比如活跃线程数:

jvm_threads_live_threads{application="order-service"}

连接池等待线程数:

hikaricp_connections_pending{application="order-service"}

这里的核心价值在于关联分析。接口 P99 延迟持续走高,同时 hikaricp_connections_pending 也在涨,基本可以判断是数据源连接池不够用了;如果再看到活跃连接数稳步上升,说明可能是慢 SQL 占用连接。三块面板放在同一行,问题定位效率会高很多。

5.3 面板变量的使用技巧

很多人忽略了 Grafana 的模板变量功能。模板变量可以让你在面板顶部做一个下拉选择,动态切换查询条件。比如配置一个 LabelValues 类型的变量 app,取值来自 Prometheus 的 application 标签枚举值,然后所有查询面板都通过 $app 变量引用。这样一套监控面板,就能复用到所有 SpringBoot 服务上。给每个微服务各配一套面板的做法不是不行,只是维护成本会随着服务数量线性增长,变量方案一劳永逸。

6. 告警规则设计:让 Prometheus 替你值夜班

监控的目的不是让你没事就盯着一堆曲线看,而是在异常发生时能第一时间收到通知。告警规则设计得好,能把"事后救火"变成"事前预警"。

6.1 一条告警的完整生命周期

一条告警从触发到通知,在 Prometheus 体系里要经过两道关口。Prometheus 本身只负责评估规则,当规则表达式持续为真超过设定时间,它就把这条告警标记为 Pending,再过一段规则中的 for 时间(比如 2 分钟),状态变为 Firing。然后,Firing 的告警才会被推送到 Alertmanager,由它负责路由、去重、静默,最后通过邮件、Webhook、钉钉、企业微信等方式通知人。

所以写告警规则其实落在两个文件里:Prometheus 的规则文件和 Alertmanager 的配置。规则文件语法如下:

groups: - name: springboot-app rules: - alert: InstanceDown expr: up{job="springboot-app"} == 0 for: 1m labels: severity: critical annotations: summary: "{{ $labels.instance }} 已停止" description: "{{ $labels.instance }} 已停止超过1分钟,请立即处理"

expr 里的 up 指标是 Prometheus 自动生成的目标健康标志,up == 0 表示采集失败。这个告警规则是每个项目都必须有的基础告警——服务都挂了还不知道,其他监控就没意义了。

6.2 针对 SpringBoot 场景的四条经典告警规则

除了进程存活告警,我在 SpringBoot 监控中常用这几条规则:

第一类是接口延迟告警。面向核心接口,用 P99 超过阈值触发:

histogram_quantile( 0.99, sum by (le, uri) ( rate(http_server_requests_seconds_bucket{uri="/api/orders"}[5m]) ) ) > 2

for: 5m

这条规则的意思是:如果 /api/orders 的 P99 延迟连续 5 分钟超过 2 秒,就触发告警。for 参数非常关键,它能过滤掉瞬时抖动。没有 for 的规则会频繁触发、频繁恢复,容易让人对告警麻木。

第二类是错误率告警:

sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) / sum(rate(http_server_requests_seconds_count[5m])) > 0.05

for: 3m

这个表达式和 Grafana 面板里查错误率的表达式一样,只是加上阈值和持续时间作为触发条件。

第三类是 GC 耗时告警。JVM 的 Full GC 时间过长会直接造成服务长时间停顿:

rate(jvm_gc_pause_seconds_sum{action="end of major GC"}[5m]) > 1

for: 5m

第四类是堆内存使用率告警。当堆内存长期超过 85% 时,OOM 风险很高:

sum(jvm_memory_used_bytes{area="heap"}) / sum(jvm_memory_max_bytes{area="heap"}) > 0.85

for: 10m

这类指标的告警规则不能太敏感,因为 JVM 堆内存有自动伸缩机制,监控曲线本身就有波动。我用等 10 分钟再触发,能有效减少误报。

6.3 Alertmanager 配置与通知

规则文件写好之后,在 prometheus.yml 里引用它:

rule_files: - 'alert_rules.yml' alerting: alertmanagers: - static_configs: - targets: ['alertmanager:9093']

Alertmanager 的配置核心是路由。举个简单的例子,把告警按严重级别分发到不同接收人:

route: group_by: ['alertname'] group_wait: 10s group_interval: 5m repeat_interval: 4h routes: - match: severity: critical receiver: pager-duty - match: severity: warning receiver: email-team receivers: - name: pager-duty webhook_configs: - url: 'http://your-webhook/critical' - name: email-team email_configs: - to: 'dev@example.com'

告警的 group_by 参数会创建分组,同一组的告警会合并成一条通知,避免故障发生时几十条告警同时轰炸邮箱。repeat_interval 表示同一条告警在持续期间多久重复通知一次,我一般设成 4 到 8 小时,不打扰值班人员休息。

告警规则的阈值设计需要反复调优。我的经验是:初版阈值宁可宽松一点,先积累一两周的真实数据,再根据曲线和实际故障情况收紧。一上来就设置极端敏感的阈值,很容易变成"狼来了"的故事,最后没人理会告警。

7. 生产环境踩坑记录:六个常见问题与排查思路

最后这部分,我把实际运维中踩过的坑以及对应解法整理出来。这些坑都很典型,基本每个团队把 Prometheus 接入 SpringBoot 后都会遇到其中的一两个。

7.1 端点暴露过宽造成的安全问题

Actuator 端点的暴露配置是有安全隐患的。常见的坑是有人图省事,用 management.endpoints.web.exposure.include=* 把全部端点暴露出来。这里最危险的是 heapdump 端点——它能直接下载 JVM 堆快照,堆快照里包含密码、Token、用户隐私数据等敏感信息。网上确实有因为 heapdump 暴露导致数据泄露的真实案例。

我的建议是遵循最小化原则,只暴露真正需要的端点:

management: endpoints: web: exposure: include: health,info,prometheus,metrics

同时,如果服务端口能对公网访问,建议用独立的 management 端口,甚至配置 basic auth 或 IP 白名单。监控数据本身也属于内部信息,暴露路径越窄越安全。

7.2 指标时间序列膨胀

前面提到标签基数问题,这里给出具体的数据:一条时间序列每秒约消耗 10 字节不到的存储空间,但一万条序列的采集数据量就是另一回事了。Prometheus 的 CPU 和内存消耗与时间序列数量强相关,序列膨胀到百万级别时,一个 Prometheus 实例很难扛住。

实际项目里最典型的高基数来源有两个。一个是 HTTP 接口的 uri 标签——如果接口路径带路径参数,比如 /api/user/123,Prometheus 会为每个 ID 生成一条序列。解决方法是给 SpringBoot 配置 URI 标签的归一化,把路径参数替换成变量名:

management: metrics: web: server: request: ignore-trailing-slash: true

不过更根本的方式是在代码层面控制接口路径设计,规范化的路径占位符对监控系统更友好。

另一个高基数来源是业务指标里打了用户 ID、订单号这类标签。前面已经说过,这类标签必须归约或者走预聚合。

7.3 短生命周期任务采集不到

Pull 模型有个天然盲区:执行 cron 任务、批处理任务时,Java 进程启动后几秒或几分钟就退出,Prometheus 还没来得及抓取。处理这个场景有几种方案:

一种是用 Pushgateway 中间组件,任务结束时把指标推送到 Pushgateway,由 Prometheus 从 Pushgateway 拉取。但 Pushgateway 的指标是有状态的——上次任务推上去的指标不会自动消失,下次任务没跑的话,旧值会一直存在。这很容易引起误判。我见过团队在 Pushgateway 上配置过期清理,但这本身就是一个新的维护负担。

另一种方案是把任务指标直接上报到同一个 Prometheus,通过增加一个 standalone 进程或让任务在自身生命周期里强制暴露一段时间。相比之下更简单的是改成 push 到现有的 influxdb 或云端,又偏离了主题。这个场景的务实建议是:如果你的批处理任务很关键,用 Pushgateway 但一定做好清理机制;如果任务不是核心链路,优先级可以排后。

7.4 采集时间不同步造成的查询偏差

Prometheus 的默认抓取周期是 15 秒,Grafana 的默认查询区间有内置的步长计算逻辑。如果你在 Grafana 里把时间范围选得很窄,比如最近 5 分钟,而查询步长被计算成 15 秒,那么只有 20 个数据点,绘制的曲线会很粗糙,延迟尖峰可能被平均掉。

解决方法是手动调整 Dashboard 面板里的 Min step 参数,或者在查询里显式设置 step。这里的关键是:监控系统的精度上限是抓取周期决定的,Prometheus 的 15 秒采集频率意味着秒级以下的波动天然会被平滑,不要指望用这个粒度去分析极端瞬时的抖动。真有这种需求,应该靠链路追踪系统而不是监控系统。

7.5 多环境共用一个 Prometheus 时的标签冲突

如果开发环境、测试环境、生产环境的多个 SpringBoot 服务都向同一个 Prometheus 上报指标,并且服务名相同,那么 instance 和 application 标签就没办法区分环境。你会看到不同环境的指标搅在一起,告警也不知道在报哪个环境的服务。

这个问题的解法在接入的时候就要想好。每个环境起一个独立的 Prometheus 实例,或者至少在 scrape_configs 里给不同环境添加环境标签:

- job_name: 'springboot-prod' metrics_path: '/actuator/prometheus' static_configs: - targets: ['prod-server:8080'] labels: environment: prod

查指标时通过 environment 标签过滤,告警规则同理。

7.6 业务指标没人看、告警没人管的后续问题

技术问题解决完之后,还有一个组织层面的常见坑:面板搭好了,告警配置好了,过两周之后没人再看。原因多半是监控内容与业务方的关注点脱节,或者告警噪音太大。

我的建议是:每个服务至少选一个业务核心指标——比如订单服务的"下单成功率"、支付服务的"支付网关耗时"——把它放在监控面板最显眼的位置,同时配一条独立的告警规则。当监控真正帮团队提前发现过一次问题后,运维侧和业务侧对这套系统的信任度才会有质的变化。

结个尾:这套监控体系我的最终体会

从凌晨三点接电话的狼狈,到这套 Prometheus + SpringBoot 监控体系落地,我最大的体会是:监控系统不能只当成"基础设施"来搭,它决定了你在故障发生时的响应速度和底气。把采集做通只是第一步,真正花时间的是设计有意义的自定义指标、调优告警阈值、维护标签基数治理,这些工作没有一项是装个软件就能自动完成的。

最后分享一个小习惯:我每接一个新服务,会先把四类基础指标(JVM、线程、HTTP、数据源)的自动化检查纳入发布流程,上线前跑一遍验证采集路径,确认 Prometheus 的 Targets 是 UP、关键指标有数据,再提交发布确认单。这个动作只要十分钟,但可以省掉很多"上线后才发现监控没接"的尴尬。监控系统不是一次搭完就结束的,它是跟着业务一起演进的,持续优化才有持续价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询