news 2026/10/19 13:22:37

OpenFeign自定义Metrics:从P99定位到精确诊断的微服务观测实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenFeign自定义Metrics:从P99定位到精确诊断的微服务观测实践

前阵子排查一个线上问题,现象简单:核心接口的P99从50ms涨到800ms,依赖的下游服务网络抖动。我第一反应去翻OpenFeign监控面板,结果只有请求量和耗时曲线,没有失败率、没有重试次数,连到底是连接超时还是读超时都看不出来。当时我就意识到一个问题——OpenFeign自带的Metrics能力,只能告诉你“调用发生了”,不能告诉你“调用过程里发生了什么”。

这篇文章就是来补这块短板的。我会用OpenFeign官方提供的扩展机制,做一套自定义Metrics,把HTTP调用失败率、重试次数、业务请求最终成功率这些真正对排查有用的指标记下来。整个过程不动业务代码,基于Micrometer注册到Prometheus这类监控系统。如果你正在用Spring Cloud OpenFeign,又对调用链路的可观测性有要求,这套方案可以直接抄作业。

1. 先搞清楚:OpenFeign默认的Metrics到底缺什么

1.1 Feign有Metrics机制,但默认统计粒度很粗

很多人不知道,Feign从早期版本就提供了Metrics相关的扩展点,后来官方还专门出了feign-micrometer模块。接入方式很简单,给Feign.Builder加一个Capability:

Feign.builder() .addCapability(new MicrometerCapability(meterRegistry)) .build();

加上这行之后,Micrometer会自动对Feign的Client等组件做增强,在/actuator/prometheus里就会出现类似这样的指标:

feign_Client_http_requests_total{method="GET", uri="/orders", http_status="200"} feign_Client_http_response_duration_seconds_bucket{method="GET", uri="/orders"}

问题是,这套默认指标有几个明显的边界:

  • 它是按HTTP请求维度计数的,一次业务调用如果内部重试了3次,会产生4条独立的请求记录,但你看不出来这是一次业务请求。
  • 它不统计重试次数。Feign的Retryer到底retry了几次、每次间隔多久,全部不可见。
  • 它不区分失败类型。连接超时、读超时、DNS解析失败、5xx响应,在默认指标里要么混在一起,要么要到uri+http_status维度里去翻。
  • 它不知道业务层面的最终结果。下游返回了500但有ErrorDecoder兜底转成业务异常,这次调用在HTTP层面是失败的,在业务层面也是失败的;但如果重试后第二次成功了,业务层面其实是成功的。

还有一个隐藏问题:如果uri带了动态参数,比如/orders/12345、/orders/67890,直接拿默认uri做标签,会把手里的监控搞出超高基数。所以不管从哪个角度看,默认能力都只能作为兜底,真要在生产环境用它判断服务健康度,远远不够。

1.2 业务指标清单:先定义清楚再动手

动手写代码之前,我习惯先把指标定义清楚,否则写着写着会发现“这个指标到底代表什么”自己都说不清。这次我最终圈定了六类指标:

指标名含义计算方式用途
feign.http.callsHTTP调用次数每次底层HTTP执行+1,按status分类观察真实网络请求量
feign.http.durationHTTP调用耗时每次底层HTTP执行计时判断下游耗时变化
feign.retry.count重试次数每次Retryer触发重试+1判断重试是否频繁
feign.business.calls业务请求次数一次Feign方法调用+1,按result分类判断业务接口最终成败
feign.business.failure.rate5分钟失败率滑动窗口内失败数/总数定位短时故障
feign.business.retry.rate5分钟重试率滑动窗口内重试次数/调用数判断重试补偿是否有效

这里最关键的是区分HTTP调用和业务请求。打个比方,用户下了一单外卖,相当于一次业务请求;骑手从商家跑到用户家,相当于一次HTTP调用。如果骑手途中爆胎,平台重新派了一个骑手,那这次业务请求过程中发生了两次HTTP调用。OpenFeign默认指标只知道两趟跑腿分别用了多长时间,不知道用户那单最终有没有送到。

所以在自定义Metrics里,我会同时保留两个视角:HTTP调用级,用来观察底层网络质量;业务请求级,用来观察下游服务的真实可用性。

2. 方案选型:直接扩Feign还是套一层AOP

2.1 三个候选方案对比

