SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-13

systemd로 프로세스 메모리 및 CPU 사용량 제한하기

systemd 유닛 파일에 MemoryMax, CPUQuota, TasksMax를 설정하여 VPS 자원을 보호하십시오. 설정 후 적용 여부를 확인하는 방법과 메모리 부족 시 발생하는 시스템 정지 현상의 원인을 상세히 설명합니다.

systemd 드롭인 파일을 사용하여 프로세스의 메모리 및 CPU 제한하기

Linux VPS에서 프로세스의 메모리와 CPU를 제한하려면 해당 프로세스를 실행하는 유닛 파일에 몇 줄을 추가하면 됩니다. MemoryMax=는 메모리의 하드 제한(hard ceiling)입니다. CPUQuota=은 프로세서 시간에 대한 제한입니다. 두 설정 모두 systemd가 서버의 모든 서비스를 관리하기 위해 이미 사용 중인 커널 기능인 cgroup v2(control groups, version 2)에 의해 강제됩니다.

sudo systemctl edit myapp.service

이 명령을 실행하면 주석으로 설명이 포함된 드롭인 파일이 열립니다. 주석 위에 다음 내용을 추가하십시오.

[Service]
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
CPUQuota=80%
TasksMax=128
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p MemoryHigh -p MemoryMax -p CPUQuotaPerSecUSec -p TasksMax

systemctl show은 커널의 고유 단위인 MemoryMax=805306368CPUQuotaPerSecUSec=800ms를 사용하여 설정한 값을 다시 출력해야 합니다. 만약 MemoryMax=infinity이 출력된다면 드롭인 파일이 로드되지 않은 것입니다. 파일이 /etc/systemd/system/myapp.service.d/override.conf 경로에 올바르게 생성되었는지, 그리고 파일이 [Service] 헤더로 시작하는지 확인하십시오. 상단에 섹션 정의 없이 설정 줄만 작성하면 systemd는 Assignment outside of section. Ignoring. 로그를 기록하며 서비스를 제한 없이 시작합니다.

이 가이드의 나머지 부분에서는 이러한 수치를 선택하는 방법과 설정 후에도 발생할 수 있는 문제들을 다룹니다.

프로세스가 메모리를 가득 채우지 않아도 VPS를 멈추게 만드는 이유

메모리 제한에 도달한 프로세스는 약 1초 내에 종료되고 서비스가 재시작됩니다. 이는 정상적인 경우입니다. 문제가 되는 상황은 아무것도 종료되지 않을 때입니다. 서버는 ping에 응답하고 SSH 연결은 수락하지만, 셸 프롬프트는 나타나지 않습니다. 기기는 살아 있고 바쁘게 작동하지만, 그 작업 중 유용한 것은 하나도 없습니다.

이 현상의 메커니즘은 직관적이지 않습니다. 여유 메모리가 부족해지면 커널은 새로운 페이지를 할당하는 대신 기존 페이지를 회수합니다. 회수하기 가장 쉬운 페이지는 파일 기반 페이지이며, 페이지 캐시에는 실행 중인 모든 프로그램의 실행 코드가 담겨 있습니다. 따라서 커널은 sshd의 텍스트 페이지를 제거하고, sshd가 다음 명령어를 실행할 때마다 저장 장치에서 해당 바이트를 다시 읽어와야 하는 페이지 폴트가 발생합니다. 모든 프로세스가 실행되는 대신 디스크를 기다리게 됩니다. 동일한 페이지가 반복해서 나가고 들어오는 현상을 스래싱(thrashing)이라고 합니다.

VPS에서는 노트북보다 이 현상이 더 심각해지는 두 가지 이유가 있습니다. 첫째, 스토리지가 네트워크로 연결되거나 공유되는 경우가 많아 로컬 NVMe 장치보다 페이지 폴트당 처리 시간이 훨씬 깁니다. 둘째, 커널은 시간을 측정하는 것이 아니라 실패를 측정합니다. 커널은 페이지 회수가 아무리 느리더라도 페이지를 반환하기만 하면 작업이 진행 중이라고 판단하여 OOM(Out of Memory) 킬러를 호출하지 않습니다. 서버는 아무것도 종료되지 않은 채 수 분 동안 이 상태로 머물 수 있습니다.

