记录一次Feign调用500错误

前言

最近某个应用在服务间使用Feign时一直报500 status reading,严重影响到公司业务进行。

报错如下:

分析思路

找到问题原因

首先跟踪了feign源码发现报错是在response中返回的,然后查看被调用方无任何日志信息。此时判断非业务异常返回。

再确认非业务异常后,通过tcpdump将tcp信息导出得到以下信息

拿到报文后,显示自己调用该接口来测试是否报错,但无论如何都调用成功,在经过一段时间折腾后发现host与tcp的请求地址不一致,为什么会出现这种情况?

http是应用层的东西只是规定了一种协议,而真正发起连接是tcp,而tcp只是发数据根本不知道host是啥。也就意味着host可以随意变更。

发现host不一致时,尝试着更改host,最终尝试更改host也并不会引发报错。所以到现在只能为最老实的方式 → 复制所有的header和参数与报文保持一致。此时调用报错且与tcpdump结果一致。然后通过删减header最终确认了是Content-Length和content-length同时存在导致tomcat服务器直接拦截了请求。

此时思路就来,那么在何时产生的两个名称相同但大小写不同的key存在。接下来就跟随Feign源码查看问题。通过调试进入了feign初始化request的代码SynchronousMethodHandler

public Object invoke(Object[] argv) throws Throwable {    // 通过feignclient获取,如果为post请求会添加Content-Length头    RequestTemplate template = buildTemplateFromArgs.create(argv);    Retryer retryer = this.retryer.clone();    while (true) {      try {        return executeAndDecode(template);      } catch (RetryableException e) {        retryer.continueOrPropagate(e);        if (logLevel != Logger.Level.NONE) {          logger.logRetry(metadata.configKey(), logLevel);        }        continue;      }    }  }
Object executeAndDecode(RequestTemplate template) throws Throwable {// 调用所有请求拦截器获取request    Request request = targetRequest(template);    if (logLevel != Logger.Level.NONE) {      logger.logRequest(metadata.configKey(), logLevel, request);    }    Response response;    long start = System.nanoTime();    try {      response = client.execute(request, options);      // ensure the request is set. TODO: remove in Feign 10      response.toBuilder().request(request).build();    } catch (IOException e) {      if (logLevel != Logger.Level.NONE) {        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));      }      throw errorExecuting(request, e);    }

调用所有请求拦截器获取request

Request targetRequest(RequestTemplate template) {    for (RequestInterceptor interceptor : requestInterceptors) {      interceptor.apply(template);    }    return target.apply(new RequestTemplate(template));  }

接下来看RequestInterceptor 的实现类有哪些,最终找到同事自己写的一个方法拦截器,如下

public class FeignInterceptor implements RequestInterceptor {    @Override    public void apply(RequestTemplate requestTemplate) {        final ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();        if (attrs != null) {            final HttpServletRequest request = attrs.getRequest();            final Enumeration<String> headerNames = request.getHeaderNames();            if (headerNames != null) {                //遍历请求头里面的属性字段,将logId和token添加到新的请求头中转发到下游服务                while (headerNames.hasMoreElements()) {                    final String name = headerNames.nextElement();                    final String value = request.getHeader(name);                    requestTemplate.header(name, value);                }            }        } else {        }    }}

通过以上代码,估计就很快发现了问题,这里将本次请求的所有header都放进了feign调用的header里面。此时通过断点发现本次请求的请求头全是小写的content-length。此时就更加确信是这个问题导致。但这个拦截器我在其他服务上也看到了,但为什么偏偏只有这个服务出错了呢。所以为了最终到本质,继续往下看源码

经过一系列调试最终跟踪到了Okhttp(同事将原本默认的feign调用的httpclient换成了okhttp)

public feign.Response execute(feign.Request input, feign.Request.Options options)      throws IOException {    okhttp3.OkHttpClient requestScoped;    if (delegate.connectTimeoutMillis() != options.connectTimeoutMillis()        || delegate.readTimeoutMillis() != options.readTimeoutMillis()        || delegate.followRedirects() != options.isFollowRedirects()) {      requestScoped = delegate.newBuilder()          .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)          .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)          .followRedirects(options.isFollowRedirects())          .build();    } else {      requestScoped = delegate;    }    // 转换feign request为okhttp request。    Request request = toOkHttpRequest(input);    Response response = requestScoped.newCall(request).execute();    return toFeignResponse(response, input).toBuilder().request(input).build();  }
static Request toOkHttpRequest(feign.Request input) {    Request.Builder requestBuilder = new Request.Builder();    requestBuilder.url(input.url());    MediaType mediaType = null;    boolean hasAcceptHeader = false;    for (String field : input.headers().keySet()) {      if (field.equalsIgnoreCase("Accept")) {        hasAcceptHeader = true;      }      for (String value : input.headers().get(field)) {// 将所有头放进header中,跟踪到里面发现使用list保存        requestBuilder.addHeader(field, value);        if (field.equalsIgnoreCase("Content-Type")) {          mediaType = MediaType.parse(value);          if (input.charset() != null) {            mediaType.charset(input.charset());          }        }      }    }    // Some servers choke on the default accept string.    if (!hasAcceptHeader) {      requestBuilder.addHeader("Accept", "*/*");    }    byte[] inputBody = input.body();    boolean isMethodWithBody =        HttpMethod.POST == input.httpMethod() || HttpMethod.PUT == input.httpMethod()            || HttpMethod.PATCH == input.httpMethod();    if (isMethodWithBody) {      requestBuilder.removeHeader("Content-Type");      if (inputBody == null) {        // write an empty BODY to conform with okhttp 2.4.0+        // http://johnfeng.github.io/blog/2015/06/30/okhttp-updates-post-wouldnt-be-allowed-to-have-null-body/        inputBody = new byte[0];      }    }    RequestBody body = inputBody != null ? RequestBody.create(mediaType, inputBody) : null;    requestBuilder.method(input.httpMethod().name(), body);    return requestBuilder.build();  }

此时还有一个以为为何原来的不报错呢?同样跟踪下原来的代码,最终跟踪到Client#``convertAndSend

for (String field : request.headers().keySet()) {        if (field.equalsIgnoreCase("Accept")) {          hasAcceptHeader = true;        }        for (String value : request.headers().get(field)) {          if (field.equals(CONTENT_LENGTH)) {            if (!gzipEncodedRequest && !deflateEncodedRequest) {              contentLength = Integer.valueOf(value);              connection.addRequestProperty(field, value);            }          } else {// 这里面进行了一些受限header的屏蔽            connection.addRequestProperty(field, value);          }        }      }
public synchronized void addRequestProperty(String var1 海南红色教育培训 www.fjganxun.cn , String var2) {        if (!this.connected && !this.connecting) {            if (var1 == null) {                throw new NullPointerException("key is null");            } else {                // 判断是否允许的外部扩展头                if (this.isExternalMessageHeaderAllowed(var1, var2)) {                    this.requests.add(var1, var2);                    if (!var1.equalsIgnoreCase("Content-Type")) {                        this.userHeaders.add(var1, var2);                    }                }            }        } else {            throw new IllegalStateException("Already connected");        }    }
private static final String[] restrictedHeaders = new String[]{"Access-Control-Request-Headers", "Access-Control-Request-Method", "Connection", "Content-Length", "Content-Transfer-Encoding", "Host", "Keep-Alive", "Origin", "Trailer", "Transfer-Encoding", "Upgrade", "Via"};private boolean isRestrictedHeader(String var1, String var2) {        if (allowRestrictedHeaders) {            return false;        } else {            var1 = var1.toLowerCase();             // 包含了content-length                    if (restrictedHeaderSet.contains(var1)) {                return !var1.equals("connection") || !var2.equalsIgnoreCase("close");            } else {                return var1.startsWith("sec-");            }        }    }

至此所有疑团消失。完结撒花!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot Feign是一个基于RestTemplate的声明式Web Service客户端,它简化了微服务之间的远程调用Feign通过注解方式定义接口,内部封装了负载均衡和服务熔断的功能,提供了一种更简洁、更方便的方式进行服务之间的通信。 熔断机制是一种保护机制,用于防止由于服务不可用或无法正常响应而导致的服务雪崩效应。假设某个微服务在高峰期间不可用,当其他微服务不断向该服务发送请求时,由于无法得到正常的响应,会占用大量的线程和资源,最终导致整个系统崩溃。为了解决这个问题,可以使用熔断器。 熔断器是Feign中的一种机制,用于处理服务调用失败或超时的情况。当服务调用超时或出现错误时,熔断器会暂时中断对该服务的调用,并返回给客户端一个预先定义的默认值或错误信息,而不是一直等待响应。这样可以保护系统不受故障服务的影响,提高系统的可用性和稳定性。 在Spring Boot中使用Feign进行服务调用熔断的具体步骤如下: 1. 在使用Feign的微服务项目中引入相应的依赖: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> ``` 2. 创建Feign接口,并使用@FeignClient注解指定要调用的微服务的名称: ```java @FeignClient(value = "service-name", fallback = ServiceFallback.class) public interface ServiceClient { @GetMapping("/api/path") String invokeService(); } ``` 3. 实现熔断的回退逻辑,创建一个Fallback类,实现Feign接口并定义相应的错误处理方法: ```java @Component public class ServiceFallback implements ServiceClient { @Override public String invokeService() { return "Fallback message"; } } ``` 4. 在应用主类上添加@EnableFeignClients注解启用Feign和熔断功能: ```java @SpringBootApplication @EnableFeignClients public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 通过以上步骤,当微服务调用超时或出现错误时,Feign会自动调用相应的熔断器回退逻辑并返回默认值,确保系统的稳定性。可以根据实际情况自定义熔断处理逻辑,比如记录日志、发送警报等操作,以便及时处理故障。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值