Files
joungmin c88cb6ad54 docs(design): #335 데몬 분산 락 설계서 (Architect)
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 단계)
2026-06-15 15:16:06 +09:00

5.4 KiB
Raw Permalink Blame History

설계서: 데몬 스케줄러 분산 락 (#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. 흐름

  1. 30초마다 fixedDelay로 run() 호출.
  2. ShedLock AOP가 SET NX EX 시도.
  3. 성공: 본문 실행. 실패: 즉시 반환(no-op).
  4. 본문 종료 시 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 충돌 주의.