이 현상은 직접 확인할 수 있습니다. Linux 4.20 이상에서는 커널이 PSI(Pressure Stall Information)를 제공합니다.

cat /proc/pressure/memory
cat /proc/pressure/io
some avg10=63.72 avg60=41.02 avg300=12.33 total=13729481
full avg10=48.15 avg60=30.44 avg300=8.90 total=9114233

full 줄이 중요합니다. full avg10=48.15은 지난 10초 동안 서버의 모든 실행 가능한 작업이 메모리 관련 작업으로 인해 48%의 시간 동안 멈춰 있었음을 의미하며, 결과적으로 아무것도 실행되지 않았습니다. 정상적인 서버라면 full 값이 0에 가깝습니다. 10을 넘어가면 사람이 느끼기에 느려지며, 40 이상이면 흔히 서버가 멈췄다고 표현하는 상태가 됩니다.

이것이 바로 제한 설정만으로는 보장이 되지 않는 이유입니다. MemoryHigh=로 제한된 유닛은 종료되는 대신 스로틀링(throttling)이 걸립니다. 따라서 서비스는 살아 있지만 느려진 상태로 유지되며, systemd 입장에서는 실패한 것이 아니므로 재시작되지 않습니다. 스왑이 허용된 제한 유닛은 공유 장치에서 처리되는 읽기/쓰기 작업을 발생시키며, 이로 인해 서버 내 다른 모든 서비스의 /proc/pressure/io 수치까지 높일 수 있습니다. 제한 설정은 부족한 자원을 누가 부담할지 결정할 뿐, 용량 자체를 늘려주지는 않습니다.

VPS가 cgroup v2를 사용하는지 확인하기

stat -fc %T /sys/fs/cgroup

cgroup2fs은 통합 계층 구조이며, 아래의 모든 설정에 필요한 요소입니다. tmpfs는 시스템이 구형 v1 레이아웃으로 부팅되었음을 의미하며, 이 경우 MemoryHigh=MemorySwapMax=가 존재하지 않고 유닛별 OOM 동작 방식이 달라집니다. Ubuntu 22.04 이상 및 Debian 11 이상은 기본적으로 v2를 사용합니다. 오래된 이미지나 systemd.unified_cgroup_hierarchy=0 옵션으로 부팅된 커널은 v2를 사용하지 않습니다.

cgroup v2 환경에서 systemd는 모든 유닛에 대해 메모리 계정을 기본적으로 활성화하므로, 관련 수치를 즉시 확인할 수 있습니다.

systemd-cgtop -m

위 명령은 메모리 사용량 순으로 cgroup을 나열합니다. 서버가 응답 가능한 상태일 때 "무엇이 자원을 점유하고 있는지" 파악하는 가장 빠른 방법입니다. 서버를 새로 구축하는 중이라면, 이 작업에 앞서 새 VPS 설정의 첫 10분에서 다루는 계정 및 방화벽 작업을 먼저 수행해야 합니다.

MemoryHigh 제한과 MemoryMax 종료

두 메모리 설정의 차이는 장애가 발생하는 양상을 결정합니다.

  • MemoryHigh=은 소프트 제한입니다. 이 값을 초과하면 커널은 해당 cgroup에서 메모리를 공격적으로 회수하며 의도적으로 할당 속도를 늦춥니다. 사용량은 설정값을 넘을 수 있으며, 프로세스가 종료되지는 않습니다.
  • MemoryMax=은 하드 제한입니다. 이 제한 내에서 메모리 할당을 충족할 수 없으면, 해당 cgroup 내부에서 OOM killer가 작동하여 해당 유닛의 프로세스 중 하나를 종료합니다.

후자의 특성이 완전히 신뢰할 수 없는 서비스에 MemoryMax=을 설정해야 하는 실질적인 이유입니다. 제한이 없으면 메모리 부족은 서버 전체의 문제가 되며, 전역 OOM killer는 oom_score를 기준으로 희생자를 선택하는데, 이는 보통 가장 큰 프로세스를 의미합니다. 가장 큰 프로세스는 메모리 누수가 발생한 스크립트가 아니라 대개 데이터베이스입니다. 제한을 설정하면 종료 대상이 문제를 일으킨 유닛 내부로 한정됩니다.

