성능 튜닝: 10k+ 연결을 위한 Linux 커널 최적화

발행: (2025년 12월 21일 오전 06:30 GMT+9)
11 min read
원문: Dev.to

Source: Dev.to

Source:

Introduction

고성능 동시성 실시간 아키텍처에서는 성능 병목 현상이 애플리케이션 레이어에서 운영 체제로 자연스럽게 이동합니다. Gevent 또는 Eventlet 위에서 실행되는 잘 최적화된 Flask‑SocketIO 애플리케이션은 이론적으로 수만 개의 동시 연결을 처리할 수 있습니다. 하지만 기본 Linux 환경에서는 CPU나 메모리 자원이 포화되기 훨씬 전에 애플리케이션이 충돌하거나 연결을 받지 못하게 됩니다.

이 정체 현상은 Linux 커널이 기본적으로 범용 컴퓨팅에 맞게 튜닝되어 있어, 지속적인 TCP 연결을 대량으로 종료 지점으로 사용하는 경우에 최적화되지 않았기 때문입니다. 연결이 오래 지속되고 상태를 유지하는 WebSocket 서버에서는 자원 고갈이 다음과 같은 형태로 나타납니다:

  • 파일 디스크립터 제한
  • 임시 포트 고갈
  • TCP 스택 혼잡

아래 기사에서는 Flask‑SocketIO를 10 000 연결 장벽 너머로 확장하기 위해 필요한 커널 수준 튜닝을 구체적으로 설명합니다.

File Descriptors

Unix‑계열 운영 체제에서는 “모든 것이 파일이다”라고 합니다. 여기에는 TCP 소켓도 포함됩니다. 클라이언트가 서버에 연결하면 커널은 해당 소켓을 나타내는 파일 디스크립터(FD) 를 할당합니다.

  • 기본적으로 대부분의 Linux 배포판은 프로세스당 1024개의 열린 파일 디스크립터 제한을 적용합니다 – 이는 오래된 제약입니다.
  • WebSocket 서버의 경우, 대략 1 000명의 동시 사용자(로그 파일 및 공유 라이브러리용 디스크립터 몇 개 포함)만 연결해도 애플리케이션이 충돌하거나 다음과 같은 오류가 발생합니다
OSError: [Errno 24] Too many open files

커널은 다음 두 가지 제한을 구분합니다:

  • Soft limit – 사용자가 조정 가능한 상한.
  • Hard limit – root에 의해 설정된 절대 상한.

Verification

ulimit -n
# → 1024

Remediation

시스템 전체 (/etc/security/limits.conf):

* soft nofile 65535
* hard nofile 65535

systemd 서비스 (/etc/systemd/system/app.service):

systemd는 사용자 제한을 무시하므로, 유닛 파일에 명시적으로 정의해야 합니다:

[Service]
LimitNOFILE=65535

Ephemeral Ports

파일 디스크립터가 수신 연결을 제한한다면, 임시 포트송신 연결을 제한합니다. 이 구분은 Redis와 같은 메시지 브로커를 사용하는 Flask‑SocketIO 아키텍처에서 매우 중요합니다.

Flask 애플리케이션이 Redis에 연결하거나(Nginx가 Flask/Gunicorn 워커에 연결) 할 때 TCP 소켓을 엽니다. 커널은 임시 포트 범위에서 로컬 포트를 할당합니다.

  • 기본 범위는 종종 좁게 설정되어 있습니다(예: 32768–60999), 약 28 000개의 포트만 제공합니다.
  • 높은 처리량 상황—예를 들어 Flask 애플리케이션이 Redis에 공격적으로 퍼블리시하거나 Nginx가 대량 트래픽을 프록시하는 경우—서버는 사용 가능한 로컬 포트를 모두 소진할 수 있습니다.

Symptoms

  • 로그에 EADDRNOTAVAIL (Cannot assign requested address) 오류가 나타남.
  • Redis가 정상임에도 불구하고 Flask 애플리케이션이 Redis와 통신하지 못함.
  • Nginx가 업스트림에 소켓을 열 수 없어 502 Bad Gateway 반환.

Tuning

# 현재 범위 확인
sysctl net.ipv4.ip_local_port_range

범위를 확장하려면 /etc/sysctl.conf에 다음을 추가합니다:

net.ipv4.ip_local_port_range = 1024 65535

변경 사항 적용:

sudo sysctl -p

TIME_WAIT State

TCP 확장에서 가장 오해받는 부분은 TIME_WAIT 상태입니다. TCP 연결이 종료될 때, 종료를 시작한 쪽은 2 * MSL(Maximum Segment Lifetime) 동안 TIME_WAIT에 머무르며, 일반적으로 60 초입니다. 이는 지연된 패킷이 올바르게 처리되고 동일 포트에 대한 새로운 연결과 혼동되지 않도록 보장합니다.

클라이언트가 페이지를 지속적으로 새로 고치거나 재연결하는 등 고 churn 환경에서는 서버가 수만 개의 소켓을 TIME_WAIT 상태에 쌓을 수 있습니다. 이러한 소켓은:

  • 시스템 자원을 소비한다.
  • 4‑tuple(소스 IP, 소스 포트, 목적 IP, 목적 포트)을 잠궈 새로운 송신 연결을 방해한다.

tcp_tw_recycle사용 금지

예전 가이드에서는 net.ip... (이하 생략) 를 활성화하라고 권장했지만, 현대 커널에서는 이 옵션을 사용하면 심각한 네트워크 문제와 보안 위험이 발생합니다. (이후 내용은 원문을 그대로 유지합니다.)

Source:

