可观测性:Spring Boot Actuator & Prometheus & Grafana
- 尽管系统健壮,但它依旧是一个“黑盒子”。当压力来临时,无法知道瓶颈在哪里,无法知道系统内部发生了什么。
- V2.3引入可观测性,用数据说话。
配置 Spring Boot Actuator
配置过程
- 添加Maven依赖,在pom.xml文件中添加如下内容,保存文件,并让Maven重新加载依赖。
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency> - 修改application.properties,需要告诉Actuator,允许通过Web访问Prometheus这个端点,并为所有指标打上一个统一的标签,方便后续在Grafana中筛选。
1
2
3
4
5
6
7
8
9
10
11# ================== Application Info ==================
# 定义应用名称,这个名称将被用作监控标签
spring.application.name=seckill-system
# ================== Actuator Configuration ==================
# 暴露所有 Web 端点(在生产环境中应按需暴露,* 是为了开发方便)
# 确保 prometheus 在其中
management.endpoints.web.exposure.include=*
# (最佳实践) 为所有从本应用出去的指标,都自动打上一个名为'application'的标签
management.metrics.tags.application=${spring.application.name} - 验证:启动应用,在浏览器中访问http://localhost:8080/actuator/prometheus,有一个纯文本页面,显示了大量的指标数据。
Actuator的工作过程
- 自动化扫描:当添加了相关依赖并启动应用时,它会扫描项目中的“类路径”,为所有的关键组件(DB、Redis、Tomcat…)安装探针。
- 持续数据采集:Actuator通过Micrometer持续的从探针中读取数据,并把他们转换成一种标准格式,存在内存的一个注册表中。
- 对于web请求:Spring Boot会自动配置一个过滤器Filter,JMeter发来的每一个HTTP请求,在进入Controller和离开之后,都会经过这个过滤器。过滤器会记录下请求的耗时、URL、HTTP状态码等信息,再喂给Micrometer。
- 对于数据库连接池:HikariCP连接池本身就提供了获取“活跃连接数”、“空闲连接数”等状态的方法。Actuator会定期调用这些方法,更新MicroMeter中的读数。
- 对于JVM指标:Actuator通过标准的JMX接口,从JVM获取内存使用、线程数量、GC次数等底层信息。
- 按需格式化输出: Micrometer 内存中已经有了所有最新的、标准格式的指标数据。现在需要把它们暴露出去,让外部系统能看到。当访问 /actuator/prometheus 时,之前加入的 micrometer-registry-prometheus 依赖开始工作。它从 Micrometer 的标准“注册表”中读取所有指标,然后将它们翻译成 Prometheus 能够理解的那种纯文本、带标签的特定格式。
- /actuator/metrics (JSON格式): 这是一个通用的、人类可读的公告栏,它用 JSON 格式列出了所有可用的指标名称。
- /actuator/prometheus (Prometheus格式): 这是特意开启的、一个专门给 Prometheus 看的公告栏。
- 被外部系统抓取:Prometheus 服务器每隔15秒,就会像一个爬虫一样,准时地来访问你应用暴露的 http://host.docker.internal:8080/actuator/prometheus 这个地址。它读取页面上所有的文本数据,解析它们,然后连同当前的时间戳一起,存入自己的时间序列数据库中。
配置 Prometheus
配置
- 添加Maven依赖,在pom.xml文件中添加如下内容,保存文件,并让Maven重新加载依赖。
1
2
3
4<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency> - 创建 prometheus.yml 配置文件,添加以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 全局配置
global:
scrape_interval: 15s # Prometheus 每隔 15 秒来抓取一次数据
# 抓取配置
scrape_configs:
# 第一个抓取任务,命名为 'seckill-app-host'
- job_name: 'seckill-app-host'
# 指定去哪里抓取指标
metrics_path: /actuator/prometheus
# 静态配置,定义抓取的目标
static_configs:
# 【核心】告诉 Prometheus,目标就在你的电脑主机上
# host.docker.internal 是一个特殊的 DNS 名称,在 Docker 容器内部会被解析为你电脑主机的 IP 地址
- targets: ['host.docker.internal:8080'] - 更新 docker-compose.yml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19services:
# ... 已有的 redis 和 rabbitmq 服务 ...
# 【新增】Prometheus 服务
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090" # 将 Prometheus 的 Web 界面暴露到主机的 9090 端口
volumes:
# 将我们刚创建的配置文件,挂载到容器内部
- ./prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- monitor-net # 加入到我们之前定义的网络中
# 【关键】手动为容器指路,告诉它 host.docker.internal 在哪里
extra_hosts:
- "host.docker.internal:host-gateway"
# ... 你已有的 grafana 服务和 networks 定义 ...
Prometheus的工作过程
- 发现目标:Prometheus容器启动会,第一件事是读取并解析挂载给它的配置文件Prometheus.yml。
- 进行抓取:按照配置文件中的内容,每隔15秒(scrape_interval: 15s),抓取器会向配置文件中部署的
http://host.docker.internal:8080/actuator/prometheus发送一个HTTP GET请求。应用响应了这个请求,返回了一大段纯文本,充满了指标名称和数值。 - 数据解析与存储:Prometheus拿到这段纯文本后,解析器逐行读取进行解析,以 (指标+标签, 时间戳, 值) 的形式,高效地追加写入到它自己的时间序列数据库 (TSDB) 中。
- 周而复始:15秒后,Prometheus 会重复第二步和第三步,抓取新的数据,并记录下新的时间戳和值。随着时间的推移,每个指标都会在数据库中形成一条按时间排序的数据点序列,这就是“时间序列”。
配置 Grafana
配置
- 更新 docker-compose.yml 文件
1
2
3
4
5
6
7
8
9
10
11
12
13services:
# ... 已有的 redis, rabbitmq, prometheus 服务 ...
# 【新增】Grafana 服务
grafana:
image: grafana/grafana-oss:latest
container_name: grafana
ports:
- "3000:3000" # 将 Grafana 的 Web 界面暴露到主机的 3000 端口
networks:
- monitor-net
depends_on:
- prometheus # 确保在 Prometheus 启动后再启动 Grafana
Grafana的工作过程
- 用户发起请求,http://localhost:3000/…
- Grafana 服务端解析panel:Grafana 服务器收到请求,从自己的数据库中读取这个panel的配置,找到对应的PromQL查询。
- 向 Prometheus 发起Query:Grafana 服务器根据在数据源中配置的地址,向 Prometheus 服务器 (http://prometheus:9090) 发送一个 API 请求,即PromQL的查询语句。
- Prometheus 回答:Prometheus 收到查询后,它的 PromQL 引擎开始工作。在自己的时间序列数据库中,筛选出所有符合条件的时间序列数据。打包成一个结构化的JSON数据,并将其作为HTTP响应返回给Grafana服务器。
- Grafana 浏览器端展示: Grafana 服务器拿到 Prometheus 返回的 JSON 数据后,几乎原封不动地把它转发给了浏览器,浏览器中的Grafana前端开始工作,接收到这组JSON数据后,调用图形库,在panel区域将这些点连接起来,形成折线图。
- 自动刷新:每隔30秒,浏览器就会自动重复第二步到第五步的全过程,向 Grafana 服务器请求更新,从而拉取到最新的数据并重新绘画,让panel看起来是实时的。
启动和配置数据
- 运行命令
docker-compose up -d,启动Redis、RabbitMQ、Prometheus。Grafana这四个容器。 - 配置 Grafana 数据
- 登录: 访问 http://localhost:3000 (用户名/密码: admin/admin)。
- 添加数据源:
- 左侧菜单 ⚙️ Configuration -> Data Sources -> Add data source -> Prometheus。
- 在 URL 处填入:http://prometheus:9090。
- 点击 Save & test,你应该会看到绿色的成功提示。
- 创建仪表盘:
- 左侧菜单 + -> Dashboard -> Add a new panel。
- 在查询区域,切换到 Code 模式。
- 在输入框中,输入你的第一个查询语句,比如查看存活线程数。
- 启动应用,可以在Grafana仪表盘看到如下内容。

可观测的数据信息
Web 应用层
QPS/RPS(每秒请求数)
- 代表应用服务器每秒钟成功处理了多少个HTTP请求。
- 衡量系统入口流量的核心指标。
- Grafana 面板标题: API QPS (Requests/sec)
- PromQL 语句:
1
2// 计算所有URL的总QPS
sum(rate(http_server_requests_seconds_count{application="seckill-system"}[1m])) - 预期场景: 压测开始时,这条线会瞬间飙升到一个高位,反映 JMeter 施加的压力。因为我们是异步处理,这个值可以非常高。压测结束后会断崖式下跌。
API响应时间(P95延迟)
- P95 (95th percentile) 响应时间意味着“95%的请求,其响应时间都低于这个值”。
- 这是一个比“平均值”更能反映用户真实体验的指标。
- Grafana 面板标题: API Response Time P95 (ms)
- PromQL 语句:
1
2// 计算 P95 响应时间,并转换为毫秒
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{application="seckill-system"}[1m])) by (le)) * 1000 - 预期场景: 由于你的 V2.x 架构是异步的,Controller 提交任务后立刻返回,所以这个值在整个压测期间应该始终保持在一个非常低的水平(比如 < 50ms)。如果它突然飙高,说明你的 Tomcat 前端线程池可能已经饱和,无法及时响应新请求。
数据库连接池
活跃数据库连接数
- OrderConsumerService 在某一时刻,正在占用的数据库连接数量。
- Grafana 面板标题: Active DB Connections
- PromQL 查询:
hikaricp_connections_active{application="seckill-system"} - 预期场景: 压测开始后,随着 RabbitMQ 消息被消费,这条线会快速上升。如果消费者并发数大于或等于连接池最大数,这条线会迅速触顶并保持在最大值,形成一条“天花板”。这条“天花板”就是数据库处理能力的明确瓶颈信号。
等待连接的线程数
- 有多少个线程因为连接池已满,正在排队等待获取一个空闲连接。
- Grafana 面板标题: Pending DB Connections
- PromQL 查询:
hikaricp_connections_pending{application="seckill-system"} - 预期场景: 在正常情况下,这个值应该永远是 0。如果它在压测时持续大于 0,这是一个非常严重的警告,说明数据库连接池已经完全饱和,成为了系统的瓶颈,大量消费者线程都在无效等待。
JVM 健康情况
JVM堆内存使用情况
- Java 应用占用了多少内存
- Grafana 面板标题: JVM Heap Memory Usage
- PromQL 查询:
jvm_memory_used_bytes{application="seckill-system", area="heap"} - 预期场景: 内存使用会随着请求处理而上升。会看到一条规律性的“锯齿状”曲线,每次下降都是一次垃圾回收 (GC) 的发生。只要整体趋势是平稳的,没有无限上涨,就说明内存健康。
系统CPU使用率
- Spring Boot 应用进程占用了多少 CPU 资源
- Grafana 面板标题: System CPU Usage
- PromQL 查询:
process_cpu_usage{application="seckill-system"} - 预期场景: 压测期间,CPU 使用率会显著升高。如果它长时间处于 90% 以上的高位,说明 CPU 可能是瓶颈之一。
业务线程池
线程池活跃线程数
- 在某一时刻,有多少个 seckill-thread-x 正在忙碌.
- Grafana 面板标题: App Thread Pool - Active Threads
- PromQL 查询:
1
2// 假设线程池 Bean 名字是 seckillExecutorService
executor_active_threads{application="seckill-system", name="seckillExecutorService"} - 预期场景: 压测开始后,活跃线程数会迅速上升到 corePoolSize (核心线程数),如果任务持续堆积,会进一步上升到 maximumPoolSize (最大线程数)。
线程池队列中的任务数
- 有多少个“下单任务”正在“任务等待区”排队
- Grafana 面板标题: App Thread Pool - Queued Tasks
- PromQL 查询:
executor_queued_tasks{application="seckill-system", name="seckillExecutorService"} - 预期场景: 压测开始的瞬间,这个值会迅速飙升,可能会达到设置的队列容量上限(如100)。随着后台线程的处理,这个值会逐渐下降。
一个成功的秒杀系统
正确性
- 核心指标:
- 绝不超卖:最终成功创建的订单数,必须小于或等于商品初始库存
- 数据最终一致性:压测结束后,Redis中扣减的库存数、MYSQL中创建的订单数。MYSQL中最终扣减的库存数这三者必须完全相等。
- 如何衡量:通过数据库查询来事后审计。
性能
- 核心指标:
- 高吞吐量(QPS/RPS):系统每秒钟能处理多少个秒杀请求,这是衡量系统容量的最核心的指标。
- 低响应时间(P95/P99):用户点击“秒杀”后,多长时间内能得到“成功”、“失败”或“排队中”的响应。
- 高成功率:在系统容量上限之内,所有的合法请求都应该被成功处理(即使结果是“已售罄”),而不应该服务器错误或超时而失败。
- 如何衡量:
- JMeter聚合报告:提供吞吐量、平均/P95响应时间、错误率等核心数据。
- Grafana Panel:实时、图形化地展示QPS和响应时间曲线。
高可用性与稳定性
- 核心指标:
- 无单点故障:系统的任何一个组件宕机,都不应该导致整个服务中断。
- 自动故障转移:在关键组件发生故障时,系统能在无人干预的情况下,在短时间内自动恢复服务。
- 过载保护:当流量超出系统极限时,系统不应该崩溃,而应该优雅地拒绝超出的请求(限流、降级)。
- 如何衡量:通过混沌工程的思想进行演习。
可扩展性
- 核心指标:
- 水平扩展能力:能否通过简单地增加机器(服务器实例),来线性的提升整个系统的处理能力。
- 如何衡量:
- 架构评审:分析系统是否存在“有状态”的节点,阻碍了水平扩展。
- 压测验证扩展是否有效。
可观测性
- 核心指标:
- Metrics (指标): 是否有对关键组件(JVM、连接池、线程池、QPS、延迟)的实时、量化监控。
- Logging (日志): 日志是否规范、有效,是否易于查询和分析(集中化日志)。
- Tracing (追踪): 在微服务架构中,能否追踪一个请求在多个服务之间的完整调用链路。
- 如何衡量:集中化监控panel。
安全性
- 核心指标:
- 防刷能力:能否有效识别和拦截来自脚本的、非法的、重复的高频请求。
- 如何衡量:分析压测日志。
