Handlebars GuavaTemplateCache Full GC
UI 리뉴얼 시점부터 몇몇 서버들에서 memory leak 발생하였고 이로 인해 Full GC 발생.
Heap dump 떠서 확인해보니 GuavaTemplateCache 에 cache 가 2G 를 차지하고 있었음
하지만 로컬에서 동일한 조건에서 테스트를 해보았는데 NullTemplateCache 를 타고 GuavaTemplateCache 는 타지 않음
spring boot 의 AutoConfiguration 에 의해서 GuavaTemplateCache 빈이 생성되기는 하나 설정 값에 의해서 NullTemplateCache 가 사용됨.
아래와 같은 의문들이 생겼다.
의문1) Local 은 NullTemplateCache 가 사용되는데 운영에서는 GuavaTemplateCache 가 사용되는 이유?
의문2) 한달 이상 문제 없이 동작하던 서버가 배포도 없었는데 메모리릭이 발생한 이유?
의문3) 캐시가 객체가 계속 쌓이는게 문제라면 캐시가 만료되지 않는 이유?
의문4) 일부 서버에서는 문제가 발생하고 일부 서버에서는 문제가 발생하지 않는 이유?
의문5) UI 리뉴얼을 오픈하면서 문제가 생긴 이유?
의문1) Local 은 NullTemplateCache 가 사용되는데 운영에서는 GuavaTemplateCache 가 사용되는 이유?
application-local.yml 에서 handlebars.cache : false 로 설정하고 있다. 따라서 Local 에서는 AutoConfiguration 으로 생성되는 GuavaTemplateCache 를 사용하지 않고
NullTemplateCache 를 사용하였고 운영에서는 handlebars.cache :true 이므로 기본으로 생성되는 빈인 GuavaTemplateCache 를 사용하였다.
handlebars:
cache: false
prettyPrint: false
의문2) 한달 이상 문제 없이 동작하던 서버가 배포도 없었는데 문제가 발생한 이유?
메모리가 쌓이기 시작한 시점에 UI 리뉴얼 릴리즈가 전체 유저 대상으로 진행되었다. 따라서 새로운 Template 이 각 도메인 서버로 전달되기 시작.
새로운 handlebars template 이 전달되면서 GuavaTemplateCache 에 만료되지 않는 Template cache 들이 저장되면서 메모리가 점점 부족해짐
의문3) 캐시가 만료되지 않은 이유?
AutoConfiguration 에 의해서 TemplateCache 가 적용이 되면 아래와 같은 설정에 의해서 만료 제한이 없는 캐시가 사용된다.
/**
* Constructs a new {@code CacheBuilder} instance with default settings, including strong keys,
* strong values, and no automatic eviction of any kind.
*/
public static CacheBuilder<Object, Object> newBuilder() {
return new CacheBuilder<Object, Object>();
}
spring 개발자들이 automatic eviction 을 적용하지 않은 것을 기본 설정으로 사용한 이유는 Template Cache 이기 때문에 사용자에 혹은 조건에 따라 Template 은 변경되지 않을 것이라고 생각한 것 같다.
의문4) 일부 서버에서는 문제가 발생하지 않는 이유?
Spring Boot 가 아닌 Legacy Framework 로 개발된 서버들의 경우에는 template cache 의 max capacity 와 ttl timedout 이 정의 되어있다. 따라서 새로운 Template 이 적용된 시점에 마찬가지로 캐시가 쌓이기 시작했지만 max capacity 만큼만 늘어났을 것이다.
의문5) UI 리뉴얼 을 오픈하면서 문제가 생긴 이유?
UI 리뉴얼 이 오픈되면서 새로운 template 이 각 도메인 서버로 전달이 되기 시작했다. GuavaTemplateCache 에 cache 가 계속 쌓이는것이 문제인데 로컬에서 재현이 되었다.
로컬에서 로그인한 유저를 계속 바꿔가면서 서버의 특정 페이지에 접근했을떄 GuavaTemplateCache 의 cache 상태이다.
StringTemplateSource 타입의 캐시가 접근하는 유저 수 만큼 계속 늘어나는 것을 확인할 수 있다.
URLTemplateSource 는 늘어나지 않는데 StringTemplateSource 만 늘어나는 이유는 key 로 사용되는 filename 의 차이에서 온다.
cache hit 여부를 판단하기 위해 사용하는 key 는 TemplateSource 의 filename 필드이다. URLTemplateSource 의 filename 은 실제 뷰 파일의 path 가 저장된다.
하지만 StringTemplateSource 는 아래 로직을 사용한다.
public Template compileInline(final String input, final String startDelimiter,
final String endDelimiter) throws IOException {
notNull(input, "The input is required.");
String filename = "inline@" + Integer.toHexString(Math.abs(input.hashCode()));
return compile(new StringTemplateSource(filename, input),
startDelimiter, endDelimiter);
}
때문에 로그인한 사용자마다 서로 다른 filename 이 만들어졌고 이로 인해 만료 조건이 없는 캐시가 무한 증식 되었다. String 타입의 input 의 hasCode 로 filename 을 만든다. 따라서 String 값이 변경될때마다 다른 hasCode 가 만들어지면서 다른 code 값을 가지게 된다.
숏텀으로 cache max capacity 를 사용하여 Full GC 가 발생하지 않도록 대응하였으나 Long Term 으로는 TemplateCache 는 말그대로 Template 을 위한 컴포넌트이므로 사용자에 따라 다른 Template 이 리턴되는걸 방지하고
다른 값이 입력되어야 하는 부분은 Template 화 하여 같은 템플릿은 cache 에서 hit 될 수 있도록 개선이 필요하다.
문제 없는 StringTemplateSource 의 일부분
"data": {
"userId": "{{default user.id}}",
"userName": "{{default user.userName}}",
},
문제가 발생한 StringTemplateSource 의 일부분
newFeaturedChildMenus: [],
userId: 'goodsimple',
userName: 'GoodSimple',
};
문제가 있는 Template 은 유저별로 데이터가 data 가 정해진 채로 Template 이 사용된것에 비해 문제가 없는 Template 은 사용자별로 달라지는 데이터가 Mustache 로 감싸져서 모든유저에게 동일한 Template 이 사용되었을 것이다.
따라서 모든 유저가 동일한 cache 가 hit 되어 캐시가 계속 쌓이는 문제가 없었다.