问题 1
使用 Feign 调用服务,当触发熔断机制时,遇到了以下问题:
异常形如:TestService#addRecord(ParamVO) failed and no fallback available.;
获取不到服务提供方抛出的原始异常信息;
实现某些业务方法不进入熔断,直接往外抛出异常;
接下来将一一解决上述问题。
failed and no fallback available.这种异常信息,是因为项目开启了熔断:
feign.hystrix.enabled: true
复制代码
当调用服务时抛出了异常,却没有定义 fallback 方法,就会抛出上述异常。由此引出了第一个解决方式。当调用服务时抛出了异常,却没有定义 fallback 方法,就会抛出上述异常。由此引出了第一个解决方式。
@FeignClient(name = "serviceId", fallbackFactory = TestServiceFallback.class)
public interface TestService {
@RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
Result get(@PathVariable("id") Integer id);
}
复制代码
在@FeignClient
注解中指定fallbackFactory
,上面例子中是TestServiceFallback
:
import feign.hystrix.FallbackFactory;
import org.apache.commons.lang3.StringUtils;
@Component
public class TestServiceFallback implements FallbackFactory<TestService> {
private static final Logger LOG = LoggerFactory.getLogger(TestServiceFallback.class);
public static final String ERR_MSG = "Test接口暂时不可用: ";
@Override
public TestService create(Throwable throwable) {
String msg = throwable == null ? "" : throwable.getMessage();
if (!StringUtils.isEmpty(msg)) {
LOG.error(msg);
}
return new TestService() {
@Override
public String get(Integer id) {
return ResultBuilder.unsuccess(ERR_MSG + msg);
}
};
}
}
复制代码
通过实现FallbackFactory
,可以在create
方法中获取到服务抛出的异常。但是请注意,这里的异常是被Feign
封装过的异常,不能直接在异常信息中看出原始方法抛出的异常。这时得到的异常信息形如:
status 500 reading TestService#addRecord(ParamVO); content:
{"success":false,"resultCode":null,"message":"/ by zero","model":null,"models":[]
,"pageInfo":null,"timelineInfo":null,"extra":null,"validationMessages":null,"valid":false
}
复制代码
说明一下,本例子中,服务提供者的接口返回信息会统一封装在自定义类Result
中,内容就是上述的content
:
{"success":false,"resultCode":null,"message":"/ by zero","model":null,"models":[],"pageInfo":null,"timelineInfo":null,"extra":null,"validationMessages":null,"valid":false}
复制代码
因此,异常信息我希望是message
的内容:/ by zero
,这样打日志时能够方便识别异常。
保留原始异常信息
当调用服务时,如果服务返回的状态码不是 200,就会进入到Feign
的ErrorDecoder
中,因此如果我们要解析异常信息,就要重写ErrorDecoder
:
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
/**
* @Author: CipherCui
* @Description: 保留 feign 服务异常信息
* @Date: Created in 1:29 2018/6/2
*/
public class KeepErrMsgConfiguration {
@Bean
public ErrorDecoder errorDecoder() {
return new UserErrorDecoder();
}
/**
* 自定义错误
*/
public class UserErrorDecoder implements ErrorDecoder {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public Exception decode(String methodKey, Response response) {
Exception exception = null;
try {
// 获取原始的返回内容
String json = Util.toString(response.body().asReader());
exception = new RuntimeException(json);
// 将返回内容反序列化为Result,这里应根据自身项目作修改
Result result = JsonMapper.nonEmptyMapper().fromJson(json, Result.class);
// 业务异常抛出简单的 RuntimeException,保留原来错误信息
if (!result.isSuccess()) {
exception = new RuntimeException(result.getMessage());
}
} catch (IOException ex) {
logger.error(ex.getMessage(), ex);
}
return exception;
}
}
}
复制代码
上面是一个例子,原理是根据response.body()
反序列化为自定义的Result
类,提取出里面的message
信息,然后抛出RuntimeException
,这样当进入到熔断方法中时,获取到的异常就是我们处理过的RuntimeException
。
注意上面的例子并不是通用的,但原理是相通的,大家要结合自身的项目作相应的修改,要使上面代码
发挥作用,还需要在@FeignClient
注解中指定configuration
:
@FeignClient(name = "serviceId", fallbackFactory = TestServiceFallback.class,
configuration = {KeepErrMsgConfiguration.class})
public interface TestService {
@RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
String get(@PathVariable("id") Integer id);
}
复制代码
不进入熔断,直接抛出异常
有时我们并不希望方法进入熔断逻辑,只是把异常原样往外抛。这种情况我们只需要捉住两个点:不进入熔断、原样。
原样就是获取原始的异常,上面已经介绍过了,而不进入熔断,需要把异常封装成HystrixBadRequestException
,对于HystrixBadRequestException
,Feign
会直接抛出,不进入熔断方法。
因此我们只需要在上述KeepErrMsgConfiguration
的基础上作一点修改即可:
public class NotBreakerConfiguration {
@Bean
public ErrorDecoder errorDecoder() {
return new UserErrorDecoder();
}
/**
* 自定义错误
*/
public class UserErrorDecoder implements ErrorDecoder {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public Exception decode(String methodKey, Response response) {
Exception exception = null;
try {
String json = Util.toString(response.body().asReader());
exception = new RuntimeException(json);
Result result = JsonMapper.nonEmptyMapper().fromJson(json, Result.class);
// 业务异常包装成 HystrixBadRequestException,不进入熔断逻辑
if (!result.isSuccess()) {
exception = new HystrixBadRequestException(result.getMessage());
}
} catch (IOException ex) {
logger.error(ex.getMessage(), ex);
}
return exception;
}
}
}
复制代码
总结
为了更好的达到熔断效果,我们应该为每个接口指定fallback
方法。而根据自身的业务特点,可以灵活的配置上述的KeepErrMsgConfiguration
和NotBreakerConfiguration
,或自己编写Configuration
。
问题 2
com.netflix.hystrix.exception.HystrixRuntimeException:XXXXXXXXX
could not be queued for execution and no fallback available.
复制代码
隔离服务的线程池的堆积队列满了
说明:
1、由于线程池的最大数量导致的,官方说随着线程池的数量越大,资源开销也就越大,所以调整时要慎重。
2、Hystrix 默认是 10 个线程,超过就会报这个异常。
hystrix:
threadpool:
default:
coreSize: 200 ##并发执行的最大线程数,默认10
maxQueueSize: 200 ##BlockingQueue的最大队列数
queueSizeRejectionThreshold: 50 ##即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝
default:
execution:
timeout:
enabled: true
isolation:
strategy: THREAD
semaphore:
maxConcurrentRequests: 1000
thread:
timeoutInMilliseconds: 30000
复制代码
com.netflix.hystrix.exception.HystrixRuntimeException:XXXXXXXX
timed-out and no fallback available.
复制代码
超时,并且没有 fallback 方法
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
默认 1000ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000,可以修改大点,如果不确定超时时间,或者可以关闭,都不超时了
hystrix.command.default.execution.timeout.enabled=false
abstract class HystrixThreadPoolProperties {
static int default_coreSize = 10;
static int default_maximumSize = 10;
static int default_keepAliveTimeMinutes = 1;
static int default_maxQueueSize = -1;
static boolean default_allow_maximum_size_to_diverge_from_core_size = false;
static int default_queueSizeRejectionThreshold = 5;
static int default_threadPoolRollingNumberStatisticalWindow = 10000;
static int default_threadPoolRollingNumberStatisticalWindowBuckets = 10;
}
复制代码
Feign 自定义 Configuration 和 root 容器有效隔离:
Spring Cloud Netflix 提供了默认的 Bean 类型:
Decoder feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder)
Encoder feignEncoder: SpringEncoder
Logger feignLogger: Slf4jLogger
Contract feignContract: SpringMvcContract
Feign.Builder feignBuilder: HystrixFeign.Builder
Spring Cloud Netflix 没有默认值,可以在 feign 上下文配置:
自定义 feign 的消息编码:
不要在如下代码中 getObject 方法内 new 对象,外部会频繁调用 getObject 方法。
ObjectFactory<HttpMessageConverters> messageConvertersObjectFactory = new ObjectFactory<HttpMessageConverters>() {
@Override
public HttpMessageConverters getObject() throws BeansException {
return httpMessageConverters;
}};
复制代码
Feign 配置
#Hystrix支持,如果为true,hystrix库必须在classpath中
feign.hystrix.enabled=false
#请求和响应GZIP压缩支持
feign.compression.request.enabled=true
feign.compression.response.enabled=true
#支持压缩的mime types
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
# 日志支持
logging.level.project.user.UserClient: DEBUG
复制代码
Logger.Level 支持
必须为每一个 Feign Client 配置来告诉 Feign 如何输出日志,可选:
NONE, No logging (DEFAULT).
BASIC, Log only the request method and URL and the response status code and execution time.
HEADERS, Log the basic information along with request and response headers.
FULL, Log the headers, body, and metadata for both requests and responses.
FeignClient.fallback 正确的使用方法
配置的 fallback class 也必须在 FeignClient Configuration 中实例化,否则会报
java.lang.IllegalStateException: No fallback instance of type class
异常。
例子:
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
public interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}
public class HystrixClientFallback implements HystrixClient {
@Override
public Hello iFailSometimes() {
return new Hello("fallback");
}
}
@Configuration
public class FooConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
@Bean
public HystrixClientFallback fb(){
return new HystrixClientFallback();
}
}
复制代码
使用 Feign Client 和 @RequestMapping 时,注意事项
当前工程中有和 Feign Client 中一样的 Endpoint 时,Feign Client 的类上不能用 @RequestMapping 注解否则,当前工程该 endpoint http 请求且使用 accpet 时会报 404.
有一个 Controller
@RestController
@RequestMapping("/v1/card")
public class IndexApi {
@PostMapping("balance")
@ResponseBody
public Info index() {
Info.Builder builder = new Info.Builder();
builder.withDetail("x", 2);
builder.withDetail("y", 2);
return builder.build();
}
}
复制代码
有一个 Feign Client
@FeignClient(
name = "card",
url = "http://localhost:7913",
fallback = CardFeignClientFallback.class,
configuration = FeignClientConfiguration.class
)
@RequestMapping(value = "/v1/card")
public interface CardFeignClient {
@RequestMapping(value = "/balance", method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
Info info();
}
复制代码
if @RequestMapping is used on class, when invoke http /v1/card/balance, like this :
如果 @RequestMapping 注解被用在 FeignClient 类上,当像如下代码请求/v1/card/balance 时,注意有 Accept header:
Content-Type:application/json
Accept:application/json
POST http://localhost:7913/v1/card/balance
复制代码
那么会返回 404。
如果不包含 Accept header 时请求,则是 OK:
Content-Type:application/json
POST http://localhost:7913/v1/card/balance
复制代码
或者像下面不在 Feign Client 上使用 @RequestMapping 注解,请求也是 ok,无论是否包含 Accept:
@FeignClient(
name = "card",
url = "http://localhost:7913",
fallback = CardFeignClientFallback.class,
configuration = FeignClientConfiguration.class
)
public interface CardFeignClient {
@RequestMapping(value = "/v1/card/balance", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
Info info();
}
复制代码
https://github.com/Netflix/Hystrix/issues/1428
https://github.com/Netflix/Hystrix/wiki/Configuration#allowmaximumsizetodivergefromcoresize
评论