在动手之前,我对比过三种常见方案。

第一个是AOP切面。切所有@FeignClient接口的公共子类或者接口本身,在方法前后记录耗时和异常。这个方案看起来最简单,但实际上只能包在接口方法外层,抓不到Feign内部的重试过程。你看到的异常是重试耗尽之后抛出来的那一个,中间重试了几次、每次底层HTTP状态是什么,AOP一无所知。

第二个是RequestInterceptor。Feign的RequestInterceptor只会在发请求前执行,能往Request里塞东西,但没有后置回调。你想在响应回来之后记录状态码,对不起,Feign官方没有提供对称的“ResponseInterceptor”。有人会自己去装饰Request模板或者用AsyncResponseHandler,那基本就是自己重新实现一遍Feign的处理链路,得不偿失。

第三个是Capability扩展。这是Feign官方留下的扩展口,它的enrich系列方法可以同时包装Client、Retryer、InvocationHandlerFactory等核心组件。你可以在HTTP执行前后记录调用,在Retryer触发时记录重试,在方法调用成功或失败时记录业务结果。三条链路全都能抓到。

这三个方案的区别,用一个表格看更清楚:

对比项AOP切面RequestInterceptorCapability扩展
实现成本低低中
是否能拿重试次数否否能
是否能区分HTTP/业务两层语义困难否能
对Feign内部结构的侵入性无侵入低侵入官方扩展点
是否影响业务代码无无无

2.2 为什么我最终选了Capability

原因有两个。

第一,Capability能覆盖Feign完整的调用生命周期。Feign整个执行链路无非是:业务方法被InvocationHandler调用,内部构造Request,交给Client发HTTP请求,如果抛了RetryableException就经过Retryer决定是否重来一次。Capability刚好可以把这三个关键位置的组件都包一遍,所有我要的指标都能在对应位置拿到。

第二,它能天然兼容Spring Cloud OpenFeign的Bean装配机制。Spring Cloud OpenFeign提供了FeignBuilderCustomizer接口,运行时会拿着它去定制每一个Feign.Builder。我只需要注册一个FeignBuilderCustomizer,把自定义的Capability挂上去,所有Feign客户端就自动生效了,不用改任何业务Feign接口。

2.3 指标存储设计:谁写数据、谁算失败率

指标数据直接打到Micrometer上。计数用Counter,耗时用Timer,这两个都是标准用法,没什么好说的。真正需要设计的是失败率和重试率。

这两个指标是比例值,不是累计值。如果直接用“累计失败数 / 累计总数”去算,会有一个问题:上线早期请求量少、成功率高,后面某段时间大量失败,会被前面积累的总数稀释掉,失败率的数字不够敏感。所以我用的是5分钟滑动窗口,只统计最近5分钟内完成调用的样本。短时故障能明显看出来,又不会因为窗口太小产生剧烈抖动。

滑动窗口的核心代码不复杂,我会在下一章给出完整实现。这里先说明一个原则:Micrometer的Gauge是不能直接increment的,它只是一个取值函数。所以我会用一个AtomicLong保存当前窗口内算出来的百分比值,每次有业务调用完成时重新计算,再把Gauge指向这个AtomicLong。这样Gauge永远返回最新的失败率。

3. 动手实现:自定义Metrics全流程

3.1 第一步:定义指标记录器FeignMetricsRecorder

这个类是整个方案的“数据中枢”,所有组件拿到调用结果之后,都往这里面写。先看代码:

