Post

CPU, Memory 모니터링을 통한 성능 측정 및 병목현상 원인 파악과 해결 과정

병목현상이란?

Untitled

병목현상은 다수의 요청이 동일한 자원이나 시스템으로 몰려 특정 지점에서 처리 용량을 초과하게 되어 발생하는 성능 저하 현상을 의미합니다.

이번 포스팅에서는 병목현상의 발생 원인을 시각적 자료와 함께 분석하고, 이를 해결하기 위한 성능 개선 방법을 제시해보고자 합니다.

또한, 병목현상을 분석하여 그 원인이 내부 시스템 흐름에 어떻게 영향을 미치는지에 대한 과정도 기록하겠습니다.

이 글은 프로젝트에서 개발한 API를 실제 운영 환경과 유사한 조건에서 성능 테스트를 진행한 결과를 바탕으로 작성되었습니다.

AWS 테스트 환경 구성

서버 구성 Untitled

  • [db.t3.micro] Mysql RDS 20GB 1vCPU, 1GB Mem - 데이터베이스
  • [t3a.small] AWS EC2 WAS 8GB 2vCPU, 2GiB Mem - 애플리케이션 서버 (WAS)
  • [t3a.small] AWS EC2 K6 8GB 2vCPU, 2GiB Mem - K6 부하 생성기
  • AWS ELB(Elastic Load Balancing) - 로드밸런서
  • AWS CloudWatch - 모니터링


RDS 테스트 데이터 구성

Untitled

사용자 데이터 1000건

Untitled

여러 상태의 모임 데이터 70만건

Untitled

챌린지 데이터 70만건


테스트 시나리오

  • 가상 사용자 수(VU)를 10분에 걸쳐 50명으로 설정하여 점진적으로 부하 테스트를 진행.
  • 95% 확률로 게시물 조회를 요청하고, 5% 확률로 로그인 후 게시물을 생성하는 시나리오.
  • 사용자는 요청 후 1초간 sleep 후 다시 요청한다.
  • K6를 사용한 스크립트를 통해 테스트를 진행.

K6 테스트 스크립트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  // 부하를 생성하는 단계(stages)를 설정
  stages: [
    // 10분에 걸쳐 vus(virtual users, 가상 유저수)가 50에 도달하도록 설정
    { duration: '10m', target: 50 }
  ],
};

export default function () {
  let random = Math.random();
  // 100명 중 5명의 비율로 게시글을 작성
  if (random < 0.05) {
	  
	  // 1. 로그인 요청
	  const loginPayload = JSON.stringify({
	    email: 'email1@naver.com',
	    password: 'Wjdwptk1!',
	  });
  
	  const loginHeaders = { 'Content-Type': 'application/json' };
          const loginResponse = http.post('http://{ELB 주소}/api/auths/login', loginPayload, { headers: loginHeaders });
  
  
	  if (loginResponse.status === 200) {
              const loginResult = JSON.parse(loginResponse.body);
              const token = loginResult?.result?.token; // 토큰 값 추출
              
              const postPayload = JSON.stringify({
	          name: '제목',
	          content: '내용',
	          endDate: '2025-03-13',
	          startDate: '2025-02-08',
	          minCapacity: 1001,
	          maxCapacity: 1002,
	          bookId: 1,
	          gatheringStatus: 'RECRUITING',
	          readingTimeGoal: 'ONE_HOUR',
	          gatheringWeek: 'ONE_WEEK',
        });
        
        const postHeaders = {
	          'Content-Type': 'application/json',
	          Authorization: `Bearer ${token}`, // Authorization 헤더 추가
        };
        
        http.post('http://{ELB 주소}/api/gathering', postPayload, { headers: postHeaders });
	  }
    
  // 100명 중 95명의 비율로 게시글을 조회
  } else {
    http.get('http://{ELB 주소}/api/gatheringSearch/filtering?gatheringSortType=NEWEST_FIRST&page=0&size=10&startDate=2025-01-20&endDate=2025-03-30&readingTimeGoals=ONE_HOUR&today=false');
  }
  
  // 1초 휴식을 주어 더 현실적으로 테스트
  sleep(1);
}


테스트 측정 결과

Throughput

