Programming/Linux

(Linux) Too many open files 에러가 발생하는 경우

Jan92 2023. 10. 18. 23:40

Too many open files 에러가 발생하는 경우

 

Too many open files

해당 포스팅은 Linux 서버에서 발생하는 'Too many open files' 에러에 대한 해결 방법과 해당 에러가 발생할 수 있는 상황에 대해서 살펴본 내용입니다.

 

 


File Descriptor / open files

Too many open files 에러에 대해 살펴보기 위해서는 먼저 '파일 디스크립터(File Descriptor / open files)'에 대해서 알아야 하는데요. Linux에서는 파일을 열면(open) 파일 디스크립터를 반환하며, 반환된 파일 디스크립터는 파일을 읽고 쓰는 데 사용됩니다.

 

문제는 리눅스 환경에서 Java의 소켓 통신(HTTP, API, JDBC 커넥션 등) 또한 파일로 취급된다는 것인데요.

때문에 소켓을 열 때마다 파일 디스크립터가 증가하고, 파일 디스크립터의 개수가 시스템 제한을 초과하는 경우 해당 에러가 발생하게 되는 것입니다.

 

***

정리하자면 'Too many open files' 예외가 발생했다는 것은 OS 및 JVM 프로세스의 시스템이 파일 디스크립터가 부족한 상황에서 실행되고 있다는 것입니다.

 


해결 방안

해당 문제는 '파일 디스크립터의 개수가 시스템 제한을 초과하는 경우' 발생하기 때문에, 문제를 해결하기 위해서는 쉽게 시스템의 오픈 가능한 파일 제한(limit)을 늘려주면 됩니다.

 

* 문제를 해결하는 것은 간단하지만 발생 원인에 대해 생각해 보는 것은 꼭 필요한 부분이며, 아래 이어지는 내용을 통해 다뤄볼 예정이니 함께 봐주시면 좋을 것 같습니다.

 

Hard Limit, Soft Limit

설정할 Limit에는 Soft Limit, Hard Limit 두 가지가 있는데요.

 

'Soft Limit'는 non-root 계정에서도 설정이 가능하며, 일시적으로 이를 넘을 경우 시스템 상에서 경고 메일 정도만 보낼 뿐 큰 문제가 되지 않습니다.

반면 'Hard Limit'는 root 계정에서만 설정할 수 있으며, 설정된 값을 절대로 넘을 수 없습니다.

따라서 Too many open files가 발생하는 경우, 프로세스에 기본적으로 할당된 Hard Limit를 초과했다고 볼 수 있는데요.

 

//Hard Limit 조회
$ ulimit -aH

//Soft Limit 조회
$ ulimit -aS

 

Hard Limit, Soft Limit는 다음 명령어를 통해 조회할 수 있습니다.

 

//open files limit 즉시 적용
$ ulimit -n 4096

open files 값에 대한 limit를 즉시 변경 적용하고 싶은 경우 위 명령어를 사용할 수 있으며, 이렇게 적용된 값은 시스템이 재시작되면 다시 원래대로 변경됩니다.

 

 

// '/etc/security/limits.conf' 파일에 추가하는 코드
root           hard    nofile          1024
root           soft    nofile          1024
{특정계정}       hard    nofile          1024
{특정계정}       soft    nofile          1024

open files 값에 대한 limit를 영구적으로 변경하고 싶은 경우 '/etc/security/limits.conf' 파일에 위와 같은 코드 추가가 필요한데요.

(*) 문자를 통해 특정 계정이 아닌 모든 계정에 대해 설정을 적용할 수 있으며, root의 경우 (*)에 속하지 않기 때문에 따로 추가되어야 합니다.

해당 코드를 추가하여 저장한 뒤 세션을 재시작하면 해당 변경사항이 적용되는 것을 확인할 수 있습니다.

 

 

***

여기서 한 가지 중요한 것은 이때 설정하는 limit 값은 시스템 전체의 limit 값보다는 작아야 한다는 것인데요.

시스템 전체 limit 값은 아래 명령어로 확인 가능합니다.

 

// system limit 확인
$ cat /proc/sys/fs/file-max

 

 

+++

추가적으로 프로세스의 리소스 제한에 대한 확인 및 변경이 필요한 경우 아래 명령어를 통해 진행할 수 있습니다.

//프로세스 pid 확인
$ ps -ef | grep java

//프로세스의 리소스 제한 확인
prlimit --nofile --output RESOURCE, SOFT, HARD --pid {target pid}