public class FeignMetricsRecorder { private final MeterRegistry registry; private final Duration window = Duration.ofMinutes(5); private final Deque<Sample> samples = new ArrayDeque<>(); private final AtomicLong failureRateRef = new AtomicLong(); private final AtomicLong retryRateRef = new AtomicLong(); public FeignMetricsRecorder(MeterRegistry registry) { this.registry = registry; Gauge.builder("feign.business.failure.rate", failureRateRef, AtomicLong::get) .description("最近5分钟失败率(%)") .register(registry); Gauge.builder("feign.business.retry.rate", retryRateRef, AtomicLong::get) .description("最近5分钟重试率(%)") .register(registry); } public void recordHttpCall(String client, Request request, int status, long costNanos, Throwable error) { Counter.builder("feign.http.calls") .tag("client", safeTag(client)) .tag("status", classify(status, error)) .register(registry) .increment(); Timer.builder("feign.http.duration") .tag("client", safeTag(client)) .register(registry) .record(costNanos, TimeUnit.NANOSECONDS); } public void recordRetry(String client, Request request) { Counter.builder("feign.retry.count") .tag("client", safeTag(client)) .register(registry) .increment(); } public synchronized void recordBusinessCall(String client, boolean success, int attempts, int retries, long costNanos) { long now = System.currentTimeMillis(); samples.addLast(new Sample(now, success, retries)); while (!samples.isEmpty() && samples.peekFirst().timestamp < now - window.toMillis()) { samples.removeFirst(); } long total = 0; long fail = 0; long retrySum = 0; for (Sample s : samples) { total++; if (!s.success) { fail++; } retrySum += s.retries; } failureRateRef.set(total == 0 ? 0 : fail * 100 / total); retryRateRef.set(total == 0 ? 0 : retrySum * 100 / total); Counter.builder("feign.business.calls") .tag("client", safeTag(client)) .tag("result", success ? "success" : "failure") .register(registry) .increment(); } private String classify(int status, Throwable error) { if (error != null) { return "exception"; } if (status >= 200 && status < 300) { return "2xx"; } if (status >= 300 && status < 400) { return "3xx"; } if (status >= 400 && status < 500) { return "4xx"; } return "5xx"; } private String safeTag(String value) { return value == null || value.isBlank() ? "unknown" : value; } private static class Sample { long timestamp; boolean success; int retries; Sample(long timestamp, boolean success, int retries) { this.timestamp = timestamp; this.success = success; this.retries = retries; } } }

这里我故意用了一个synchronized方法去操作ArrayDeque。因为ArrayDeque本身线程不安全,而一次Feign调用几百毫秒才完成一次,这个频率下加锁完全没压力。如果你的系统调用量极高,每秒几千次Feign调用,再考虑换成ConcurrentLinkedQueue加定时清理,或者直接用Micrometer内部的TimeWindowCounter。先把正确性做出来,再去纠结性能,这个顺序不能反。

另外注意,recordHttpCall里每来一条就register一次。生产环境建议预先缓存Meter,避免反复创建同一个指标对象。这里面Micrometer本身有缓存机制,开销不大,但为了严谨,还是提一句。

3.2 第二步:创建线程级上下文FeignCallContext

一次业务请求内部可能发生多次HTTP调用和多次重试,怎么把这两者关联到同一个业务请求上?我的方案是用ThreadLocal保存一个上下文对象。

final class FeignCallContext { private static final ThreadLocal<FeignCallContext> HOLDER = new ThreadLocal<>(); final String client; int attempts = 0; int retries = 0; boolean finalSuccess = false; private FeignCallContext(String client) { this.client = client; } static FeignCallContext begin(String client) { FeignCallContext ctx = new FeignCallContext(client); HOLDER.set(ctx); return ctx; } static FeignCallContext current() { return HOLDER.get(); } void clear() { HOLDER.remove(); } }

为什么用ThreadLocal?因为Feign同步调用整个过程发生在同一个线程里。第一次HTTP调用失败,抛异常,随后Retryer决定重试,下一次HTTP调用还在同一个线程里执行。所以我在MetricsMethodHandler里begin之后,MetricsClient和MetricsRetryer都能通过FeignCallContext.current()取到同一个上下文,累加各自的计数。等业务方法最终返回或者抛出异常,在finally里clear,避免线程池复用导致上下文串台。

这里有一个必须强调的点:务必在finally里清理ThreadLocal。很多线上偶发问题都是ThreadLocal没清干净引起的。因为Tomcat等Web容器的工作线程是复用的,上一次请求留下的上下文可能被下一次请求读到,指标就会张冠李戴。

3.3 第三步:包装Client,记录每次真实HTTP调用

Client是Feign真正发起HTTP请求的地方。我写一个MetricsClient,把原来的Client包一层:

