-
Notifications
You must be signed in to change notification settings - Fork 1
[문제해결] 무중단 배포 설정하기 #103
Copy link
Copy link
Open
Labels
기능구현This will not be worked onThis will not be worked on
Description
현재 인프라 구성
[ALB]
├─► EC2 App 1 (Spring Boot)
└─► EC2 App 2 (Spring Boot)
│
├─► EC2 Redis (공유, 단일 인스턴스)
└─► RDS (공유, 단일 인스턴스)
- App 서버 2대가 Redis와 RDS를 공유
/health엔드포인트로 헬스체크 가능
지금까지 구현을 고려했을 때 발생할 수 있는 문제
Warmup Runner 변경: 재시작 직후 Redis가 DB 기반으로 초기화되기 전에 요청이 들어오면 score 0으로 응답
선택 전략: 롤링 배포
EC2 2대 환경에서 Blue-Green은 동일한 스펙의 인스턴스 2대를 추가해야 해서 비용 부담이 있다.
대신 ALB의 연결 드레이닝(Connection Draining) 기능을 활용한 롤링 배포로 무중단을 달성한다.
초기: ALB → [App1 v1] [App2 v1]
1단계: ALB → [App1 드레이닝] [App2 v1] ← App1 신규 요청 차단
2단계: ALB → [App1 v2 기동] [App2 v1] ← App1 /health 확인 후 재등록
3단계: ALB → [App1 v2] [App2 드레이닝] ← App2 신규 요청 차단
완료: ALB → [App1 v2] [App2 v2]
배포 전 선행 작업: DB 마이그레이션 분리
신버전 배포 전에 마이그레이션을 먼저 적용해야 구버전 코드와 공존이 가능하다.
-- V{N}__add_like_count_to_booth.sql
ALTER TABLE booth ADD COLUMN like_count INT NOT NULL DEFAULT 0;DEFAULT 0으로 추가하면 구버전 코드는 해당 컬럼을 무시하고 계속 동작- 마이그레이션 적용 완료 확인 후 앱 배포 진행
배포 절차 (단계별)
Phase 1 — DB 마이그레이션
1. RDS 스냅샷(백업) 생성
2. Flyway 마이그레이션 스크립트 실행 (like_count 컬럼 추가)
3. 구버전 앱 2대 정상 동작 확인
Phase 2 — App1 교체
1. ALB Target Group에서 App1 deregister
→ 드레이닝 대기 (기존 연결 처리 완료, 예: 30초)
2. App1에 신버전 jar 배포 후 재기동
→ Warmup Runner 실행: DB → Redis 초기화 (like_count 복원)
3. GET /health → 200 확인
4. ALB Target Group에 App1 재등록
5. ALB가 App1으로 트래픽 보내는지 확인 (1~2분 모니터링)
Phase 3 — App2 교체
6. ALB Target Group에서 App2 deregister
7. App2에 신버전 jar 배포 후 재기동
→ Warmup Runner: Redis가 이미 App1이 초기화해둔 상태
→ ADD NX 옵션 또는 기존 값이 있으면 스킵하도록 처리 권장
8. GET /health → 200 확인
9. ALB Target Group에 App2 재등록
Warmup Runner: App2 재기동 시 Redis 덮어쓰기 문제
App1이 DB 값으로 Redis를 초기화한 뒤, App2가 재기동하면 다시 Redis를 초기화한다.
이 사이에 사용자 좋아요 요청이 들어와 Redis score가 올라간 상태라면 App2의 Warmup이 덮어써버릴 수 있다.
해결책: Warmup 시 현재 Redis 값보다 DB 값이 클 때만 적용
@Override
public void run(ApplicationArguments args) {
List<Booth> booths = boothRepository.findAll();
booths.forEach(booth -> {
String key = "like:ranking";
String member = String.valueOf(booth.getId());
Double currentScore = redisTemplate.opsForZSet().score(key, member);
// Redis에 값이 없거나 DB 값이 더 크면 DB 값으로 초기화
if (currentScore == null || currentScore < booth.getLikeCount()) {
redisTemplate.opsForZSet().add(key, member, booth.getLikeCount());
}
});
}스케줄러 중복 실행 처리
2대가 동시에 syncLikeCountToDB()를 실행하면 RDS에 동시 업데이트가 발생한다.
Redis 값을 그대로 쓰는 멱등 연산이므로 정합성 문제는 없지만, 불필요한 DB 부하를 줄이려면 ShedLock을 추가한다.
<!-- build.gradle -->
implementation 'net.javacrumbs.shedlock:shedlock-spring:5.x.x'
implementation 'net.javacrumbs.shedlock:shedlock-provider-jdbc-template:5.x.x'-- Flyway로 함께 추가
CREATE TABLE shedlock (
name VARCHAR(64) NOT NULL,
lock_until TIMESTAMP(3) NOT NULL,
locked_at TIMESTAMP(3) NOT NULL,
locked_by VARCHAR(255) NOT NULL,
PRIMARY KEY (name)
);@Scheduled(fixedDelay = 5 * 60 * 1000)
@SchedulerLock(name = "boothLikeSync", lockAtMostFor = "4m", lockAtLeastFor = "1m")
public void syncLikeCountToDB() {
// 2대 중 lock을 획득한 1대만 실행
}롤백 계획
| 상황 | 롤백 방법 |
|---|---|
| App1 기동 실패 | ALB에 App1 재등록 안 하면 됨 — App2가 트래픽 전담, 무중단 |
| App2 기동 실패 | App1이 이미 정상 운영 중 — App2만 구버전으로 롤백 후 재기동 |
| 마이그레이션 오류 | RDS 스냅샷으로 복원 (배포 전 백업 필수) |
| Redis 데이터 유실 | Warmup Runner 재실행으로 DB → Redis 복원 가능 |
논의 필요 사항
- ALB 드레이닝 시간: 현재 설정값 확인 필요 — 짧으면 처리 중 요청이 끊길 수 있음 (권장: 30초)
- 스케줄러 주기: flush 주기 = 최대 데이터 손실 허용 시간 — 4.md 논의 사항과 연계
- ShedLock 도입 여부: 부하가 크지 않다면 생략 가능, 중복 실행 자체는 정합성에 무해
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
기능구현This will not be worked onThis will not be worked on