//프로세스의 리소스 제한 변경
prlimit --nofile=65535 --pid={target pid}

 


원인이 될 수 있는 경우

'Too many open files' 관련된 내용을 구글링 해보면 limit를 변경하는 해결책만 이야기되는 경우가 많은 것 같은데요.

앞서 말했던 것처럼 문제를 해결하는 것도 중요한 부분이지만, 근본적인 문제의 원인에 대해 생각해 보는 것도 필요하다고 생각하여 내용을 추가하게 되었습니다.

 

먼저 일시적인 요청 과부하에 의해 해당 에러가 발생하는 경우는 제한된 '파일 디스크립터(File Descriptor / open files)'를 늘려서 해결하는 것이 맞는데요.

 

***

만약 요청 과부하가 원인이 아니라면 socket, input/output stream, 파일 등을 사용 후 close가 제대로 되지 않아 File Descriptor가 누적되어 해당 오류가 발생한 것일 수도 있습니다.

 

이 경우에는 단순히 limit를 변경하는 것이 아니라 해당 원인이 되는 코드를 수정하는 것이 올바른 해결책인데요.

아래 잘못 사용된 RestTemplate으로 인해 해당 오류가 발생하는 상황을 예시로 만들어보았습니다.

 

@Slf4j
@RestController
@RequiredArgsConstructor
public class TmofController {

    @GetMapping("/tmof")
    public String tmof(@RequestParam String index) {
        RestTemplate restTemplate = new RestTemplateBuilder()
                .setConnectTimeout(Duration.ofMinutes(30))  // 30분
                .setReadTimeout(Duration.ofMinutes(30))     // 30분
                .build();

        log.info("index: " + index);

        return restTemplate.getForObject("http://localhost:8082/target?index="+index, String.class);
    }
}

(TmofController class)

 

첫 번째 서버(port:8081)의 예시 Controller입니다.

RestTemplate의 경우 Bean으로 등록하여 사용하는 것이 정상적이지만, 여기서는 소켓이 계속 생성되어 열린 상태를 만들기 위해 'new RestTemplateBuilder().build()'를 통해 요청 시마다 새로운 객체를 계속 생성하여 타겟이 되는 서버(port:8082)로 요청을 보내고 있습니다.

 

 

@Slf4j
@RestController
public class TargetController {

    @GetMapping("/target")
    public String target(@RequestParam String index) throws InterruptedException {
        log.info("index: "+ index);

        Thread.sleep(20 * 60 * 1000);    //1200000초(20분) 딜레이
        return "target";
    }
}

(TargetController class)

 

두 번째로 타겟이 되는 서버(port:8082)의 예시 Controller입니다.

여기서는 요청이 들어왔을 때 'Thread.sleep()'을 통해 소켓을 일정 시간 동안 열어두게 됩니다.

(Timeout으로 인해 소켓이 닫히는 것을 방지하기 위해 요청을 보내는 서버의 RestTemplate의 ConnectTimeout, ReadTimeout은 Thread.sleep() 시간보다 길게 잡았습니다.)

 

 

#!/usr/bin/env bash

for num in {0..110}
do
  curl localhost:8081/tmof?index=$num &
done

그리고 다음과 같은 쉘 스크립트(Shell script) 파일을 통해 첫 번째 서버의 예시 컨트롤러 경로로 해당 요청을 반복하여 보내게 됩니다.

그 결과 아래와 같이 첫 번째 서버에서 Too many open files 예외가 발생하는 것을 확인할 수 있었습니다.

 

***

해당 테스트는 낮은 성능의 서버에서 진행되어 Hard Limit, Soft Limit 값을 256으로 준 뒤, 위 쉘 스크립트 파일을 2~3회 실행(약 2~300 회의 api 요청)하여 아래와 같은 결과를 발생시켰습니다.

 

java.io.IOException: Too many open files

* 위 쉘 스크립트는 bash 명령어를 통해 실행되어야 하며, sh 명령어를 통해 실행되는 경우 '{0..110}' 반복 범위 자체를 변수로 읽어버려 반복 요청이 실행되지 않게 됩니다.

 

 

여기까지 리눅스 서버에서 발생하는 'Too many open files' 에러에 대한 해결 방법과 발생할 수 있는 원인에 대해서 살펴봤는데요.

내용 중 잘못된 부분이 있다면 댓글로 남겨주시면 확인하여 수정하겠습니다. 감사합니다.

 

 

 

< 참고 자료 >

https://techblog.woowahan.com/2569/
https://markruler.github.io/posts/java/too-many-open-files/