하드웨어 중급 #3 메모리 심화 — available, 더티 페이지, 컨테이너 한도

5 분 소요

2편의 CPU에 이어 메모리입니다. 기초 3편에서 메모리 계층, 스왑, OOM Killer, 그리고 “남는 메모리는 페이지 캐시로 쓰인다"는 개념을 잡았습니다. 이번 글은 그 개념들이 운영 장면에서 던지는 질문에 답합니다. 메모리가 정말 부족한지 어떻게 판정하는가, 갑자기 디스크가 폭주하는 쓰기는 어디서 오는가, 컨테이너는 왜 서버 메모리가 남는데도 죽는가입니다.

free에서 봐야 할 열은 하나입니다 #

기초 3편의 결론을 운영 동작으로 옮기면 이렇습니다. free 출력에서 판단 기준은 free 열이 아니라 available입니다.

free -h
$ free -h
       total   used   free   shared  buff/cache  available
Mem:    62Gi   18Gi  1.2Gi    0.5Gi        43Gi       42Gi

free 1.2GiB만 보면 위험해 보이지만, buff/cache 43GiB의 대부분은 페이지 캐시라서 애플리케이션이 달라면 즉시 비워 줍니다. 그걸 반영한 추정치가 available 42GiB이고, “지금 새 프로세스가 스왑 없이 쓸 수 있는 양"에 대한 커널의 답입니다. 모니터링 경보도 free가 아니라 available 기준으로 걸어야 페이지 캐시가 일하는 정상 상태를 장애로 오인하지 않습니다.

그럼 진짜 부족은 어떻게 드러날까요. 1편의 틀로 말하면 사용률(available 감소)보다 포화가 결정적입니다. 스왑 in/out이 지속적으로 발생하고(vmstat의 si/so), available이 계속 줄어드는 추세라면 부족이 맞습니다. 스왑이 조금 차 있는 것 자체는 문제가 아닙니다. 안 쓰는 페이지를 내려 둔 흔적일 수 있어서, 차 있는 양이 아니라 오가는 흐름을 봅니다.

더티 페이지 — 쓰기는 일단 메모리에 쌓입니다 #

애플리케이션이 파일에 쓰면, 그 내용은 곧장 디스크로 가지 않고 페이지 캐시에 더티 페이지(dirty page, 메모리에는 반영됐지만 아직 디스크에 안 쓰인 페이지)로 쌓입니다. 커널이 백그라운드에서 천천히 디스크로 내려보내는 구조라, 쓰기가 빠르게 느껴지는 대신 두 가지 운영 현상이 생깁니다.

  • 쓰기 폭주(burst flush) — 더티 페이지가 한도에 가까워지면 커널이 한꺼번에 디스크로 쏟아 냅니다. 평소 한가하던 디스크가 주기적으로 100%를 치고, 그 순간 다른 I/O의 지연이 튑니다. “몇 분마다 서비스가 잠깐 버벅인다"는 신고의 단골 원인입니다.
  • 유실 가능성 — 디스크에 내려가기 전에 전원이 나가면 그 더티 페이지는 사라집니다. 데이터베이스가 fsync를 고집하는 이유이고, 6편에서 배터리 캐시와 함께 다시 만납니다.

폭주가 문제라면 커널의 더티 비율 설정(vm.dirty_ratio, vm.dirty_background_ratio)으로 “조금씩 자주” 내려보내게 조절할 수 있습니다. 다만 9편에서 정리할 원칙대로, 이런 손잡이는 측정으로 원인을 확정한 뒤에 만집니다.

swappiness — 스왑의 성향 조절 #

vm.swappiness는 “스왑을 켤지 끌지"가 아니라 메모리가 부족해질 때 무엇을 먼저 내보낼지의 성향입니다. 커널의 선택지는 둘입니다. 페이지 캐시를 줄이거나, 안 쓰는 익명 페이지(프로세스 메모리)를 스왑으로 내리거나입니다. 값이 높으면(기본 60) 스왑도 적극적으로 쓰고, 낮으면 페이지 캐시를 먼저 줄입니다.

