Blog

Springboot 웹서버를 JMeter 멀티 스레드 에러

tag
트러블슈팅
날짜
2023/10/10
생성 일시
2023/10/28 06:39
작성자

문제 발생

Springboot 웹서버를 JMeter로 성능 테스트 중, 단일 스레드 환경에서는 문제가 없으나 멀티 스레드 환경에서 문제가 발생하였다.

시도한 해결 방안

1.
로깅 강화 로깅을 통해 문제 발생 지점을 확인하려 하였다. 로그에는 스택 트레이스, 입력 값, 쓰레드 이름 및 ID 등을 포함시켰다.
import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PerformanceLoggingUtil { private static Logger getLogger(Class<?> clazz) { return LoggerFactory.getLogger(clazz); } public static void logPerformanceInfo(Class<?> clazz, String message) { Logger logger = getLogger(clazz); String threadName = Thread.currentThread().getName(); long threadId = Thread.currentThread().getId(); long timestamp = System.currentTimeMillis(); String formattedMessage = String.format("[Timestamp: %d, Thread Name: %s, Thread ID: %d] %s", timestamp, threadName, threadId, message); if (logger.isInfoEnabled()) { logger.info(formattedMessage); } } public static void logPerformanceError(Class<?> clazz, String message, Throwable t) { Logger logger = getLogger(clazz); String threadName = Thread.currentThread().getName(); long threadId = Thread.currentThread().getId(); long timestamp = System.currentTimeMillis(); String formattedMessage = String.format("[Timestamp: %d, Thread Name: %s, Thread ID: %d] %s", timestamp, threadName, threadId, message); if (logger.isErrorEnabled()) { logger.error(formattedMessage, t); } } }
Java
복사
해당 쓰레드 정보가 보인다는 것을 알 수 있다
1.
동기화와 락synchronized 키워드와 ReentrantLock을 사용하여 문제를 해결하려 시도하였으나 문제는 계속 발생하였다.
2.
synchronized 키워드 사용
@Transactional(isolation = Isolation.REPEATABLE_READ) public MessageDto updateAnswer(AnswerRequestDto requestDto,Long answerId, User user) { try{ PerformanceLoggingUtil.logPerformanceInfo(AnswerService.class, "설문지 응답 업데이트 시작"); Answer answer; synchronized (answerRepository) { // 해당 로직 블록을 동시에 하나의 스레드만 실행할 수 있게 한다. answer = answerRepository.findById(answerId).orElseThrow(() -> new NullPointerException("예외가 발생하였습니다.")); } // Answer answer = answerRepository.findByIdForUpdate(answerId) // .orElseThrow(() -> new IllegalArgumentException("해당 ID에 대한 답변을 찾을 수 없습니다.")); if (!answer.getUser().getUserId().equals(user.getUserId())){ throw new IllegalArgumentException("예외가 발생하였습니다."); } // 사용자가 응답자가 아닐 시 에러 출력 if(answer.getSurvey().getMaxChoice() < requestDto.getAnswer()){ throw new IllegalArgumentException("예외가 발생하였습니다."); } // 선택지에 없는 응답으로 변경 시 에러 출력 answer.update(requestDto.getAnswer()); MessageDto message = new MessageDto("수정이 완료되었습니다."); PerformanceLoggingUtil.logPerformanceInfo(AnswerService.class, "설문지 응답 업데이트 완료"); return message; } catch(Exception e){ PerformanceLoggingUtil.logPerformanceError(AnswerService.class, "설문지 응답 업데이트 중 오류 발생", e); throw e; } }
Java
복사
1.
트랜잭션 격리 수준 조정 현재 설정된 Isolation.REPEATABLE_READ 대신 Isolation.SERIALIZABLE을 사용하여 트랜잭션 격리 수준을 더 높게 설정할 수 있다.
방법
@Transactional(isolation = Isolation.SERIALIZABLE)
Java
복사
주의사항
SERIALIZABLE 수준은 성능에 영향을 줄 수 있다. 따라서 적용 후 성능 테스트를 반드시 수행하여야 합니다.
트랜잭션 격리 수준을 Isolation.SERIALIZABLE로 변경하여 동시성 문제를 해결하려고 시도하였으나 해결되지 않았다.
1.
Semaphore Semaphore는 동시에 액세스할 수 있는 스레드 수를 제한하는 데 사용되는 동시성 도구이다. 주로 한정된 자원을 여러 스레드가 동시에 사용하려 할 때 이를 제어하는 데 사용된다.
이를 이용해서 에러 줄이는 걸 시도해 보겠다
@Service @RequiredArgsConstructor public class AnswerService { private final AnswerRepository answerRepository; private final SurveyRepository surveyRepository; private final Semaphore semaphore = new Semaphore(3); // 동시에 3개의 스레드만 허용 public MessageDto createAnswer(AnswerRequestDto requestDto, User user) throws InterruptedException { semaphore.acquire(); // Semaphore 획득 try { Survey survey = surveyRepository.findById(requestDto.getSurveyId()).orElseThrow(() -> new NullPointerException("예외가 발생하였습니다.")); if (answerRepository.findByUserAndSurvey(user, survey).isPresent()) { throw new IllegalArgumentException("예외가 발생하였습니다."); } // 이미 선택한 설문지를 중복 응답 시 에러 출력 if (survey.getMaxChoice() < requestDto.getAnswer()) { throw new IllegalArgumentException("예외가 발생하였습니다."); } // 선택지에 없는 응답 시 에러 출력 Answer answer = new Answer(requestDto.getAnswer(), user, survey); if (survey.getDeadline().isBefore(LocalDateTime.now())) { throw new IllegalArgumentException("예외가 발생하였습니다."); } Answer savedAnswer = answerRepository.save(answer); MessageDto message = new MessageDto("작성이 완료되었습니다."); return message; } finally { semaphore.release(); // Semaphore 해제 } }
Java
복사
동시 접근 가능한 스레드를 3개로 설정 했을때 문제가 발생한다 여전히
그럼 1개로 수정해 보겠다 1개로 수정해도 여전히 오류가 발생한다
1.
Locking여전히 오류는 잡히지 않는다.
정의 Java의 java.util.concurrent.locks 패키지에 있는 Lock 인터페이스와 구현체들을 사용하여 명시적으로 락을 관리하는 방법이다.
방법ReentrantLock 등의 구현체를 사용하여 락을 얻고 해제한다고 한다.
private final Lock lock = new ReentrantLock(); public MessageDto createAnswer(AnswerRequestDto requestDto, User user) { lock.lock(); try { Survey survey = surveyRepository.findById(requestDto.getSurveyId()).orElseThrow(() -> new NullPointerException("예외가 발생하였습니다.")); if (answerRepository.findByUserAndSurvey(user, survey).isPresent()) { throw new IllegalArgumentException("예외가 발생하였습니다."); } // 이미 선택한 설문지를 중복 응답 시 에러 출력 if (survey.getMaxChoice() < requestDto.getAnswer()) { throw new IllegalArgumentException("예외가 발생하였습니다."); } // 선택지에 없는 응답 시 에러 출력 Answer answer = new Answer(requestDto.getAnswer(), user, survey); if (survey.getDeadline().isBefore(LocalDateTime.now())) { throw new IllegalArgumentException("예외가 발생하였습니다."); } Answer savedAnswer = answerRepository.save(answer); MessageDto message = new MessageDto("작성이 완료되었습니다."); return message; } finally { lock.unlock(); } }
Java
복사
주의점 항상 finally 블록에서 락을 해제해야 한다.
1.
로깅 강화 로깅을 더욱 강화하기로 하였다.
@Override public String toString() { return "Answer{" + "answerId=" + answerId + ", answerNum=" + answerNum + ", survey=" + (survey != null ? survey.getId() : "null") + ", user=" + (user != null ? user.getId() : "null") + ", version=" + version + '}'; }
Java
복사
단일 스레드에서는 create -> update로 정상적으로 요청이 실행되지만
다중 스레드에서는 서버가 정상적으로 작동하지 않아서 순서가 보장 되지 않는다는 것을 알 수 있다. => constant Timer를 이용하면 순서 보장이 가능하다
하지만 여전히 문제가 발생한다.
다중 스레드 환경에서 쓰레드간에 병렬 처리로 처리가 완료 되는 순서가 보장되지 않기 때문이다. 스레드가 1, 2, 3, 4, 5로 시작 되더라도 DB에 저장이 완료되는 시간이 항상 1,2,3,4,5로 되지는 않는다 즉 스레드는 1, 3, 2, 4, 5 형태로 완료 되면 DB에서 각 요청에 대해 1, 3, 2, 4, 5 형태로 저장된다.
하지만 지금은 csv 파일을 읽어 업데이트할 survey Id를 가지고 오는데 이는 1, 2, 3, 4, 5로 고정 되어 있기에 오류가 발생한 것이다.
그래서 요청간의 순서를 보장하더라도 여전히 오류가 발생한다.

근본적인 원인 파악

스레드의 문제가 아니라 로직의 문제였다. 다중 스레드 환경에서는 서버가 정상적으로 작동하지 않아 요청 처리 순서가 보장되지 않았다. 이로 인해 데이터의 일관성 문제가 발생하였다.

최종 해결 방법

1.
응답 형태 변경 기존 메시지 반환 방식에서 Entity Dto로 바꾸어서 반환하였다.
2.
이전 요청의 결과를 다음 요청에 반영 JMeter의 Json Extractor를 사용하여 이전 요청의 결과를 다음 요청에 활용하였다. JMeter에서 이전 요청 결과를 다음 요청에 적용하기
3.
스레드 조절 요청 처리 순서를 보장하기 위해 JMeter의 스레드 설정을 조절하였다.

정리

멀티 스레드 환경에서 발생하는 문제를 해결하기 위해서는 동시성 문제뿐만 아니라 로직의 문제도 함께 고려해야 한다. 이번 문제에서는 로직의 문제가 주 원인이었으며, 이를 해결하여 문제를 해결할 수 있었다.