systemd로 프로세스 메모리와 CPU 제한하기
systemd unit에 MemoryHigh, MemoryMax, CPUQuota, TasksMax를 설정하는 방법을 설명합니다. 제한 후에도 VPS 전체가 멈출 수 있는 이유와 OOM kill 확인법까지 다룹니다.
systemd drop-in으로 프로세스 메모리와 CPU 제한하기
프로세스를 실행하는 unit에 몇 줄을 추가하면 Linux VPS에서 프로세스의 메모리와 CPU 사용량을 제한할 수 있다. MemoryMax=는 메모리의 하드 제한이다. CPUQuota=은 프로세서 시간의 제한이다. 두 설정 모두 cgroup v2(control groups, version 2)가 적용한다. cgroup v2는 systemd가 이미 서버의 각 서비스 사용량을 계산하는 데 사용하는 커널 기능이다.
sudo systemctl edit myapp.service그러면 주석으로 지침이 포함된 drop-in 파일이 열린다. 다음 내용을 주석보다 위에 추가한다.
[Service]
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
CPUQuota=80%
TasksMax=128sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p MemoryHigh -p MemoryMax -p CPUQuotaPerSecUSec -p TasksMaxsystemctl show은 커널에서 사용하는 단위로 설정한 값을 다시 표시해야 한다. 단위는 MemoryMax=805306368 및 CPUQuotaPerSecUSec=800ms이다. MemoryMax=infinity이 출력되면 drop-in이 로드되지 않은 것이다. 파일이 /etc/systemd/system/myapp.service.d/override.conf에 생성되었는지 확인한다. 또한 파일이 [Service] 헤더로 시작하는지도 확인한다. 설정 줄 위에 섹션이 없으면 systemd가 Assignment outside of section. Ignoring.을 기록하고 제한 없이 서비스를 시작한다.
이 가이드의 나머지 부분에서는 이 값을 선택하는 방법과 값을 설정한 뒤에도 발생할 수 있는 문제를 설명한다.
폭주한 프로세스가 VPS를 메모리 고갈 없이 멈추게 하는 이유
엄격한 메모리 제한에 도달한 프로세스가 약 1초 안에 종료되고 서비스가 다시 시작된다면 좋은 경우다. 나쁜 경우는 아무것도 종료되지 않는 경우다. 서버는 ping에 응답하고 SSH 연결도 수락하지만 셸 프롬프트가 나타나지 않는다. 시스템은 작동 중이고 바쁘지만, 그 작업은 아무런 유용한 결과를 내지 않는다.
그 원리는 직관적이지 않다. 사용 가능한 메모리가 부족해지면 커널은 새 메모리를 할당하는 대신 페이지를 회수한다. 회수하기 가장 쉬운 페이지는 파일이 뒷받침하는 페이지다. 페이지 캐시에는 실행 중인 모든 프로그램의 실행 코드가 들어 있다. 따라서 커널은 sshd의 코드 페이지를 제거한다. 그러면 sshd가 실행할 다음 명령은 페이지 폴트가 되고, 저장 장치에서 해당 바이트를 다시 읽어야 한다. 결국 모든 프로세스가 실행되지 못하고 디스크를 기다리게 된다. 같은 페이지가 계속 제거되었다가 다시 로드되는 상태가 반복되며, 이를 thrashing이라고 한다.
두 가지 요인 때문에 노트북보다 VPS에서 이 문제가 더 심해진다. 저장 장치가 네트워크에 연결되어 있거나 여러 시스템이 공유하는 경우가 많으므로, 페이지 폴트마다 걸리는 시간이 로컬 NVMe 장치보다 길다. 또한 커널은 경과 시간이 아니라 실패를 측정한다. 페이지 회수가 느리더라도 계속 페이지를 하나씩 반환하는 동안에는 커널이 작업이 진행되고 있다고 판단하므로 out of memory (OOM) killer를 호출하지 않는다. 그 결과 어떤 프로세스도 종료되지 않은 상태로 서버가 여러 분 동안 머물 수 있다.
이 상태가 발생하는 과정은 확인할 수 있다. Linux 4.20 이상에서는 커널이 pressure stall information (PSI)을 제공한다.
cat /proc/pressure/memory
cat /proc/pressure/iosome avg10=63.72 avg60=41.02 avg300=12.33 total=13729481
full avg10=48.15 avg60=30.44 avg300=8.90 total=9114233full 줄이 중요하다. full avg10=48.15은 최근 10초 동안 서버에서 실행 가능한 모든 작업이 메모리 작업을 기다리며 멈춰 있었던 시간이 전체의 48%였다는 뜻이다. 즉, 아무 작업도 실행되지 않았다. 정상적인 서버의 full 값은 0에 가깝다. 10을 넘으면 사람이 느끼기에도 느려지고, 40 이상이면 일반적으로 시스템이 멈췄다고 표현하는 상태다.
이 때문에 제한만 설정한다고 해서 문제가 해결되는 것은 아니다. MemoryHigh= 아래에서 실행되는 unit은 종료되지 않고 제한되므로, 살아 있는 상태로 계속 느리게 동작한다. systemd 관점에서는 실패하지 않았기 때문에 다시 시작되지도 않는다. swap 사용이 허용된 제한 unit은 읽기와 쓰기를 발생시키며, 이 작업은 해당 unit에 비용으로 기록되지만 실제 처리는 하나의 공유 장치가 담당한다. 따라서 서버의 다른 모든 서비스에 대해 /proc/pressure/io 값도 높일 수 있다. 제한은 자원 부족의 비용을 누가 부담할지 결정할 뿐이며, 처리 용량을 만들어 내지는 못한다.
VPS에서 cgroup v2를 사용하는지 확인합니다
stat -fc %T /sys/fs/cgroupcgroup2fs은 통합 계층입니다. 아래의 모든 설정이 이 계층을 필요로 합니다. tmpfs는 시스템이 이전 v1 레이아웃으로 부팅되었다는 뜻입니다. 이 경우 MemoryHigh=과 MemorySwapMax=가 존재하지 않으며 단위별 OOM 동작도 다릅니다. Ubuntu 22.04 이상과 Debian 11 이상은 기본적으로 v2를 사용합니다. 오래된 이미지나 systemd.unified_cgroup_hierarchy=0가 지정된 커널로 부팅한 시스템은 그렇지 않습니다.
cgroup v2에서 systemd는 기본적으로 모든 단위의 메모리 회계를 활성화합니다. 따라서 필요한 수치는 이미 수집되고 있습니다.
systemd-cgtop -m이 명령은 메모리 사용량을 기준으로 cgroup을 정렬해 표시합니다. 서버가 아직 응답할 수 있을 때 “무엇이 이 서버의 메모리를 모두 사용하고 있는가”를 가장 빠르게 확인하는 방법입니다. 새 서버라면 이 작업보다 먼저 새 VPS에서 처음 10분 동안 수행할 작업에서 계정과 방화벽을 설정해야 합니다.
MemoryHigh는 스로틀링을 적용합니다. MemoryMax는 종료합니다.
두 메모리 설정의 차이가 장애 발생 시 동작을 결정합니다.
MemoryHigh=은 소프트 상한입니다. 사용량이 이 값을 초과하면 커널은 해당 cgroup에서 메모리를 적극적으로 회수하고 할당을 의도적으로 지연합니다. 사용량은 여전히 이 값을 초과할 수 있으며, 프로세스를 종료하지는 않습니다.MemoryMax=은 하드 상한입니다. 이 상한 안에서 할당을 충족할 수 없으면 OOM killer가 해당 cgroup 내부에서 실행되고, 해당 unit에 속한 프로세스 하나를 종료합니다.
무제한으로 신뢰할 수 없는 대상에 MemoryMax=을 설정해야 하는 진짜 이유는 이 두 번째 동작입니다. 상한이 없으면 메모리 부족은 전체 시스템의 문제가 되고, 전역 OOM killer는 oom_score를 기준으로 종료 대상을 선택합니다. 대부분의 경우 이는 가장 큰 프로세스를 의미합니다. 가장 큰 프로세스는 메모리를 누수시킨 스크립트가 아니라 일반적으로 데이터베이스입니다. 상한을 설정하면 메모리 부족을 일으킨 unit 내부에서 프로세스가 종료됩니다.
두 설정을 모두 지정하고, MemoryHigh=은 MemoryMax=보다 약 20~30% 낮게 설정합니다. 이 간격은 경고 구간입니다. 느린 메모리 누수는 High를 넘으면서 서비스가 느려진 것으로 나타나고, 갑작스러운 사용량 급증은 Max을 곧바로 초과하여 프로세스를 종료합니다.
백분율 값은 설치된 물리 메모리를 기준으로 계산됩니다. 따라서 4 GB 요금제에서 MemoryMax=25%은 1 GB이며, 요금제의 메모리를 늘린 뒤에도 전체 메모리의 1/4로 유지됩니다. MemorySwapMax=0은 해당 unit이 swap을 전혀 사용하지 않도록 하므로, 장시간 느려지는 대신 빠르고 명확하게 프로세스가 종료됩니다.
일부 서비스는 사용량을 측정하는 대신 필요한 메모리 규모를 미리 결정할 수 있습니다. Ollama unit은 지정한 context window를 기준으로 KV cache 크기를 정합니다. 따라서 상한을 정하기 전에 num_ctx를 높일 때 RAM 사용량이 얼마나 늘어나는지 확인합니다.
상한을 설정할 때는 옆에 restart policy도 지정해야 합니다. 그렇지 않으면 프로세스가 종료된 뒤 서비스가 중지된 상태로 남습니다.
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5sStartLimit*은 [Unit]에, Restart=은 [Service]에 지정해야 합니다. 둘 중 하나라도 잘못된 section에 넣으면 systemd가 무시합니다. 5분 안에 5번 재시작하는 것은 일시적인 오류가 아니라 누수에 가깝습니다. 따라서 그 이후 systemd는 재시작을 포기하고 unit을 failed 상태로 둡니다. 나중에 문제를 확인하려면 이 상태가 필요합니다. 문제를 숨기는 crash loop 상태보다 낫습니다.
CPUQuota로 CPU를 제한하거나 CPUWeight로 CPU를 공유하기
CPUQuota=은 CPU 1개에서 사용할 수 있는 시간 중 일정 비율을 사용한다. CPUQuota=50%은 코어 1개의 절반이다. CPUQuota=200%는 코어 2개에 해당하며, unit은 필요한 만큼 여러 스레드에 분산해 사용할 수 있다. 2 vCPU 요금제에서는 CPUQuota=200%가 전체 시스템에 해당한다.
대부분의 서비스에서는 CPUWeight=를 기본값으로 사용하는 편이 좋다. CPUWeight=는 1부터 10000까지의 상대적 비율이며, 커널 기본값은 100이다. 다른 작업과 경쟁할 때만 적용된다. 부하가 있는 동안 CPUWeight=20의 백업 작업은 100으로 설정된 웹 서버보다 CPU를 적게 할당받는다. 반면 시스템이 유휴 상태라면 전체 CPU를 사용할 수 있다. 엄격한 quota는 유휴 CPU 용량을 버리게 만든다.
CPU 제한으로 얻는 효과를 정확히 이해해야 한다. CPU를 집중적으로 사용하는 프로세스도 스케줄러가 모든 프로세스에 실행 시간을 계속 할당하므로 Linux를 거의 멈추게 하지는 않는다. 시스템을 중단시키는 원인은 대개 메모리다. 예를 들어 한 시간 동안 최대 CPU를 계속 사용할 수 있는 빌드 작업이나 agent처럼 예측 가능한 상한이 필요할 때 CPUQuota=를 사용한다. 이러한 작업에 필요한 용량을 산정하는 문제는 별도로 다뤄야 하며, 코딩 agent VPS에 필요한 RAM과 CPU 용량에서 설명한다.
실행 중인 프로세스가 CPU를 많이 사용하지 않는데도 CPU 사용량이 높게 표시된다면 원인은 hypervisor 반대편에 있을 수 있다. 이는 시끄러운 이웃으로 인한 CPU steal time일 수 있으며, 직접 설정한 quota로는 이를 바꿀 수 없다.
TasksMax가 fork loop를 중지한다
TasksMax=은 unit이 보유할 수 있는 프로세스와 스레드 수다. 스레드도 포함되므로 Java 또는 Go 서비스에는 프로세스 목록에 표시되는 것보다 더 큰 여유가 필요하다. 이는 loop로 fork하는 스크립트를 방어하는 가장 저렴한 방법이다. 시스템의 process ID가 고갈되기 전에 unit 내부에서 fork가 실패하기 때문이다.
TasksMax=128unit이 제한에 도달하면 커널은 cgroup 이름이 포함된 로그 한 줄을 기록한다.
cgroup: fork rejected by pids controller in /system.slice/myapp.service프로그램 자체는 대개 fork: retry: Resource temporarily unavailable을 보고한다. manager가 기본적으로 적용하는 값을 systemctl show -p DefaultTasksMax로 확인한다.
systemd-run으로 일회성 작업 제한하기
이 기능을 사용하는 데 unit 파일은 필요하지 않다. systemd-run는 단일 명령을 중심으로 transient unit을 생성한다.
sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh--scope는 Running scope as unit: run-r7c1a....scope를 출력한 후 터미널에서 명령을 실행한다. 출력은 화면에 계속 표시되며, 명령이 종료되면 제한도 사라진다. systemd.resource-control의 모든 속성은 -p 뒤에 사용할 수 있다.
오래 실행되는 작업은 --scope를 생략하고 이름을 지정한다. 그러면 작업이 transient service로 백그라운드에서 실행되고 journal에 로그가 기록된다.
sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -froot가 아닌 경우에도 --user와 함께 같은 옵션을 사용할 수 있다. 단, 사용자 manager에는 위임된 controller만 있으므로 특정 속성이 거부될 수 있다. 이 경우 sudo와 함께 실행한다. 작업을 영구적으로 관리해야 한다면 설정을 변경하지 않고 실제 unit으로 옮길 수 있다. 스크립트를 systemd service와 timer로 실행하기를 참조한다.
정직하게 답하는 swap 문제
swap은 장애를 방지하지 않고 장애가 발생하는 양상을 바꾼다.
swap이 없으면 메모리 누수가 한계에 도달하고 몇 초 안에 무언가가 종료된다. 장애는 명확하고 짧으며, 이후 journal에서 원인을 확인하기 쉽다. swap이 있으면 kernel이 사용되지 않는 anonymous page를 디스크로 기록해 시간을 번다. 프로세스가 곧 안정될 상황이었다면 swap이 문제를 해결한다. 하지만 프로세스가 계속 폭주한다면 swap은 5초 동안 발생할 장애를 20분간의 stall로 바꾼다. 이 stall이 더 나쁜 이유는 종료된 프로세스가 있어도 shell은 계속 사용할 수 있지만, thrashing 상태의 시스템에서는 그렇지 않기 때문이다.
swapon --show
free -h소형 VPS에서는 다음과 같이 절충할 수 있다. 한 번 할당된 뒤 다시 사용되지 않는 page를 위해 적당한 크기의 swap file을 유지하고, 손실되어도 되는 unit에는 MemorySwapMax=0을 설정한다. 중요한 서비스에는 swap을 유지한다. 예측하기 어려운 서비스는 빠르게 한계에 도달한 뒤 재시작하게 한다.
vm.swappiness을 낮추는 방법은 효과가 제한적이다. 그 이유를 알아둘 필요가 있다. 이 설정은 page cache를 축출할지 anonymous page를 swap할지의 균형만 바꾼다. 두 경우 모두 나중에 디스크 읽기 비용이 발생한다. 어떤 page가 thrashing되는지는 바꾸지만, 시스템이 thrashing되는지 여부는 바꾸지 않는다.
멈춤이 발생하기 전에 조기 OOM 데몬이 종료한다
커널은 reclaim이 완전히 실패할 때까지 기다린다. 소형 VPS에서는 그 대기 시간이 시스템에 접속할 수 없게 되는 정확한 구간이다. 두 userspace 데몬은 메모리를 직접 감시하고 더 일찍 프로세스를 종료해 이 문제를 해결한다.
earlyoom은 사용 가능한 메모리와 남은 swap을 감시한다. 둘 중 하나가 임계값 아래로 내려가면 점수가 가장 높은 프로세스를 종료한다.
sudo apt install earlyoom
systemctl status earlyoomDebian 및 Ubuntu 패키지는 설치할 때 서비스를 시작한다. 옵션은 /etc/default/earlyoom에 있다.
EARLYOOM_ARGS="-m 5,2 -s 5,2 --avoid '^(sshd|systemd)$' --prefer '^(node|python3)$'"-m PERCENT는 사용 가능한 메모리의 최솟값을 설정하고 -s PERCENT은 남은 swap의 최솟값을 설정한다. 두 값 모두 기본값은 10 percent다. 각 쌍의 두 번째 숫자는 SIGKILL 기준이다. 첫 번째 값 아래로 내려가면 earlyoom은 SIGTERM을 보내고, 두 번째 값 아래로 내려가면 SIGKILL을 보낸다. 두 번째 값의 기본값은 첫 번째 값의 절반이다. sudo systemctl restart earlyoom로 변경 사항을 적용한다. journalctl -u earlyoom를 확인하면 어떤 프로세스를 종료했는지와 해당 프로세스가 사용하던 메모리 양을 볼 수 있다.
systemd-oomd은 다른 선택지다. 매뉴얼 페이지에서는 이를 “커널 공간에서 OOM이 발생하기 전에 cgroups-v2와 pressure stall information(PSI)을 사용해 모니터링하고 교정 조치를 수행하는 system service”로 설명한다. 이 도구는 개별 프로세스가 아니라 전체 cgroup에 작동한다. 따라서 남아 있는 하위 프로세스 하나가 아니라 unit을 종료한다. unit은 ManagedOOMMemoryPressure=kill 또는 ManagedOOMSwap=kill로 사용을 선택하며, 임계값은 /etc/systemd/oomd.conf에 있다.
systemctl status systemd-oomd
oomctloomctl은 현재 모니터링 중인 대상을 출력한다. 서버 이미지에서는 unit별로 사용을 선택하는 방식이므로 아무것도 출력되지 않는 경우가 많다. 데몬 하나만 선택해 사용한다. 두 도구를 모두 실행하면 어느 프로세스를 종료할지 결정하려고 서로 경쟁하게 된다. 또한 종료 원인을 나중에 재구성하기가 더 어려워진다.
어떤 unit이 원인이었는가?
커널부터 확인해야 한다. 커널은 자신이 실행한 모든 kill을 기록하기 때문이다.
journalctl -k --grep "Killed process" --since "2 hours ago"global OOM killer가 실행한 kill은 다음과 같이 표시된다.
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:0anon-rss은 프로세스가 종료될 때 RAM에 보유하고 있던 메모리다. 여기서는 약 1.8 GB다. 대괄호 안의 이름은 주의해서 읽어야 한다. 이 이름은 커널이 선택한 victim을 나타낸다. 커널은 가장 큰 프로세스를 선택하지만, 그 프로세스가 메모리 부족을 일으킨 원인인 것은 항상 아니다.
cgroup limit으로 인한 kill은 접두사가 다르다. 그 위에 출력된 보고서에는 자체 한도에 도달한 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는 하나의 unit이 지정한 MemoryMax=에 도달했고 시스템의 나머지 부분은 정상이었다는 뜻이다. 일반적인 Out of memory는 시스템 전체의 메모리가 부족했다는 뜻이다. 따라서 cap이 없었거나 합산했을 때 지나치게 컸을 가능성이 있다.
그런 다음 systemd가 확인한 내용을 조회한다.
systemctl status myapp.service
journalctl -u myapp.service -n 50myapp.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 status는 같은 내용을 한 줄로 표시하며, 그 값은 Active: failed (Result: oom-kill)이다.
세 번째 확인 대상은 cgroup counter다. throttling을 기록하는 유일한 출처이기도 하다. throttling은 로그 라인을 전혀 생성하지 않는다.
cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
cat /sys/fs/cgroup/system.slice/myapp.service/memory.peaklow 0
high 4213
max 118
oom 12
oom_kill 12high은 unit이 MemoryHigh=을 초과해 throttling된 횟수를 센다. max은 hard cap에 도달한 횟수를 센다. oom_kill은 실제로 kill된 프로세스 수를 센다. oom_kill 0와 함께 high이 크다면 앞에서 설명한 silent case다. 서비스는 실행 중이지만 매우 느려졌고, 누구에게도 failure를 보고하지 않은 상태다. memory.peak은 Linux 5.19 이상에서 cgroup이 도달한 최고 사용량을 저장한다. 이 값을 기준으로 MemoryMax=의 크기를 정해야 한다. unit이 재시작하면 두 파일 모두 초기화된다. systemd가 cgroup을 다시 생성하기 때문이다.
이 모든 과정에는 하나의 prerequisite가 있다. /var/log/journal이 존재하지 않으면 journal이 RAM에 저장된다. 그러면 시스템을 복구하기 위해 필요했던 reboot 후 모든 로그 라인이 사라진다.
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --list-bootsjournalctl --list-boots에 current boot보다 많은 boot가 표시되면 history가 보존되고 있다는 뜻이다. 따라서 journalctl -k -b -1을 사용해 장애가 발생한 boot의 kernel 메시지를 확인할 수 있다.
소형 VPS의 시작점
2 GB 요금제에서는 커널과 페이지 캐시에 300~400 MB를 남겨 두고, 모든 제한의 합계가 2 GB 전체에 이르지 않게 해야 한다. 모든 unit가 동시에 최대치에 도달할 수 있기 때문이다. 가장 중요한 서비스에 가장 큰 몫을 할당한 다음, 해당 서비스를 보조하는 예측이 어려운 항목에는 제한을 설정한다.
[Service]
MemoryHigh=256M
MemoryMax=384M
MemorySwapMax=0
CPUWeight=20
TasksMax=64
Restart=on-failure
RestartSec=5s접속 경로를 유지하려면 설정을 하나 더 적용할 가치가 있다. ssh.service의 drop-in에 OOMScoreAdjust=-500을 설정하면 전역 OOM killer가 SSH daemon을 종료 대상으로 선택할 가능성이 크게 줄어든다. 이는 control panel에서 서버를 재부팅하지 않고 문제를 해결할 수 있는지 여부를 좌우한다. 이 설정은 커널이 종료 대상을 선택하는 방식만 변경한다. 멈춤 시간이 줄어드는 것은 아니다.
컨테이너는 자체 cgroup에서 실행된다. 이 cgroup은 사용자의 unit 파일이 아니라 컨테이너 runtime이 생성한다. 따라서 docker.service에 설정한 제한은 컨테이너 하나의 제한이 되지 않는다. MemoryMax=과 CPUQuota=에 해당하는 컨테이너별 설정은 Docker Compose에서 메모리 및 CPU 제한 설정에서 설명한다.
FAQ
실행 중인 프로세스를 종료하는 대신 VPS가 멈춘 이유는 무엇입니까?
커널은 회수에 걸린 시간이 아니라 회수 작업이 페이지를 반환했는지를 기준으로 진행 여부를 판단하기 때문입니다. 메모리가 부족하면 커널은 실행 중인 프로그램의 실행 파일 페이지를 포함해 페이지 캐시를 축출한 다음, 다음 명령을 실행할 때 해당 페이지를 다시 읽습니다. 모든 작업이 스토리지를 기다리고 메모리 할당이 기술적으로 실패하지 않으므로 OOM killer가 호출되지 않습니다. 이 상황이 발생하는 동안 /proc/pressure/memory을 확인합니다. full avg10이 40을 넘으면 지난 10초 동안 실행된 작업이 거의 없다는 의미입니다. earlyoom 같은 userspace daemon을 사용하면 시스템이 이 상태에 도달하기 전에 프로세스를 종료할 수 있습니다.
MemoryHigh와 MemoryMax의 차이는 무엇입니까?
MemoryHigh=은 할당을 제한하는 소프트 상한입니다. 커널은 해당 unit에서 메모리를 적극적으로 회수하고 할당 속도를 늦추지만, 사용량은 이 값을 초과할 수 있으며 어떤 프로세스도 종료되지 않습니다. MemoryMax=은 하드 상한입니다. 이 상한 안에서 충족할 수 없는 할당이 발생하면 해당 unit 자체의 cgroup에서 OOM killer가 실행됩니다. 따라서 문제를 일으킨 프로세스가 시스템에서 가장 큰 프로세스 대신 종료됩니다. MemoryHigh=을 MemoryMax=보다 낮게 설정하고 두 값의 차이를 경고 구간으로 간주합니다.
OOM killer가 어떤 서비스를 종료했는지 확인하려면 어떻게 해야 합니까?
journalctl -k --grep "Killed process" --since "2 hours ago"을 실행합니다. Memory cgroup out of memory로 시작하는 줄은 한 unit이 자체 MemoryMax=에 도달했다는 의미입니다. 일반적인 Out of memory은 시스템 전체의 메모리가 고갈되었다는 의미입니다. 그런 다음 journalctl -u <unit> -n 50를 실행하고 Failed with result 'oom-kill'을 찾습니다. 서버에 /var/log/journal이 없다면 journal이 RAM에만 저장되었고 재부팅과 함께 증거도 사라진 것입니다. 다음 장애가 발생하기 전에 해당 디렉터리를 생성합니다.
소형 VPS에 swap을 추가해야 합니까?
작은 swap 파일은 한 번 할당된 뒤 다시 사용되지 않는 비활성 페이지를 처리하는 데 도움이 됩니다. 실행이 폭주하는 프로세스에는 도움이 되지 않습니다. 프로세스 종료를 지연시키고, 짧은 장애를 로그인해서 해결할 수 없는 긴 멈춤으로 바꿀 뿐입니다. swap은 적정한 크기로 유지합니다. 그리고 감당할 수 있는 unit에 MemorySwapMax=0을 설정합니다. 그러면 해당 unit이 상한에 도달한 뒤 빠르게 재시작하고, 중요한 서비스는 swap을 계속 사용할 수 있습니다.
unit 파일을 작성하지 않고 명령을 제한할 수 있습니까?
가능합니다. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh은 터미널에서 명령을 일시적인 scope 안에서 실행하며 해당 제한을 적용합니다. 명령이 종료되면 제한도 사라집니다. systemd.resource-control의 모든 속성은 -p 뒤에 사용할 수 있으므로 MemorySwapMax=, TasksMax=, CPUWeight=도 사용할 수 있습니다. --scope을 제거하고 --unit=name을 추가하면 작업이 백그라운드에서 실행되고 출력은 journal에 기록됩니다.