ShedLock + Redis lock provider 선택. DaemonScheduler.run을 @SchedulerLock(name='daemon-runner', lockAtMostFor=PT15M, lockAtLeastFor=PT30S) 로 보호. RollingUpdate 시 두 파드 공존 중 YouTube/OCI 중복 호출 차단. 설계서: docs/design/335-daemon-distributed-lock/README.md (Approved, 12개 섹션) Refs: #335 (Architect 단계)
5.4 KiB
5.4 KiB
설계서: 데몬 스케줄러 분산 락 (#335)
상태: Approved 작성: [AI] Architect · 최종수정: 2026-06-15 추적성 — Redmine: #335 · 부모: #275 (현행화 backend-daemon, 09-Done) · 구현 파일:
backend-java/build.gradle,backend-java/src/main/java/com/tasteby/TastebyApplication.java,backend-java/src/main/java/com/tasteby/config/ShedLockConfig.java(신규),backend-java/src/main/java/com/tasteby/service/DaemonScheduler.java· 테스트: 수동 (롤링 업데이트 시 두 파드 공존 시뮬레이션)
1. 목적 (Why)
OKE 운영에서 backend Pod 1개로 동작하지만 RollingUpdate(maxSurge>0) 시 신·구 Pod이 잠시 공존. 또한 dev(PM2)와 운영이 같은 Oracle ATP를 공유 — 이미 DAEMON_ENABLED 플래그로 dev 폴링은 차단했지만, 운영 자체에서 두 Pod이 같은 30초 주기로 scanAllChannels를 호출하면 YouTube/OCI GenAI 중복 호출 + 동일 영상 두 번 처리 + 봇 감지 위험. ShedLock으로 한 인스턴스만 실행하도록 보장.
2. 범위 (Scope)
- 포함
DaemonScheduler.run()을 분산 락으로 보호 (lockAtMostFor + lockAtLeastFor).- Lock provider: Redis (이미 운영 중인 in-cluster Redis 재사용).
- 의존성:
net.javacrumbs.shedlock:shedlock-spring,shedlock-provider-redis-spring.
- 제외 (out of scope)
- 다른 @Scheduled 메서드(CacheService.checkHealth, 향후 추가될 cron). 필요 시 같은 패턴으로 확장.
- 락 획득 실패 시 알람 — Spring Actuator/Micrometer 도입 후 후속.
- DB 기반 lock provider (JDBC) — Redis가 충분.
3. 인수조건
- build.gradle에 shedlock-spring + shedlock-provider-redis-spring 추가.
@EnableSchedulerLock활성화.DaemonScheduler.run에@SchedulerLock(name="daemon-runner", ...)적용.- 락 키는
lock:daemon-runner형태로 Redis에 저장 (prefix 기본). - 운영 배포 후 로그에 lock acquire/release 메시지 또는 정상 동작 확인.
- 회귀 없음 — 자동 cron 정상 동작.
4. 컨텍스트 & 제약
- Redis는 in-cluster 단일 인스턴스. ShedLock의 Redis provider는 단일 인스턴스에서 SET NX EX로 동작.
- Pod 1개 운영이라 평소엔 락 경합 없음 → ShedLock 부하 미미 (Redis 1회 SET NX EX, <1ms).
lockAtMostFor: 락이 강제로 해제되기까지 시간.scanAllChannels는 channel 6 × 영상 fetch 시간 ≈ 최대 10분 예상.PT15M로 안전 마진.lockAtLeastFor: 작업이 빨리 끝나도 락 유지하는 최소 시간 (다음 cron이 즉시 잡지 못하게). 30초 cycle이라 PT30S로 충분.
5. 아키텍처 개요
[Pod A] [Pod B]
│ │
│ @Scheduled(fixedDelay=30s)
▼ ▼
DaemonScheduler.run DaemonScheduler.run
│ │
│ @SchedulerLock │ @SchedulerLock
▼ ▼
LockProvider (Redis)
├─ SET lock:daemon-runner EX 900 NX ✓ → Pod A 진행
└─ SET lock:daemon-runner EX 900 NX ✗ → Pod B 즉시 종료(no-op)
│
▼
scanAllChannels / processPending 실행 (A만)
│
▼ 종료 시 락 키 lockUntil 시각으로 갱신 (lockAtLeastFor 보장)
6. 데이터 모델
Redis 키 1개:
- key:
lock:daemon-runner - value: lockedBy(host:pid) + lockedAt
- expiry: lockAtMostFor
7. 함수 명세
| 함수 | 책임 | 시그니처 | 비고 |
|---|---|---|---|
DaemonScheduler.run() (수정) |
@SchedulerLock 추가 | 기존 | name="daemon-runner" |
ShedLockConfig.lockProvider(...) (신규) |
Bean 등록 | LockProvider lockProvider(RedisConnectionFactory) |
Redis provider |
8. 흐름
- 30초마다 fixedDelay로 run() 호출.
- ShedLock AOP가 SET NX EX 시도.
- 성공: 본문 실행. 실패: 즉시 반환(no-op).
- 본문 종료 시 lockUntil 갱신.
9. 엣지케이스
- lockAtMostFor 초과 작업: 락 자동 해제 후 다른 Pod이 잡을 수 있음. scanAllChannels가 15분 넘기지 않게 channel별 timeout 적용 권고(설계서 #275 §11 참고).
- Pod 죽음: lockAtMostFor 만료 후 자동 해제.
- Redis 다운: SET 실패 → Spring AOP가 RuntimeException → 다음 30초에 재시도. 캐시 disabled와 별개.
- clock skew: ShedLock은 Redis 서버 시간 기준이라 클러스터 노드 간 시간 차이 무관.
10. 테스트 계획
- 수동: Pod 2개 동시 실행 (kubectl scale deploy backend --replicas=2) 후 로그에서 한 쪽만
Running scheduled channel scan...찍히는지 확인. - 자동: 후속 (ShedLock 자체는 lib 차원에서 테스트됨).
11. 리스크 & 대안
- 선택: ShedLock + Redis.
- 대안 A: Redis SET NX EX 수동 구현 — 가능하나 ShedLock이 lockAtMostFor/lockAtLeastFor 자동 처리해서 더 안전.
- 대안 B: DB(Oracle) 기반 ShedLock — 추가 테이블 필요 + DB 부하. Redis가 더 단순.
- 대안 C: 단일 leader pod (k8s Lease object) — Spring Cloud Kubernetes 도입 부담 크다.
12. 미해결 질문
- ShedLock 의존성이 standard library가 아닌 4th-party에 가까움 — 검증된 라이브러리(8년+ 사용, 4k+ stars)지만 향후 Spring 마이크로 버전 호환성은 별도 모니터링.
- CacheService.checkHealth는 락 안 걸어도 됨(idempotent). 추가 cron 도입 시 same name 충돌 주의.