Skip to main content

분산 락이 실패하는 이유? GC 일시정지 15초가 동시성을 무너뜨린 실제 사례

Published 4 min readMarkdown

# 서론 – “왜 우리 시스템은 갑자기 멈추나요?”

분산 시스템을 운영하다 보면 “잠시 멈췄다가 다시 살아났다”는 현상을 경험하게 됩니다.

특히 Redis와 같은 외부 저장소를 이용해 분산 락(Distributed Lock)을 구현했음에도 불구하고, 갑자기 락이 해제되지 않아 서비스가 정지하는 경우가 있습니다.

최근 Reddit에 올라온 한 개발자의 포스트에서는 Java 애플리케이션이 15초 동안 가비지 컬렉션(GC) 일시정지에 빠지면서, Redis 락을 잡고 있던 프로세스 A가 응답하지 않아 전체 결제 흐름이 멈춘 사례가 소개되었습니다.

“프로세스 A가 Redis에서 락을 잡고 출금 작업을 시작했지만, Java가 GC를 실행하면서 15초 동안 멈춰버렸다.”

이 글에서는 왜 GC 일시정지가 분산 락을 깨뜨리는지, 그리고 예방·대응 방법을 초보자도 이해하기 쉽게 풀어보겠습니다.


# 1️⃣ 분산 락이란 무엇이고, 어떻게 동작하나요?

# 1-1. 기본 개념

  • 분산 락은 여러 서비스 인스턴스가 동시에 같은 자원(예: 데이터베이스 레코드, 파일, 결제 트랜잭션 등)에 접근하지 못하도록 제어하는 메커니즘입니다.

  • 가장 흔히 쓰이는 구현체는 Redis의 SET key value NX PX ttl 명령으로, 키가 없을 때만 생성하고 TTL(Time‑To‑Live) 로 자동 해제를 보장합니다.

# 1-2. 일반적인 흐름

  1. 락 요청 → SET lock_key my_id NX PX 10000 (10초 TTL)

  2. 성공 → 비즈니스 로직 수행 (예: 출금 처리)

  3. 완료 → DEL lock_key 혹은 TTL 만료에 의해 자동 해제

# 1-3. 실패 시나리오

  • 프로세스 비정상 종료 → TTL이 지나면 자동 해제 → 정상 복구

  • 프로세스가 일시정지(GC, 스레드 대기 등) → TTL이 감소하지만 로직이 진행되지 않아 락이 해제되지 않음 → 다른 인스턴스가 락을 획득 못함 → 서비스 정지

핵심 포인트: 분산 락은 시간 제한에 의존하므로, 락을 잡은 스레드가 예상보다 오래 멈출 경우 타임아웃보다 더 오래 락이 유지될 수 있습니다.


# 2️⃣ GC 일시정지(Garbage Collection Pause)란?

# 2-1. GC의 역할

Java 가상 머신(JVM)은 힙 메모리에 할당된 객체를 자동으로 정리해 메모리 누수를 방지합니다. 이 과정이 가비지 컬렉션이며, 크게 Stop‑The‑World(STW) 와 Concurrent 두 가지 방식이 있습니다.

# 2-2. Stop‑The‑World(STW) 일시정지

  • STW 모드에서는 모든 애플리케이션 스레드가 멈추고 GC 스레드만 실행됩니다.

  • Pause 시간은 힙 크기, 객체 생존율, GC 알고리즘 등에 따라 수 ms부터 수 초까지 다양합니다.

# 2-3. 왜 15초가 나왔을까?

  • 대용량 힙(예: 8~16 GB) + Full GC (Mark‑Sweep‑Compact)

  • 메모리 압박 → 오래된 객체가 많이 남아 있어 전체 힙을 스캔해야 함

  • JVM 옵션 미조정 → -XX:+UseG1GC 대신 Parallel GC 사용 → 일시정지 시간이 급증

결과: 15초 동안 모든 비즈니스 로직이 멈추고, Redis 락은 그대로 유지되어 다른 인스턴스가 접근 못함.


# 3️⃣ 실제 사고 분석 – Reddit 사례 상세 해부

| 단계 | 발생 상황 | 문제점 |

|------|----------|--------|

| 1️⃣ 락 획득 | Process A가 SET lock_key A NX PX 15000 호출 → 락 잡힘 | 정상 |

| 2️⃣ 비즈니스 로직 | 출금 처리 시작 (DB 업데이트, 외부 API 호출) | 진행 중 |

