上篇传送门:

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 Bean
  • liquibase:获取已应用的所有Liquibase数据库迁移。需要一个或多个 Liquibase Bean
  • health:获取应用程序健康指标(运行状况信息)
  • httptrace:获取HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应交换)。需要 HttpTraceRepository Bean
  • info:获取应用程序信息
  • integrationgraph:显示 Spring Integration 图。需要依赖 spring-integration-core
  • loggers:显示和修改应用程序中日志的配置
  • logfile:返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)
  • metrics:获取系统度量指标信息
  • mappings:显示所有@RequestMapping路径的整理列表
  • scheduledtasks:显示应用程序中的计划任务
  • sessions:允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序
  • shutdown:关闭应用,要求endpoints.shutdown.enabled设置为true,默认为 false
  • threaddump:获取系统线程转储信息
  • heapdump:返回hprof堆转储文件
  • jolokia:通过HTTP公开JMX bean(当Jolokia在类路径上时,不适用于WebFlux)。需要依赖 jolokia-core
  • prometheus:以Prometheus服务器可以抓取的格式公开指标。需要依赖 micrometer-registry-prometheus

Prometheus/Metrics 端点说明

序号参数参数说明是否监控监控手段重要度
---JVM---
1jvm.memory.maxJVM最大内存
2jvm.memory.committedJVM可用内存展示并监控堆内存和Metaspace重要
3jvm.memory.usedJVM已用内存展示并监控堆内存和Metaspace重要
4jvm.buffer.memory.usedJVM缓冲区已用内存
5jvm.buffer.count当前缓冲区数
6jvm.threads.daemonJVM守护线程数显示在监控页面
7jvm.threads.liveJVM当前活跃线程数显示在监控页面;监控达到阈值时报警重要
8jvm.threads.peakJVM峰值线程数显示在监控页面
9jvm.classes.loaded加载classes数
10jvm.classes.unloaded未加载的classes数
11jvm.gc.memory.allocatedGC时,年轻代分配的内存空间
12jvm.gc.memory.promotedGC时,老年代分配的内存空间
13jvm.gc.max.data.sizeGC时,老年代的最大内存空间
14jvm.gc.live.data.sizeFullGC时,老年代的内存空间
15jvm.gc.pauseGC耗时显示在监控页面
---TOMCAT---
16tomcat.sessions.createdtomcat已创建session数
17tomcat.sessions.expiredtomcat已过期session数
18tomcat.sessions.active.currenttomcat活跃session数
19tomcat.sessions.active.maxtomcat最多活跃session数显示在监控页面,超过阈值可报警或者进行动态扩容重要
20tomcat.sessions.alive.max.secondtomcat最多活跃session数持续时间
21tomcat.sessions.rejected超过session最大配置后,拒绝的session个数显示在监控页面,方便分析问题
22tomcat.global.error错误总数显示在监控页面,方便分析问题
23tomcat.global.sent发送的字节数
24tomcat.global.request.maxrequest最长时间
25tomcat.global.request全局request次数和时间
26tomcat.global.received全局received次数和时间
27tomcat.servlet.requestservlet的请求次数和时间
28tomcat.servlet.errorservlet发生错误总数
29tomcat.servlet.request.maxservlet请求最长时间
30tomcat.threads.busytomcat繁忙线程显示在监控页面,据此检查是否有线程夯住
31tomcat.threads.currenttomcat当前线程数(包括守护线程)显示在监控页面重要
32tomcat.threads.config.maxtomcat配置的线程最大数显示在监控页面重要
33tomcat.cache.accesstomcat读取缓存次数
34tomcat.cache.hittomcat缓存命中次数
---CPU...---
35system.cpu.countCPU数量
36system.load.average.1mload average超过阈值报警重要
37system.cpu.usage系统CPU使用率
38process.cpu.usage当前进程CPU使用率超过阈值报警
39http.server.requestshttp请求调用情况显示10个请求量最大,耗时最长的URL;统计非200的请求量重要
40process.uptime应用已运行时间显示在监控页面
41process.files.max允许最大句柄数配合当前打开句柄数使用
42process.start.time应用启动时间点显示在监控页面
43process.files.open当前打开句柄数监控文件句柄使用率,超过阈值后报警重要

补充说明:
由于SpringBoot是内置Tomcat,很多在Prometheus上关于Tomcat的指标很难监控得到,需要编码获取。(也许还有更好的解决方案)

