아무거나

Redis 성능 개선을 위한 GZIP 압축 로직을 적용시 Native OOM 이슈에 대한 분석 본문

Java & Kotlin/Java

Redis 성능 개선을 위한 GZIP 압축 로직을 적용시 Native OOM 이슈에 대한 분석

전봉근 2024. 12. 25. 13:53
반응형

원인

원인은 Redis 의 Packet Loss 가 발생하였고 해당 애플리케이션의 전체 Pod 에서 OOM 이 발생하면서 재시작되는 현상이였고 Pod 의 Heap 메모리 사용량은 여유가 있는 상태였습니다.
왜 이러한 문제가 발생하였는지 알아보겠습니다.

 

컨테이너에서 OOM 발생

먼저 서버로그를 확인해본 결과 Gzip 압축 적용후에 점진적으로 메모리 사용량이 증가한 내용이 확인되었다.

 

또한 컨테이너에 OOM Kill 도 발생한 것을 확인할 수 있었다.

 

그러나 이상한점은 Heap Memory 는 정상적인 수치로 확인되었다.

 

 

원인분석

Native Memory 에서 OOM 이 발생하였고, Heap Memory 는 정상 적이라 애플리케이션 코드에 문제가 있다고 판단 하였고 관련하여 상기 언급 되었던 배포 항목중 gzip 데이터 압축 로직에 대해 검토하였다.

 

1. try-catch 구문 에서의 Stream 객체 들의 메모리 반환 부분 의심 

 

2. GZIPOutputStream 외 다른 Stream 객체들도 finish() 와 close() 로 메모리 반환을 하고 있었지만, 정상 종료되지 않았을 경우 close() 하지 않는 것이 원인

    - GZIPOutputStream 의 finish() 메소드를 확인해보면 기본 stream 을 닫지 않고 출력 스트림에 압축 데이터 쓰기를 완료하게끔 작성 되어있다.

 

3. 여기서 더 내부적으로 파악해보면 2번 이미지에 def 라는 객체는 Deflater 의 메소드를 활용한 것인데 Deflater 는 네이티브 영역의 메모리(C heap) 을 사용한다. 즉, 특정 케이스로 인하여 정상적으로 종료되지 않았을 경우 스트림이 닫히질 않아 네이티브 영역의 메모리가 쌓이게 되고 그로 인하여 컨테이너 전체의 OOM 이 발생하여 OOM Killer 에 의해 POD 가 자동으로 재 기동 된 것이다. 네이티브 메모리 영역 관련해서는 JDK8 의 Metaspace 영역 을 참고하자.

 

해결방법

GZIPOutputStream의 디플레이터로 인한 기본 메모리 누수 해결 을 참고해보면 기본 Deflater 의 close() 메소드를 선언하지 않아서 문제가 발생한다고 되어있다. 그러므로 GZIPOutputStream 의 finish() 호출 후 내부의 기본 Deflater 를 close 하도록 코드를 수정하였다.

1. try-with-resources 구문으로 대체 아이템 9. try-finally보다는 try-with-resources를 사용하라 (with. Autocloseable)

    * OpenJDK 18 부터는 finally 를 더 이상 사용하지 않는다고 합니다. (https://openjdk.org/jeps/421)

 

2. 코드예시 (Autocloseable 로 인하여 gzipOutputStream.finish() > GZIPOutputStream close > ByteArrayOutputStream Close 순으로 자원을 반납)

 

결과

적용 후 안정적으로 메모리가 확보된 것을 확인할 수 있다.

 

 

참고

  • https://stackoverflow.com/questions/55790261/closing-gzipoutputstream-after-bytestream-copy-in-finally-block-break-zip
  • https://recepinanc.medium.com
  • https://bugs.java.com
  • https://kubernetes.io
  • https://www.ibm.com/
  • https://devguli.com/

 

 

반응형
Comments