Published on

Optimizing Java Chap8

Authors
  • avatar
    Name
    ywj9811
    Twitter

GC 로깅, 모니터링, 튜닝, 툴

GC 로깅 개요

GC 로깅 켜기

우선 GC 로깅을 켜야 로그를 읽을 수 있으니 어플리케이션 시작 시 다음 스위치를 추가해야 한다.

-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintTenuringDistribution
-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps

이는 순서대로, GC 이벤트에 로깅할 파일을 저장, GC 이벤트 세부 정보를 로깅, 툴링에 꼭 필요한, 부가적인 GC 이벤트 세부 정보를 추가, GC이벤트 발생 시간을 VM시작 이후 경과한 시간을 초 단위로, 벽시계 시간 기준으로 출력하는 것이다.

GC 로그 vs VMX

2.7장에서 소개한 VisualGC는 JVM 힙 상태를 실시간으로 표시하는 툴로, JMX 인터페이스를 통해 JVM 데이터를 수집한다.

JMX는 GC에 영향을 주기 때문에 성능 엔지니어는 고려해야 한다.

JMX의 단점

JMX를 이용하여 어플리케이션을 모니터링하는 클라이언트는 대부분 런타임을 샘플링하여 현재 상태를 업데이트 받는데, 이를 위해 클라이언트는 데이터를 계속 넘겨받기 위해 런타임에 있는 JMX 빈을 폴링한다.

문제는 가비지 수집이다.

수집기가 언제 실행될지 클라이언트는 알 수 없고, 이로 인해 GC 데이터를 깊이 있게, 정확하게 분석할 수 없다.

예를 들어 각 수집 전후의 힙 상태 정보와 같은 내용을 알기 어렵다.

뿐만 아니라 JMXConnector 명세를 구현한 코드는 내부적으로 RMI에 의존하기 때문에 RMI 기반 통신 채널의 고질적인 문제점에 취약하다.

GC 로그 데이터의 장점

최신 가비지 수집기는 수많은 부품이 한데 조립된, 엄청나게 복잡한 구현체로 수집기의 성능 역시 불가능하지는 않지만 그만큼 예측하기 힘들다.

이처럼 전체 구성 컴포넌트가 서로 맞물려 작동하면서 최종적 동작, 성능이 귀결되는 소프트웨어를 발현적이라 하는데 이는 매우 유동적인 비용 모델이다.

이러한 모습에서 GC 로그 수집 및 분석은 튜닝 활동에서 절대적으로 빠질 수 없는데, GC 로그에 쌓인 기초 데이터는 특정 GC 이벤트와 연관 지을 수 있어서 모든 의미 있는 작업을 수행할 수 있다.

로그 파싱 툴

센섬

이는 jClarity 에서 제작한 상용 메모리 분석기로 데스크톱 툴 (단일 JVM에서 간단히 분석 가능한), 서비스 모니터링 용도 (여러 JVM을 대상으로) 로 모두 사용할 수 있다.

GCViewer

GCViewer는 GC 로그 파싱 및 그래프 출력 등 기본 기능을 갖춘 데스크톱 툴이다.

오픈소스기에 무료라는 큰 장점을 가지고 있다.

물론 무료이기에 내용이 빈약하다는 단점은 있다.

GC 기본 튜닝

GC는 언제 튜닝해야 할까?

GC 튜닝 역시 다른 튜닝 기법처럼 전체 진단 과정의 일부여야 한는데, 다음 사실을 기억해야 한다.

  • GC가 성능 문제를 일으키는 근원이라고 확인하거나 그렇지 않다고 배제하는 행위는 저렴하다.
  • UAT에서 GC 플래그를 켜는 것도 저렴한 행위이다.
  • 메모리 프로파일러, 실행 프로파일러를 설정하는 작업은 결코 저렴하지 않다.

그리고 엔지니어는 튜닝을 수행하면서 다음 네가지 주요 인자를 면밀히 관찰/측정해야 한다.

  • 할당
  • 중단 민감도
  • 처리율 추이
  • 객체 수명

그리고 튜닝 시 GC 플래그는 다음과 같이 추가한다.

  • 한번에 한 플래그씩 추가한다.
  • 각 플래그가 무슨 작용을 하는지 숙지해야 한다.
  • 부수 효과를 일으키는 플래그 조합도 있음을 명심한다.

또한, 현재 이벤트가 발생 중이라면 성능 문제를 일으키는 원인이 GC인지 아닌지 판단하는 건 어렵지 않다.

