本系列代码地址:https://github.com/HashZhang/...
咱们应用 Spring Boot 的 SPI 机制对 Undertow 进行订制,次要有如下两个方面:
- 须要在 accesslog 中关上响应工夫统计。
- 冀望通过 JFR 监控每个 Http 申请,同时占用空间不能太大。
接下来咱们顺次实现这两个需要:
首先,咱们的框架作为根底组件,应该依照根底组件的规范来开发,应用 这个系列之前介绍的 spring.factories 这个 Spring Boot SPI 机制,在引入咱们这个根底组件依赖的时候,就主动加载对应配置。
而后,对于是否关上响应工夫统计,应该依据用户配置的 accesslog 格局而定(Undertow 的 accesslog 配置能够参考这个系列之前的文章)。
由此咱们来编写代码。目前比拟遗憾的是,Spring Boot 对接 Undertow 并没有间接的配置能够让 Undertow 关上响应工夫统计,然而能够通过实现 WebServerFactoryCustomizer
接口的形式,对结构 WebServer
的 WebServerFactory
进行订制。其底层实现原理非常简单(以下参考源码:WebServerFactoryCustomizerBeanPostProcessor.java):
- Spring Boot 中指定了
WebServerFactoryCustomizerBeanPostProcessor
这个BeanPostProcessor
. WebServerFactoryCustomizerBeanPostProcessor
的postProcessBeforeInitialization
办法(即在所有 Bean 初始化之前会调用的办法)中,如果 Bean 类型是WebServerFactory
,就将其作为参数传入注册的所有WebServerFactoryCustomizer
Bean 中进行自定义。
接下来咱们来实现自定义的 WebServerFactoryCustomizer
DefaultWebServerFactoryCustomizer
package com.github.hashjang.spring.cloud.iiford.spring.cloud.webmvc.undertow;import io.undertow.UndertowOptions;import org.apache.commons.lang.StringUtils;import org.springframework.boot.autoconfigure.web.ServerProperties;import org.springframework.boot.web.embedded.undertow.ConfigurableUndertowWebServerFactory;import org.springframework.boot.web.server.WebServerFactoryCustomizer;public class DefaultWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableUndertowWebServerFactory> { private final ServerProperties serverProperties; public DefaultWebServerFactoryCustomizer(ServerProperties serverProperties) { this.serverProperties = serverProperties; } @Override public void customize(ConfigurableUndertowWebServerFactory factory) { String pattern = serverProperties.getUndertow() .getAccesslog().getPattern(); // 如果 accesslog 配置中打印了响应工夫,则关上记录申请开始工夫配置 if (logRequestProcessingTiming(pattern)) { factory.addBuilderCustomizers(builder -> builder.setServerOption( UndertowOptions.RECORD_REQUEST_START_TIME, true ) ); } } private boolean logRequestProcessingTiming(String pattern) { //如果没有配置 accesslog,则间接返回 false if (StringUtils.isBlank(pattern)) { return false; } //目前只有 %D 和 %T 这两个占位符和响应工夫无关,通过这个判断 //其余的占位符信息,请参考系列之前的文章 return pattern.contains("%D") || pattern.contains("%T"); }}
而后咱们通过 spring.factories SPI 机制将这个类以一个单例 Bean 的模式,注册到咱们利用 ApplicationContext 中,如图所示:
在 Configuration 和 spring.factories 之间多了一层 AutoConfiguration 的起因是:
- 隔离 SPI 与 Configuration,在 AutoConfiguration 同一治理相干的 Configuration。
@AutoConfigurationBefore
等相似的注解只能用在 SPI 间接加载的 AutoConfiguration 类下面才无效,隔离这一层也是出于这个考量。
在系列后面的文章中,咱们提到过咱们引入了 prometheus 的依赖。在引入这个依赖后,对于每个 http 申请,都会在申请完结返回响应的时候,将响应工夫以及响应码和异样等,记入统计,其中的内容相似于:
http_server_requests_seconds_count{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/query/orders",} 120796.0http_server_requests_seconds_sum{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/query/orders",} 33588.274025738http_server_requests_seconds_max{exception="None",method="POST",outcome="SUCCESS",status="200",uri="/query/orders",} 0.1671125http_server_requests_seconds_count{exception="MissingRequestHeaderException",method="POST",outcome="CLIENT_ERROR",status="400",uri="/query/orders",} 6.0http_server_requests_seconds_sum{exception="MissingRequestHeaderException",method="POST",outcome="CLIENT_ERROR",status="400",uri="/query/orders",} 0.947300794http_server_requests_seconds_max{exception="MissingRequestHeaderException",method="POST",outcome="CLIENT_ERROR",status="400",uri="/query/orders",} 0.003059704
能够看出,记录了从程序开始到当初,以 exception,method,outcome,status,uri 为 key 的调用次数,总工夫和最长工夫。
同时呢,还能够搭配 @io.micrometer.core.annotation.Timer
注解,订制监控并且减少 Histogram,例如:
//@Timer 注解想失效须要注册一个 io.micrometer.core.aop.TimedAspect Bean 并且启用切面@Beanpublic TimedAspect timedAspect(MeterRegistry registry) { return new TimedAspect(registry);}@Timed(histogram=true)@RequestMapping("/query/orders")public xxxx xxxx() { .....}
这样就会除了下面的数据额定失去相似于 bucket 的统计数据:
http_server_requests_seconds_bucket{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/facts-center/query/frontend/market-info",le="0.001",} 0.0http_server_requests_seconds_bucket{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/facts-center/query/frontend/market-info",le="0.001048576",} 0.0http_server_requests_seconds_bucket{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/facts-center/query/frontend/market-info",le="0.001398101",} 0.0http_server_requests_seconds_bucket{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/facts-center/query/frontend/market-info",le="0.001747626",} 0.0//省略两头的工夫层级http_server_requests_seconds_bucket{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/facts-center/query/frontend/market-info",le="30.0",} 1.0http_server_requests_seconds_bucket{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/facts-center/query/frontend/market-info",le="+Inf",} 1.0
以上这些统计给咱们剖析问题带来了如下不便的中央:
- 采集剖析压力过大:咱们采纳了 grafana 采集 prometheus 上报的指标数据,grafana 的时序数据库会将采集到的数据全副保留。自带的 http 监控指标过多,一个门路,一个后果,一个异样,一个办法就有一个特定指标,如果是有将参数作为门路参数的接口,那么这个指标就更多更多了,例如将 userId 放入门路中。咱们其实只关注出问题的时间段的申请情况,然而咱们并不能预测啥时候出问题,也就无奈按需提取,只能始终采集并保留,这也就导致压力过大。
- 指标对于压力不敏感,无奈很精确的用指标进行报警:因为指标并不是采集后就清空,而是从程序开始就始终采集。所以随着程序的运行,这些指标对于刹时压力的体现稳定越来越小。
所以,咱们根本不会通过这个指标进行问题定位,也就没必要开启了,于是咱们禁用这个 http 申请响应采集,目前没有很优雅的形式独自禁用,只能通过主动扫描注解中排除,例如:
@SpringBootApplication( scanBasePackages = {"com.test"} //敞开 prometheus 的 http request 统计,咱们用不到 , exclude = WebMvcMetricsAutoConfiguration.class)
- 首先,JFR 采集是过程内的,并且 JVM 做了很多优化,性能耗费很小,能够指定保留多少天或者保留最多多大的 JFR 记录(保留在本地长期目录),咱们能够随用随取。
- 并且,咱们能够将咱们感兴趣的信息放入 JFR 事件,作比拟灵便的定制。
- 对于某个申请工夫过长始终没有响应的,咱们能够分为收到申请和申请响应两个 JFR 事件。
咱们来定义这两个 JFR 事件,一个是收到申请的事件,另一个是申请响应的事件:
HttpRequestReceivedJFREvent.java
package com.github.hashjang.spring.cloud.iiford.spring.cloud.webmvc.undertow.jfr;import jdk.jfr.Category;import jdk.jfr.Event;import jdk.jfr.Label;import jdk.jfr.StackTrace;import javax.servlet.ServletRequest;@Category({"Http Request"})@Label("Http Request Received")@StackTrace(false)public class HttpRequestReceivedJFREvent extends Event { //申请的 traceId,来自于 sleuth private final String traceId; //申请的 spanId,来自于 sleuth private final String spanId; public HttpRequestReceivedJFREvent(ServletRequest servletRequest, String traceId, String spanId) { this.traceId = traceId; this.spanId = spanId; }}
HttpRequestJFREvent.java
package com.github.hashjang.spring.cloud.iiford.spring.cloud.webmvc.undertow.jfr;import io.undertow.servlet.spec.HttpServletRequestImpl;import io.undertow.servlet.spec.HttpServletResponseImpl;import jdk.jfr.Category;import jdk.jfr.Event;import jdk.jfr.Label;import jdk.jfr.StackTrace;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import java.util.Enumeration;@Category({"Http Request"})@Label("Http Request")@StackTrace(false)public class HttpRequestJFREvent extends Event { //申请的 http 办法 private final String method; //申请的门路 private final String path; //申请的查问参数 private final String query; //申请的 traceId,来自于 sleuth private String traceId; //申请的 spanId,来自于 sleuth private String spanId; //产生的异样 private String exception; //http 响应码 private int responseStatus; public HttpRequestJFREvent(ServletRequest servletRequest, String traceId, String spanId) { HttpServletRequestImpl httpServletRequest = (HttpServletRequestImpl) servletRequest; this.method = httpServletRequest.getMethod(); this.path = httpServletRequest.getRequestURI(); this.query = httpServletRequest.getQueryParameters().toString(); Enumeration<String> headerNames = httpServletRequest.getHeaderNames(); StringBuilder stringBuilder = new StringBuilder(); headerNames.asIterator().forEachRemaining(s -> stringBuilder.append(s).append(":").append(httpServletRequest.getHeader(s)).append("\n")); this.traceId = traceId; this.spanId = spanId; } public void setResponseStatus(ServletResponse servletResponse, Throwable throwable) { this.responseStatus = ((HttpServletResponseImpl) servletResponse).getStatus(); this.exception = throwable != null ? throwable.toString() : null; }}
而后,咱们仿照文中后面敞开的 WebMvcMetricsAutoConfiguration
中的 WebMvcMetricsFilter
编写咱们本人的 Filter 并仿照注册,这里咱们只展现外围代码:
JFRTracingFilter.java
@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpRequestJFREvent httpRequestJFREvent = null; try { //从 sleuth 中获取 traceId 和 spanId TraceContext context = tracer.currentSpan().context(); String traceId = context.traceId(); String spanId = context.spanId(); //收到申请就创立 HttpRequestReceivedJFREvent 并间接提交 HttpRequestReceivedJFREvent httpRequestReceivedJFREvent = new HttpRequestReceivedJFREvent(servletRequest, traceId, spanId); httpRequestReceivedJFREvent.commit(); httpRequestJFREvent = new HttpRequestJFREvent(servletRequest, traceId, spanId); httpRequestJFREvent.begin(); } catch (Exception e) { log.error("JFRTracingFilter-doFilter failed: {}", e.getMessage(), e); } Throwable throwable = null; try { filterChain.doFilter(servletRequest, servletResponse); } catch (IOException | ServletException t) { throwable = t; throw t; } finally { try { //无论如何,都会提交 httpRequestJFREvent if (httpRequestJFREvent != null) { httpRequestJFREvent.setResponseStatus(servletResponse, throwable); httpRequestJFREvent.commit(); } } catch (Exception e) { log.error("JFRTracingFilter-doFilter final failed: {}", e.getMessage(), e); } }}
咱们这一节针对 Undertow 进行了两个定制:别离是须要在 accesslog 中关上响应工夫统计以及通过 JFR 监控每个 Http 申请,同时占用空间不能太大。下一节,咱们将开始介绍咱们微服务的注册核心 Eureka 的应用以及细节配置。
微信搜寻“我的编程喵”关注公众号,每日一刷,轻松晋升技术,斩获各种offer: