N+1 트러블슈팅, Jmeter : N+1 관련 부하 테스트

조대훈·2024년 10월 28일
post-thumbnail

1. Apache JMeter란?

Apache JMeter는 서버가 제공하는 성능 및 부하를 측정할 수 있는 테스트 도구이다. JMeter는 순수 Java 애플리케이션 오픈소스이며 서버나 네트워크 또는 개체에 대해 과부하를 시뮬레이션하여 강도를 테스트 하거나 다양한 부하 유형에서 전체 성능을 분석하는데 사용할 수 있다.

1-1 JMeter 를 고르게 된 이유

  1. 설치와 사용이 간단하다. CLI 기반의 실행 환경이 아닌 GUI 기반의 인터페이스로 러닝 커브 곡선이 비교적 낮다.
  2. 오랜 기간 동안 유지보수되어 타 성능 테스트 도구들 보다 많은 래퍼런스를 제공한다.
  3. 로컬 환경에서 단순 응답시간과 TPS 측정만 필요했다.

1-2 주요 개념

응답 시간

  • 클라이언트가 서버에 요청하고 그 요청에대한 응답을 받을 때 까지 걸린 시간
  • 응답 시간의 종류
    • 처리 시간 Processing Time : 실제 서버가 요청을 처리하는데 걸린 시간
    • 대기 시간 Latency Time

TPS

  • 서버가 초당 처리할 수 있는 요청의 개수
  • TPS 가 높을 수록 초당 처리할 수 있는 요청의 수가 많다.

1-3 설치 및 실행

  1. 설치 명령어 : brew install Jmeter
  2. 실행 명령어 : Jmeter
  3. 이후 Plugin Manager 를 통해 3Basic Graphs 추가
  4. Add -> Listener 를 통해 jp@gc 로 시작하는 외부 플러그인 설치 확인

2. JMeter 구성요소

  1. ThreadGroup : 몇 개의 쓰레드가 동시에 요청을 보내는지
  2. Sampler : 어떤 유저가 해야 하는 요청
  3. LogicController : 테스트 계획의 실행 흐름을 제어
  4. Listener : 응답을 받았을 때 어떤 동작을 취하는지 (검증, 리포트, 그래프 그리기 등)
  5. Configuration : Sampler 또는 Listener가 사용할 값 (쿠키, JDBC 커넥션 등)

2-1 그래프 설정

기본으로 제공하는 그래프 외에 3Basic Graph 를 추가해서 사용 해보려고 한다.




(그래프 추가 플러그인이 설치가 완료된 모습)

100개의 Thread (사용자)가 1초에 30번 (Ramp-up) 100초 동안 (Loop Count) 실행

캐싱 어노테이션을 주석 처리 하여 캐싱 유무 테스트를 진행 했다.


그래프 추가와 댓글 읽기 캐싱(전-후) 추가

JWT 인증 토큰 방식을 사용하고 있는 관계로 헤더에 컨텐츠 타입과 Authorization 액세스 토큰 설정 해주었다.


2-2 캐싱 적용 전 후로 보는 Aggresgate Report 결과

  • 평균 및 중앙값 응답 시간: 80% 개선
  • 90% 라인 응답 시간: 77.8% 개선
  • 최대 응답 시간: 45% 개선
  • 처리량: 1.64% 개선

결과표
1만건의 요청.
전반적으로, 특히 응답 시간 측면에서 상당한 성능 향상이 있었다. 평균 및 일반적인 케이스(중앙값, 90% 라인)에서 약 80% 가까이 응답 속도가 개선되었고, 최소 45%의 개선이 있었습니다. 단순 로컬 환경에서 테스트라 추후 정확한 테스트가 필요해 보인다.

2-3 N+1 쿼리 최적화 전 (1)

@Query("SELECT COUNT(n) FROM Notify n WHERE n.receiver = :user AND n.isRead = false")  
int countUnreadNotifications(User user);

데이터베이스에 두 번 접근. 유저를 찾는 쿼리가 실행되고, 그 다음 해당 유저가 가진 알림 갯수를 COUNT 하는 쿼리가 실행 된다.

2-4 N+1 쿼리 최적화 후(1)

@Query("SELECT COUNT(n) FROM Notify n JOIN n.receiver u WHERE u.email = :email AND n.isRead = false")  
int countUnreadNotifications(String email);


개선 후 단문의 쿼리만 나가는 모습

전체적인 소폭 개선과 최대 응답시간이 눈에 띄게 줄었다.

2-5 N+1 문제 발생 쿼리 (2)