Untitled

  • VUs : 50
  • TPS : 1.2/s


Latency

Untitled

  • 평균 요청 비율 : 1.2/s
  • 평균 응답시간(Latency) : 46.3/s


HTTP Request Fail

Untitled


이번 테스트는 50명의 가상 사용자가 동시 요청하는 기준으로 진행되었습니다. 테스트 결과, 초당 처리 가능한 요청 수(TPS)는 평균 1.2/s 로 측정되었으며, 지연시간(Latency)는 약 46.3/s에 달했습니다.

HTTP Request Fail의 수 또한 0.55/s 로 측정되어 전체적으로 많은 트래픽도 아니였지만 현저히 불안정한 테스트 결과가 발생하였습니다.


CPU, Memory 모니터링 측정 결과

Untitled

테스트 실행 초기 모니터링 결과, EC2와 RDS의 CPU 및 메모리 사용률이 점진적으로 증가하며 부하가 발생하는 모습이 확인되었습니다. 이 시점에서는 서버와 DB가 균등하게 요청을 처리하고 있음을 파악할 수 있었습니다.

Untitled

테스트 실행 최종 모니터링 측정 결과 VUs가 증가함에 따라 트래픽이 늘었고, EC2와 RDS의 CPU 사용률이 급격히 차이를 보였습니다. 특히, RDS의 CPU 사용률은 99% 가까이 차지하며 DB에서 병목현상이 발생하고 있다는 것을 확인할 수 있었습니다.


RDS 병목 현상 분석 (커넥션 풀 부족)

테스트에서 발생한 오류는 다음과 같습니다

1
2
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms  
(total=10, active=10, idle=0, waiting=1)
CommandDescription
total=10커넥션 풀 최대 크기가 10개로 설정됨.
active=1010개의 커넥션이 모두 사용 중.
idle=0유휴 상태(사용 가능한) 커넥션이 없음.
waiting=1새로운 요청이 커넥션을 기다리고 있음.
request timed out after 30000ms30초 동안 기다렸지만 커넥션을 얻지 못해 타임아웃 발생.

Untitled

이 오류는 DB 커넥션 풀의 최대 크기인 10개가 모두 사용 중일 때 새로운 요청이 대기 상태로 오래 머물게 되어 발생한 문제입니다. EC2의 CPU 사용량이 낮고, RDS CPU 사용량이 높았던 것도 이러한 이유 때문입니다.


이로 인해 발생하는 문제

  • 동시 요청이 많아질수록 점점 더 많은 스레드가 대기 상태로 쌓임
  • 일정 수 이상의 요청이 들어오면 스레드 풀이 가득 차서 새로운 요청을 처리하지 못함
  • 결과적으로 응답 지연이 발생하고 서버가 제대로 응답하지 못하는 성능 저하 현상이 발생

RDS 성능 개선 해결책

  • 캐싱 적용 (Redis 등) → DB 조회 요청을 줄여서 커넥션 점유 시간을 줄임.
  • 쿼리 최적화 → EXPLAIN을 사용하여 실행 계획을 확인 후, 적절한 인덱스 추가.
  • RDS 인스턴스 업그레이드

이러한 해결책 중 제가 개발한 API 문제에선 쿼리 최적화가 부족하다고 판단하였습니다.


쿼리 최적화 및 병목 현상 해결 과정

우선적으로 문제가 되는 쿼리를 확인하여 분석해보았습니다.

문제 발생 쿼리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
select
    g1_0.GATHERING_ID,
    b1_0.BOOK_ID,
    b1_0.AUTHOR,
    ... 생략,
    g1_0.START_DATE,
    g1_0.VIEW_COUNT 
from
    GATHERING g1_0 
left join
    CHALLENGE c1_0 
        on c1_0.CHALLENGE_ID=g1_0.CHALLENGE_ID 
left join
    BOOK b1_0 
        on b1_0.BOOK_ID=g1_0.BOOK_ID 
left join
    IMAGE i1_0 
        on i1_0.IMAGE_ID=g1_0.IMAGE_ID 
where
    c1_0.START_DATE>='2025-01-20'
    and c1_0.END_DATE<='2025-03-30'
    and c1_0.READING_TIME_GOAL='ONE_HOUR'
