1. pinpoint
基于Google的Dapper,Pinpoint可以追踪到一个事务(transaction)中的分布式请求,即指Pinpoint能够追踪到从应用A到应用B的一次分布式请求中的所有数据。形成一个调用链路,可根据调用链路进行分析。
2. 问题描述
在排查慢接口的过程中,突然发现一个服务的中出现一个自调用的链路,如图
相当于服务A通过fegin接口又调用了服务A,这个操作就很迷,自己原有的接口为啥还要舍近求远绕一步操作呢?
3. 排查过程
3.1 首先排查了本项目的使用的fegin接口,并无发现使用本服务的接口调用。那就奇怪了,这个接口是从哪里凭空出现的呢。
3.2 既然本项目中没搜到,那很可能是调用了依赖jar包中被封装后的fegin SDK包,过程不在细说,结果在依赖包中虽然有该服务的fegin接口,但并未被代码层面使用。
经过收集更多的信息发现这个自调用出现的频率很低,一般只有2次,偶尔会有4次,6次。测试、uat、生产环境均有此问题,只不过是出现的时间点没有规律,生产出现后在一段时间内都不会有调用了。从调用时间及频率上无法得到更多的信息,只能继续沿着jar包中的fegin接口逐个排查。
3.3 在排查过程中突然发现有一个接口在一个model 的静态方法中通过SpringContextHolder getBean的方式调用了服务A的接口。让我们一起回顾下知识点
- 凡是声明为static的语句(变量,方法,代码块),是在加载类的时候就执行的。在这个类第一次被调用或实例化的时候就会被执行。
静态代码块只会执行一次,一般会用来初始化一些值,并且在所有对象中全局共享。 - 由于spring bean初始化的机制,在静态类方法中是不能直接调用bean的方法的,但可以通过SpringContextHolder 静态持有SpringContext的引用来实现,见 参考1
这段代码如下
@Data
@JsonInclude(value= JsonInclude.Include.NON_NULL)
public class UnsalableAreaModel implements Serializable {
public static UnsalableAreaModel ALL = new UnsalableAreaModel("ALL","全部",UnsalableAreaModel.TENANT);
public static String TENANT="TENANT";
private String code;
private String name;
private String type;
private List child;
public UnsalableAreaModel(){
}
public UnsalableAreaModel(String code, String name, String type){
this.code=code;
this.name=name;
this.type=type;
this.child = UnsalableAreaModelInner.parseChild(code);
}
UnsalableAreaModelInner.parseChild方法代码如下
public static List parseChild(String code){
List<UnsalableAreaModelInner> re = new ArrayList<>();
if(StringUtils.isNotBlank(code)){
XXClient XXFeignClient = SpringContextHolder.getBean(XXClient.class);
DataResponseEntity res = XXFeignClient.getDictList(code);
JSONArray j = JSON.parseArray(JSON.toJSONString(res.getData()));
//逻辑忽略
}
return xx;
}
3.4 根据这段代码我们重新整理下调用链
- UnsalableAreaModelInner 在静态方法中使用SpringContextHolder 调用了服务的接口
- UnsalableAreaModel在有参构造函数中调用了UnsalableAreaModelInner.parseChild,也就是说当 UnsalableAreaModel被创建对象实例化的时候就会触发调用该方法
- 根据上面两个线索,锁定了如下代码。在某个接口中使用了UnsalableAreaModel的静态成员变量。
UnsalableAreaModel.AREA
- 将UnsalableAreaModel.class类反编译后得到代码,至于为什么在编译后将原本的静态成员赋值拆分成了静态块,请听下回分解。
public static UnsalableAreaModel ALL;
//我是忽略符//
static {
ALL = new UnsalableAreaModel("ALL", "全部", TENANT);
}
3.5 将逻辑拆分,发版后问题解决。
4. 总结
很多技术问题都来源于编码不规范、设计环节缺失导致,在排查过程中需要静下心来,逐项分析和排查。对于出现的偶发“灵异”事件,也只是超出了我们的技术能力和认知而已。