ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OOM: unable to create new native thread 추적하기
    JVM 2024. 9. 23. 02:51
    반응형

    1) 서론

    개인적으로 개발하던 애플리케이션에서 OOM이 발생했습니다.
     
    현상과 문제 되는 코드는 명확하게 발견했는데요.
     
    원인을 찾아나가는 과정 중에 배운 것을 공유합니다.


    2) OutOfMemoryError 발생

    JVM 기반 애플리케이션에서 발생했고, 더 이상 요청을 받을 수 없는 상태가 되었습니다.

    Caused by: java.lang.OutOfMemoryError: unable to create new native thread
    	at java.lang.Thread.start0(Native Method)
    	at java.lang.Thread.start(Thread.java:717)

     
    이유는 명확했습니다.
     
    스레드풀 생성에 singleton pattern이 적용되지 않았습니다. 한번만 초기화되고, 이후에는 초기화된 인스턴스를 재사용했어야 합니다. 하지만 요청마다 계속해서 생성된 것이 원인입니다.
     
    문제가 되는 스레드풀을 여러 번 생성해 보겠습니다.
     
    Kotlin function으로 작성했기 때문에 호출할 때마다 ThreadPoolExecutor 클래스를 초기화하게 됩니다. 즉 생성한 인스턴스를 어딘가에 할당하고, 재사용할 수 없는 구조입니다. 아래는 Java로 decompile 한 예시입니다.

    Kotlin -> Kotlin ByteCode -> Java Decompile

     
    Local PC 사양 상 아래와 같이 제한하여 재현해 봅니다

    1. 4천 회 반복하여 스레드 풀 생성
    2. 생성된 스레드풀의 스레드 활성화

     

    ExecutorService 인터페이스를 구현한 스레드 풀은 기본적으로 lazy initialization 입니다. 생성 시점에는 단순히 core, max thread pool size를 설정할 뿐 스레드를 생성하지 않습니다. 그렇기 때문에 print문을 이용해서 task를 전달하고, thread active 시킵니다.

     
    수행한 테스트를 Profiling 합니다.

     
    몇 가지 특징을 볼 수 있는데요.

    • pool-XX-thread 이름의 스레드들이 지속적으로 생성된 것을 확인할 수 있습니다
    • 스레드 풀 생성하는 getThreadPool() 함수에서만 2.61MB 메모리 사용했습니다

     
    Active thread는 어떻게 되었을까요?

    • 테스트 수행 전 JVM에서 관리하는 actvie thread는 11개입니다
    • 스레드 풀 생성 후 task를 할당했을 때 active thread는 4012개입니다

     
    정리해 보겠습니다.
     
    TPS 4천 일 때 초당 메모리를 2.61MB 사용, 스레드를 4012개 사용하게 됩니다. 만약 60초가 지속된다면 약 160MB 메모리 할당, 12만 개의 스레드를 생성하게 됩니다.
     
    당연히 OOM이 발생할 수밖에 없습니다.
     
     


    3) 그런데 왜 native thread 생성하는 걸까요?

    발생한 에러를 볼 때 메시지가 조금 특이합니다. Natvie thread를 생성할 수 없다고 합니다.

    Caused by: java.lang.OutOfMemoryError: unable to create new native thread
    	at java.lang.Thread.start0(Native Method)
    	at java.lang.Thread.start(Thread.java:717)

     
    여기서 Native thread는 Kernal thread 입니다. JVM은 스레드를 사용할 때 OS의 kernal thread를 사용합니다. 즉 JVM 애플리케이션에서 스레드를 무한정 만들고, OS에서 active 할 수 있는 thread 수를 넘어간다면 더 이상 JVM에서는 더 이상 할당받을 스레드가 없어지기 때문에 에러가 발생합니다.
     
    OpenJDK8 소스를 직접 확인해 봅니다.
     
    아래의 사진에서 쓰레드 풀에서 task 할당받고, Thread.start() 호출하면 내부적으로 C++로 구현된 start0() native method를 호출하는 것을 확인할 수 있습니다.

     
    아래와 같이 JVM_StartThread 호출합니다.

     
    아래 두 개의 코드를 확인했을 때 natvie thread는 OS에서 생성된 Kernel thread임을 알 수 있습니다.
     
    그리고 주석을 참고했을 때 너무 많은 스레드가 active 상태이기 때문에 메모리가 부족하면 OS thread가 null 일 수 있음을 경고하고 있습니다. 그리고 이때 OOM을 발생시키도록 권장하고 있습니다.

     
    아래에서 다시 최초로 호출한 caller JVM_StartThread 메서드로 돌아옵니다.
    위에서 언급한 과정들에서 native thread를 생성하지 못했다면 unable to create new native thread 에러 메시지를 발생시키는 것을 확인할 수 있습니다.

     

     

    위 코드의 전체적인 흐름은 아래와 같습니다.


     
    조금 복잡하니 정리해 봅니다.

    • JVM에서 사용하는 user level thread는 OS kerneal thread를 사용합니다.
    • 지나치게 많은 kernel thread의 active 상태로 인해서 메모리가 부족하다면 JVM은 Kernel thread를 할당받을 수 없습니다.
    • JVM은 스레드 생성에 실패하면 OOM을 발생시킨다.

    4) 그래서 왜 쓰레드를 생성하지 못 하나요?

    특이한점은 CPU, Memory 시스템 자원의 임계치가 도달하지 않았고 정상인데, 쓰레드를 생성하지 못 하고 있었습니다.

    CPU, Memory 가용치가 남았는데 무엇이 문제일까요?
     
    ulimit 명령어를 활용해서 프로세스별 자원을 확인합니다.

    $ ulimit -su -S && ulimit -su -H
    
    # 최소 (S)
    -s: stack size (kbytes)             8176 // 프로세스별 stack 사이즈
    -u: max user processes              2666 // 한명의 user가 사용할 수 있는 쓰레드 수
    
    # 최대 (H)
    -s: stack size (kbytes)             65520
    -u: max user processes              2666

     
    Maximum user processes은 각 OS user가 생성할 수 있는 최대 쓰레드 개수인데요. 만약 A프로세스 10개를 spring 계정이 모두 생성하게 된다면, A프로세스 10개는 최대 쓰레드를 2666개까지만 사용할 수 있습니다.

    초과하여 생성 시도하는 경우에는 쓰레드를 더 이상 생성할 수 없습니다. 그리고 앞서 본 것처럼 JVM 입장에서는 OS thread가 NULL이고 에러를 생성하게 됩니다.
     
    즉 CPU, Memory 자원의 할당량이 정상적일 때는 active thread 개수를 확인하는것이 중요합니다.


    5) 결론

    개발을 할 때 시스템 자원의 낭비가 되지 않도록 하는 것은 매우 중요합니다. 당연하게 여기던 것을 실수로 놓칠 수 있고, 큰 장애가 발생할 수도 있습니다.
     
    이러한 실수를 방지하기 위해서는 꼼꼼히 개발하는 것도 중요하지만, 모니터링을 통해서 미리 감지하고 예방하는 것도 중요하다고 생각합니다.
     
    불필요한 장애가 발생했지만 이로 인해서 많이 배울 수 있었습니다.


    참고 문헌

    반응형

    'JVM' 카테고리의 다른 글

    왜 JVM이 필요할까?  (8) 2020.06.06
    Java Memory Model(자바 메모리 모델)  (7) 2020.05.27
    [Java]JVM Architecture란?  (6) 2020.05.24

    댓글

Designed by Tistory.