cgroup v2 메모리 제한이 다르게 동작하는 이유와 해결방법! 10분 읽고 완벽 이해하자!

리눅스 시스템에서 애플리케이션의 메모리 사용량을 효율적으로 관리하는 것은 안정적인 서비스 운영에 필수적입니다. 특히 컨테이너 환경에서는 각 컨테이너가 사용할 수 있는 메모리 양을 제한하여 다른 컨테이너나 호스트 시스템에 영향을 주지 않도록 하는 것이 매우 중요합니다. 이를 위해 리눅스 커널은 ‘컨트롤 그룹’, 줄여서 cgroup이라는 강력한 기능을 제공합니다.

cgroup v2는 기존 cgroup v1의 여러 단점을 개선하여 더 통합적이고 유연한 리소스 관리 기능을 제공합니다. 그러나 많은 사용자들이 cgroup v2의 메모리 제한 기능을 설정할 때 예상과 다른 동작을 경험하곤 합니다. 예를 들어, 분명히 메모리 제한을 설정했는데도 애플리케이션이 예상보다 많은 메모리를 사용하거나, 제한에 도달하지 않았는데도 OOM(Out Of Memory) 킬러에 의해 종료되는 상황이 발생하기도 합니다.

이 문서는 cgroup v2 메모리 제한이 왜 때때로 예상과 다르게 동작하는지 그 원인을 깊이 있게 탐구하고, 실질적인 해결책과 유용한 팁을 제공하여 독자들이 메모리 관리를 더 효과적으로 수행할 수 있도록 돕습니다.

cgroup v2 메모리 관리의 기본 이해

cgroup은 리눅스 프로세스들을 그룹화하고, 이 그룹에 속한 프로세스들의 CPU, 메모리, I/O 등 시스템 리소스 사용량을 제한하거나 모니터링하는 메커니즘입니다. cgroup v2는 v1과 달리 단일 계층 구조(unified hierarchy)를 사용하여 리소스 관리를 더 직관적이고 일관되게 만들었습니다.

cgroup v2 메모리 컨트롤러 주요 설정 파일

  • memory.max: 이 cgroup이 사용할 수 있는 최대 메모리 양을 설정합니다. 이 값에 도달하면 OOM 킬러가 작동하여 프로세스를 종료할 수 있습니다.
  • memory.high: ‘소프트’ 제한입니다. 이 값에 도달하면 커널은 해당 cgroup의 메모리 사용량을 줄이기 위해 적극적으로 메모리 회수(reclaim)를 시도합니다. OOM 킬러가 즉시 작동하지는 않지만, 성능 저하의 신호가 될 수 있습니다.
  • memory.min: 이 cgroup이 최소한으로 확보해야 하는 메모리 양을 설정합니다. 시스템 전체 메모리가 부족할 때도 이 cgroup의 메모리는 최대한 보호됩니다.
  • memory.swap.max: 이 cgroup이 사용할 수 있는 최대 스왑 메모리 양을 설정합니다.
  • memory.current: 이 cgroup이 현재 사용하고 있는 총 메모리 양을 보여줍니다.
  • memory.stat: 이 cgroup의 다양한 메모리 통계 정보를 제공합니다. (예: anon, file, slab, vmscan 등)

예상과 다른 동작의 주요 원인 분석

cgroup v2의 메모리 제한이 예상과 다르게 동작하는 가장 흔한 원인들은 다음과 같습니다.

커널 페이지 캐시의 영향

