Spring Boot代理API耗时异常,如何分步测量各环节耗时?
Spring Boot代理API耗时排查方案
一、手动埋点(最直接定位)
在代理的关键环节插入时间记录,计算各步骤耗时并打印日志,核心监控节点包括:
- 请求接收起始点
- 请求体读取完成后
- 转发请求前
- 目标API响应接收完成后
- 响应返回给客户端前
示例代码:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpServletRequest; @RestController @RequestMapping("/proxy") public class ProxyController { private static final Logger log = LoggerFactory.getLogger(ProxyController.class); private final RestTemplate restTemplate; private static final String TARGET_API_URL = "http://target-api.example.com"; public ProxyController(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @PostMapping("/**") public ResponseEntity<String> proxyRequest(@RequestBody String payload, HttpServletRequest request) { // 1. 请求接收起始点 long totalStart = System.currentTimeMillis(); String requestUri = request.getRequestURI(); log.info("开始处理请求:{},请求方法:{}", requestUri, request.getMethod()); // 2. 读取请求体完成后 long afterReadPayload = System.currentTimeMillis(); int payloadSize = payload.getBytes().length; log.info("读取请求体耗时:{}ms,请求体大小:{}字节", afterReadPayload - totalStart, payloadSize); // 3. 转发请求前 long beforeForward = System.currentTimeMillis(); String targetUrl = TARGET_API_URL + requestUri.replace("/proxy", ""); log.info("准备转发至目标API:{}", targetUrl); // 4. 调用目标API并接收响应 ResponseEntity<String> targetResponse = restTemplate.postForEntity(targetUrl, payload, String.class); long afterForward = System.currentTimeMillis(); log.info("目标API响应耗时:{}ms,响应状态码:{}", afterForward - beforeForward, targetResponse.getStatusCodeValue()); // 5. 返回响应前 long beforeResponse = System.currentTimeMillis(); log.info("处理响应准备返回耗时:{}ms", beforeResponse - afterForward); // 总耗时 log.info("请求{}处理完成,总耗时:{}ms", requestUri, System.currentTimeMillis() - totalStart); return targetResponse; } }
二、Spring拦截器统一监控
通过自定义拦截器统一拦截所有请求,批量记录各阶段耗时,同时借助ContentCachingRequestWrapper读取请求体大小,避免重复埋点:
1. 自定义计时拦截器
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.util.ContentCachingRequestWrapper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class TimingInterceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(TimingInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { long startTime = System.currentTimeMillis(); request.setAttribute("startTime", startTime); log.info("请求进入:{} {},客户端IP:{}", request.getMethod(), request.getRequestURI(), request.getRemoteAddr()); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { if (request instanceof ContentCachingRequestWrapper) { ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request; long startTime = (Long) request.getAttribute("startTime"); long currentTime = System.currentTimeMillis(); int payloadSize = wrappedRequest.getContentAsByteArray().length; log.info("读取请求体完成,耗时:{}ms,请求体大小:{}字节", currentTime - startTime, payloadSize); } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { long startTime = (Long) request.getAttribute("startTime"); long totalTime = System.currentTimeMillis() - startTime; String errorMsg = ex != null ? ",异常:" + ex.getMessage() : ""; log.info("请求结束:{} {},响应状态:{},总耗时:{}ms{}", request.getMethod(), request.getRequestURI(), response.getStatus(), totalTime, errorMsg); } }
2. 注册拦截器和请求体缓存过滤器
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.ContentCachingRequestFilter; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new TimingInterceptor()) .addPathPatterns("/**"); } @Bean public FilterRegistrationBean<ContentCachingRequestFilter> contentCachingRequestFilter() { FilterRegistrationBean<ContentCachingRequestFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new ContentCachingRequestFilter()); registrationBean.addUrlPatterns("/*"); return registrationBean; } }
三、利用Micrometer做精细化监控
Spring Boot默认集成Micrometer,可通过它对转发请求、请求体读取等环节做指标监控,方便后续做可视化分析:
1. 给RestTemplate添加拦截器记录转发耗时
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.web.client.RestTemplate; import java.io.IOException; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate(MeterRegistry meterRegistry) { RestTemplate restTemplate = new RestTemplate(); restTemplate.getInterceptors().add((request, body, execution) -> { Timer.Sample sample = Timer.start(meterRegistry); try { ClientHttpResponse response = execution.execute(request, body); sample.stop(Timer.builder("proxy.forward.request.duration") .tag("target_url", request.getURI().toString()) .tag("status_code", String.valueOf(response.getStatusCode().value())) .register(meterRegistry)); return response; } catch (IOException e) { sample.stop(Timer.builder("proxy.forward.request.duration") .tag("target_url", request.getURI().toString()) .tag("error", e.getClass().getSimpleName()) .register(meterRegistry)); throw e; } }); return restTemplate; } }
2. 自定义计时器记录其他环节
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpServletRequest; @RestController @RequestMapping("/proxy") public class ProxyController { private final RestTemplate restTemplate; private final MeterRegistry meterRegistry; private static final String TARGET_API_URL = "http://target-api.example.com"; public ProxyController(RestTemplate restTemplate, MeterRegistry meterRegistry) { this.restTemplate = restTemplate; this.meterRegistry = meterRegistry; } @PostMapping("/**") public ResponseEntity<String> proxyRequest(@RequestBody String payload, HttpServletRequest request) { // 记录读取请求体耗时 Timer.Sample readSample = Timer.start(meterRegistry); int payloadSize = payload.getBytes().length; readSample.stop(Timer.builder("proxy.read.request.body.duration") .tag("request_uri", request.getRequestURI()) .tag("payload_size", String.valueOf(payloadSize)) .register(meterRegistry)); // 转发请求(已通过RestTemplate拦截器记录耗时) String targetUrl = TARGET_API_URL + request.getRequestURI().replace("/proxy", ""); return restTemplate.postForEntity(targetUrl, payload, String.class); } }
四、额外排查点
- 检查连接池配置:若使用默认RestTemplate客户端,可能存在连接复用不足导致请求等待。可切换到
HttpComponentsClientHttpRequestFactory配置连接池:
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.springframework.context.annotation.Bean; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(200); // 最大总连接数 connectionManager.setDefaultMaxPerRoute(50); // 每个目标地址的最大连接数 HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory( HttpClients.custom().setConnectionManager(connectionManager).build()); factory.setConnectTimeout(3000); // 连接超时时间 factory.setReadTimeout(5000); // 读取超时时间 return new RestTemplate(factory); } }
- 关联耗时与请求体大小:在日志中同时记录请求体大小和各环节耗时,对比耗时久的请求是否payload明显更大,验证你的猜测。
内容的提问来源于stack exchange,提问作者Himanshu Bhatt
相关产品推荐
相关产品推荐