public class MetricsClient implements Client { private final Client delegate; private final FeignMetricsRecorder recorder; public MetricsClient(Client delegate, FeignMetricsRecorder recorder) { this.delegate = delegate; this.recorder = recorder; } @Override public Response execute(Request request, Request.Options options) throws IOException { FeignCallContext ctx = FeignCallContext.current(); String client = ctx != null ? ctx.client : resolveClient(request); long start = System.nanoTime(); try { Response response = delegate.execute(request, options); if (ctx != null) { ctx.attempts++; } recorder.recordHttpCall(client, request, response.status(), System.nanoTime() - start, null); return response; } catch (IOException e) { if (ctx != null) { ctx.attempts++; } recorder.recordHttpCall(client, request, 0, System.nanoTime() - start, e); throw e; } } private String resolveClient(Request request) { try { return URI.create(request.url()).getHost(); } catch (Exception ex) { return "unknown"; } } }

这里有几个细节要说明。

第一,HTTP调用失败包括两部分:异常和5xx。IOException网络异常走catch分支,状态码为0,分类为exception;HTTP 500返回时Feign的默认Decoder会抛FeignException,但它发生在我这个方法返回之后,所以MetricsClient这里记录的是HTTP层面的调用结果,状态码是500,分类为5xx。两种失败最后会反映到不同维度,方便排查。

第二,尝试次数累加放在真正执行之后。我这里的ctx.attempts++无论成功失败都会执行。因为一次HTTP执行,无论结果如何,都算一次尝试。这个数最终会和重试次数对得上:attempts = 1 + retries。

第三,client标签的兜底。如果ThreadLocal里拿不到上下文(比如某些场景没有经过MethodHandler包装),就尝试从URL的host里解析服务名。在负载均衡场景下,Feign的URL里通常就是服务名或注册中心名字,拿到的host很有辨识度。

3.4 第四步:包装Retryer,把重试次数抓出来

Retryer负责决定要不要重试。包装它就能在每次重试决策发生的时候记录一次重试:

public class MetricsRetryer implements Retryer { private final Retryer delegate; private final FeignMetricsRecorder recorder; public MetricsRetryer(Retryer delegate, FeignMetricsRecorder recorder) { this.delegate = delegate; this.recorder = recorder; } @Override public void continueOrPropagate(RetryableException e) { FeignCallContext ctx = FeignCallContext.current(); if (ctx != null) { ctx.retries++; } String client = ctx != null ? ctx.client : resolveClient(e.request()); recorder.recordRetry(client, e.request()); delegate.continueOrPropagate(e); } @Override public Retryer clone() { return new MetricsRetryer(delegate.clone(), recorder); } }

这里有一个特别容易踩的坑:Retryer的clone方法必须一起包装。Feign在处理每次请求时,会先retryer.clone()出一个独立的实例,用来跟踪本次请求的重试状态。如果我只包装了continueOrPropagate而不处理clone,那么clone出来的新实例是未包装的,我的recordRetry根本不会被调用,重试次数永远是0。

类似的问题还会出现在其他需要clone的组件上。所以凡是涉及到Feign里“每个请求独立状态”的组件,包装时必须考虑clone路径。

另外我遇到过一种情况:项目引入了spring-retry,用的不是Feign原生Retryer,而是Spring Retry的RetryTemplate。这种情况下重试决策发生在别的地方,包装Feign原生Retryer可能抓不到。如果你用的也是这种组合,要在RetryTemplate层面做类似的处理,或者把Retryer.Default显示配置出来。最简单的排查办法:先把重试策略显式设成new Retryer.Default(100L, 1000L, 3),确认自定义Metrics能看到重试次数之后,再考虑要不要换成Spring Retry。

3.5 第五步:包装MethodHandler,统计业务级成败

前面几层记录的都还是HTTP调用级数据,现在需要回到业务方法入口,统计“一次Feign方法调用最终到底成没成功”。

Feign的方法是经过InvocationHandler分发给MethodHandler处理的。我包装一下MethodHandler:

public class MetricsMethodHandler implements MethodHandler { private final MethodHandler delegate; private final FeignMetricsRecorder recorder; private final String client; public MetricsMethodHandler(MethodHandler delegate, FeignMetricsRecorder recorder, String client) { this.delegate = delegate; this.recorder = recorder; this.client = client; } @Override public Object invoke(Object[] argv) throws Throwable { FeignCallContext ctx = FeignCallContext.begin(client); long start = System.nanoTime(); try { Object result = delegate.invoke(argv); ctx.finalSuccess = true; recorder.recordBusinessCall(client, true, ctx.attempts, ctx.retries, System.nanoTime() - start); return result; } catch (Throwable t) { recorder.recordBusinessCall(client, false, ctx.attempts, ctx.retries, System.nanoTime() - start); throw t; } finally { ctx.clear(); } } }

有了这层包装,就能把FeignCallContext里累计的attempts和retries在业务方法结束时取出来,连同最终成功失败状态一起写进记录器。比如某次调用第一次返回500,触发重试,第二次成功,那么attempts=2、retries=1、finalSuccess=true。这样重试次数高但业务成功率也高的情况,说明重试机制在有效兜底;只有当重试次数高且业务成功率也低的时候,才能确认下游真的出问题了。

接下来是InvocationHandlerFactory的包装。Feign默认用InvocationHandler.Default来创建动态代理,InvocationHandlerFactory就是创建它的地方:

public class MetricsInvocationHandlerFactory implements InvocationHandlerFactory { private final InvocationHandlerFactory delegate; private final FeignMetricsRecorder recorder; public MetricsInvocationHandlerFactory(InvocationHandlerFactory delegate, FeignMetricsRecorder recorder) { this.delegate = delegate; this.recorder = recorder; } @Override public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) { String client = target.name(); Map<Method, MethodHandler> wrapped = new HashMap<>(); dispatch.forEach((method, handler) -> wrapped.put(method, new MetricsMethodHandler( handler, recorder, client))); return delegate.create(target, wrapped); } }

为什么用target.name()作为client标签?在Spring Cloud OpenFeign里,Target的name就是@FeignClient注解里配的服务名。用它做标签,监控面板上天然就能按服务名拆分。

3.6 第六步:通过Capability串起来,并接入Spring Boot

把上面几层用Capability封装到一起:

public class MetricsCapability implements Capability { private final FeignMetricsRecorder recorder; public MetricsCapability(FeignMetricsRecorder recorder) { this.recorder = recorder; } @Override public Client enrich(Client client) { return new MetricsClient(client, recorder); } @Override public Retryer enrich(Retryer retryer) { if (retryer == null || retryer == Retryer.NEVER_RETRY) { return retryer; } return new MetricsRetryer(retryer, recorder); } @Override public InvocationHandlerFactory enrich(InvocationHandlerFactory factory) { return new MetricsInvocationHandlerFactory(factory, recorder); } }

注意enrich(Retryer)里那个判断:如果当前Retryer是NEVER_RETRY,说明本来就不重试,我直接返回原对象,避免无意义的包装。

最后在Spring Boot里把它注册成全局配置:

@Configuration public class FeignMetricsAutoConfiguration { @Bean public FeignMetricsRecorder feignMetricsRecorder(MeterRegistry meterRegistry) { return new FeignMetricsRecorder(meterRegistry); } @Bean public FeignBuilderCustomizer feignMetricsCustomizer( FeignMetricsRecorder recorder) { return builder -> builder.addCapability(new MetricsCapability(recorder)); } }

FeignBuilderCustomizer是Spring Cloud OpenFeign提供的定制口,所有Feign.Builder被创建的时候都会应用这个定制器。还需要在配置里暴露Prometheus端点:

management: endpoints: web: exposure: include: health,metrics,prometheus

如果你的应用里还没有micrometer-registry-prometheus依赖,记得加进去。启动之后,访问/actuator/prometheus,搜索feign前缀的指标,就能看到这一套数据了。

在Prometheus里可以直接写查询了,典型的有:

# 按服务看5分钟HTTP 5xx速率 sum(rate(feign_http_calls_total{status="5xx"}[5m])) by (client) # 按服务看重试频率 sum(rate(feign_retry_count_total[5m])) by (client) # 看服务最近5分钟业务失败率 feign_business_failure_rate{client="order-service"}

注意Micrometer在Prometheus命名规则下,feign.http.calls会变成feign_http_calls_total,feign.retry.count会变成feign_retry_count_total,Gauge的feign.business.failure.rate会保持feign_business_failure_rate。写查询前先在/actuator/prometheus页面搜一下确认名称。

4. 藏在细节里的坑:标签粒度、配置上下文、并发清理

4.1 按服务拆tag,别做全站一个计数器

如果不用client标签区分服务,整个微服务集群的Feign指标全打在一个计数器上,出了问题你只能知道总失败率高了,不知道是哪个下游服务拖了后腿。这个意识一定要有。

我的代码里面有两个地方可以拿client名:一个是在MetricsMethodHandler里拿到Target.name(),这个最准确;另一个是在MetricsClient里从request.url()解析host,这个是兜底方案。实际运行中,以Target.name()为准,因为Spring Cloud OpenFeign的@FeignClient(name="order-service")里配置得非常明确。

4.2 FeignContext子上下文与Bean作用范围

这是一个很隐蔽的坑,我经历过不止一次。

@FeignClient(name="xxx", configuration = XxxConfig.class)里的XxxConfig是这个Feign客户端私有的子上下文。你在里面的Bean,只会对当前这个Feign客户端生效。如果你还有一个全局的@Configuration,里面的FeignBuilderCustomizer会被所有Feign客户端共享,因为FeignContext以主ApplicationContext作为父上下文,会从父级找到这个Bean。

所以,当你发现“自定义的FeignBuilderCustomizer为什么只对部分client生效”时,先检查是不是把它放到了某个@FeignClient的configuration里。要全局生效,最简单的方式是放到启动类所在的包下面,作为主上下文里的普通@Configuration。

同理,如果你在某个@FeignClient的configuration里注入了主上下文的Bean,有时会因为子上下文还没准备好而拿不到。建议用ObjectProvider或者@Lazy延迟获取。

4.3 并发采样队列:别在热路径上做傻事

我上面的recordBusinessCall用的是synchronized+ArrayDeque,简单、正确、不易错。但要清楚它的适用边界:单机每秒几十次Feign调用完全没问题;如果每秒几千次,再加滑动窗口遍历,锁竞争会变成热点。

这种情况下建议换思路:

  • 用ConcurrentLinkedQueue,配合ScheduledExecutorService每10秒清理一次过期样本。
  • 或者直接用Micrometer自带的TimeWindowCounter,它内部就是时间窗口,不用自己维护队列和锁。

不过大多数业务系统的Feign调用频率到不了那个量级。先把指标的正确性和可读性做出来,比一味追求高性能重要得多。

5. 常见问题与排查技巧实录

5.1 指标一直是0,可能的原因

我从排错顺序上整理一个清单:

  1. Prometheus端点没暴露。先访问/actuator/prometheus,看里面有没有feign开头的指标。没有的话,检查management.endpoints.web.exposure.include是否配置了prometheus。
  2. Feign根本没有走这个Builder。如果某些Feign Client是通过Feign.builder()手动构建的,没有经过FeignBuilderCustomizer,那自然没有指标。Spring Cloud环境里一般都会走,但排查时可以打个断点确认MetricsCapability是否被装配。
  3. 标签名拼写不一致。Prometheus里指标名带不带_total后缀,Counter和Gauge的命名规范不一样。在/actuator/prometheus里搜feign确认一下实际生成的指标名,再写查询。
  4. 指标被丢弃了。某些监控系统中,如果指标没有任何样本,会延迟显示。稍等一会儿再刷新。

5.2 重试次数统计不到

这个问题的出现频率很高,原因通常有这几个:

  • Retryer本身就是NEVER_RETRY。Feign默认不重试,如果你没有配置任何Retryer,重试次数当然永远是0。解决办法是显式配置一个,比如在配置类里提供:
@Bean public Retryer feignRetryer() { return new Retryer.Default(100L, 1000L, 3); }
  • clone方法没包装。前面已经强调过,MetricsRetryer.clone()必须返回包装后的实例,否则每个请求clone出去的新Retryer都不带统计逻辑。
  • 用的不是Feign原生Retryer。如果项目里用Spring Retry,需要去包装RetryTemplate,或者把重试策略迁回Feign原生Retryer。

有一点值得留意:重试次数不等于失败的HTTP调用次数。一次业务请求如果重试了2次,feign.retry.count会增加2,feign.http.calls也会多2条调用记录。这两个指标是在不同位置统计的,可以互相印证。

5.3 失败率数值飘忽不定

失败率用的是5分钟滑动窗口。窗口越小,对瞬时波动越敏感,但也越容易在流量低的时候出现大起大落。窗口越大,越能平滑噪声,但会掩盖短时故障。

我踩过的坑是:刚开始把窗口设成1分钟,结果每次发布或者下游重启时,失败率在几分钟内剧烈跳动,报警频繁触发而且不好定位。后来改成5分钟,再配合Prometheus的rate()函数看短时趋势,效果稳定很多。