데이터베이스 서버에서 swappiness를 낮추는(예: 10) 관례는 이 동작에서 나옵니다. DB 프로세스의 메모리가 스왑으로 내려가면 쿼리 지연이 디스크 속도로 떨어지므로, 차라리 페이지 캐시를 양보하는 쪽을 선호하는 것입니다. 반대로 일반 서버에서 0에 가깝게 만드는 것은 권하지 않습니다. 스왑이라는 완충이 사라지면 부족 상황이 곧장 OOM으로 직행합니다.

OOM Killer의 대상 선정에도 손잡이가 있습니다. 프로세스별 oom_score_adj를 낮추면(예: -500) 중요한 프로세스가 마지막 후보가 됩니다. “메모리가 터질 때 DB만은 살리고 싶다"는 요구의 표준 답입니다.

컨테이너의 메모리 — 한도는 서버가 아니라 cgroup #

컨테이너 시대에 가장 흔한 메모리 사건은 서버가 아니라 컨테이너 단위에서 납니다. 컨테이너의 메모리 한도는 cgroup(control group, 프로세스 묶음별로 자원 사용량을 제한하는 리눅스 기능)으로 걸리고, 한도를 넘으면 서버 전체에 메모리가 남아 있어도 그 컨테이너의 프로세스가 OOM으로 죽습니다. 쿠버네티스에서 만나는 OOMKilled가 이것입니다.

운영에서 헷갈리는 지점 두 가지를 짚어 두겠습니다.

  • 컨테이너 안의 free는 호스트 값을 보여 줍니다. 컨테이너 안에서 free를 치면 호스트 전체 메모리가 보여서, 애플리케이션이 자기 한도를 오판하기 쉽습니다. 한도는 cgroup 파일(memory.max)이나 오케스트레이터의 설정에서 확인해야 합니다.
  • 페이지 캐시도 컨테이너의 사용량에 계산됩니다. 파일 I/O가 많은 컨테이너는 애플리케이션 메모리가 작아도 캐시 때문에 한도에 다가갈 수 있습니다. 한도에 닿으면 커널이 그 컨테이너의 캐시를 먼저 회수하므로 보통은 죽지 않지만, 사용량 그래프가 한도에 붙어 보이는 이유는 됩니다.

처방의 방향도 서버와 다릅니다. 서버 메모리 부족은 증설이지만, 컨테이너 OOMKilled는 대부분 한도 설정과 애플리케이션의 실제 사용량(힙 설정 등)을 맞추는 일입니다.

자주 만나는 함정 #

  • free 열을 보고 증설을 결정한다 — 페이지 캐시를 부족으로 오인한 과잉 투자입니다. available과 스왑 흐름으로 판정합니다.
  • 스왑 사용량이 있다고 장애로 본다 — 차 있는 스왑은 흔적, 오가는 스왑이 증상입니다. si/so의 지속 발생 여부를 봅니다.
  • 컨테이너 OOM을 서버 메모리 문제로 판단한다 — 호스트에 메모리가 남아도 cgroup 한도 초과면 죽습니다. 죽은 단위가 컨테이너라면 먼저 한도와 실사용량을 비교합니다.

정리 #

이번 글에서 잡은 그림입니다.

  • 메모리 여유의 판단 기준은 free가 아니라 available이고, 부족의 증상은 스왑의 지속적인 in/out입니다.
  • 쓰기는 더티 페이지로 메모리에 쌓였다가 한꺼번에 내려가며 주기적 디스크 폭주를 만들 수 있습니다.
  • swappiness는 부족 시 무엇을 먼저 내보낼지의 성향이고, oom_score_adj로 OOM의 우선순위를 조절합니다.
  • 컨테이너의 메모리 사건은 cgroup 한도에서 납니다. 서버가 아니라 컨테이너의 한도와 실사용량을 봅니다.

다음 — NUMA #

다음 글인 “하드웨어 중급 #4 NUMA"에서는 메모리 이야기를 한 층 더 내려갑니다. CPU 소켓이 두 개를 넘는 서버에서 메모리는 균일하지 않은데, 어느 코어가 어느 메모리에 접근하느냐로 성능이 갈리는 구조와, 그것이 데이터베이스와 가상화에 미치는 영향을 다루겠습니다.

X