整体复盘:

一个不算普通的周五中午,同事收到了大量了cpu异常的报警。根据报警表现和通过arthas查看,很明显的问题就是内存不足,疯狂无效gc。而且结合arthas和gc日志查看,老年代打满了,gc不了一点。既然问题是内存问题,那么老样子,通过jmap和heap dump 文件分析。

不感兴趣的可以直接看结论

  1. 通过jmap命令查看的类似下图,并没有项目中明显的自定义类,而占空间最大的又是char数组,当时线上占900M左右,整个老年代也就1.8个G;此时dump文件同事还在下载,网速较慢。

  2. 通过业务日志查看,很多restTempalte请求报错,根据报错信息可知是某xx认证过期了,导致接收到回调,业务处理时调接口报错了;查询数据库,大概有20多万回调。根据过期时间和内存监控,大概能对的上号,表明内存异常和这个认证过期有关。怀疑度最高的只有回调以及回调补偿任务,但是一行一行代码看过去,并不觉得有什么异常。

下载完dump文件后,先重启了服务器,避免影响业务,然后着手分析文件。


  1. 在dump文件下载完之后,使用jvisualvm分析,最多的char里大部分都是一些请求的路径,如“example/test/1",”“example/test/2"之类的,都是接口统一,但是参数不一样,因为是GET请求,所以实际路径都不一样。Jvisualvm点击gc_root又一直计算不出来,在等待计算的过程中,一度走了弯路

于是又现下载jprofiler,通过jprofiler的聚类,确定了一定是这个Meter导致的,而通过JProfile的分析,终于定位到是

org.springframework.boot.actuate.metrics.web.client.MetricsClientHttpRequestInterceptor#intercept这个类。然后发现,MetricsClientHttpRequestInterceptor 持有一个meterRegistry,里面核心是个map,所以是map没有清除。根据依赖分析,发现是有次需求引入了redisson-spring-boot-starter,而redisson依赖了spring-boot-starter-actuator,这东西默认启动了,会拦截所有的RestTempalte请求,然后记录一些指标。





所以问题变成了,为什么map没有清掉已经执行完的请求?

我之前并没有研究过spring的actuator,只是看过skywalking的流程,所以我以为也和skywalking一样,记录然后上报,上报之后删除本地的。所以当时怀疑,难道是和我们请求都异常了有关,但是正如下面的代码,无论是否异常,都是执行finnally,所以又不太可能。

meterRegistry点击查看代码
ClientHttpResponse response = null;
try {
response = execution.execute(request, body);
return response;
}
finally {
try {
getTimeBuilder(request, response).register(this.meterRegistry).record(System.nanoTime() - startTime,
TimeUnit.NANOSECONDS);
}
catch (Exception ex) {
logger.info("Failed to record metrics.", ex);
}
if (urlTemplate.get().isEmpty()) {
urlTemplate.remove();
}
}

而在我自己尝试复现之后,micrometer的指标根本不会被自动清除,生命周期和应用的生命周期一样。因为并不存在上报,数据全部在内存(虽然可以导出到数据库,但并没有深入研究)。其实也合理,因为如果要通过Grafana等可视化平台查看的时候,我们也希望查看任意时刻的监控。不过看代码应该是有留一些手动删除的,应该是页面操作之类的才会触发。

结论

所以到此为止,可以定结论,那就是因为引入了redisson-spring-boot-starter,导致不知情引入了spring-boot-starter-actuator。

因此默认开启了http.client.request指标的监控,关于http.client.request,有一个属性是maxUriTags,默认值是100,其作用是限制meterMap里uri的个数。但是maxUriTags起作用的地方MeterFilter没有生效。

由于maxUriTags没有生效,导致监控信息里的uri因为业务大量的GET请求中存在唯一id,本身就很占内存。压死内存的最后稻草是认证过期和补偿任务。补偿任务为保证及时性一直在频繁执行,而接口的uri里两个变量(token和uniId)导致meterMap里的key不重复,一直在插入,20万回调,token两小时更新一次,持续了两天,最终产生了124万条字符串,被map持有,无法回收。

