原创文章,如需转载,请注明来自:https://bigzuo.github.io/
关键词:GC Overhead Limit Reached、CPU burst、Spring boot metrics
公司在基于 Spring Cloud 框架的微服务改造过程中,遇到如下问题:当把之前稳定运行在 Spring 3 框架上的业务代码迁移到 Spring Cloud 框架上之后,系统在运行几天后,CPU 的使用率会逐渐增加, GC 频率也会越来越快,最后 CPU 使用率接近100%,GC 每秒钟至少回收超过1G 的新生代对象,系统响应也变的非常慢。
系统异常时 CPU 的使用率:
如下是对应的 GC 的回收日志:
1 | 2018-11-06T13:08:07.050+0800: 651403.763: [GC (Allocation Failure) [ParNew: 629120K->69888K(629120K), 0.2756745 secs] 1597577K->1071507K(2027264K), 0.2760656 secs][Times: user=0.42 sys=0.04, real=0.27 secs] |
我们首先打印了系统的线程堆栈,但是并未发现任何异常。后来我们直接关掉了所有的访问流量,但是系统的异常情况依然持续。由于系统本身没有异步任务,所以基本排除是业务代码的问题,并且这些代码也在 Spring 3 框架上正常运行了超过了半年时间。
用 Java VisualVM 分析系统的 Jvm 内存 dump 信息如下:
我们发现内存中数量最多的对象是AtomicLong
,实例数量超过150万,达到堆空间中实例总数的24.4%,占了10.6%的空间。
继续往下追踪,发现这些对象关联的 GC Root 都是是org.springframework.boot.actuate.autoconfigure.MetricsFilter
对象,而这个对象的功能是 Spring boot actuator 组件中用于收集 Web 请求产生的 Metric 统计数据,如HTTP 状态码、执行时间等。
现在基本确认,系统的异常应该和 Actuator 组件收集 Metric 统计数据有关。但是,在关掉系统所有流量后,异常状态依然持续,这又是为什么呢?
我们在 Spring Cloud Dalston.SR5 版本的官方文档中发现如下内容:
Without any additional dependencies or configuration, a Spring Cloud based service will autoconfigure a Servo
MonitorRegistry
and begin collecting metrics on every Spring MVC request.By default, a Servo timer with the namerest
will be recorded for each MVC request which is tagged with:
HTTP method
HTTP status (e.g. 200, 400, 500)
URI (or “root” if the URI is empty), sanitized for Atlas
The exception class name, if the request handler threw an exception
The caller, if a request header with a key matching
netflix.metrics.rest.callerHeader
is set on the request. There is no default key fornetflix.metrics.rest.callerHeader
. You must add it to your application properties if you wish to collect caller information.… …
Avoid using hardcoded url parameters within
RestTemplate
. When targeting dynamic endpoints use URL variables. This will avoid potential “GC Overhead Limit Reached” issues whereServoMonitorCache
treats each url as a unique key.
1 | // recommended |
上述内容大意是:在没有增加特殊的依赖或者配置的情况下,Spring Cloud 应用汇自动配置一个Servo MonitorRegistry
用来收集每次 Spring MVC 请求的 Metric 信息,如 HTTP 方法名、状态码、URL 名称等信息。不要在 RestTemplate
Api 中对 Url 的参数进行硬编码,否则可能会引起 “GC Overhead Limit Reached” 异常,因为ServoMonitorCache
会认为每个带参数的 Url 都是一个独立的 key,所以 Url 中的参数有多少个不同的值,就会产生多少个 key,进而导致内存使用大量增加,触发 GC 异常。而每个 key 的 value 都记录着这个 URL 的Metric 数据,所以 key 只要产生就会一直存在,因此就算完全没有请求进来,Actuator 组件依然会每次都遍历所有的 key,统计对应 key 的 Metric 信息,故而 “GC Overhead Limit Reached” 异常会一直存在。
根据这个分析,我们在应用代码中发现如下代码片段:
1 | long timestamp = System.currentTimeMillis(); |
如果是这个原因引起的异常,那应用的表现应该是刚开始运行正常,随着这段代码的执行, GC 回收应该会越来越频繁,CPU 的使用率也会越来越高,基本应该和时间增加成正相关。查询了这个应用最近一段时间的 CPU 使用率情况变化,发现和猜测完全吻合:
问题定位后,我们通过优化应用代码解决了问题。当然,也可以通过关闭如下开关,避免收集 Spring Cloud 应用 Web 请求相关的 Metric 数据,也可以解决问题:
1 | spring.autoconfigure.exclude = org.springframework.boot.actuate.autoconfigure.MetricFilterAutoConfiguration |
或:
1 | spring.metrics.servo.enabled=false |
参考资料
Log warning when ServoMonitorCache reaches certain configurable size
Decouple MetricsFilter from Spring MVC
ServoMonitorCache and GC Overhead Limit Reached: Added documentation/heads up accordingly