본문 바로가기
서버연구 블로그 서버연구 블로그

ulimit 설정이 웹서버 동시접속에 미치는 영향

읽는 시간 약 17분

웹서버의 동시접속 수를 늘리려고 Nginx의 worker_connections 값을 크게 잡았는데, 실제로는 일정 구간부터 접속이 더 늘지 않는 경우가 있습니다.

설정 파일에는 분명 수만 개의 연결을 처리하도록 해놨는데 로그에는 갑자기 이런 메시지가 나타납니다.

Too many open files

이때 Nginx 설정만 계속 만져도 해결되지 않습니다.

Linux에서는 프로세스가 열 수 있는 File Descriptor 수에 제한이 있고, 웹서버 역시 그 제한 안에서만 socket을 만들 수 있기 때문입니다.

그래서 웹서버 동시접속을 볼 때는 worker_connections와 함께 ulimit을 같이 봐야 합니다.

다만 여기서 한 가지 더 중요한 게 있습니다.

ulimit을 크게 올린다고 동시접속자가 그 숫자만큼 늘어나는 것은 아닙니다.

실제로 어떤 제한에 걸리는지부터 봐야 합니다.

Nginx 연결도 결국 File Descriptor를 사용한다

웹브라우저가 Nginx에 TCP 연결을 하나 만들면 Nginx 입장에서는 하나의 socket이 생깁니다.

Linux에서는 이 socket도 File Descriptor로 관리합니다.

대략 이런 구조입니다.

Client
   │
   │ TCP Connection
   ▼
Nginx
   │
   └── File Descriptor 사용

문제는 File Descriptor가 socket에만 사용되는 게 아니라는 점입니다.

Nginx 프로세스는 로그 파일, 설정 관련 파일, listening socket, upstream 연결 같은 곳에서도 FD를 사용합니다.

그래서

ulimit -n = 1024

라고 해서 정확히 1,024명의 사용자를 받을 수 있다는 뜻은 아닙니다.

실제 사용할 수 있는 연결 수는 그보다 작아질 수 있습니다.

worker_connections만 크게 올리면 왜 안 될까

Nginx에는 다음과 같은 설정이 있습니다.

events {worker_connections 4096;}

처음 보면 worker 하나가 4,096명의 사용자를 받을 수 있는 것처럼 보입니다.

하지만 worker_connections는 단순한 사용자 숫자가 아닙니다.

Nginx worker가 동시에 열 수 있는 전체 연결 수에 가깝습니다.

그리고 이 숫자는 운영체제가 해당 프로세스에 허용한 open file limit를 넘을 수 없습니다.

예를 들어 Nginx 설정이

worker_processes 4;

events {worker_connections 10000;}

이라고 해보겠습니다.

계산만 하면

4 × 10,000 = 40,000 connections

처럼 보입니다.

그런데 실제 Nginx worker의 FD limit가 1,024라면 이야기가 달라집니다.

cat /proc/$(pgrep -o nginx)/limits | grep "open files"

환경에 따라 다음처럼 나올 수 있습니다.

Max open files            1024                 4096

이 상태에서는 worker_connections 10000이라는 설정을 넣어도 OS 쪽 한도가 먼저 걸립니다.

즉, Nginx 설정값을 크게 만든 것과 실제로 그만큼 연결을 열 수 있는 것은 별개의 문제입니다.

Reverse Proxy에서는 연결 하나가 더 단순하지 않다

Nginx를 정적 웹서버로만 사용하는 경우보다 Reverse Proxy로 사용할 때 계산이 더 까다롭습니다.

사용자가 Nginx에 연결하고, Nginx가 다시 WAS로 연결한다고 해보겠습니다.

Client
  │
  │ socket ①
  ▼
Nginx
  │
  │ socket ②
  ▼
WAS

하나의 요청을 처리하는 과정에서 client 쪽 연결과 upstream 쪽 연결이 동시에 존재할 수 있습니다.

그래서 단순하게

worker_connections = 최대 사용자 수

라고 계산하면 실제 상황과 차이가 날 수 있습니다.