MemoryHigh=MemoryMax=보다 약 20~30% 낮게 설정하여 두 값을 모두 사용하십시오. 이 간격은 경고 구간 역할을 합니다. 완만한 누수는 High를 넘어서며 서비스가 느려지는 현상으로 나타나고, 급격한 메모리 급증은 Max을 바로 통과하여 프로세스가 종료됩니다.

백분율 값은 설치된 물리적 메모리를 기준으로 계산됩니다. 따라서 4 GB 플랜에서 MemoryMax=25%는 1 GB이며, 플랜 크기를 조정해도 전체의 4분의 1이라는 비율은 유지됩니다. MemorySwapMax=0를 설정하면 해당 유닛이 스왑을 전혀 사용하지 않게 되어, 긴 시간 동안 느려지는 현상 대신 빠르고 명확한 종료가 이루어집니다.

제한 설정 시에는 반드시 재시작 정책(restart policy)을 함께 사용해야 합니다. 그렇지 않으면 프로세스가 종료된 후 서비스가 중단된 상태로 남게 됩니다.

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=5s

StartLimit*[Unit] 섹션에, Restart=[Service] 섹션에 작성해야 합니다. 섹션을 잘못 지정하면 systemd는 해당 설정을 무시합니다. 5분 동안 5번 재시작이 발생하면 이는 일시적인 오류가 아닌 누수로 간주됩니다. 이 경우 systemd는 재시작을 포기하고 유닛을 failed 상태로 남겨두는데, 이는 문제를 숨기는 크래시 루프(crash loop)보다 나중에 원인을 파악하기에 더 적합한 상태입니다.

CPUQuota로 CPU 사용량을 제한하거나 CPUWeight로 자원을 공유하기

CPUQuota=은 단일 CPU에서 사용 가능한 시간의 백분율을 설정합니다. CPUQuota=50%은 한 개 코어의 절반을 의미합니다. CPUQuota=200%는 두 개 코어와 동일한 성능을 제공하며, 유닛은 이를 원하는 만큼의 스레드로 분산하여 사용할 수 있습니다. 2 vCPU 플랜 환경에서 CPUQuota=200%은 시스템 전체 자원을 의미합니다.

CPUWeight=는 대부분의 서비스에 더 적합한 기본 설정입니다. 이는 1에서 10000 사이의 상대적 가중치이며, 커널 기본값은 100입니다. 이 설정은 자원 경합이 발생할 때만 작동합니다. 예를 들어, 부하가 걸린 상황에서 CPUWeight=20로 설정된 백업 작업은 100으로 설정된 웹 서버에 자원을 양보하지만, 시스템이 유휴 상태일 때는 전체 자원을 사용할 수 있습니다. 반면 하드 쿼터(hard quota)를 설정하면 이러한 유휴 자원을 활용하지 못하고 버리게 됩니다.

CPU 제한을 설정할 때 얻을 수 있는 이점을 명확히 이해해야 합니다. CPU 집약적인 프로세스는 스케줄러가 모든 프로세스에 시간을 분배하므로 Linux 시스템을 거의 멈추게 하지 않습니다. 시스템을 다운시키는 주된 원인은 메모리 부족입니다. CPUQuota=은 빌드 작업이나 에이전트처럼 한 시간 동안 최대 성능으로 동작할 수 있는 작업에 대해 예측 가능한 상한선을 설정하고 싶을 때 사용하십시오. 이러한 워크로드의 크기를 산정하는 방법은 코딩 에이전트 VPS에 필요한 RAM과 CPU 용량에서 다룹니다.

프로세스가 별다른 작업을 하지 않는데도 CPU 사용량이 높게 나타난다면, 하이퍼바이저 반대편의 문제일 수 있습니다. 이는 노이지 네이버(noisy neighbour)로 인한 CPU 스틸 타임 문제이며, 어떠한 쿼터를 설정하더라도 해결할 수 없습니다.