解决方案

  1. 不需要监控

    直接排除掉spring-boot-starter-actuator
  2. 需要监控但不需要http.client.request指标
    management:
    metrics:
    web:
    client:
    request:
    autotime:
    enabled: false
  3. 需要http.client.request指标

    jar包升到2.5.1或以上
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-actuator-autoconfigure</artifactId>
    <version>2.5.1</version>
    </dependency>

复现:

新建测试项目

相关代码和配置如下

点击查看代码
@SpringBootApplication
@Slf4j
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Application.class);
RestTemplate bean = run.getBean(RestTemplate.class);
for (int i = 0; i < 300000; i++) {
try {
String forObject = bean.getForObject("http://localhost:9999/first/echo?i="+i, String.class);
}catch (Exception e){
log.error("执行"+i+"次");
}
}
}
} @Configuration
public class RestTemplateTestConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder){
return builder.build();
}
} <dependencies>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies> server:
port: 8080
spring:
redis:
host: ************
password: **********
#management:
# endpoints:
# web:
# exposure:
# include: "metrics"
# metrics:
# web:
# client:
# request:
# autotime:
# enabled: false

启动项目通过jconsole查看整个堆的监控和老年代监控分别如下,可以看出老年代一直在增长,并不会回收,



甚至手动触发GC,老年代也回收不了

[Full GC (System.gc()) [Tenured: 195217K->195457K(204800K), 0.3975261 secs] 233021K->195457K(296960K), [Metaspace: 30823K->30823K(33152K)], 0.3976223 secs] [Times: user=0.39 sys=0.00, real=0.40 secs]

通过jprofiler确定主要是meterMap占据内存了,最多的都是字符串。

分析

actuator导致rest启动了metrics记录

在使用RestTemplateBuilder构建RestTemplate的时候,会触发懒加载的RestTemplateAutoConfiguration里的RestTemplateBuilderConfigurer,在此期间,config中会注入RestTempalteCustomizer类型的bean。

而项目中引用了redisson-spring-boot-starter,从依赖分析可以看出间接引用了actuator相关的包。

这导致会在RestTemplateMetricsConfiguration配置类中实例化一个叫做MetricsRestTemplateCustomizer的bean,这个bean会通过上面的restTepalteBuilderConfigurer.configure方法给restTemplate添加拦截器MetricsClientHttpRequestInterceptor。

拦截器的intercept方法会在finnally中最终记录此次请求的一些指标

io.micrometer.core.instrument.Timer.Builder#register->
io.micrometer.core.instrument.MeterRegistry#time->
io.micrometer.core.instrument.MeterRegistry#registerMeterIfNecessary->
io.micrometer.core.instrument.MeterRegistry#getOrCreateMeter{
meterMap.put(mappedId, m);
}

最终存到了是SimpleMeterRegistry这个bean的meterMap中去,这个bean也是actuator-autoconfigure自动注入的

但是到目前为止,只是启动了metrics记录,假如maxUriTags有效的话,会在超过100条记录后getOrCreateMeter方法里的accept这里过滤掉,并不会走到下面的meterMap.put(mappedId, m)

为什么maxUriTags没有生效?

maxUriTags只在下图这个位置使用了,作用是构建了一个MeterFilter,根据debug我们可以确定bean是产生了的

但是在accept这里打上断点,再触发一些请求可以发现,代码并不会走到这里

往上跟,没有走到这里的情况只能是filters里没有这个MeterFilter,但我们刚才又确定metricsHttpCLientUriTagFilter这个bean是产生了的,那么就只能是没有添加到filters,也就是没有调用过meterFilter

从meterFilter往上只有可能是addFilters,一层一层往上最终到了MeterRegistryPostProcessor#postProcessAfterInitialization这个方法