order by
    g1_0.CREATED_TIME desc 
limit
    0, 10;


Explain Analyze 쿼리 실행 계획 분석

Untitled

  • 70만 데이터에 대해 Full Table Scan이 발생하며 응답 시간이 약 3362ms로 측정
  • 응답시간(Latency) : 3362ms

1차 해결책으로 초기 풀테이블 스캔하며 필터링하는 조건을 Index로 설정하여 다시 실행 결과를 분석해보았습니다.

1
CREATE INDEX idx_challenge_dates_goal ON CHALLENGE(READING_TIME_GOAL, START_DATE, END_DATE);


Index 적용 후 Explain Analyze 쿼리 실행 결과

Untitled

  • 70만 데이터 Full Table Scan
  • 응답시간(Latency) : 2072ms

Index를 적용하였지만 MySQL 옵티마이저는 인덱스를 활용하는 것 보다 Full Table Scan을 사용하는 것이 더 효율적이라고 판단했는지 Index 가 적용되지 않은 것을 확인할 수 있었습니다.


문제 파악 및 최적화 접근 방식

Untitled

문제의 원인은 Sorting(정렬 과정)에서 발생한 것으로 확인되었습니다.

처음에 MySQL 옵티마이저는 멀티 컬럼 인덱스를 사용하여 필터링 후 정렬을 시도할 계획이었으나, 실제로 인덱스를 사용하여 필터링한 후 정렬을 수행하는 것이 오히려 비효율적이라 판단하였습니다.

이에 따라 옵티마이저는 전체 테이블 스캔이 더 효율적일 것이라고 결론을 내렸습니다.

1
CREATE INDEX idx_challenge_end_date ON CHALLENGE(END_DATE);

멀티 컬럼 인덱스를 사용하지 않고 order by 조건에만 Index를 추가한다.


따라서 해결책으로는 코드 레벨에서 불필요한 ORDER BY 조건을 제거하고, 초기 필터링 과정에서 동시에 정렬을 수행하도록 개선하였습니다.


최종 실행 결과

Untitled

  • 이전 Sorting -> 필터링 과 조인이 진행된 후 실행
  • 개선된 Sorting -> 필터링하고 정렬 후 조인 실행
  • 3362ms(3.362초) -> 0.0703ms(0.7초) 로 단축시켰습니다.


개선된 API 테스트 측정 및 CPU, Memory 모니터링 결과


Throughput

Untitled

  • VUs : 49
  • 최적화 전 TPS : 1.2/s
  • 최적화 후 TPS : 47.8/s

TPS가 약 39.83배 증가하였습니다. Scale Up/Out 및 캐싱 없이 단순 쿼리 최적화만으로 성능이 높게 향상된 것을 확인할 수 있습니다.


Latency

Untitled

  • 최적화 전 Latency : 46.3/s
  • 최적화 후 Latency : 0 ~ 50/ms

쿼리 최적화를 통하여 Latency가 약 926배 감소하였습니다. 이는 시스템의 효율성이 높아져 한정된 시간 내에 더 많은 요청을 처리할 수 있음을 의미하며, 궁극적으로는 서비스의 확장성과 안정성 향상에 기여하였습니다.


HTTP Request Fail

Untitled

요청 실패 수가 이전 결과에 비해 10배 감소하여 다소 안정적인 모습이 확인되었습니다.


CPU, Memory 모니터링 측정 결과

Untitled

  • 최적화 전 RDS CPU 사용량 : 약 99.4 %
  • 최적화 후 RDS CPU 사용량 : 약 4.57 %

RDS CPU 사용률 90% 이상 감소하였습니다.


마무리하며

이번 테스트를 통해 병목현상이 발생하는 원인을 구체적으로 확인할 수 있었습니다.

병목현상의 해결 방안으로는 쿼리 최적화를 선택했지만, 실제 상황에서는 다양한 원인에 따라 적합한 해결책이 달라질 수 있습니다. 따라서 상황에 맞는 대응이 중요하다고 생각합니다.

또한, CPU와 메모리 모니터링을 활용해 병목현상의 정확한 원인을 파악하는 것은 굉장히 중요하다고 생각합니다.

This post is licensed under CC BY 4.0 by the author.

Trending Tags