TasksMax를 이용한 포크 루프 방지

TasksMax=은 유닛이 가질 수 있는 프로세스와 스레드의 총 개수입니다. 스레드도 개수에 포함되므로, Java나 Go 서비스는 프로세스 목록에서 보이는 것보다 더 많은 여유 공간이 필요합니다. 이는 포크 루프를 도는 스크립트에 대한 가장 저렴한 보호 수단입니다. 시스템 전체의 프로세스 ID가 고갈되는 대신 해당 유닛 내부에서 포크가 실패하기 때문입니다.

TasksMax=128

유닛이 제한에 도달하면 커널은 cgroup 이름을 명시한 로그를 남깁니다.

cgroup: fork rejected by pids controller in /system.slice/myapp.service

프로그램 자체는 보통 fork: retry: Resource temporarily unavailable 오류를 보고합니다. systemctl show -p DefaultTasksMax를 사용하여 관리자가 기본적으로 적용하는 제한 값을 확인하십시오.

systemd-run을 사용하여 일회성 작업 제한하기

이 기능을 사용하기 위해 별도의 unit 파일이 필요하지는 않습니다. systemd-run은 단일 명령을 중심으로 임시 unit을 생성합니다.

sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh

--scopeRunning scope as unit: run-r7c1a....scope를 출력한 뒤 터미널에서 명령을 실행합니다. 출력 결과는 화면에 그대로 표시되며, 명령이 종료되면 제한 설정도 사라집니다. systemd.resource-control의 모든 속성은 -p 뒤에 사용할 수 있습니다.

장시간 실행되는 작업의 경우 --scope를 생략하고 이름을 지정하십시오. 그러면 해당 작업은 임시 서비스로서 백그라운드에서 실행되며 로그는 journal에 기록됩니다:

sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -f

root 권한이 아닐 때도 --user과 함께 동일한 옵션을 사용할 수 있습니다. 단, 사용자 관리자(user manager)는 자신에게 위임된 컨트롤러만 사용할 수 있으므로 특정 속성은 거부될 수 있습니다. 그런 경우에는 sudo을 사용하여 실행하십시오. 작업이 영구적인 서비스로 정착되면, 해당 설정은 변경 없이 실제 unit으로 옮길 수 있습니다. 스크립트를 systemd 서비스 및 타이머로 실행하기를 참조하십시오.

스왑에 대한 솔직한 답변

스왑은 장애를 방지하기보다는 장애의 양상을 바꿉니다.

스왑이 없으면 메모리 누수가 한계치에 도달하는 즉시 몇 초 안에 프로세스가 종료됩니다. 장애는 즉각적이고 짧으며, 이후 저널을 통해 원인을 파악하기 쉽습니다. 스왑이 있으면 커널이 사용되지 않는 익명 페이지를 디스크로 내보내 시간을 벌어줍니다. 프로세스의 메모리 사용량이 곧 안정화될 상황이라면 스왑이 도움이 됩니다. 하지만 메모리 누수가 계속되는 상황이라면 스왑은 5초짜리 장애를 20분짜리 지연으로 바꿉니다. 프로세스가 죽으면 셸은 여전히 사용할 수 있지만, 시스템이 스왑으로 인해 멈춰버리면(thrashing) 아무것도 할 수 없으므로 후자가 더 나쁜 상황입니다.

swapon --show
free -h

소규모 VPS에서 현실적인 타협안은 다음과 같습니다. 한 번 할당된 후 다시 사용되지 않는 페이지를 위해 적절한 크기의 스왑 파일을 유지하고, 종료되어도 무방한 유닛에는 MemorySwapMax=0을 설정하십시오. 중요한 서비스는 스왑을 유지하고, 예측 불가능한 서비스는 빠르게 한계에 도달하여 재시작되도록 하는 것입니다.

