프로젝트 진행 중 개발이 완료된 서버를 실제 운영을 진행하는 테스트 기간 중에 서버를 가동 후 특정 시간이 되면 OutofMemoryError와 함께 서버가 다운되는 현상이 발생하였다.
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:714)
at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1368)
메세지를 받으면 메세지를 큐에 담고 순차적으로 메세지와 매핑된 메소드를 ThreadPoolExecutor를 통해 실행하는 과정으로 진행되는데 장비의 가동률이 증가하는 시간에 받는 메세지의 수가 증가하고 이에 따라 메세지를 처리하기 위해 실행 중인 스레드가 증가하며 특정 스레드 수를 넘기면 에러와 함께 서버가 다운되는 현상이였다.
스레드의 평균 메세지 처리 시간은 1~3초 이다.
원인이 무엇인가?
대규모 트래픽으로 인한 서버가 다운되는 주된 원인은 메모리 오버플로우가 있다.
우리 서버는 메세지와 매핑된 메소드. 즉, Task를 처리하기 때문에 동시에 처리할 수 없는 메세지는 큐에 넣어 순차적으로 처리하게 되는데 이 때 큐에 담긴 메세지가 메모리 용량보다 많아져 메모리 오버플로우가 발생할 수 있다.
하지만 해당 서버는 다른 사이트에서 구동 중인 서버와 동일한 사양의 Linux 였고 타 사이트는 더 많은 메세지를 통신하지만 메모리 오버플로우가 발생하지 않았다. 물론 Linux 사양 역시 이 정도 부하는 거뜬히 버틸 수 있는 사양이었기 때문에 메모리 오버플로우가 원인은 아닌 것으로 판단했다. (서버 역시 다른 사이트 서버와 동일했기에 메모리 누수 문제가 발생할 가능성도 낮을 것으로 판단.)
또한 서버 다운시 발생한 에러 코드 또한 unable to create new native thread. 즉 새로운 스레드를 생성할 수 없다는 에러이기 때문에 스레드 생성에 문제가 발생한 것으로 방향을 잡았다.
그래서 JMX을 이용한 모니터링을 한 결과 메세지를 처리하기 위해 실행 중인 스레드가 4000개가 넘어가면 에러와 함께 서버가 다운되는 것을 확인하였다.
어떻게 해결하였는가?
Linux 시스템에서 각 JVM 스레드는 시스템 전체 및 사용자 전체 프로세스 수 제한이 적용된다는 점에서 "프로세스"로 계산된다. -출처 : https://kb.novaordis.com/index.php/Java_Threads#Java_Threads_and_Linux_Processes
Linux의 제한 설정으로 인해 JVM 스레드가 제한 설정 이상 생성되지 못 하는 것이 원인이였다.
Linux에서는 OS 레벨에서의 제한 설정이 있으며 리눅스 시스템 제한 설정과 사용자 별 제한 설정이 존재한다.
사용자 별 제한 설정은 ulimit 명령어로 확인할 수 있다.
/etc/security/limits.conf #사용자별 제한 설정 파일
#soft ulimit
ulimit -a
#hard ulimit
ulimit -aH
soft limit은 hard limit까지 자동으로 증가될 수 있으며
hard limit은 한 번 설정 되면 root 권한이 아닌 사용자에 의해 값이 증가할 수 없다.
즉 hard limit 이상의 스레드를 생성할 수 없다.
- open files : 프로세스에서 열 수 있는 파일 갯수 제한. 리눅스에서는 Socket도 파일로 취급하여 open files에 영향을 받는다. 즉, 네트워크 소켓 연결도 open file로 카운트 한다.
- max user processes : 사용자가 가질 수 있는 네이티브 스레드의 양을 제한한다. 리눅스에서는 JVM 스레드도 프로세스로 계산되어 max user processes의 영향을 받는다.
/etc/security/limits.conf의 nproc 값을 서버가 감당할 수 있을 정도로 증가시킴으로써 문제를 해결하였다.
번외
사용자 별 제한 설정 이외에 리눅스 시스템 전체 제한 설정도 존재한다.
전체 사용자의 프로세스나 스레드 수는 리눅스 시스템 전체 제한 설정을 넘을 수 없다.
/proc/sys/kernel/pid_max #커털에 할당할 수 있는 시스템 전체 프로세스 수 제한 설정
/proc/sys/kernel/threads-max #시스템 전체 스레드 수 제한 설정
- pid_max는 동시에 실행할 수 있는 최대 프로세스 수와는 관련이 없다. pid_max 는 커널이 할당할 수 있는 최대 숫자를 의미한다.
- threads-max는 커널이 관리하는 task_struct의 최대 수를 의미한다. 즉, 동시에 생성할 수 있는 최대 프로세스의 수 이다.
자바에서 생성하는 스레드는 사용자 수준 스레드이지만 JVM 내부적으로는 OS의 커널 수준 스레드와 매핑된다.
리눅스에서 스레드의 구현은 스레드를 에뮬레이트한 경량 프로세스이며 task_struct로 구현되기 때문에 리눅스 커널은 프로세스와 스레드 모두 프로세스로 본다.
즉, 자바에서 생성하는 스레드는 리눅스 커널 입장에서는 프로세스가 하나 더 추가되는 것과 다를게 없다.
그렇기 때문에 사용자의 max user processes 값에 영향을 받게 된다. max user processes 값 이상의 스레드=프로세스를 생성할 수 없다.
사용자가 생성하는 스레드는 리눅스 커널에서 프로세스로 보기 때문에 사용자는 max user processes 이상의 스레드를 생성할 수 없으므로
사용자의 스레드 수 < max user processes
모든 사용자의 프로세스 수는 커널이 할당할 수 있는 pid_max의 숫자를 넘길 수 없기 때문에
모든 사용자의 프로세스 수 < pid_max
모든 사용자가 실행 중인 스레드의 수는 커널이 관리할 수 있는 task_struct의 최대값을 넘길 수 없으므로
모든 사용자의 실행 중인 스레드 수 < max_threads
자바 스레드와 리눅스 스레드에 대한 자세한 내용은 해당 포스팅에서 설명한다.
2022.07.15 - [DevOps] - [JVM] 자바 스레드와 리눅스 스레드, LWP
Reference.
https://stackoverflow.com/questions/344203/maximum-number-of-threads-per-process-in-linux
https://woowabros.github.io/experience/2018/04/17/linux-maxuserprocess-openfiles.html
https://lannstark.tistory.com/101
https://blog.naver.com/crushhh/222281899021
https://letsmakemyselfprogrammer.tistory.com/98
https://blogshine.tistory.com/338
https://awesometic.tistory.com/15
'issue' 카테고리의 다른 글
[Gradle Error] A problem occurred evaluating initialization script 에러 문제 해결 (0) | 2023.08.28 |
---|