如果你需要两类视角,可以保留一个5分钟失败率Gauge用于告警,再用PromQL的sum(rate(feign_http_calls_total{status="5xx"}[1m]))看1分钟短时波动。一慢一快,互为补充。

5.4 重试和失败率容易被误读

还有一种情况容易看走眼:监控面板上重试次数变高,业务失败率也跟着变高,第一反应是“下游挂了”。其实这两者同时变高,只说明重试没有挽救业务请求,服务确实不可用;如果重试次数高但业务失败率很低,反而是好事,说明重试机制在正常工作,下游的短暂错误被自动消化了。

所以告警规则不能只盯重试次数,要同时看“重试率”和“业务失败率”。比如可以设两条规则:

告警条件含义
5分钟业务失败率 > 10%,持续5分钟业务可用性明显下降
5分钟重试率 > 20%,且业务失败率 > 5%重试无法兜底,下游大概率出问题

6. 实操后的几点体会

这套自定义Metrics方案做完之后,我自己最大的感受是:先量后治理,很多问题其实早就存在,只是没被量化出来。之前某次线上排查,我一直以为是下游抖动导致超时,后来看到重试率数据才发现,Feign的默认Retryer根本没生效,超时之后直接抛异常,根本没有重试环节。没有指标的时候,我们只能靠猜;有了指标,至少在错误的链条上能往前推一步。

其次是建议不要在一个类里堆太多逻辑。这次我刻意把“记录指标”和“包装Feign组件”拆开,FeignMetricsRecorder只负责数据写入,MetricsClient、MetricsRetryer、MetricsMethodHandler各管一段。后面想加一个“按接口维度统计耗时”的需求,只需要在包装层多传一个方法名进去,完全不用动数据层。

最后再分享一个小技巧:上线这套Metrics之后,一定要先看几天真实数据,再决定告警阈值。很多团队拿到指标第一件事就是配告警,结果频繁误报,最后把告警关了,反而没人看数据。我的习惯是先让指标在面板上裸奔一周,记录一下正常波动范围,再根据P95或者P99设定阈值,这样报警才有意义。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 13:22:34

40节AI商业课+20本PDF手册:GPT/Midjourney/SD实战+数字人/短视频/ PPT

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 13:18:27

社区商铺管理系统:Spring Boot+Vue+MySQL全栈实战解析

社区商铺管理系统&#xff0c;这类毕设题目每年都能见到不少&#xff0c;但真正做扎实的其实不多。我前后参与评审和带过几个类似的毕业设计项目&#xff0c;这套“社区商铺管理系统”算是比较典型的一套完整方案&#xff0c;后端是Spring Boot&#xff0c;前端是Vue加Element …

作者头像 李华
网站建设 2026/10/19 13:15:04

JSP逆向思维实战:从注册功能拆解Web请求链路与设计边界

1. 从"注册"这个动作说起&#xff1a;为什么逆向思维值得聊做Web开发的人对注册功能再熟悉不过了。一个表单&#xff0c;几个输入框&#xff0c;提交按钮&#xff0c;后端写库&#xff0c;完事。但如果你把视角倒过来——不是站在"我要实现注册功能"的角度…

作者头像 李华
网站建设 2026/10/19 13:15:00

PostgreSQL恢复监控实战:SQL洞察WAL回放与主备延迟

如果你管过 PostgreSQL 的主从架构&#xff0c;大概率经历过这种时刻&#xff1a;备库恢复突然变慢、主备延迟飙高、或者备库干脆卡住不动&#xff0c;而日志里只剩下一条孤零零的 restartpoint complete 。真正让人头疼的是——你没法从日志里看清"现在到底恢复到哪里了…

作者头像 李华
网站建设 2026/10/19 13:14:41

开源代码审查 CLI 工具:基于 Git Hook 与本地 LLM 的增量式审查实践

1. 项目概述&#xff1a;一个真正能落地的开源代码审查 CLI 工具“open-code-review”这个名字乍看平平无奇&#xff0c;但拆开来看——open不是指“开源”这个老生常谈的标签&#xff0c;而是指开放上下文、开放权限边界、开放模型接入能力&#xff1b;code review也不是简单地…

作者头像 李华