vm.swappiness 값을 낮추는 것은 근본적인 해결책이 아니며, 그 이유를 이해할 필요가 있습니다. 이 설정은 페이지 캐시를 비우는 것과 익명 페이지를 스왑하는 것 사이의 균형만 바꿀 뿐이며, 두 경우 모두 나중에 디스크 읽기 비용이 발생합니다. 이는 시스템이 멈추는 현상 자체를 해결하는 것이 아니라, 어떤 페이지가 스왑될지 결정하는 기준만 바꿀 뿐입니다.

커널 OOM 발생 전 조기 종료 데몬

커널은 메모리 회수가 완전히 실패할 때까지 기다리지만, 소규모 VPS 환경에서는 그 대기 시간이 곧 서버 접속이 끊기는 원인이 됩니다. 두 가지 사용자 공간 데몬은 메모리 상태를 직접 감시하여 더 빠르게 프로세스를 종료함으로써 이 문제를 해결합니다.

earlyoom은 가용 메모리와 여유 스왑을 감시하며, 어느 하나라도 임계값 아래로 떨어지면 점수가 가장 높은 프로세스를 종료합니다.

sudo apt install earlyoom
systemctl status earlyoom

Debian 및 Ubuntu 패키지는 설치 시 서비스를 자동으로 시작합니다. 옵션은 /etc/default/earlyoom에서 설정합니다.

EARLYOOM_ARGS="-m 5,2 -s 5,2 --avoid '^(sshd|systemd)$' --prefer '^(node|python3)$'"

-m PERCENT는 가용 메모리 최솟값을, -s PERCENT은 여유 스왑 최솟값을 설정하며 기본값은 둘 다 10퍼센트입니다. 각 쌍의 두 번째 숫자는 SIGKILL 시점입니다. earlyoom은 첫 번째 값 아래로 떨어지면 SIGTERM을 보내고, 첫 번째 값의 절반인 두 번째 값 아래로 떨어지면 SIGKILL을 보냅니다. 변경 사항을 적용하려면 sudo systemctl restart earlyoom를 사용하고, 어떤 프로세스가 종료되었으며 해당 프로세스가 얼마만큼의 메모리를 점유하고 있었는지 확인하려면 journalctl -u earlyoom를 읽어보십시오.

systemd-oomd은 또 다른 선택지입니다. 매뉴얼 페이지에 따르면 이 서비스는 "cgroups-v2와 PSI(pressure stall information)를 사용하여 커널 공간에서 OOM이 발생하기 전에 감시하고 교정 조치를 취하는 시스템 서비스"입니다. 이 서비스는 개별 프로세스가 아닌 cgroup 단위로 동작하므로, 특정 자식 프로세스가 아닌 유닛 전체를 종료합니다. 유닛은 ManagedOOMMemoryPressure=kill 또는 ManagedOOMSwap=kill을 통해 참여를 설정하며, 임계값은 /etc/systemd/oomd.conf에서 관리합니다.

systemctl status systemd-oomd
oomctl

oomctl은 현재 감시 중인 대상을 출력합니다. 서버 이미지에서는 기본적으로 유닛별로 참여를 설정해야 하므로 아무것도 출력되지 않는 경우가 많습니다. 데몬 하나를 선택하여 사용하십시오. 두 서비스를 동시에 실행하면 종료 대상을 선정하기 위해 서로 경쟁하게 되며, 프로세스가 종료된 원인을 파악하기가 더 어려워집니다.

어떤 유닛이 원인인가?

커널은 모든 프로세스 종료 기록을 남기므로 커널부터 확인해야 합니다.

journalctl -k --grep "Killed process" --since "2 hours ago"

전역 OOM killer에 의한 종료는 다음과 같이 표시됩니다.

Out of memory: Killed process 4127 (node) total-vm:2731084kB, anon-rss:1874232kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:4212kB oom_score_adj:0

anon-rss은 프로세스가 종료될 당시 RAM에 점유하고 있던 메모리 양으로, 여기서는 약 1.8 GB입니다. 괄호 안의 이름을 주의 깊게 확인하십시오. 이는 커널이 선택한 희생양이며, 커널은 항상 가장 큰 프로세스를 선택하므로 반드시 메모리 부족을 유발한 프로세스라고 볼 수는 없습니다.

