website: 백엔드 컨테이너 메모리 상한 + JVM 힙 캡 추가
RAM 위험 점검을 하다가, 지금 운영 중인 docker-compose에 컨테이너별 메모리 제한이 전혀 없다는 걸 확인했다.
호스트는 12GB RAM에 스왑도 없는 구성인데, backend-stage와 backend-prod 두 JVM이 각자 서로를 모르는 채로 떠 있었다. JVM 기본 힙 정책이 호스트 RAM의 25%까지 쓰는 거라, 최악의 경우 두 컨테이너가 각각 3GB씩 힙을 잡아먹을 수 있는 상황이었다. 컨테이너 레벨에서 아무 제한이 없으니 한쪽이 커지기 시작하면 다른 서비스나 호스트 전체가 말려들 위험이 있었고, nginx나 sqlite-web처럼 딱히 무겁지 않은 서비스도 사실상 무제한으로 열려 있었다.
먼저 컨테이너 자체에 mem_limit으로 cgroup 상한을 걸었다. 배분은 backend-prod 6g, backend-stage 4g, nginx·sqlite-web-stage·sqlite-web-prod 각각 256m씩. 다 더하면 10.75g고, 나머지 1.25g 정도는 OS·cron·venv 같은 백그라운드 프로세스 몫으로 남겨뒀다. 이렇게 상한을 걸어두면 어느 컨테이너가 한도를 넘어도 그 컨테이너만 OOM killer에 죽고 restart: unless-stopped로 알아서 재시작되는데, 호스트 전체 OOM으로 번져서 nginx나 sshd까지 말려드는 것보다는 훨씬 낫다는 판단이었다.
그런데 컨테이너 상한만 걸어두면 반쪽짜리라는 게 걸렸다. JVM은 컨테이너 cgroup 제한을 인식하긴 하지만 여전히 그 상한의 25%를 힙으로 잡는 게 기본값이라, mem_limit: 4g를 걸어놔도 힙은 1g 정도만 쓰고 나머지 3g는 그냥 낭비되는 셈이었다. 그래서 JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=70.0을 backend-stage/prod 양쪽에 추가해서, 컨테이너 상한 대비 70%까지 힙으로 쓰도록 맞췄다. 나머지 30%는 metaspace, 스레드 스택, direct buffer 같은 힙 밖 오버헤드 몫으로 남겨둔 거다. 결과적으로 힙 절대치 자체는 예전 기본값(호스트 25%)보다 커질 수 있지만, 이제는 컨테이너 상한이라는 벽 안에서만 커지니까 안전하다.
각 서비스 블록 위에 왜 이 숫자로 배분했는지, mem_limit 도입 배경이 뭔지 주석으로 남겨뒀다. 나중에 실측치를 보고 조정할 걸 염두에 두고 지금 값은 잠정치라고 명시해뒀다 — 실제로 얼마나 쓰는지 로그를 좀 더 쌓아본 다음에 배분을 다시 손볼 생각이다.