OOM 정책 마스터하기: Systemd의 Out-of-Memory 이벤트 대응 튜닝

systemd를 사용하여 Linux의 Out-of-Memory(OOM) 킬러 동작을 제어하는 방법을 알아보세요. 이 가이드는 `OOMScoreAdjust` 및 `OOMPolicy` 지시문을 탐구하여 메모리 부족 상황에서 종료될 프로세스를 제어함으로써 중요 서비스를 보호하는 방법을 설명합니다. 향상된 시스템 안정성과 복원력을 위해 systemd의 OOM 튜닝을 마스터하세요.

OOM 정책 마스터하기: Systemd의 Out-of-Memory 이벤트 대응 튜닝

Out-of-memory(메모리 부족) 오류는 편리한 시간에 거의 발생하지 않습니다. 배치 가져오기에서 평소보다 큰 파일을 처리하거나, 서비스가 밤새 메모리를 누수하거나, 백업이 트래픽 급증과 겹치거나, 배포로 인해 작업자 프로세스 수가 두 배로 늘어나는 경우가 있습니다. Linux가 할당을 위해 충분한 메모리를 확보할 수 없을 때, 커널은 OOM 킬러를 호출하여 프로세스를 종료함으로써 시스템이 계속 실행되도록 할 수 있습니다.

불편한 점은 기본 희생자가 사용자가 선택했을 서비스가 아닐 수 있다는 것입니다. 공유 호스트에서는 메인 API보다 재시도 가능한 큐 작업자가 먼저 죽는 것을 선호할 수 있습니다. 데이터베이스 서버에서는 시스템을 복구할 수 있도록 SSH와 모니터링이 계속 살아 있기를 원할 수 있습니다. Systemd는 이러한 결정을 위한 두 가지 조정 도구를 제공합니다: OOMScoreAdjust=OOMPolicy=.

OOMScoreAdjust=는 어떤 프로세스가 선택될지에 영향을 줍니다. OOMPolicy=는 서비스 내 프로세스가 종료된 후 systemd가 수행할 작업을 제어합니다. 이들은 서로 다른 문제를 해결하며, 혼동하면 잘못된 운영 매뉴얼로 이어집니다.

커널이 점수를 매기는 방식

모든 Linux 프로세스에는 /proc/<pid>/oom_score에서 볼 수 있는 OOM 점수가 있습니다. 점수가 높을수록 프로세스가 OOM 희생자가 될 가능성이 더 높습니다. 커널은 메모리 사용량 및 기타 컨텍스트에서 해당 점수를 도출한 다음 /proc/<pid>/oom_score_adj의 조정 값을 적용합니다.

Systemd의 OOMScoreAdjust=는 시작하는 프로세스에 대해 해당 조정 값을 기록합니다. 범위는 -1000에서 1000까지입니다.

  • -1000은 가장 강력한 보호를 제공하며 해당 프로세스에 대한 OOM 종료를 사실상 비활성화합니다.
  • 음수 값은 프로세스가 종료될 가능성을 낮춥니다.
  • 양수 값은 프로세스가 종료될 가능성을 높입니다.
  • 0은 조정을 중립으로 유지합니다.

가장 안전한 접근 방식은 일반적으로 "모든 중요한 것을 보호"하는 것이 아닙니다. 모든 서비스가 보호되면 호스트가 이미 메모리가 부족할 때 커널이 선택할 수 있는 유용한 옵션이 줄어듭니다. 소수의 서비스만 보호하고 폐기 가능한 작업은 더 쉽게 종료되도록 만드세요.

주요 API 서비스의 경우 적당한 조정으로 충분한 경우가 많습니다:

[Service]
OOMScoreAdjust=-300

작업을 재시도할 수 있는 큐 작업자의 경우:

[Service]
OOMScoreAdjust=500

해당 작업자는 메모리 압박 중에 먼저 죽을 수 있지만, 그것이 요점입니다. 실패한 작업은 큐로 돌아갈 수 있습니다. 죽은 데이터베이스나 연결할 수 없는 호스트는 더 큰 사고입니다.

OOMPolicy가 실제로 하는 일

OOMPolicy=는 유닛을 "중요"로 표시하지 않으며, 먼저 종료할 프로세스를 선택하지 않습니다. 지원되는 값은 continue, stop, kill입니다.

  • continue: systemd가 OOM 이벤트를 기록하고 프로세스가 남아 있으면 유닛을 계속 실행합니다.
  • stop: systemd가 이벤트를 기록하고 유닛을 정상적으로 중지합니다.
  • kill: 유닛 내 하나의 프로세스가 OOM으로 종료되면 해당 유닛의 나머지 프로세스가 그룹으로 종료됩니다.

