Skip to content

Daewony/db-bottleneck-test

Repository files navigation

[PoC] 0.001초의 함정: DB 병목 현상 튜닝 프로젝트

“이 정도면 충분하다”는 타협은 시스템 장애의 시발점이 될 수 있습니다.

이 리포지토리는 포인트 조회 시스템의 “읽기(Read) 병목”을 데이터로 증명하고,
집계 테이블 + Redis 캐시를 도입해 해결한 전 과정을 담은 Proof of Concept (개념 증명) 프로젝트입니다.


⚙️ 실행 환경 (Test Environment)

항목 내용
Device MacBook Pro 16 (Apple M4 Pro)
Memory 24GB
주의 본 테스트는 고성능 환경에서 진행되었습니다. 저사양 환경에서는 병목이 훨씬 극적으로 나타날 수 있습니다.

🧨 1. “시한폭탄” 쿼리 (V1)

문제 요약

‘고래 사용자(user_id=1)’의 총 포인트를 조회할 때,
user_id에 인덱스가 있음에도 SUM() 연산으로 인해 인덱스 전체 스캔(Index Scan) 이 발생했습니다.

  • 증거 (EXPLAIN)
    • type: ref (인덱스 사용)
    • rows: 272,898
    • 즉, 단 1명의 데이터를 찾기 위해 27만 건을 스캔

데이터가 1억 건이면, 1억 건을 스캔하는 장애로 이어질 수 있습니다.

📸 [증거 이미지]
v1-explain-bomb


⚡ 2. “0.001초의 함정” (V2)

문제 해결 시도

user_point_summary라는 집계 테이블을 도입했습니다.

  • EXPLAIN 결과
    • type: const, rows: 1
    • 쿼리 속도: 0.001초

📸 [EXPLAIN 결과]
v2-explain-solution


그러나...

“1초에 5,000명이 동시에 호출한다면?”

k6를 이용한 부하 테스트(VUS 5000) 결과,
쿼리는 빠르지만 DB 커넥션 풀 병목이 발생했습니다.

  • 평균 응답 속도: 343ms (0.001초 → 0.34초로 340배 증가)
  • 요청 실패율: 0.62% (2,346건)
  • 환경: connectionLimit = 10

📸 [부하 테스트 결과]
v2-k6-bottleneck

쿼리가 아무리 빨라도, DB로 트래픽이 몰리면 병목은 여전했습니다.
이것이 바로 “0.001초의 함정입니다.


🧊 3. “DB 부하 0” 아키텍처 (V3)

해결 방법

DB 커넥션 병목이 원인임을 확인하고, Redis 캐시를 도입했습니다.

구분 처리 방식
조회(Read) Cache-Aside (Redis 먼저 조회)
적립(Write) Write-Aside (DB 성공 시 Redis 캐시 삭제)

실험 결과

📸 [Redis 처리 흐름 (DB Sleep)]
v3-db-sleep

📸 [k6 테스트 결과 (V3)]
v3-k6-solution

항목 V2 (DB 병목) V3 (Redis 해결)
초당 요청 수 12,453 13,623
평균 응답 속도 343ms 278ms
실패율 0.62% 0.75%
DB 상태 Busy Sleep (부하 0)

Redis를 도입함으로써, DB는 5,000명 공격에도 Sleep 상태를 유지했습니다.
병목이 DB에서 **API 서버로 “전가”**됨을 확인했습니다.


🚀 프로젝트 실행 방법 (How-to-Run)

1️⃣ 환경 준비

MySQL (schema 실행)

mysql < database.sql

Redis 실행

redis-server

패키지 설치

npm install

2️⃣ 환경 변수 설정

.env 파일 생성

  • DB_PASSWORD=YOUR_MYSQL_PASSWORD

3️⃣ 서버 실행 (2개의 터미널)

터미널 #1: Redis 실행(V3 상황)

redis-server

터미널 #2: API 서버 실행

node api_server.js

4️⃣ 부하 테스트 실행 (k6)

[V2] “0.001초의 함정” 재현

  • load_test.js 20번째 줄 TARGET_API = 'v2'
  • k6 run --vus 5000 --duration 30s load_test.js

[V3] “DB 부하 0” 증명

  • load_test.js 20번째 줄 TARGET_API = 'v3'
  • k6 run --vus 5000 --duration 30s load_test.js

💡 MySQL Workbench에서 SHOW PROCESSLIST를 반복 실행하면 DB가 Sleep 상태로 유지되는 것을 실시간으로 확인할 수 있습니다.

📚 배운 점 및 정리 (Lesson Learned)

'빠른 쿼리'가 '빠른 시스템'을 보장하지 않는다: 0.001초짜리 V2 쿼리도, '대규모 트래픽' 앞에서는 'Connection Pool' 병목으로 인해 avg=343ms로 급증, 0.62%의 '장애'를 유발함을 '데이터'로 증명했습니다.

'진짜 병목'을 찾아야 한다: EXPLAIN은 '느린 쿼리(I/O)'를 잡는 데 유용하지만, '0.001초의 함정(CPU/Connection)'은 k6와 SHOW PROCESSLIST 같은 '부하 테스트'와 '모니터링'을 통해서만 '증명'할 수 있었습니다.

병목은 '해결'하는 것이 아니라 '전가'하는 것이다: V3(Redis)는 'DB(셰프)'의 병목을 'API 서버(웨이터)'로 '성공적으로 전가'시켰습니다. 'DB 부하 0'을 달성한 대신, 'API 서버'의 '스케일 아웃(Scale-out)'이라는 '다음 문제'를 정의하게 되었습니다.

'감'이 아닌 '데이터'로 증명한다: 이 경험을 통해 '충분하다'는 감이 아닌, EXPLAIN과 k6라는 '데이터'로 시스템의 한계를 '증명'하고 끝까지 개선하는 개발자의 집요한 태도를 배웠습니다.

About

데이터베이스 병목 현상 테스트

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors