어느 날 운영 DB(PostgreSQL)가 응답을 멈췄었다. 프로세스를 확인해보니 postgres가 그냥 사라져 있었음.. 범인은 리눅스 커널이었다.
서버 RAM이 거의 다 찼을 때, 커널이 메모리를 가장 많이 쓰는 프로세스를 골라 강제 종료했는데 — 하필 그게 postgres였던;
이것을 해결했던 게 26년 3월 16일. 이 때 dev랑 stg에도 같이 반영을 해뒀으면 좋았을텐데 (이때 stg가 있었나?), 기록도 제대로 안해두고 반영도 안해둬서 26년 6월 15일에 다시 작업하게 된 내용 기록 시작.
리눅스는 메모리를 오버커밋(overcommit)한다. 프로세스가 “이만큼 쓸게"라고 요청하면 실제 물리 메모리보다 많이 약속해준다. 대부분은 실제로 다 쓰지 않으니 문제가 없지만, 정말로 메모리가 바닥나는 순간이 오면 커널은 선택을 강요받는다.
이때 발동하는 게 OOM(Out Of Memory) Killer다. 커널은 각 프로세스에 “badness score”(oom_score)를 매기고, 점수가 가장 높은 프로세스를 죽여서 메모리를 회수한다. 점수는 대체로 메모리를 많이 쓰는 프로세스일수록 높다.
DB가 탑재된 서버에서 메모리를 가장 많이 쓰는 프로세스는?? 당연히 DB. 그래서 메모리 압박 상황에서 postgres가 1순위 희생양이 된 것.
# dmesg에 이런 흔적이 남는다
Out of memory: Killed process 1234 (postgres) ...
우리 인스턴스는 RAM이 약 1.9GB인 작은 서버였다. 여기에:
여유 버퍼가 없어 곧장 한계에 부딪힌다. 그리고 스왑(swap)이 없으면 커널은 디스크로 잠시 내려놓을 곳도 없어져서, 곧바로 OOM Killer를 호출한다.
스왑은 “메모리가 부족할 때 디스크를 임시 메모리로 빌려 쓰는 안전망”이다.
즉 스왑은 근본 해결책까지는 아닌데,, “갑자기 터져서 프로세스가 죽는 사고”를 막아주는 완충 장치다. (스왑을 쓰기 시작하면 느려지지만, 죽는 것보다는 느린 게 낫다,,)
참고: PostgreSQL은 패키지 기본 설정으로 마스터 프로세스에
oom_score_adj = -900을 줘서 되도록 자기를 죽이지 않도록 커널에 신호를 보낸다. 하지만 이는 우선순위를 낮출 뿐, 정말 메모리가 없으면 결국 죽음 ㅠ. 스왑이라는 물리적 여유가 함께 있어야 한다.
3월 16일의 사건 이후 운영(prod)에는 조치를 해뒀는데, 개발(dev)・릴리즈(release) 환경에는 빠져 있었다. 세 서버를 같은 기준으로 점검했다.
| 항목 | prod | dev | release |
|---|---|---|---|
| RAM | 1.9GB | 1.9GB | 1.9GB |
| 스왑 활성 | ✅ 2GB | ⚠️ 0B (파일만 존재) | ❌ 없음 |
| fstab 영구등록 | ✅ | ❌ | ❌ |
postgres oom_score_adj | -900 | -900 | -900 |
확인에 쓴 명령들:
free -h # Swap 줄이 0B인지 확인
swapon --show # 활성 스왑 목록
grep swap /etc/fstab # 재부팅 후에도 유지되는지
/swapfile이 만들어져 있긴 한데, swapon을 안 해서 실제로는 비활성,, fstab에도 없어 재부팅하면 사라지는 상태. → 반만 적용oom_score_adj = -900은 세 서버 모두 동일했는데, 이건 앞서 말한 패키지 기본값이라 누가 따로 한 조치는 아니었다. 실제 prod의 방어책은 “스왑” 하나로 좁혀짐.
dev (파일은 있으니 활성화만):
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 재부팅 후에도 유지
release (처음부터):
sudo fallocate -l 2G /swapfile # 안 되면: sudo dd if=/dev/zero of=/swapfile bs=1M count=2048
sudo chmod 600 /swapfile # 보안상 600 필수
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
포인트:
swapon만 하면 지금 세션에만 적용된다. fstab에 등록해야 재부팅 후에도 자동으로 올라온다. 이 두 단계를 모두 해야 “영구 적용”됨.
swapon --show && free -h
세 서버 모두 Swap: 2.0Gi가 잡히고 fstab에 한 줄씩(중복 없이) 등록된 걸 확인했다. 이제 dev・release도 prod와 동일한 안전망을 갖추게 된.
스왑은 또 안 죽게 막아주는 응급처치고, 근본 원인은 따로 있다.
세 서버(dev・prod・stg) 전부 구성이 똑같다:
| 항목 | prod | dev | release |
|---|---|---|---|
| RAM / Swap | 1.9GB / 2GB | 1.9GB / 2GB | 1.9GB / 2GB |
| DB | postgres(호스트) | 동일 | 동일 |
| 앱 스택 | frontend・backend・nginx・redis (Docker) | 동일 | 동일 |
컨테이너 mem_limit | 0 (무제한) | 0 | 0 |
| redis maxmemory | 0 / noeviction | 0 / noeviction | 0 / noeviction |
그러니까 이번 OOM 사고는 “postgres가 메모리를 많이 써서”가 아니라, 6개 가까운 프로세스가 1.9GB를 두고 칸막이 없이 경쟁하다가, 터지는 순간 제일 큰 postgres가 총 맞은 거,,,, 흑흑.
우리는 앱을 다 Docker로 띄고 있었다. 근데 컨테이너 메모리 limit이 전부 0(무제한)이다.
컨테이너는 호스트 커널・호스트 RAM을 그대로 공유한다. limit이 없으면 컨테이너 안 프로세스도 그냥 호스트 위 프로세스랑 똑같이 1.9GB를 두고 경쟁한다. 즉 limit 없는 Docker는 격리 효과가 사실상 0. redis 컨테이너가 부풀면 → 호스트 OOM Killer 발동 → 컨테이너 밖 postgres가 죽는다. Docker를 쓰는 의미가 사실상 조금 떨어지는,,
Docker의 가치는 “컨테이너로 띄웠다”가 아니라
mem_limit으로 터뜨린 놈만 죽게 격리하는 데 있는데, limit이 0이면 베어메탈이랑 위험도가 다르지 않다.
redis는 인메모리라 RAM을 직접 먹는데, 우리 설정은 maxmemory 0(무제한) + noeviction이다.
maxmemory 0 → 캐시가 무한정 자랄 수 있음. 한도가 없으니 RAM을 끝까지 먹을 수 있다.noeviction → 꽉 차도 오래된 키를 안 비우고 그냥 쓰기를 거부(에러)한다. 즉 메모리도 안 줄고 장애도 나는 좋지 않은 조합…DB랑 한 박스에서 이러고 있으면, redis가 슬금슬금 부푸는 것만으로 DB를 밀어낼 수 있다.
postgres는 호스트에서 oom_score_adj = -900으로 되도록 자기를 죽이지 말라는 신호를 켜두고 있었다(패키지 기본값). 근데도 죽은 이유는,,
결국 소프트한 우선순위(-900)만 믿고, 물리적 여유(스왑)도 칸막이(limit)도 없던 게 이번 사고의 본질.
redis maxmemory + eviction 정책 필요함 — 예: maxmemory 200mb, maxmemory-policy allkeys-lru. 캐시가 RAM을 끝까지 먹는 걸 원천 차단.
컨테이너 mem_limit 설정 — 이미 Docker를 쓰고 있으니 compose에 한 줄씩만 추가해도 된다. 이걸로 “터뜨린 컨테이너만 죽고 postgres는 살게” 할 수 있음.
services:
redis: { mem_limit: 256m, command: redis-server --maxmemory 200mb --maxmemory-policy allkeys-lru }
backend: { mem_limit: 500m }
frontend:{ mem_limit: 400m }
nginx: { mem_limit: 128m }
# 단, limit 총합 + postgres 몫이 (RAM + swap) 안에 들어오게 설계할 것
vm.swappiness 낮추기 (60→10) — 평소엔 RAM 우선, 스왑은 진짜 위급할 때만.
PostgreSQL 파라미터 점검 — shared_buffers・work_mem가 1.9GB 대비 과하지 않은지.
mem_limit 없는 컨테이너는 베어메탈과 위험도가 다르지 않음,, 우리는 Docker를 쓰면서 이점을 잘 못살리고 있긴 하다 ㅠswapon과 fstab은 별개, 둘 다 해야 영구 적용.