우선 vmstat과 같은 툴로 고수준의 머신 지표를 체크하고 성능이 떨어진 시스템에 로그인해서 다음을 확인해야 한다.

  • CPU 사용율이 100%에 가까운가?
  • 대부분의 시간(90%) 이상이 유저 공간에서 소비되는가?
  • GC 로그가 쌓이고 있다면 GC가 현재 실행중이라는 것이다.

위 조건이 맞다면 이는 문제의 출처가 GC일 가능성이 높다.

할당이란?

할당률 분석은 튜닝 방법 뿐만 아니라, 실제로 가비지 수집기를 튜닝했을 때 성능이 개선될지 여부를 판단하는데 꼭 필요하는 과정이다.

→ 영 세대 수집 이벤트 데이터를 활용하면 할당된 데이터 양, 단위 수집 시간을 계산할 수 있고, 일정 시간 동안의 평균 할당률을 산출할 수 있다.

만약 할당률 수치가 1GB/s 이상으로 일정 시간 지속된다면 십중팔구 가비지 수집 튜닝만으로 해결하기 어렵다.

(핵심부의 할당 로직을 제거하는 리팩토링을 수행하여 메모리 효율을 개선하는 방향으로 가야한다.)

초기 할당 전략

초기 할당 전략은 네가지 단순 영역에 집중하는 것이 좋다.

  1. 굳이 없어도 그만인 사소한 객체 할당 (예 : 로그 디버깅 메시지)
  2. 박싱 비용
  3. 도메인 객체
  4. 엄청나게 많은 논JDK 프레임워크 객체

우선 불필요한 객체를 생성하는 부위를 찾아 그냥 제거해버리면 된다.

과도한 박싱도 그중 하나이지만, JSON 직렬화, 역직령화용 자동 생성 코드 혹은 ORM 코드 등등 다양하다.

드물지만 도메인 객체가 메모리를 많이 차지하는 경우도 있는데 아래와 같은 타입이 문제이다.

  • char[] : 스트림을 구성하는 문자character
  • byte[] : 바이너리 데이터
  • 맵 엔트리
  • Object[]
  • 내부 자료 구조

이경우 단순 힙 히스토그램을 그려보면 불필요한 도메인 객체가 히스토그램의 상위권을 점유하면서 과하게 생성되는 모습을 지켜볼 수 있다.

객체가 할당되는 동작을 살펴보면 이유를 알 수 있는데, 동작의 순서는 아래와 같다.

우선 TLAB은 스레드 당 크기가 동적 조정되며 일반 객체는 남은 TLAB 공간에 할당된다.

여유 공간이 없으면 스레드는 VM에게 새 TLAB을 요청하고 재시도 하는데, 그래도 안들어간다면 TLAB은 외부 영역에 위치한 에덴에 직접 객체를 할당하려고 할 것이다.

근데 이것도 실패하게 되면 영GC를 수행하는 단계를 수행할 것인데, 이때 또 실패하게 되면 최후의 방법으로 테뉴어드 영역에 직접 할당하게 될 것이다.

따라서 결국 덩치 큰 배열 (특히 byte[], charp[])은 곧바로 테뉴어드에 할당될 가능성이 크다.

이때, 핫스팟은 TLAB 및 큰 객체의 조기 승격에 관한 튜닝 플래그를 제공한다.

-XX:PretenureSizeThreshold=<n>
-XX:MinTLABSize=<n>

또, 할당률은 테뉴어드로 승격되는 객체 수에 영향을 끼친다.

단명 자바 객체의 수명이 불변이라고 가정하면 할당률이 높을수록 영GC 발생 주기는 짧아지는데, 너무 자주 수집이 일어나면 단명 객체는 장례를 치를 시간도 없이 테뉴어드로 잘못 승격될 가능성이 있다.

즉, 할당이 폭주하면 조기 승격 문제가 불거질 것이다.

이러한 조기 승격 문제에는 다음 스위치가 요긴하게 쓰인다.

-XX:MaxTenuringThreshold=<n>

테뉴어드 영역으로 승격되기 전까지 객체가 통과해야 할 가비지 수집 횟수를 정하는 것이다.

디폴트로 4이며 1~15 사이의 한계치를 정해줄 수 있다.

  • 한계치가 높을수록 진짜 장수한 객체를 더 많이 복사해야 한다.
  • 한계치가 너무 낮으면 단명 객체가 승격되어 테뉴어드에 메모리압을 가중시킨다.

하지만 이때 위 두가지를 잘 따져봐야 한다.

(하지만, 더 나아진 선례가 없는 경우 스위치를 함부로 변경하지는 마십쇼)

수집기 스레드와 GC루트

