한국어English日本語简体中文繁體中文DeutschไทยTiếng ViệtРусскийPortuguês (Brasil)EspañolBahasa Indonesia

게임 렉 백서 › L10 메모리

서버 GC 전체 멈춤 Stop-the-world GC pause

원인 ID mem-gc · 주 담당 게임개발팀·서버 개발 · 함께 인프라팀·서버 인프라

그림과 실험이 있는 원본 카드로 열기 →

Java·C# 서버가 가비지를 수집하려고 모든 스레드를 멈추는 동안(stop-the-world) 서버 전체가 멈춥니다.

왜 힙이 차서 GC 시작 → 그러면 모든 게임 스레드를 멈추고 수집(살아 있는 데이터가 많을수록 오래) → 화면에서는 서버의 모두가 동시에 멈췄다가 몰아치기

증상
멈춤, 몰아치기
요인
정체
누가 겪나
서버 전체
언제
일정한 주기로, 오래 켜 둘수록
담당
주 담당 게임개발팀·서버 개발 · 함께 인프라팀·서버 인프라
게임개발팀 할 일
멈춤이 짧은 GC(ZGC, Shenandoah, 목표 시간을 줄인 G1)를 실행 옵션으로 직접 지정, 할당 줄이기, 힙 크기 조정.
인프라팀 할 일
힙을 넉넉히 줄 수 있는 메모리의 인스턴스, 컨테이너에는 CPU 2개 이상·메모리 약 1.8GB 이상 배정(JDK 26 이하는 그보다 작으면 Serial GC를 기본으로 고름), GC 멈춤 시간 모니터링.
수치 감각
새 객체(Young 영역)만 수집하는 Minor GC는 수~수십 ms. 살아 있는 데이터가 수 GB인 힙 전체를 수집하는 Full GC는 1초를 넘기도 합니다. ZGC는 힙 크기와 거의 상관없이 1ms 미만이고 Shenandoah도 멈춤이 힙 크기에 비례하지 않아 짧습니다.
그래프에서는
일정 주기로 튐 · 서버 틱 시간, GC 멈춤 시간
확인할 곳
GC 로그를 켜서 멈춘 시각과 길이를 서버 틱 시간 그래프에 겹쳐 봄. Java는 시작 옵션 -Xlog:gc*(JDK 8 이하는 -XX:+PrintGCDetails)의 Pause 줄, .NET은 dotnet-counters의 GC 멈춤 지표(.NET 9 이후 dotnet.gc.pause.time, 8 이하 % Time in GC since last GC), Go는 GODEBUG=gctrace=1이 GC마다 남기는 줄을 봄
이러면 맞음
틱이 튄 시각과 GC 멈춤 시각이 겹치고 멈춤 길이가 튄 길이와 비슷함. 서버의 모든 존·채널이 같은 순간에 튐
이러면 아님
GC 로그에 긴 멈춤이 없는데도 틱이 튀면 락·동기 호출·디스크 쓰기 같은 다른 원인. 한 존만 튀면 스크립트 엔진의 GC(mem-script-gc)나 그 존의 부하
확인 수단
인프라 도구로 확인(게임 코드 불필요)
더 알아보기
Java의 G1은 한 번 멈춤 목표가 기본 200ms라, 20틱 서버에서는 틱 4개에 해당합니다. 컨테이너에 CPU를 2개 미만이나 메모리를 약 1.8GB 미만으로 주면 JDK 26 이하의 Java는 단일 스레드로 수집하는 Serial GC를 기본 GC로 골라 멈춤이 훨씬 길어집니다. C#(.NET) 서버는 보통 서버 GC와 백그라운드 GC를 켜지만 새 객체를 담는 0·1세대(Gen0·1) 수집과 압축을 동반한 Full GC는 여전히 모든 스레드를 멈춥니다. Go는 멈춤이 보통 1ms 미만이지만 할당이 많으면 메모리를 요청한 쪽이 GC 작업을 분담해야 해서 틱이 느려집니다. 어느 방식이든 할당이 수집보다 빠르면 결국 게임 스레드가 멈춥니다. G1은 Full GC로 넘어가고 ZGC는 메모리를 요청한 스레드를 수집이 끝날 때까지 멈춥니다.
실제 사례
Riot Games 2021: League of Legends EUW 5시간 장애: 부가 DB 하나가 서버 전체를 멈춤

출처

  1. Garbage-First (G1) Garbage Collector Oracle
    G1 멈춤 목표 기본값 200ms(MaxGCPauseMillis), 수집 중 메모리가 바닥나면 힙 전체를 멈추고 압축하는 Full GC로 넘어감
  2. JEP 523: Make G1 the Default Garbage Collector in All Environments OpenJDK
    그동안 CPU 1개이거나 메모리 1792MB 미만이면 Serial GC를 기본으로 골랐고, JDK 27부터는 어디서나 G1이 기본
  3. JEP 439: Generational ZGC OpenJDK
    ZGC 멈춤은 1ms 이하이고 힙 크기와 무관, G1 멈춤은 수 ms~수 초. 할당이 회수보다 빠르면 할당 멈춤(allocation stall) 위험
  4. JEP 189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental) OpenJDK
    Shenandoah 멈춤 시간은 힙이 200MB든 200GB든 비슷함
  5. Background garbage collection .NET
    백그라운드 GC는 2세대 수집에만 적용되고 0·1세대 수집(포그라운드 GC)은 관리 스레드를 모두 멈춤
  6. A Guide to the Go Garbage Collector Go
    Go GC는 대부분 동시에 돌고 짧은 전체 멈춤만 있음, 할당이 많으면 고루틴이 GC 일을 떠맡아(assist) 지연이 생김
  7. JEP 271: Unified GC Logging OpenJDK
    JDK 9부터 GC 로그를 통합 로깅(-Xlog)으로 다시 구현, -Xlog:gc는 예전 -XX:+PrintGC처럼 GC마다 한 줄
  8. The java Command Oracle
    예전 GC 로그 옵션을 -Xlog로 바꾸는 표: -XX:+PrintGCDetails는 -Xlog:gc*
  9. dotnet-counters diagnostic tool .NET
    .NET 9 이후는 System.Runtime 미터(dotnet.gc.pause.time 등), .NET 8 이하는 예전 EventCounter(% Time in GC since last GC 등)로 표시
  10. runtime package Go
    GODEBUG=gctrace=1: GC마다 한 줄, 단계별 벽시계 시간, GC 시작·끝의 힙 크기와 목표 힙

함께 보면 좋은 원인

같은 층: L10 메모리

같은 증상(멈춤)의 다른 층 원인

그림과 실험이 있는 원본 카드 보기