编写客户端前须知

通过上面我们可以掌握到,拿取并查看Prometheus中自身的一些监控指标。
而在流量监控这个开发需求中,我们需要自定义一些监控指标并且封装到该端口中。也正是本篇的技术所在,所以下面会先介绍编码中用到的一些概念。也就是上一篇对官方文档分析中剩下的精华部分。

总体结构

重新看下基于Prometheus的流量监控在技术上的架构和可能做的事:
完整架构
公司架构

而实现监控的关键就是上文所说的:

当Prometheus获取实例的HTTP端点时,客户库发送所有跟踪的度量指标数据到服务器上。

也就是回调
客户端必须在内部写入回调。客户通常应该遵循下面描述的结构。
这个关键类是Collector。这个有一个典型的方法collect(), 返回0~N个度量指标和这些指标的样本数据。
CollectorCollectorRegistry进行注册。通过传递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、警告策略、钉钉机器人等(这块也可以跟进学习)

代码学习参考

client_java样例
Gauge样例

方案一:SpringBoot -> Pushgateway <- Prometheus

首先,在调用到QpsSubmitFilter的时候,分别调用了YzpUtil.getIpAddr(req)方法获取Ip和TpsCache.increase(this.projectName, simpleDateFormat.format(new Date()), ip)方法以时间和ip维度对tps累加。
下面具体分析TpsCache这个类:

  1. 类加载时,静态代码块优先执行,通过调用java.net包中的方法,将当前ip封装成map并放到gmap<"ip",{localIp}>中。
  2. increase()方法加了方法锁,该方法详细流程如下:

    1. 根据projectName项目名参数拼接tps 成 map的key值,获取map对应的value值:timeMap<time,ConcurrentSkipListMap<>>
    2. 如果timeMap不为空,再根据time时间参数作为map的key值,获取对应的value值:ipMap<String, Integer>。也就是该时间域下的Ip-Tps映射集。
      否则,初始化timeMap和ipMap。
    3. 如果ipMap不为空,则调用tpsIncrease()方法对该ip对应的tps进行累加并更新ipMap。
      否则,调用initIpMap()方法初始化ipMap,初始化时将该ip参数对应的tps置为1。
  3. push()方法是利用网关采集数据,并将metric通过Put推送到PushGateway上。
  4. submit()方法

    1. 先获取到当前ip,如果ip获取失败,会打日志警告,并且该方法直接当成成功返回。
    2. 接着同样是,根据projectName项目名参数拼接tps 成 gaugeMap<String, Gauge>的key值,获取对应的value值:qpsMin。
      如果为空则注册该指标并且将它放入到gaugeMap中。
    3. 再从map里面拿出该key对应的值projMap,如果存在则更新PushGateway上job为“tps”的gmap对应值,并且根据time时间参数拿到对应的map集,拼接出{ip}---{time}---{localIp}作为标签key,ip对应的tps作为value 提交。并且清除已处理的数据。
      否则,如果projMap为空。则清空PushGateway上job为“tps”的gmap对应值和qpsMin,并且打出警告日志。
  5. 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上制定频率来拉取,所以原先的缓存策略也直接取消掉了,使得目前代码更加简洁直观。

详细流程
(目前的实现如下,可能还有许多地方需要改进)

  1. 由于要先提前定义好指标,才能在InitPrometheus下提交到监控端口中。所以需要在MyMetrics下定义基于Gauge的自定义指标(可以动态获取项目名,方便后续扩展)

    1. 静态代码块调用获取yml配置值的工具类,处理后赋给projectName,作为指标名
    2. 根据访问Ip、提交时间戳(精确到分)、请求者Ip、请求地址、请求方式(Get/Post)、响应状态,实例化Gauge。
    3. submit()方法在每次调用的时候再对应的标签下累加一次值,其中调用到的getLocalIp()通过java.net包实现,获取当前ip
  2. InitPrometheus继承了ApplicationListener<ContextRefreshedEvent>,在初始化的时候将Metrics标签注册到暴露给Prometheus监听的端口上。
  3. PrometheusSubmitFilter提交过滤器,过滤掉静态资源路径下的请求、获取Metrics标签对应的参数值(涉及到自定义工具类GetIpAddressUtil)。并调用MyMetrics的submit()方法。
  4. 将该拦截器注册到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
Last modification:January 29th, 2021 at 01:55 pm
喵ฅฅ