예를 들어 worker 하나가 10,000개의 connection을 처리하도록 되어 있다고 해서 Reverse Proxy 환경에서 사용자 10,000명을 항상 동시에 처리할 수 있다는 뜻은 아닙니다.

Keep-Alive나 upstream connection 재사용 여부에 따라 실제 FD 소비 방식도 달라집니다.

이 부분 때문에 Nginx 동시접속 계산은 숫자 하나만 보고 판단하면 자주 틀립니다.

지금 웹서버가 FD를 얼마나 쓰는지 먼저 본다

설정값을 올리기 전에 현재 사용량부터 확인하는 편이 좋습니다.

Nginx worker PID를 확인합니다.

pgrep -a nginx

예를 들어 worker PID가 18432라면 현재 열린 FD 수를 볼 수 있습니다.

ls /proc/18432/fd | wc -l

결과가

3847

이라면 해당 worker가 현재 약 3,847개의 FD를 사용하고 있다는 뜻입니다.

같은 프로세스의 limit도 바로 확인합니다.

cat /proc/18432/limits | grep "open files"

예를 들어

Max open files            4096                 65535

이고 현재 FD가 3,847개라면 꽤 위험한 상태입니다.

아직 서비스가 살아 있어도 조금만 연결이 더 늘면 새로운 socket을 만들지 못할 가능성이 있습니다.

이런 식으로 현재 사용량과 limit를 같이 보는 것이 설정값 하나만 보는 것보다 훨씬 정확합니다.

실제 장애는 프로세스가 죽는 형태로만 나타나지 않는다

FD 한도에 도달했다고 Nginx가 바로 종료되는 것은 아닙니다.

오히려 더 애매하게 보일 때가 많습니다.

기존 연결은 계속 동작하는데 새로운 접속만 실패하거나, 간헐적으로 502·503 오류가 늘어날 수 있습니다.

Nginx error log에서는 이런 메시지를 볼 수 있습니다.

socket() failed (24: Too many open files)

또는

accept4() failed (24: Too many open files)

이 상태에서

systemctl status nginx

를 확인하면 서비스 자체는 active로 보일 수도 있습니다.

그래서 모니터링에서 프로세스 생존 여부만 보고 있으면 장애를 놓치기 쉽습니다.

프로세스가 살아 있다는 것과 새로운 연결을 정상적으로 받을 수 있다는 것은 같은 의미가 아닙니다.

터미널에서 확인한 ulimit이 Nginx 값과 다를 수 있다

이 부분에서 가장 많이 헷갈립니다.

SSH로 서버에 접속해서

ulimit -n

을 실행했더니

65535

가 나왔다고 해보겠습니다.

그러면 Nginx도 65,535라고 생각하기 쉽습니다.

하지만 Nginx가 systemd에서 실행되고 있다면 실제 서비스에 적용된 값은 다를 수 있습니다.

확인은 다음처럼 합니다.

systemctl show nginx | grep LimitNOFILE

예를 들어

LimitNOFILE=4096
LimitNOFILESoft=4096

이라면 shell에서는 65,535라고 보여도 실제 Nginx는 4,096 제한을 받고 있는 것입니다.

그래서 운영 서버에서는

SSH shell의 ulimit 보다
실제로 실행 중인 프로세스의 /proc/PID/limits

를 보는 편이 더 확실합니다.

systemd 환경에서는 LimitNOFILE을 봐야 한다

Nginx가 systemd 서비스로 동작한다면 override 설정을 사용할 수 있습니다.

systemctl edit nginx

예를 들어 다음과 같이 설정할 수 있습니다.

[Service]
LimitNOFILE=65535

설정을 변경한 뒤에는 systemd 설정을 다시 읽고 Nginx를 재시작해야 합니다.

systemctl daemon-reload
systemctl restart nginx

그다음 실제 프로세스에 적용됐는지 다시 확인합니다.

cat /proc/$(pgrep -o nginx)/limits | grep "open files"

여기서 중요한 것은 설정 파일을 수정했다는 사실이 아닙니다.

