- Published on
MySQL CPU 100%? Redis 핫키·슬로우로그 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 MySQL CPU가 100%로 고정될 때, 본능적으로 EXPLAIN부터 뜯어보게 됩니다. 하지만 운영에서 자주 보는 패턴은 MySQL이 원인이 아니라 결과인 경우입니다. 특히 Redis 캐시가 “잘 동작하는 것처럼 보이지만” 특정 키에 부하가 쏠리거나(핫키), 캐시 미스 폭주가 발생하면, 그 트래픽이 고스란히 MySQL로 흘러가 CPU를 태웁니다.
이 글은 다음 흐름으로 문제를 좁힙니다.
- MySQL CPU 100%가 Redis 문제로 증폭되는 메커니즘
- Redis
SLOWLOG로 느린 명령과 패턴 찾기 - 핫키(Hot Key) 탐지 및 분산 전략
- 캐시 스탬피드, TTL 동기화, 락 설계
- 튜닝 후 검증 체크리스트
참고로, CPU 폭증이라는 현상 자체는 애플리케이션 스레딩/동시성 이슈와도 연결됩니다. 비슷한 관점의 진단 프레임은 Spring Boot 3 가상스레드 도입 후 CPU 폭증 원인도 함께 보면 도움이 됩니다.
1) 왜 Redis 문제가 MySQL CPU 100%로 보이나
대표적인 연쇄 반응은 아래 3가지입니다.
1-1. 핫키로 Redis가 병목이 되면, 캐시 히트율이 떨어진다
핫키는 초당 수만 번 조회되는 단일 키(예: user:123:profile, rank:global)를 말합니다. 단일 키에 부하가 몰리면 Redis는 단일 스레드 이벤트 루프 특성상 해당 명령 처리로 큐가 밀립니다. 그 결과:
- 같은 키를 기다리던 요청들이 타임아웃
- 애플리케이션은 “캐시 실패”로 판단하고 DB로 폴백
- MySQL로 동시 쿼리가 폭증하며 CPU 100%
1-2. 캐시 스탬피드(Stampede): TTL 만료 시점에 동시 미스
TTL이 동일하게 설정된 키들이 같은 시점에 만료되면, 한 순간에 수천 요청이 동시에 캐시 미스를 내고 DB를 두드립니다.
- Redis는 정상이어도 MySQL이 터짐
- 혹은 Redis에서
GET은 빠르지만, DB 재생성 로직이 비싸서 MySQL이 터짐
1-3. Redis 명령 자체가 비싸서 전체 레이턴시가 상승
예: 큰 SORT, 큰 범위 ZRANGE, 무거운 Lua 스크립트, 큰 Hash에 대한 HGETALL 등. 이런 명령이 느려지면 애플리케이션 타임아웃이 증가하고, 재시도/폴백 패턴이 DB 부하를 키웁니다.
2) 1차 확인: MySQL이 “원인”인지 “피해자”인지
MySQL부터 보더라도, 목표는 “누가 쿼리 폭주를 만들었는지”를 확인하는 것입니다.
2-1. MySQL에서 폭주 쿼리 형태 확인
-- 실행 중인 쿼리 확인
SHOW FULL PROCESSLIST;
-- InnoDB 상태(락, 대기, 스레드 등)
SHOW ENGINE INNODB STATUS;
여기서 같은 형태의 SELECT ... WHERE id = ?가 폭증한다면 캐시 미스/핫키 가능성이 큽니다.
2-2. 애플리케이션 지표에서 “캐시 히트율”과 “폴백 비율” 확인
- Redis
GET성공률(히트율) - Redis 타임아웃/에러율
- DB 폴백 호출 수
- 동일 요청에 대한 재시도 횟수
캐시 히트율이 갑자기 떨어지거나 Redis 타임아웃이 증가했다면, MySQL 최적화만으로는 근본 해결이 어렵습니다.
3) Redis SLOWLOG로 느린 명령부터 잡기
Redis는 느린 명령을 SLOWLOG로 기록할 수 있습니다. 핫키든 스크립트든, “비싼 명령”이 먼저 드러납니다.
3-1. SLOWLOG 활성화/설정
# 10ms 이상 걸리는 명령을 슬로우로그로
redis-cli CONFIG SET slowlog-log-slower-than 10000
# 최근 1024개까지 보관
redis-cli CONFIG SET slowlog-max-len 1024
운영에서는 설정 변경이 부담될 수 있으니, 가능한 구성 관리(예: redis.conf)로 반영하고 재기동 전략까지 포함해 계획하세요.
3-2. 슬로우로그 조회
# 최근 20개
redis-cli SLOWLOG GET 20
# 길이 확인
redis-cli SLOWLOG LEN
# 초기화(주의)
redis-cli SLOWLOG RESET
슬로우로그에서 확인할 것:
- 어떤 커맨드가 느린가:
EVAL,ZRANGE,HGETALL,MGET,KEYS등 - 어떤 키 패턴이 반복되는가:
rank:*,session:*등 - 느린 시간이 “간헐적”인지 “지속적”인지
3-3. LATENCY DOCTOR로 이벤트 루프 지연 확인
redis-cli --latency-history -i 1
redis-cli LATENCY DOCTOR
이벤트 루프 지연이 보이면, 단일 요청이 느리다기보다 서버가 밀리는 상황일 수 있습니다(핫키, 큰 응답, 네트워크 백프레셔 등).
4) 핫키 탐지: 어떤 키가 트래픽을 독점하는가
핫키는 슬로우로그에 반드시 잡히지 않습니다. GET 자체는 빠르기 때문입니다. 대신 “특정 키가 전체 QPS의 상당 부분을 차지”하는지 찾아야 합니다.
4-1. Redis MONITOR는 운영에서 신중히
redis-cli MONITOR
MONITOR는 모든 명령을 스트리밍하므로 부하가 큽니다. 운영에서는 짧게, 트래픽이 낮은 시간, 혹은 복제본에서 제한적으로 사용하세요.
4-2. redis-cli --hotkeys (Redis 버전/빌드에 따라 지원)
redis-cli --hotkeys
이 기능은 내부적으로 샘플링을 통해 “자주 접근되는 키 후보”를 보여줍니다. 결과가 나오면 다음 단계는 “왜 그 키가 핫키가 되었는지”를 애플리케이션 관점에서 분석하는 것입니다.
4-3. 애플리케이션 레벨에서 키별 카운팅(가장 확실)
Redis 서버만으로는 정확한 키별 QPS를 항상 얻기 어렵습니다. 실전에서는 아래처럼 앱에서 샘플링/집계를 넣는 편이 빠릅니다.
- 키 prefix별 카운터
- 상위 N개 키만 로그
- 특정 API에서 생성되는 키를 태깅
예: Java/Kotlin 의사코드
String key = "user:" + userId + ":profile";
if (ThreadLocalRandom.current().nextInt(1000) == 0) {
metrics.counter("redis.key.sample", "key", key).increment();
}
redis.get(key);
5) 핫키 튜닝 전략 6가지
핫키는 “Redis 성능” 문제가 아니라 “키 설계” 문제인 경우가 많습니다.
5-1. 키 샤딩(분산)으로 단일 키 집중을 쪼개기
예: rank:global 하나에 모든 조회가 몰리면, rank:global:{0..15}로 쪼갭니다.
rank:global:0
rank:global:1
...
rank:global:15
조회 시에는 샤드 하나만 읽는 구조로 바꾸거나, 여러 샤드를 합산해야 한다면 “읽기 경로”를 재설계해야 합니다(예: 미리 합쳐둔 스냅샷 키를 별도로 둠).
5-2. 캐시 계층 추가: 로컬 캐시(near-cache)
핫키가 “모든 인스턴스에서 동일하게 반복 조회”되는 값이라면, Redis 앞단에 로컬 캐시(Caffeine 등)를 두면 Redis QPS 자체가 크게 줄어듭니다.
- TTL 짧게(예: 1~3초)
- 값이 크지 않고, 정합성 요구가 낮을 때 특히 효과적
5-3. 큰 값은 쪼개거나 압축하고, 불필요한 HGETALL 금지
핫키가 큰 payload를 반환하면 네트워크/직렬화 비용이 커집니다.
- 큰 Hash에서 일부 필드만 필요하면
HMGET사용 - JSON 통째 저장 대신 필드 분리 또는 압축
- 가능하면 응답 크기를 줄이기
5-4. 파이프라이닝/배치로 왕복 감소
동일 요청에서 Redis를 여러 번 호출하면 RTT가 누적됩니다.
MGET/HMGET활용- 클라이언트 파이프라이닝
예: redis-cli 파이프라이닝 예시
(echo "PING"; echo "GET a"; echo "GET b") | redis-cli --pipe
5-5. 읽기/쓰기 분리(복제본 활용) 단, 핫키는 여전히 주의
읽기 부하가 큰 경우 복제본에서 읽도록 구성하면 도움이 됩니다. 하지만 핫키가 복제본 한 대에만 몰리면 동일한 문제가 반복됩니다.
- 클라이언트의 read preference
- 복제 지연 시 정합성 요구 확인
5-6. TTL 지터(jitter)로 만료 시점 분산
동일 TTL을 주면 만료가 동시에 터집니다. TTL에 랜덤을 섞어 만료를 분산합니다.
import random
base = 300 # 5분
jitter = random.randint(0, 60) # 0~60초
ttl = base + jitter
r.setex(key, ttl, value)
6) 캐시 스탬피드 방지: 락·단일 플라이트(single-flight)
핫키의 또 다른 얼굴은 “만료 순간 DB 재생성 폭주”입니다. 해결은 한 번만 DB를 치고 나머지는 기다리게 만드는 것입니다.
6-1. 간단한 분산 락(SET NX EX)
# 락 획득: 성공하면 OK
redis-cli SET lock:user:123 1 NX EX 3
패턴:
- 캐시 miss
- 락 시도(
SET ... NX EX) - 락 성공한 1개 요청만 DB 조회 후 캐시 채움
- 나머지는 짧게 sleep 후 재조회(또는 구독/알림)
주의점:
- 락 TTL은 DB 조회 최악 시간을 커버하되 너무 길지 않게
- 락 해제는 소유자 검증이 필요(토큰 기반)
6-2. 소유자 검증 포함한 안전한 락 해제(Lua)
-- unlock.lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
호출 예시(인라인 코드로 부등호 방지): EVAL을 사용합니다.
# KEYS[1]=lock key, ARGV[1]=token
redis-cli EVAL "$(cat unlock.lua)" 1 lock:user:123 8f3a-token
EVAL이 슬로우로그에 자주 등장한다면 스크립트 자체를 줄이거나, 키/값 크기와 호출 빈도를 낮추는 게 우선입니다.
6-3. stale-while-revalidate(낡은 값 허용)로 DB 보호
정합성이 엄격하지 않다면, 만료 직후에도 잠깐은 낡은 값을 제공하고 백그라운드에서 갱신합니다.
- 사용자 경험: 갑작스런 지연 감소
- DB 보호: 동시 재생성 방지
구현은 “소프트 TTL” 필드를 값에 포함하거나, data key와 meta key를 분리하는 방식이 흔합니다.
7) Redis 튜닝이 MySQL CPU를 낮추는지 검증하는 방법
튜닝 후에는 “Redis가 빨라졌다”가 아니라 “MySQL CPU 100%가 재발하지 않는다”를 확인해야 합니다.
7-1. 확인해야 할 핵심 지표
- Redis:
instantaneous_ops_per_sec,connected_clients,blocked_clients - Redis:
keyspace_hits,keyspace_misses(히트율) - Redis:
LATENCY DOCTOR경고 유무 - MySQL: QPS, CPU,
Threads_running, 쿼리 평균 시간 - 애플리케이션: Redis 타임아웃/재시도율, DB 폴백 비율
INFO로 기본 지표를 확인할 수 있습니다.
redis-cli INFO stats
redis-cli INFO clients
redis-cli INFO commandstats
7-2. 재발 방지를 위한 운영 가드레일
- 캐시 키 설계 리뷰 체크리스트(단일 키에 트래픽 집중 가능성)
- TTL 지터 기본 적용
- 캐시 미스 시 DB 폴백에 rate limit 적용
- Redis 타임아웃 시 “즉시 DB 폴백”이 아니라 단계적 완화(서킷 브레이커)
장애가 503으로 드러나는 경우도 많으니, 증상 기반으로 함께 보면 좋은 글은 Spring Boot 3+ Tomcat 503 원인별 진단·해결입니다.
8) 실전 시나리오: 핫키 하나가 MySQL을 태우는 과정과 처방
상황
- 키:
product:ranking:today - API: 메인 페이지 진입마다 랭킹 조회
- Redis TTL: 60초(모든 인스턴스 동일)
증상
- 매 60초마다 Redis miss가 동시 발생
- DB에서 랭킹 집계 쿼리 폭주
- MySQL CPU 100%, 응답 지연 후 타임아웃, 재시도로 더 악화
처방
- TTL 지터: 60초
+0~20초 랜덤 - single-flight: miss 시 락 획득한 1개만 DB 집계
- stale-while-revalidate: 집계가 비싸면 낡은 값 허용
- 로컬 캐시 1~2초: 핫 트래픽 흡수
이 4가지만 적용해도 “매 분마다 터지는” 패턴이 사라지는 경우가 많습니다.
9) 체크리스트(요약)
- Redis
SLOWLOG에서 느린 명령/키 패턴을 먼저 확인했는가 - 핫키가 슬로우로그에 안 잡힐 수 있음을 알고 별도 탐지(샘플링/
--hotkeys)를 했는가 - TTL 지터로 만료 동시성을 분산했는가
- 캐시 miss 재생성에 single-flight 또는 락을 적용했는가
- 큰 값/큰 응답을 줄이고
HGETALL같은 과도한 명령을 피했는가 - 파이프라이닝/배치로 RTT를 줄였는가
- 튜닝 후 MySQL QPS/CPU와 Redis 히트율이 함께 개선되는지 확인했는가
마무리
MySQL CPU 100%는 종종 “DB가 느려서”가 아니라 “DB로 몰아넣는 구조” 때문에 발생합니다. Redis 핫키와 캐시 스탬피드는 그 구조적 원인의 대표 사례입니다.
해결의 핵심은 Redis 설정 몇 개를 바꾸는 것이 아니라, 키 설계(분산), 만료 전략(TTL 지터), 재생성 제어(single-flight), 응답 크기/호출 수 최적화를 함께 적용하는 것입니다. 이 순서대로 점검하면 MySQL과 Redis 모두 안정화되는 경우가 많습니다.