[Java] - OOM by Direct Buffer Memory

janjanee·2024년 8월 8일
0

Java

목록 보기
18/19

문제상황

  • Kafka Producer 로 초당 100개의 메시지를 1분 이상 발행하면 Kafka Consumer 에서 아래와 같은 오류가 발생
    • OOM
    • Redis Connection Timeout
  • 에러 로그
Caused by: java.lang.OutOfMemoryError: Cannot reserve 2269664 bytes of direct buffer memory (allocated: 9970390, limit: 10485760)
  • 원인
    • Direct Buffer Memory limit이 대략 10MB 정도인데, 그 수치를 넘어서서 OOM이 발생함

Direct Buffer Memory

  • Java NIO 에서 해당 메모리를 사용해서 효율적인 방법으로 네트워크, 디스크에 데이터를 쓴다. (쓰기가 빠르다고 함)

  • java.nio.DirectByteBuffer 를 사용하는 곳에서 OOM - Direct Buffer Memory 가 발생할 수 있음

  • Direct Buffer Memory는 JVM Heap 영역 외부의 native memory 영역에서 별도로 관리

    • GC에 의해 해제되지 않음
    • 따라서, 직접 자원해제 하는 코드가 필요함
  • 기본값은?

    • Max Heap 에 따라 다름.
    • buildpack을 사용하여 빌드했을 시 default 10MB
  • 10MB -> 100MB로 늘리기로 결정

  • 적용법

    ByteBuffer directBuf = ByteBuffer.allocateDirect(100);

    또는

    -XX:MaxDirectMemorySize=100M

관련 라이브러리 코드 분석

Redis Connection Timeout

  • Redis Client로 사용중인 Lettuce 내부의 netty ByteBuf 객체 null 문제

  • CommandHandler.java → channelRead()-> this.buffer.refCnt() 에서 NPE 발생

  • private ByteBuf buffer;

    • ByteBuf 객체는 java.nio.ByteBuffer를 확장해서 만든 netty 클래스
    package io.netty.buffer;
    import java.nio.ByteBuffer;
    import java.nio.ByteOrder;
    ...
    
    public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf>, ByteBufConvertible {
      public ByteBuf() {
      }
  • CommandHandler.java → channelRead()-> input.release()

    • 해당 코드의 finally 에서 자원을 해제하는 코드가 존재함.
    • 그러나 A 컨슈머 스레드의 자원이 해제되기 전에 B 컨슈머 스레드가 read 하다가 memory가 부족해서 자원을 얻지 못하고 this.buffer = null이 발생한듯

Kafka Consumer polling 시 OOM 발생

Caused by: java.lang.OutOfMemoryError: Cannot reserve 2269664 bytes of direct buffer memory (allocated: 9970390, limit: 10485760)
	at java.base/java.nio.Bits.reserveMemory(Unknown Source) ~[na:na]
	at java.base/java.nio.DirectByteBuffer.<init>(Unknown Source) ~[na:na]
	at java.base/java.nio.ByteBuffer.allocateDirect(Unknown Source) ~[na:na]
	at java.base/sun.nio.ch.Util.getTemporaryDirectBuffer(Unknown Source) ~[na:na]
	at java.base/sun.nio.ch.IOUtil.read(Unknown Source) ~[na:na]
	at java.base/sun.nio.ch.IOUtil.read(Unknown Source) ~[na:na]
	at java.base/sun.nio.ch.SocketChannelImpl.read(Unknown Source) ~[na:na]
	at org.apache.kafka.common.network.PlaintextTransportLayer.read(PlaintextTransportLayer.java:103) ~[kafka-clients-3.7.0.jar:na]
	
  • read에서 크게 2가지 작업을 함

    • getTemporaryDirectBuffer → DirectByteBuffer 객체 생성
    • offerFirstTemporaryDirectBuffer → 자원 해제
  • IOUtil.read() → Util.getTemporaryDirectBuffer() → ByteBuffer.allocateDirect()

    • allocateDirect 에서 DirectByteBuffer 객체를 만드는걸 확인할 수 있음.
    • DirectByteBuffer를 생성해야 하는데 Memory 가 부족해서 OOM 발생

  • offerFirstTemporaryDirectBuffer() → free()

    • 자원해제 코드를 확인할 수 있음.


Ref

profile
얍얍 개발 펀치

0개의 댓글