실행 중인 프로세스에 실제로 적용됐는지 확인하는 것까지가 작업입니다.

운영 서버에서는 이 마지막 확인을 빼먹어서 변경이 적용되지 않은 채 장애가 반복되는 경우가 있습니다.

Nginx에도 별도 FD 설정이 있다

Nginx에는 다음 설정도 있습니다.

worker_rlimit_nofile 65535;

예를 들어 전체 설정을 단순화하면 이런 형태가 될 수 있습니다.

worker_processes auto;

worker_rlimit_nofile 65535;

events {worker_connections 16384;}

그렇다고 숫자를 전부 크게 맞추는 것이 정답은 아닙니다.

실제로는 systemd의 LimitNOFILE, Nginx의 worker_rlimit_nofile, worker_connections, 현재 FD 사용량을 같이 봐야 합니다.

한쪽만 크게 바꿔놓으면 다른 제한이 먼저 걸릴 수 있습니다.

ulimit을 올렸더니 장애가 더 늦게 발생했다면

이 상황은 꽤 중요한 단서입니다.

예를 들어 기존 limit가

4096

이었고 매일 오후쯤 Too many open files가 발생했다고 해보겠습니다.

그래서 limit를

65535

로 올렸더니 장애가 며칠 동안 없어졌습니다.

겉으로 보면 해결된 것 같습니다.

그런데 FD 사용량을 보면

09:00   3,200
12:00   8,700
15:00  14,300
18:00  21,900

처럼 계속 증가하고 있다면 이야기가 다릅니다.

이것은 단순히 동시접속량이 많아서일 수도 있지만, FD가 정상적으로 반환되지 않는 FD Leak일 가능성도 있습니다.

limit를 올린 것은 장애가 발생하는 시간을 뒤로 미룬 것뿐입니다.

결국 65,535도 채울 수 있습니다.

이때는 어떤 FD가 증가하는지 확인해야 합니다.

lsof -nP -p PID

socket이 대부분이라면 TCP 상태도 같이 봅니다.

ss -tanp

특히 CLOSE_WAIT가 비정상적으로 계속 늘어나는 경우라면 애플리케이션이나 upstream 연결 정리 문제를 의심해볼 수 있습니다.

동시접속 증가와 FD Leak은 모습이 다르다

둘 다 FD가 증가하기 때문에 처음에는 비슷하게 보입니다.

하지만 시간 흐름을 보면 차이가 있습니다.

정상적인 트래픽 증가라면 보통 이런 형태입니다.

평소       FD 2,000
트래픽 증가 FD 12,000
트래픽 감소 FD 2,300

연결이 늘면서 FD도 올라갔다가 트래픽이 줄면 다시 내려옵니다.

반대로

09:00 FD 2,000
12:00 FD 5,000
15:00 FD 9,000
18:00 FD 14,000
21:00 FD 19,000

처럼 트래픽이 줄어도 FD가 계속 쌓인다면 정상적인 동시접속만으로 보기 어렵습니다.

그래서 운영 서버에서 FD 문제를 볼 때는 최대값 하나보다 시간에 따른 증가와 감소 패턴이 더 유용합니다.

ulimit을 크게 올린다고 서버 처리량까지 늘어나는 것은 아니다

여기서 가장 많이 하는 오해가 있습니다.

ulimit 4096 → 65535

로 변경하면 웹서버가 갑자기 16배 많은 사용자를 처리할 수 있을 것처럼 느껴집니다.

실제로는 그렇지 않습니다.

FD는 여러 제한 중 하나일 뿐입니다.

동시접속이 크게 늘어나면 CPU, 메모리, 네트워크 대역폭, upstream 서버의 connection limit, DB connection pool 같은 다른 자원이 먼저 한계에 도달할 수 있습니다.

예를 들어 Nginx는 30,000개의 연결을 정상적으로 받아도 뒤쪽 WAS가 1,000개의 연결밖에 처리하지 못한다면 병목은 Nginx가 아니라 WAS에서 발생합니다.

그래서 ulimit 튜닝의 목적은

서버 성능을 만들어내는 것 보다는

