この記事の概要
- SpringBootを使用したWebアプリケーションを運用していた
- JVMメモリを見てみると単調増加している。。。
- 調査・対応してみたのでそのときのメモ
環境
- Java8
- spring-boot-starter-web:2.1.1
- spring-boot-starter-thymeleaf:2.1.1
調査
- まずはどんなオブジェクトが増え続けているのか調べる
- アプリケーションはkubernetesで管理されている
- 権限管理の問題でpod内には入れない → jmapでheapdumpが取れない
そこでmanagementエンドポイントを使うことにした
management:
endpoints:
web:
exposure:
include: "heapdump"
base-path: "/"
server:
port: 9990
上記のようにinclude
にheapdump
を記載することで以下のように実行するとheampdumpが取得できる!
curl localhost:9990/heapdump -o heap.dump
あとはEclipseのMemoryAnalyzerで調査!
使い方や調査結果は省略させていただきます
原因
- MemoryAnalyzerで調査したところリダイレクト時にURLをデフォルトでキャッシュしている処理があり、キャッシュが溢れているようだった
- こんなのがダメ
リダイレクトサンプル
@Controller
public class Sample {
@GetMapping("/sample")
public String sample() {
return "redirect:/hoge/" + UUID.randomUUID().toString();
}
}
-
org.thymeleaf.spring5.view.ThymeleafViewResolver.createView
が犯人
抜粋
// Process redirects (HTTP redirects)
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length(), viewName.length());
final RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
}
- リダイレクトURLごとにBean生成しているのね。。。
対応
- ThymeleafViewResolverを継承して以下のようなクラスを作った
import java.util.Locale;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.InternalResourceView;
import org.springframework.web.servlet.view.RedirectView;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
@Slf4j
public class ThymeleafViewResolverWrapper extends ThymeleafViewResolver {
public static final String REDIRECT_URL_PREFIX = "redirect:";
@Override
protected View createView(final String viewName, final Locale locale) throws Exception {
// First possible call to check "viewNames": before processing redirects and forwards
if (!getAlwaysProcessRedirectAndForward() && !canHandle(viewName, locale)) {
log.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
return null;
}
// Process redirects (HTTP redirects)
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
log.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
return new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
}
// Process forwards (to JSP resources)
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
// The "forward:" prefix will actually create a Servlet/JSP view, and that's precisely its aim per the Spring
// documentation. See http://docs.spring.io/spring-framework/docs/4.2.4.RELEASE/spring-framework-reference/html/mvc.html#mvc-redirecting-forward-prefix
log.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
final String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
return new InternalResourceView(forwardUrl);
}
// Second possible call to check "viewNames": after processing redirects and forwards
if (getAlwaysProcessRedirectAndForward() && !canHandle(viewName, locale)) {
log.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
return null;
}
log.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a " +
"{} instance will be created for it", viewName, getViewClass().getSimpleName());
return loadView(viewName, locale);
}
}
- Bean化しているところを消しただけ
- 解決!!!