ulimit 설정으로 서버 프로세스 제한 걸 때 알아야 할 것들

Views: 0

서버 운영에서 “ulimit”는 단순한 쉘 명령이 아니라, 커널 자원 관리와 서비스 안정성을 잇는 핵심 관문이다. 잘못 설정하면 “Too many open files”나 예기치 않은 프로세스 종료가 잦아지고, 반대로 과도하게 풀면 장애 전파 범위가 커진다. 이 글은 ulimit의 동작 원리부터 사용자·서비스별 영구 설정, systemd와 PAM의 연결, 커널 한도와의 관계, 그리고 현장에서 자주 겪는 함정과 해결책까지 체계적으로 정리한다.

ulimit의 기본 개념

ulimit는 현재 셸(프로세스)과 그 자식 프로세스에 적용되는 “프로세스 자원 한도”를 조회·변경하는 인터페이스다. 대부분의 셸에서 내장(builtin) 명령으로 제공된다.

  • soft limit: 현재 세션이 즉시 사용하는 값. 사용자/프로세스가 하드 한도 범위 내에서 올릴 수 있다.
  • hard limit: 상한선. 보통 관리자만 변경 가능하며, soft를 올릴 수 있는 최대치다.

대표 옵션

  • -n nofile: 한 프로세스가 동시에 열 수 있는 FD(파일 디스크립터) 수
  • -u nproc: 사용자별 생성 가능한 프로세스/스레드 수(커널/배포판에 따라 스레드 포함)
  • -c core: 코어덤프 파일 크기
  • -s stack: 스택 크기
  • -l memlock: 잠금 메모리 크기
  • -f fsize: 생성 파일 최대 크기
  • -v as: 가상 메모리 한도

현재 한도 확인

ulimit -a
cat /proc/$$/limits

“한도”는 누가 최종 결정하나: 적용 순서 이해