FD 제한이 불필요하게 먼저 병목이 되는 것을 막는 것

이라고 보는 편이 정확합니다.

시스템 전체 File Descriptor도 같이 봐야 한다

프로세스별 FD 제한 외에 Linux 전체에서 관리하는 file handle 상태도 있습니다.

시스템의 관련 한도는 다음처럼 확인할 수 있습니다.

cat /proc/sys/fs/file-max

현재 할당 상태는

cat /proc/sys/fs/file-nr

로 볼 수 있습니다.

대규모 웹서버에서는 특정 worker 하나의 FD만 보는 것으로 끝내지 않고 서버 전체 사용량도 같이 확인하는 편이 좋습니다.

여러 Nginx worker뿐 아니라 애플리케이션, DB, 모니터링 에이전트 등도 모두 같은 서버에서 FD를 사용하기 때문입니다.

실제 동시접속을 계산할 때는 여유를 둬야 한다

예를 들어 Nginx worker가 4개이고 각각 worker_connections 8192라고 해보겠습니다.

단순 계산으로는

4 × 8192 = 32768

입니다.

하지만 이 숫자를 그대로

동시 사용자 32,768명

이라고 적으면 실제 환경에서는 틀릴 가능성이 높습니다.

Reverse Proxy라면 upstream 연결이 있고, listening socket과 로그 파일 등의 FD도 필요합니다. Keep-Alive가 오래 유지되면 요청을 처리하지 않는 연결도 FD를 잡고 있을 수 있습니다.

결국 실제 용량을 잡을 때는 이론적인 최대값보다 운영 중 측정한 FD 사용량과 연결 구조를 기준으로 여유를 두는 편이 안전합니다.

운영 서버라면 저는 이 세 숫자를 같이 본다

웹서버에서 동시접속 문제가 의심될 때 ulimit -n 하나만 보는 것보다 다음 세 값을 한 화면에서 비교하면 훨씬 이해가 빠릅니다.

현재 FD 사용량
프로세스 FD Limit
현재 TCP Connection 수

예를 들어

현재 FD          15,840
Soft Limit       16,384
TCP Connection   15,100

이라면 FD limit가 실제 병목에 가까워지고 있다는 판단을 할 수 있습니다.

반대로

현재 FD           3,000
Soft Limit       65,535
TCP Connection    2,600

인데 웹서비스가 느리다면 FD부터 올릴 이유가 없습니다.

CPU, upstream, DB, I/O 같은 다른 병목을 찾는 게 맞습니다.

이렇게 현재 숫자와 limit를 비교해서 원인을 하나씩 제외하는 방식이 무작정 Kernel Parameter를 조정하는 것보다 훨씬 안전합니다.

마무리

ulimit은 웹서버 동시접속 성능을 결정하는 마법 같은 설정이 아닙니다.

하지만 값이 너무 낮으면 Nginx의 worker_connections를 아무리 크게 잡아도 운영체제의 File Descriptor 제한에 먼저 걸릴 수 있습니다.

반대로 ulimit을 크게 올렸다고 서버가 그만큼 많은 사용자를 처리할 수 있다는 뜻도 아닙니다.

CPU, 메모리, 네트워크, upstream, DB까지 결국 함께 버텨줘야 합니다.

그래서 웹서버의 동시접속을 볼 때는 숫자 하나를 크게 만드는 것보다

현재 FD 사용량이 얼마인지, 실제 프로세스 limit가 얼마인지, 연결이 정상적으로 반환되고 있는지를 먼저 확인하는 것이 좋습니다.

특히 limit를 올렸더니 장애가 사라진 것처럼 보인다면 한 번 더 확인해볼 필요가 있습니다.

FD 사용량이 다시 내려오고 있다면 용량 부족 문제일 가능성이 높고, 계속 증가만 하고 있다면 limit보다 FD가 왜 반환되지 않는지를 먼저 찾아야 합니다.

그 차이를 구분하는 것이 ulimit을 단순 설정값으로 보는 것과 실제 서버 장애 관점에서 보는 것의 차이입니다.

instone
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.