가장 흔하고 중요한 오해 중 하나는 memory.max가 애플리케이션의 힙 메모리나 스택 메모리만을 제한한다고 생각하는 것입니다. 그러나 cgroup v2의 memory.max는 애플리케이션이 직접 사용하는 메모리(Resident Set Size, RSS)뿐만 아니라, 운영체제가 디스크 I/O 성능 향상을 위해 사용하는 ‘커널 페이지 캐시(Kernel Page Cache)’도 포함하여 계산합니다.

  • 문제점: 애플리케이션이 파일 읽기/쓰기 작업을 많이 수행하면 페이지 캐시가 크게 증가할 수 있습니다. 이 페이지 캐시는 memory.max에 포함되기 때문에, 애플리케이션의 실제 코드나 데이터 메모리 사용량이 낮아 보여도 페이지 캐시 때문에 전체 메모리 사용량이 memory.max에 도달하여 OOM 킬러에 의해 종료될 수 있습니다.
  • 영향: 개발자는 애플리케이션이 메모리 누수가 없는데도 OOM이 발생한다고 오해할 수 있습니다. 특히 데이터베이스, 웹 서버, 파일 처리 애플리케이션에서 이런 현상이 두드러집니다.

스왑 메모리 활용

리눅스 시스템은 물리 메모리가 부족할 때 일부 메모리 페이지를 디스크의 스왑 공간으로 옮겨(스왑 아웃) 물리 메모리를 확보합니다. cgroup v2는 memory.swap.max를 통해 각 cgroup이 사용할 수 있는 스왑 메모리의 양을 제한할 수 있습니다.

  • 문제점: memory.swap.max가 충분히 크거나 설정되지 않은 경우, cgroup의 메모리 사용량이 memory.max에 도달하기 전에 많은 메모리가 스왑 아웃될 수 있습니다. 이 경우 memory.current 값은 낮게 유지되지만, 실제로는 애플리케이션의 성능이 심각하게 저하될 수 있습니다.
  • 영향: 애플리케이션이 응답 없음 상태가 되거나, 매우 느려지는 현상이 발생할 수 있습니다. 사용자는 OOM이 발생하지 않았는데도 서비스가 마비되었다고 느낄 수 있습니다.

메모리 오버커밋 정책

리눅스 커널은 기본적으로 ‘메모리 오버커밋(Memory Overcommit)’을 허용합니다. 이는 시스템이 실제 물리 메모리보다 더 많은 메모리를 프로그램들에게 할당해주겠다고 약속하는 것을 의미합니다. 실제 메모리 사용량은 약속된 양보다 적을 것이라는 가정을 기반으로 합니다.

  • 문제점: cgroup의 memory.max 제한은 해당 cgroup 내에서만 작동합니다. 시스템 전체적으로 메모리 오버커밋이 심하게 발생하고, 다른 cgroup이나 호스트 프로세스가 갑자기 많은 메모리를 사용하게 되면, 제한된 cgroup이 memory.max에 도달하지 않았음에도 불구하고 시스템 전체 OOM 킬러에 의해 종료될 수 있습니다.
  • 영향: 특정 cgroup의 메모리 사용량이 충분히 낮은데도 불구하고, 예상치 못하게 OOM 킬러에 의해 프로세스가 종료되는 상황이 발생할 수 있습니다.

메모리 압력과 OOM 킬러의 동작

cgroup v2의 memory.high는 메모리 압력(Memory Pressure)을 관리하기 위한 중요한 도구입니다. 이 값에 도달하면 커널은 해당 cgroup의 메모리 회수를 시작하여 메모리 사용량을 줄이려고 시도합니다.

  • 문제점: memory.high는 강제적인 제한이 아니므로, 이 값에 도달해도 OOM 킬러가 즉시 작동하지 않습니다. 커널이 메모리 회수에 실패하고, 결국 memory.max에 도달하거나 시스템 전체 메모리가 고갈되어야 OOM 킬러가 작동합니다. 이 과정에서 애플리케이션의 성능이 점진적으로 저하될 수 있습니다.
  • 영향: 사용자는 memory.high를 넘어도 애플리케이션이 계속 실행되는 것을 보고 제한이 제대로 작동하지 않는다고 오해할 수 있습니다. 또한, OOM 킬러가 작동하기까지의 지연 시간 동안 서비스 품질이 저하될 수 있습니다.

OOM 킬러의 우선순위

리눅스 커널의 OOM 킬러는 시스템 전체 메모리 부족 상황에서 어떤 프로세스를 종료할지 결정하기 위한 복잡한 휴리스틱을 사용합니다. cgroup v2 내에서 OOM이 발생할 때도 이 정책이 영향을 미칩니다.

  • 문제점: cgroup 내의 OOM 킬러는 해당 cgroup 내의 프로세스만 대상으로 하지만, 시스템 전체 OOM 킬러는 모든 프로세스를 대상으로 합니다. 이 두 가지가 상호작용하면서 예상치 못한 결과가 발생할 수 있습니다. 예를 들어, 특정 cgroup의 memory.max에 도달하기 전에 시스템 전체 OOM 킬러가 다른 cgroup의 프로세스를 먼저 종료할 수도 있습니다.
  • 영향: 특정 cgroup에 메모리 제한을 걸었음에도 불구하고, 다른 cgroup의 프로세스가 OOM 킬러에 의해 종료되는 등 예측 불가능한 동작이 발생할 수 있습니다.

실생활에서의 활용 방법과 유용한 팁

cgroup v2 메모리 제한의 복잡성을 이해했다면, 이제 이를 효과적으로 활용하기 위한 실용적인 방법들을 알아보겠습니다.

정확한 메모리 모니터링

애플리케이션의 실제 메모리 사용 패턴을 이해하는 것이 중요합니다. memory.current 외에도 memory.stat 파일을 주의 깊게 살펴보세요.

  • memory.stat 파일에서 anon (익명 메모리, 힙/스택 등)과 file (페이지 캐시) 사용량을 구분하여 확인하세요.
  • vmstat, free -h, top, htop, /proc/meminfo 등 다양한 시스템 도구를 사용하여 시스템 전체 및 프로세스별 메모리 사용량을 모니터링하세요.
  • 컨테이너 환경에서는 Docker의 docker stats나 Kubernetes의 메트릭 서버(kubectl top pod)를 활용하세요.

적절한 memory.max 설정

memory.max 값은 애플리케이션이 안정적으로 작동하는 데 필요한 최대 메모리 양을 반영해야 합니다. 여기에는 애플리케이션 코드 및 데이터뿐만 아니라 예상되는 페이지 캐시 사용량까지 포함해야 합니다.

  • : 개발 및 테스트 환경에서 애플리케이션의 피크 메모리 사용량을 충분히 테스트하고, 여기에 약간의 여유를 두어 memory.max를 설정하는 것이 좋습니다. 페이지 캐시로 인한 변동성을 고려하여 넉넉하게 설정하는 것이 중요합니다.
  • 전문가 조언: “너무 타이트한 memory.max 설정은 불필요한 OOM 킬을 유발하여 서비스 안정성을 해칠 수 있습니다. 항상 애플리케이션의 실제 사용량과 페이지 캐시를 고려한 충분한 버퍼를 확보하세요.”

memory.high의 전략적 활용

memory.high는 OOM 킬러를 피하면서도 메모리 압력을 조기에 감지하고 대응하는 데 유용합니다.

  • 활용 방법: memory.max보다 약간 낮은 값으로 memory.high를 설정하세요. 이 값에 도달하면 커널이 메모리 회수를 시작하므로, 애플리케이션 성능이 저하되기 시작하는 지점을 예측할 수 있습니다.
  • : memory.high에 도달했을 때 경고를 발생시키고, 애플리케이션이 스스로 메모리 사용량을 줄이도록 유도하거나, 스케일 아웃을 준비하는 등의 자동화된 조치를 취할 수 있습니다.

스왑 메모리 관리

