Tomcat DBCP leak

Рубрика: Development, Java | 26 June 2007, 08:12 | juriy

Хочу описать небольшие, но крайне неприятные грабли, по которым мне довелось потоптаться. Грабли заключаются в том, что Tomcat не закрывает открытые соединения DBCP пула после undeploy/stop/redeploy.
Эффект следующий: вы спокойно отлаживаете какой-нибудь проектик, который использует DBCP. Через часа полтора-два отладки, приходит злой DBA и сообщает, что вы “совсем охренели по 700 сессий держать”. Если для пользователя, под которым вы ходите в БД установлено ограничение по количеству сессий, то вы получаете ошибку вроде “Exceeded simultaneous session per user limit” ну или что-то вроде того.

Чтобы повторить этот эффект необходимо произвести следующие действия:

1. Проверить количество подключений к серверу. На Oracle можно это сделать таким вот нехитрым запросом:

select t.MACHINE, count(*)
from v$session t
where t.USERNAME = 'myuser'
group by t.MACHINE

2. Поднять Tomcat, задеплоить приложение, и выполнить запрос к сервлету, который использует пул.

3. Снова проверяем количество сессий – число должно увеличиться на значение, указанное в параметре initialSize конфигурации пула:


<parameter>
    <name>initialSize</name>
    <value>1</value>
</parameter>

4. Выполнить undeploy/deploy приложения, выполнить еще один запрос к сервлету и снова промониторить количество сессий.Вы увидите, что количество сессий снова увеличилось, нисмотря на то, что приложение было переустановлено.Разработчики говорят “это не баг – это фича”. Какая, дескать, нашему Tomcat’у разница, что за ресурсы вы там открываете. Сами открыли – сами закрывайте.

Без проблем, отвечаем мы и начинаем писать закрывалку ресурсов DBCP.

Для начала в web.xml объявим listener’а.


<listener>
    <listener-class>ResourceManager</listener-class>
</listener>

Теперь, собственно, его код:


import java.util.Enumeration;import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.log4j.Logger;

public final class ResourceManager implements ServletContextListener {

	Logger logger = Logger.getLogger("ResourceManager");

	public ResourceManager() {
	}

	public void contextInitialized(ServletContextEvent event) {
		logger.info("Startup");
	}

	public void contextDestroyed(ServletContextEvent event) {
		logger.info("Shutdown");
		closeDbcpPool();
	}

	private void closeDbcpPool() {
		Context initCtx;
		try {
			initCtx = new InitialContext();
			Enumeration bindings = initCtx.listBindings("java:comp/env/jdbc/pool");
			while (bindings.hasMoreElements()) {
				Binding binding = null;
				try {
					binding = (Binding) bindings.nextElement();
					DataSource someDateSource = (DataSource) initCtx.lookup(
					"java:comp/env/jdbc/pool/" + binding.getName());

					if (someDateSource instanceof BasicDataSource) {
						BasicDataSource basicDS = (BasicDataSource)someDateSource;
						basicDS.close();
						logger.info(binding.getName() + " removed");
					}

				} catch (Exception e) {
					if (binding != null)
						logger.warn("Error closing " + binding.getName() +
								" : " + e.getMessage());
					else
						logger.warn("Error closing pool " +
								" : " + e.getMessage());
				}
			}
		} catch (NamingException e) {
			e.printStackTrace();
		}
	}

}

Повторяем описанный эксперимент – видно, что теперь все сессии закрыты.

К сожалению – не все так гладко. Сессии, которые назодятся в состоянии active не будут закрыты. Чтобы проеврить – делаем сервлет, который выполняет запрос достаточно долго. Если нет желания выдумывать такой запрос то можно просто написать


while (resultSet.next()) {
    try {
        Thread.sleep(1000);
    } catch (Exception e) {
    }
}

Теперь повторяем эксперимент. После undeploy на БД останется открытой одна лишняя сессия (это и есть наш “долгий” запрос). А на страничке статуса сервера видно, что один поток работает уж очень долго.

Другими словами, мне не удалось полностью обезопасить себя от “потоков-зомби” после закрытия приложения. Хорошая новость – после отработки запроса HTTP поток спокойно вернется обратно в пул.

C другой стороны, если объект Connection закрыть в отдельном потоке во время исполнения запроса, то соединение спокойно закроется. При этом в цикле выборки (while (resultSet.next())) будет поймана исключительная ситуация.

Комментариев: 3

3 Responses to “Tomcat DBCP leak”

Комментарии:

  1. Zverek

    Вот спасибо! Недавно напоролся на эти грабли, думал это что-то у меня не так, пока с помощью JMX не убедился, что Tomcat не закрывает пул соединений после остановки приложения, причём такая “фича” как в 5 так и в 6.

  2. Ковыряльщик

    Спасибо за таблетку :-)

  3. postres

    Если приложение нестабильно то может следует задуматься, чтобы отвязать его от пула соеденений? Расшарить пул на уровне сервера как глобальный JNDI ресурс, тогда деплой/андеплой наздоровье пока из Tomcat вся память не вытечет. При том даже расшаренный пул можно коректно закрыть http://www.javatalks.ru/ftopic13967.php

Leave a Reply