GC 또한 여러가지 트레이드 오프가 도사리고 있는데, 예를 들면 GC 루트 탐색 시간은 다음과 같은 요인의 영향을 받는다.

  • 어플리케이션 스레드 개수

    스레드가 많아지면 스택 프레임을 더 많이 탐색해야 하고 세이프포인트에 도달하는 시간도 길어지는 등 GC 시간에 영향을 끼친다.

  • 코드 새키에 쌓인 컴파일드 코드량

  • 힙 크기

이 외에도 런타임 조건 및 적용 가능한 병렬화 정도에도 영향을 받는다.

수집기별 튜닝

Parallel GC 튜닝

Parallel GC는 가장 단순한 수집기이기에 튜닝 역시 제일 쉽다.

이 수집기의 목표와 트레이드 오프는 명확한데 아래와 같다.

  • 풀 STW
  • GC 처리율이 높고 계산 비용이 싸다.
  • 부분 수집이 일어날 가능성은 없다.
  • 중단 시간은 힙 크기에 비례하여 늘어난다.

이러한 특성들이 별문제가 되지 않는 어플리케이션에는 Parallel GC가 아주 효과적인 선택이다.

CMS 튜닝

CMS는 튜닝이 까다롭기로 소문난 수집기이다.

CMS처럼 중단 시간이 짧은 수집기는 정말로 STW 중단 시간을 단축시켜야 하는 유스케이스에 한해 어쩔 수 없을때만 사용해야 한다.

그래야 튜닝하기 힘들고 복잡한 CMS에 의해 고생하지 않을 것이다.

그럼에도 불구하고 이를 개선하기 위해서는 아래와 같은 방식을 사용하자.

  1. 처리율

    CMS 수집이 일어나면 기본적으로 코어 절반은 GC에 할당 되므로 어플리케이션 처리율은 그만큼 반토막 난다.

    이때 유용한 방법은 CMF 발생 직전의 수집기 상태를 살펴보는 것이다.

    만약, CMS 수집이 끝나자마자 CMS 수집이 새로 시작되는 백투백 수집 현상은 동시 수집기가 얼마 못가고 고장날 것이라는 신호이다.

    → 어플리케이션 할당 속도가 회수 속도를 능가하면서 결국 CMF가 발생

    그러므로 성능 엔지니어는 이를 고려해보고 코어를 늘리는 해결 방안을 모색해야 한다.

    혹은 GC에 할당된 코어 수를 줄이는 방법 또한 있다.

  2. 단편화로 인한 CMF

    튜닝 분석에 필요한 데이터가 GC 로그에만 존재하는 다른 사례이다.

    이러한 CMF는 CMS가 관리하는 프리 리스트 때문에 발생한다.

    -XX:PrintFLSStatistics=1
    

    이러한 JVM 스위치를 추가하면 GC 로그에 몇몇 추가 정보가 표시된다.

    → 총 프리 공간, 최대 청크 크기, 블록 개수, 평균 블록 크기, 트리 높이

    이를 통해 메모리 청크의 크기 분포를 대략 짐작할 수 있을 것인데, 예를 들면 덩치 큰 라이브 객체를 테뉴어드로 옮기려 할 때 그만한 크기의 청크가 바닥난 경우 GC 승격이 악화되어 결국 CMF로 이어질 것이다.

G1 튜닝

엔드 유저가 최대 힙 크기와 최대 GC 중단 시간을 간단히 설정하면 나머지는 수집기가 알아서 처리하게 하는 것이 G1 튜닝의 최종 목표이다.

물론 아직은 현실과 살짝 동떨어진 느낌이 있다.

G1의 경우 Java9 부터 디폴트 수집기이기에 성능엔지니어는 어쩔 수 없이 G1 튜닝 문제를 다루어야 하지만, 현재까지 내부적인 변화가 지속적으로 있고 튜닝에 대한 베스트 프랙티스가 계속해서 새롭게 나오기 때문에 우선은 개선이 된 부분과 CMS를 능가할 것으로 전망되는 부분만 짚어보도록 하자.

G1 수집기는 할당률에 뒤처지지 않는 한 계속 조금씩 압착하기 때문에 CMF가 일어날 가능성은 전혀 없다.

그리고 만약 어플리케이션에서 할당률이 계속 높은 상태로 대부분 단명 객체가 생성되고 있다면 다음을 고려해볼 수 있다.

  • 영 세대를 크게 설정한다.
  • 테뉴어드 한계치를 최대 15 정도로 늘려준다.
  • 어플리케이션에서 수용 가능한 최장 중단 시간 목표를 정한다.

참고

위의 스위치 설정의 경우 DockerFile과 같은 곳에서 설정 후 실행하면 된다고 한다.

직접 ENTRYPOINT 내에 작성 혹은 스크립트로 적용 등등의 방법이 있다고 한다.