스탬피드 현상의 측정과 방어 이전에 캐시 다이어그램을 보여드리겠습니다.
아래 사진은 제 기존 캐시 설계입니다. 빠른 복구를 위해 Tier2로 설계했습니다.



아래 사진은 캐시 전략입니다.

아래 사진은 전체 캐시 흐름도 입니다.

캐시 스탬피드의 방법들에 대해 측정했습니다.
10분간 부하테스트를 실시했습니다.
글 상세 조회 30%
주간 인기글 상세 조회 14%
레전드 인기글 상세 조회 10%
실시간 인기글 목록 조회 20%
주간 인기글 목록 조회 14%
레전드 인기글 목록 조회 12%로 구성했습니다.
테스트 시작후 8분까지 선형적으로 램프업 해나가며 마지막 2분은 최대 부하를 유지했습니다.
최대 부하는 사용자 300명이 0.4초에 한 번씩 요청을 보냅니다.
커넥션 풀은 30으로 고정하였습니다.
| 캐시 | 최대 TPS | 평균 Latancy | 레디스 메모리 | 커넥션 획득 시간 | 비고 |
| 캐시 미적용 | 113 | 1451ms | 4.3MB | 최대 1000ms | |
| 캐시 적용 | 594 | 68ms | 4.2MB | 최대 125ms | 스탬피드 발생 |
| 분산락 적용 | 443 | 160ms | 2.5MB | 최대 2.5ms | 응답시간 하락 |
| Jitter 적용 | 474 | 167ms | 2.3MB | 최대 150ms | 스탬피드 해결 X |
| PER 적용 | 601 | 60ms | 2.3MB | 최대 0.7ms | 스탬피드 해결 O |
1. 캐시 미적용


30개의 커넥션 풀로 요청을 감당하지 못하는 모습입니다.
200개의 요청이 커넥션을 대기 중입니다.

커넥션 병목으로 인해 사용시간은 0.25초, 획득 시간은 후반부에 1초까지 대기하는 모습입니다.

GC카운트와 시간입니다. 10분간 0.4초 동안 STW가 발생합니다. 양호한 편이지만 캐시 적용시 더 줄어들 여지가 있습니다.

2. 캐시 적용

평균 Latency가 캐시 적용 전 보다 20배 이상 줄어들었습니다.

10분간 캐시 히트율입니다.
상세글 96퍼, 인기글목록들이 약 99퍼대입니다.
테스트환경이라 낙관적인 수치이며
운영환경에서는 약 90퍼 정도로 측정되고 있습니다.


GC 타임이 캐시 적용 전보다 400ms -> 119ms 로 3배 이상 감소했습니다.

부하 시작후 주간/레전드 인기글 목록 TTL 만료시 커넥션이 폭발했습니다. 캐시스탬피드를 의심할 수 있습니다.

TTL 만료직후 커넥션 사용시간과 획득 시간도 폭발하였습니다.

해당 지점의 로그
서로 다른 스레드들이 TTL만료로 인한 DB조회를 위해 같은 로직을 호출하고 있습니다.
계산해보니 2초간 주간 인기글 목록 5개를 가져오는 쿼리 10번 발생
레전드 인기글 목록 50개를 가져오는 쿼리 40번 발생 했습니다.
| 2025-11-02 23:09:52.510 |
2025-11-02 14:09:52.510 [http-nio-8080-exec-177] WARN j.b.d.p.a.service.PostCacheService - [CACHE_RECOVERY] START - type=LEGEND, thread=http-nio-8080-exec-177
|
|
| 2025-11-02 23:09:52.393 |
2025-11-02 14:09:52.391 [http-nio-8080-exec-171] WARN j.b.d.p.a.service.PostCacheService - [CACHE_RECOVERY] START - type=LEGEND, thread=http-nio-8080-exec-171
|
|
| 2025-11-02 23:09:52.085 |
2025-11-02 14:09:52.084 [http-nio-8080-exec-172] WARN j.b.d.p.a.service.PostCacheService - [CACHE_RECOVERY] START - type=LEGEND, thread=http-nio-8080-exec-172
|
|
| 2025-11-02 23:09:51.956 |
2025-11-02 14:09:51.956 [http-nio-8080-exec-173] WARN j.b.d.p.a.service.PostCacheService - [CACHE_RECOVERY] START - type=LEGEND, thread=http-nio-8080-exec-173
|
| 2025-11-02 23:09:44.396 |
2025-11-02 14:09:44.135 [http-nio-8080-exec-33] WARN j.b.d.p.a.service.PostCacheService - [CACHE_RECOVERY] START - type=WEEKLY, thread=http-nio-8080-exec-33
|
|||
| 2025-11-02 23:09:44.099 |
2025-11-02 14:09:44.097 [http-nio-8080-exec-61] WARN j.b.d.p.a.service.PostCacheService - [CACHE_RECOVERY] START - type=WEEKLY, thread=http-nio-8080-exec-61
|
|||
| 2025-11-02 23:09:44.001 |
2025-11-02 14:09:43.998 [http-nio-8080-exec-38] WARN j.b.d.p.a.service.PostCacheService - [CACHE_RECOVERY] START - type=WEEKLY, thread=http-nio-8080-exec-38
|
레디스 초당 히트율에서도 TTL 만료시점에 히트율이 급 감소하였습니다.