cgroup 제한에 의한 종료는 접두사가 다르며, 그 위에 출력된 보고서가 자체 한계치에 도달한 cgroup 이름을 명시합니다.

Memory cgroup out of memory: Killed process 8811 (python3) total-vm:1044320kB, anon-rss:769112kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:1720kB oom_score_adj:0

해당 접두사가 진단의 핵심입니다. Memory cgroup out of memory는 특정 유닛이 설정한 MemoryMax=에 도달했으며 시스템의 나머지 부분은 정상임을 의미합니다. 일반적인 Out of memory는 시스템 전체의 메모리가 부족함을 의미하며, 이는 제한 설정이 없었거나 너무 관대하게 설정되었음을 뜻합니다.

그다음 systemd가 무엇을 기록했는지 확인하십시오.

systemctl status myapp.service
journalctl -u myapp.service -n 50
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'oom-kill'.

systemctl statusActive: failed (Result: oom-kill)와 동일한 내용을 한 줄로 요약합니다.

cgroup 카운터는 세 번째 정보원이며, 로그를 남기지 않는 스로틀링(throttling) 현상을 확인할 수 있는 유일한 방법입니다.

cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
cat /sys/fs/cgroup/system.slice/myapp.service/memory.peak
low 0
high 4213
max 118
oom 12
oom_kill 12

high은 유닛이 MemoryHigh=을 초과하여 스로틀링된 횟수를 나타냅니다. max는 하드 제한에 도달한 횟수이며, oom_kill은 실제로 종료된 프로세스 수입니다. oom_kill 0가 0이 아닌데 high이 크다면 앞서 언급한 조용한 장애 사례입니다. 즉, 서비스는 실행 중이지만 매우 느려졌으며 그 어떤 오류도 보고하지 않은 상태입니다. memory.peak(Linux 5.19 이상)은 해당 cgroup이 도달했던 최대 사용량을 담고 있으며, 이 값을 기준으로 MemoryMax=를 산정해야 합니다. systemd가 cgroup을 새로 생성하므로 유닛이 재시작되면 두 파일 모두 초기화됩니다.

이 모든 과정에는 한 가지 전제 조건이 있습니다. /var/log/journal가 존재하지 않으면 저널은 RAM에만 저장되며, 시스템 복구를 위해 재부팅하는 순간 모든 로그가 사라집니다.

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --list-boots

journalctl --list-boots이 현재 부팅보다 많은 항목을 보여준다면 기록이 유지되고 있다는 뜻이므로, journalctl -k -b -1을 통해 시스템이 죽기 직전 부팅의 커널 메시지를 확인할 수 있습니다.

소규모 VPS를 위한 시작점

2 GB 플랜을 사용하는 경우, 커널과 페이지 캐시를 위해 300에서 400 MB를 남겨두어야 합니다. 모든 유닛이 동시에 최대치에 도달할 수 있으므로, 각 제한의 합계가 2 GB 전체를 넘지 않도록 설정하십시오. 가장 중요한 서비스에 가장 큰 자원을 할당하고, 그 주변의 부차적인 서비스들에 제한을 두어야 합니다.

[Service]
MemoryHigh=256M
MemoryMax=384M
MemorySwapMax=0
CPUWeight=20
TasksMax=64
Restart=on-failure
RestartSec=5s

접속 경로를 유지하는 것은 추가적인 설정을 할 가치가 있습니다. ssh.service의 드롭인 파일에 OOMScoreAdjust=-500을 설정하면, 전역 OOM killer가 SSH 데몬을 희생양으로 선택할 가능성이 크게 낮아집니다. 이는 서버를 직접 복구할 수 있는지, 아니면 제어판에서 재부팅해야 하는지를 결정짓는 차이가 됩니다. 이 설정은 커널이 희생양을 선택하는 우선순위만 변경할 뿐, 시스템 멈춤 현상을 단축시키지는 않습니다.

컨테이너는 유닛 파일이 아닌 컨테이너 런타임에 의해 생성된 자체 cgroup에서 실행됩니다. 따라서 docker.service에 설정한 제한은 개별 컨테이너에 적용되지 않습니다. MemoryMax=CPUQuota=에 대응하는 컨테이너별 설정은 Docker Compose에서 메모리 및 CPU 제한 설정하기에서 다룹니다.

