禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 项目编排

  • 场景设计

  • CI-CD

  • 架构设计

  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从micrometer计量器角度快速上手promQL
      • 写在文章开头
      • 案例项目前置说明
      • 详解各大计量器工作原理
        • counter(计数器)
        • 应用场景
        • 使用示例
        • 核心原理
        • guage(仪表类型)
        • 应用场景
        • 使用示例
        • 工作原理
        • timer
        • 应用场景
        • 使用示例
        • 工作原理
      • 详解promQL
        • promQL 指标的基本构成说明
      • promQL常见表达式
        • promQL核心概念
        • 匹配表达式
        • 逻辑表达式
      • 常见函数
        • 聚合函数
        • 时间样本分析常用函数
        • 更多关于promQL
      • 实践——promQL与grafana的串联
        • 长尾问题说明
        • 基于计数器分析http请求增量情况
        • 基于timer计时器分析接口耗时
        • 基于gauge分析CPU负载情况
      • 小结
      • 参考
    • 硬核安利一个监控告警开源项目Nightingale
  • 软件工程实践
  • 监控方法论
sharkchili
2026-03-25
目录

从micrometer计量器角度快速上手promQL

# 写在文章开头

因为近期工作比较忙碌,所以文章的更新相对慢了一些,近期笔者集成了一些比较核心的监控指标交由Prometheus采集,并通过promQL进行查询分析实现图表渲染。因为这一套完整的监控流程涉及计量器指标采集再通过Prometheus构建时间序列,再通过grafana结合promQL查询渲染,所以了解每一个环节的实现和理念,才能准确串联上述流程。

遗憾的是,就笔者近期了解的情况来看,这方面的资料要么面向全流程搭建的新手教程,要么就是非常突兀的promQL基本说明,并没有做到笔者所认为的全流程泛化梳理,所以笔者打算综合这些理念,结合一个比较有代表意义的案例将这些概念串联,以帮助读者更好的理念和运用监控。

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。

为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。

# 案例项目前置说明

本文通过spring boot web项目作为演示案例,所有的采集指标都会通过Prometheus数据源发布到rgafana上并通过promQL进行增强渲染,所以该项目主要会引入暴露springboot监控指标进行和prometheus套件依赖:

 <!--暴露spring监控指标-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>2.4.1</version>
        </dependency>

        <!--用于导出prometheus系统类型的指标数据-->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <version>1.1.4</version>
        </dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13

同时笔者也变写了一个测试的TestController:

  1. 声明计时器采集test0的耗时(本质上通过休眠模拟)指标
  2. 其余两个接口通过@Timed注解收集接口时间维度的各项指标
@RestController
@Slf4j
public class TestController {

    @Autowired
    private MeterRegistry registry;
    private Timer timer;


    @PostConstruct
    private void init() {
        //名称设置为http.timer,标签设置为uri为/hello,选用合适的名称辅助开发推断理解
        timer = Timer
                .builder("http.timer")
                .publishPercentiles(0.1, 0.5, 0.95) //发布百分位数区间
                .description("接口请求耗时统计") // 指标的描述
                .tags("uri", "/hello") // url标签指明为hello
                .register(registry);


    }


    @GetMapping("/test0")
    public String function() {
        timer.record(RandomUtil.randomInt(200), TimeUnit.MILLISECONDS);
        return "test0";
    }

    @GetMapping("/test1")
    @Timed
    public String test1() {
        ThreadUtil.sleep(RandomUtil.randomInt(200));
        return "test1";
    }