동일한 서버에서도 터미널, SSH, 서비스마다 한도가 달라질 수 있다. 이유는 적용 지점이 다르기 때문이다. 실무 감각으로 기억할 순서는 다음과 같다.

  1. 커널·cgroup 전역 상한
  • 커널 sysctl(예: fs.file-max, kernel.pid_max)과 cgroup(Task/PID/Memory 등) 한도가 절대 상한을 잡는다.
  • systemd 기반 시스템에서는 PID/Tasks 상한을 TasksMax=로, FD 상한을 각 서비스의 LimitNOFILE=로도 제어한다.
  1. 로그인 경로의 PAM limits
  • SSH, 콘솔 로그인 등 PAM을 통과하는 세션은 /etc/security/limits.conflimits.d/*.conf의 규칙을 적용받는다.
  • SSH에서 적용하려면 sshd_configUsePAM yes가 필수다.
  1. 셸 내 ulimit
  • 셸 프로파일(~/.bashrc, ~/.zshrc, /etc/profile 등)에서 ulimit를 올리면 해당 셸과 자식에게만 적용된다.
  1. 프로세스 상속
  • 부모가 가진 한도를 자식이 상속한다. 서비스 관리자는 부모가 systemd인지, 사용자인지에 따라 결과가 달라진다.

핵심: 전역 커널 상한 > cgroup/systemd > PAM > 셸 순서로 생각하면 충돌 원인 파악이 빨라진다.

로그인 사용자에 대한 영구 설정(PAM)

대부분의 사용자 세션은 PAM을 경유한다. 아래 방식이 표준적이다.

# /etc/security/limits.d/99-custom.conf
# <도메인>   <타입>   <아이템>     <값>
@developers  soft     nofile       65535
@developers  hard     nofile       65535
@developers  soft     nproc        4096
@developers  hard     nproc        8192
  • 도메인: 사용자명 또는 그룹명(@그룹)
  • 타입: soft/hard
  • 아이템: nofile, nproc 등
  • SSH에 적용하려면 /etc/ssh/sshd_configUsePAM yes 유지
  • susu -의 차이: su -는 로그인 셸처럼 PAM과 프로파일을 초기화하므로 한도 적용 결과가 달라질 수 있다.

검증

ssh user@host
ulimit -n
cat /proc/$$/limits | grep -E 'Max open files|Max processes'

서비스 단위 영구 설정(systemd)

데몬·백그라운드 서비스는 PAM 경로를 거치지 않는다. 따라서 systemd 유닛에 직접 한도를 명시해야 한다.

예시: Nginx의 FD 상한 조정

# /etc/systemd/system/nginx.service.d/limits.conf
[Service]
LimitNOFILE=200000
LimitNPROC=16384
TasksMax=infinity

적용

systemctl daemon-reload
systemctl restart nginx
systemctl show nginx -p LimitNOFILE -p TasksMax

중요 포인트

  • 소켓도 FD를 소비하므로 트래픽 높은 프록시/DB/메시징 서버는 LimitNOFILE을 넉넉히 잡는다.
  • 스레드 풀형 서버는 LimitNPROC(스레드 포함으로 계산되는 환경 다수)에 걸려 스레드 생성 실패가 날 수 있다.
  • 많은 서비스가 상위 슬라이스의 TasksMax를 상속한다. 전역 완화가 필요하면 /etc/systemd/system.conf 또는 user.confDefaultTasksMax=로 조정한다. 단, 무한대는 신중하게 사용한다.

커널·시스템 전역 한도와의 관계

  • fs.file-max: 시스템 전체에서 열 수 있는 FD 총량 상한. 프로세스별 nofile 합보다 작으면 병목이 생긴다.
sysctl fs.file-max
sysctl -w fs.file-max=2097152
  • kernel.pid_max: 시스템 전체 PID 상한. 사용자별 nproc을 키워도 PID 전역 상한에 막히면 프로세스/스레드 생성 실패가 난다.
sysctl kernel.pid_max
  • cgroup v2 환경: 많은 배포판이 기본 v2를 사용한다. 프로세스 수는 “PIDs 컨트롤러”로 제한되며, systemd의 TasksMax=로 쉽게 제어한다. 컨테이너 런타임도 cgroup 한도를 동시에 건다.

컨테이너 환경에서의 ulimit

  • Docker: 컨테이너 시작 시 --ulimit nofile=..., --ulimit nproc=...로 지정 가능. Docker 데몬의 기본 ulimit을 설정할 수도 있다.
  • Kubernetes: 컨테이너 런타임/CRI마다 노출 방법이 다르며, 종종 파드 보안 설정과 상호작용한다. 높은 FD가 필요한 애플리케이션은 노드 단(systemd) 한도와 런타임 한도를 함께 점검한다.
  • 컨테이너 내부에서 ulimit를 올려도, 상위 호스트의 cgroup/systemd 한도를 초과할 수 없다.

실무 설정 레시피

대용량 연결 처리용 FD 상향

목표: 웹 프록시/DB 프런트엔드의 “Too many open files” 제거

  1. 커널 전역 FD 풀 점검·확대
sysctl fs.file-max
sysctl -w fs.file-max=2097152
  1. 서비스 유닛에 한도 설정
[Service]
LimitNOFILE=200000
  1. 소켓/워커 설정 동반 조정
    애플리케이션의 워커 수, 이벤트 루프, DB 커넥션 풀도 함께 늘리지 않으면 기대 효과가 제한된다.
  2. 검증
systemctl show <svc> -p LimitNOFILE
lsof -p <pid> | wc -l

빌드 서버의 폭주 방지(nproc)

목표: CI 빌드/테스트가 노드를 고갈시키지 않도록 사용자별 프로세스 상한 설정

  1. PAM limits로 그룹 제한
@ci  soft nproc 4096
@ci  hard nproc 8192
  1. systemd 서비스(에이전트)가 스폰하는 작업이면 LimitNPROC=TasksMax=도 세팅
  2. 부작용 점검: 스레드 많은 JVM/Go/Node 작업이 에러를 내면 상한을 단계적으로 올린다.

디버깅을 위한 코어덤프 허용

목표: 장애 분석 시 코어 파일 확보

  1. 시스템 정책과 보안 요건 확인 후 제한 완화
ulimit -c unlimited
  1. 영구화는 PAM 또는 프로파일에 설정, systemd 서비스는 LimitCORE=infinity
  2. 저장 위치와 크기 정책: coredumpctl 또는 kernel.core_pattern 활용

자주 겪는 함정과 해결법

  • 셸에서 올렸는데 서비스에는 반영되지 않음
    서비스는 PAM/셸을 거치지 않는다. systemd 유닛의 Limit* 항목을 수정해야 한다.
  • SSH에선 적용되는데 su로 전환하면 초기화됨
    su는 비로그인 셸이라 PAM 규칙을 재적용하지 않는다. su -를 사용하거나 /etc/pam.d/su 구성을 확인한다.
  • FD를 크게 올렸는데 여전히 에러
    애플리케이션 자체의 FD 상한(예: JVM -XX:MaxFDLimit)이나 워커 수, 파일 핸들 캐시가 병목일 수 있다. 또한 fs.file-max가 충분한지 확인한다.
  • nproc이 스레드까지 묶여 OOM 비슷한 현상
    많은 배포판에서 스레드=프로세스로 계산한다. 스레드 많은 서버는 LimitNPROCTasksMax를 함께 조정한다.
  • 컨테이너에서만 실패
    호스트의 cgroup 한도 또는 런타임의 --ulimit 기본값이 낮을 수 있다. 노드 단 설정부터 확인한다.

운영 점검 체크리스트

  • 현재 프로세스 한도: cat /proc/$$/limits
  • 서비스 한도: systemctl show <svc> -p LimitNOFILE -p LimitNPROC -p TasksMax
  • 실패 로그: journalctl -u <svc> -b -p warning..emerg
  • 전역 자원: sysctl fs.file-max, sysctl kernel.pid_max
  • 실제 사용량 관찰: lsof -p <pid> | wc -l, ps -o nlwp= -p <pid>(스레드 수)

보안과 안정성 관점

  • 제한은 “문제의 확산”을 막는 안전장치다. 특히 멀티테넌시 서버, 공용 빌드 노드, 공개 API 프런트엔드는 합리적 상한이 필수다.
  • 반대로 핵심 인프라(DB, 로드밸런서)는 한도를 낮게 잡으면 서비스 중단으로 직결될 수 있다. 트래픽 패턴과 피크 시나리오에 맞춰 여유치를 둔다.
  • 모든 변경은 스테이징에서 부하 테스트로 검증하고, 운영 반영 시 롤백 계획을 준비한다.

빠르게 써먹는 명령 모음

# 세션 한도 전체 보기
ulimit -a
cat /proc/$$/limits

# 셸에서 임시 상향
ulimit -n 200000
ulimit -u 8192

# systemd 서비스 한도 설정(드롭인)
mkdir -p /etc/systemd/system/myapp.service.d
cat >/etc/systemd/system/myapp.service.d/limits.conf <<'EOF'
[Service]
LimitNOFILE=200000
LimitNPROC=16384
TasksMax=infinity
EOF
systemctl daemon-reload && systemctl restart myapp && systemctl show myapp -p LimitNOFILE -p TasksMax

# PAM limits: 로그인 사용자/그룹
echo '@web soft nofile 65535' >> /etc/security/limits.d/99-web.conf
echo '@web hard nofile 65535' >> /etc/security/limits.d/99-web.conf

# 전역 커널 FD 풀
sysctl -w fs.file-max=2097152

마무리

ulimit는 단독으로 존재하지 않는다. 커널 전역 한도, cgroup(systemd), PAM, 셸이 층층이 얽혀 실제 유효값을 만든다. 어떤 경로로 프로세스가 시작되는지부터 파악하고, 전역-서비스-사용자 순서로 논리적으로 맞춰가면 “열린 파일 부족”이나 “프로세스 생성 실패” 같은 고질적 장애를 설계 단계에서 차단할 수 있다.