TTL 만료 시점 커맨드 콜이 급감한 것으로 캐시 스탬피드를 확정할 수 있습니다.

3. 분산락 (대기 3초, 락 제한 5초, 전용 커넥션 8)
Redisson를 사용하여 분산락을 구현했습니다.
이전 테스트로 보아 락 경합에 소요되는 시간은 길어야 2초인 것을 감안하여 시간을 설정했습니다.
기본설정은 대기시간과 소유시간이 길어 커스텀했습니다. 락을 획득하지 못한 스레드는 이전 캐시 값을 반환하도록 하였습니다.
락 소요에 어느정도 Latency가 크게 느려졌습니다.


락 대기와 동시성으로 인한 어느정도의 TPS와 Lantency의 하락은 예상했으나 너무 큰 폭으로 감소하였습니다.
커넥션 사진을 보면 TTL만료 시점에도 커넥션이 증가하지 않았고 커넥션 병목은 해결되었습니다.
또한 커넥션 획득 시간이 최대 125ms에서 2.5ms로 50배 감소하였습니다.

레디스 메모리 사용량은 락이 없을 때 보다 1.7 MB 줄었습니다.

아래는 테스트간의 응답시간입니다.
테스트 후반으로 갈수록 응답시간이 증가하나 TTL만료시점에 폭발하던 현상은 사라졌고 후반에 부하에 의한 응답시간 증가로 볼 수 있습니다.

한마디로 분산락을 적용하여 캐시 스탬피드와 DB 병목을 해결했습니다. DB 커넥션 대기는 50배 줄었으며 레디스 메모리 사용량도 30% 줄었습니다. 그러나 평균 Lantency가 2배 이상 상승하였습니다.
사실 처음에는 분산락 적용이 큰 오류가 발생했습니다. 위 결과는 그것을 해결하고 최적화 한 결과입니다.
https://jaeiktech.tistory.com/81에 관련 증상에 대한 분석과 해결이 있습니다.
4. Jitter 적용
캐시 저장시 -+30초의 랜덤 jitter추가
아무것도 적용하지 않은 캐시보다 TPS와 Latency가 감소했습니다. 예상한대로의 결과입니다.
현재 캐시 스탬피드의 중점은 2Tier 캐시에서 전달받은 주간/레전드 글의 id목록을 가지고 DB에서 조회해오는 것입니다.
수많은 캐시들을 대상으로 일정시간에 DB의 부하를 줄이는 방법으로는 유용하지만
제 프로젝트는 일반글의 상세내용은 캐싱하지 않으며 주간/레전드 글의 상세내용만 캐싱하고 그 해시들은 하나의 캐시로 취급하기에 TTL을 랜덤으로 한다고 한들 캐시 스탬피드의 영향을 받는 캐시는 단 2개기에 5분마다 일어나던 스탬피드 현상이 조금 시간이 변동될 뿐 캐시 스탬피드를 막을 수 없고 락이 없을 때와 동일한 현상이 일어납니다.
오히려 TTL이 변동되어 테스트 시간동안 두번의 스탬피드가 발생하여 테스트상으로는 락이 없는 상황보다 성능이 안좋게 나왔습니다.


동일하게 TTL 만료시간마다 커넥션이 폭발합니다. 락이 없는 상황에서는 1번의 스탬피드가 발생했지만 Jitter를 적용한 상황에서는 TTL이 변동되어 우연히 테스트 중 2번의 스탬피드가 발생했습니다.

커넥션 사용시간과 획득 대기 시간도 락이 없을 때와 동일합니다.

Redis의 초당 히트도 락이 없을 때와 동일한 양상을 보이며 두 번의 캐시 스탬피드가 발생한 것을 뒷받침 합니다.