v4.tcp_tw_recycle`. It was removed in Linux kernel 4.12 because it breaks connections for users behind NAT by aggressively dropping out‑of‑order packets.

tcp_tw_reuse – Safe alternative

net.ipv4.tcp_tw_reuse allows the kernel to reclaim a TIME_WAIT socket for a new outgoing connection if the new connection’s timestamp is strictly greater than the last packet seen on the old connection. This is safe for most internal infrastructure (e.g., Flask ↔ Redis).

Configuration (/etc/sysctl.conf):

# Allow reuse of sockets in TIME_WAIT state for new connections
net.ipv4.tcp_tw_reuse = 1

Apply:

sudo sysctl -p

Benchmarking WebSockets

Standard HTTP benchmarking tools like ab (Apache Bench) are useless for WebSockets. They measure requests per second, whereas the primary metric for WebSockets is concurrency (simultaneous open connections) and message latency.

  • Artillery – supports WebSocket scenarios.
  • Locust – can be scripted for persistent connections.

Test methodology

  1. Ramp‑up – Don’t connect 10 k users instantly; this triggers “thundering‑herd” protection or SYN‑flood defenses. Ramp up over minutes.
  2. Sustain – Hold the connections open for an extended period.
  3. Broadcast – While connections are held, trigger a broadcast event to measure the latency of the Redis back‑plane and the Nginx proxy buffering.

Interpretation

Failure pointLikely cause
~1024 usersFile‑descriptor limit still in effect
~28 000 usersEphemeral‑port range exhausted
>30 000 TIME_WAIT socketsChurn problem or missing tcp_tw_reuse

Observability

Observability is the only way to confirm that kernel tuning is effective. When running high‑concurrency workloads, monitor specific OS‑level metrics.

What to watch

  • process_open_fds for the Gunicorn/uWSGI process – if this line flattens at a specific number (e.g., 1024 or 4096) while CPU is low, you have hit a hard limit.
  • Socket state countsESTABLISHED, TIME_WAIT, etc.
    • ESTABLISHED should match your active user count.
    • TIME_WAIT spikes to 30 k+ → churn problem or need tcp_tw_reuse.
  • Allocated socketssockstat output.

Example commands:

# Open file descriptors used by the process (replace <pid>)
cat /proc/<pid>/fd | wc -l

# Socket statistics
ss -s

# Detailed socket list (filter by state)
ss -tan state established | wc -l
ss -tan state time-wait | wc -l

네트워킹 버퍼가 소비하는 메모리

If you are using iptables or Docker, the nf_conntrack table limits how many connections the firewall tracks.

# Check kernel log for conntrack table overflow
dmesg | grep "nf_conntrack: table full, dropping packet"

튜닝 (예시):

sysctl -w net.netfilter.nf_conntrack_max=131072

과도한 커널 튜닝의 위험

영역잠재적 문제영향
보안임시 포트 범위를 확대하면 포트 스캔이 약간 쉬워집니다 (프라이빗 VPC 내부에서는 무시해도 될 정도).
안정성파일 디스크립터 제한을 너무 높게 설정하면(예: 수백만) 애플리케이션의 메모리 누수가 전체 서버를 다운시킬 수 있습니다 (프로세스만이 아니라).
연결 추적nf_conntrack_max를 증가시키면 커널 메모리(RAM)를 소비합니다. 10만 개 이상의 추적 연결 상태를 저장할 수 있을 만큼 서버에 충분한 RAM이 있는지 확인하세요.

골든 룰:
sysctl 설정을 무작정 적용하지 마세요. 설정은 구성 관리 도구(Ansible, Terraform)를 통해 배포하고, 왜 필요한지 문서화하며, 부하 테스트로 검증하세요.

Flask‑SocketIO를 10 000+ 연결로 확장하기

높은 동시성을 달성하는 것은 소프트웨어 문제뿐만 아니라 시스템 엔지니어링 문제이기도 합니다. 기본 Linux 설정은 보수적으로 되어 있어 데스크톱이나 저트래픽 서버에 맞춰져 있습니다. 다음 항목을 체계적으로 조정하면:

  1. 파일 디스크립터 제한 (ulimit)
  2. 임시 포트 범위 (net.ipv4.ip_local_port_range)
  3. TCP TIME‑WAIT 재사용 (net.ipv4.tcp_tw_reuse)

OS가 동시에 많은 소켓을 처리할 수 있게 됩니다.

운영 준비 체크리스트

  • 파일 디스크립터 제한 – Gunicorn 프로세스에 대해 ulimit -n > 65535.
  • 임시 포트 범위net.ipv4.ip_local_port_range = 1024 65535.
  • TCP TIME‑WAIT 재사용net.ipv4.tcp_tw_reuse = 1.
  • TCP TIME‑WAIT 재활용net.ipv4.tcp_tw_recycle = 0 (또는 해당 키가 없도록).
  • Conntrack 테이블 – 상태 기반 방화벽을 사용하는 경우 nf_conntrack_max를 늘립니다.

예시 sysctl 설정

# /etc/sysctl.d/99-flask-socketio.conf
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0
net.netfilter.nf_conntrack_max = 131072

변경 사항 적용:

sudo sysctl --system

주의:

  • nf_conntrack_max를 올린 뒤 메모리 사용량을 모니터링하세요.
  • 열린 파일 디스크립터 수(lsof, cat /proc/<pid>/fd)를 확인하세요.
  • 예상 트래픽 하에서 시스템이 안정적인지 확인하기 위해 locustwrk와 같은 도구로 부하 테스트를 수행하세요.
Back to Blog

관련 글

더 보기 »