我们上面说过负责记录的bean叫做simpleMeterRegistry,但是我们在这里打上条件断点发现并没有走到这里

找到SimpleMeterRegistry和MeterRegistryPostProcessor这两个bean注入的地方打断点观察,都产生了,且MeterRegistryPostProcessor比SimpleMeterRegistry产生的要早



理论上没问题,但现在确实没走到,所以只能在SimpleMeterRegistry产生的时候在org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization打断点,然后可以发现,在simpleMeterRegistry实例化快结束的时候,调用后处理器时this.beanPostProcessors确实没有MeterRegistryPostProcessor



一般来说,postPorcessor的bean注入是在refresh方法的registerBeanPostProcessors中,是早于普通bean的实例化

所以simpleMeterRegistry实例化的时候没有MeterRegistryPostProcessor是不合理的情况,定位simpleMeterRegistry是何时实例化的成了关键问题

simpleMeterRegistry的实例化时机

在new SimpleMeterRegistry这里打上断点观察堆栈发现,simpleMeterRegistry是MetricsRepositoryMethodInvocationListener的参数,MetricsRepositoryMethodInvocationListener则是metricsRepositoryMethodInvocationListenerBeanPostProcessor的参数

所以是在实例化metricsRepositoryMethodInvocationListenerBeanPostProcessor这个处理器的时候,因为依赖导致先实例化了simpleMeterRegistry这个bean依赖







导致实例化了SimpleMeterRegistry,而这个时候由于没有注册,所以SimpleMeterRegistry在执行applyBeanPostProcessorsAfterInitialization时就执行不到meterRegistryPostProcessor了

spring已经修复了这个问题,spring-boot-actuator-autoconfigure版本大于2.5.0的都已经没有问题了。解决方案

2.5.1 版本中,添加了一个这个ObjectProvider,在源头上不会立即把依赖的bean初始化完

2.5.0 版本

public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (Optional.class == descriptor.getDependencyType()) {
return createOptionalDependency(descriptor, requestingBeanName);
}
//由于使用了ObjectProvider,所以这里只是返回了一个DependencyObjectProvider
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
}
else {
//2.5.0版本中会在这个方法加载入参依赖的bean
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
if (result == null) {
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
return result;
}
}

线上gc问题-SpringActuator的坑的更多相关文章

  1. [转]线上GC故障解决过程记录

    排查了三四个小时,终于解决了这个GC问题,记录解决过程于此,希望对大家有所帮助.本文假定读者已具备基本的GC常识和JVM调优知识,关于JVM调优工具使用可以查看我在同一分类下的另一篇文章: http: ...

  2. 一次线上GC故障解决过程记录

    排查了三四个小时,终于解决了这个GC问题,记录解决过程于此,希望对大家有所帮助.本文假定读者已具备基本的GC常识和JVM调优知识,关于JVM调优工具使用可以查看我在同一分类下的另一篇文章: http: ...

  3. 记一次线上gc调优的过程

           近期公司运营同学经常表示线上我们一个后台管理系统运行特别慢,而且经常出现504超时的情况.对于这种情况我们本能的认为可能是代码有性能问题,可能有死循环或者是数据库调用次数过多导致接口运行 ...

  4. 记一次线上Kafka消息堆积踩坑总结

    2018年05月31日 13:26:59 xiaoguozi0218 阅读数:2018更多 个人分类: 大数据   年后上线的系统,与其他业务系统的通信方式采用了第三代消息系统中间件Kafka.由于是 ...

  5. 记一次线上FGC问题排查

    引言 本文记录一次线上 GC 问题的排查过程与思路,希望对各位读者有所帮助.过程中也走了一些弯路,现在有时间沉淀下来思考并总结出来分享给大家,希望对大家今后排查线上 GC 问题有帮助. 背景 服务新功 ...

  6. JAVA 线上故障排查套路,从 CPU、磁盘、内存、网络到GC 一条龙!

    线上故障主要会包括cpu.磁盘.内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍. 同时例如jstack.jmap等工具也是不囿于一个方面的问题的, ...

  7. 线上定位GC内存泄露问题

    原因:Java中存在内存泄露,就是因为对象无用却可达. 举个例子: 在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个Vector中,如果我们仅仅释放引用本身,那么Vector仍然引 ...

  8. 关于GC(上):Apache的POI组件导致线上频繁FullGC问题排查及处理全过程

    某线上应用在进行查询结果导出Excel时,大概率出现持续的FullGC.解决这个问题时,记录了一下整个的流程,也可以作为一般性的FullGC问题排查指导. 1. 生成dump文件 为了定位FullGC ...

  9. 线上问题排查,一不小心踩到阿里的 arthas坑了

    最近帮新来的校招同学排查一个线上问题,问题本身不是很难,但是过程中踩到了一个arthas的坑,挺有意思的. 同时,也分享下在排查过程中使用的一些比较实用的工具,包括tcpdump.arthas.sim ...

  10. 线上排查:内存异常使用导致full gc频繁

    线上排查:内存异常使用导致full gc频繁 问题系统 日常巡检发现,应用线上出现频繁full gc 现象 应用线上出现频繁full gc 排查过程 分析dump 拉dump文件:小插曲:dump时如 ...

