동시접속이 많은 웹 서버나 DB 서버를 운영하다 보면 애플리케이션 코드에는 문제가 없는데도 특정 부하 구간에서 연결이 끊기거나 응답이 느려지는 상황을 만난다. 원인을 따라가 보면 애플리케이션이 아니라 커널 기본값이 병목인 경우가 많다. TCP 백로그 큐가 기본값(128)으로는 순간적으로 몰리는 연결을 다 받지 못하거나, 파일 디스크립터 상한이 낮아 커넥션이 거부되는 식이다. 이런 커널 런타임 파라미터는 커널을 새로 빌드하거나 재부팅할 필요 없이 sysctl로 바로 조정할 수 있다. 이 글에서는 sysctl의 동작 원리와 자주 쓰는 튜닝 파라미터, 재부팅 후에도 유지되도록 영구 적용하는 방법을 정리한다.
sysctl 핵심 개념
sysctl은 커널이 /proc/sys 아래에 노출해둔 런타임 파라미터를 읽고 쓰는 인터페이스다. 예를 들어 vm.swappiness 파라미터는 실제로는 /proc/sys/vm/swappiness 파일과 같은 값을 가리키며, 점(.)으로 구분된 sysctl 이름은 슬래시(/)로 구분된 파일 경로와 그대로 대응한다. 즉 sysctl 명령은 이 파일들을 사람이 다루기 편하게 감싼 래퍼에 가깝다.
설정을 영구적으로 유지하려면 설정 파일을 쓰는데, Ubuntu는 여러 디렉터리를 순서대로 읽는다.
| 경로 | 용도 |
|---|---|
/etc/sysctl.d/*.conf | 관리자가 직접 추가하는 커스텀 설정 (권장 위치) |
/run/sysctl.d/*.conf | 부팅 시 임시로 생성되는 런타임 설정 |
/usr/lib/sysctl.d/*.conf | 패키지가 설치하는 기본 설정 |
/etc/sysctl.conf | 레거시 단일 설정 파일 (가장 나중에 적용됨) |
같은 파라미터가 여러 파일에 중복 설정돼 있으면 파일명을 알파벳 순으로 정렬했을 때 나중에 오는 파일의 값이 우선한다. 그래서 커스텀 설정은 99-custom.conf처럼 숫자 접두사를 크게 잡아 다른 설정보다 나중에 적용되도록 하는 관례가 흔하다.
여기서 다루는 sysctl 파라미터는 대부분 시스템 전체에 적용되는 전역 설정이라는 점도 알아둘 필요가 있다. 만약 컨테이너 하나에만 다른 swappiness 값을 주고 싶다면 전역 vm.swappiness 대신 cgroups의 memory.swappiness로 그룹 단위 오버라이드를 거는 편이 더 정확하다. sysctl과 cgroups는 서로 다른 계층에서 리소스를 제어하는 도구이므로, 시스템 전체 기본값은 sysctl로, 특정 서비스·컨테이너만의 예외는 cgroups로 나눠 관리하는 게 일반적이다.
실전 코드
현재 값은 sysctl 명령이나 /proc/sys 파일을 직접 읽어서 확인할 수 있다. 두 방법의 결과는 동일하다.
sysctl vm.swappiness
# 또는
cat /proc/sys/vm/swappiness
재부팅 전까지 즉시 적용해서 값을 테스트해보고 싶다면 -w 옵션을 쓴다. 이 방식은 재부팅하면 원래 값으로 돌아간다.
sudo sysctl -w net.core.somaxconn=4096
sudo sysctl -w vm.swappiness=10
테스트한 값이 만족스러우면 /etc/sysctl.d/에 설정 파일을 만들어 영구 적용한다. 웹 서버 환경을 예로 자주 쓰는 파라미터를 모아본다.
# /etc/sysctl.d/99-web-tuning.conf
# 대기 중인 연결 요청 큐 크기 (기본값 128은 트래픽이 몰리면 금방 찬다)
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
# TIME_WAIT 상태 소켓을 재사용해 포트 고갈 방지
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# 로컬에서 사용할 수 있는 임시 포트 범위 확장
net.ipv4.ip_local_port_range = 1024 65535
# 프로세스가 열 수 있는 파일 디스크립터 상한
fs.file-max = 2097152
# 메모리 여유가 있을 때 스왑을 최대한 늦게 쓰도록 설정
vm.swappiness = 10
네트워크 연결이 아니라 디스크 I/O가 많은 DB 서버라면 튜닝 대상이 달라진다. 커널은 쓰기 요청을 곧바로 디스크에 반영하지 않고 페이지 캐시에 더티 페이지(dirty page)로 쌓아뒀다가 일정 조건에서 한꺼번에 flush하는데, 이 임계치를 조정하면 순간적인 쓰기 폭주(I/O spike)를 완화할 수 있다.
# /etc/sysctl.d/98-db-tuning.conf
# 전체 메모리 중 이 비율을 넘으면 프로세스가 쓰기 작업 중 blocking됨 (기본 20)
vm.dirty_ratio = 10
# 전체 메모리 중 이 비율을 넘으면 백그라운드 flush를 시작함 (기본 10)
vm.dirty_background_ratio = 5
# 디렉터리/inode 캐시를 얼마나 적극적으로 회수할지 (기본 100, 낮을수록 캐시 유지 우선)
vm.vfs_cache_pressure = 50
dirty_background_ratio를 낮추면 커널이 더 자주, 더 적은 양씩 디스크에 flush하므로 한 번에 몰아서 쓰다가 I/O가 튀는 상황을 줄일 수 있다. 반대로 너무 낮추면 flush 빈도가 지나치게 잦아져 오히려 전체 처리량이 떨어질 수 있으므로, 이 값 역시 실제 워크로드로 측정하며 조정해야 한다.
파일을 저장한 뒤 --system 옵션으로 모든 sysctl.d 설정 파일을 우선순위대로 다시 읽어 적용한다.
sudo sysctl --system
실행하면 어떤 파일에서 어떤 값이 적용됐는지 순서대로 출력된다.
* Applying /usr/lib/sysctl.d/50-default.conf ...
* Applying /etc/sysctl.d/99-web-tuning.conf ...
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.ip_local_port_range = 1024 65535
fs.file-max = 2097152
vm.swappiness = 10
* Applying /etc/sysctl.conf ...
이후 sysctl net.core.somaxconn으로 실제 값이 반영됐는지 다시 확인하면 된다.
값을 바꿨다고 끝나는 게 아니라, 실제로 병목이 해소됐는지 확인하는 과정이 더 중요하다. 소켓 상태별 개수는 ss -s로, 시스템 전체 파일 디스크립터 사용량은 /proc/sys/fs/file-nr로 확인할 수 있다.
$ ss -s
Total: 812
TCP: 634 (estab 210, closed 380, orphaned 0, timewait 375)
$ cat /proc/sys/fs/file-nr
2688 0 2097152
file-nr의 세 값은 순서대로 현재 할당된 파일 디스크립터 수, 사용 중이지만 아직 회수되지 않은 수(커널 버전에 따라 항상 0), fs.file-max로 설정한 시스템 전체 상한이다. TIME_WAIT 소켓 수가 비정상적으로 많다면 앞서 설정한 tcp_tw_reuse·tcp_fin_timeout 튜닝이 실제로 효과가 있었는지 이 값의 변화 추이로 판단할 수 있다.
여기서 자주 헷갈리는 부분이 fs.file-max와 ulimit -n의 차이다. fs.file-max는 시스템 전체가 열 수 있는 파일 디스크립터 총합의 상한이고, ulimit -n(커널 파라미터로는 RLIMIT_NOFILE)은 프로세스 하나가 열 수 있는 개수 상한이다. fs.file-max를 아무리 크게 올려도 nginx나 애플리케이션 서버 프로세스의 ulimit -n이 낮게 잡혀 있으면(기본값 1024인 경우가 흔하다) 그 프로세스는 여전히 병목을 겪는다. systemd로 기동하는 서비스라면 유닛 파일에 LimitNOFILE= 값을 따로 지정해야 두 설정이 함께 맞물려 동작한다.
주의사항
- 보안 관련 파라미터는 성능을 이유로 함부로 끄지 않는다. 예를 들어
net.ipv4.tcp_syncookies를 0으로 설정해 SYN 쿠키를 비활성화하면 SYN flood 공격에 취약해진다. 성능 파라미터와 보안 파라미터를 구분해서 다뤄야 한다. - 같은 파라미터를 여러
.conf파일에 중복 정의하면 알파벳 순으로 나중에 로드된 파일이 이긴다. 여러 사람이 서버를 함께 관리하는 환경에서는 이 우선순위 규칙 때문에 의도치 않게 값이 덮어써질 수 있으므로, 커스텀 설정은 한 파일에 모아 관리하는 편이 안전하다. - 컨테이너 환경에서는
net.*계열 파라미터 상당수가 네트워크 네임스페이스 단위로 분리돼 있다. 호스트에서net.core.somaxconn을 조정해도 컨테이너 내부 값에는 반영되지 않을 수 있고, 반대로 컨테이너 안에서 수정을 시도하면 권한이 없거나(namespaced가 아닌 파라미터의 경우) 호스트 전체에 영향을 줄 수도 있다. 컨테이너를 쓴다면 해당 파라미터가 네임스페이스 단위인지 먼저 확인해야 한다. sysctl --system으로 재로드해도 이미 맺어져 있던 연결이나 이미 할당된 커널 자료구조에는 즉시 반영되지 않는 파라미터가 있다. 새로 생성되는 연결·프로세스부터 적용되는 값이 많으므로, 튜닝 효과를 확인할 때는 애플리케이션을 재시작하거나 새 연결을 기준으로 측정해야 한다.- 튜닝 값은 워크로드마다 최적치가 다르다. 여기서 소개한 값은 일반적인 웹 서버 기준 출발점일 뿐이므로, 프로덕션에 적용하기 전 스테이징 환경에서 부하 테스트로 실제 효과를 검증하는 과정을 거치는 것이 안전하다.
마무리
sysctl은 커널을 다시 빌드하거나 재부팅하지 않고도 네트워크 큐 크기, 스왑 정책, 파일 디스크립터 상한 같은 런타임 동작을 즉시 바꿀 수 있는 도구다. -w로 값을 테스트하고 /etc/sysctl.d/에 파일로 남겨 영구 적용하는 흐름만 익혀두면, 트래픽이 늘어난 서버에서 애플리케이션 코드를 건드리지 않고도 병목을 상당 부분 해소할 수 있다. 다만 값 하나하나가 성능과 보안 양쪽에 영향을 주므로, 파라미터의 의미를 이해하지 못한 채 인터넷에서 본 설정을 그대로 복사해 넣는 것은 피해야 한다.