Поздравьте меня, друзья! Сегодня с моих плеч свалилась гора, которая плющила меня на протяжении целого месяца: отвлекала от продуктивной общественно-полезной деятельности и постоянно держала в напряжении, не давая расслабиться. Имя этой «горы» на техническом жаргоне — утечка памяти. Нормальные люди могут на этом месте прервать чтение и разойтись по своим делам; всех прочих, интересующихся веб-программированием, приглашаю читать дальше.

Сайт Английский без дураков сделан на Java. Хотя большинство вебмастеров сочтут это полным вздором, Java — практически идеальный инструмент для создания вебсайтов: намного более удобный и не в пример более мощный, чем их любимый ПХП. Единственная причина, по которой никто об этом не догадывается, заключается в том, что джавистов и вообще не так уж много, а джавистов, которые не зашорены стереотипами корпоративного веб-программизма (коими буквально насквозь пронизана официальная жабная веб-парадигма) — тех можно просто пересчитать по пальцам. Так вот, я — один из этих немногих.

Несколько слов о внутреннем устройстве сайта. Движок сайта сделан на базе фреймворка под названием Breeze, который мы с моим другом tux'ом сваяли три года назад, и в котором были воплощены самые передовые веб-программистские идеи того времени (взгляд на вебсайт как приложение, а не свалку JSP; отказ от JSP в пользу шаблонных движков; пересмотренная модель MVC; минимум конфигурации за счет удобных конвенций в стиле RoR; понятие контекста приложения и контекста потока; аспектные штучки типа интерцепторов, и многое другое). Придет день, и Бриз еще прогремит на весь вебный мир, ну а пока мы потихоньку используем его для собственных нужд, и не можем нарадоваться.

За время более чем двухлетней эксплуатации в различных проектах фреймворк показал себя с самой лучшей стороны как платформа чрезвычайно удобная, гибкая, мощная, легкая в развитии и весьма устойчивая в работе. Тем страннее было обнаружить, что с переводом «Английского без дураков» на Бриз сайт стал время от времени вылетать по OutOfMemoryError. Проблема усугублялась тем, что прогон приложения под профайлером никакой утечки памяти не обнаруживал! Более того, будучи жестко нагруженным лоуд-тестером, сервер стоял, как вкопанный, спокойно выдерживая сеансы из многих тысяч обращений. И тем не менее, практически каждый день сервер приходилось перезапускать.

Сказать, что такая ситуация отравляла мне жизнь — это, знаете ли, будет слишком мягко. Временами, когда даун случался особенно некстати, я готов был просто изрубить сервер в куски (хотя при чем тут, собственно, сервер… благо, что он был далеко). Но поделать ничего не мог: трудно поймать черную кошку в темной комнате. Особенно если. В том, что «кошки» там нет, меня убеждали результаты стресс-тестов. Однако факты — вещь упрямая, и если логи говорят тебе, что сервер вылетел по OutOfMemoryError, это перечеркивает любые логические построения в пользу невозможности подобного инцидента.

Разумеется, я пытался ковыряться в дампах памяти. Увы, ничего особенно подозрительного не находил: обычные рабочие ошметки трудовой програмы. Неизвестно, сколько бы еще продолжались эти блуждания впотьмах, если бы на днях, в очередной раз роя интернет в поисках ключика, я не наткнулся на малоизвестную программу Memory Analyzer. Без особой надежды на успех качнул, установил, запустил, открыл дамп и — о, чудо! Вся карта залежей памяти вдруг открылась передо мной, как на ладони!


В две секунды стало ясно, что треды Томката не отпускают переменную типа ThreadLocal, в которой хранится контекст потока, и хотя объем инфы в каждом контексте не так уж велик, с ростом числа задействованных потоков в сумме накапливается достаточно, чтобы связать критическую массу памяти.

Это было настоящее озарение, инсайт! В свете вновь открывшегося знания пофиксить проблему заняло ровно одну строчку программы:

public class BreezeController implements Controller {
   ...
   public void service(HttpServletRequest req, HttpServletResponse resp, ServletContext sctx) {
       ...
       try {
           ...
       }
       finally {
           ...<br />
           <b>Application.getThreadContext().remove(RequestContext.DEFAULT_NAME);</b>
       }
   }
}

Между делом нашлось объяснение и тому странному факту, что сервер легко держал тестовую нагрузку, но ложился под существенно менее интенсивным реальным трафиком. Как ни странно, злую шутку сыграла… высокая производительность фреймворка. В ходе тестов я настроил нагрузчик на выдачу двух запросов в секунду; так вот, сервер справлялся с ними одной левой! В смысле, одним-единственным рабочим потоком. Между тем, для затыкания сервера требовалось включить в работу — хотя бы по одному разу — гораздо большее их число из пула потоков. И это именно то, что обеспечивает реальный браузер! Получив страницу, он начинает пачками закачивать статику: таблицы стилей, скрипты, картинки и прочее, и тогда серверу приходится задействовать дополнительные параллельные потоки. Если в это же время разом заходили еще несколько пользователей, в потоках застревало слишком много контекстной инфы, и сервер вылетал.

Напрашивается вопрос: а почему я не допер до всего этого раньше? А потому, что предыдущая программа, HeapAnalyzer, которую я использовал для анализа дампов, была полное гамно отражала картину памяти гораздо менее наглядным образом, только я, к сожалению, об этом не знал. Так что я даже ссылку на нее давать не буду.


Вот такие вот портянки приходилось отматывать в HeapAnalyzer, чтобы докопаться до сути, и все равно без толку. Почувствуйте, как говорится, разницу.

Мораль? Мораль сей басни такова: кто ищет, тот всегда найдет. Даже если для этого придется перерыть весь Интернет.

А у вас были смешные истории с поисками мемори ликов?

Благодарности:

  • Фирме Sun за опцию -XX:+HeapDumpOnOutOfMemoryError.
  • Товарищу по имени Krum Tsvetkov за отличную программу Memory Analyzer и доходчивое пособие по ее использованию, которое он не поленился составить.
  • Форуму программистов Vingrad и всем винградцам, принявшим участие в обсуждении проблемы.

ЗЫ. Прошу иметь в виду, что этот сайт посвящен вовсе не Java, а изучению английского, и пишу я здесь о своих злоключениях с отловом утечек памяти лишь потому, что программинг — это тоже часть работы над проектом, причем часть немаловажная и, увы, порой достаточно ресурсоемкая.