    @GetMapping("/test2")
    @Timed
    public String test2() {

        ThreadUtil.sleep(RandomUtil.randomInt(200));
        return "test2";
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

因为用到的计时器注解 @Timed,所以我们还需要配置TimedAspect创建注解的代理是使之生效:

@Configuration
public class TimedConfiguration {
    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
}
1
2
3
4
5
6
7

这里笔者也简单普及一下TimedAspect这个切面的工作原理,在springboot进行自动装配的时候扫描到TimedAspect,该切面会针对所有所有带有Timed注解的bean的方法做一个环绕增强,在连接点前后记录耗时并通过Timer记录耗时到计时器中:

对应的TimedAspect的切点实现timedMethod如下:

 @Around("execution (@io.micrometer.core.annotation.Timed * *.*(..))")
    public Object timedMethod(ProceedingJoinPoint pjp) throws Throwable {
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        Timed timed = method.getAnnotation(Timed.class);
        //通过注解获取方法的元信息
        if (timed == null) {
            method = pjp.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());
            timed = method.getAnnotation(Timed.class);
        }

        final String metricName = timed.value().isEmpty() ? DEFAULT_METRIC_NAME : timed.value();
        //启动计时器
        Timer.Sample sample = Timer.start(registry);
        String exceptionClass = "none";
				
        try {
        //调用方法
            return pjp.proceed();
        } catch (Exception ex) {
          //......
        } finally {
            try {
							//记录方法耗时
                sample.stop(Timer.builder(metricName)
                        .description(timed.description().isEmpty() ? null : timed.description())
                        .tags(timed.extraTags())
                        .tags(EXCEPTION_TAG, exceptionClass)
                        .tags(tagsBasedOnJoinPoint.apply(pjp))
                        .publishPercentileHistogram(timed.histogram())
                        .publishPercentiles(timed.percentiles().length == 0 ? null : timed.percentiles())
                        .register(registry));
            } catch (Exception e) {
                // ignoring on purpose
            }
        }
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

最后就是指标暴露和端口发布的配置:

server.port=8080
spring.application.name=web-service

# 暴露并开启所有的端点,Spring Boot Actuator会自动配置一个 URL 为 /actuator/Prometheus 的 HTTP 服务来供 Prometheus 抓取数据
management.endpoints.web.exposure.include=*
# 展示所有的健康信息
management.endpoint.health.show-details=always
# 默认/actuator/Prometheus,添加这个tag方便区分不同的工程
management.metrics.tags.application=${spring.application.name}
# Actuator 监控端点独立端口设置为 18080(与主应用端口分离)
management.server.port=18080
1
2
3
4
5
6
7
8
9
10
11

# 详解各大计量器工作原理

# counter(计数器)

# 应用场景

counter从名字即可了解这个计量器本质上是一个只增不减的计数器,它是有状态的(即依赖于历史的值),从使用方法上来看,它是单调递增的且上界是不可确定的,所以使用counter进行监控的指标一般是需要存在不断累加且需要针对累加的趋势进行分析的。

最典型的场景就是接口请求总数,例如我们需要针对上述的test1接口请求进行计数,从而构成时间序列存储这些数据,同时针对单位时间内这个接口增量趋势进行分析,主流的做法就是通过counter采集每一次请求,并将该指标通过prometheus交给grafana通过promQL进行即席查询分析:

# 使用示例

针对counter计数器的核心本质,即只要做到针对并发请求进行高效计数即可,其余一些分析维度的工作全部交由prometheus等数据源进行定期的采集分析即可,对应笔者项目中的应用方式就如下这个环绕切面的代码段:

  1. 拦截所有带有http注解的接口
  2. 拉取该接口方法名并生成标签
  3. 针对该url的counter进行累加
@Around("execution(@org.springframework.web.bind.annotation.GetMapping * *(..)) || " +
            "execution(@org.springframework.web.bind.annotation.PostMapping * *(..)) || " +
            "execution(@org.springframework.web.bind.annotation.RequestMapping * *(..))")
    public Object countHttpRequest(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //拉取接口方法名
        String url = extractUrl(method);
        //针对url创建切面
        Counter counter = Counter.builder("http_requests_total")
                .tag("url", url)
                .register(meterRegistry);
        //针对接口请求数进行累加        
        counter.increment();
      

        return joinPoint.proceed();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 核心原理

所以其实现的核心主要还是强调计数的准确性和高效性,考虑到counter在并发场景下更多是针对计数进行累加,且只有在grafana等监控系统查询时才需要获取计数值,所以针对这种写多读少且需要保证并发安全的场景。

所以counter底层采用了基于数组分散并发累加压力的计数器DoubleAdder:

对此我们可以查看counter底层的源码实现即PrometheusCounter的increment印证这一点:

public class PrometheusCounter extends AbstractMeter implements Counter {
    private DoubleAdder count = new DoubleAdder();

   //......

    @Override
    public void increment(double amount) {
        if (amount > 0)
        		//通过DoubleAdder完成并发累加
            count.add(amount);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13

# guage(仪表类型)

# 应用场景

guage也是我们常用的仪表盘,和counter有所不同,guage计数器可以增减,它是无状态的,即此刻的数值与历史数值并没有依赖关系,它更常用于观察带有上下界的指标,即侧重于那些系统状态的指标:

  1. cpu利用率
  2. 内存利用率
  3. 网络带宽

所以针对gauge的使用理念,我们也还是通过gauge采集单位时间下的指标的数值,然后交给grafana让其通过promQL分析其增减趋势亦或者上下浮动情况以准确针对系统情况进行深入分析:

# 使用示例

类似的我们通过spring boot的registry全局注册一个guage,通过原子类进行设置值,即直接通过原子类记录当前cpu使用率,让prometheus定期采集交给grafana进行即席查询分析:

AtomicInteger cost = registry.gauge("cpu.usage", Tags.of("core-number", "0"), new AtomicInteger(0));
        //随机数模拟cpu使用率
        cost.set(RandomUtil.randomInt(100));
1
2
3

# 工作原理

我们注册gauge的时候通过构造函数指明底层采用AtomicInteger进行数值维护,后续我们就可以直接操作这个原子类引用完成数值维护修改:

AtomicInteger cost = registry.gauge("cpu.usage", Tags.of("core-number", "0"), new AtomicInteger(0));
1

为什么可以采用原子类AtomicInteger呢?查看gauge底层实现,从源码可以看到该参数为泛型T只需继承Number类,保证获取数值时可以通过doubleValue方法返回值即可。

而AtomicInteger恰好继承Number类且保证并发计数安全,所以才适用于作为gauge底层的计数器:

@Nullable
    public <T extends Number> T gauge(String name, Iterable<Tag> tags, T number) {
  
        return gauge(name, tags, number, Number::doubleValue);
    }
1
2
3
4
5

结合这个泛型构造,我们也可以直接采用LongAdder作为gauge底层的计量器,由于其底层采用数组三列并发累加压力,所以更适用于作为监控计数的指标:

LongAdder gauge = registry.gauge("cpu.usage", Tags.of("core-number", "0"), new LongAdder());
1

# timer

# 应用场景

timer计时器主要是跟踪大量短耗时的事件进行多维度的采集,通过计时器统计某个事件耗时时,其底层会维护针对此事件:

  1. 事件总数:采用LongAdder维护
  2. 事件总耗时:同样采用LongAdder维护
  3. 事件耗时最大值:通过TimeWindowMax时间窗口进行维护

后续我们就可以通过prometheus构成时间序列将其交给grafana,此时我们就可以根据这些指标计算:

  1. 当前一段时间请求总数
  2. 当前一段时间的平均耗时
  3. 当前一段时间的最大值

# 使用示例

对应的用法上文已经介绍过,我们可以自定义注册一个timer,后续直接用这个timer的record方法记录耗时:

 @Autowired
    private MeterRegistry registry;
    private Timer timer;


    @PostConstruct
    private void init() {
        //名称设置为http.timer,标签设置为uri为/hello,选用合适的名称辅助开发推断理解
        timer = Timer
                .builder("http.timer")
                .publishPercentiles(0.5, 0.95) //发布百分位数区间
                .description("接口请求耗时统计") // 指标的描述
                .tags("url", "/test0") // url标签指明为hello
                .register(registry);


    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

耗时记录使用示例如下:

@GetMapping("/test0")
    public String function() {
        int sleepTime = RandomUtil.randomInt(200);
        ThreadUtil.sleep(sleepTime);
        timer.record(sleepTime, TimeUnit.MILLISECONDS);
        return "test0";
    }
1
2
3
4
5
6
7

# 工作原理

关于timer针对上述三个度量指标,从上文表述我们就知道大体就是通过:

  1. count记录请求总数
  2. totalTime记录总耗时
  3. max窗口工具类维护最大耗时

对应的我们也可以通过timer底层实现PrometheusTimer印证这一点:

public class PrometheusTimer extends AbstractTimer {
		//......
		//记录请求总数
    private final LongAdder count = new LongAdder();
    //记录总耗时
    private final LongAdder totalTime = new LongAdder();
    //窗口内记录最大耗时
    private final TimeWindowMax max;

	//......
}
1
2
3
4
5
6
7
8
9
10
11

当我们的通过timer记录本次接口耗时,record方法本质做的是:

  1. count原子自增请求总数
  2. totalTime原子累加记录总耗时
  3. max通过一个环形缓冲区维护1min以内请求的最大值

对应第一点和第二点都是简单的原子累加操作,这里就不多做赘述了,我们着重的说明一下最大耗时这个操作的底层工作原理,这个记录最大值的工具类TimeWindowMax本质上是用一个唤醒缓冲区实现(本质上就是一个数组),数组3个元素分别代表:

  1. 当前1min内的最大值
  2. 当前2min内的最大值
  3. 当前3min内的最大值:

我们都知道这个max计数器记录的都是当前1min内耗时最大的值,假设我们当前这分钟的最大值为200ms,那么ringbuffer[0]记录的最大值就是200ms。 注意:TimeWindowMax维护最大值是会遍历数组中每个元素进行比对,然后将最大值写入:

一旦ringbuffer[0]使用时间超过1min,例如当前时间是3:50距离ringbuffer[0]使用开始时间1:00已经超过170s,TimeWindowMax就会执行如下步骤:

  1. 计算时间差为170,已经超过ringbuffer[0]的窗口区间(当前1min内的最大值),所以将该原子类重置为0,指针移动到ringbuffer[1]
  2. ringbuffer[1]代表当前2min内的最大值,170s也大于其窗口时间区间120s,所以这个窗口也过期直接重置为0,指针移动到ringbuffer[1]
  3. ringbuffer[2]代表当前3min内的数组,对应窗口活跃保质期为180s大于170s,所以没过期

所以ringbuffer[2]这个窗口后续作为当前1min内的窗口,其他窗口循环重置后循环复用作为当前2min、3min内的窗口,这就是这个算法的巧妙所在:

对应的我们也可以通过源码印证这一点,可以看到timer底层调用record入口来自AbstractTimer,这个抽象类对外暴露recordNonNegative这个抽象方法,对应也就是我们的工具类PrometheusTimer的recordNonNegative方法:

@Override
    public final void record(long amount, TimeUnit unit) {
        if (amount >= 0) {
           	//......
           	//记录请求总数、耗时、最大值
            recordNonNegative(amount, unit);

           	//......
        }
    }
1
2
3
4
5
6
7
8
9
10

查看PrometheusTimer的recordNonNegative可以发现他做了如下三件事:

  1. counter自增维护请求总数
  2. totalTime累加计算总耗时
  3. max.record记录最大耗时
@Override
    protected void recordNonNegative(long amount, TimeUnit unit) {
    			//累计请求总数
        count.increment();
        long nanoAmount = TimeUnit.NANOSECONDS.convert(amount, unit);
        //累加总耗时
        totalTime.add(nanoAmount);
        //维护最大值
        max.record(nanoAmount, TimeUnit.NANOSECONDS);

       //......
    }
1
2
3
4
5
6
7
8
9
10
11
12

关于请求和耗时累计逻辑比较直观,笔者就不多做介绍了,步入max的record就可以看到核心所在:

  1. 调用rotate执行我们上述图解的窗口滑动算法整理三个窗口
  2. 基于必要整理重置后的窗口数组和当前耗时进行比对,维护最新的最大值
public void record(double sample, TimeUnit timeUnit) {
			//窗口旋转维护
        rotate();
        //遍历各个缓冲区并维护最大值
        final long sampleNanos = (long) TimeUtils.convert(sample, timeUnit, TimeUnit.NANOSECONDS);
        for (AtomicLong max : ringBuffer) {
            updateMax(max, sampleNanos);
        }
    }
1
2
3
4
5
6
7
8
9

查看rotate源码中可以看到,rotate就是实现窗口旋转的核心,其内部做了如下几件事:

  1. 它会获取当前时间距离上次窗口旋转时间,判断是否超期,若超过60s则说明存在过期窗口需要滑动窗口,进入步骤2
  2. cas上锁保证只有一个线程执行此操作
  3. 遍历各个元素,通过距离上次旋转时间timeSinceLastRotateMillis不断循环减去60s和durationBetweenRotatesMillis比较以做到

1. 第1次循环减去0个60,即查看第一个窗口得到的timeSinceLastRotateMillis是否超过60,若超过则说明过期
2. 第2次循环减去1个60,即查看第2个窗口得到的timeSinceLastRotateMillis-60s是否超过60(即是否超过2min),若超过则说明过期
3. ......
1
2
3
4
  1. 完成窗口重置和滑动后,将本次的耗时分别于各个窗口进行比对,如果比窗口值大则直接写入,这个算法比较巧妙,读者可以结合笔者的说明自行理解:


private void rotate() {
			//计算上次旋转窗口的时间
        long timeSinceLastRotateMillis = clock.wallTime() - lastRotateTimestampMillis;
        //如果没有超过60s则返回
        if (timeSinceLastRotateMillis < durationBetweenRotatesMillis) {
            // Need to wait more for next rotation.
            return;
        }
				//上个自旋锁保证并发互斥,进行窗口滑动操作
        if (!rotatingUpdater.compareAndSet(this, 0, 1)) {
            // Being rotated by other thread already.
            return;
        }

        try {
            int iterations = 0;
            synchronized (this) {
                do {
                	//重置当前窗口
                    ringBuffer[currentBucket].set(0);
                    //移动到下一个窗口,如果超过上界则回到索引0位置
                    if (++currentBucket >= ringBuffer.length) {
                        currentBucket = 0;
                    }
                    //减去60s查看这个窗口是否超过区间,因为是do while循环,所以多次循环就可以做到查看60s、120s(循环1次减去一个60和60进行比对)、180s(循环2次减去2个60和60进行比对)对应的3个缓冲区是否过期
                    timeSinceLastRotateMillis -= durationBetweenRotatesMillis;
                    //上次滑动窗口时间加上60s,即代表这个窗口区间理论上的旋转窗口时间
                    lastRotateTimestampMillis += durationBetweenRotatesMillis;
                    //
                } while (timeSinceLastRotateMillis >= durationBetweenRotatesMillis && ++iterations < ringBuffer.length);
            }
        } finally {
            rotating = 0;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 详解promQL

# promQL 指标的基本构成说明

在正式介绍promQL表达式之前,我们需要先针对Prometheus风格的指标构成进行一下必要的对齐:

  1. #号部分为必要的描述和注释说明,如下注释分别对应我们自定义的指标描述和Prometheus的计量器说明(本例则是guage)
  2. jvm_threads_states_threads为指标名称
  3. 后续{}部分则是针对jvm_threads_states_threads各个不同维度区分的标签
# HELP jvm_threads_states_threads The current number of threads having NEW state
# TYPE jvm_threads_states_threads gauge
jvm_threads_states_threads{application="web-service",state="blocked",} 0.0
jvm_threads_states_threads{application="web-service",state="waiting",} 23.0
jvm_threads_states_threads{application="web-service",state="runnable",} 11.0
jvm_threads_states_threads{application="web-service",state="timed-waiting",} 4.0
jvm_threads_states_threads{application="web-service",state="new",} 0.0
jvm_threads_states_threads{application="web-service",state="terminated",} 0.0
1
2
3
4
5
6
7
8

我们以jvm_threads_states_threads{application="web-service",state="blocked",} 0.0为例说明一下,这是一个典型的Prometheus风格的指标值,通过标签名限定当前指标的语义,例如jvm_threads_states_threads就代表不同状态的线程数,同时通过标签声明指标的维度,以该指标为例,则是通过应用名称application和状态state区分单指标下不同维度的数值,最后就是指标的数值:

# promQL常见表达式

# promQL核心概念

瞬时向量(Instant vector):一组时间序列上,每个时间上只有一个样本,他们共享相同的时间戳,即表达式的返回值只会包含该时间中的最新的样本值:

区间向量(Range vector):即一个时间范围内的每个时间序列包含一段时间范围内的样本数据:

时间向量:以时间为横坐标,序列作为纵坐标构成一组反应状态变化的向量图,该向量图通过定时周期性采集,随着时间的流逝生成一个离散的样本数据序列。 通过指标名称结合标签生成多条趋势线条,也就是多条时间序列,而序列也就是我们常说的vector:

# 匹配表达式

有了上述对于计量器的基本介绍,我们在针对promQL中几个比较常见的表达式和函数展开介绍,promQL中也存在着逻辑表达式,这其中涉及匹配表达式和逻辑表达式。

我们先来说说匹配表达式:

  1. 完全匹配:与字符串完全匹配即=
  2. 不匹配:与字符串不匹配即!=
  3. 正则匹配:与字符串正则匹配=~
  4. 正则反向过滤:与字符串正则不匹配!~

它可以针对多维度的指标进行筛选和检索,例如我们从spring actuator上看到jvm线程各个状态的指标及其对应的线程数:

# HELP jvm_threads_states_threads The current number of threads having NEW state
# TYPE jvm_threads_states_threads gauge
jvm_threads_states_threads{application="web-service",state="blocked",} 0.0
jvm_threads_states_threads{application="web-service",state="waiting",} 23.0
jvm_threads_states_threads{application="web-service",state="runnable",} 11.0
jvm_threads_states_threads{application="web-service",state="timed-waiting",} 4.0
jvm_threads_states_threads{application="web-service",state="new",} 0.0
jvm_threads_states_threads{application="web-service",state="terminated",} 0.0
1
2
3
4
5
6
7
8

默认情况下,它在Prometheus的console渲染显示如下:

如果我们希望只希望查看timed-waiting的线程数,此时我们就可以通过标签结合相等匹配器实现,对应的表达式为:

jvm_threads_states_threads{state="timed-waiting"}
1

此时视图就会准确过滤筛选出状态为timed-waiting的线程数:

同理过滤出状态非timed-waiting的表达式为:

jvm_threads_states_threads{state!="timed-waiting"}
1

同理,如果我们希望匹配r开头的表达式则是:

jvm_threads_states_threads{state=~"r.*"}
1

# 逻辑表达式

promQL也存在和各种逻辑运算的表达式匹配:

  1. and:即两个序列即上述的vector进行与运算产生新的集合,只有两个即可都存在的元素才会显示
  2. or:只要左右任何一边的vector表达式计算为真,就显示左右vector的所有元素
  3. unless:即左右两边的vector进行或运算构成新的并集,然后通过unless右边的vector进行过滤,将右边vector存在的元素移除

对此我们不妨距离说明,关于逻辑表达式我们以一个针对http请求数计算的指标http_requests_total为例进行演示,对应指标如下:

# HELP http_requests_total  
# TYPE http_requests_total counter
http_requests_total{application="web-service",url="/test2",} 1.0
http_requests_total{application="web-service",url="/test0",} 2.0
http_requests_total{application="web-service",url="/test1",} 340.0
1
2
3
4
5

如果我们希望查询请求数大于300且映射以test开头,在promQL表达式则是采用and,对应的表达式如下,最终的输出结果也是:

http_requests_total > 300 and http_requests_total{url=~"/test.*"}	
1

最终输出的也是test1:

同理如果希望查询请求数大于300或者映射为test0,则表达式如下:

http_requests_total > 300 or http_requests_total{url="/test0"}	
1

需要注意的是promQL表达式中的or并非短路运算,即表达式为真的情况下,左右vector都会输出,也就是大于请求数大于300和test0映射都会输出:

最后则是unless,相较于常规的逻辑表达式,该逻辑表达式的执行逻辑为将左右或运算得到交集后,结果交由右边过滤得出目标标签数据,例如我们需要查询出请求总数大于0但要排除test0,对应的表达式就如下所示:

http_requests_total > 0 unless http_requests_total{url="/test0"}	
1

对应的推算过程为:

  1. 将请求数大于0和/test0的指标通过或运算构成新集合即/test0、/test1、/test2
  2. 基于右边vector将非test0的元素过滤,最终得到/test1和/test2:

# 常见函数

# 聚合函数

接下来就是介绍一些比较常见的函数,和常见的sql语句一样,promQL也有如下常见内置函数:

  1. sum:指标求和
  2. avg:指标平均数
  3. max:指标最大值
  4. min:指标最小值

我们还是以http_requests_total为例,对应不同接口的请求总数如下:

# HELP http_requests_total  
# TYPE http_requests_total counter
http_requests_total{application="web-service",url="/test2",} 1.0
http_requests_total{application="web-service",url="/test0",} 2.0
http_requests_total{application="web-service",url="/test1",} 340.0
1
2
3
4
5

假设我们希望定位出http_requests_total的最大值,对应的就可以使用max(http_requests_total),其余函数同理,这些函数本质上就是基于当前指标通过函数聚合计算,比较简单笔者就不多做演示了。

# 时间样本分析常用函数

对于监控来说,我们更希望看到监控指标的整体趋势,观察系统的动态以便进行针对性的调优,这其中常见的函数有:

  1. max_over_time:指定一段时间的最大值
  2. avg_over_time:指定一段时间的平均值
  3. min_over_time:指定一段时间的最小值
  4. rate:计算指定时间范围内平均每秒增长率
  5. delta:观察系统一段时间指标上下浮动差

假设我们通过timer维护一份基于时间维度的各个接口耗时、请求总数、最大值等信息:

# HELP method_timed_seconds  
# TYPE method_timed_seconds summary
method_timed_seconds_count{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="test2",} 1.0
method_timed_seconds_sum{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="test2",} 0.032457149
method_timed_seconds_count{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="test1",} 840.0
method_timed_seconds_sum{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="test1",} 84.248322878
# HELP method_timed_seconds_max  
# TYPE method_timed_seconds_max gauge
method_timed_seconds_max{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="test2",} 0.0
method_timed_seconds_max{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="test1",} 0.198215155
1
2
3
4
5
6
7
8
9
10

若我们希望查看过去1h耗时的最大值分布,对应表达式为 max_over_time(method_timed_seconds_max[1h]),对应输出结果如下,其余平均值、最小值也是同理。

这其中还有用一个针对指标整体浮动变化的函数delta,例如我们有一个cpu的guage指标:

# HELP system_cpu_usage The "recent cpu usage" for the whole system
# TYPE system_cpu_usage gauge
system_cpu_usage{application="web-service",} 0.005636978579481398
1
2
3

如果我们希望通过cpu浮动情况判断程序资源消耗稳定性就可以通过delta即delta(system_cpu_usage[2h])检测过去2h的cpu浮动变化:

我们在介绍一下比较实用的函数,针对请求接口总数这种单向攀升的指标,我们也会关注它的增长趋势已判断服务器整体资源是否符合未来增长趋势,我们就可以通过rate函数来分析如下接口请求总数指标:

# HELP http_requests_total  
# TYPE http_requests_total counter
http_requests_total{application="web-service",url="/test2",} 1.0
http_requests_total{application="web-service",url="/test0",} 2.0
http_requests_total{application="web-service",url="/test1",} 840.0
1
2
3
4
5

对应表达式为rate(http_requests_total[1h]),对应输出发布图如下,我们可以非常直观的看到test1接口在单位时间内疯狂的攀升:

# 更多关于promQL

更多关于promQL的函数实践和演示,笔者也推荐这份教程: https://flashcat.cloud/tags/promql/ (opens new window)

# 实践——promQL与grafana的串联

# 长尾问题说明

监控的目的本质上是针对指标的趋势分析确保能够对系统有一个准确的决策优化思路,这其中就有一个比较经典的长尾问题,以我们监控接口耗时为例,1min内平均耗时为200ms,但是偶发出现5s,这种偶发波动对于rate等函数进行平均化之后就会被削平,从而无法及时的发现偶发飙升的数值进而无法及时发现问题,这种情况也就是长尾问题。

对于此类问题,我们就需要综合指标多维度针对指标进行图表分析,从而进行准确的进一步决策。我们还是以接口请求总数的指标为例,假设此时我们收到接口的请求总数counter情况如下,可以看到有大量请求打到test1上,所以test1的请求总数为910:

# HELP http_requests_total  
# TYPE http_requests_total counter
http_requests_total{application="web-service",url="/test2",} 1.0
http_requests_total{application="web-service",url="/test0",} 2.0
http_requests_total{application="web-service",url="/test1",} 910.0




1
2
3
4
5
6
7
8
9

针对这些接口,笔者也通过timer计时器针对性的进行指标采集,还是以test1说明:

  1. 请求总数为909(采集时间和上述有些误差)
  2. 请求总耗时为91s
  3. 最大耗时为199ms
# HELP method_timed_seconds  
# TYPE method_timed_seconds summary
# ......
method_timed_seconds_count{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="test1",} 909.0
method_timed_seconds_sum{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="test1",} 91.202510617



# HELP method_timed_seconds_max  
# TYPE method_timed_seconds_max gauge
# ......
method_timed_seconds_max{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="test1",} 0.199230275
1
2
3
4
5
6
7
8
9
10
11
12

对应的我们将http_requests_total写入粘贴到grafana上渲染后如下图所示:

# 基于计数器分析http请求增量情况

我们先通过http_requests_total对接口请求情况进行分析,从整体情况来看test1请求在不断的飙升,所以我们希望针对该接口增量趋势进行分析,于是键入rate(http_requests_total{url="/test1"}[1m])分析了test1接口的增长情况。 可以看到整体是一段时间一段时间的波动,按照实际业务场景可以是服务定时任务在单位时间内的feign请求:

# 基于timer计时器分析接口耗时

看到此波动,就需要关心这个接口的耗时情况,通过timer计时器的指标(method_timed_seconds_)筛选业务峰值的时间区间真是这种飙升的量级请求的各维度耗时进行汇总分析,以确定的接口飙升是否存在瓶颈。

首先我们需要查询接口最大耗时max_over_time(method_timed_seconds_sum[1h])查看过去1h的最大耗时,整体来看基本稳定在200ms以内,符合团队指定的标准:

明确没有存在瓶颈的情况下,我们也需要判断接口单位时间内的平均耗时已确定系统过去一段时间是否稳定运行,已确定程序或者系统是否存在波动,已明确是否有隐患,表达式为method_timed_seconds_sum{method="test1"}/method_timed_seconds_count{method="test1"},结合上述指标来看在00:10那一刻请求数飙升所以那段时间平均耗时增加,请求降下来后耗时也将下来了:

# 基于gauge分析CPU负载情况

最后我们还是需要通过分析一下cpu和内存使用情况已确定这种飙升对于系统的压力如何,我们直接通过max_over_time(system_cpu_usage[1h])查看最大开始也就是3%并没有超过业界认定的瓶颈70%,基本确定没有问题:

关于内存笔者这里也直接关联到jvm_memory_used_bytes这个guage,通过avg_over_time(jvm_memory_used_bytes[1h])查看过去1h的使用情况,可以看到在00:10新生代飙升到500m左右完成后直接压降:

老年带控制在30m以内稳定攀升,还未到达gc临界点,整体来看飙升的接口会很快被gc,所以系统整体情况良好:

为方便笔者直接通过jmap查看当前java进程情况,可以看到堆内存分配的2g左右的堆内存,此时的老年代也没用到最大值仅仅动态扩容到83MB,整体内存使用情况良好:

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 2065694720 (1970.0MB)
   NewSize                  = 42991616 (41.0MB)
   MaxNewSize               = 688390144 (656.5MB)
   OldSize                  = 87031808 (83.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 小结

本文深入分析了java常用计量仪micrometer中:

  1. 有状态累加计量器counter
  2. 无状态仪表盘gauge
  3. 大量短耗时时间指标采集工具timer

基于这些指标我们结合通过promQL函数进行多维度的演示并给出了日常生产故障分析和排查步骤,希望对你有帮助。

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。

为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。

# 参考

保姆级 Prometheus PromQL 讲解与实战操作 :https://cloud.tencent.com/developer/article/2242333 (opens new window)

Prometheus官方文档:https://prometheus.ac.cn/docs/prometheus/latest/querying/functions/ (opens new window)

长尾优化汇总 :https://juejin.cn/post/7170872097151909919 (opens new window)

Spring AOP统一功能处理(切面、切点、连接点、通知)(上) :https://developer.aliyun.com/article/1202048 (opens new window)

Micrometer源码分析 :https://juejin.cn/post/7254032949230469181#heading-15 (opens new window)

【博客488】prometheus-----长尾问题,跳变问题,数据外推问题,增量丢失问题:https://blog.csdn.net/qq_43684922/article/details/126814495#:~:text=本文深入探讨了Prometheus在监控中遇到的长尾问题,解释了rate和irate函数在处理快速变化数据时的局限性,以及irate如何更好地反映瞬时变化。,同时,文章详细阐述了Counter类型的指标在数据跳变时的处理方式,特别是Prometheus如何自动处理Counter重置。 此外,还讨论了数据外推的概念,特别是在rate和increase函数中的应用。 (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
Java监控度量Micrometer全解析
硬核安利一个监控告警开源项目Nightingale

← Java监控度量Micrometer全解析 硬核安利一个监控告警开源项目Nightingale→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×