5. PER 적용(타임아웃 10초, EXPIRY_GAP: 120초)
데이터가 만료되기 전에 미리 계산하여 캐시에 저장하여 캐시 스탬피드를 줄이는 기법을 사용했습니다.
TTL만기가 2분이하로 남은 캐시들은 확률적 선계산으로 캐시를 갱신하였습니다.
락 획득 실패시 기존 Stale Data 를 반환
타임아웃내 실패시 락 반환 데드락 방지 다음 스레드에 의해 DB 조회가 1번 더 일어나거나 덮어쓰기가 가능함
비동기 스레드가 분산락을 획득하게하여 요청에 병목이 생기지않도록 하였습니다.
스탬피드 현상을 막으면서 오히려 성능이 향상했습니다.


커넥션 풀을 확인해 보았을 때 TTL이 만기가 되었어도 커넥션이 폭발하지않았습니다.

분산락과 동일하게 캐시 스탬피드를 막았지만 분산락보다 훨씬 짧은 1ms미만의 커넥션 획득 대기 시간을 보여줍니다.

PER은 TTL이 다가올수록 확률적으로 캐시를 미리 갱신하도록 하는 기법입니다. 캐시가 만료되기 전에 갱신을 시도함으로써, 만료 시점에 대량의 요청이 한꺼번에 몰리는 것을 방지합니다.
분산락과 PER 모두 캐시 스탬피드를 해결하는데 사용할 수 있지만 제 프로젝트 같은 경우는 PER이 더 적합했습니다.
PER의 장점
1. 락을 아예 걸지 않기 때문에 락 관련 비용 자체가 사라집니다.
2. Redisson의 복잡한 내부 구현에 비해 구현이 간단합니다.
3. Redis 장애에 강합니다. 예를들어 Pub/Sub에 장애가 났다고 할 경우 Redisson 기반 분산락은 모든 요청이 타임아웃되고 서비스가 중단될 위험이 있지만 PER은 Get Set만 사용하기 때문에 비교적 안전합니다.
분산락의 장점
1. TTL을 정밀하게 통제해야할 때 : PER은 확률적 갱신이기 때문에 정밀하게 통제하기 어렵습니다.
2. 정합성이 중요할 때 : 절대 동시에 갱신되면 안되는 데이터 (예를들어 돈이 들어간 데이터, 무거운 캐시) 가 있을 때 좋습니다. PER은 확률 기반이기 때문에 반드시 1개만 갱신된다는 보장이 없습니다.
3. 반드시 새로운 데이터만 제공되어야할 때 : (예를들어 결제, 재고) PER은 지난 버전의 데이터가 사용자에게 전달될 수 있습니다.
제가 테스트한 실시간, 주간, 인기글, 게시글 상세 캐시는 정합성과 정밀한 TTL이 요구 되지 않습니다.
또한 글 수정시 Cache Invalidation를 적용하고 자연스럽게 캐시에 등록되게끔 해서 이미 새로운 데이터가 제공되게 하고 있습니다. 이와 같이 정밀성과 정합성이 필요로 하지 않을때는 PER이 제 현재 상황에서는 더 좋은방법이고 PER로 구현을 하였습니다.
다만 정합성이 중요한 서비스의 경우는 분산락이 더 적합하다고 생각합니다.
PER 테스트 결과
아래와 같이 캐시 스탬피드도 개선하고 동시에 더 좋은 성능을 지닌 아키텍처를 구현할 수 있었습니다.
| 캐시 | 최대 TPS | 평균 Latancy | 레디스 메모리 | 커넥션 획득 시간 | 비고 |
| 캐시 적용 | 594 | 68ms | 4.2MB | 최대 125ms | 스탬피드 발생 |
| 분산락 | 443 | 160ms | 2.5MB | 최대 2.5ms | 스탬피드 해결 |
| PER 적용 | 601 | 60ms | 2.3MB | 최대 0.7ms | 현재 채택 |
'트러블슈팅과 고민' 카테고리의 다른 글
| 데이터 분할로 친구 관계의 연쇄 작용 제어: 실시간 친구 추천 시스템 구축기 (0) | 2025.11.23 |
|---|---|
| 분산락에서 비동기 스레드풀 실행 거부 예외 식별과 해결 (0) | 2025.11.19 |
| 멀티 스레드 환경에서 공유 세션으로 발생한 동시성 경합과 커넥션 누수 분석 및 해결 (0) | 2025.10.19 |
| 소셜 로그인/회원가입 반환 프로토콜 재설계 (0) | 2025.10.03 |
| 헥사고날 아키텍처에서 신고(Report) 엔티티의 도메인 위치 딜레마 (1) | 2025.09.05 |