Рубрики
Без рубрики

Лучший способ обнаружения утечек подключений к базе данных

Автор оригинала: Vlad Mihalcea.

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

Утечка соединения происходит, когда соединение получено без закрытия.

Каждая реляционная база данных предлагает способ проверки состояния базового соединения, поэтому можно легко открыть новый SQL-терминал и проверить, нет ли каких-либо зависших соединений. Однако этот минималистичный подход неверен, поскольку он подразумевает, что мы внедрили в производство сломанную версию нашего приложения.

Утечки в соединениях должны быть обнаружены во время тестирования, что предотвращает возникновение утечек в производственных условиях.

В этом посте будет продемонстрировано, как вы можете автоматизировать обнаружение пула соединений с помощью простого модульного тестирования. Этот подход позволяет нам обнаруживать утечки соединений как в нашей фактической базе кода, так и в наших процедурах тестирования. Если модульные тесты приводят к утечке соединений, то процесс непрерывной интеграции будет прерван при достижении максимального порога подключения к базе данных.

Чтобы проверить, не происходит ли утечка соединений в данном тестовом классе, мы проверим количество оборванных соединений до и после того, как данный класс будет использоваться тестировщиком JUnit:

@BeforeClass
public static void initConnectionLeakUtility() {
    if ( enableConnectionLeakDetection ) {
        connectionLeakUtil = new ConnectionLeakUtil();
    }
}

@AfterClass
public static void assertNoLeaks() {
    if ( enableConnectionLeakDetection ) {
        connectionLeakUtil.assertNoLeaks();
    }
}

Утилита Утечка соединения Util выглядит следующим образом:

public class ConnectionLeakUtil {

    private JdbcProperties jdbcProperties = JdbcProperties.INSTANCE;

    private List idleConnectionCounters = 
        Arrays.asList(
            H2IdleConnectionCounter.INSTANCE,
            OracleIdleConnectionCounter.INSTANCE,
            PostgreSQLIdleConnectionCounter.INSTANCE,
            MySQLIdleConnectionCounter.INSTANCE
    );

    private IdleConnectionCounter connectionCounter;

    private int connectionLeakCount;

    public ConnectionLeakUtil() {
        for ( IdleConnectionCounter connectionCounter : 
            idleConnectionCounters ) {
            if ( connectionCounter.appliesTo( 
                Dialect.getDialect().getClass() ) ) {
                this.connectionCounter = connectionCounter;
                break;
            }
        }
        if ( connectionCounter != null ) {
            connectionLeakCount = countConnectionLeaks();
        }
    }

    public void assertNoLeaks() {
        if ( connectionCounter != null ) {
            int currentConnectionLeakCount = countConnectionLeaks();
            int diff = currentConnectionLeakCount - connectionLeakCount;
            if ( diff > 0 ) {
                throw new ConnectionLeakException( 
                    String.format(
                        "%d connection(s) have been leaked! Previous leak count: %d, Current leak count: %d",
                        diff,
                        connectionLeakCount,
                        currentConnectionLeakCount
                    ) 
                );
            }
        }
    }

    private int countConnectionLeaks() {
        try ( Connection connection = newConnection() ) {
            return connectionCounter.count( connection );
        }
        catch ( SQLException e ) {
            throw new IllegalStateException( e );
        }
    }

    private Connection newConnection() {
        try {
            return DriverManager.getConnection(
                jdbcProperties.getUrl(),
                jdbcProperties.getUser(),
                jdbcProperties.getPassword()
            );
        }
        catch ( SQLException e ) {
            throw new IllegalStateException( e );
        }
    }
}

Интерфейс Счетчик неработающих подключений определяет контракт для подсчета количества неактивных подключений с использованием реализации для конкретной базы данных.

public interface IdleConnectionCounter {

    /**
     * Specifies which Dialect the counter applies to.
     *
     * @param dialect dialect
     *
     * @return applicability.
     */
    boolean appliesTo(Class dialect);

    /**
     * Count the number of idle connections.
     *
     * @param connection current JDBC connection to be used for querying the number of idle connections.
     *
     * @return idle connection count.
     */
    int count(Connection connection);
}

Для каждого поддерживаемого гибернационного режима Диалекта , который мы используем во время тестирования, должен быть счетчик неработающих подключений реализация, чтобы мы могли проверить количество пропущенных подключений.

H2

public class H2IdleConnectionCounter implements IdleConnectionCounter {

    public static final IdleConnectionCounter INSTANCE = 
        new H2IdleConnectionCounter();

    @Override
    public boolean appliesTo(Class dialect) {
        return H2Dialect.class.isAssignableFrom( dialect );
    }

