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

Watchers

Forks

Releases

No releases published

Packages

No packages published