스왑 메모리 사용 여부와 양은 성능과 안정성에 큰 영향을 미칩니다.

  • : 고성능이 요구되는 애플리케이션의 경우 memory.swap.max0으로 설정하여 스왑 사용을 완전히 비활성화하는 것을 고려할 수 있습니다. 이 경우 memory.max를 초과하면 즉시 OOM 킬이 발생하므로, memory.max를 더 정확하게 설정해야 합니다.
  • : 일반적인 서비스에서는 memory.swap.maxmemory.max보다 약간 크게 설정하여 일시적인 메모리 스파이크에 대비하는 것도 한 방법입니다. 그러나 스왑 사용은 성능 저하를 의미한다는 것을 항상 기억해야 합니다.

OOM 킬러 정책 조정

memory.oom.group 설정은 OOM 발생 시 해당 cgroup 내의 모든 프로세스를 함께 종료할지 여부를 결정합니다.

  • 활용 방법: 기본값은 0으로, OOM 킬러가 개별 프로세스를 종료합니다. 1로 설정하면 cgroup 내의 모든 프로세스가 함께 종료됩니다. 이는 다중 프로세스 애플리케이션(예: 웹 서버)에서 한 프로세스가 OOM으로 종료될 때 다른 프로세스들도 불안정해지는 것을 방지하고, 깨끗한 재시작을 유도하는 데 유용합니다.

컨테이너 환경에서의 고려사항

Docker, Kubernetes와 같은 컨테이너 오케스트레이션 플랫폼은 내부적으로 cgroup v2를 활용합니다.

  • Docker: docker run --memory , --memory-swap 옵션을 사용하여 cgroup 메모리 제한을 설정할 수 있습니다.
  • Kubernetes: Pod의 리소스 요청(requests) 및 제한(limits)을 통해 cgroup 설정을 제어합니다. limits.memorymemory.max에 해당하며, requests.memorymemory.min과 유사하게 작동합니다.
  • : 컨테이너 환경에서는 호스트 시스템의 메모리 오버커밋 설정과 컨테이너 런타임의 기본 설정이 cgroup 동작에 영향을 미칠 수 있으므로, 해당 플랫폼의 문서를 잘 숙지해야 합니다.

흔한 오해와 사실 관계

오해1 memory.max는 애플리케이션의 힙 메모리만 제한한다

memory.max는 애플리케이션의 힙, 스택, 코드 섹션뿐만 아니라 커널 페이지 캐시, 커널이 할당한 특정 버퍼 등 해당 cgroup이 사용하는 모든 유형의 메모리를 포함하여 계산합니다. 이는 특히 파일 I/O가 많은 애플리케이션에서 중요한 고려사항입니다.

오해2 memory.max에 도달하면 즉시 애플리케이션이 종료된다

memory.max에 도달한다고 해서 프로세스가 즉시 종료되는 것은 아닙니다. 커널은 먼저 메모리 회수(reclaim)를 시도하여 메모리 사용량을 줄이려고 노력합니다. 이 과정이 실패하고 더 이상 메모리를 확보할 수 없을 때 OOM 킬러가 작동하여 프로세스를 종료합니다. 이 과정에는 약간의 지연이 있을 수 있습니다.

오해3 스왑을 비활성화하면 메모리 부족 문제가 해결된다

스왑은 시스템 메모리가 부족할 때 최후의 보루 역할을 합니다. 스왑을 완전히 비활성화하면, 일시적인 메모리 스파이크나 예상치 못한 메모리 사용량 증가 시 OOM 킬러가 더 빠르게 작동하여 서비스 중단을 초래할 수 있습니다. 스왑은 성능 저하를 유발하지만, 서비스 안정성을 높이는 데 기여할 수도 있습니다. 시스템의 특성과 애플리케이션의 요구사항에 따라 신중하게 결정해야 합니다.

오해4 memory.high는 강력한 메모리 제한이다

