Loading... 上篇传送门: <div class="preview"> <div class="post-inser post box-shadow-wrap-normal"> <a href="http://www.tangsong.fun/index.php/Prometheus1.html" target="_blank" class="post_inser_a no-external-link no-underline-link"> <div class="inner-image bg" style="background-image: url(https://blog-picture01.oss-cn-shenzhen.aliyuncs.com/img/20210128114519.JPG);background-size: cover;"></div> <div class="inner-content" > <p class="inser-title">[Prometheus]理论</p> <div class="inster-summary text-muted"> Prometheus是由SoundCloud开发的开源监控报警系统和时序列数据库(TSDB)。基于Go语言开发,是... </div> </div> </a> <!-- .inner-content #####--> </div> <!-- .post-inser ####--> </div> <!--more--> ## SpringBoot配置Actuator metrics的监控 Actuator 是 Spring Boot 提供的对应用系统的自省和监控功能。通过 Actuator,可以使用数据化的指标去度量应用的运行情况,比如查看服务器的磁盘、内存、CPU等信息,系统的线程、gc、运行状态等等。Prometheus指标只是其中之一,也是本次流量监控用到的主体。 Actuator 通常通过使用 HTTP 和 JMX 来管理和监控应用,大多数情况使用 HTTP 的方式。 总之,先跑起来再说: (1)Maven引入Actuator和Prometheus ```java <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上配置信息 ```yaml management: endpoints: web: exposure: # 开启所有端点 include: '*' ``` (3)然后就可以在项目中查看运行的指标了。 Actuator指标:`localhost:8080/actuator/metrics` Prometheus指标:`localhost:8080/actuator/prometheus` 贴一份Actuator对应Prometheus的监控指标: ```java # 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 | --- | | | | | 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`也应该遵循这点。 ```java class YourClass { static final Counter requests = Counter.build() .name("requests_total") .help("Requests.").register(); } ``` 上面的例子,使用默认的 `CollectorRegistry`进行注册。如果只是单纯的调用build()方法,度量指标将不会被注册(对于单元测试来说很方便),你还可以将 `CollectorRegistry`传递给register()(方便批作业处理)。这种方法就是默认注册,自定义指标只需要模仿它写就行了。 #### 模型分析(以Gauge为例) ```java 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 客户端库应该支持任意大小的标签列表。客户端库必须验证标签名称是否符合[要求](https://prometheus.io/docs/concepts/data_model/#metric-names-and-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样例](https://github.com/prometheus/client_java) [Gauge样例](https://github.com/prometheus/client_java/blob/master/simpleclient/src/main/java/io/prometheus/client/Gauge.java) ### 方案一: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()方法加了方法锁,该方法详细流程如下: 3. 根据 `projectName`项目名参数拼接tps 成 map的key值,获取map对应的value值:`timeMap<time,ConcurrentSkipListMap<>>` 4. 如果timeMap不为空,再根据 `time`时间参数作为map的key值,获取对应的value值:ipMap<String, Integer>。也就是该时间域下的Ip-Tps映射集。 否则,初始化timeMap和ipMap。 5. 如果ipMap不为空,则调用 `tpsIncrease()`方法对该ip对应的tps进行累加并更新ipMap。 否则,调用 `initIpMap()`方法初始化ipMap,初始化时将该ip参数对应的tps置为1。 6. push()方法是利用网关采集数据,并将metric通过Put推送到PushGateway上。 7. submit()方法 8. 先获取到当前ip,如果ip获取失败,会打日志警告,并且该方法直接当成成功返回。 9. 接着同样是,根据 `projectName`项目名参数拼接tps 成 gaugeMap<String, Gauge>的key值,获取对应的value值:qpsMin。 如果为空则注册该指标并且将它放入到gaugeMap中。 10. 再从map里面拿出该key对应的值projMap,如果存在则更新PushGateway上job为“tps”的gmap对应值,并且根据time时间参数拿到对应的map集,拼接出 `{ip}---{time}---{localIp}`作为标签key,ip对应的tps作为value 提交。并且清除已处理的数据。 否则,如果projMap为空。则清空PushGateway上job为“tps”的gmap对应值和qpsMin,并且打出警告日志。 11. destory()方法,删除pg上job名称为tps对应的gmap。 PreDestroy注解可以使得用户回调通知此实例正在被删除。 #### 代码解读 ```java 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; } } ``` ```java 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; } ``` ```java @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的自定义指标(可以动态获取项目名,方便后续扩展) 2. 静态代码块调用获取yml配置值的工具类,处理后赋给projectName,作为指标名 3. 根据访问Ip、提交时间戳(精确到分)、请求者Ip、请求地址、请求方式(Get/Post)、响应状态,实例化Gauge。 4. submit()方法在每次调用的时候再对应的标签下累加一次值,其中调用到的getLocalIp()通过java.net包实现,获取当前ip 5. `InitPrometheus`继承了 `ApplicationListener<ContextRefreshedEvent>`,在初始化的时候将Metrics标签注册到暴露给Prometheus监听的端口上。 6. `PrometheusSubmitFilter`提交过滤器,过滤掉静态资源路径下的请求、获取Metrics标签对应的参数值(涉及到自定义工具类 `GetIpAddressUtil`)。并调用MyMetrics的submit()方法。 7. 将该拦截器注册到 `CommonConfiguration`中。 #### 代码解读(优化后) ```java /** * 描述: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; } } ``` ```java /** * 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); } } ``` ```java /** * 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)); } } } ``` ```java @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监控指标分析 可能对流量监控有用的参数: ```java # 请求总次数 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:August 21, 2022 © Allow specification reprint Like 0 喵ฅฅ