FAQ

왜 폭주하는 프로세스를 종료하지 않고 VPS가 멈추나요?

커널은 프로세스가 얼마나 오래 걸리는지가 아니라, 메모리 회수(reclaim)가 페이지를 반환하는지 여부로 진행 상황을 판단하기 때문입니다. 메모리가 부족하면 커널은 실행 중인 프로그램의 실행 페이지를 포함한 페이지 캐시를 제거한 뒤, 다음 명령 실행 시 다시 읽어 들입니다. 모든 작업이 저장 장치를 기다리게 되고 기술적으로는 할당 실패가 발생하지 않으므로, OOM killer는 호출되지 않습니다. 문제가 발생할 때 /proc/pressure/memory을 확인하십시오. full avg10가 40을 넘으면 지난 10초 동안 거의 아무런 작업도 수행되지 않았음을 의미합니다. earlyoom와 같은 사용자 공간 데몬을 사용하면 시스템이 해당 상태에 도달하기 전에 프로세스를 종료할 수 있습니다.

MemoryHigh와 MemoryMax의 차이점은 무엇인가요?

MemoryHigh=은 스로틀링(throttling)을 유발하는 소프트 제한입니다. 커널은 해당 유닛에서 메모리를 강제로 회수하고 할당 속도를 늦추지만, 사용량이 설정값을 초과할 수 있으며 프로세스가 종료되지는 않습니다. MemoryMax=은 하드 제한입니다. 이 제한 내에서 할당을 충족할 수 없으면 해당 유닛의 cgroup 내부에서 OOM killer가 호출됩니다. 따라서 시스템 전체에서 가장 큰 프로세스가 아니라 문제를 일으킨 프로세스가 종료됩니다. MemoryHigh=MemoryMax=보다 낮게 설정하고, 그 사이의 간격을 경고 구간으로 활용하십시오.

OOM killer가 어떤 서비스를 종료했는지 어떻게 확인하나요?

journalctl -k --grep "Killed process" --since "2 hours ago"을 실행하십시오. Memory cgroup out of memory로 시작하는 줄은 특정 유닛이 자체 MemoryMax=에 도달했음을 의미하며, 일반적인 Out of memory은 시스템 전체의 메모리가 부족했음을 의미합니다. 그 후 journalctl -u <unit> -n 50를 실행하여 Failed with result 'oom-kill'를 찾으십시오. 서버에 /var/log/journal이 존재하지 않는다면 저널이 RAM에 저장되어 재부팅과 함께 증거가 사라진 것입니다. 다음 사고에 대비하여 해당 디렉터리를 미리 생성해 두십시오.

소규모 VPS에 스왑을 추가해야 하나요?

작은 스왑 파일은 한 번 할당된 후 다시 사용되지 않는 콜드 페이지(cold pages)를 처리하는 데 도움이 됩니다. 하지만 폭주하는 프로세스에는 도움이 되지 않습니다. 스왑은 종료를 지연시킬 뿐이며, 짧은 중단 시간을 로그인조차 할 수 없는 긴 정지 상태로 바꿉니다. 스왑은 적절한 수준으로 유지하고, 종료되어도 무방한 유닛에는 MemorySwapMax=0을 설정하십시오. 그러면 중요한 서비스는 스왑을 유지하면서 해당 유닛들은 제한에 도달하여 빠르게 재시작됩니다.

유닛 파일을 작성하지 않고 명령어를 제한할 수 있나요?

네, 가능합니다. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh은 해당 제한을 적용하여 터미널 내의 임시 스코프(transient scope)에서 명령어를 실행하며, 명령어가 종료되면 제한도 사라집니다. systemd.resource-control의 모든 속성은 -p 이후에 사용할 수 있으므로, MemorySwapMax=, TasksMax=, CPUWeight=도 동일하게 작동합니다. --scope를 생략하고 --unit=name를 추가하면 작업을 백그라운드에서 실행하고 그 출력을 저널에 기록할 수 있습니다.