Оптимизация spring-mvc
Общие решения всегда медленнее частных. Ниже я собираюсь немного оптимизировать spring-mvc. Оптимизация прежде всего рассчитана на уменьшение генерируемого мусора. Прежде чем начать оптимизировать надо определиться какие функции фреймворка можно выкинуть и какими фичами пренебречь:
- ISO-8859-1-encoded URLs. Человеко-понятные-урл (ЧПУ) используются SEO продвижения в поисковых движках. Но что если это не нужно? Зачем на каждый запрос тратить процессорное время и память?
- Всегда абсолютные пути для сервлетов-контроллёров. По умолчанию spring-mvc позволяет использовать относительные пути для include запросов. При оптимизации выполненной ниже и использовании Jetty результат такой же. Возможно это актуально для других контейнеров.
- Не использовать jstl. Достаточно спорное предположение, однако кто-то может не использовать jstl и писать на обычных JSP. Я не знаю jstl. И пишу <% %>.
Итак, первая достаточно безболезненная оптимизация не требующая никаких жертв: выключить publishEvent в DispatcherServlet. По умолчанию он отправляет в ApplicationContext сообщение о времени обработки запроса. В production зачастую уже поздно что-то мерить. Делается это в web.xml:
<init-param>
<param-name>publishEvents</param-name>
<param-value>false</param-value>
</init-param>
Избавиться от @RequestMapping. Это очень удобно передавать @Param напрямую в метод. Однако реализация AnnotationMethodHandlerAdapter в spring-mvc достаточно требовательна к ресурсам и генерирует кучу мусора на каждый запрос. Логичнее было бы сделать найденные методы кешируемыми, но согласно https://jira.springsource.org/browse/SPR-6151 разработчики считают сложным исправить. Поэтому для простоты и небольшого увеличения скорости сделаем новый контроллёр:
public interface FastController {
String handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
String getRequestMappingURL();
}
Чем он лучше, чем org.springframework.web.servlet.mvc.Controller? Он позволяет задавать url в том же месте где и содержится его реализация. Не нужно делать лишних движений, чтобы добавить его в spring.xml. Соответственно необходимо определить классы, которые будут его использовать:
public class FastUrlDetector extends AbstractDetectingUrlHandlerMapping {
public FastUrlDetector() {
setAlwaysUseFullPath(true);
setUrlDecode(false);
}
@Override
protected String[] determineUrlsForHandler(String beanName) {
ApplicationContext context = getApplicationContext();
Class<?> handlerType = context.getType(beanName);
if (FastController.class.isAssignableFrom(handlerType)) {
FastController controller = (FastController) context.getBean(beanName);
String result = controller.getRequestMappingURL();
if (result == null) {
throw new IllegalArgumentException("controller doesnt have url mapping: " + beanName);
}
if( result.isEmpty() ) {
throw new IllegalArgumentException("controller doesnt have url mapping: " + beanName);
}
if( !result.startsWith("/") ) {
throw new IllegalArgumentException("only absolute urls are required. Beanname: " + beanName + " Url: " + result);
}
return new String[]{result};
}
return null;
}
}
public class FastMethodHandlerAdapter implements HandlerAdapter {
public boolean supports(Object handler) {
return (handler instanceof FastController);
}
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return new ModelAndView(((FastController) handler).handleRequest(request, response));
}
public long getLastModified(HttpServletRequest request, Object handler) {
if (handler instanceof LastModified) {
return ((LastModified) handler).getLastModified(request);
}
return -1;
}
}
Они практически не генерируют мусора. Убрать new ModelAndView не получится не переписав DispatcherServlet. Тем более генерация ModelAndView занимает небольшой процент мусора генерируемого при каждом запросе. После этого необходимо добавить Adapter и Decoder в spring.xml чтобы они автоматически подцеплялись DispatcherServlet при поиске контроллёров.
<bean class="FastMethodHandlerAdapter"/>
<bean class="FastUrlDetector" />
Далее. Следующим большим местом которое генерирует много мусора является Renderer. Я не знаю как работает jstl и почему spring-mvc делает множество приседаний для его работы. Поэтому я просто выкинул JstlView (которое используется по умолчанию для .jsp) и заменил его на:
public class FastJSPView extends AbstractUrlBasedView {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
RequestDispatcher rd = request.getRequestDispatcher(getUrl());
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.include(request, response);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.forward(request, response);
}
}
protected boolean useInclude(HttpServletRequest request, HttpServletResponse response) {
return (WebUtils.isIncludeRequest(request) || response.isCommitted());
}
}
public class FastJSPViewResolver extends UrlBasedViewResolver {
public FastJSPViewResolver() {
setViewClass(FastJSPView.class);
}
}
Часть кода в FastJSPView скопирована с JstlView. И соответственно необходимо добавить в spring.xml:
<bean id="viewResolver"
class="com.st.FastJSPViewResolver">
<property name="prefix">
<value>/WEB-INF/pages/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
Чтобы проверить что есть некоторые улучшения ниже приведён тестовый контроллер который перенаправляет запрос в jsp:
@Controller
public class FastServlet implements FastController {
public String handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
return "index2";
}
public String getRequestMappingURL() {
return "/test2";
}
}
Аннотация @Controller используется для автоматического поиска контроллёра в classpath при старте приложения. В результате под нагрузкой jmeter (50 пользователей) получаются следующие показатели:
- Настройки GC по умолчанию. Оптимизированная версия ~3 коллекции в секунду, неоптимизированная ~6 коллекций
- Latency & throughput одинаковые для обоих версий.
- Время работы HandlerAdapter.handle (от общего времени обработки запроса): для оптимизированной версии 0%, не оптимизированной 31%. Результат впечатляющий. Очевидно это связано с тем что вызов метода напрямую быстрее поиска метода по аннотации и вызова через Reflection
- Время работы для DispatcherServlet.getLastModified: для оптимизированной версии 0%, неоптимизированной 11%. Связано с тем что AbstractHandlerMapping.getHandler использует абсолютные пути и не использует DefaultAnnotationHandlerMapping.
- Среднее количество генерируемых объектов в минуту: для оптимизированной версии 4к-5к, неоптимизированной 9к-14к. Уменьшение в 2 раза!
Дополнительные находки:
- ServletRequestAttributes. Не очень удачная абстракция. На каждый запрос создаётся этот объект. Не совсем понятно зачем он нужен когда обычный HTTPServletRequest предоставляет методы setAttribute и getAttribute и пр.
- Не очень удачная реализация некоторых объектов в Jetty: Response.setLocale, Request.getRequestDispatcher, Dispatcher.forward. После оптимизации они стали теми местами которые генерируют наибольшее количество мусора. Не совсем понятно зачем им генерировать много объектов, также непонятно почему они не кешируют результаты вычислений.
- при использовании for each генерируется итератор, который превращается в мусор. Настольные microbenchmark’и показали что итерация по ArrayList при использовании простых индексов быстрее в два раза.
Выводы:
- чем больше слоёв абстракции и “упрощений”, тем медленнее обработка.
- текущие технологии есть куда оптимизировать.
- нужно хорошо понимать что можно оптимизировать а что нет