表现:接口偶然失败
某天我正开心的在GitHub上乱晃悠,测试妹子就丢了个Bug过来。
“这个接口请求偶尔会失败。”附了一张截图。
“OK,交给我”。看了下接口,里面就是个列表查询,顺手开了PostMan对着Idea开始怼之后就暂时没管了,心说过半小时再说。半个小时之后,发现并没有什么异常。
咨询测试妹子后得知重现步骤大概如下:查询列表,点击某条数据,弹出详情页之后然后关掉详情页返回列表页,此时Bug有几率重现。
排查过程
搜集日志
开了Debug之后准备进行重现,大概重复了几次操作之后确实发现存在请求失败情况,而且观察到一条很奇怪的日志:
Hystrix circuit short-circuited and is OPEN
熟悉的朋友都知道这是Hystrix接口开启熔断的日志。说起来这个接口确实存在RPC调用,不过奇怪的是另一个服务并没有出现异常,这就有点见鬼了。更见鬼的是我重新对接口进行了暴力测试,单独测试接口并不会出现异常。
思考
既然出现了熔断,说明RPC调用确实出错了。偶尔出现的情况也符合熔断的规则。既然我单独测试列表接口并不会触发,那么就说明其实真正的出错的位置并不在这里。那么会不会是详情接口调用了相同的接口进而导致熔断呢?
于是我盯上了查询详情接口,查看代码后发现两个接口确实都调用了同一个接口,大致长这样:
@GetMapping("getById")
XXX getById(@RequestParam("id") String id);
按道理这样简单的查询不应该出错才对。捞了一批数据Debug之后发现了问题,当传递的id为空的情况下确实会触发参数异常进而导致熔断。
Why?
平常都话我们如果这么请求:getById?id=,那么其实是不会报错的。怎么到了Feign这里就走不通了呢?
继续Debug,跟踪到ReflectiveFeign这个类的时候发现了端倪。
核心代码如下:
@Override
public RequestTemplate create(Object[] argv) {
RequestTemplate mutable = RequestTemplate.from(metadata.template());
mutable.feignTarget(target);
if (metadata.urlIndex() != null) {
int urlIndex = metadata.urlIndex();
checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
mutable.target(String.valueOf(argv[urlIndex]));
}
Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
int i = entry.getKey();
Object value = argv[entry.getKey()];
if (value != null) { // Null values are skipped.
if (indexToExpander.containsKey(i)) {
value = expandElements(indexToExpander.get(i), value);
}
for (String name : entry.getValue()) {
varBuilder.put(name, value);
}
}
}
RequestTemplate template = resolve(argv, mutable, varBuilder);
if (metadata.queryMapIndex() != null) {
// add query map parameters after initial resolve so that they take
// precedence over any predefined values
Object value = argv[metadata.queryMapIndex()];
Map<String, Object> queryMap = toQueryMap(value);
template = addQueryMapQueryParameters(queryMap, template);
}
if (metadata.headerMapIndex() != null) {
template =
addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
}
return template;
}
可以看到,此方法是用来创建代理的。Null values are skipped.这句注释也说明当参数为空时Feign就不会传输此参数了。
到了我们这个接口就要命了,如果传递Id为空,那么其实是可以正常处理的。可是不传递参数就会直接报错了。
解决方式
考虑到线上数据确实可能存在异常,因此修改接口如下:
@GetMapping("getById")
XXX getById(@RequestParam(value = "id",required = false) String id);
在具体实现前添加断言,问题解决。
总结
这下整个流程就比较清楚了。首先是参数估计传的null 导致参数异常,达到了Hystrix阈值之后直接触发熔断,熔断时间过后服务恢复,可以正常查询。之所以单独测试列表接口无法重现,是因此此接口对熔断的RPC接口强依赖,详情页接口则是弱依赖,并且触发点并不是列表接口。