2024년 9월 8일
이론
조회 : 173|4분 읽기
Nginx "Too Many Open Files" 오류 해결 과정
개요
최근 서버에서 "Too many open files" 오류가 발생하면서 웹 서비스가 정상적으로 동작하지 않는 문제가 발생했습니다. 특히, 클라이언트의 요청에 대한 응답이 지연되거나 실패하는 경우가 잦았습니다.
처음에는 다양한 원인을 의심했습니다.
클라이언트에서 겪은 문제
- 변경하지 않은 상태에서 CORS 에러 발생
- 404, 403과 같은 다양한 상태 코드 에러
- 정상적으로 HTML은 렌더링되지만, 서버 호출이 이루어지지 않음
- 또 어떤 페이지로는 이동이 안됨
서버에서 확인한 문제
- 서버 로그에서 캐시 오류 발견 → 해당 API의 @Cacheable을 삭제했지만 동일한 오류 발생
- 모든 요청이 에러로 처리되지 않고 특정 상황(예: 새로고침 3번 후나 로그인 후)에만 오류가 발생
- 일부 사용자에 따라 다르게 나타남 (특정 사용자만 오류를 경험)
원인을 찾지 못하고 있던 도중, Nginx의 로그를 확인하면서 의심스러운 오류를 발견하게 되었습니다. 이 오류는 주로 파일 디스크립터(file descriptor) 제한과 관련이 있었으며, 이번 글에서는 이를 해결한 과정을 정리하고, 과거에 유사한 문제가 외부 API 연동에서 발생했을 때의 해결 경험도 함께 공유하려고 합니다.
발생한 오류 예시
1. 오류 로그:
2024/09/02 14:44:44 [alert] 3180895#0: *6111 socket() failed (24: Too many open files) while connecting to upstream, client: 111.111.111.111, server: api.wjdghks.com, request: "GET /search/v1/getMain HTTP/1.1", upstream: "http://127.0.0.1:1111/search/v1/getMain", host: "api.wjdghks.com", referrer: "https://api.wjdghks.com/"
2. 추가적인 오류:
2024/09/02 14:39:48 [crit] 3180895#0: accept4() failed (24: Too many open files)
이러한 오류는 클라이언트 요청을 처리하는 과정에서 Nginx가 사용할 수 있는 파일 디스크립터 한도를 초과해 발생한 문제였습니다.
따라서 정적 파일인 js, image, html 등을 간헐적으로 클라이언트에 서빙하지 못하거나 요청을 정상적으로 보내지 못하였습니다.
외부 API 연동 문제
사실, 이전에 외부 API 연동 시에도 유사한 문제가 발생한 적이 있습니다. 우리는 RestTemplate을 사용해 외부 API와 통신하고 있었습니다.
문제는 갑작스럽게 외부 API 서버가 응답하지 않으면서 서버가 과부하되었고, 서비스가 중단되었습니다. 응급으로 서버를 재부팅하면 일시적으로 문제는 해결되었지만, 얼마 지나지 않아 다시 같은 문제가 발생했습니다.
RestTemplate은 HTTP 요청 시 소켓을 사용하여 통신하므로, 소켓이 열린 상태에서 서버가 다수의 파일 디스크립터를 소비하게 됩니다. 결국, 이는 파일 디스크립터 한계에 도달하는 문제로 이어졌습니다.
이 문제를 해결하기 위해 서버의 파일 디스크립터 수를 늘리는 방식으로 방어하였고, 이를 통해 소켓 통신과 파일 디스크립터 문제가 밀접하게 관련되어 있음을 알게 되었습니다.
다음은 당시 발생했던 Java 오류입니다.
java1java.io.IOException: Too many open files 2 at java.base/sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) ~[na:na] 3 at java.base/sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:533) ~[na:na] 4 at java.base/sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:285) ~[na:na] 5 at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:469) ~[tomcat-embed-core-9.0.36.jar!/:9.0.36] 6 at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:95) ~[tomcat-embed-core-9.0.36.jar!/:9.0.36] 7 at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
이처럼, 외부 API 연동 시에도 파일 디스크립터 문제가 서버 안정성에 중요한 역할을 한다는 것을 알게 되었습니다.
원인 분석
이 문제는 시스템의 파일 디스크립터(file descriptor) 제한과 관련이 있었습니다.
파일 디스크립터는 운영체제가 프로세스에서 파일, 소켓 등 리소스를 처리하기 위해 사용하는 고유한 식별자입니다. 기본적으로 리눅스 시스템에서 설정된 파일 디스크립터 수는 제한되어 있으며, 이 수를 초과하면 더 이상 파일이나 소켓을 열 수 없습니다.
기본 설정에서의 파일 디스크립터 수는 대개 1024로 설정되어 있었는데, 이는 트래픽이 많은 환경에서는 매우 부족한 값이었습니다.
시스템 설정 확인
우선, 서버에서 현재 설정된 파일 디스크립터의 한도를 확인했습니다.
bash1ulimit -n
이 명령을 통해 현재 사용 가능한 최대 파일 디스크립터 수를 확인할 수 있었습니다. 설정된 값은 기본적으로 1024로, 트래픽이 많은 환경에서는 매우 부족한 값이었습니다.
해결 방법
1. 시스템 파일 디스크립터 한도 조정
시스템의 파일 디스크립터 한도를 늘리기 위해, /etc/security/limits.conf 파일을 수정했습니다.
bash1* soft nofile 65536 // 사용자가 일반적으로 사용할 수 있는 최대 개수 2* hard nofile 65536 // 파일 디스크립터의 절대 최대값
이 설정을 통해 각 사용자의 파일 디스크립터 한도를 65536으로 늘렸습니다.
2. Nginx 설정 수정
다음으로, Nginx 설정에서 파일 디스크립터 수를 늘리기 위해 worker_rlimit_nofile을 설정했습니다.
nginx1worker_rlimit_nofile 65536; 2events { 3 worker_connections 65536; 4}
이 설정은 Nginx가 사용할 수 있는 파일 디스크립터 수를 65536으로 늘리는 설정입니다. 이를 통해 Nginx가 더 많은 클라이언트 요청을 처리할 수 있게 되었습니다.
3. Nginx 캐시 설정
또한, Nginx에서 자주 열리는 파일을 캐시하여 파일 디스크립터 부담을 줄일 수 있도록 open_file_cache 설정을 적용했습니다.
nginx1open_file_cache max=1000 inactive=20s; 2open_file_cache_valid 30s; 3open_file_cache_min_uses 2; 4open_file_cache_errors off;
이 설정은 Nginx가 자주 사용하는 파일을 캐시하여, 파일을 매번 다시 여는 대신 캐시에서 가져올 수 있도록 최적화합니다. 이는 파일 디스크립터 사용을 줄이는 데 매우 효과적입니다.
4. RestTemplate 개선
앞서 언급한 외부 API 연동 문제에 대해, RestTemplate 사용 시 소켓 연결을 효과적으로 관리하기 위해 타임아웃 설정과 커넥션 풀링 등을 도입하여 서버 과부하를 방지할 수 있었습니다.
java1RestTemplate restTemplate = new RestTemplate(); 2restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
java1import org.apache.http.client.config.RequestConfig; 2import org.apache.http.impl.client.CloseableHttpClient; 3import org.apache.http.impl.client.HttpClients; 4import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 5import org.springframework.web.client.RestTemplate; 6 7public class RestTemplateConfig { 8 9 public RestTemplate restTemplate() { 10 // RequestConfig로 타임아웃 설정 (밀리초 단위) 11 RequestConfig requestConfig = RequestConfig.custom() 12 .setConnectTimeout(5000) // 연결 타임아웃 (5초) 13 .setSocketTimeout(5000) // 소켓 타임아웃 (5초) 14 .setConnectionRequestTimeout(5000) // 연결 요청 타임아웃 (5초) 15 .build(); 16 17 // HttpClient에 타임아웃 적용 18 CloseableHttpClient httpClient = HttpClients.custom() 19 .setDefaultRequestConfig(requestConfig) 20 .build(); 21 22 // HttpComponentsClientHttpRequestFactory에 HttpClient 설정 23 HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); 24 25 // RestTemplate에 factory 설정 26 RestTemplate restTemplate = new RestTemplate(factory); 27 28 return restTemplate; 29 } 30}
이처럼 소켓 관련 설정을 관리함으로써 서버가 안정적으로 외부 API를 연동할 수 있도록 했습니다.
해결 결과
이와 같은 설정을 적용한 후, 더 이상 "Too many open files" 오류가 발생하지 않았습니다. 또한, 서버 성능과 안정성도 크게 개선되었습니다. 외부 API 연동 시 발생했던 문제도 파일 디스크립터 한도 조정과 RestTemplate 개선을 통해 해결되었습니다.
이미지 예시
다음은 오류 발생 시의 로그 예시입니다.
결론
이번 글에서는 Nginx에서 발생한 "Too many open files" 오류와 외부 API 연동 시 발생한 문제를 어떻게 해결했는지에 대해 다루었습니다.
서버에서 처리할 수 있는 파일 디스크립터 한도는 특히 트래픽이 많을 때 중요한 요소입니다. 시스템 및 애플리케이션 설정을 통해 이를 최적화하는 것은 서버 성능과 안정성을 높이는 데 필수적입니다.