上篇传送门:
SpringBoot配置Actuator metrics的监控
Actuator 是 Spring Boot 提供的对应用系统的自省和监控功能。通过 Actuator,可以使用数据化的指标去度量应用的运行情况,比如查看服务器的磁盘、内存、CPU等信息,系统的线程、gc、运行状态等等。Prometheus指标只是其中之一,也是本次流量监控用到的主体。
Actuator 通常通过使用 HTTP 和 JMX 来管理和监控应用,大多数情况使用 HTTP 的方式。
总之,先跑起来再说:
(1)Maven引入Actuator和Prometheus
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
(2)yml上配置信息
management:
endpoints:
web:
exposure:
# 开启所有端点
include: '*'
(3)然后就可以在项目中查看运行的指标了。
Actuator指标:localhost:8080/actuator/metrics
Prometheus指标:localhost:8080/actuator/prometheus
贴一份Actuator对应Prometheus的监控指标:
# HELP tomcat_sessions_rejected_sessions_total
# TYPE tomcat_sessions_rejected_sessions_total counter
tomcat_sessions_rejected_sessions_total 0.0
# HELP tomcat_sessions_alive_max_seconds
# TYPE tomcat_sessions_alive_max_seconds gauge
tomcat_sessions_alive_max_seconds 0.0
# HELP jvm_buffer_count_buffers An estimate of the number of buffers in the pool
# TYPE jvm_buffer_count_buffers gauge
jvm_buffer_count_buffers{id="direct",} 97.0
jvm_buffer_count_buffers{id="mapped",} 0.0
# HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process
# TYPE process_cpu_usage gauge
process_cpu_usage 0.0
# HELP tomcat_servlet_error_total
# TYPE tomcat_servlet_error_total counter
tomcat_servlet_error_total{name="default",} 0.0
tomcat_servlet_error_total{name="jsp",} 0.0
tomcat_servlet_error_total{name="dispatcherServlet",} 0.0
tomcat_servlet_error_total{name="cn.taqu.core.web.filter.ApiDispatcherServlet",} 0.0
# HELP tomcat_global_request_seconds
# TYPE tomcat_global_request_seconds summary
tomcat_global_request_seconds_count{name="http-nio-8906",} 82.0
tomcat_global_request_seconds_sum{name="http-nio-8906",} 3.117
# HELP tomcat_cache_access_total
# TYPE tomcat_cache_access_total counter
tomcat_cache_access_total 0.0
# HELP process_uptime_seconds The uptime of the Java virtual machine
# TYPE process_uptime_seconds gauge
process_uptime_seconds 88.809
# HELP tomcat_global_request_max_seconds
# TYPE tomcat_global_request_max_seconds gauge
tomcat_global_request_max_seconds{name="http-nio-8906",} 1.246
# HELP tomcat_sessions_active_max_sessions
# TYPE tomcat_sessions_active_max_sessions gauge
tomcat_sessions_active_max_sessions 0.0
# HELP tomcat_sessions_created_sessions_total
# TYPE tomcat_sessions_created_sessions_total counter
tomcat_sessions_created_sessions_total 0.0
# HELP process_start_time_seconds Start time of the process since unix epoch.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.610695930801E9
# HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads
# TYPE jvm_threads_live_threads gauge
jvm_threads_live_threads 343.0
# HELP logback_events_total Number of error level events that made it to the logs
# TYPE logback_events_total counter
logback_events_total{level="warn",} 0.0
logback_events_total{level="debug",} 0.0
logback_events_total{level="error",} 0.0
logback_events_total{level="trace",} 0.0
logback_events_total{level="info",} 19.0
# HELP tomcat_threads_config_max_threads
# TYPE tomcat_threads_config_max_threads gauge
tomcat_threads_config_max_threads{name="http-nio-8906",} NaN
# HELP jvm_threads_states_threads The current number of threads having NEW state
# TYPE jvm_threads_states_threads gauge
jvm_threads_states_threads{state="runnable",} 117.0
jvm_threads_states_threads{state="blocked",} 0.0
jvm_threads_states_threads{state="waiting",} 209.0
jvm_threads_states_threads{state="timed-waiting",} 17.0
jvm_threads_states_threads{state="new",} 0.0
jvm_threads_states_threads{state="terminated",} 0.0
# HELP system_cpu_count The number of processors available to the Java virtual machine
# TYPE system_cpu_count gauge
system_cpu_count 12.0
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/index",} 2.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/index",} 0.8098944
http_server_requests_seconds_count{exception="None",method="POST",outcome="SUCCESS",status="200",uri="root",} 1.0
http_server_requests_seconds_sum{exception="None",method="POST",outcome="SUCCESS",status="200",uri="root",} 0.0271729
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/**",} 73.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/**",} 0.4981964
http_server_requests_seconds_count{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/common/getLanguageList",} 2.0
http_server_requests_seconds_sum{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/common/getLanguageList",} 0.0204194
http_server_requests_seconds_count{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/inspectRule/dataGrid",} 1.0
http_server_requests_seconds_sum{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/inspectRule/dataGrid",} 1.2461368
http_server_requests_seconds_count{exception="None",method="GET",outcome="REDIRECTION",status="302",uri="REDIRECTION",} 1.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="REDIRECTION",status="302",uri="REDIRECTION",} 0.1364228
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/inspectRule/manager",} 1.0
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/inspectRule/manager",} 0.1832796
http_server_requests_seconds_count{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/user/addServiceSystemLog",} 1.0
http_server_requests_seconds_sum{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/user/addServiceSystemLog",} 0.0549094
# HELP http_server_requests_seconds_max
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/index",} 0.6628419
http_server_requests_seconds_max{exception="None",method="POST",outcome="SUCCESS",status="200",uri="root",} 0.0271729
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/**",} 0.0253742
http_server_requests_seconds_max{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/common/getLanguageList",} 0.0133106
http_server_requests_seconds_max{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/inspectRule/dataGrid",} 1.2461368
http_server_requests_seconds_max{exception="None",method="GET",outcome="REDIRECTION",status="302",uri="REDIRECTION",} 0.1364228
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/inspectRule/manager",} 0.1832796
http_server_requests_seconds_max{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/user/addServiceSystemLog",} 0.0549094
# HELP jvm_gc_memory_allocated_bytes_total Incremented for an increase in the size of the young generation memory pool after one GC to before the next
# TYPE jvm_gc_memory_allocated_bytes_total counter
jvm_gc_memory_allocated_bytes_total 2.345259752E9
# HELP jvm_buffer_total_capacity_bytes An estimate of the total capacity of the buffers in this pool
# TYPE jvm_buffer_total_capacity_bytes gauge
jvm_buffer_total_capacity_bytes{id="direct",} 712848.0
jvm_buffer_total_capacity_bytes{id="mapped",} 0.0
# HELP jvm_gc_memory_promoted_bytes_total Count of positive increases in the size of the old generation memory pool before GC to after GC
# TYPE jvm_gc_memory_promoted_bytes_total counter
jvm_gc_memory_promoted_bytes_total 4.7747424E7
# HELP jvm_buffer_memory_used_bytes An estimate of the memory that the Java virtual machine is using for this buffer pool
# TYPE jvm_buffer_memory_used_bytes gauge
jvm_buffer_memory_used_bytes{id="direct",} 712849.0
jvm_buffer_memory_used_bytes{id="mapped",} 0.0
# HELP jvm_gc_pause_seconds Time spent in GC pause
# TYPE jvm_gc_pause_seconds summary
jvm_gc_pause_seconds_count{action="end of major GC",cause="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of major GC",cause="Metadata GC Threshold",} 0.136
jvm_gc_pause_seconds_count{action="end of minor GC",cause="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.013
jvm_gc_pause_seconds_count{action="end of minor GC",cause="Allocation Failure",} 6.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Allocation Failure",} 0.069
# HELP jvm_gc_pause_seconds_max Time spent in GC pause
# TYPE jvm_gc_pause_seconds_max gauge
jvm_gc_pause_seconds_max{action="end of major GC",cause="Metadata GC Threshold",} 0.136
jvm_gc_pause_seconds_max{action="end of minor GC",cause="Metadata GC Threshold",} 0.013
jvm_gc_pause_seconds_max{action="end of minor GC",cause="Allocation Failure",} 0.023
# HELP tomcat_threads_current_threads
# TYPE tomcat_threads_current_threads gauge
tomcat_threads_current_threads{name="http-nio-8906",} NaN
# HELP tomcat_global_error_total
# TYPE tomcat_global_error_total counter
tomcat_global_error_total{name="http-nio-8906",} 0.0
# HELP jvm_threads_daemon_threads The current number of live daemon threads
# TYPE jvm_threads_daemon_threads gauge
jvm_threads_daemon_threads 237.0
# HELP tomcat_cache_hit_total
# TYPE tomcat_cache_hit_total counter
tomcat_cache_hit_total 0.0
# HELP tomcat_threads_busy_threads
# TYPE tomcat_threads_busy_threads gauge
tomcat_threads_busy_threads{name="http-nio-8906",} NaN
# HELP tomcat_servlet_request_max_seconds
# TYPE tomcat_servlet_request_max_seconds gauge
tomcat_servlet_request_max_seconds{name="default",} 0.0
tomcat_servlet_request_max_seconds{name="jsp",} 0.0
tomcat_servlet_request_max_seconds{name="dispatcherServlet",} 1.246
tomcat_servlet_request_max_seconds{name="cn.taqu.core.web.filter.ApiDispatcherServlet",} 0.0
# HELP jvm_threads_peak_threads The peak live thread count since the Java virtual machine started or peak was reset
# TYPE jvm_threads_peak_threads gauge
jvm_threads_peak_threads 345.0
# HELP jvm_classes_unloaded_classes_total The total number of classes unloaded since the Java virtual machine has started execution
# TYPE jvm_classes_unloaded_classes_total counter
jvm_classes_unloaded_classes_total 3.0
# HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use
# TYPE jvm_memory_committed_bytes gauge
jvm_memory_committed_bytes{area="heap",id="PS Survivor Space",} 3.2505856E7
jvm_memory_committed_bytes{area="heap",id="PS Old Gen",} 2.36453888E8
jvm_memory_committed_bytes{area="heap",id="PS Eden Space",} 4.97549312E8
jvm_memory_committed_bytes{area="nonheap",id="Metaspace",} 9.76896E7
jvm_memory_committed_bytes{area="nonheap",id="Code Cache",} 2.0709376E7
jvm_memory_committed_bytes{area="nonheap",id="Compressed Class Space",} 1.1976704E7
# HELP tomcat_sessions_active_current_sessions
# TYPE tomcat_sessions_active_current_sessions gauge
tomcat_sessions_active_current_sessions 0.0
# HELP tomcat_sessions_expired_sessions_total
# TYPE tomcat_sessions_expired_sessions_total counter
tomcat_sessions_expired_sessions_total 0.0
# HELP tomcat_global_sent_bytes_total
# TYPE tomcat_global_sent_bytes_total counter
tomcat_global_sent_bytes_total{name="http-nio-8906",} 2920966.0
# HELP jvm_gc_live_data_size_bytes Size of old generation memory pool after a full GC
# TYPE jvm_gc_live_data_size_bytes gauge
jvm_gc_live_data_size_bytes 4.3797632E7
# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="heap",id="PS Survivor Space",} 3.2505856E7
jvm_memory_max_bytes{area="heap",id="PS Old Gen",} 2.841116672E9
jvm_memory_max_bytes{area="heap",id="PS Eden Space",} 1.344274432E9
jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
jvm_memory_max_bytes{area="nonheap",id="Code Cache",} 2.5165824E8
jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
# HELP tomcat_global_received_bytes_total
# TYPE tomcat_global_received_bytes_total counter
tomcat_global_received_bytes_total{name="http-nio-8906",} 118.0
# HELP jvm_gc_max_data_size_bytes Max size of old generation memory pool
# TYPE jvm_gc_max_data_size_bytes gauge
jvm_gc_max_data_size_bytes 2.841116672E9
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="PS Survivor Space",} 3.2479312E7
jvm_memory_used_bytes{area="heap",id="PS Old Gen",} 6.7252472E7
jvm_memory_used_bytes{area="heap",id="PS Eden Space",} 4.5849296E7
jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 9.2626808E7
jvm_memory_used_bytes{area="nonheap",id="Code Cache",} 2.063232E7
jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space",} 1.1170784E7
# HELP system_cpu_usage The "recent cpu usage" for the whole system
# TYPE system_cpu_usage gauge
system_cpu_usage 0.3472113977877134
# HELP tomcat_servlet_request_seconds
# TYPE tomcat_servlet_request_seconds summary
tomcat_servlet_request_seconds_count{name="default",} 0.0
tomcat_servlet_request_seconds_sum{name="default",} 0.0
tomcat_servlet_request_seconds_count{name="jsp",} 0.0
tomcat_servlet_request_seconds_sum{name="jsp",} 0.0
tomcat_servlet_request_seconds_count{name="dispatcherServlet",} 83.0
tomcat_servlet_request_seconds_sum{name="dispatcherServlet",} 3.0
tomcat_servlet_request_seconds_count{name="cn.taqu.core.web.filter.ApiDispatcherServlet",} 0.0
tomcat_servlet_request_seconds_sum{name="cn.taqu.core.web.filter.ApiDispatcherServlet",} 0.0
# HELP jvm_classes_loaded_classes The number of classes that are currently loaded in the Java virtual machine
# TYPE jvm_classes_loaded_classes gauge
jvm_classes_loaded_classes 15999.0
当然,这里查看的只是默认的Prometheus指标,而我们一般流量监控需要的各种指标是要手动编码实现的。
不过不急,这里先分析一下上面两个端点详情:
Actuator 端点说明
auditevents
:获取当前应用暴露的审计事件信息beans
:获取应用中所有的 Spring Beans 的完整关系列表caches
:获取公开可以用的缓存conditions
:获取自动配置条件信息,记录哪些自动配置条件通过和没通过的原因configprops
:获取所有配置属性,包括默认配置,显示一个所有 @ConfigurationProperties 的整理列版本env
:获取所有环境变量flyway
:获取已应用的所有Flyway数据库迁移信息,需要一个或多个 Flyway Beanliquibase
:获取已应用的所有Liquibase数据库迁移。需要一个或多个 Liquibase Beanhealth
:获取应用程序健康指标(运行状况信息)httptrace
:获取HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应交换)。需要 HttpTraceRepository Beaninfo
:获取应用程序信息integrationgraph
:显示 Spring Integration 图。需要依赖 spring-integration-coreloggers
:显示和修改应用程序中日志的配置logfile
:返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)metrics
:获取系统度量指标信息mappings
:显示所有@RequestMapping路径的整理列表scheduledtasks
:显示应用程序中的计划任务sessions
:允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序shutdown
:关闭应用,要求endpoints.shutdown.enabled设置为true,默认为 falsethreaddump
:获取系统线程转储信息heapdump
:返回hprof堆转储文件jolokia
:通过HTTP公开JMX bean(当Jolokia在类路径上时,不适用于WebFlux)。需要依赖 jolokia-coreprometheus
:以Prometheus服务器可以抓取的格式公开指标。需要依赖 micrometer-registry-prometheus
Prometheus/Metrics 端点说明
序号 | 参数 | 参数说明 | 是否监控 | 监控手段 | 重要度 |
---|---|---|---|---|---|
--- | JVM | --- | |||
1 | jvm.memory.max | JVM最大内存 | |||
2 | jvm.memory.committed | JVM可用内存 | 是 | 展示并监控堆内存和Metaspace | 重要 |
3 | jvm.memory.used | JVM已用内存 | 是 | 展示并监控堆内存和Metaspace | 重要 |
4 | jvm.buffer.memory.used | JVM缓冲区已用内存 | |||
5 | jvm.buffer.count | 当前缓冲区数 | |||
6 | jvm.threads.daemon | JVM守护线程数 | 是 | 显示在监控页面 | |
7 | jvm.threads.live | JVM当前活跃线程数 | 是 | 显示在监控页面;监控达到阈值时报警 | 重要 |
8 | jvm.threads.peak | JVM峰值线程数 | 是 | 显示在监控页面 | |
9 | jvm.classes.loaded | 加载classes数 | |||
10 | jvm.classes.unloaded | 未加载的classes数 | |||
11 | jvm.gc.memory.allocated | GC时,年轻代分配的内存空间 | |||
12 | jvm.gc.memory.promoted | GC时,老年代分配的内存空间 | |||
13 | jvm.gc.max.data.size | GC时,老年代的最大内存空间 | |||
14 | jvm.gc.live.data.size | FullGC时,老年代的内存空间 | |||
15 | jvm.gc.pause | GC耗时 | 是 | 显示在监控页面 | |
--- | TOMCAT | --- | |||
16 | tomcat.sessions.created | tomcat已创建session数 | |||
17 | tomcat.sessions.expired | tomcat已过期session数 | |||
18 | tomcat.sessions.active.current | tomcat活跃session数 | |||
19 | tomcat.sessions.active.max | tomcat最多活跃session数 | 是 | 显示在监控页面,超过阈值可报警或者进行动态扩容 | 重要 |
20 | tomcat.sessions.alive.max.second | tomcat最多活跃session数持续时间 | |||
21 | tomcat.sessions.rejected | 超过session最大配置后,拒绝的session个数 | 是 | 显示在监控页面,方便分析问题 | |
22 | tomcat.global.error | 错误总数 | 是 | 显示在监控页面,方便分析问题 | |
23 | tomcat.global.sent | 发送的字节数 | |||
24 | tomcat.global.request.max | request最长时间 | |||
25 | tomcat.global.request | 全局request次数和时间 | |||
26 | tomcat.global.received | 全局received次数和时间 | |||
27 | tomcat.servlet.request | servlet的请求次数和时间 | |||
28 | tomcat.servlet.error | servlet发生错误总数 | |||
29 | tomcat.servlet.request.max | servlet请求最长时间 | |||
30 | tomcat.threads.busy | tomcat繁忙线程 | 是 | 显示在监控页面,据此检查是否有线程夯住 | |
31 | tomcat.threads.current | tomcat当前线程数(包括守护线程) | 是 | 显示在监控页面 | 重要 |
32 | tomcat.threads.config.max | tomcat配置的线程最大数 | 是 | 显示在监控页面 | 重要 |
33 | tomcat.cache.access | tomcat读取缓存次数 | |||
34 | tomcat.cache.hit | tomcat缓存命中次数 | |||
--- | CPU... | --- | |||
35 | system.cpu.count | CPU数量 | |||
36 | system.load.average.1m | load average | 是 | 超过阈值报警 | 重要 |
37 | system.cpu.usage | 系统CPU使用率 | |||
38 | process.cpu.usage | 当前进程CPU使用率 | 是 | 超过阈值报警 | |
39 | http.server.requests | http请求调用情况 | 是 | 显示10个请求量最大,耗时最长的URL;统计非200的请求量 | 重要 |
40 | process.uptime | 应用已运行时间 | 是 | 显示在监控页面 | |
41 | process.files.max | 允许最大句柄数 | 是 | 配合当前打开句柄数使用 | |
42 | process.start.time | 应用启动时间点 | 是 | 显示在监控页面 | |
43 | process.files.open | 当前打开句柄数 | 是 | 监控文件句柄使用率,超过阈值后报警 | 重要 |
补充说明:
由于SpringBoot是内置Tomcat,很多在Prometheus上关于Tomcat的指标很难监控得到,需要编码获取。(也许还有更好的解决方案)
编写客户端前须知
通过上面我们可以掌握到,拿取并查看Prometheus中自身的一些监控指标。
而在流量监控这个开发需求中,我们需要自定义一些监控指标并且封装到该端口中。也正是本篇的技术所在,所以下面会先介绍编码中用到的一些概念。也就是上一篇对官方文档分析中剩下的精华部分。
总体结构
重新看下基于Prometheus的流量监控在技术上的架构和可能做的事:
而实现监控的关键就是上文所说的:
当Prometheus获取实例的HTTP端点时,客户库发送所有跟踪的度量指标数据到服务器上。
也就是回调:
客户端必须
在内部写入回调。客户通常应该
遵循下面描述的结构。
这个关键类是Collector
。这个有一个典型的方法collect()
, 返回0~N个度量指标和这些指标的样本数据。Collector
用CollectorRegistry
进行注册。通过传递CollectorRegistry
中被称之为bridge
的class/method/function来暴露数据。该bridge
返回Prometheus支持的数据格式数据。每次这个CollectorRegistry
被收集时,都必须回调Collector.collect()
方法。CollectorRegistry
应该提供register()/unregister()
方法,并且应该允许将Collector
注册到多个CollectorRegistrys(目前一个项目里面只有一个CollectorRegistry)。且客户库必须是线程安全的。
如果上面看得很绕,可以先理解一下他们的关系再回看一遍:
CollectorRegistry是Collector的注册器,Collector是Metrics的收集器,Metrics有四种模型其中最出名的就是Gauge,SimpleCollector是Collector的子类,当Gauge调用到register()方法时执行的是SimpleCollector。
通俗的讲,就是回调的底层不需要你实现,它已经写好封装了。你只需要通过Metrics中的其中一种模型定义好监控指标,并且将它注册到Collector中(实际上是SimpleCollector,注册也只是调用Metrics.register()方法就行了),然后再把它注册到CollectorRegistry中(同样也只是调用CollectorRegistry.register()方法就行了)。
具体代码实现后面会细说,接下来算是一些开发规范:
命名
客户库应该遵循function/method/class
在这个文档中提及的命名规范。
如:setToCurrentTime()
的驼峰命名对于Java是最好的。
禁止提供与此处给出的相同或者相似functions/methods/classes
,但具有不同的语义。
Metrics
Metrics应该主要用作类静态变量,也就是说,它们是自定义Metrics类下的全局变量。
通常用例是对整个代码进行测试,而不是在对象的一个实例的上下文中检测一段代码。用户不应该担心在代码中使用他们的度量标准,客户端库应该为他们做这件事(剩下的翻译太艹了不同翻译器不同意思,理解后大致得出为:客户端应该将这些自定义指标包装起来统一管理,也就是下文我写的InitPrometheus
类)
必须有一个默认的CollectorRegistry
, 四种度量指标类型必须在不需要用户任何干预下,就能完成默认被注册,同时也提供一种别的注册方法,用于批处理作业和单元测试。自定义的Collectors
也应该遵循这点。
class YourClass {
static final Counter requests = Counter.build()
.name("requests_total")
.help("Requests.").register();
}
上面的例子,使用默认的CollectorRegistry
进行注册。如果只是单纯的调用build()方法,度量指标将不会被注册(对于单元测试来说很方便),你还可以将CollectorRegistry
传递给register()(方便批作业处理)。这种方法就是默认注册,自定义指标只需要模仿它写就行了。
模型分析(以Gauge为例)
Gauge表示一个可以上下波动的值。
gauge必须有以下的方法:
inc(): 每次增加1
inc(double v): 每次增加给定值v
dec(): 每次减少1
dec(double v): 每次减少给定值v
set(double v): 设置gauge值成v
Gauges值必须从0开始,你可以提供一个从不等于0的值开始。
gauge应该有以下方法:
set_to_current_time(): 将gauge设置为当前的unix时间(以秒为单位)。
关于gauge的建议:
执行一段代码,设置gauge类型数据样本值为这段代码执行的时间,这对于批量任务是非常有用的。在Java中是startTimer/setDuration。
标签Labels
客户端库应该支持任意大小的标签列表。客户端库必须验证标签名称是否符合要求。
提供访问度量指标名称列表最常用的方法, 是通过labels()
方法,该方法可以获取标签值列表,或者获取Map键值对(标签名称:标签值)列表,并返回“child”,然后在Child上调用常用的.inc()/.desc()/.observe()
等方法。
label()
返回Child应该由用户缓存,以避免再次查找,这在延迟至关重要的代码中很重要。
带有标签的度量指标应该支持一个具有与labels()
相同签名的remove()
方法,它将从不再导出它的度量标准中删除一个Child,另一个clear()方法可以从度量指标中删除所有的Child
。
应该有一种使用默认初始化给定Child的方法,通常只需要调用labels()。没有标签的度量指标必须被初始化,已避免缺少度量指标的问题。
度量指标名称Metrics
许多客户库提供三个部分的名称:namespace_subsystem_name
, 其中只有该name
是强制性的。
不鼓励使用动态/自动生成的度量指标名称或者其子部分,除非自定义"Collector"是从其他工具/监控系统代理的。你可以使用标签名称替代动态或者自动生成的度量指标名称。
带有自定义Collector的客户库,在度量指标上必须有desc/help。
建议将Metrics的desc/help作为强制性参数,但不需要检查其长度,提供Collectors的库应该要有一个比较好的desc,帮助理解其含义。
性能考虑
labels()
的结果应该是可缓存的。带有标签的指标的并行映射往往相对较慢。而不带标签的默认指标则查找性能更高。labels在执行inc()/dec()/set()等时应避免阻塞,因为在进行拉取时,整个应用程序都会被阻塞。
setTocurrentTime ();//设置为当前unix时间
作为一个高级的用例,labels也可以通过setChild()方法从回调中获取它的值。请记住,Gauge上的默认inc()、dec()和set()方法会保护线程安全,因此在使用这种方法时,请确保您正在报告的并发账户的值。
编写客户端
需要做的事
从公司架构中可以知道,我们主要是要搞定SPringBoot(客户端)和Prometheus(服务端),就可以搭载出一个完整的流量监控了,具体事情如下:
SpringBoot(研发负责)
(1)配置基于Actuator的prometheus监控的yml,并提供监控的接口(不要有权限校验):http://127.0.0.1:8906/actuator/prometheus
(2)分析需要监听的监控指标,封装prometheus采集需要的格式,并提交到监控接口中。
Prometheus(运维负责)
(1)Prometheus监控配置(目标、规则、拉取频率等)。
抓取的周期和时间主要看指标的关键程度,我们这边权衡完告知高老师 帮忙配置。
初步得知一般是设置 5、10、15秒这样,一些非关键的指标会设置在一分钟采集一次。
(2)配置Grafana、警告策略、钉钉机器人等(这块也可以跟进学习)
代码学习参考
方案一:SpringBoot -> Pushgateway <- Prometheus
首先,在调用到QpsSubmitFilter的时候,分别调用了YzpUtil.getIpAddr(req)
方法获取Ip和TpsCache.increase(this.projectName, simpleDateFormat.format(new Date()), ip)
方法以时间和ip维度对tps累加。
下面具体分析TpsCache这个类:
- 类加载时,静态代码块优先执行,通过调用java.net包中的方法,将当前ip封装成map并放到
gmap<"ip",{localIp}>
中。 increase()方法加了方法锁,该方法详细流程如下:
- 根据
projectName
项目名参数拼接tps 成 map的key值,获取map对应的value值:timeMap<time,ConcurrentSkipListMap<>>
- 如果timeMap不为空,再根据
time
时间参数作为map的key值,获取对应的value值:ipMap<String, Integer>。也就是该时间域下的Ip-Tps映射集。
否则,初始化timeMap和ipMap。 - 如果ipMap不为空,则调用
tpsIncrease()
方法对该ip对应的tps进行累加并更新ipMap。
否则,调用initIpMap()
方法初始化ipMap,初始化时将该ip参数对应的tps置为1。
- 根据
- push()方法是利用网关采集数据,并将metric通过Put推送到PushGateway上。
submit()方法
- 先获取到当前ip,如果ip获取失败,会打日志警告,并且该方法直接当成成功返回。
- 接着同样是,根据
projectName
项目名参数拼接tps 成 gaugeMap<String, Gauge>的key值,获取对应的value值:qpsMin。
如果为空则注册该指标并且将它放入到gaugeMap中。 - 再从map里面拿出该key对应的值projMap,如果存在则更新PushGateway上job为“tps”的gmap对应值,并且根据time时间参数拿到对应的map集,拼接出
{ip}---{time}---{localIp}
作为标签key,ip对应的tps作为value 提交。并且清除已处理的数据。
否则,如果projMap为空。则清空PushGateway上job为“tps”的gmap对应值和qpsMin,并且打出警告日志。
- destory()方法,删除pg上job名称为tps对应的gmap。
PreDestroy注解可以使得用户回调通知此实例正在被删除。
代码解读
public class QpsSubmitFilter implements Filter {
private String projectName;
public QpsSubmitFilter() {
}
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmm");
String ip = YzpUtil.getIpAddr(req);
TpsCache.increase(this.projectName, simpleDateFormat.format(new Date()), ip);
chain.doFilter(request, response);
}
public void destroy() {
}
public String getProjectName() {
return this.projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
}
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Real-IP");
if (StringUtils.isEmpty(ip)) {
ip = request.getHeader("x-forwarded-for");
}
if (!StringUtils.isEmpty(ip)) {
String[] lst = ip.split(",");
if (lst.length > 0) {
ip = lst[0];
} else {
ip = null;
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
@Service
public class TpsCache {
private static final Logger logger = LoggerFactory.getLogger(TpsCache.class);
/**
* ConcurrentSkipListMap:有序的哈希表,线程安全,通过跳表实现。(key有序、支持高并发)
* ######
* Map<String, ConcurrentSkipListMap<String, ConcurrentSkipListMap<String, Integer>>>;
* ↓
* map<{projectName}tps, timeMap<{time}, ipMap<String, Integer>>>;
*/
private static Map<Object, Object> map = new ConcurrentSkipListMap();
/**
* Gauge(测量器):是一个度量指标,它表示一个既可以递增, 又可以递减的值。
* 测量器主要测量类似于温度、当前内存使用量等,也可以统计当前服务运行随时增加或者减少的Goroutines数量
*/
private static Map<String, Gauge> gaugeMap = new HashMap();
/**
* 指定Prometheus访问界面,使用Client SDK向Pushgateway推数据,让Prometheus从Pushgateway上进行数据采集
*/
private static PushGateway pg = new PushGateway("120.27.198.180:9091");
/**
* 客户端上的收集注册器
*/
private static CollectorRegistry cr = new CollectorRegistry(true);
/**
* <"ip",{localIp}></>
*/
private static Map<String, String> gmap = new HashMap();
/**
* 默认构造方法
*/
public TpsCache() {
}
/**
* 根据项目名,以时间和ip维度对tps进行一次累加
* @param projectName 项目名
* @param time 时间维度
* @param ip ip维度
*/
public static synchronized void increase(String projectName, String time, String ip) {
// 根据projectName拼接tps 成 map的key值
String key = projectName + "tps";
// 根据获取map对应的value值:timeMap<time,ConcurrentSkipListMap<>>
ConcurrentSkipListMap timeMap = (ConcurrentSkipListMap)map.get(key);
if (timeMap != null) {
// 根据time作为key值获取timeMap的value值:ipMap<String, Integer>
ConcurrentSkipListMap<String, Integer> ipMap = (ConcurrentSkipListMap)timeMap.get(time);
// 如果该time时间下的ipMap存在,则增加
if (ipMap != null) {
tpsIncrease(ip, ipMap);
// 否则,初始化ipMap
} else {
initIpMap(time, ip, timeMap);
}
// 如果该timeMap不存在就初始化timeMap和ipMap
} else {
timeMap = new ConcurrentSkipListMap();
initIpMap(time, ip, timeMap);
map.put(key, timeMap);
}
}
/**
* 利用网关采集数据,并将metric通过Put推送到PushGateway上
* @return
*/
public static boolean push() {
try {
// 注册器、job名、推送数据,该方法新数据会全盘覆盖老数据
pg.push(CollectorRegistry.defaultRegistry, "mydemo", gmap);
return true;
} catch (IOException var1) {
logger.error("push metric to pushgateway fail ", var1);
return false;
}
}
/**
* 将time之前的数据提交到prometheus上。
* @param projectName
* @param time
* @return
*/
public static boolean submit(String projectName, String time) {
// 获取到当前ip,如果ip获取失败,该方法直接当成成功返回。
String localIp = getLocalIp();
if (localIp == null) {
return true;
} else {
// 先从gaugeMap里面拿出该key对应的值,如果该gaugeMap不存在则初始化创建
String key = projectName + "tps";
Gauge qpsMin = (Gauge)gaugeMap.get(key);
// 如果为空则注册该指标并且将它放入到gaugeMap中
if (qpsMin == null) {
// Gauge.build()...register(),会向Collector中注册该指标,当访问/metrics地址时,返回该指标的状态。
// name(key)该metric度量指标名称、help("qps min ")该metric备注
// labelNames(new String[]{"default"}) 指定该指标的名称
qpsMin = (Gauge)((Builder)((Builder)((Builder)Gauge.build().name(key)).help("qps min ")).labelNames(new String[]{"default"})).register(cr);
gaugeMap.put(key, qpsMin);
}
// 再从map里面拿出该key对应的值,如果存在则进行如下操作:
ConcurrentSkipListMap projMap = (ConcurrentSkipListMap)map.get(key);
if (projMap != null && projMap.size() > 0) {
while(true) {
// 获取该小于或等于时间点的timeMap
Entry<String, ConcurrentSkipListMap<String, Integer>> timeMap = projMap.floorEntry(time);
// 父集不存在,可能是刚好被删除或其它错误。则清空子集并返回false。
if (timeMap == null) {
qpsMin.clear();
return false;
}
try {
// 将 gmap<"ip",{localIp}>,在Pushgateway上且job为“tps”中删除。
pg.delete("tps", gmap);
} catch (IOException var14) {
logger.error("pg delete fail ", var14);
}
// 拿到的是一个升序排列的ipMap中的迭代器
Iterator var7 = ((ConcurrentSkipListMap)timeMap.getValue()).entrySet().iterator();
while(var7.hasNext()) {
Entry tpsEle = (Entry)var7.next();
String ip = (String)tpsEle.getKey();
Integer tps = (Integer)tpsEle.getValue();
// 拼接成:{ip}---{time}---{localIp}
String value = ip + "----" + (String)timeMap.getKey() + "----" + localIp;
// 以value 获取标签值列表
Child child = (Child)qpsMin.labels(new String[]{value});
// 将没有标签的gauge设置为给定值
child.set((double)tps);
logger.info("key " + key + " value :" + value + " tps :" + tps);
}
try {
pg.pushAdd(cr, "tps", gmap);
} catch (IOException var13) {
logger.error("tps push fail ", var13);
}
projMap.remove(timeMap.getKey());
}
} else {
try {
pg.delete("tps", gmap);
} catch (IOException var15) {
//这里的日志应该是delete fail
logger.error("tps push fail ", var15);
}
logger.warn("{} no request", time);
qpsMin.clear();
return true;
}
}
}
/**
* java.net获取当前ip
* @return
*/
private static String getLocalIp() {
String localIp = null;
try {
localIp = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException var2) {
logger.error("localip error");
}
return localIp;
}
/**
* 某time时间段下的ip维度映射关系,根据ip获取对应的tps,如果不存在则初始化。然后tps累加1并更新ipMap
* @param ip ip
* @param ipMap ipMap<ip,tps>
*/
private static void tpsIncrease(String ip, ConcurrentSkipListMap<String, Integer> ipMap) {
// 获取该ip对应的tps
Integer tps = (Integer)ipMap.get(ip);
if (tps == null) {
tps = 0;
}
tps = tps + 1;
ipMap.put(ip, tps);
}
/**
* 初始化该time下的IpMap
* @param time
* @param ip
* @param timeMap
*/
private static void initIpMap(String time, String ip, ConcurrentSkipListMap timeMap) {
ConcurrentSkipListMap<String, Integer> ipMap = new ConcurrentSkipListMap();
ipMap.put(ip, 1);
timeMap.put(time, ipMap);
}
/**
* 删除pg上job名称为tps对应的gmap
* @PreDestroy 用户回调通知此实例正在被删除
*/
@PreDestroy
public void destory() {
try {
pg.delete("tps", gmap);
} catch (IOException var2) {
//这里的日志应该是delete fail
logger.error("tps push fail ", var2);
}
}
/**
* 静态代码块优先执行,将当前ip封装成map并放到gmap中
*/
static {
gmap.put("ip", getLocalIp());
}
}
方案二:SpringBoot <- Prometheus
由于公司采用的是让Prometheus直接从项目开放的监听接口上直接拉取,所以原先项目->Pushgateway<-Prometheus
方式不适用当前的方案,暂移到当前文档底下的旧文档中当成备用方案。
目前采用:访问Ip、提交时间戳(精确到分)、请求者Ip、请求地址、请求方式(Get/Post)、响应状态。拆分了六个维度去进行监控。
同时,由于不需要定时提交到Pushgateway上,而是Prometheus上制定频率来拉取,所以原先的缓存策略也直接取消掉了,使得目前代码更加简洁直观。
详细流程
(目前的实现如下,可能还有许多地方需要改进)
由于要先提前定义好指标,才能在InitPrometheus下提交到监控端口中。所以需要在
MyMetrics
下定义基于Gauge的自定义指标(可以动态获取项目名,方便后续扩展)- 静态代码块调用获取yml配置值的工具类,处理后赋给projectName,作为指标名
- 根据访问Ip、提交时间戳(精确到分)、请求者Ip、请求地址、请求方式(Get/Post)、响应状态,实例化Gauge。
- submit()方法在每次调用的时候再对应的标签下累加一次值,其中调用到的getLocalIp()通过java.net包实现,获取当前ip
InitPrometheus
继承了ApplicationListener<ContextRefreshedEvent>
,在初始化的时候将Metrics标签注册到暴露给Prometheus监听的端口上。PrometheusSubmitFilter
提交过滤器,过滤掉静态资源路径下的请求、获取Metrics标签对应的参数值(涉及到自定义工具类GetIpAddressUtil
)。并调用MyMetrics的submit()方法。- 将该拦截器注册到
CommonConfiguration
中。
代码解读(优化后)
/**
* 描述:Gauge实现的自定义标签、指标
*
* @author CaiTianXin
* @date 2021/1/21 10:52
*/
@Slf4j
public class MyMetrics {
/**
* 项目名,采用动态获取避免写死在一个系统
* 格式:xxx_tps
*/
public static String projectName;
/**
* 通过自定义工具类获取到application.yml上的service.code
*/
static {
projectName = GetYmlPropertiesUtil.getCommonYml("service.code") + "_tps";
}
/**
* 指标:{projectName}_tps
* 标签:ip、time、localIp、path、method、code
* 格式:antispam_v2_tps{ip="127.0.0.1",time="202101221406",localIp="10.10.10.234",path="/tq-anti-spam-admin/js/public/constant.js",method="GET",code="200",} 1.0
*/
public static Gauge baseGauge = Gauge.build()
.name(projectName)
.labelNames("ip", "time", "localIp", "path", "method")
.help("Five metrics for this label.")
.register();
public static Counter baseCounter = Counter.build()
.name(projectName + "_total")
.labelNames("ip", "time", "localIp", "path", "method", "code")
.help("Six metrics for this label.")
.register();
/**
* 累计一次请求
* @param time 时间戳
* @return boolean
*/
public static boolean submitGaugeInc(String ip, String time, String path, String method) {
// 获取到当前ip,如果ip获取失败,该方法直接当成成功返回。
String localIp = getLocalIp();
if (localIp != null) {
baseGauge.labels(ip, time, localIp, path, method).inc();
}
return true;
}
public static boolean submitGaugeDec(String ip, String time, String path, String method) {
// 获取到当前ip,如果ip获取失败,该方法直接当成成功返回。
String localIp = getLocalIp();
if (localIp != null) {
baseGauge.labels(ip, time, localIp, path, method).dec();
}
return true;
}
public static boolean submitCounterInc(String ip, String time, String path, String method, String code) {
// 获取到当前ip,如果ip获取失败,该方法直接当成成功返回。
String localIp = getLocalIp();
if (localIp != null) {
baseCounter.labels(ip, time, localIp, path, method, code).inc();
}
return true;
}
/**
* 通过java.net包获取当前ip
* @return String
*/
private static String getLocalIp() {
String localIp = null;
try {
localIp = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException var2) {
log.error("localip error");
}
return localIp;
}
}
/**
* init普罗米修斯
*
* @author CaiTianXin
* @date 2021/01/22
*/
@Component
public class InitPrometheus implements ApplicationListener<ContextRefreshedEvent> {
@Resource
PrometheusMeterRegistry meterRegistry;
/**
* 将Metrics标签注册到暴露给Prometheus监听的端口上
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
CollectorRegistry prometheusRegistry = meterRegistry.getPrometheusRegistry();
prometheusRegistry.register(MyMetrics.baseGauge);
prometheusRegistry.register(MyMetrics.baseCounter);
}
}
/**
* Prometheus提交过滤器
*
* @author CaiTianXin
* @date 2021/01/21
*/
public class PrometheusSubmitFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmm");
String requestURI = req.getRequestURI();
// 过滤掉静态资源目录下的请求记录
if (requestURI.contains("/static/")) {
chain.doFilter(request, response);
} else {
String method = req.getMethod();
String ip = GetIpAddressUtil.getIpAddr(req);
String time = simpleDateFormat.format(new Date());
MyMetrics.submitGaugeInc(ip, time, requestURI, method);
chain.doFilter(request, response);
MyMetrics.submitGaugeDec(ip, time, requestURI, method);
int status = res.getStatus();
MyMetrics.submitCounterInc(ip, time, requestURI, method, String.valueOf(status));
}
}
}
@Configuration
public class CommonConfiguration {
private static final Log LOG = LogFactory.getLog(CommonConfiguration.class);
/**
* 流量监控
* @return
*/
@Bean("prometheusSubmitFilter")
public PrometheusSubmitFilter prometheusSubmitFilter() {
PrometheusSubmitFilter prometheusSubmitFilter = new PrometheusSubmitFilter();
return prometheusSubmitFilter;
}
}
当前指标展示
可能存在的改进点
(1)目前提交采用的是对访问Ip、提交时间戳(精确到分)、请求者Ip、请求地址、请求方式(Get/Post)、响应状态,这六种标签集成的指标。每次提交请求就进行inc()累加一次。
这样需要在chain.doFilter(request, response);
后获取状态码之后再进行一次提交。
是否可以改为:
拆分成两个指标,一个不包含响应状态,在doFilter之前就提交inc()累加一次。当dofilter执行完后对不包含响应状态的指标提交dec()消除一次。另一个包含状态码的指标在该操作之后提交inc()累加一次。
考量来自如果在执行该请求对应的方法出错时,能过在第一个指标上发现。
后续我将尝试验证该思路。
修改后的验证:
只有该条访问Prometheus指标的请求未被处理,其它请求不知道怎么模拟失败。
(2)针对文档中性能考虑这一模块提到的,labels()的结果应该是可缓存的。
不知道如果是Prometheus直接拉取项目的话,还是否需要缓存?
Actuator的prometheus监控指标分析
可能对流量监控有用的参数:
# 请求总次数
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/index",} 2.0
# 总共花了多长时间
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/index",} 0.8098944
# 最长一次花费多久
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/index",} 0.6628419
# tomcat配置的线程最大数
tomcat_threads_config_max_threads{name="http-nio-8906",} NaN
# tomcat当前线程数(包括守护线程)
tomcat_threads_current_threads{name="http-nio-8906",} NaN
# tomcat繁忙线程
tomcat_threads_busy_threads{name="http-nio-8906",} NaN
# JVM守护线程数
jvm_threads_daemon_threads 237.0
# JVM当前活跃线程数
jvm_threads_live_threads 343.0
# JVM峰值线程数
jvm_threads_peak_threads 345.0