| 3️⃣ GC 발생 | JVM이 Full GC 실행 → 15초 STW | 모든 스레드 멈춤 |

| 4️⃣ 락 유지 | TTL은 계속 감소하지만, 로직이 멈춰 해제되지 않음 | 다른 인스턴스가 락을 못 잡음 |

| 5️⃣ 서비스 장애 | 사용자는 출금 요청에 대한 응답을 받지 못함 | 시스템 전체 정지 |

# 교훈

  • GC 일시정지는 락 유지 시간보다 길어질 수 있다.

  • TTL 설정만으로는 충분히 보호되지 않는다.

  • 모니터링과 알림이 없으면 문제 발견이 늦어져 서비스 복구가 지연된다.


# 4️⃣ 예방·대응 전략 – GC와 락을 동시에 관리하기

# 4-1. GC 튜닝

  1. Heap Size 최적화 – 과도하게 큰 힙은 Full GC를 유발합니다.

  2. G1GC 혹은 ZGC 사용 – Low‑Pause 컬렉터로 STW 시간을 10 ms 이하로 유지합니다.

  3. JVM 옵션 예시

bash

   -Xms4g -Xmx4g \

   -XX:+UseG1GC \

   -XX:MaxGCPauseMillis=200 \

   -XX:+UnlockExperimentalVMOptions -XX:+UseZGC
  1. Heap Dump & 분석 – jmap, jvisualvm 로 메모리 사용 패턴을 파악하고, 객체 생성량을 줄입니다.

# 4-2. 락 설계 보강

  • Lock Timeout보다 짧은 작업 타임 보장 → 비즈니스 로직에 타임아웃 적용 (예: Future.get(5, TimeUnit.SECONDS))

  • Watchdog 스레드 도입 → 락이 일정 시간 이상 유지되면 자동 해제하거나 알림 전송

  • Redlock 알고리즘 사용 → 다중 Redis 인스턴스로 내구성 강화

# 4-3. 모니터링 & 알림

| 항목 | 도구 | 알림 기준 |

|------|------|-----------|

| GC Pause Time | Prometheus + Grafana (jvm_gc_pause_seconds) | 100 ms 초과 시 Slack 알림 |

| Lock Duration | Redis INFO COMMANDSTATS + custom metric | TTL 초과 시 PagerDuty |

| Application Latency | OpenTelemetry | 95th percentile > 2 s |

# 4-4. 테스트 환경 구축

  • Chaos Engineering: Chaos Mesh 혹은 Gremlin 으로 GC 지연을 인위적으로 발생시켜 시스템 복원력을 검증합니다.

  • Load Test: k6 혹은 JMeter 로 동시에 여러 인스턴스가 락을 잡는 상황을 시뮬레이션합니다.


# 5️⃣ 결론 – 지금 바로 적용할 수 있는 실천 체크리스트

  1. GC 로그 활성화 (-Xlog:gc*) → Pause 시간 확인

  2. Heap Size를 현재 사용량의 2배 이하로 제한하고, Low‑Pause GC 도입

  3. Redis 락에 Watchdog을 추가하고, 타임아웃을 비즈니스 로직보다 짧게 설정

  4. Prometheus + Grafana 로 GC Pause와 락 상태를 실시간 모니터링

  5. Chaos 테스트로 GC 지연 상황을 사전에 검증

핵심 메시지: GC 일시정지는 눈에 보이지 않지만, 분산 락과 결합될 때 치명적인 서비스 중단을 일으킬 수 있습니다. 적절한 GC 튜닝과 락 설계 보강, 그리고 모니터링을 통해 이러한 위험을 미리 차단하세요.


# 📢 행동 유도

  • 지금 바로 프로젝트의 pom.xml 혹은 build.gradle에 GC 튜닝 옵션을 추가하고, Redis 락 구현 코드를 리뷰해 보세요.

  • 팀 회의에서 “GC 일시정지와 분산 락” 주제로 5분 발표를 준비하면, 팀 전체의 인식 개선에 큰 도움이 됩니다.

  • 블로그 구독 및 댓글로 여러분이 겪은 비슷한 사례를 공유해 주세요. 함께 해결책을 찾아갑시다!


SEO 키워드: 분산 락, GC 일시정지, 자바 가비지 컬렉션

No comments yet. Be the first to comment.

More from this blog