[JVM 밑바닥까지 파헤치기] 5장 최적화 사례 분석 및 실전
최적화 사례 분석 및 실전
사례 분석
대용량 메모리 기기 대상 배포 전략
- 하루 페이지 뷰 15만건 정도의 웹 사이트
- 쿼드 제온 프로세서, 16GB 메모리
- 운영체제 64비트 CentOS 5.4
- 웹 서버: 레진(Resin)
- 웹 사이트: 64비트 JDK 5로 구동
- -Xmx와 -Xms 매개 변수를 지정하여 자바 힙 크기를 12GB로 고정
다음과 같은 환경에서 웹 사이트가 장시간 응답하지 않는 일이 자주 발생하였다.
응답 지연의 원인은 GC였다.
시스템 하드웨어와 소프트웨어 구성을 고려하여 핫스팟 가상 머신을 서버 모드로 실행했고, 기본 설정인 패러렐 컬렉터(패러렐 스캐빈지+패러렐 올드)가 메모리 관리를 책임졌다. 패러렐 컬렉터는 일시 정지 시간보다는 처리량에 중점을 둔 컬렉터 12GB 힙 메모리를 GC하기 위해 풀 가비지 컬렉션이 일어나 최장 12초까지 일시정지하게 된 것이다.
결론적으로 힙 메모리를 너무 크게 잡아서 회수하고 재활용하는 데 너무 오래 걸리는 것이 주된 문제였다.
간단하게는 힙 크기를 줄여서 일시 정지 시간을 줄일 수 있지만… 이 경우 하드웨어를 십분 활용할 수 없다. 이 경우 하드웨어를 활용하기 위해 배포 전략을 수정할 수 있다.
- 가상 인스턴스 하나에 거대한 자바 힙 메모리를 관리한다.
- 가상 머신 여러개를 논리적 클러스터로 구성한다.
이번 사례에서는 첫번째 방법을 택했는데, 이 경우 셰넌도어나 ZGC처럼 지연 시간 통제를 목표로 하는 GC를 이용해 해결할 수 있다. 책에서는 CMS 를 택했다.
또, 전체 GC를 제어해 사용자가 없는 시간대에 풀 GC가 일어나도록 스케줄링해 해결할 수도 있다.
두번째 방식은 논리적인 클러스터를 나누고, 앞에 로드밸런서로 역프록시를 수행하게 해 요청을 분배하는 방식이다. k8s나 클라우드로 클러스터화를 진행해도 무방하다.
클러스터 간 동기화로 인한 메모리 오버플로
- 브라우저-서버 기반 경영 정보 시스템
- 듀얼 프로세서, 8GB 메모리 컴퓨터 2대
- 각 컴퓨터에 미들웨어인 웹로직 9.2를 3개씩 구동해 총 6노드의 선호도 클러스터를 구성
다음과 같은 환경에서, JBossCache로 글로벌 캐시를 구축한 뒤로 메모리 오버플로가 간혹 발생하였다.
힙 덤프 스냅숏을 살펴보니 수많은 org.jgroups.protocols.pbcast.NAKACK
객체가 발견되었다.
특정 상황에서 네트워크가 데이터 전송량을 다 처리하지 못하게 되면 재전송된 데이터가 메모리에 계속 쌓이다가 오버플로를 일으키는 것이다.
이와 같이, 클러스터 전체에서 공유해야 하는 데이터를 JBossCache 같은 분산 클러스터 캐시를 이용해 동기화하면 네트워크 통신이 자주 일어날 수 있다.
힙 메모리 부족으로 인한 오버플로 오류
- 서버 푸시 기술을 활용해 클라이언트가 서버로부터 시험 데이터를 실시간으로 받아 볼 수 있는 브라우저-서버 기반 온라인 시험 시스템
- 서버 푸시 프레임워크: CometD 1.1.1
- 서버 소프트웨어: 제티(Jetty) 7.1.4
- 하드웨어: 인텔 코어 i5 CPU, 4GB 메모리, 32비트 윈도우
다음과 같은 환경에서 메모리 오버플로가 간혹 발생하였고, 메모리가 부족해 힙 덤프를 생성할 수 없었다.
jstat
을 활용해 상황을 지켜봤더니 가비지 컬렉션은 자주 일어나지 않았다. 또한 에덴, 생존자 공간, 구세대, 신세대 그리고 힙 메모리까지 모두 안정적이였다.
시스템 로그를 출력해 보았을 때 다이렉트 메모리가 부족하여 메모리 오버플로가 발생한 것을 볼 수 있었다. 이와 같은 경우, -XX:MaxDirectMemorySize
를 활용하여 메모리 크기를 조절할 수 있다.
시스템을 느려지게 하는 외부 명령어
- 대학교 운영을 디지털화해 주는 시스템
- 프로세서 4개가 장착된 솔라리스 10 시스템
- 미들웨어: 글래스피시
다음과 같은 환경에서 동시성 스트레스 테스트를 수행하자 응답 속도가 지나치게 느려졌다.
사용자 요청 처리시 시스템 정보가 필요해 외부 셸 스크립트를 실행하도록 작성(Runtime.getRuntime().exec()
)하는 경우, 프로세스가 지나치게 많이 생성되어 이로 인해 자원을 매우 많이 소비하게 된다.
셸 스크립트를 실행하는 대신, 필요한 정보를 Java API로 가져오도록 고치면 시스템이 정상적으로 작동한다.
서버 가상 머신 프로세스 비정상 종료
- 듀얼 프로세서, 8GB 메모리 컴퓨터 2대
- 웹로직 9.2를 3개씩 구동하여 총 6노드의 선호도 클러스터로 구성
다음과 같은 환경에서 java.net.SocketException: Connection reset
를 대량으로 발생시키며 비정상 종료되는 현상이 있었다.
동기화 요청을 보내 보니, 최대 3분이 되어서야 응답이 왔고 그마저도 돌아온 결과는 모두 타임아웃에 의한 연결 중단이었다.
이와 같은 경우 문제가 되는 연동 인터페이스를 수정하고, 비동기 호출 부분을 sub/pub방식의 메시지 큐로 변경해 해결할 수 있었다.
부적절한 데이터 구조로 인한 메모리 과소비
- 64비트 자바 가상 머신을 이용하는 백그라운드 원격 프로시저 호출(RPC) 서버
- 메모리 설정:
-Xms4g
-Xmx8g
-Xmn1g
- GC: 파뉴+CMS 컬렉터 조합
HashMap<Long, Long>
구조에서 키와 값에 해당하는long
정수 2개만이 사용자에게 의미가 있는 데이터다(총 16바이트). 이 두long
데이터 각각은 java.lang.Long 객체로 감싸지며,Long
객체는 8바이트의 마크 워드, 8바이트의 클래스 포인터, 데이터를 담기 위한 8바이트의long
변수로 구성된다(총 24바이트).Long
객체는 Map.Entry에 저장되며, Map.Entry는 16바이트의 객체 헤더, 8바이트의 next 필드, 4바이트의 해시 필드로 구성된다. 여기에 총 32바이트 크기로 맞추기 위한 정렬용 패딩 4바이트도 추가된다(총 32바이트). 마지막으로 HashMap에서는 8바이트 참조를 통해 Map.Entry를 가리킨다. 결과적으로 long 정수 두 개를 담는 데 쓰이는 실제 메모리는 (Long(24바이트)2)+Entry(32바이트)+HashMap 안의 참조(8바이트)까지 해서 총 88바이트다. 메모리 사용량 중 유효 데이터의 비율은 겨우 18%다(16/88).
해결 방법으로는 다음 두가지 방법이 제시된다.
- GC 최적화
- 증상을 완화할 수는 있지만, 사이드 이펙트가 커질 수 있으므로 권장되지 않는다.
- 프로그램 자체를 고치기
HashMap<Long, Long>
자료구조는 데이터의 공간 효율성이 좋지 않다.
안전 지점으로 인한 긴 일시정지
- HBase 클러스터 환경
- JDK8 , G1 컬렉터
GC 시간은 짧으나 사용자 스레드가 정지한 시간이 긴 경우, 이 스레드부터 확인한다.
책에서 나온 사례의 경우 HBase 연결 타임아웃 초기화 기능이 문제였다.
클러스터에는 맵리듀스와 스파크 태스크의 연결이 다수 있었고, 태스크 각각이 다수의
MapperReducerExecuter
를 동시에 시작시켰다. 그리고 그 각각은 다시 HBase 클라이언트로 동작하면서 동시에 엄청난 수의 연결을 만들어 냈다. 또, 하필 이 연결을 초기화하는 순환문의 루프 변수가int
타입이었기 때문에, 즉 카운티드 루프였기 때문에 핫스팟이 순환문 안에 안전 지점을 설정하지 않은 것이다.
문제의 루프 변수 타입을
long
으로 변경하면 해결된다.
Leave a comment