이 설정을 사용하여 반쯤 살아있는 서비스를 방지하세요. 다중 프로세스 웹 서비스가 작업자를 잃고 손상된 상태에서 계속 트래픽을 수락하는 경우 continue는 실패를 숨길 수 있습니다. OOMPolicy=kill은 실패를 명확하게 하고 Restart=on-failure가 서비스를 깨끗한 상태로 복원할 수 있게 합니다.

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

도우미 프로세스가 있는 배치 작업의 경우 stop이 나머지 프로세스에 대해 덜 갑작스러울 수 있습니다:

[Service]
OOMPolicy=stop

커널이 선택한 프로세스는 이미 사라졌습니다. stop은 systemd가 서비스의 나머지 부분에 대해 수행하는 작업에만 영향을 미치므로, 이를 정상적인 저장 지점으로 의존하지 마세요. 장기 실행 작업은 자체 작업을 체크포인트해야 합니다.

실용적인 튜닝 패턴

먼저 서비스를 세 그룹으로 분류하세요.

첫째, 호스트를 복구 가능하게 유지하는 서비스를 식별하세요: SSH, 네트워킹, 모니터링 및 주요 워크로드. 가장 중요한 서비스에만 적당한 음수 조정을 제공하세요.

둘째, 재시도할 수 있는 서비스를 식별하세요: 작업자, 가져오기 도구, 보고서 생성기, 이미지 프로세서, 캐시 워머, 개발 도우미. 이러한 서비스에 양수 조정을 제공하세요.

셋째, 각 서비스가 하나의 프로세스가 종료된 후에도 안전하게 계속 실행될 수 있는지 결정하세요. 그렇지 않은 경우 OOMPolicy=kill 및 재시작 정책을 사용하세요.

현실적인 작업자 재정의는 다음과 같을 수 있습니다:

# /etc/systemd/system/image-worker.service.d/oom.conf
[Service]
OOMScoreAdjust=500
OOMPolicy=kill
Restart=on-failure
RestartSec=10s

주요 애플리케이션 서비스는 다음과 같을 수 있습니다:

# /etc/systemd/system/api.service.d/oom.conf
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
RestartSec=5s

실패 모드를 테스트하지 않았다면 OOMScoreAdjust=-1000은 피하는 것이 좋습니다. 보호된 서비스가 메모리를 누수하는 경우, 시스템은 여전히 복구 방법이 필요합니다.

변경 사항 적용 및 확인

패키지된 유닛 파일을 편집하는 대신 드롭인을 사용하세요:

sudo systemctl edit api.service

재정의를 저장한 후 systemd를 다시 로드하고 서비스를 다시 시작하세요:

sudo systemctl daemon-reload
sudo systemctl restart api.service

병합된 유닛과 systemd가 보는 값을 확인하세요:

systemctl cat api.service
systemctl show api.service -p OOMPolicy -p OOMScoreAdjust

그런 다음 실행 중인 프로세스를 검사하세요:

PID=$(systemctl show api.service -p MainPID --value)
cat /proc/$PID/oom_score_adj
cat /proc/$PID/oom_score

oom_score_adj는 구성된 조정 값과 일치해야 합니다. oom_score는 프로세스가 메모리를 더 많이 또는 적게 사용함에 따라 변경될 수 있습니다.

사고 후에는 유닛 로그와 커널 로그를 모두 확인하세요:

journalctl -u api.service --since "1 hour ago"
journalctl -k --since "1 hour ago" | grep -i oom

systemd-oomd를 사용하는 시스템에서는 다음도 확인하세요:

systemctl status systemd-oomd
oomctl

OOM 정책은 용량 계획이 아닙니다

OOM 튜닝은 최후의 방어선입니다. 여전히 메모리 제한, 알림 및 정상적인 급증을 위한 충분한 여유 공간이 필요합니다. 예측 가능한 경계가 있는 서비스의 경우 cgroup 메모리 제어를 고려하세요:

[Service]
MemoryHigh=1500M
MemoryMax=2G

MemoryHigh=는 하드 제한 전에 압력을 가합니다. MemoryMax=는 상한선입니다. 정확한 동작은 systemd 버전 및 cgroup 설정에 따라 다르지만, 운영 아이디어는 간단합니다: 호스트를 소비하기 전에 하나의 서비스를 제한하세요.

