[Перевод] Устойчивость микросервисных Spring приложений: роль аннотации @Transactional в предотвращении утечки соединений

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

Микросервисные Spring приложения сильно зависят от стабильного и эффективного взаимодействия с базой данных, которое часто осуществляется через Java Persistence API (JPA). Для поддержания высокой производительности важно правильно управлять пулом соединений и предотвращать утечки соединений, чтобы взаимодействие с базой данных не снижало производительность приложения. 

История проблемы

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

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

public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName,
                               List notifList, String attributes, String notifTitle,
                               String notifSubject, String notifPreviewText, String contentType, 
                               boolean doNotDelete, boolean isLetter, String groupId) throws EDeliveryException {

    try {
        StoredProcedureQuery query = entityManager.createStoredProcedureQuery("p_create_notification");
        DbUtility.setParameter(query, "v_notif_code", notifCode); 
        DbUtility.setParameter(query, "v_user_uuid", userId); 
        DbUtility.setNullParameter(query, "v_user_id", Integer.class); 
        DbUtility.setParameter(query, "v_acct_id", acctId); 
        DbUtility.setParameter(query, "v_message_url", s3KeyName); 
        DbUtility.setParameter(query, "v_ecomm_attributes", attributes); 
        DbUtility.setParameter(query, "v_notif_title", notifTitle); 
        DbUtility.setParameter(query, "v_notif_subject", notifSubject); 
        DbUtility.setParameter(query, "v_notif_preview_text", notifPreviewText); 
        DbUtility.setParameter(query, "v_content_type", contentType); 
        DbUtility.setParameter(query, "v_do_not_delete", doNotDelete); 
        DbUtility.setParameter(query, "v_hard_copy_comm", isLetter); 
        DbUtility.setParameter(query, "v_group_id", groupId); 
        DbUtility.setOutParameter(query, "v_notif_id", BigInteger.class); 

        query.execute(); 
        BigInteger notifId = (BigInteger) query.getOutputParameterValue("v_notif_id"); 
        return notifId.longValue();
    } catch (PersistenceException ex) { 
        logger.error("DbRepository::createInboxMessage - Error creating notification", ex); 
        throw new EDeliveryException(ex.getMessage(), ex); 
    } 
} 

Анализ проблемы

Как иллюстрирует наш сценарий, когда процедура сталкивалась с ошибкой, исключение передавалось наверх из слоя репозитория в слой сервиса и, в конце концов, на контроллер. При этой передаче возникали проблемы, заставляющие API отвечать HTTP статусом, отличным от 200 — чаще всего 500 или 400. После нескольких таких процедур контейнер сервиса достигал точки, после которой он уже не мог справляться с входящими запросами, что и порождало ошибку 502 Gateway Timeout. Это критическое состояние получало отражение в наших системах мониторинга, при этом логи Kibana отображали проблему следующим образом:   

HikariPool-1 - Connection is not available, request timed out after 30000ms.

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

Решение

В качестве одного из решений, мы могли бы корректно обработать исключение и не пересылать его выше, позволяя JPA и Spring высвободить соединения и вернуть их в пул. Альтернативное решение — использовать аннотацию @Transactional на методе. Ниже приведен тот же метод с аннотацией:  

@Transactional
public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName,
                               List notifList, String attributes, String notifTitle,
                               String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter,
                               String groupId) throws EDeliveryException {
	………
}

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

public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName,
                               List notifList, String attributes, String notifTitle,
                               String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter,
                               String loanGroupId) {
    try {
        .......
        query.execute();
    	BigInteger notifId = (BigInteger) query.getOutputParameterValue("v_notif_id");
    	return notifId.longValue();      
    } catch (PersistenceException ex) {
        logger.error("DbRepository::createInboxMessage - Error creating notification", ex);
    }
    return -1;
}

Используем @Transactional 

Аннотация @Transactional во фреймворке Spring управляет границами транзакции. Она начинает транзакцию, когда метод, помеченный аннотацией, стартует, затем подтверждает или откатывает ее после окончания работы метода. Когда метод выбрасывает исключение, аннотация @Transactional обеспечивает откат транзакции, что помогает правильным образом высвободить соединение и вернуть его в пул.  

Без @Transactional 

Когда метод репозитория, вызывающий сохраненную процедуру, не помечен аннотацией @Transactional, Spring не управляет границами транзакции внутри этого метода. Управление транзакцией должно программироваться вручную для случая, когда сохраненная процедура выбрасывает исключение. Если не управлять этим должным образом, может создаться ситуация, когда соединение с базой данных не закрывается и не возвращается в пул соединений, приводя к утечке. 

Оптимальный подход 

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

  • Обеспечивайте обработку исключений внутри метода таким образом, чтобы эта обработка включала правильный откат транзакции и закрытие соединений с базой данных, особенно если не используете @Transactional

Вывод 

Эффективное управление транзакциями исключительно важно для поддержания нормального состояния и высокой производительности микросервисных приложений на Spring, использующих JPA. Аннотация @Transactional помогает избежать утечек соединений и гарантирует, что работа с базой данных не ухудшит производительность или стабильность системы. Следование этим правилам улучшает надежность и эффективность наших Spring микросервисов, предоставляя стабильные и быстрые по времени ответа сервисы приложению-потребителю или конечным пользователям.

4db32a0a6370259c8249ec2a6f7d38ff.png

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех,  присоединяйтесь!

© Habrahabr.ru