memory.high는 ‘소프트’ 제한입니다. 이 값에 도달하면 커널은 메모리 회수를 시작하지만, OOM 킬러가 즉시 작동하지는 않습니다. memory.high는 주로 성능 저하를 조기에 감지하고, OOM 킬을 피하기 위한 예방적 조치로 활용됩니다. 실제 프로세스 종료는 memory.max에 의해 결정됩니다.

자주 묻는 질문과 답변

Q1 컨테이너에서 memory.max를 설정했는데도 애플리케이션이 OOM 킬러에 의해 종료돼요. 왜 그런가요?

가장 흔한 원인은 페이지 캐시 때문입니다. 애플리케이션의 힙/스택 메모리 사용량은 낮아 보여도, 파일 I/O로 인한 페이지 캐시가 memory.max를 초과하여 OOM이 발생할 수 있습니다. 또한, 스왑이 활성화되어 있다면 스왑 공간을 다 쓰고 OOM이 발생할 수도 있습니다. memory.stat 파일을 확인하여 anonfile 메모리 사용량을 구분하여 분석하고, 시스템 전체 메모리 오버커밋 설정도 확인해보세요.

Q2 memory.high는 어떻게 사용하는 것이 가장 효과적인가요?

memory.high는 애플리케이션의 성능이 저하되기 시작하는 지점을 나타내는 ‘경고등’으로 활용하는 것이 좋습니다. memory.max보다 10~20% 정도 낮은 값으로 설정하여, 이 값에 도달하면 모니터링 시스템에서 경고를 발생시키고, 필요시 애플리케이션의 스케일 아웃을 준비하거나 메모리 프로파일링을 시작하는 등의 조치를 취할 수 있습니다. 이는 OOM 킬을 사전에 방지하고 서비스 연속성을 유지하는 데 큰 도움이 됩니다.

Q3 memory.max를 너무 낮게 설정하면 어떤 문제가 발생하나요?

memory.max를 너무 낮게 설정하면 애플리케이션이 정상적으로 작동하는 데 필요한 최소한의 메모리조차 확보하지 못해 OOM 킬러에 의해 자주 종료될 수 있습니다. 이는 서비스의 안정성을 심각하게 해치고, 잦은 재시작으로 인해 사용자 경험이 저하될 수 있습니다. 항상 애플리케이션의 실제 메모리 사용 패턴을 분석하여 충분한 여유를 두고 설정해야 합니다.

Q4 cgroup v2 메모리 제한을 비용 효율적으로 활용하는 방법이 있나요?

네, 있습니다.

  • 정확한 리소스 할당: 애플리케이션의 실제 메모리 사용 패턴을 정확히 파악하여 memory.max를 설정하면, 불필요하게 많은 클라우드 인스턴스 크기(메모리)를 할당하는 것을 방지할 수 있습니다. 이는 클라우드 비용 절감으로 이어집니다.
  • memory.high 활용: memory.high를 통해 메모리 압력을 조기에 감지하고 대응하면, OOM 킬러에 의한 갑작스러운 서비스 중단을 줄일 수 있습니다. 서비스 중단은 재시작 비용(CPU, 네트워크 등)뿐만 아니라 비즈니스 기회 손실로 이어질 수 있으므로, 이를 방지하는 것이 비용 효율적입니다.
  • 스왑 정책 최적화: 스왑 사용 여부와 memory.swap.max 설정을 애플리케이션의 특성에 맞춰 최적화하면, 고가의 물리 메모리 증설 없이도 안정적인 서비스 운영이 가능할 수 있습니다. 물론 이 경우 성능 저하와의 트레이드오프를 고려해야 합니다.

결론적으로, cgroup v2 메모리 제한을 정확히 이해하고 모니터링하며 최적화하는 것은 시스템 안정성뿐만 아니라 리소스 비용 효율성 측면에서도 매우 중요합니다.

이 게시물이 얼마나 유용했습니까?

평점을 매겨주세요.

평균 평점 0 / 5. 투표 수 : 0

가장 먼저 게시물을 평가해보세요.

댓글 남기기