스왑도 같은 방식으로 고려해야 합니다. 스왑이 없으면 짧은 급증이 갑작스러운 OOM 종료로 이어질 수 있습니다. 너무 많은 느린 스왑은 지연 시간이 무용지물이 되는 동안 호스트를 계속 살릴 수 있습니다. OOM 정책을 스왑, 메모리 제한, 재시작 동작 및 알림과 함께 검토하세요.

예시: 하나의 호스트, 세 가지 서비스

소규모 프로덕션 호스트가 API, Redis 캐시 및 백그라운드 보고서 작업자를 실행한다고 가정해 보겠습니다. 보고서 작업자는 유용하지만 작업을 재시도할 수 있습니다. Redis는 지연 시간을 개선하지만 애플리케이션은 데이터베이스로 이동하여 일부 요청을 계속 처리할 수 있습니다. API는 고객 대면 서비스입니다.

합리적인 첫 번째 시도는 다음과 같을 수 있습니다:

# api.service
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
# redis.service 드롭인, 이 Redis 인스턴스가 캐시 전용인 경우
[Service]
OOMScoreAdjust=0
OOMPolicy=kill
# report-worker.service
[Service]
OOMScoreAdjust=600
OOMPolicy=kill
Restart=on-failure

이것이 모든 가능한 경우에 작업자가 먼저 죽는 것을 보장하지는 않지만, 의도를 명확히 합니다. 보고서 작업자가 너무 커지면 더 쉬운 대상이 됩니다. API가 프로세스 중 하나를 잃으면 systemd가 나머지를 종료하고 깨끗하게 다시 시작합니다. Redis가 캐시일 뿐이라면 과도하게 보호하지 않기로 선택할 수 있습니다. Redis가 기본 데이터 저장소라면 다른 결정을 내릴 것입니다.

이것이 OOM 정책이 제품 이름이 아닌 서비스 역할에 연결되어야 하는 이유입니다. "Redis"는 자동으로 중요하거나 폐기 가능하지 않습니다. "재구축할 수 있는 캐시"와 "세션 상태의 유일한 복사본"은 다른 운영 개체입니다.

재앙을 만들지 않고 테스트하기

설정이 적용되었는지 확인하기 위해 프로덕션 서버를 충돌시킬 필요는 없습니다. 검사부터 시작하세요:

systemctl show report-worker.service -p OOMScoreAdjust -p OOMPolicy
systemctl status report-worker.service

그런 다음 실행 중인 프로세스를 확인하세요:

PID=$(systemctl show report-worker.service -p MainPID --value)
cat /proc/$PID/oom_score_adj

더 깊은 테스트를 위해 동일한 systemd 버전 및 cgroup 모드를 가진 스테이징 호스트 또는 폐기 가능한 가상 머신을 사용하세요. 공유 프로덕션 상자가 아닌 곳에서 제어된 메모리 압력 도구를 실행하세요. 목표는 광범위한 동작을 확인하는 것입니다: 작업자가 더 쉽게 종료되고, 주요 서비스가 반쯤 살아 있지 않으며, 재시작 동작이 저널에 표시됩니다.

컨테이너를 사용하는 경우 배포하는 것과 동일한 형태로 테스트하세요. systemd 아래에서 직접 실행되는 서비스는 자체 메모리 제한이 있는 컨테이너 내부의 프로세스와 동일하게 동작하지 않습니다. 커널은 호스트가 전역적으로 메모리가 부족하기 전에 컨테이너 제한을 적용할 수 있습니다. 이 경우 컨테이너 런타임, Kubernetes 또는 cgroup 설정이 무엇이 죽을지 결정하는 첫 번째 계층이 될 수 있습니다.

사고 후 분석

OOM 이벤트 후에는 "RAM이 더 필요해"로 바로 뛰어드는 것을 피하세요. 때로는 그렇습니다. 때로는 캐시가 TTL을 잊었습니다. 때로는 배포가 작업자 동시성을 변경했습니다. 때로는 지속성 또는 백업 활동으로 인해 copy-on-write 메모리가 급증했습니다.

세 가지를 확인하세요:

journalctl -k --since "2026-05-24 01:00" | grep -i oom
journalctl -u api.service --since "2026-05-24 01:00"
systemctl show api.service -p Result -p NRestarts

커널 로그는 일반적으로 어떤 프로세스가 종료되었는지 알려줍니다. 유닛 로그는 systemd가 어떻게 반응했는지 알려줍니다. 재시작 카운터는 서비스가 깨끗하게 복구되었는지 또는 깜빡였는지 알려줍니다.