随机推荐

  1. 【OpenGL ES】凸镜贴图

    1 前言 ​ 正方形图片贴到圆形上 中将正方形图片上的纹理映射到圆形模型上,同理,也可以将圆形上的纹理映射到凸镜的球形曲面上.如下图,最左边的竖条是原图片的截面(纹理坐标),最右边的竖条是变换后的顶点 ...

  2. git 添加子模块

    参考:https://www.jianshu.com/p/10ae453701ed 问题:如果一个子模块的分支不是最新的该怎么处理? 方法:在主仓库内使用 cd 命令切换到子模块的仓库,使用 git ...

  3. win32-使用GDI+缩放图像

    滑动鼠标滚轮可以改变图像大小 #include <windows.h> #include <tchar.h> #include <Urlmon.h> // URLD ...

  4. 【Android逆向】破解看雪test3.apk方案二

    方案二就是要hook那三个条件,不让追加字符串变成false v20 = "REAL"; clazz = _JNIEnv::FindClass(env, "android ...

  5. 项目实战:C#上位机+arduino下位机+控制点亮LED灯

    前言   当前比较流行的arduino开发,联动做一个Demo.   应用构架   上位机:C#上位机通过串口发送接收控制协议,来控制下位机:  下位机:arduino下位机主控,接受上位机串口协议控 ...

  6. CoaXPress 协议的CRC及其具体实现

    CoaXPress CRC 在CXP协议中,CRC用在stream packet和control packet中,用于指示数据是否错误,如果是control packet, device发现CRC错误 ...

  7. 【Azure App Service】App Service设置访问限制后,使用git clone代码库出现403报错

    问题描述 在App Service中,为App Service配置了访问限制,结果导致在克隆App Service的代码时候,遇见403错误. 问题解答 因为在使用 git clone App Ser ...

  8. 聊聊微信小程序的流式(stream)响应请求

    场景:类似ChatGPT的逐字显示效果. 流程:服务端我用Python的flask框架(向外提供API接口)实现,服务部署在replit上,Python调用azure 的chatgpt服务(需要申请) ...

  9. Java // 使用二维数组打印 10 行杨辉三角

    1 // 使用二维数组打印 10 行杨辉三角 2 public static void main(String[] args) 3 { 4 //1.声明 并初始化二维数组 5 int[][]yangh ...

  10. Java与sql中的字符串表示

    在 Java 中,双引号 "" 用于表示字符串字面量,而单引号 '' 用于表示字符字面量.这意味着在 Java 中,您可以使用双引号来包围包含任意数量字符的字符串,包括零个字符(空 ...