    @Override
    public int count(Connection connection) {
        try ( Statement statement = connection.createStatement() ) {
            try ( ResultSet resultSet = statement.executeQuery(
                    "SELECT COUNT(*) " +
                    "FROM information_schema.sessions " +
                    "WHERE statement IS NULL" ) ) {
                while ( resultSet.next() ) {
                    return resultSet.getInt( 1 );
                }
                return 0;
            }
        }
        catch ( SQLException e ) {
            throw new IllegalStateException( e );
        }
    }
}

Оракул

public class OracleIdleConnectionCounter implements IdleConnectionCounter {

    public static final IdleConnectionCounter INSTANCE = 
        new OracleIdleConnectionCounter();

    @Override
    public boolean appliesTo(Class dialect) {
        return Oracle10gDialect.class.isAssignableFrom( dialect );
    }

    @Override
    public int count(Connection connection) {
        try ( Statement statement = connection.createStatement() ) {
            try ( ResultSet resultSet = statement.executeQuery(
                    "SELECT COUNT(*) " +
                    "FROM v$session " +
                    "WHERE status = 'INACTIVE'" ) ) {
                while ( resultSet.next() ) {
                    return resultSet.getInt( 1 );
                }
                return 0;
            }
        }
        catch ( SQLException e ) {
            throw new IllegalStateException( e );
        }
    }
}

PostgreSQL

public class PostgreSQLIdleConnectionCounter implements IdleConnectionCounter {

    public static final IdleConnectionCounter INSTANCE = 
        new PostgreSQLIdleConnectionCounter();

    @Override
    public boolean appliesTo(Class dialect) {
        return PostgreSQL91Dialect.class.isAssignableFrom( dialect );
    }

    @Override
    public int count(Connection connection) {
        try ( Statement statement = connection.createStatement() ) {
            try ( ResultSet resultSet = statement.executeQuery(
                    "SELECT COUNT(*) " +
                    "FROM pg_stat_activity " +
                    "WHERE state ILIKE '%idle%'" ) ) {
                while ( resultSet.next() ) {
                    return resultSet.getInt( 1 );
                }
                return 0;
            }
        }
        catch ( SQLException e ) {
            throw new IllegalStateException( e );
        }
    }
}

MySQL

public class MySQLIdleConnectionCounter implements IdleConnectionCounter {

    public static final IdleConnectionCounter INSTANCE = 
        new MySQLIdleConnectionCounter();

    @Override
    public boolean appliesTo(Class dialect) {
        return MySQL5Dialect.class.isAssignableFrom( dialect );
    }

    @Override
    public int count(Connection connection) {
        try ( Statement statement = connection.createStatement() ) {
            try ( ResultSet resultSet = statement.executeQuery(
                    "SHOW PROCESSLIST" ) ) {
                int count = 0;
                while ( resultSet.next() ) {
                    String state = resultSet.getString( "command" );
                    if ( "sleep".equalsIgnoreCase( state ) ) {
                        count++;
                    }
                }
                return count;
            }
        }
        catch ( SQLException e ) {
            throw new IllegalStateException( e );
        }
    }
}

Я создал эту утилиту, чтобы мы могли отслеживать все модульные тесты, в которых происходит утечка соединений в Спящий режим ORM проект. При запуске его против hibernate-core я могу легко определить тесты виновника:

:hibernate-core:test

org.hibernate.jpa.test.EntityManagerFactoryClosedTest > classMethod FAILED
    org.hibernate.testing.jdbc.leak.ConnectionLeakException

org.hibernate.jpa.test.EntityManagerFactoryUnwrapTest > classMethod FAILED
    org.hibernate.testing.jdbc.leak.ConnectionLeakException

org.hibernate.jpa.test.cdi.NoCdiAvailableTest > classMethod FAILED
    org.hibernate.testing.jdbc.leak.ConnectionLeakException

org.hibernate.jpa.test.factory.SynchronizationTypeTest > classMethod FAILED
    org.hibernate.testing.jdbc.leak.ConnectionLeakException

Когда я открываю отчет для закрытого теста EntityManagerFactory , я даже вижу, сколько соединений утекает:

org.hibernate.testing.jdbc.leak.ConnectionLeakException: 1 connection(s) have been leaked! Previous leak count: 0, Current leak count: 1

Тест типа синхронизации даже указывает на то, что также имеются утечки предыдущих соединений:

org.hibernate.testing.jdbc.leak.ConnectionLeakException: 1 connection(s) have been leaked! Previous leak count: 2, Current leak count: 3

Обнаружение утечек соединений является обязательным требованием для каждого корпоративного приложения. Хотя вы можете найти сценарии, которые периодически запускаются и уничтожают все неработающие подключения к базе данных, это всего лишь вспомогательный подход.

Лучший способ борьбы с утечками соединений-это исправить базовую базу кода, чтобы соединения всегда закрывались должным образом. Чтобы убедиться, что в рабочей среде отсутствует утечка соединения, каждый интеграционный тест должен проверять, что фактический проверяемый исходный код или сама логика тестирования не допускают утечки соединения.