그런 다음 종료된 프로세스를 의도한 우선순위와 비교하세요. 보호된 서비스가 폐기 가능한 작업자보다 먼저 죽었다면, 작업자가 실제로 튜닝한 유닛 아래에서 실행 중이었는지, 재정의가 로드되었는지, 다른 메모리 제한이 먼저 발동되었는지 확인하세요. 선택된 희생자가 정책과 일치하지만 사고가 여전히 사용자에게 피해를 준 경우 서비스 분류를 변경해야 할 수 있습니다.

값뿐만 아니라 이유도 문서화하세요

OOM 설정은 나쁜 날까지 유닛 드롭인에 조용히 자리 잡고 있기 때문에 잊기 쉽습니다. 재정의 또는 인프라 저장소에 조정 이유를 설명하는 짧은 주석을 남기세요.

[Service]
# 재시도 가능한 큐 작업자. 호스트 압박 중에 api.service보다 이 작업자를 먼저 종료하는 것을 선호합니다.
OOMScoreAdjust=600
OOMPolicy=kill

해당 주석은 사고 검토 중에 시간을 절약합니다. 그것 없이는 누군가가 양수 OOM 점수를 보고 의도적인 우선순위 결정임을 깨닫지 못한 채 0으로 "수정"할 수 있습니다.

또한 설정을 마지막으로 검토한 시점을 기록하세요. 서비스는 시간이 지남에 따라 역할이 변경될 수 있습니다. 한때 폐기 가능한 썸네일을 처리하던 작업자가 나중에 결제, 내보내기 또는 고객이 볼 수 있는 작업을 처리할 수 있습니다. OOM 정책은 서비스의 원래 목적이 아닌 현재 위험을 따라야 합니다.

일반적인 잘못된 구성

한 가지 잘못된 구성은 데이터베이스, API, 작업자, 캐시, 로그 전달자 및 모니터링 에이전트를 모두 한 번에 보호하는 것입니다. 신중해 보이지만 커널의 옵션을 줄입니다. 우선순위를 선택하세요.

또 다른 잘못된 구성은 누락된 자식 프로세스를 용납할 수 없는 서비스에 OOMPolicy=continue를 설정하는 것입니다. 프로세스 관리자, 웹 서버 또는 사용자 정의 데몬은 워크로드의 일부가 사라진 후에도 유닛을 활성 상태로 유지할 수 있습니다. 로드 밸런서가 포트가 열려 있는지만 확인하는 경우 트래픽이 저하된 서비스로 계속 흐를 수 있습니다.

세 번째 잘못된 구성은 재시도 동작 없이 양수 조정을 하는 것입니다. 서비스를 쉽게 종료할 수 있게 만든다면 종료가 허용 가능한지 확인하세요. 큐 작업자의 경우 작업이 성공적으로 처리된 후에만 승인됨을 의미합니다. 배치 작업의 경우 체크포인트를 의미합니다. 캐시 워머의 경우 캐시를 나중에 재구축할 수 있음을 의미합니다.

마지막으로, 자동 재시작만으로 OOM 이벤트를 숨기지 마세요. 누수 서비스를 다시 시작하면 시간을 벌 수 있지만 메모리가 상승하고 서비스가 죽으며 사용자가 주기적인 실패를 보는 루프를 만들 수 있습니다. 프로세스 상태뿐만 아니라 재시작 횟수 및 메모리 증가에 대한 알림을 추가하세요.

간단한 운영 매뉴얼

실제 서버를 튜닝할 때는 반복 가능한 체크리스트를 사용하세요:

  1. 복구 및 사용자 트래픽에 필요한 서비스를 나열하세요.
  2. 먼저 종료될 수 있는 재시도 가능한 서비스를 나열하세요.
  3. 폐기 가능한 작업에 양수 OOMScoreAdjust 값을 추가하세요.
  4. 보호할 가치가 있는 소수의 서비스에만 적당한 음수 값을 추가하세요.
  5. 부분적으로 실행되어서는 안 되는 서비스에 OOMPolicy=kill을 사용하세요.
  6. systemctl show/proc을 통해 적용된 값을 확인하세요.
  7. OOM 이벤트가 발생하기 전에 메모리 압박에 대한 알림을 설정하세요.

목표는 OOM 이벤트를 무해하게 만드는 것이 아닙니다. 목표는 이해할 수 있게 만드는 것입니다. OOMScoreAdjust=는 희생자를 선택하는 데 도움이 됩니다. OOMPolicy=는 유닛의 나머지 부분에 어떤 일이 발생하는지 정의하는 데 도움이 됩니다. 함께 사용하면 메모리가 이미 고갈되었을 때 더 예측 가능한 실패 순서를 제공합니다.