Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ dependencies {

// Virtual Thread 전용 지표
implementation 'io.micrometer:micrometer-java21'

// 서킷 브레이커
implementation 'io.github.resilience4j:resilience4j-circuitbreaker:2.4.0'
}

tasks.named('test') {
Expand Down
5 changes: 4 additions & 1 deletion docs/01-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ URL Shortener는 긴 URL을 짧은 코드로 변환하고, 단축 URL 요청이
- 캐시 데이터가 유실돼도 MySQL을 통해 원본 URL을 복구할 수 있어야 한다.
- 단일 App 장애 시 Nginx가 다른 App 인스턴스로 GET 요청을 전환한다.
- 현재 서비스 경로의 단일 장애 지점은 Nginx와 MySQL이며, Redis 장애는 MySQL Fallback으로 기능을 유지한다.
- Redis 장애가 반복되면 Circuit Breaker가 Redis 호출을 차단해 반복적인 Timeout을 줄인다.

### 확장성

Expand Down Expand Up @@ -115,6 +116,7 @@ URL Shortener는 긴 URL을 짧은 코드로 변환하고, 단축 URL 요청이
* Redis 장애 시 MySQL Fallback 실험
* 다중 애플리케이션 인스턴스 및 Nginx Failover 실험
* 다중 인스턴스 Snowflake nodeId 검증
* Redis Circuit Breaker 적용 및 장애 구간 응답 지연 비교

### 제외

Expand All @@ -141,4 +143,5 @@ URL Shortener는 긴 URL을 짧은 코드로 변환하고, 단축 URL 요청이
* [ ] Redis 장애 시 대응 방법을 확인했다.
* [ ] 코드 생성 전략별 장단점과 트레이드오프를 설명할 수 있다.
* [ ] 다중 인스턴스에서 서로 다른 nodeId로 단축 코드 유일성을 검증했다.
* [ ] 단일 App 장애 시 다른 인스턴스로 요청이 전환되는 것을 확인했다.
* [ ] 단일 App 장애 시 다른 인스턴스로 요청이 전환되는 것을 확인했다.
* [ ] Redis 장애 시 Circuit Breaker를 통해 반복적인 Redis Timeout을 줄였다.
24 changes: 19 additions & 5 deletions docs/03-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ POST 생성 요청은 처리 성공 여부가 불분명한 상태에서 재시

### URL 리다이렉트

1. `shortCode`로 Redis를 조회한다.
1. Circuit Breaker가 CLOSED이면 `shortCode`로 Redis를 조회한다.
2. Cache Hit이면 원본 URL을 반환한다.
3. Cache Miss이면 MySQL의 `short_code` 인덱스로 조회하고 Redis에 저장한다.
4. Redis 조회에 실패하면 MySQL로 Fallback한다.
5. 원본 URL을 `302 Found`로 반환한다.
5. Redis 실패가 반복돼 Circuit Breaker가 OPEN되면 Redis 호출을 생략하고 바로 MySQL을 조회한다.
6. Redis 복구가 확인되면 Circuit Breaker가 CLOSED로 돌아가 Cache Aside 경로를 다시 사용한다.
7. 원본 URL을 `302 Found`로 반환한다.

### 실패 흐름

Expand Down Expand Up @@ -249,15 +251,26 @@ Hash와 난수 방식에서는 `short_code`에 Unique Constraint를 적용해
| DB 장애 | URL 생성 및 조회 불가 | Connection 오류, Actuator | 503 반환, DB 복구 |
| Prometheus 장애 | 지표 수집 불가 | Scrape 상태 | 컨테이너 재시작 |
| Grafana 장애 | 대시보드 조회 불가 | 컨테이너 상태 | 컨테이너 재시작 |
| Redis 장애 | 응답 지연 및 DB 부하 증가 | Cache Error, Fallback 지표 | MySQL Fallback 후 자동 복귀 |
| Redis 장애 | 응답 지연 및 DB 부하 증가 | Cache Error, Fallback 지표 | Circuit Breaker OPEN 후 MySQL Fallback |

초기 구조에서는 DB 장애 시 요청을 처리할 대체 저장소가 없다.

Redis GET에 실패하면 MySQL로 Fallback한다.

Redis 장애가 확인된 요청에서는 Redis SET을 생략해 Timeout이 중복되지 않도록 했다.
Redis 장애가 확인된 요청에서는 Redis SET을 생략해
한 요청에서 Redis Timeout이 중복되지 않도록 한다.

이는 기능 지속을 위한 Graceful Degradation이며 Redis 자체의 고가용성을 구성한 것은 아니다.
Redis 실패가 반복돼 Circuit Breaker의 실패율 임계값을 초과하면
Circuit이 OPEN 상태로 전환된다.

OPEN 상태에서는 Redis GET 자체를 호출하지 않고
즉시 MySQL로 Fallback한다.

일정 시간이 지난 뒤 제한된 요청으로 Redis 복구 여부를 확인하고,
정상 응답이 확인되면 다시 Cache Aside 경로로 복귀한다.

이는 기능 지속과 장애 구간의 반복 Timeout을 줄이기 위한
Graceful Degradation이며 Redis 자체를 이중화한 것은 아니다.

App1 장애 시 Nginx가 App2를 통해 GET 리다이렉트 요청을 계속 처리한다.

Expand Down Expand Up @@ -340,6 +353,7 @@ Client
* 코드 생성 재시도 횟수
* Redis Cache Error 수
* MySQL Fallback 수
* Redis Circuit Breaker Rejected 수

### Logs

Expand Down
105 changes: 104 additions & 1 deletion docs/04-experiment.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,107 @@ App1 재기동 후 Healthy 상태로 복구됐으며
시스템 전체의 SPOF를 제거한 것은 아니다.
이번 실험은 애플리케이션 계층의 단일 장애 지점을 개선하는 데 범위를 한정한다.

## 19. Redis Circuit Breaker

Redis 장애 시 MySQL Fallback을 적용해 서비스 가용성은 유지했지만,
각 요청이 Redis Timeout을 기다린 뒤 MySQL로 전환되면서
장애 구간 p95가 약 200ms까지 증가하는 문제가 남았다.

Redis 장애가 지속될 때 반복적인 Timeout을 줄이기 위해
Redis GET 경로에 Circuit Breaker를 적용했다.

### 설정

| 항목 | 값 |
|---|---:|
| Sliding Window | 최근 10건 |
| 최소 호출 수 | 5건 |
| 실패율 임계값 | 50% |
| OPEN 유지 시간 | 5초 |
| HALF_OPEN 시험 호출 | 3건 |
| Redis Timeout | 200ms |

```text
CLOSED
→ Redis 호출 허용

Redis 실패율 임계값 초과
→ OPEN
→ Redis 호출 차단
→ MySQL Fallback

OPEN 5초 경과
→ HALF_OPEN
→ Redis 시험 호출

Redis 복구 확인
→ CLOSED
→ Cache Aside 경로 복귀
```

### 실험 조건

Fallback-only 실험과 동일한 조건으로 측정했다.

| 항목 | 조건 |
|---|---|
| VU | 100 |
| 실행 시간 | 120초 |
| 정상 구간 | 0~30초 |
| Redis 중지 | 30~60초 |
| 복구 관찰 | 60~120초 |
| Redis Timeout | 200ms |
| HikariCP | 최대 10개 |

### 결과

Redis 장애 직후에는 실제 Redis 호출 실패가 발생하지만,
실패가 누적되면서 Circuit Breaker가 OPEN 상태로 전환된다.

OPEN 이후에는 Redis 호출 자체가 차단되고
요청은 즉시 MySQL Fallback 경로로 처리됐다.

Grafana에서 Circuit Breaker Rejected는 최대 약 5.7K req/s,
MySQL Fallback은 약 5.8K req/s까지 증가했으며,
5xx 오류는 발생하지 않았다.

장애 구간의 p95는 초기 약 35ms를 기록한 뒤
대체로 20~30ms 수준으로 유지됐다.
Fallback만 적용했을 때 장애 구간 p95가 약 200ms였던 것과 비교하면,
반복적인 Redis Timeout 대기가 크게 감소했다.

Redis 복구 후에는 Circuit Breaker Rejected,
MySQL Fallback과 DB Lookup이 다시 0으로 감소했고
Cache Hit이 증가하면서 정상 조회 경로로 복귀했다.

### Fallback-only 비교

| 지표 | Fallback only | Circuit Breaker |
|---|---:|---:|
| VU | 100 | 100 |
| Redis Timeout | 200ms | 200ms |
| Redis 장애 시간 | 30초 | 30초 |
| 장애 구간 p95 | 약 200ms | 약 20~30ms |
| 장애 구간 p99 | 약 220ms | 약 40~80ms |
| 5xx 오류율 | 0% | 0% |
| Redis 호출 | 장애 중 반복 | OPEN 이후 차단 |
| MySQL Fallback | 발생 | 발생 |
| Redis 복구 후 Cache 복귀 | 성공 | 성공 |

Fallback만 적용했을 때는 Redis 장애가 전체 서비스 장애로
이어지는 것은 막을 수 있었지만,
각 요청이 Redis Timeout을 기다린 뒤 DB를 조회하는 문제가 남았다.

Circuit Breaker 적용 후에는 장애를 감지한 뒤 Redis 호출을 차단해
MySQL을 바로 조회하도록 변경했다.

따라서 Fallback은 Redis 장애 시 기능을 유지하고,
Circuit Breaker는 장애가 지속되는 동안 반복적인 Redis 호출과
Timeout 비용을 줄이는 역할을 한다.

#### Grafana 측정 결과

![Redis Circuit Breaker](images/redis-circuit-breaker-100vu.png)

## 19. 실험 한계

Expand All @@ -635,6 +736,8 @@ App1 재기동 후 Healthy 상태로 복구됐으며
- Redis 장애 실험은 프로세스 중지만 재현했으며 네트워크 지연과 패킷 손실은 검증하지 않았다.
- Redis 장애 중 더 높은 부하에서는 MySQL과 커넥션 풀이 포화될 수 있다.
- 다중 인스턴스 실험은 로컬 Docker 환경에서 App 2개와 Nginx 1개로 수행했으며, 실제 독립 서버 장애를 재현한 것은 아니다.
- Circuit Breaker의 상태 전환은 Rejected 지표를 통해 간접적으로 확인했으며,
CLOSED, OPEN, HALF_OPEN 상태 자체를 별도 메트릭으로 기록하지 않았다.

## 20. 후속 실험

Expand All @@ -648,5 +751,5 @@ App1 재기동 후 Healthy 상태로 복구됐으며
- [x] Sequence ID + Base62, Hash, Snowflake ID + Base62 비교
- [x] Redis 장애 시 MySQL Fallback 및 자동 복구 검증
- [x] 다중 애플리케이션 인스턴스와 장애 전환 검증
- [ ] Circuit Breaker를 통한 Redis 장애 구간 Timeout 감소
- [x] Circuit Breaker를 통한 Redis 장애 구간 Timeout 감소
- [ ] Redis Sentinel 또는 Cluster 기반 고가용성 구성
Binary file added docs/images/redis-circuit-breaker-100vu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading