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

song yuheon·2023년 10월 10일
0

Trouble Shooting

목록 보기
26/57
post-thumbnail
post-custom-banner

문제 발생


  • 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);
            }
        }
    }


해당 쓰레드 정보가 보인다는 것을 알 수 있다


  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;
            }
    
        }


  1. 트랜잭션 격리 수준 조정
    현재 설정된 Isolation.REPEATABLE_READ 대신 Isolation.SERIALIZABLE을 사용하여 트랜잭션 격리 수준을 더 높게 설정할 수 있다.
    • 방법
      @Transactional(isolation = Isolation.SERIALIZABLE)
    • 주의사항
      • 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 해제
        }


    }

동시 접근 가능한 스레드를 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();
          }
      }
    • 주의점
      항상 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 +
                '}';
    }

단일 스레드에서는 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의 스레드 설정을 조절하였다.


정리

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


profile
backend_Devloper
post-custom-banner

0개의 댓글