欢迎关注公众号:冬瓜白
相关文章:
- 多模式 Web 应用开发记录<一>背景&全局变量优化
- 多模式 Web 应用开发记录<二>自己动手写一个 Struts
- 多模式 Web 应用开发记录<三>预初始化属性
经过一段时间的开发和测试,我们成功地将项目从 “Spring + FreeMarker + Struts2 + Resin” 迁移到了 “Spring Boot + Spring Web MVC + Embedded Tomcat + FreeMarker”。但是在上线后发现了一个问题:视图首次渲染的速度非常慢,甚至可以达到数十秒。
使用 Trace 工具,发现大部分时间都耗费在了 Render 操作上。在 FreeMarker 中,Render 是指将数据模型和模板文件结合起来,生成最终的输出结果的过程。
这种时候使用 Arthas Trace 要找出造成渲染慢的底层原因还是相当困难的,但是可以使用 Async-profiler 采样第一次请求的火焰图,但是这里有个细节,在进行火焰图采样之前,可以先请求其他的非涉及 FreeMarker 渲染的接口,避免受到 Spring MVC 底层资源初始化的影响。
我选择了一个相对“干净”的 FreeMarker 渲染接口进行采样,这个接口没有复杂的业务逻辑,仅仅是渲染一个视图。采样结果显示,大量的 CPU 时间都花费在了 org.apache.catalina.webresources.JarWarResourceSet.getArchiveEntries
上。这可能是因为 Tomcat 在处理 FreeMarker 视图时需要加载大量的资源,这些资源主要包括 FreeMarker 模板和静态资源。
针对这个问题,有三种可能的优化策略:
- 减少资源数量:可以尝试减少 FreeMarker 模板和静态资源的数量,但是这种方法并不现实。考虑到我们的项目背景,我们并不想在前端处理上花费太多的精力。
- 预加载资源:可以在应用启动时预加载一些资源。
- 使用 JAR 文件:一般来说,从 JAR 文件加载资源通常比从 WAR 文件加载资源更快。
由于服务已经使用了 JAR 文件,所以我决定采用“资源预加载”的策略进行优化。
那么,如何进行资源预加载呢?我的思路是:提前渲染那些需要渲染的 FreeMarker 视图。换句话说,提前执行那些执行速度较慢的操作。这种思路其实在很多框架的预处理和预请求优化中都有应用。可以使用 Spring 提供的 MockHttpServletRequest
来实现这个目标。
首先定义一个注解,用来标注需要提前预加载的视图:
/**
* @author dongguabai
* @date 2024-04-16 21:38
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreloadOnStartup {
}
在 Spring Boot 启动后异步并发预加载:
/**
* @author dongguabai
* @date 2024-04-17 10:31
*/
@Component
public class Preloader implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(Preloader.class);
private final ApplicationContext applicationContext;
private final RequestMappingHandlerAdapter handlerAdapter;
private final RequestMappingHandlerMapping handlerMapping;
private final ViewResolver viewResolver;
private static final int QUEUE_SIZE = 200;
public Preloader(ApplicationContext applicationContext,
RequestMappingHandlerAdapter handlerAdapter,
RequestMappingHandlerMapping handlerMapping,
ViewResolver viewResolver) {
this.applicationContext = applicationContext;
this.handlerAdapter = handlerAdapter;
this.handlerMapping = handlerMapping;
this.viewResolver = viewResolver;
}
@Override
public void run(ApplicationArguments args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
try {
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
RequestMappingInfo mappingInfo = entry.getKey();
HandlerMethod handlerMethod = entry.getValue();
Method method = handlerMethod.getMethod();
if (method.isAnnotationPresent(PreloadOnStartup.class)) {
Set<RequestMethod> methods = mappingInfo.getMethodsCondition().getMethods();
Set<String> patterns = mappingInfo.getPatternsCondition().getPatterns();
RequestMethod requestMethod = methods.isEmpty() ? RequestMethod.POST : methods.iterator().next();
for (String pattern : patterns) {
executorService.execute(() -> {
try {
MockHttpServletRequest request = new MockHttpServletRequest(requestMethod.name(), pattern);
Parameter[] methodParams = method.getParameters();
for (Parameter param : methodParams) {
Class<?> type = param.getType();
Object value = null;
if (!type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
value = type.getConstructor().newInstance();
}
request.addParameter(param.getName(), value != null ? value.toString() : null);
}
MockHttpServletResponse response = new MockHttpServletResponse();
ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
RequestContextHolder.setRequestAttributes(attributes);
HandlerExecutionChain handlerExecutionChain = handlerMapping.getHandler(request);
Object handler = handlerExecutionChain.getHandler();
ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
String viewName = modelAndView.getViewName();
View view = viewResolver.resolveViewName(viewName, Locale.getDefault());
view.render(modelAndView.getModel(), request, response);
log.info("Successfully loaded method: {}", method.getName());
} catch (Exception e) {
log.error("Failed to load method: {}", method.getName(), e);
} finally {
RequestContextHolder.resetRequestAttributes();
}
});
}
}
}
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
log.warn("Preloader did not finish within 60 seconds");
}
} catch (InterruptedException e) {
log.error("Preloader was interrupted", e);
Thread.currentThread().interrupt();
}
} finally {
executorService.shutdownNow();
}
}
}
看下效果对比:
优化前 | 优化后 |
---|---|
4s+ | 0.2s+ |
总结
本文探讨了一个在项目迁移过程中遇到的性能问题:FreeMarker 视图首次渲染速度慢。
使用了 Trace 分析和 Async-profiler 工具定位到问题后,然后提出了三种可能的优化策略。最终选择了预加载资源的策略,并使用 Spring 提供的 MockHttpServletRequest
来实现。优化结果非常显著,首次渲染的时间从4秒以上降低到了0.2秒以上。