一个线上服务偶尔卡顿,分析发现是loadClass导致的线程阻塞,而loadClass的原因与Feign配置有关。
现象
最近线上的一个服务偶尔出现卡顿,表现为不特定时刻出现几分钟的异常,在这段时间内响应时间激增,如图:
分析
首先查看日志,找到慢请求,发现在服务间Feign调用后会出现一段时间的间隔。对Feign的Logger和HttpMessageConverterExtractor
开启DEBUG日志,看到在Feign输出HTTP返回数据后到Jackson反序列化之间有几秒的间隔。
1 | 2019-12-20 10:25:55.493|*,*,*|DEBUG|feign.slf4j.Slf4jLogger:(72)|[ServiceName#methodName] <--- END HTTP (1571-byte body) |
这点很奇怪,在GC日志中也没有Stop-The-World出现。用jstack打印堆栈,发现有几个和Feign相关的线程处于BLOCKED状态:
1 | "http-nio-8080-exec-276" #3021 daemon prio=5 os_prio=0 tid=0x00007fbd501a8800 nid=0xdea waiting for monitor entry [0x00007fbcbd76d000] |
查看registerWellKnownModulesIfAvailable
处的代码,可以看到其逻辑为若classpath中有JodaTime的LocalDate
,则加载Jackson对应的JodaModule
(这个项目中没有引用)。
1 | if (ClassUtils.isPresent("org.joda.time.LocalDate", this.moduleClassLoader)) { |
LaunchedURLClassLoader.loadClass
将调用ClassLoader.loadClass
来加载类,加载时需要获取锁,因此在并发环境下,可能导致线程BLOCKED状态。
1 | protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
依据以上信息,出现卡顿的流程大致如下:
- Feign请求时会初始化
MappingJackson2HttpMessageConverter
时尝试加载JodaModule
。 - 而这个类并不在classpath中,因此无法在
findLoadedClass
中找到,每次都需要重新加载。 - 执行
loadClass
时需要加锁,在线上高并发场景下会导致线程BLOCKED状态。
解决方式一:避免ClassLoader反复加载
可以看出卡顿的直接原因是反复尝试加载不在classpath中的JodaModule
,因此将这个依赖添加到工程中。加载一次后,再次调用可以通过findLoadedClass
获得,减少加载类导致的资源消耗,从而减少BLOCKED的出现。
1 | <dependency> |
解决方式二:避免HttpMessageConverters重复初始化
但是还有另一个问题需要考虑:为什么每次请求都会初始化MappingJackson2HttpMessageConverter
?查看SpringDecoder
代码,可以看到每次反序列化response时会调用ObjectFactory<HttpMessageConverters>
来获取converters。
1 |
|
而在FeignConfig中配置的这个ObjectFactory的实现是new一个HttpMessageConverters
对象。
1 |
|
HttpMessageConverters
的构造方法会默认执行getDefaultConverters
。其逻辑可查看WebMvcConfigurationSupport
代码,其中AllEncompassingFormHttpMessageConverter
的构造函数会创建MappingJackson2HttpMessageConverter
对象。
1 | public HttpMessageConverters(HttpMessageConverter<?>... additionalConverters) { |
这就是每一个请求都会初始化MappingJackson2HttpMessageConverter
并触发loadClass
的原因,因此每一个Feign请求的开销都很大。由于我们只需要使用自定义的MappingJackson2HttpMessageConverter
来执行反序列化,可以想办法避免执行getDefaultConverters
:
第一种方法是指定HttpMessageConverters
的构造方法参数addDefaultConverters
为false:
1 | ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(false, Collections.singletonList(jacksonConverter)); |
第二种方法则是使用Feign的JacksonDecoder
:
1 |
|
1 | <dependency> |