Hibernate: 
    select
        u1_0.id,
        u1_0.age,
        u1_0.deleted_at,
        u1_0.email,
        u1_0.gender,
        u1_0.img,
        u1_0.is_deleted,
        u1_0.mobile,
        u1_0.name,
        u1_0.nickname,
        u1_0.password,
        u1_0.social 
    from
        user u1_0 
    where
        u1_0.email=? // 유저 정보 조회
Hibernate: 
    select
        jcr1_0.user_id,
        jcr1_1.id,
        jcr1_1.activity_type,
        jcr1_1.can_join,
        jcr1_1.is_participant,
        jcr1_1.participant_count 
    from
        chat_room_participants jcr1_0 
    join
        chat_rooms jcr1_1 
            on jcr1_1.id=jcr1_0.chat_room_id 
    where
        jcr1_0.user_id=? // 유저가 참여한 채팅방 조회 
Hibernate: 
    select
        p1_0.id,
        p1_0.activity_type,
        p1_0.capacity,
        p1_0.chat_room_id,
        p1_0.content,
        p1_0.del_flag,
        p1_0.latitude,
        p1_0.local_date,
        p1_0.longitude,
        p1_0.meeting_time,
        p1_0.participate_flag,
        p1_0.place_name,
        p1_0.road_name,
        p1_0.title,
        u1_0.id,
        u1_0.age,
        u1_0.deleted_at,
        u1_0.email,
        u1_0.gender,
        u1_0.img,
        u1_0.is_deleted,
        u1_0.mobile,
        u1_0.name,
        u1_0.nickname,
        u1_0.password,
        u1_0.social,
        p1_0.view_count 
    from
        post p1_0 
    left join
        user u1_0 
            on u1_0.id=p1_0.user_id 
    where
        p1_0.chat_room_id=? // 채팅방과 연관된 게시물 조회 
Hibernate: 
    select
        cml1_0.chat_room_id,
        cml1_0.id,
        cml1_0.chat_message_type,
        cml1_0.content,
        cml1_0.created_at,
        cml1_0.user_id 
    from
        chat_messages cml1_0 
    where
        cml1_0.chat_room_id=? // 특정 채팅방의 연관된 메세지 조회 

3N+1 발생 중

2-6 N+1 문제 발생 쿼리(2) -> 복합 쿼리로 개선

@Query("SELECT NEW com.example.simplechatapp.dto.UserChatRoomDTO(" +  
       "cr.id, " +                  // chatRoomId  
       "p.id, " +                   // postId  
       "p.title, " +                // postTitle  
       "p.meetingTime, " +          // meetingTime  
       "cr.participantCount, " +    // participantCount  
       "p.capacity, " +             // capacity  
       "MAX(cm.createdAt), " +      // lastMessageTime  
       "SUBSTRING(MAX(CASE WHEN cm.id = (SELECT MAX(cm2.id) FROM ChatMessage cm2 WHERE cm2.chatRoom = cr) THEN cm.content ELSE NULL END), 1, 30), " + // lastMessagePreview  
       "cr.activityType) " +        // activityType  
       "FROM User u " +  
       "JOIN u.joinedChatRoom cr " +  
       "JOIN cr.post p " +  
       "LEFT JOIN cr.chatMessageList cm " +  
       "WHERE u.email = :email " +  
       "GROUP BY cr.id, p.id, p.title, p.meetingTime, cr.participantCount, p.capacity, cr.activityType")  
List<UserChatRoomDTO> findUserChatRoomDTOs(@Param("email") String email);

단일문의 쿼리로 원하는 정보를 불러오고 있다.

Hibernate: 
    select
        jcr1_1.id,
        p1_0.id,
        p1_0.title,
        p1_0.meeting_time,
        jcr1_1.participant_count,
        p1_0.capacity,
        max(cml1_0.created_at),
        substring(max(case 
            when cml1_0.id=(select
                max(cm1_0.id) from
                    chat_messages cm1_0 
                where
                    cm1_0.chat_room_id=jcr1_1.id) 
                    then cml1_0.content 
                else null 
        end), 1, 30),
        jcr1_1.activity_type 
    from
        user u1_0 
    join
        chat_room_participants jcr1_0 
            on u1_0.id=jcr1_0.user_id 
    join
        chat_rooms jcr1_1 
            on jcr1_1.id=jcr1_0.chat_room_id 
    join
        post p1_0 
            on jcr1_1.id=p1_0.chat_room_id 
    left join
        chat_messages cml1_0 
            on jcr1_1.id=cml1_0.chat_room_id
profile
백엔드 개발자를 꿈꾸고 있습니다.

0개의 댓글